diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 33ba401..bda05c1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -60,13 +60,13 @@ way than just downloading it. Here are the basic instructions: [README](/README.md#how-to-install). ```bash - go get github.com/nttcom/eclcloud/v2 + go get github.com/nttcom/eclcloud/v3 ``` 2. Move into the directory that houses your local repository: ```bash - cd ${GOPATH}/src/github.com/nttcom/eclcloud/v2 + cd ${GOPATH}/src/github.com/nttcom/eclcloud/v3 ``` 3. Fork the `nttcom/eclcloud` repository and update your remote refs. You @@ -130,7 +130,7 @@ process of testing expectations with assertions: import ( "testing" - "github.com/nttcom/eclcloud/v2/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper" ) func TestSomething(t *testing.T) { @@ -156,9 +156,9 @@ Here is a truncated example of mocked HTTP responses: import ( "testing" - th "github.com/nttcom/eclcloud/v2/testhelper" - fake "github.com/nttcom/eclcloud/v2/testhelper/client" - "github.com/nttcom/eclcloud/v2/ecl/network/v2/networks" + th "github.com/nttcom/eclcloud/v3/testhelper" + fake "github.com/nttcom/eclcloud/v3/testhelper/client" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/networks" ) func TestGet(t *testing.T) { diff --git a/Makefile b/Makefile index 6e8e5ec..0705b24 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,9 @@ fmtcheck: (! gofmt -s -d . | grep '^') vet: - go vet ./... && cd v2 && go vet ./... + go vet ./... && cd v2 && go vet ./... && cd ../v3 && go vet ./... && cd ../v4 && go vet ./... test: - go test ./... -count=1 && cd v2 && go test ./... -count=1 + go test ./... -count=1 && cd v2 && go test ./... -count=1 && cd ../v3 && go test ./... -count=1 && cd ../v4 && go test ./... -count=1 .PHONY: fmt fmtcheck vet test diff --git a/README.md b/README.md index 011c989..bfe4111 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ explicitly, or tell eclcloud to use environment variables: ```go import ( - "github.com/nttcom/eclcloud/v2" - "github.com/nttcom/eclcloud/v2" - "github.com/nttcom/eclcloud/v2/utils" + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/utils" ) // Option 1: Pass in the values yourself @@ -82,7 +82,7 @@ in the flavor ID (hardware specification) and image ID (operating system) we're interested in: ```go -import "github.com/nttcom/eclcloud/v2/compute/v2/servers" +import "github.com/nttcom/eclcloud/v3/compute/v2/servers" server, err := servers.Create(client, servers.CreateOpts{ Name: "My new server!", diff --git a/go.mod b/go.mod index c295665..fbc51f7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/nttcom/eclcloud -go 1.13 +go 1.17 diff --git a/v2/go.mod b/v2/go.mod index 549415c..3f1e45a 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -1,3 +1,3 @@ module github.com/nttcom/eclcloud/v2 -go 1.13 +go 1.17 diff --git a/v3/auth_options.go b/v3/auth_options.go new file mode 100644 index 0000000..9f442bf --- /dev/null +++ b/v3/auth_options.go @@ -0,0 +1,418 @@ +package eclcloud + +/* +AuthOptions stores information needed to authenticate to an Enterprise Cloud. +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. + +An example of manually providing authentication information: + + opts := eclcloud.AuthOptions{ + IdentityEndpoint: "https://keystone-{your_region}-ecl.api.ntt.com/v3/", + Username: "{api key}", + Password: "{api secret key}", + TenantID: "{tenant id}", + } + + provider, err := ecl.AuthenticatedClient(opts) + +An example of using AuthOptionsFromEnv(), where the environment variables can +be read from a file, such as a standard openrc file: + + opts, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(opts) +*/ +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. + // + // The IdentityEndpoint is typically referred to as the "auth_url" or + // "OS_AUTH_URL" in the information provided by the cloud operator. + IdentityEndpoint string `json:"-"` + + // 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 string `json:"username,omitempty"` + UserID string `json:"-"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // The same fields are known as project_id and project_name in the Identity + // V3 API, but are collected as TenantID and TenantName here in both cases. + // 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. + // If DomainID or DomainName are provided, they will also apply to TenantName. + // It is not currently possible to authenticate with Username and a Domain + // and scope to a Project in a different Domain by using TenantName. To + // accomplish that, the ProjectID will need to be provided as the TenantID + // option. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // AllowReauth should be set to true if you grant permission for Eclcloud to + // cache your credentials in memory, and to allow Eclcloud 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. + // + // NOTE: The reauth function will try to re-authenticate endlessly if left + // unchecked. The way to limit the number of attempts is to provide a custom + // HTTP client to the provider client and provide a transport that implements + // the RoundTripper interface and stores the number of failed retries. For an + // example of this, see here: + // https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Scope determines the scoping of the authentication request. + Scope *AuthScope `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` +} + +// AuthScope allows a created token to be limited to a specific domain or project. +type AuthScope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + // Populate the request map. + authMap := make(map[string]interface{}) + + if opts.Username != "" { + if opts.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": opts.Username, + "password": opts.Password, + } + } else { + return nil, ErrMissingInput{Argument: "Password"} + } + } else if opts.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": opts.TokenID, + } + } else { + return nil, ErrMissingInput{Argument: "Username"} + } + + if opts.TenantID != "" { + authMap["tenantId"] = opts.TenantID + } + if opts.TenantName != "" { + authMap["tenantName"] = opts.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + 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,omitempty"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type applicationCredentialReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + User *userReq `json:"user,omitempty"` + Secret *string `json:"secret,omitempty"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + 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 + var userRequest userReq + + if opts.Password == "" { + if opts.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if opts.Username != "" { + return nil, ErrUsernameWithToken{} + } + if opts.UserID != "" { + return nil, ErrUserIDWithToken{} + } + if opts.DomainID != "" { + return nil, ErrDomainIDWithToken{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithToken{} + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: opts.TokenID, + } + + } else if opts.ApplicationCredentialID != "" { + // Configure the request for ApplicationCredentialID authentication. + // There are three kinds of possible application_credential requests + // 1. application_credential id + secret + // 2. application_credential name + secret + user_id + // 3. application_credential name + secret + username + domain_id / domain_name + if opts.ApplicationCredentialSecret == "" { + return nil, ErrAppCredMissingSecret{} + } + req.Auth.Identity.Methods = []string{"application_credential"} + req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ + ID: &opts.ApplicationCredentialID, + Secret: &opts.ApplicationCredentialSecret, + } + } else if opts.ApplicationCredentialName != "" { + if opts.ApplicationCredentialSecret == "" { + return nil, ErrAppCredMissingSecret{} + } + // make sure that only one of DomainName or DomainID were provided + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + req.Auth.Identity.Methods = []string{"application_credential"} + if opts.DomainID != "" { + userRequest = userReq{ + Name: &opts.Username, + Domain: &domainReq{ID: &opts.DomainID}, + } + } else if opts.DomainName != "" { + userRequest = userReq{ + Name: &opts.Username, + Domain: &domainReq{Name: &opts.DomainName}, + } + } + req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ + Name: &opts.ApplicationCredentialName, + User: &userRequest, + Secret: &opts.ApplicationCredentialSecret, + } + } else { + // If no password or token ID or ApplicationCredential are available, authentication can't continue. + return nil, ErrMissingPassword{} + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if opts.Username == "" && opts.UserID == "" { + return nil, ErrUsernameOrUserID{} + } + + if opts.Username != "" { + // If Username is provided, UserID may not be provided. + if opts.UserID != "" { + return nil, ErrUsernameOrUserID{} + } + + // Either DomainID or DomainName must also be specified. + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + + if opts.DomainID != "" { + if opts.DomainName != "" { + return nil, ErrDomainIDOrDomainName{} + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + + if opts.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + } + + if opts.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if opts.DomainID != "" { + return nil, ErrDomainIDWithUserID{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithUserID{} + } + + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &opts.UserID, Password: opts.Password}, + } + } + } + + b, err := BuildRequestBody(req, "") + if err != nil { + return nil, err + } + + if len(scope) != 0 { + b["auth"].(map[string]interface{})["scope"] = scope + } + + return b, nil +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + // For backwards compatibility. + // If AuthOptions.Scope was not set, try to determine it. + // This works well for common scenarios. + if opts.Scope == nil { + opts.Scope = new(AuthScope) + if opts.TenantID != "" { + opts.Scope.ProjectID = opts.TenantID + } else { + if opts.TenantName != "" { + opts.Scope.ProjectName = opts.TenantName + opts.Scope.DomainID = opts.DomainID + opts.Scope.DomainName = opts.DomainName + } + } + } + + if opts.Scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + if opts.Scope.ProjectID != "" { + return nil, ErrScopeProjectIDOrProjectName{} + } + + if opts.Scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"id": &opts.Scope.DomainID}, + }, + }, nil + } + + if opts.Scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"name": &opts.Scope.DomainName}, + }, + }, nil + } + } else if opts.Scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if opts.Scope.DomainID != "" { + return nil, ErrScopeProjectIDAlone{} + } + if opts.Scope.DomainName != "" { + return nil, ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &opts.Scope.ProjectID, + }, + }, nil + } else if opts.Scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if opts.Scope.DomainName != "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &opts.Scope.DomainID, + }, + }, nil + } else if opts.Scope.DomainName != "" { + // DomainName + return map[string]interface{}{ + "domain": map[string]interface{}{ + "name": &opts.Scope.DomainName, + }, + }, nil + } + + return nil, nil +} + +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} diff --git a/v3/doc.go b/v3/doc.go new file mode 100644 index 0000000..58918f9 --- /dev/null +++ b/v3/doc.go @@ -0,0 +1,93 @@ +/* +Package eclcloud provides interface to Enterprise Cloud. +The library has a three-level hierarchy: providers, services, and +resources. + +Authenticating with Providers + +Provider structs represent the cloud providers that offer and manage a +collection of services. You will generally want to create one Provider +client per Enterprise Cloud. + +Use your Enterprise Cloud credentials to create a Provider client. The +IdentityEndpoint is typically referred to as "auth_url" or "OS_AUTH_URL" in +information provided by the cloud operator. Additionally, the cloud may refer to +TenantID or TenantName as project_id and project_name. Credentials are +specified like so: + + opts := eclcloud.AuthOptions{ + IdentityEndpoint: "https://keystone-{region}-ecl.api.ntt.com/v3/", + Username: "{api key}", + Password: "{api secret key}", + TenantID: "{tenant_id}", + } + + provider, err := ecl.AuthenticatedClient(opts) + +You may also use the ecl.AuthOptionsFromEnv() helper function. This +function reads in standard environment variables frequently found in an +Enterprise Cloud `openrc` file. Again note that Gophercloud currently uses "tenant" +instead of "project". + + opts, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(opts) + +Service Clients + +Service structs are specific to a provider and handle all of the logic and +operations for a particular Enterprise Cloud 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 := eclcloud.EndpointOpts{Region: "RegionOne"} + + client := ecl.NewComputeV2(provider, opts) + +Resources + +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 + }) + +If you want to obtain the entire collection of pages without doing any +intermediary processing on each page, you can use the AllPages method: + + allPages, err := servers.List(client, nil).AllPages() + allServers, err := servers.ExtractServers(allPages) + +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 eclcloud diff --git a/v3/ecl/auth_env.go b/v3/ecl/auth_env.go new file mode 100644 index 0000000..c2a7faa --- /dev/null +++ b/v3/ecl/auth_env.go @@ -0,0 +1,97 @@ +package ecl + +import ( + "github.com/nttcom/eclcloud/v3" + "os" +) + +var nilOptions = eclcloud.AuthOptions{} + +/* +AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +settings found on the various Enterprise Cloud 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, OS_TENANT_NAME, OS_PROJECT_ID, and +OS_PROJECT_NAME are optional. + +OS_TENANT_ID and OS_TENANT_NAME are mutually exclusive to OS_PROJECT_ID and +OS_PROJECT_NAME. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will +still be referred as "tenant" in eclcloud. + +To use this function, first set the OS_* environment variables (for example, +by sourcing an `openrc` file), then: + + opts, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(opts) +*/ +func AuthOptionsFromEnv() (eclcloud.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") + applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID") + applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME") + applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET") + + // If OS_PROJECT_ID is set, overwrite tenantID with the value. + if v := os.Getenv("OS_PROJECT_ID"); v != "" { + tenantID = v + } + + // If OS_PROJECT_NAME is set, overwrite tenantName with the value. + if v := os.Getenv("OS_PROJECT_NAME"); v != "" { + tenantName = v + } + + if authURL == "" { + err := eclcloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_AUTH_URL", + } + return nilOptions, err + } + + if username == "" && userID == "" { + err := eclcloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERNAME", "OS_USERID"}, + } + return nilOptions, err + } + + if password == "" && applicationCredentialID == "" && applicationCredentialName == "" { + err := eclcloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_PASSWORD", + } + return nilOptions, err + } + + if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" { + err := eclcloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET", + } + return nilOptions, err + } + + ao := eclcloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + ApplicationCredentialID: applicationCredentialID, + ApplicationCredentialName: applicationCredentialName, + ApplicationCredentialSecret: applicationCredentialSecret, + } + + return ao, nil +} diff --git a/v3/ecl/baremetal/v2/availabilityzones/doc.go b/v3/ecl/baremetal/v2/availabilityzones/doc.go new file mode 100644 index 0000000..6619157 --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/doc.go @@ -0,0 +1,22 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(client).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } +*/ +package availabilityzones diff --git a/v3/ecl/baremetal/v2/availabilityzones/requests.go b/v3/ecl/baremetal/v2/availabilityzones/requests.go new file mode 100644 index 0000000..247bbd2 --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/requests.go @@ -0,0 +1,13 @@ +package availabilityzones + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// List will return the existing availability zones. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/v3/ecl/baremetal/v2/availabilityzones/results.go b/v3/ecl/baremetal/v2/availabilityzones/results.go new file mode 100644 index 0000000..5f6ca32 --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/results.go @@ -0,0 +1,37 @@ +package availabilityzones + +import ( + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an ECL +// AvailabilityZone. +type AvailabilityZone struct { + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` + Hosts interface{} `json:"hosts"` +} + +// AvailabilityZonePage stores a single page of all AvailabilityZone results +// from a List call. +// Use the ExtractAvailabilityZones function to convert the results to a slice of +// AvailabilityZones. +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/v3/ecl/baremetal/v2/availabilityzones/testing/doc.go b/v3/ecl/baremetal/v2/availabilityzones/testing/doc.go new file mode 100644 index 0000000..59fc76c --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains baremetal availability zone unit tests +package testing diff --git a/v3/ecl/baremetal/v2/availabilityzones/testing/fixtures.go b/v3/ecl/baremetal/v2/availabilityzones/testing/fixtures.go new file mode 100644 index 0000000..0677ebd --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/testing/fixtures.go @@ -0,0 +1,36 @@ +package testing + +import ( + az "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/availabilityzones" +) + +const getResponse = ` +{ + "availabilityZoneInfo": [{ + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupa" + }, { + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupb" + }] +} +` + +var azResult = []az.AvailabilityZone{ + { + Hosts: nil, + ZoneName: "zone1-groupa", + ZoneState: az.ZoneState{Available: true}, + }, + { + Hosts: nil, + ZoneName: "zone1-groupb", + ZoneState: az.ZoneState{Available: true}, + }, +} diff --git a/v3/ecl/baremetal/v2/availabilityzones/testing/requests_test.go b/v3/ecl/baremetal/v2/availabilityzones/testing/requests_test.go new file mode 100644 index 0000000..df6e771 --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/testing/requests_test.go @@ -0,0 +1,33 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + az "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/availabilityzones" + th "github.com/nttcom/eclcloud/v3/testhelper" + + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListAvailabilityZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + allPages, err := az.List(fakeclient.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := az.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, azResult, actual) +} diff --git a/v3/ecl/baremetal/v2/availabilityzones/urls.go b/v3/ecl/baremetal/v2/availabilityzones/urls.go new file mode 100644 index 0000000..e62fdd9 --- /dev/null +++ b/v3/ecl/baremetal/v2/availabilityzones/urls.go @@ -0,0 +1,7 @@ +package availabilityzones + +import "github.com/nttcom/eclcloud/v3" + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} diff --git a/v3/ecl/baremetal/v2/flavors/doc.go b/v3/ecl/baremetal/v2/flavors/doc.go new file mode 100644 index 0000000..23dbe2b --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/doc.go @@ -0,0 +1,25 @@ +/* +Package flavors contains functionality for working with +ECL Baremetal Server's flavor resources. + +Example to list flavors + + listOpts := flavors.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := flavors.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v", flavor) + } +*/ +package flavors diff --git a/v3/ecl/baremetal/v2/flavors/requests.go b/v3/ecl/baremetal/v2/flavors/requests.go new file mode 100644 index 0000000..640b476 --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/requests.go @@ -0,0 +1,88 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Get retrieves the flavor with the provided ID. +// To extract the Flavor object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// ListOpts holds options for listing flavors. +// It is passed to the flavors.List function. +type ListOpts struct { + // Now there are no definition as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Flavor optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.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 + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// IDFromName is a convenience function that returns a flavor's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &eclcloud.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &eclcloud.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/v3/ecl/baremetal/v2/flavors/results.go b/v3/ecl/baremetal/v2/flavors/results.go new file mode 100644 index 0000000..6ceda50 --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/results.go @@ -0,0 +1,79 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// Extract provides access to the individual Flavor returned by +// the Get and functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} diff --git a/v3/ecl/baremetal/v2/flavors/testing/doc.go b/v3/ecl/baremetal/v2/flavors/testing/doc.go new file mode 100644 index 0000000..159b3e8 --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains baremetal flavor unit tests +package testing diff --git a/v3/ecl/baremetal/v2/flavors/testing/fixtures.go b/v3/ecl/baremetal/v2/flavors/testing/fixtures.go new file mode 100644 index 0000000..90a766c --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/testing/fixtures.go @@ -0,0 +1,86 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/flavors" +) + +var listResponse = fmt.Sprintf(` +{ + "flavors": [ + { + "id": "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "name": "General Purpose 1", + "vcpus": 4, + "ram": 32768, + "disk": 550, + "links": [ + { + "href": "https://baremetal-server.ntt/v2/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "self" + }, + { + "href": "https://baremetal-server.ntt/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "bookmark" + } + ] + }, + { + "id": "303b4993-cf29-4301-abd0-99512b5413a5", + "name": "General Purpose 2", + "vcpus": 8, + "ram": 262144, + "disk": 3950, + "links": [ + { + "href": "https://baremetal-server.ntt/v2/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + "rel": "self" + }, + { + "href": "https://baremetal-server.ntt/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + "rel": "bookmark" + } + ] + } + ] +}`) + +var getResponse = fmt.Sprintf(` +{ + "flavor": { + "id": "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "links": [ + { + "href": "https://baremetal-server.ntt/v2/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "self" + }, + { + "href": "https://baremetal-server.ntt/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "bookmark" + } + ], + "name": "General Purpose 1", + "vcpus": 4, + "ram": 32768, + "disk": 550 + } +}`) + +var expectedFlavors = []flavors.Flavor{expectedFlavor1, expectedFlavor2} + +var expectedFlavor1 = flavors.Flavor{ + ID: "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + Name: "General Purpose 1", + Disk: 550, + RAM: 32768, + VCPUs: 4, +} + +var expectedFlavor2 = flavors.Flavor{ + ID: "303b4993-cf29-4301-abd0-99512b5413a5", + Name: "General Purpose 2", + Disk: 3950, + RAM: 262144, + VCPUs: 8, +} diff --git a/v3/ecl/baremetal/v2/flavors/testing/requests_test.go b/v3/ecl/baremetal/v2/flavors/testing/requests_test.go new file mode 100644 index 0000000..4d51e70 --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/testing/requests_test.go @@ -0,0 +1,76 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/flavors" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := flavors.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := flavors.ExtractFlavors(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedFlavors, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListFlavorsAllPages(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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := flavors.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + + allFlavors, err := flavors.ExtractFlavors(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allFlavors)) +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/flavors/%s", "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := flavors.Get(fakeclient.ServiceClient(), "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedFlavor1, actual) +} diff --git a/v3/ecl/baremetal/v2/flavors/urls.go b/v3/ecl/baremetal/v2/flavors/urls.go new file mode 100644 index 0000000..430a898 --- /dev/null +++ b/v3/ecl/baremetal/v2/flavors/urls.go @@ -0,0 +1,13 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} diff --git a/v3/ecl/baremetal/v2/keypairs/doc.go b/v3/ecl/baremetal/v2/keypairs/doc.go new file mode 100644 index 0000000..dd95992 --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/doc.go @@ -0,0 +1,52 @@ +/* +Package keypairs provides the ability to manage key pairs. + +Example to List Key Pairs + + allPages, err := keypairs.List(client).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to Create a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + } + + keypair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", keypair) + +Example to Import a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + PublicKey: "public-key", + } + + keypair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair + + err := keypairs.Delete(client, "keypair-name").ExtractErr() + if err != nil { + panic(err) + } +*/ +package keypairs diff --git a/v3/ecl/baremetal/v2/keypairs/requests.go b/v3/ecl/baremetal/v2/keypairs/requests.go new file mode 100644 index 0000000..2ce786c --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/requests.go @@ -0,0 +1,60 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies KeyPair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. + // If provided, this key will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new KeyPair on the server, or to import a +// pre-existing keypair. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *eclcloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), nil) + return +} diff --git a/v3/ecl/baremetal/v2/keypairs/results.go b/v3/ecl/baremetal/v2/keypairs/results.go new file mode 100644 index 0000000..d3d1804 --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/results.go @@ -0,0 +1,84 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// KeyPair is an SSH key known to the Enterprise Cloud 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 `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate + // or validate a longer public key. + Fingerprint string `json:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. + // "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"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 `json:"private_key"` + + // UserID is the user who owns this KeyPair. + UserID string `json:"user_id"` +} + +// KeyPairPage stores a single page of all KeyPair results from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + var s struct { + KeyPairs []KeyPair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + return s.KeyPairs, err +} + +type keyPairResult struct { + eclcloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response +// as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/baremetal/v2/keypairs/testing/doc.go b/v3/ecl/baremetal/v2/keypairs/testing/doc.go new file mode 100644 index 0000000..bf23f88 --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains keypairs unit tests +package testing diff --git a/v3/ecl/baremetal/v2/keypairs/testing/fixtures.go b/v3/ecl/baremetal/v2/keypairs/testing/fixtures.go new file mode 100644 index 0000000..2a59706 --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/testing/fixtures.go @@ -0,0 +1,92 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/keypairs" +) + +const listOutput = ` +{ + "keypairs": [ + { + "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" + }, + { + "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" + } + ] +} +` + +const createRequest = `{ "keypair": { "name": "createdkey" } }` +const createResponse = ` +{ + "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" + } +} +` + +const getResponse = ` +{ + "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" + } +} +` + +const importRequest = ` +{ + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } +}` +const importResponse = ` +{ + "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" + } +} +` + +var firstKeyPair = keypairs.KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +var secondKeyPair = keypairs.KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +var expectedKeyPairSlice = []keypairs.KeyPair{firstKeyPair, secondKeyPair} + +var createdKeyPair = keypairs.KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + 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", +} + +var importedKeyPair = keypairs.KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} diff --git a/v3/ecl/baremetal/v2/keypairs/testing/requests_test.go b/v3/ecl/baremetal/v2/keypairs/testing/requests_test.go new file mode 100644 index 0000000..ff3abaf --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/testing/requests_test.go @@ -0,0 +1,111 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/keypairs" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listOutput) + }) + + count := 0 + err := keypairs.List(fakeclient.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdKeyPair, actual) +} + +func TestImportKeypair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, importRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, importResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &importedKeyPair, actual) +} + +func TestGetKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := keypairs.Get(fakeclient.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &firstKeyPair, actual) +} + +func TestDeleteKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) + + err := keypairs.Delete(fakeclient.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v3/ecl/baremetal/v2/keypairs/urls.go b/v3/ecl/baremetal/v2/keypairs/urls.go new file mode 100644 index 0000000..2637342 --- /dev/null +++ b/v3/ecl/baremetal/v2/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/nttcom/eclcloud/v3" + +const resourcePath = "os-keypairs" + +func resourceURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *eclcloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *eclcloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/v3/ecl/baremetal/v2/servers/doc.go b/v3/ecl/baremetal/v2/servers/doc.go new file mode 100644 index 0000000..0ab1220 --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/doc.go @@ -0,0 +1,120 @@ +/* +Package servers contains functionality for working with +ECL Baremetal Server resources. + +Example to create server + + createOpts := servers.CreateOpts{ + Name: "server-test-1", + Networks: []servers.CreateOptsNetwork{ + { + UUID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + FixedIP: "10.0.0.100", + }, + }, + AdminPass: "aabbccddeeff", + ImageRef: "b5660a6e-4b46-4be3-9707-6b47221b454f", + FlavorRef: "05184ba3-00ba-4fbc-b7a2-03b62b884931", + AvailabilityZone: "zone1-groupa", + UserData: "IyEvYmluL2Jhc2gKZWNobyAiS3VtYSBQb3N0IEluc3RhbGwgU2NyaXB0IiA+PiAvaG9tZS9iaWcvcG9zdC1pbnN0YWxsLXNjcmlwdA==", + RaidArrays: []servers.CreateOptsRaidArray{ + { + PrimaryStorage: true, + Partitions: []map[string]interface{}{ + { + "lvm": true, + "partition_label": "primary-part1", + }, + { + "lvm": false, + "size": "100G", + "partition_label": "var", + }, + }, + }, + { + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + "disk4_uuid", + }, + Partitions: []map[string]interface{}{ + { + "lvm": true, + "partition_label": "secondary-part1", + }, + }, + }, + }, + LVMVolumeGroups: []servers.CreateOptsLVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []map[string]string{ + { + "size": "300G", + "lv_label": "LV_root", + }, + { + "size": "2G", + "lv_label": "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.CreateOptsFilesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + } + server, err := servers.Create(client, createOpts).Extract() + +Example to list servers + + listOpts := servers.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := servers.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v", server) + } + +Example to delete server + + err = servers.Delete(client, "server-id"").ExtractErr() + if err != nil { + panic(err) + } + +*/ +package servers diff --git a/v3/ecl/baremetal/v2/servers/requests.go b/v3/ecl/baremetal/v2/servers/requests.go new file mode 100644 index 0000000..35fd45c --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/requests.go @@ -0,0 +1,186 @@ +package servers + +import ( + "encoding/base64" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Get retrieves the server with the provided ID. +// To extract the Server object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts holds options for listing servers. +// It is passed to the servers.List function. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Server optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// CreateOptsNetwork represents networks information in server creation. +type CreateOptsNetwork struct { + UUID string `json:"uuid,omitempty"` + Port string `json:"port,omitempty"` + FixedIP string `json:"fixed_ip,omitempty"` + Plane string `json:"plane,omitempty"` +} + +// CreateOptsRaidArray represents raid configuration for the server resource. +type CreateOptsRaidArray struct { + PrimaryStorage bool `json:"primary_storage,omitempty"` + RaidCardHardwareID string `json:"raid_card_hardware_id,omitempty"` + DiskHardwareIDs []string `json:"disk_hardware_ids,omitempty"` + RaidLevel int `json:"raid_level,omitempty"` + Partitions []CreateOptsPartition `json:"partitions,omitempty"` +} + +// CreateOptsPartition represents partition configuration for the server resource. +type CreateOptsPartition struct { + LVM bool `json:"lvm,omitempty"` + Size string `json:"size,omitempty"` + PartitionLabel string `json:"partition_label,omitempty"` +} + +// CreateOptsLVMVolumeGroup represents LVM volume group configuration for the server resource. +type CreateOptsLVMVolumeGroup struct { + VGLabel string `json:"vg_label,omitempty"` + PhysicalVolumePartitionLabels []string `json:"physical_volume_partition_labels,omitempty"` + LogicalVolumes []CreateOptsLogicalVolume `json:"logical_volumes,omitempty"` +} + +// CreateOptsLogicalVolume represents logical volume configuration for the server resource. +type CreateOptsLogicalVolume struct { + LVLabel string `json:"lv_label,omitempty"` + Size string `json:"size,omitempty"` +} + +// CreateOptsFilesystem represents file system configuration for the server resource. +type CreateOptsFilesystem struct { + Label string `json:"label,omitempty"` + FSType string `json:"fs_type,omitempty"` + MountPoint string `json:"mount_point,omitempty"` +} + +// CreateOptsPersonality represents personal files configuration for the server resource. +type CreateOptsPersonality struct { + Path string `json:"path,omitempty"` + Contents string `json:"contents,omitempty"` +} + +// CreateOpts represents options used to create a server. +type CreateOpts struct { + Name string `json:"name" required:"true"` + Networks []CreateOptsNetwork `json:"networks" required:"true"` + AdminPass string `json:"adminPass,omitempty"` + ImageRef string `json:"imageRef,omitempty"` + FlavorRef string `json:"flavorRef" required:"true"` + AvailabilityZone string `json:"availability_zone,omitempty"` + KeyName string `json:"key_name,omitempty"` + UserData []byte `json:"-"` + RaidArrays []CreateOptsRaidArray `json:"raid_arrays,omitempty"` + LVMVolumeGroups []CreateOptsLVMVolumeGroup `json:"lvm_volume_groups,omitempty"` + Filesystems []CreateOptsFilesystem `json:"filesystems,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Personality []CreateOptsPersonality `json:"personality,omitempty"` +} + +// ToServerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + return map[string]interface{}{"server": b}, nil +} + +// Create accepts a CreateOpts struct and creates a new server +// using the values provided. +// This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/v3/ecl/baremetal/v2/servers/result.go b/v3/ecl/baremetal/v2/servers/result.go new file mode 100644 index 0000000..4829132 --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/result.go @@ -0,0 +1,208 @@ +package servers + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a Server. +type GetResult struct { + commonResult +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Extract provides access to the individual Server returned by +// the Get and functions. +func (r commonResult) Extract() (*Server, error) { + var s struct { + Server *Server `json:"server"` + } + err := r.ExtractInto(&s) + return s.Server, err +} + +// RaidArray represents raid configuration for the server resource. +type RaidArray struct { + PrimaryStorage bool `json:"primary_storage"` + RaidCardHardwareID string `json:"raid_card_hardware_id"` + DiskHardwareIDs []string `json:"disk_hardware_ids"` + RaidLevel int `json:"raid_level"` + Partitions []Partition `json:"partitions"` +} + +// Partition represents partition configuration for the server resource. +type Partition struct { + LVM bool `json:"lvm"` + Size int `json:"size"` + PartitionLabel string `json:"partition_label"` +} + +// LVMVolumeGroup represents LVM volume group configuration for the server resource. +type LVMVolumeGroup struct { + VGLabel string `json:"vg_label"` + PhysicalVolumePartitionLabels []string `json:"physical_volume_partition_labels"` + LogicalVolumes []LogicalVolume `json:"logical_volumes"` +} + +// LogicalVolume represents logical volume configuration for the server resource. +type LogicalVolume struct { + LVLabel string `json:"lv_label"` + Size int `json:"size"` +} + +// Filesystem represents file system configuration for the server resource. +type Filesystem struct { + Label string `json:"label"` + FSType string `json:"fs_type"` + MountPoint string `json:"mount_point"` +} + +// NICPhysicalPort represents port configuraion for the server resource. +type NICPhysicalPort struct { + ID string `json:"id"` + MacAddr string `json:"mac_addr"` + NetworkPhysicalPortID string `json:"network_physical_port_id"` + Plane string `json:"plane"` + AttachedPorts []AttachedPort `json:"attached_ports"` + HardwareID string `json:"hardware_id"` +} + +// AttachedPort represents attached port configuration for the server resource. +type AttachedPort struct { + PortID string `json:"port_id"` + NetworkID string `json:"network_id"` + FixedIPs []FixedIP `json:"fixed_ips"` +} + +// FixedIP represents fixed IP configuration for the server resource. +type FixedIP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +// ChassisStatus represents chassis status for the server resource +type ChassisStatus struct { + ChassisPower bool `json:"chassis-power"` + PowerSupply bool `json:"power-supply"` + CPU bool `json:"cpu"` + Memory bool `json:"memory"` + Fan bool `json:"fan"` + Disk int `json:"disk"` + NIC bool `json:"nic"` + SystemBoard bool `json:"system-board"` + Etc bool `json:"etc"` +} + +// Personality represents personal files configuration for the server resource. +type Personality struct { + Path string `json:"path"` + Contents string `json:"contents"` +} + +// Server represents hardware configurations for server resources +// in a region. +type Server struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Updated time.Time `json:"-"` + Created time.Time `json:"-"` + Status string `json:"status"` + AdminPass string `json:"adminPass"` + PowerState string `json:"OS-EXT-STS:power_state"` + TaskState string `json:"OS-EXT-STS:task_state"` + VMState string `json:"OS-EXT-STS:vm_state"` + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` + Progress int `json:"progress"` + Image map[string]interface{} `json:"image"` + Flavor map[string]interface{} `json:"flavor"` + Metadata map[string]string `json:"metadata"` + Links []eclcloud.Link `json:"links"` + RaidArrays []RaidArray `json:"raid_arrays"` + LVMVolumeGroups []LVMVolumeGroup `json:"lvm_volume_groups"` + Filesystems []Filesystem `json:"filesystems"` + NICPhysicalPorts []NICPhysicalPort `json:"nic_physical_ports"` + ChassisStatus ChassisStatus `json:"chassis-status"` + MediaAttachments []map[string]interface{} `json:"media_attachments"` + Personality []Personality `json:"personality"` +} + +// UnmarshalJSON to override default +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Created eclcloud.JSONRFC3339Milli `json:"created"` + Updated eclcloud.JSONRFC3339Milli `json:"updated"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Server(s.tmp) + + r.Created = time.Time(s.Created) + r.Updated = time.Time(s.Updated) + + return err +} + +// ServerPage contains a single page of all servers from a ListDetails call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page ServerPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"servers_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page ServerPage) IsEmpty() (bool, error) { + flavors, err := ExtractServers(page) + return len(flavors) == 0, err +} + +// ExtractServers provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s struct { + Servers []Server `json:"servers"` + } + err := (r.(ServerPage)).ExtractInto(&s) + return s.Servers, err +} diff --git a/v3/ecl/baremetal/v2/servers/testing/doc.go b/v3/ecl/baremetal/v2/servers/testing/doc.go new file mode 100644 index 0000000..d255096 --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains baremetal server unit tests +package testing diff --git a/v3/ecl/baremetal/v2/servers/testing/fixtures.go b/v3/ecl/baremetal/v2/servers/testing/fixtures.go new file mode 100644 index 0000000..72b3e48 --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/testing/fixtures.go @@ -0,0 +1,898 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/servers" +) + +var listResponse = fmt.Sprintf(` +{ + "servers": [ + { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "zone1-groupa", + "created": "2012-09-07T16:56:37Z", + "flavor": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ] + }, + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "Test Server1", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-09-07T16:56:37Z", + "user_id": "fake", + "raid_arrays": [ + { + "primary_storage": true, + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "lvm": false, + "size": 100, + "partition_label": "var" + } + ] + }, + { + "primary_storage": false, + "raid_card_hardware_id": "raid_card_uuid", + "internal_disk_ids": [ + "disk4_uuid", + "disk5_uuid", + "disk6_uuid", + "disk7_uuid" + ], + "raid_level": 10, + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ] + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", + "secondary-part1" + ], + "logical_volumes": [ + { + "lv_label": "LV_root" + }, + { + "size": 2, + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "nic_physical_ports": [ + { + "id": "39285bf9-12fb-4064-b98b-a552efc51cfc", + "mac_addr": "0a:31:c1:d5:6d:9c", + "network_physical_port_id": "38268d94-584a-4f14-96ff-732a68aa7301", + "plane": "data", + "attached_ports": [ + { + "port_id": "61b7da1e-9571-4d63-b779-e003a56b8105", + "network_id": "9aa93722-1ec4-4912-b813-b975c21460a5", + "fixed_ips": [ + { + "subnet_id": "0419bbde-2b82-4107-9d8a-6bba76e364af", + "ip_address": "192.168.10.2" + } + ] + } + ], + "hardware_id": "063468e8-61ab-4afd-be38-c937254aeb9a" + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true + }, + "media_attachments": [], + "personality": [ + { + "path": "/home/big/banner.txt", + "contents": "ZWNobyAiS3VtYSBQZXJzb25hbGl0eSIgPj4gL2hvbWUvYmlnL3BlcnNvbmFsaXR5" + } + ] + }, + { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "zone1-groupa", + "created": "2012-09-07T16:56:37Z", + "flavor": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884932", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "16d193736a5cfdb60c697ca27ad071d6126fa13baeb670fc9d10645e", + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884932", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "Test Server2", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-09-07T16:56:37Z", + "user_id": "fake", + "raid_arrays": [ + { + "primary_storage": true, + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "lvm": false, + "size": 100, + "partition_label": "var" + } + ] + }, + { + "primary_storage": false, + "raid_card_hardware_id": "raid_card_uuid", + "internal_disk_ids": [ + "disk4_uuid", + "disk5_uuid", + "disk6_uuid", + "disk7_uuid" + ], + "raid_level": 10, + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ] + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", + "secondary-part1" + ], + "logical_volumes": [ + { + "lv_label": "LV_root" + }, + { + "size": 2, + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "nic_physical_ports": [ + { + "id": "f4732cd9-31f7-408e-9f27-cc9b0ee17457", + "mac_addr": "0a:31:c1:d5:6d:9d", + "network_physical_port_id": "ab17a82d-e9a5-4e95-9b18-de3f8a47670f", + "plane": "storage", + "attached_ports": [ + { + "port_id": "6fb0d979-f05b-466c-b50c-64d5ae4c4ef6", + "network_id": "99babdfc-79eb-470a-b0d4-df02482cc509", + "fixed_ips": [ + { + "subnet_id": "9632ce5d-8750-40bf-871d-968aa3324367", + "ip_address": "192.168.10.8" + } + ] + } + ], + "hardware_id": "ab36f541-b854-46c3-8891-e9484a1ba1ac" + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true + }, + "media_attachments": [ + { + "image": { + "id": "3339fd5f-ec06-4ef8-9337-c1c70218a748", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/3339fd5f-ec06-4ef8-9337-c1c70218a748", + "rel": "bookmark" + } + ] + } + } + ] + } + ] +}`) + +var getResponse = fmt.Sprintf(` +{ + "server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "zone1-groupa", + "created": "2012-09-07T16:56:37Z", + "flavor": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ] + }, + "hostId": "16d193736a5cfdb60c697ca27ad071d6126fa13baeb670fc9d10645e", + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "Test Server1", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-09-07T16:56:37Z", + "user_id": "fake", + "raid_arrays": [ + { + "primary_storage": true, + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "lvm": false, + "size": 100, + "partition_label": "var" + } + ] + }, + { + "primary_storage": false, + "raid_card_hardware_id": "raid_card_uuid", + "internal_disk_ids": [ + "disk4_uuid", + "disk5_uuid", + "disk6_uuid", + "disk7_uuid" + ], + "raid_level": 10, + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ] + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", + "secondary-part1" + ], + "logical_volumes": [ + { + "lv_label": "LV_root" + }, + { + "size": 2, + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "nic_physical_ports": [ + { + "id": "39285bf9-12fb-4064-b98b-a552efc51cfc", + "mac_addr": "0a:31:c1:d5:6d:9c", + "network_physical_port_id": "38268d94-584a-4f14-96ff-732a68aa7301", + "plane": "data", + "attached_ports": [ + { + "port_id": "61b7da1e-9571-4d63-b779-e003a56b8105", + "network_id": "9aa93722-1ec4-4912-b813-b975c21460a5", + "fixed_ips": [ + { + "subnet_id": "0419bbde-2b82-4107-9d8a-6bba76e364af", + "ip_address": "192.168.10.2" + } + ] + } + ], + "hardware_id": "063468e8-61ab-4afd-be38-c937254aeb9a" + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true + }, + "media_attachments": [ + { + "image": { + "id": "3339fd5f-ec06-4ef8-9337-c1c70218a748", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/3339fd5f-ec06-4ef8-9337-c1c70218a748", + "rel": "bookmark" + } + ] + } + } + ], + "personality": [ + { + "path": "/home/big/banner.txt", + "contents": "ZWNobyAiS3VtYSBQZXJzb25hbGl0eSIgPj4gL2hvbWUvYmlnL3BlcnNvbmFsaXR5" + } + ] + } +}`) + +var createRequest = fmt.Sprintf(` + { + "server": { + "name": "server-test-1", + "adminPass": "aabbccddeeff", + "imageRef": "b5660a6e-4b46-4be3-9707-6b47221b454f", + "flavorRef": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "availability_zone": "zone1-groupa", + "networks": [ + { + "uuid": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "fixed_ip": "10.0.0.100" + } + ], + "raid_arrays": [ + { + "primary_storage": true, + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "size": "100G", + "partition_label": "var" + } + ] + }, + { + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk1_uuid", "disk2_uuid", "disk3_uuid", "disk4_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ], + "raid_level": 10 + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", "secondary-part1" + ], + "logical_volumes": [ + { + "size": "300G", + "lv_label": "LV_root" + }, + { + "size": "2G", + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "user_data": "dXNlcl9kYXRh", + "metadata": { + "foo": "bar" + } + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "server": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "adminPass": "aabbccddeeff" + } +}`) + +var expectedServers = []servers.Server{expectedServer1, expectedServer2} + +var expectedCreated, _ = time.Parse(eclcloud.RFC3339Milli, "2012-09-07T16:56:37Z") +var expectedUpdated, _ = time.Parse(eclcloud.RFC3339Milli, "2012-09-07T16:56:37Z") + +var expectedServer1 = servers.Server{ + ID: "05184ba3-00ba-4fbc-b7a2-03b62b884931", + TenantID: "openstack", + UserID: "fake", + Name: "Test Server1", + Updated: expectedUpdated, + Created: expectedCreated, + Status: "ACTIVE", + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "zone1-groupa", + Progress: 0, + Image: map[string]interface{}{ + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/flavors/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark", + }, + }, + }, + Metadata: map[string]string{ + "My Server Name": "Apache1", + }, + Links: []eclcloud.Link{ + { + Href: "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "self", + }, + { + Href: "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + }, + Partitions: []servers.Partition{ + { + LVM: true, + PartitionLabel: "primary-part1", + }, + { + LVM: false, + Size: 100, + PartitionLabel: "var", + }, + }, + }, + }, + LVMVolumeGroups: []servers.LVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []servers.LogicalVolume{ + { + LVLabel: "LV_root", + }, + { + Size: 2, + LVLabel: "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.Filesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + NICPhysicalPorts: []servers.NICPhysicalPort{ + { + ID: "39285bf9-12fb-4064-b98b-a552efc51cfc", + MacAddr: "0a:31:c1:d5:6d:9c", + NetworkPhysicalPortID: "38268d94-584a-4f14-96ff-732a68aa7301", + Plane: "data", + AttachedPorts: []servers.AttachedPort{ + { + PortID: "61b7da1e-9571-4d63-b779-e003a56b8105", + NetworkID: "9aa93722-1ec4-4912-b813-b975c21460a5", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "0419bbde-2b82-4107-9d8a-6bba76e364af", + IPAddress: "192.168.10.2", + }, + }, + }, + }, + HardwareID: "063468e8-61ab-4afd-be38-c937254aeb9a", + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + NIC: true, + SystemBoard: true, + Etc: true, + }, + MediaAttachments: []map[string]interface{}{}, + Personality: []servers.Personality{ + { + Path: "/home/big/banner.txt", + Contents: "ZWNobyAiS3VtYSBQZXJzb25hbGl0eSIgPj4gL2hvbWUvYmlnL3BlcnNvbmFsaXR5", + }, + }, +} + +var expectedServer2 = servers.Server{ + ID: "05184ba3-00ba-4fbc-b7a2-03b62b884932", + TenantID: "openstack", + UserID: "fake", + Name: "Test Server2", + Updated: expectedUpdated, + Created: expectedCreated, + Status: "ACTIVE", + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "zone1-groupa", + Progress: 0, + Image: map[string]interface{}{ + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884932", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark", + }, + }, + }, + Metadata: map[string]string{ + "My Server Name": "Apache1", + }, + Links: []eclcloud.Link{ + { + Href: "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "self", + }, + { + Href: "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + }, + Partitions: []servers.Partition{ + { + LVM: true, + PartitionLabel: "primary-part1", + }, + { + LVM: false, + Size: 100, + PartitionLabel: "var", + }, + }, + }, + }, + LVMVolumeGroups: []servers.LVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []servers.LogicalVolume{ + { + LVLabel: "LV_root", + }, + { + Size: 2, + LVLabel: "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.Filesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + NICPhysicalPorts: []servers.NICPhysicalPort{ + { + ID: "f4732cd9-31f7-408e-9f27-cc9b0ee17457", + MacAddr: "0a:31:c1:d5:6d:9d", + NetworkPhysicalPortID: "ab17a82d-e9a5-4e95-9b18-de3f8a47670f", + Plane: "storage", + AttachedPorts: []servers.AttachedPort{ + { + PortID: "6fb0d979-f05b-466c-b50c-64d5ae4c4ef6", + NetworkID: "99babdfc-79eb-470a-b0d4-df02482cc509", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "9632ce5d-8750-40bf-871d-968aa3324367", + IPAddress: "192.168.10.8", + }, + }, + }, + }, + HardwareID: "ab36f541-b854-46c3-8891-e9484a1ba1ac", + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + NIC: true, + SystemBoard: true, + Etc: true, + }, + MediaAttachments: []map[string]interface{}{}, + Personality: []servers.Personality(nil), +} diff --git a/v3/ecl/baremetal/v2/servers/testing/requests_test.go b/v3/ecl/baremetal/v2/servers/testing/requests_test.go new file mode 100644 index 0000000..7c49142 --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/testing/requests_test.go @@ -0,0 +1,176 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/baremetal/v2/servers" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := servers.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + fmt.Printf("person[%%#v] -> %#v\n", actual) + th.CheckDeepEquals(t, expectedServers, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s", "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := servers.Get(fakeclient.ServiceClient(), "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedServer1, actual) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, createResponse) + }) + + createOpts := servers.CreateOpts{ + Name: "server-test-1", + Networks: []servers.CreateOptsNetwork{ + { + UUID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + FixedIP: "10.0.0.100", + }, + }, + AdminPass: "aabbccddeeff", + ImageRef: "b5660a6e-4b46-4be3-9707-6b47221b454f", + FlavorRef: "05184ba3-00ba-4fbc-b7a2-03b62b884931", + AvailabilityZone: "zone1-groupa", + UserData: []byte("user_data"), + RaidArrays: []servers.CreateOptsRaidArray{ + { + PrimaryStorage: true, + Partitions: []servers.CreateOptsPartition{ + { + LVM: true, + PartitionLabel: "primary-part1", + }, + { + Size: "100G", + PartitionLabel: "var", + }, + }, + }, + { + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + "disk4_uuid", + }, + Partitions: []servers.CreateOptsPartition{ + { + LVM: true, + PartitionLabel: "secondary-part1", + }, + }, + RaidLevel: 10, + }, + }, + LVMVolumeGroups: []servers.CreateOptsLVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []servers.CreateOptsLogicalVolume{ + { + Size: "300G", + LVLabel: "LV_root", + }, + { + Size: "2G", + LVLabel: "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.CreateOptsFilesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + } + server, err := servers.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, server.AdminPass, "aabbccddeeff") +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s", "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := servers.Delete(fakeclient.ServiceClient(), "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/baremetal/v2/servers/urls.go b/v3/ecl/baremetal/v2/servers/urls.go new file mode 100644 index 0000000..07a5733 --- /dev/null +++ b/v3/ecl/baremetal/v2/servers/urls.go @@ -0,0 +1,21 @@ +package servers + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} diff --git a/v3/ecl/client.go b/v3/ecl/client.go new file mode 100644 index 0000000..ed80324 --- /dev/null +++ b/v3/ecl/client.go @@ -0,0 +1,390 @@ +package ecl + +import ( + "fmt" + "reflect" + + "github.com/nttcom/eclcloud/v3" + tokens3 "github.com/nttcom/eclcloud/v3/ecl/identity/v3/tokens" + "github.com/nttcom/eclcloud/v3/ecl/utils" +) + +const ( + // v3 represents Keystone v3. + // The version can be anything from v3 to v3.x. + v3 = "v3" +) + +/* +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. + +A basic example of using this would be: + + ao, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.NewClient(ao.IdentityEndpoint) + client, err := ecl.NewIdentityV3(provider, eclcloud.EndpointOpts{}) +*/ +func NewClient(endpoint string) (*eclcloud.ProviderClient, error) { + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + endpoint = eclcloud.NormalizeURL(endpoint) + base = eclcloud.NormalizeURL(base) + + p := new(eclcloud.ProviderClient) + p.IdentityBase = base + p.IdentityEndpoint = endpoint + p.UseTokenLock() + + return p, nil +} + +/* +AuthenticatedClient logs in to an Enterprise Cloud found at the identity endpoint +specified by the options, acquires a token, and returns a Provider Client +instance that's ready to operate. + +If the full path to a versioned identity endpoint was specified (example: +http://example.com:5000/v3), that path will be used as the endpoint to query. + +If a versionless endpoint was specified (example: http://example.com:5000/), +the endpoint will be queried to determine which versions of the identity service +are available, then chooses the most recent or most supported version. + +Example: + + ao, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(ao) + client, err := ecl.NewNetworkV2(client, eclcloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +func AuthenticatedClient(options eclcloud.AuthOptions) (*eclcloud.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 *eclcloud.ProviderClient, options eclcloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v3, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v3: + return v3auth(client, endpoint, &options, eclcloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *eclcloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo eclcloud.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +func v3auth(client *eclcloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo eclcloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + result := tokens3.Create(v3Client, opts) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + + if opts.CanReauth() { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.ReauthFunc = nil + tac.TokenID = "" + var tao tokens3.AuthOptionsBuilder + switch ot := opts.(type) { + case *eclcloud.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *tokens3.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + default: + tao = opts + } + client.ReauthFunc = func() error { + err := v3auth(&tac, endpoint, tao, eo) + if err != nil { + return err + } + client.TokenID = tac.TokenID + return nil + } + } + client.EndpointLocator = func(opts eclcloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 +// identity service. +func NewIdentityV3(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, eclcloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + // Ensure endpoint still has a suffix of v3. + // This is because EndpointLocator might have found a versionless + // endpoint or the published endpoint is still /v2.0. In both + // cases, we need to fix the endpoint to point to /v3. + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + base = eclcloud.NormalizeURL(base) + + endpoint = base + "v3/" + + return &eclcloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +func initClientOpts(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts, clientType string) (*eclcloud.ServiceClient, error) { + sc := new(eclcloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +func initSSSClientOptsForced(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts, clientType string, sssURL string) (*eclcloud.ServiceClient, error) { + sc := new(eclcloud.ServiceClient) + eo.ApplyDefaults(clientType) + url := sssURL + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 +// object storage package. +func NewObjectStorageV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "object-store") +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute +// package. +func NewComputeV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "compute") +} + +// NewBaremetalV2 creates a ServiceClient that may be used with the v2 baremetal +// package. +func NewBaremetalV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal-server") +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network +// package. +func NewNetworkV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewComputeVolumeV2 creates a ServiceClient that may be used to access the v2 +// block storage service. +func NewComputeVolumeV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev2") +} + +// NewSSSV1 creates ServiceClient that may be used to access the v1 +// SSS API service. +func NewSSSV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "sss") +} + +// NewSSSV1 creates ServiceClient that may be used to access the v1 +// SSS API service with Unscoped Token. +func NewSSSV1Forced(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts, sssURL string) (*eclcloud.ServiceClient, error) { + return initSSSClientOptsForced(client, eo, "sss", sssURL) +} + +// NewStorageV1 creates ServiceClient that may be used to access the v1 +// storage API service. +func NewStorageV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "storage") +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 +// orchestration service. +func NewOrchestrationV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration") +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS +// service. +func NewDNSV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 +// image service. +func NewImageServiceV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewVNAV1 creates a ServiceClient that may be used with the v1 virtual network appliance management package. +func NewVNAV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "virtual-network-appliance") + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc, err +} + +// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2 +// load balancer service. +func NewLoadBalancerV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "load-balancer") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewManagedLoadBalancerV1 creates a ServiceClient that may be used to access the v1 +// managed load balancer service. +func NewManagedLoadBalancerV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "managed-load-balancer") + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc, err +} + +// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering +// package. +func NewClusteringV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "clustering") +} + +// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging +// service. +func NewMessagingV2(client *eclcloud.ProviderClient, clientID string, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "messaging") + sc.MoreHeaders = map[string]string{"Client-ID": clientID} + return sc, err +} + +// NewContainerV1 creates a ServiceClient that may be used with v1 container package +func NewContainerV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "container") +} + +// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key +// manager service. +func NewKeyManagerV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "key-manager") + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management +// package. +func NewContainerInfraV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "container-infra") +} + +// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. +func NewWorkflowV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "workflowv2") +} + +// NewSecurityOrderV3 creates a ServiceClient that may be used to access the v3 Security +// Order API service. +func NewSecurityOrderV3(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "security-order-th") + // sc.ResourceBase = sc.Endpoint + "v3/" + return sc, err +} + +// NewSecurityPortalV3 creates a ServiceClient that may be used to access the v3 Security +// Portal API service. +func NewSecurityPortalV3(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "security-operation-th") + // sc.ResourceBase = sc.Endpoint + "v3/" + return sc, err +} + +// NewDedicatedHypervisorV1 creates a ServiceClient that may be used to access the v1 Dedicated Hypervisor service. +func NewDedicatedHypervisorV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "dedicated-hypervisor") +} + +// NewRCAV1 creates a ServiceClient that may be used to access the v1 Remote Console Access service. +func NewRCAV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "rca") +} + +// NewProviderConnectivityV2 creates a ServiceClient that may be used to access the v2 Provider Connectivity service. +func NewProviderConnectivityV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "provider-connectivity") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/doc.go b/v3/ecl/compute/v2/extensions/availabilityzones/doc.go new file mode 100644 index 0000000..6fbc22f --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/doc.go @@ -0,0 +1,46 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Extend server result with Availability Zone Information: + + type ServerWithAZ struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + } + + var allServers []ServerWithAZ + + allPages, err := servers.List(client, nil).AllPages() + if err != nil { + panic("Unable to retrieve servers: %s", err) + } + + err = servers.ExtractServersInto(allPages, &allServers) + if err != nil { + panic("Unable to extract servers: %s", err) + } + + for _, server := range allServers { + fmt.Println(server.AvailabilityZone) + } + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } + +*/ +package availabilityzones diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/requests.go b/v3/ecl/compute/v2/extensions/availabilityzones/requests.go new file mode 100644 index 0000000..247bbd2 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/requests.go @@ -0,0 +1,13 @@ +package availabilityzones + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// List will return the existing availability zones. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/results.go b/v3/ecl/compute/v2/extensions/availabilityzones/results.go new file mode 100644 index 0000000..c66f396 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/results.go @@ -0,0 +1,80 @@ +package availabilityzones + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ServerAvailabilityZoneExt is an extension to the base Server object. +type ServerAvailabilityZoneExt struct { + // AvailabilityZone is the availability zone the server is in. + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` +} + +// ServiceState represents the state of a service in an AvailabilityZone. +type ServiceState struct { + Active bool `json:"active"` + Available bool `json:"available"` + UpdatedAt time.Time `json:"-"` +} + +// UnmarshalJSON to override default +func (r *ServiceState) UnmarshalJSON(b []byte) error { + type tmp ServiceState + var s struct { + tmp + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ServiceState(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +// Services is a map of services contained in an AvailabilityZone. +type Services map[string]ServiceState + +// Hosts is map of hosts/nodes contained in an AvailabilityZone. +// Each host can have multiple services. +type Hosts map[string]Services + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an ECL +// AvailabilityZone. +type AvailabilityZone struct { + Hosts Hosts `json:"hosts"` + // The availability zone name + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` +} + +// AvailabilityZonePage stores a single page of all AvailabilityZone results +// from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/testing/doc.go b/v3/ecl/compute/v2/extensions/availabilityzones/testing/doc.go new file mode 100644 index 0000000..a4408d7 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/testing/doc.go @@ -0,0 +1,2 @@ +// availabilityzones unittests +package testing diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/testing/fixtures.go b/v3/ecl/compute/v2/extensions/availabilityzones/testing/fixtures.go new file mode 100644 index 0000000..921cad3 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/testing/fixtures.go @@ -0,0 +1,31 @@ +package testing + +import ( + az "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/availabilityzones" +) + +const getResponse = ` +{ + "availabilityZoneInfo": [{ + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupa" + }, { + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupb" + }] +} +` + +var azResult = []az.AvailabilityZone{ + { + Hosts: nil, + ZoneName: "zone1-groupa", + ZoneState: az.ZoneState{Available: true}, + }, +} diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/testing/requests_test.go b/v3/ecl/compute/v2/extensions/availabilityzones/testing/requests_test.go new file mode 100644 index 0000000..64ef836 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/testing/requests_test.go @@ -0,0 +1,33 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + az "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/availabilityzones" + th "github.com/nttcom/eclcloud/v3/testhelper" + + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListAvailabilityZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + allPages, err := az.List(fakeclient.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := az.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, azResult, actual) +} diff --git a/v3/ecl/compute/v2/extensions/availabilityzones/urls.go b/v3/ecl/compute/v2/extensions/availabilityzones/urls.go new file mode 100644 index 0000000..e62fdd9 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/availabilityzones/urls.go @@ -0,0 +1,7 @@ +package availabilityzones + +import "github.com/nttcom/eclcloud/v3" + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/doc.go b/v3/ecl/compute/v2/extensions/bootfromvolume/doc.go new file mode 100644 index 0000000..3f16cda --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/doc.go @@ -0,0 +1,145 @@ +/* +Package bootfromvolume extends a server create request with the ability to +specify block device options. This can be used to boot a server from a block +storage volume as well as specify multiple ephemeral disks upon creation. + +Example of Creating a Server From an Image + +This example will boot a server from an image and use a standard ephemeral +disk as the server's root disk. This is virtually no different than creating +a server without using block device mappings. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example of Creating a Server From a New Volume + +This example will create a block storage volume based on the given Image. The +server will use this volume as its root disk. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + VolumeSize: 2, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example of Creating a Server From an Existing Volume + +This example will create a server with an existing volume as its root disk. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: "volume-uuid", + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example of Creating a Server with Multiple Ephemeral Disks + +This example will create a server with multiple ephemeral disks. The first +block device will be based off of an existing Image. Each additional +ephemeral disks must have an index of -1. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + VolumeSize: 5, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package bootfromvolume diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/requests.go b/v3/ecl/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 0000000..2ba88e9 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,129 @@ +package bootfromvolume + +import ( + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/servers" + + "github.com/nttcom/eclcloud/v3" +) + +type ( + // DestinationType represents the type of medium being used as the + // destination of the bootable device. + DestinationType string + + // SourceType represents the type of medium being used as the source of the + // bootable device. + SourceType string +) + +const ( + // DestinationLocal DestinationType is for using an ephemeral disk as the + // destination. + DestinationLocal DestinationType = "local" + + // DestinationVolume DestinationType is for using a volume as the destination. + DestinationVolume DestinationType = "volume" + + // SourceBlank SourceType is for a "blank" or empty source. + SourceBlank SourceType = "blank" + + // SourceImage SourceType is for using images as the source of a block device. + SourceImage SourceType = "image" + + // SourceSnapshot SourceType is for using a volume snapshot as the source of + // a block device. + SourceSnapshot SourceType = "snapshot" + + // SourceVolume SourceType is for using a volume as the source of block + // device. + SourceVolume SourceType = "volume" +) + +// BlockDevice is a structure with options for creating block devices in a +// server. The block device may be created from an image, snapshot, new volume, +// or existing volume. The destination may be a new volume, existing volume +// which will be attached to the instance, ephemeral disk, or boot device. +type BlockDevice struct { + // SourceType must be one of: "volume", "snapshot", "image", or "blank". + SourceType SourceType `json:"source_type" required:"true"` + + // UUID is the unique identifier for the existing volume, snapshot, or + // image (see above). + UUID string `json:"uuid,omitempty"` + + // BootIndex is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType is the type that gets created. Possible values are "volume" + // and "local". + DestinationType DestinationType `json:"destination_type,omitempty"` + + // GuestFormat specifies the format of the block device. + GuestFormat string `json:"guest_format,omitempty"` + + // VolumeSize is the size of the volume to create (in gigabytes). This can be + // omitted for existing volumes. + VolumeSize int `json:"volume_size,omitempty"` + + // DeviceType specifies the device type of the block devices. + // Examples of this are disk, cdrom, floppy, lun, etc. + DeviceType string `json:"device_type,omitempty"` + + // DiskBus is the bus type of the block devices. + // Examples of this are ide, usb, virtio, scsi, etc. + DiskBus string `json:"disk_bus,omitempty"` +} + +// 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 { + err := eclcloud.ErrMissingInput{} + err.Argument = "bootfromvolume.CreateOptsExt.BlockDevice" + return nil, err + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + b, err := eclcloud.BuildRequestBody(bd, "") + if err != nil { + return nil, err + } + blockDevice[i] = b + } + 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 *eclcloud.ServiceClient, opts servers.CreateOptsBuilder) (r servers.CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/results.go b/v3/ecl/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 0000000..8630187 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,12 @@ +package bootfromvolume + +import ( + os "github.com/nttcom/eclcloud/v3/ecl/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +// It embeds the standard servers.CreateResults type and so can be used the +// same way as a standard server request result. +type CreateResult struct { + os.CreateResult +} diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/testing/doc.go b/v3/ecl/compute/v2/extensions/bootfromvolume/testing/doc.go new file mode 100644 index 0000000..2b6699f --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains bootfromvolume unit tests +package testing diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/testing/fixtures.go b/v3/ecl/compute/v2/extensions/bootfromvolume/testing/fixtures.go new file mode 100644 index 0000000..98199d6 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/testing/fixtures.go @@ -0,0 +1,275 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/bootfromvolume" + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/servers" +) + +var baseCreateOpts = servers.CreateOpts{ + Name: "createdserver", + FlavorRef: "performance1-1", +} + +var baseCreateOptsWithImageRef = servers.CreateOpts{ + Name: "createdserver", + FlavorRef: "performance1-1", + ImageRef: "asdfasdfasdf", +} + +const expectedNewVolumeRequest = ` +{ + "server": { + "name":"createdserver", + "flavorRef":"performance1-1", + "imageRef":"", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": 0, + "delete_on_termination": true, + "volume_size": 10 + } + ] + } +} +` + +var newVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOpts, + BlockDevice: []bootfromvolume.BlockDevice{ + { + UUID: "123456", + SourceType: bootfromvolume.SourceImage, + DestinationType: bootfromvolume.DestinationVolume, + VolumeSize: 10, + DeleteOnTermination: true, + }, + }, +} + +const expectedExistingVolumeRequest = ` +{ + "server": { + "name":"createdserver", + "flavorRef":"performance1-1", + "imageRef":"", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"volume", + "destination_type":"volume", + "boot_index": 0, + "delete_on_termination": true + } + ] + } +} +` + +var existingVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOpts, + BlockDevice: []bootfromvolume.BlockDevice{ + { + UUID: "123456", + SourceType: bootfromvolume.SourceVolume, + DestinationType: bootfromvolume.DestinationVolume, + DeleteOnTermination: true, + }, + }, +} + +const expectedImageRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + } + ] + } +} +` + +var imageRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + }, +} + +const expectedMultiEphemeralRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": -1, + "delete_on_termination": true, + "destination_type":"local", + "guest_format":"ext4", + "source_type":"blank", + "volume_size": 1 + }, + { + "boot_index": -1, + "delete_on_termination": true, + "destination_type":"local", + "guest_format":"ext4", + "source_type":"blank", + "volume_size": 1 + } + ] + } +} +` + +var multiEphemeralRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: -1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + { + BootIndex: -1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + }, +} + +const expectedImageAndNewVolumeRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": 1, + "delete_on_termination": true, + "destination_type":"volume", + "source_type":"blank", + "volume_size": 1, + "device_type": "disk", + "disk_bus": "scsi" + } + ] + } +} +` + +var imageAndNewVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + DeviceType: "disk", + DiskBus: "scsi", + }, + }, +} + +const expectedImageAndExistingVolumeRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": 1, + "delete_on_termination": true, + "destination_type":"volume", + "source_type":"volume", + "uuid":"123456", + "volume_size": 1 + } + ] + } +} +` + +var imageAndExistingVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: "123456", + VolumeSize: 1, + }, + }, +} diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/testing/requests_test.go b/v3/ecl/compute/v2/extensions/bootfromvolume/testing/requests_test.go new file mode 100644 index 0000000..3178a5e --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/testing/requests_test.go @@ -0,0 +1,44 @@ +package testing + +import ( + "testing" + + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestBootFromNewVolume(t *testing.T) { + + actual, err := newVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedNewVolumeRequest, actual) +} + +func TestBootFromExistingVolume(t *testing.T) { + actual, err := existingVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedExistingVolumeRequest, actual) +} + +func TestBootFromImage(t *testing.T) { + actual, err := imageRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedImageRequest, actual) +} + +func TestCreateMultiEphemeralOpts(t *testing.T) { + actual, err := multiEphemeralRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedMultiEphemeralRequest, actual) +} + +func TestAttachNewVolume(t *testing.T) { + actual, err := imageAndNewVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedImageAndNewVolumeRequest, actual) +} + +func TestAttachExistingVolume(t *testing.T) { + actual, err := imageAndExistingVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedImageAndExistingVolumeRequest, actual) +} diff --git a/v3/ecl/compute/v2/extensions/bootfromvolume/urls.go b/v3/ecl/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 0000000..69c85ab --- /dev/null +++ b/v3/ecl/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/nttcom/eclcloud/v3" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("os-volumes_boot") +} diff --git a/v3/ecl/compute/v2/extensions/keypairs/doc.go b/v3/ecl/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 0000000..24c4607 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,71 @@ +/* +Package keypairs provides the ability to manage key pairs as well as create +servers with a specified key pair. + +Example to List Key Pairs + + allPages, err := keypairs.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to Create a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", keypair) + +Example to Import a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + PublicKey: "public-key", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair + + err := keypairs.Delete(computeClient, "keypair-name").ExtractErr() + if err != nil { + panic(err) + } + +Example to Create a Server With a Key Pair + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := keypairs.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + KeyName: "keypair-name", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package keypairs diff --git a/v3/ecl/compute/v2/extensions/keypairs/requests.go b/v3/ecl/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 0000000..e7c3b33 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,86 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/servers" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // KeyName is the name of the key pair. + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if opts.KeyName == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyName + + return base, nil +} + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies KeyPair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. + // If provided, this key will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new KeyPair on the server, or to import a +// pre-existing keypair. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *eclcloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), nil) + return +} diff --git a/v3/ecl/compute/v2/extensions/keypairs/results.go b/v3/ecl/compute/v2/extensions/keypairs/results.go new file mode 100644 index 0000000..596f973 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,91 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// KeyPair is an SSH key known to the Enterprise Cloud 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 `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate + // or validate a longer public key. + Fingerprint string `json:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. + // "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"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 `json:"private_key"` + + // UserID is the user who owns this KeyPair. + UserID string `json:"user_id"` +} + +// KeyPairPage stores a single page of all KeyPair results from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `json:"keypair"` + } + var s struct { + KeyPairs []pair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + results := make([]KeyPair, len(s.KeyPairs)) + for i, pair := range s.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + eclcloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response +// as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/compute/v2/extensions/keypairs/testing/doc.go b/v3/ecl/compute/v2/extensions/keypairs/testing/doc.go new file mode 100644 index 0000000..bf23f88 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains keypairs unit tests +package testing diff --git a/v3/ecl/compute/v2/extensions/keypairs/testing/fixtures.go b/v3/ecl/compute/v2/extensions/keypairs/testing/fixtures.go new file mode 100644 index 0000000..17776b1 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/testing/fixtures.go @@ -0,0 +1,96 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/keypairs" +) + +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" + } + } + ] +} +` + +const createRequest = `{ "keypair": { "name": "createdkey" } }` +const createResponse = ` +{ + "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" + } +} +` + +const getResponse = ` +{ + "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" + } +} +` + +const importRequest = ` +{ + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } +}` +const importResponse = ` +{ + "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" + } +} +` + +var firstKeyPair = keypairs.KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +var secondKeyPair = keypairs.KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +var expectedKeyPairSlice = []keypairs.KeyPair{firstKeyPair, secondKeyPair} + +var createdKeyPair = keypairs.KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + 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", +} + +var importedKeyPair = keypairs.KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} diff --git a/v3/ecl/compute/v2/extensions/keypairs/testing/requests_test.go b/v3/ecl/compute/v2/extensions/keypairs/testing/requests_test.go new file mode 100644 index 0000000..7e4bda6 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/testing/requests_test.go @@ -0,0 +1,111 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/keypairs" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listOutput) + }) + + count := 0 + err := keypairs.List(fakeclient.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdKeyPair, actual) +} + +func TestImportKeypair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, importRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, importResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &importedKeyPair, actual) +} + +func TestGetKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := keypairs.Get(fakeclient.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &firstKeyPair, actual) +} + +func TestDeleteKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) + + err := keypairs.Delete(fakeclient.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v3/ecl/compute/v2/extensions/keypairs/urls.go b/v3/ecl/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 0000000..2637342 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/nttcom/eclcloud/v3" + +const resourcePath = "os-keypairs" + +func resourceURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *eclcloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *eclcloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/v3/ecl/compute/v2/extensions/startstop/doc.go b/v3/ecl/compute/v2/extensions/startstop/doc.go new file mode 100644 index 0000000..65c4c5a --- /dev/null +++ b/v3/ecl/compute/v2/extensions/startstop/doc.go @@ -0,0 +1,19 @@ +/* +Package startstop provides functionality to start and stop servers that have +been provisioned by the Enterprise Cloud Compute service. + +Example to Stop and Start a Server + + serverID := "47b6b7b7-568d-40e4-868c-d5c41735532e" + + err := startstop.Stop(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + + err := startstop.Start(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package startstop diff --git a/v3/ecl/compute/v2/extensions/startstop/requests.go b/v3/ecl/compute/v2/extensions/startstop/requests.go new file mode 100644 index 0000000..dc11573 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,19 @@ +package startstop + +import "github.com/nttcom/eclcloud/v3" + +func actionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Start is the operation responsible for starting a Compute server. +func Start(client *eclcloud.ServiceClient, id string) (r StartResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil) + return +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(client *eclcloud.ServiceClient, id string) (r StopResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil) + return +} diff --git a/v3/ecl/compute/v2/extensions/startstop/results.go b/v3/ecl/compute/v2/extensions/startstop/results.go new file mode 100644 index 0000000..5eec687 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/startstop/results.go @@ -0,0 +1,15 @@ +package startstop + +import "github.com/nttcom/eclcloud/v3" + +// StartResult is the response from a Start operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StartResult struct { + eclcloud.ErrResult +} + +// StopResult is the response from Stop operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StopResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/compute/v2/extensions/startstop/testing/doc.go b/v3/ecl/compute/v2/extensions/startstop/testing/doc.go new file mode 100644 index 0000000..ee45964 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/startstop/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains startstop unit tests +package testing diff --git a/v3/ecl/compute/v2/extensions/startstop/testing/requests_test.go b/v3/ecl/compute/v2/extensions/startstop/testing/requests_test.go new file mode 100644 index 0000000..1bea2fc --- /dev/null +++ b/v3/ecl/compute/v2/extensions/startstop/testing/requests_test.go @@ -0,0 +1,44 @@ +package testing + +import ( + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/startstop" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +const serverID = "645b787e-7fbb-4111-a217-63a2882930f2" + +func TestServerStart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/servers/" + serverID + "/action" + th.Mux.HandleFunc(url, 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) + }) + + err := startstop.Start(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestServerStop(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/servers/" + serverID + "/action" + th.Mux.HandleFunc(url, 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) + }) + + err := startstop.Stop(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v3/ecl/compute/v2/extensions/volumeattach/doc.go b/v3/ecl/compute/v2/extensions/volumeattach/doc.go new file mode 100644 index 0000000..484eb20 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/doc.go @@ -0,0 +1,30 @@ +/* +Package volumeattach provides the ability to attach and detach volumes +from servers. + +Example to Attach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + volumeID := "87463836-f0e2-4029-abf6-20c8892a3103" + + createOpts := volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: volumeID, + } + + result, err := volumeattach.Create(computeClient, serverID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Detach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + attachmentID := "ed081613-1c9b-4231-aa5e-ebfd4d87f983" + + err := volumeattach.Delete(computeClient, serverID, attachmentID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeattach diff --git a/v3/ecl/compute/v2/extensions/volumeattach/requests.go b/v3/ecl/compute/v2/extensions/volumeattach/requests.go new file mode 100644 index 0000000..bd07d35 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/requests.go @@ -0,0 +1,60 @@ +package volumeattach + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of +// VolumeAttachments. +func List(client *eclcloud.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return VolumeAttachmentPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + ToVolumeAttachmentCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies volume attachment creation or import parameters. +type CreateOpts struct { + // Device is the device that the volume will attach to the instance as. + // Omit for "auto". + Device string `json:"device,omitempty"` + + // VolumeID is the ID of the volume to attach to the instance. + VolumeID string `json:"volumeId" required:"true"` +} + +// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volumeAttachment") +} + +// Create requests the creation of a new volume attachment on the server. +func Create(client *eclcloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeAttachmentCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, serverID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *eclcloud.ServiceClient, serverID, attachmentID string) (r GetResult) { + _, r.Err = client.Get(getURL(client, serverID, attachmentID), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored VolumeAttachment from +// the server. +func Delete(client *eclcloud.ServiceClient, serverID, attachmentID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, serverID, attachmentID), nil) + return +} diff --git a/v3/ecl/compute/v2/extensions/volumeattach/results.go b/v3/ecl/compute/v2/extensions/volumeattach/results.go new file mode 100644 index 0000000..0b7e355 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/results.go @@ -0,0 +1,77 @@ +package volumeattach + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// VolumeAttachment contains attachment information between a volume +// and server. +type VolumeAttachment struct { + // ID is a unique id of the attachment. + ID string `json:"id"` + + // Device is what device the volume is attached as. + Device string `json:"device"` + + // VolumeID is the ID of the attached volume. + VolumeID string `json:"volumeId"` + + // ServerID is the ID of the instance that has the volume attached. + ServerID string `json:"serverId"` +} + +// VolumeAttachmentPage stores a single page all of VolumeAttachment +// results from a List call. +type VolumeAttachmentPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a VolumeAttachmentPage is empty. +func (page VolumeAttachmentPage) IsEmpty() (bool, error) { + va, err := ExtractVolumeAttachments(page) + return len(va) == 0, err +} + +// ExtractVolumeAttachments interprets a page of results as a slice of +// VolumeAttachment. +func ExtractVolumeAttachments(r pagination.Page) ([]VolumeAttachment, error) { + var s struct { + VolumeAttachments []VolumeAttachment `json:"volumeAttachments"` + } + err := (r.(VolumeAttachmentPage)).ExtractInto(&s) + return s.VolumeAttachments, err +} + +// VolumeAttachmentResult is the result from a volume attachment operation. +type VolumeAttachmentResult struct { + eclcloud.Result +} + +// Extract is a method that attempts to interpret any VolumeAttachment resource +// response as a VolumeAttachment struct. +func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) { + var s struct { + VolumeAttachment *VolumeAttachment `json:"volumeAttachment"` + } + err := r.ExtractInto(&s) + return s.VolumeAttachment, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a VolumeAttachment. +type CreateResult struct { + VolumeAttachmentResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a VolumeAttachment. +type GetResult struct { + VolumeAttachmentResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/compute/v2/extensions/volumeattach/testing/doc.go b/v3/ecl/compute/v2/extensions/volumeattach/testing/doc.go new file mode 100644 index 0000000..7d35174 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains volumeattach unit tests +package testing diff --git a/v3/ecl/compute/v2/extensions/volumeattach/testing/fixtures.go b/v3/ecl/compute/v2/extensions/volumeattach/testing/fixtures.go new file mode 100644 index 0000000..4790c94 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/testing/fixtures.go @@ -0,0 +1,86 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/volumeattach" +) + +const serverID = "4d8c3732-a248-40ed-bebc-539a6ffd25c0" +const volumeID = "a26887c6-c47b-4654-abb5-dfadf7d3f803" +const attachID = volumeID + +var listResponse = fmt.Sprintf(` +{ + "volumeAttachments": [ + { + "device": "/dev/vdd", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + }, + { + "device": "/dev/vdc", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + } + ] +}`, + volumeID, serverID, volumeID, + volumeID, serverID, volumeID, +) + +var expectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{ + firstVolumeAttachment, + secondVolumeAttachment, +} + +var firstVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdd", + ID: volumeID, + ServerID: serverID, + VolumeID: volumeID, +} + +var secondVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: volumeID, + ServerID: serverID, + VolumeID: volumeID, +} + +var getResponse = fmt.Sprintf(` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + } +}`, volumeID, serverID, volumeID) + +var createRequest = fmt.Sprintf(` +{ + "volumeAttachment": { + "volumeId": "%s", + "device": "/dev/vdc" + } +}`, volumeID) + +var createResponse = fmt.Sprintf(` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + } +}`, volumeID, serverID, volumeID) + +var createdVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: volumeID, + ServerID: serverID, + VolumeID: volumeID, +} diff --git a/v3/ecl/compute/v2/extensions/volumeattach/testing/requests_test.go b/v3/ecl/compute/v2/extensions/volumeattach/testing/requests_test.go new file mode 100644 index 0000000..54fcd59 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/testing/requests_test.go @@ -0,0 +1,95 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/extensions/volumeattach" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := volumeattach.List(fakeclient.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumeattach.ExtractVolumeAttachments(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedVolumeAttachmentSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + actual, err := volumeattach.Create(fakeclient.ServiceClient(), serverID, volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: volumeID, + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdVolumeAttachment, actual) +} + +func TestGetVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments/%s", serverID, volumeID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := volumeattach.Get(fakeclient.ServiceClient(), serverID, attachID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &secondVolumeAttachment, actual) +} + +func TestDeleteVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments/%s", serverID, volumeID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) + + err := volumeattach.Delete(fakeclient.ServiceClient(), serverID, attachID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v3/ecl/compute/v2/extensions/volumeattach/urls.go b/v3/ecl/compute/v2/extensions/volumeattach/urls.go new file mode 100644 index 0000000..ff959a0 --- /dev/null +++ b/v3/ecl/compute/v2/extensions/volumeattach/urls.go @@ -0,0 +1,25 @@ +package volumeattach + +import "github.com/nttcom/eclcloud/v3" + +const resourcePath = "os-volume_attachments" + +func resourceURL(c *eclcloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, resourcePath) +} + +func listURL(c *eclcloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func createURL(c *eclcloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func getURL(c *eclcloud.ServiceClient, serverID, aID string) string { + return c.ServiceURL("servers", serverID, resourcePath, aID) +} + +func deleteURL(c *eclcloud.ServiceClient, serverID, aID string) string { + return getURL(c, serverID, aID) +} diff --git a/v3/ecl/compute/v2/flavors/doc.go b/v3/ecl/compute/v2/flavors/doc.go new file mode 100644 index 0000000..e4e2959 --- /dev/null +++ b/v3/ecl/compute/v2/flavors/doc.go @@ -0,0 +1,137 @@ +/* +Package flavors provides information and interaction with the flavor API +in the Enterprise Cloud Compute service. + +A flavor is an available hardware configuration for a server. Each flavor +has a unique combination of disk space, memory capacity and priority for CPU +time. + +Example to List Flavors + + listOpts := flavors.ListOpts{ + AccessType: flavors.PublicAccess, + } + + allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: eclcloud.IntToPointer(1), + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + flavor, err := flavors.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List Flavor Access + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages() + if err != nil { + panic(err) + } + + allAccesses, err := flavors.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.AddAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.RemoveAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Update Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/v3/ecl/compute/v2/flavors/requests.go b/v3/ecl/compute/v2/flavors/requests.go new file mode 100644 index 0000000..6803866 --- /dev/null +++ b/v3/ecl/compute/v2/flavors/requests.go @@ -0,0 +1,357 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +/* + AccessType maps to Enterprise Cloud's Flavor.is_public field. Although the is_public + field is boolean, the request options are ternary, which is why AccessType is + a string. The following values are allowed: + + The AccessType arguement is optional, and if it is not supplied, Enterprise Cloud + returns the PublicAccess flavors. +*/ +type AccessType string + +const ( + // PublicAccess returns public flavors and private flavors associated with + // that project. + PublicAccess AccessType = "true" + + // PrivateAccess (admin only) returns private flavors, across all projects. + PrivateAccess AccessType = "false" + + // AllAccess (admin only) returns public and private flavors across all + // projects. + AllAccess AccessType = "None" +) + +/* + ListOpts filters the results returned by the List() function. + For example, a flavor with a minDisk field of 10 will not be returned if you + specify MinDisk set to 20. + + Typically, software will use the last ID of the previous call to List to set + the Marker for the current call. +*/ +type 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"` + + // SortDir allows to select sort direction. + // It can be "asc" or "desc" (default). + SortDir string `q:"sort_dir"` + + // SortKey allows to sort by one of the flavors attributes. + // Default is flavorid. + SortKey string `q:"sort_key"` + + // 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"` + + // AccessType, if provided, instructs List which set of flavors to return. + // If IsPublic not provided, flavors for the current project are returned. + AccessType AccessType `q:"is_public"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs Enterprise Cloud to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier +// processing. +func ListDetail(client *eclcloud.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 + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// type CreateOptsBuilder interface { +// ToFlavorCreateMap() (map[string]interface{}, error) +// } + +// // CreateOpts specifies parameters used for creating a flavor. +// type CreateOpts struct { +// // Name is the name of the flavor. +// Name string `json:"name" required:"true"` + +// // RAM is the memory of the flavor, measured in MB. +// RAM int `json:"ram" required:"true"` + +// // VCPUs is the number of vcpus for the flavor. +// VCPUs int `json:"vcpus" required:"true"` + +// // Disk the amount of root disk space, measured in GB. +// Disk *int `json:"disk" required:"true"` + +// // ID is a unique ID for the flavor. +// ID string `json:"id,omitempty"` + +// // Swap is the amount of swap space for the flavor, measured in MB. +// Swap *int `json:"swap,omitempty"` + +// // RxTxFactor alters the network bandwidth of a flavor. +// RxTxFactor float64 `json:"rxtx_factor,omitempty"` + +// // IsPublic flags a flavor as being available to all projects or not. +// IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + +// // Ephemeral is the amount of ephemeral disk space, measured in GB. +// Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` +// } + +// // ToFlavorCreateMap constructs a request body from CreateOpts. +// func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { +// return eclcloud.BuildRequestBody(opts, "flavor") +// } + +// // Create requests the creation of a new flavor. +// func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { +// b, err := opts.ToFlavorCreateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200, 201}, +// }) +// return +// } + +// Get retrieves details of a single flavor. Use ExtractFlavor to convert its +// result into a Flavor. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// // Delete deletes the specified flavor ID. +// func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { +// _, r.Err = client.Delete(deleteURL(client, id), nil) +// return +// } + +// // ListAccesses retrieves the tenants which have access to a flavor. +// func ListAccesses(client *eclcloud.ServiceClient, id string) pagination.Pager { +// url := accessURL(client, id) + +// return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { +// return AccessPage{pagination.SinglePageBase(r)} +// }) +// } + +// // AddAccessOptsBuilder allows extensions to add additional parameters to the +// // AddAccess requests. +// type AddAccessOptsBuilder interface { +// ToFlavorAddAccessMap() (map[string]interface{}, error) +// } + +// // AddAccessOpts represents options for adding access to a flavor. +// type AddAccessOpts struct { +// // Tenant is the project/tenant ID to grant access. +// Tenant string `json:"tenant"` +// } + +// // ToFlavorAddAccessMap constructs a request body from AddAccessOpts. +// func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) { +// return eclcloud.BuildRequestBody(opts, "addTenantAccess") +// } + +// // AddAccess grants a tenant/project access to a flavor. +// func AddAccess(client *eclcloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { +// b, err := opts.ToFlavorAddAccessMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// // RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// // RemoveAccess requests. +// type RemoveAccessOptsBuilder interface { +// ToFlavorRemoveAccessMap() (map[string]interface{}, error) +// } + +// // RemoveAccessOpts represents options for removing access to a flavor. +// type RemoveAccessOpts struct { +// // Tenant is the project/tenant ID to grant access. +// Tenant string `json:"tenant"` +// } + +// // ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts. +// func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) { +// return eclcloud.BuildRequestBody(opts, "removeTenantAccess") +// } + +// // RemoveAccess removes/revokes a tenant/project access to a flavor. +// func RemoveAccess(client *eclcloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { +// b, err := opts.ToFlavorRemoveAccessMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// // ExtraSpecs requests all the extra-specs for the given flavor ID. +// func ListExtraSpecs(client *eclcloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { +// _, r.Err = client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil) +// return +// } + +// func GetExtraSpec(client *eclcloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { +// _, r.Err = client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil) +// return +// } + +// // CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// // CreateExtraSpecs requests. +// type CreateExtraSpecsOptsBuilder interface { +// ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) +// } + +// // ExtraSpecsOpts is a map that contains key-value pairs. +// type ExtraSpecsOpts map[string]string + +// // ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on +// // the contents of ExtraSpecsOpts. +// func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) { +// return map[string]interface{}{"extra_specs": opts}, nil +// } + +// // CreateExtraSpecs will create or update the extra-specs key-value pairs for +// // the specified Flavor. +// func CreateExtraSpecs(client *eclcloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { +// b, err := opts.ToFlavorExtraSpecsCreateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// // UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// // the Update request. +// type UpdateExtraSpecOptsBuilder interface { +// ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) +// } + +// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on +// the contents of a ExtraSpecOpts. +// func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) { +// if len(opts) != 1 { +// err := eclcloud.ErrInvalidInput{} +// err.Argument = "flavors.ExtraSpecOpts" +// err.Info = "Must have 1 and only one key-value pair" +// return nil, "", err +// } + +// var key string +// for k := range opts { +// key = k +// } + +// return opts, key, nil +// } + +// // UpdateExtraSpec will updates the value of the specified flavor's extra spec +// // for the key in opts. +// func UpdateExtraSpec(client *eclcloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { +// b, key, err := opts.ToFlavorExtraSpecUpdateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// DeleteExtraSpec will delete the key-value pair with the given key for the given +// flavor ID. +// func DeleteExtraSpec(client *eclcloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { +// _, r.Err = client.Delete(extraSpecDeleteURL(client, flavorID, key), &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// IDFromName is a convienience function that returns a flavor's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &eclcloud.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &eclcloud.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/v3/ecl/compute/v2/flavors/results.go b/v3/ecl/compute/v2/flavors/results.go new file mode 100644 index 0000000..9bb77b0 --- /dev/null +++ b/v3/ecl/compute/v2/flavors/results.go @@ -0,0 +1,252 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// // CreateResult is the response of a Get operations. Call its Extract method to +// // interpret it as a Flavor. +// type CreateResult struct { +// commonResult +// } + +// GetResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// // DeleteResult is the result from a Delete operation. Call its ExtractErr +// // method to determine if the call succeeded or failed. +// type DeleteResult struct { +// eclcloud.ErrResult +// } + +// Extract provides access to the individual Flavor returned by the Get and +// Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // RxTxFactor describes bandwidth alterations of the flavor. + RxTxFactor float64 `json:"rxtx_factor"` + + // Swap is the amount of swap space, measured in MB. + Swap int `json:"-"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"os-flavor-access:is_public"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// // AccessPage contains a single page of all FlavorAccess entries for a flavor. +// type AccessPage struct { +// pagination.SinglePageBase +// } + +// // IsEmpty indicates whether an AccessPage is empty. +// func (page AccessPage) IsEmpty() (bool, error) { +// v, err := ExtractAccesses(page) +// return len(v) == 0, err +// } + +// // ExtractAccesses interprets a page of results as a slice of FlavorAccess. +// func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) { +// var s struct { +// FlavorAccesses []FlavorAccess `json:"flavor_access"` +// } +// err := (r.(AccessPage)).ExtractInto(&s) +// return s.FlavorAccesses, err +// } + +// type accessResult struct { +// eclcloud.Result +// } + +// // AddAccessResult is the response of an AddAccess operation. Call its +// // Extract method to interpret it as a slice of FlavorAccess. +// type AddAccessResult struct { +// accessResult +// } + +// // RemoveAccessResult is the response of a RemoveAccess operation. Call its +// // Extract method to interpret it as a slice of FlavorAccess. +// type RemoveAccessResult struct { +// accessResult +// } + +// // Extract provides access to the result of an access create or delete. +// // The result will be all accesses that the flavor has. +// func (r accessResult) Extract() ([]FlavorAccess, error) { +// var s struct { +// FlavorAccesses []FlavorAccess `json:"flavor_access"` +// } +// err := r.ExtractInto(&s) +// return s.FlavorAccesses, err +// } + +// // FlavorAccess represents an ACL of tenant access to a specific Flavor. +// type FlavorAccess struct { +// // FlavorID is the unique ID of the flavor. +// FlavorID string `json:"flavor_id"` + +// // TenantID is the unique ID of the tenant. +// TenantID string `json:"tenant_id"` +// } + +// // Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +// func (r extraSpecsResult) Extract() (map[string]string, error) { +// var s struct { +// ExtraSpecs map[string]string `json:"extra_specs"` +// } +// err := r.ExtractInto(&s) +// return s.ExtraSpecs, err +// } + +// // extraSpecsResult contains the result of a call for (potentially) multiple +// // key-value pairs. Call its Extract method to interpret it as a +// // map[string]interface. +// type extraSpecsResult struct { +// eclcloud.Result +// } + +// // ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// // method to interpret it as a map[string]interface. +// type ListExtraSpecsResult struct { +// extraSpecsResult +// } + +// // // CreateExtraSpecResult contains the result of a Create operation. Call its +// // // Extract method to interpret it as a map[string]interface. +// // type CreateExtraSpecsResult struct { +// // extraSpecsResult +// // } + +// // extraSpecResult contains the result of a call for individual a single +// // key-value pair. +// type extraSpecResult struct { +// eclcloud.Result +// } + +// // GetExtraSpecResult contains the result of a Get operation. Call its Extract +// // method to interpret it as a map[string]interface. +// type GetExtraSpecResult struct { +// extraSpecResult +// } + +// // // UpdateExtraSpecResult contains the result of an Update operation. Call its +// // // Extract method to interpret it as a map[string]interface. +// // type UpdateExtraSpecResult struct { +// // extraSpecResult +// // } + +// // // DeleteExtraSpecResult contains the result of a Delete operation. Call its +// // // ExtractErr method to determine if the call succeeded or failed. +// // type DeleteExtraSpecResult struct { +// // eclcloud.ErrResult +// // } + +// // Extract interprets any extraSpecResult as an ExtraSpec, if possible. +// func (r extraSpecResult) Extract() (map[string]string, error) { +// var s map[string]string +// err := r.ExtractInto(&s) +// return s, err +// } diff --git a/v3/ecl/compute/v2/flavors/urls.go b/v3/ecl/compute/v2/flavors/urls.go new file mode 100644 index 0000000..079d7ce --- /dev/null +++ b/v3/ecl/compute/v2/flavors/urls.go @@ -0,0 +1,49 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("flavors") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func accessURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-flavor-access") +} + +func accessActionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "action") +} + +func extraSpecsListURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecsGetURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecsCreateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecUpdateURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecDeleteURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} diff --git a/v3/ecl/compute/v2/images/doc.go b/v3/ecl/compute/v2/images/doc.go new file mode 100644 index 0000000..1ebc445 --- /dev/null +++ b/v3/ecl/compute/v2/images/doc.go @@ -0,0 +1,32 @@ +/* +Package images provides information and interaction with the images through +the Enterprise Cloud Compute service. + +This API is deprecated and will be removed from a future version of the Nova +API service. + +An image is a collection of files used to create or rebuild a server. +Operators provide a number of pre-built OS images by default. You may also +create custom images from cloud servers you have launched. + +Example to List Images + + listOpts := images.ListOpts{ + Limit: 2, + } + + allPages, err := images.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } +*/ +package images diff --git a/v3/ecl/compute/v2/images/requests.go b/v3/ecl/compute/v2/images/requests.go new file mode 100644 index 0000000..2e4f446 --- /dev/null +++ b/v3/ecl/compute/v2/images/requests.go @@ -0,0 +1,109 @@ +package images + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// ListDetail request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options filtering Images returned from a call to ListDetail. +type ListOpts struct { + // ChangesSince filters Images based on the last changed status (in date-time + // format). + ChangesSince string `q:"changes-since"` + + // Limit limits the number of Images to return. + Limit int `q:"limit"` + + // Mark is an Image UUID at which to set a marker. + Marker string `q:"marker"` + + // Name is the name of the Image. + Name string `q:"name"` + + // Server is the name of the Server (in URL format). + Server string `q:"server"` + + // Status is the current status of the Image. + Status string `q:"status"` + + // Type is the type of image (e.g. BASE, SERVER, ALL). + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail enumerates the available images. +func ListDetail(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns data about a specific image by its ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete deletes the specified image ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// IDFromName is a convienience function that returns an image's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractImages(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &eclcloud.ErrResourceNotFound{} + err.ResourceType = "image" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &eclcloud.ErrMultipleResourcesFound{} + err.ResourceType = "image" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/v3/ecl/compute/v2/images/results.go b/v3/ecl/compute/v2/images/results.go new file mode 100644 index 0000000..346225f --- /dev/null +++ b/v3/ecl/compute/v2/images/results.go @@ -0,0 +1,95 @@ +package images + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as an Image. +type GetResult struct { + eclcloud.Result +} + +// DeleteResult is the result from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Extract interprets a GetResult as an Image. +func (r GetResult) Extract() (*Image, error) { + var s struct { + Image *Image `json:"image"` + } + err := r.ExtractInto(&s) + return s.Image, err +} + +// Image represents an Image returned by the Compute API. +type Image struct { + // ID is the unique ID of an image. + ID string + + // Created is the date when the image was created. + Created string + + // MinDisk is the minimum amount of disk a flavor must have to be able + // to create a server based on the image, measured in GB. + MinDisk int + + // MinRAM is the minimum amount of RAM a flavor must have to be able + // to create a server based on the image, measured in MB. + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + Progress int + + // Status is the current status of the image. + Status string + + // Update is the date when the image was updated. + Updated string + + // Metadata provides free-form key/value pairs that further describe the + // image. + Metadata map[string]interface{} +} + +// ImagePage contains a single page of all Images returne from a ListDetail +// operation. Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page ImagePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"images_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image +// structs. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/v3/ecl/compute/v2/images/urls.go b/v3/ecl/compute/v2/images/urls.go new file mode 100644 index 0000000..876ded5 --- /dev/null +++ b/v3/ecl/compute/v2/images/urls.go @@ -0,0 +1,15 @@ +package images + +import "github.com/nttcom/eclcloud/v3" + +func listDetailURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/v3/ecl/compute/v2/servers/doc.go b/v3/ecl/compute/v2/servers/doc.go new file mode 100644 index 0000000..c68acb6 --- /dev/null +++ b/v3/ecl/compute/v2/servers/doc.go @@ -0,0 +1,168 @@ +/* +Package servers provides information and interaction with the server API +resource in the Enterprise Cloud Compute service. + +A server is a virtual machine instance in the compute system. In order for +one to be provisioned, a valid flavor and image are required. + +Example to List Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.List(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Get a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + server, err := servers.Get(client, serverID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", server) + +Example to Create a Server + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + result := servers.Create(computeClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a Server + + name := "update_name" + updateOpts := servers.UpdateOpts{Name: &name} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.Update(client, serverID, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.Delete(computeClient, serverID) + if result.Err != nil { + panic(err) + } + +Example to Show Metadata a server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + metadata, err := servers.Metadata(client, serverID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", metadata) + +Example to Show details for a Metadata item by key for a Server + + key := "key" + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + metadatum, err := servers.Metadatum(client, serverID, key).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", metadatum) + +Example to Create Metadata a Server + + createMetadatumOpts := servers.MetadatumOpts{"key": "value"} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.CreateMetadatum(client, serverID, createMetadatumOpts) + if err != nil { + panic(result.Err) + } + +Example to Update Metadata a Server + + updateMetadataOpts := servers.MetadataOpts{"key": "update"} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.UpdateMetadata(client, serverID, updateMetadataOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete Metadata a Server + + key := "key" + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.DeleteMetadatum(client, serverID, key) + if result.Err != nil { + panic(result.Err) + } + +Example to Reset Metadata a Server + + resetMetadataOpts := servers.MetadataOpts{"key2": "val2"} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.ResetMetadata(client, serverID, resetMetadataOpts) + if result.Err != nil { + panic(nil) + } + +Example to Resize a Server + + resizeOpts := servers.ResizeOpts{ + FlavorRef: "flavor-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.Resize(computeClient, serverID, resizeOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Snapshot a Server + + snapshotOpts := servers.CreateImageOpts{ + Name: "snapshot_name", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.CreateImage(computeClient, serverID, snapshotOpts) + if result.Err != nil { + panic(result.Err) + } + +*/ +package servers diff --git a/v3/ecl/compute/v2/servers/errors.go b/v3/ecl/compute/v2/servers/errors.go new file mode 100644 index 0000000..b513aeb --- /dev/null +++ b/v3/ecl/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ eclcloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ eclcloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ eclcloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ eclcloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ eclcloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ eclcloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ eclcloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + eclcloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e eclcloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/v3/ecl/compute/v2/servers/requests.go b/v3/ecl/compute/v2/servers/requests.go new file mode 100644 index 0000000..dee6836 --- /dev/null +++ b/v3/ecl/compute/v2/servers/requests.go @@ -0,0 +1,573 @@ +package servers + +import ( + "encoding/base64" + // "encoding/json" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/flavors" + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/images" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Host is the name of the host as a string. + Host string `q:"host"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // AllTenants is a bool to show all tenants. + AllTenants bool `q:"all_tenants"` + + // TenantID lists servers for a particular tenant. + // Setting "AllTenants = true" is required. + TenantID string `q:"tenant_id"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list servers accessible to you. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network +// attachments. +type Network struct { + // UUID of a network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// Personality is an array of files that are injected into the server at launch. +// type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the +// server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild +// operation is requested, json.Marshal will call File's MarshalJSON method. +// type File struct { +// // Path of the file. +// Path string + +// // Contents of the file. Maximum content size is 255 bytes. +// Contents []byte +// } + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +// func (f *File) MarshalJSON() ([]byte, error) { +// file := struct { +// Path string `json:"path"` +// Contents string `json:"contents"` +// }{ +// Path: f.Path, +// Contents: base64.StdEncoding.EncodeToString(f.Contents), +// } +// return json.Marshal(file) +// } + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef [optional; required if ImageName is not provided] is the ID or + // full URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // ImageName [optional; required if ImageRef is not provided] is the name of + // the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string `json:"-"` + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string `json:"-"` + + // SecurityGroups lists the names of the security groups to which this server + // should belong. + // SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the + // tenant. + Networks []Network `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the + // server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + // Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + // AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 pecifies an IPv6 address for the instance. + // AccessIPv6 string `json:"accessIPv6,omitempty"` + + // ServiceClient will allow calls to be made to retrieve an image or + // flavor ID by name. + ServiceClient *eclcloud.ServiceClient `json:"-"` +} + +// ToServerCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + sc := opts.ServiceClient + opts.ServiceClient = nil + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + // if len(opts.SecurityGroups) > 0 { + // securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + // for i, groupName := range opts.SecurityGroups { + // securityGroups[i] = map[string]interface{}{"name": groupName} + // } + // b["security_groups"] = securityGroups + // } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + b["networks"] = networks + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageRef == "" { + if opts.ImageName != "" { + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(sc, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if opts.FlavorRef == "" { + if opts.FlavorName == "" { + err := ErrNeitherFlavorIDNorFlavorNameProvided{} + err.Argument = "FlavorRef/FlavorName" + return nil, err + } + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + flavorID, err := flavors.IDFromName(sc, opts.FlavorName) + if err != nil { + return nil, err + } + b["flavorRef"] = flavorID + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get requests details on a single server, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name *string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + // AccessIPv4 *string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + // AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResizeOptsBuilder allows extensions to add additional parameters to the +// resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize +// operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON +// request body for the Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// +// Note that this implies rebuilding it. +// +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in VERIFY_RESIZE state. +// While in this state, you can explore the use of the new server's +// configuration. If you like it, call ConfirmResize() to commit the resize +// permanently. Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *eclcloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// 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 *eclcloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *eclcloud.ServiceClient, id string) (r GetMetadataResult) { + _, r.Err = client.Get(metadataURL(client, id), &r.Body, nil) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for +// the given server ID. This operation does not affect already-existing metadata +// that is not specified by opts. +func UpdateMetadata(client *eclcloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the +// contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := eclcloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key +// for the given server ID. +func CreateMetadatum(client *eclcloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadatum requests the key-value pair with the given key for the given +// server ID. +func Metadatum(client *eclcloud.ServiceClient, id, key string) (r GetMetadatumResult) { + _, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the +// given server ID. +func DeleteMetadatum(client *eclcloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + _, r.Err = client.Delete(metadatumURL(client, id, key), nil) + return +} + +// CreateImageOptsBuilder allows extensions to add additional parameters to the +// CreateImage request. +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts provides options to pass to the CreateImage request. +type CreateImageOpts struct { + // Name of the image/snapshot. + Name string `json:"name" required:"true"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to + // the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request +// body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be +// created of the server +func CreateImage(client *eclcloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + r.Err = err + r.Header = resp.Header + return +} + +// IDFromName is a convienience function that returns a server's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + allPages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractServers(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "server"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"} + } +} diff --git a/v3/ecl/compute/v2/servers/results.go b/v3/ecl/compute/v2/servers/results.go new file mode 100644 index 0000000..087f197 --- /dev/null +++ b/v3/ecl/compute/v2/servers/results.go @@ -0,0 +1,295 @@ +package servers + +import ( + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type serverResult struct { + eclcloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + serverResult +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Server. +type GetResult struct { + serverResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + serverResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ActionResult represents the result of server action operations, like reboot. +// Call its ExtractErr method to determine if the action succeeded or failed. +type ActionResult struct { + eclcloud.ErrResult +} + +// CreateImageResult is the response from a CreateImage operation. Call its +// ExtractImageID method to retrieve the ID of the newly created image. +type CreateImageResult struct { + eclcloud.Result +} + +// ExtractImageID gets the ID of the newly created server image from the header. +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Server represents a server/instance in the Enterprise Cloud. +type Server struct { + // ID uniquely identifies this server amongst all other servers, + // including those not accessible to the current tenant. + ID string `json:"id"` + + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + + // Name contains the human-readable name for the server. + Name string `json:"name"` + + // Updated and Created contain ISO-8601 timestamps of when the state of the + // server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + + // HostID is the host where the server is located in the cloud. + HostID string `json:"hostid"` + + // Status contains the current operational status of the server, + // such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, + // suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + // AccessIPv6 string `json:"accessIPv6"` + + // Image refers to a JSON object, which itself indicates the OS image used to + // deploy the server. + Image map[string]interface{} `json:"-"` + + // Flavor refers to a JSON object, which itself indicates the hardware + // configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + + // Addresses includes a list of all IP addresses assigned to the server, + // keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the server. + Metadata map[string]string `json:"metadata"` + + // Links includes HTTP references to the itself, useful for passing along to + // other APIs that might want a server reference. + Links []interface{} `json:"links"` + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the + // administrative password chosen when provisioning a new server without a + // set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied + // to it. + SecurityGroups []map[string]interface{} `json:"security_groups"` + + // Fault contains failure information about a server. + Fault Fault `json:"fault"` + + // ConfigDrive is the name of the server's config drive. + ConfigDrive string `json:"config_drive"` +} + +type Fault struct { + Code int `json:"code"` + Created time.Time `json:"created"` + Details string `json:"details"` + Message string `json:"message"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against +// the API. As Enterprise Cloud extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type MetadataResult struct { + eclcloud.Result +} + +// GetMetadataResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult contains the result of a Reset operation. Call its +// Extract method to interpret it as a map[string]interface. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single +// key-value pair. +type MetadatumResult struct { + eclcloud.Result +} + +// GetMetadatumResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteMetadatumResult struct { + eclcloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} diff --git a/v3/ecl/compute/v2/servers/testing/doc.go b/v3/ecl/compute/v2/servers/testing/doc.go new file mode 100644 index 0000000..29b7613 --- /dev/null +++ b/v3/ecl/compute/v2/servers/testing/doc.go @@ -0,0 +1,2 @@ +// Compute Server unit tests +package testing diff --git a/v3/ecl/compute/v2/servers/testing/fixtures.go b/v3/ecl/compute/v2/servers/testing/fixtures.go new file mode 100644 index 0000000..20b59c8 --- /dev/null +++ b/v3/ecl/compute/v2/servers/testing/fixtures.go @@ -0,0 +1,836 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/servers" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" + + "time" +) + +// ListResult provides a single page of Server results. +const ListResult = ` +{ + "servers": [ + { + "id": "707dbd55-b6bf-439d-804c-3002f49ac898", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "bookmark" + } + ], + "name": "Test Server2" + }, + { + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "name": "Test Server1" + } + ] +} +` + +// ListDetailsResult provides a single page of Server results in details. +const ListDetailsResult = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:f3:ed:05", + "version": 4, + "addr": "192.168.1.103", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2020-05-11T06:25:56.000000", + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "707dbd55-b6bf-439d-804c-3002f49ac898", + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "zone1_groupa", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Test Server2", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "True", + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + }, + { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:49:78:28", + "version": 4, + "addr": "192.168.1.101", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2020-05-11T03:36:27.000000", + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "zone1_groupa", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Test Server1", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:49:78:28", + "version": 4, + "addr": "192.168.1.101", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2020-05-11T03:36:27.000000", + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "zone1_groupa", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Test Server1", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "server": { + "flavorRef": "1CPU-4GB", + "imageRef": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "name": "Test Server1", + "availability_zone": "zone1-groupa", + "config_drive": true, + "user_data": "dXNlcl9kYXRh", + "metadata": { + "foo": "bar" + }, + "networks": [ + { + "uuid": "4d98b876-b5d1-4861-8650-b5a53024486a" + } + ] + } +} +` + +const CreateResponse = ` +{ + "server": { + "security_groups": [ + { + "name": "default" + } + ], + "OS-DCF:diskConfig": "MANUAL", + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "adminPass": "aabbccddeeff" + } +} +` + +const UpdateRequest = ` +{ + "server": { + "name": "Update Name" + } +} +` + +const UpdateResponse = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "version": 4, + "addr": "192.168.1.101" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Update Name", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + } +} +` + +var MetadataResult = ` +{ + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } +} +` + +var MetadatumResult = ` +{ + "meta": { + "vmha": "false" + } +} +` + +var CreateMetadatumRequest = ` +{ + "meta": { + "key1": "val1" + } +} +` + +var CreateMetadatumResponse = CreateMetadatumRequest + +var UpdateMetadataRequest = ` +{ + "metadata": { + "key1": "update_val" + } +} +` + +var UpdateMetadataResponse = UpdateMetadataRequest + +var ResetMetadataRequest = ` +{ + "metadata": { + "key1": "val1", + "key2": "val2" + } +} +` + +var ResetMetadataResponse = ResetMetadataRequest + +var ResizeRequest = ` +{ + "resize": { + "flavorRef": "2CPU-8GB" + } +} +` + +var CreateImageRequest = ` +{ + "createImage": { + "metadata": { + "key": "create_image" + }, + "name": "Test Create Image" + } +} +` + +var expectedServers = []servers.Server{expectedServer1, expectedServer2} + +var expectedCreated, _ = time.Parse(eclcloud.RFC3339Milli, "2020-05-11T06:25:53Z") +var expectedUpdated, _ = time.Parse(eclcloud.RFC3339Milli, "2020-05-18T01:51:41Z") + +var expectedServer1 = servers.Server{ + ID: "707dbd55-b6bf-439d-804c-3002f49ac898", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + UserID: "5e86848fbc63403daaeffc1b76b3a784", + Name: "Test Server2", + Updated: expectedUpdated, + Created: expectedCreated, + HostID: "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + Status: "ACTIVE", + Progress: 0, + AccessIPv4: "", + Image: map[string]interface{}{ + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1CPU-4GB", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "IF-4831": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:f3:ed:05", + "OS-EXT-IPS:type": "fixed", + "addr": "192.168.1.103", + "version": float64(4), + }, + }, + }, + Metadata: map[string]string{ + "vmha": "false", + "HA_Enabled": "false", + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "self", + }, + }, + AdminPass: "", + SecurityGroups: nil, + Fault: servers.Fault{}, + ConfigDrive: "True", +} + +var expectedServer2 = servers.Server{ + ID: "8e69a092-53f9-4225-bae6-57cfbb5d6857", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + UserID: "5e86848fbc63403daaeffc1b76b3a784", + Name: "Test Server1", + Updated: expectedUpdated, + Created: expectedCreated, + HostID: "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + Status: "ACTIVE", + Progress: 0, + AccessIPv4: "", + Image: map[string]interface{}{ + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1CPU-4GB", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "IF-4831": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:49:78:28", + "OS-EXT-IPS:type": "fixed", + "addr": "192.168.1.101", + "version": float64(4), + }, + }, + }, + Metadata: map[string]string{ + "vmha": "false", + "HA_Enabled": "false", + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self", + }, + }, + //KeyName: nil, + AdminPass: "", + SecurityGroups: nil, + Fault: servers.Fault{}, + ConfigDrive: "", +} + +var serverNameUpdated = servers.Server{ + ID: "8e69a092-53f9-4225-bae6-57cfbb5d6857", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + UserID: "5e86848fbc63403daaeffc1b76b3a784", + Name: "Update Name", + Updated: expectedUpdated, + Created: expectedCreated, + HostID: "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + Status: "ACTIVE", + Progress: 0, + AccessIPv4: "", + Image: map[string]interface{}{ + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1CPU-4GB", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "IF-4831": []interface{}{ + map[string]interface{}{ + "addr": "192.168.1.101", + "version": float64(4), + }, + }, + }, + Metadata: map[string]string{ + "vmha": "false", + "HA_Enabled": "false", + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self", + }, + }, + AdminPass: "", + SecurityGroups: nil, + Fault: servers.Fault{}, + ConfigDrive: "", +} + +var expectMetadata = map[string]string{ + "vmha": "false", + "HA_Enabled": "false", +} + +var expectMetadatum = map[string]string{ + "vmha": "false", +} + +var expectCreateMetadatum = map[string]string{ + "key1": "val1", +} + +var ecpectUpdateMetadata = map[string]string{ + "key1": "update_val", +} + +var expectResetMetadata = map[string]string{ + "key1": "val1", + "key2": "val2", +} + +// HandleListServersSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a list of two servers. +func HandleListServersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers", 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, ListResult) + }) +} + +// HandleListServersDetailsSuccessfully creates an HTTP handler at `/servers/detail` on the +// test handler mux that responds with a list of two servers. +func HandleListServersDetailsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", 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, ListDetailsResult) + }) +} + +// HandleGetServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a single server. +func HandleGetServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", expectedServer2.ID), 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, GetResult) + }) +} + +// HandleCreateServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server creation. +func HandleCreateServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, CreateResponse) + }) +} + +// HandleDeleteServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server deletion. +func HandleDeleteServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", expectedServer1.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + 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.StatusNoContent) + }) +} + +// HandleUpdateServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server update. +func HandleUpdateServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateResponse) + }) +} + +// HandleGetMetadataSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a server metadata. +func HandleGetMetadataSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata", expectedServer2.ID), 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.WriteHeader(http.StatusOK) + fmt.Fprintf(w, MetadataResult) + }) +} + +// HandleGetMetadatumSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a server metadatum. +func HandleGetMetadatumSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata/vmha", expectedServer2.ID), 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.WriteHeader(http.StatusOK) + fmt.Fprintf(w, MetadatumResult) + }) +} + +// HandleCreateMetadatumSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata creation. +func HandleCreateMetadatumSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata/key1", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateMetadatumRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, CreateMetadatumResponse) + }) +} + +// HandleDeleteMetadatumSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata deletion. +func HandleDeleteMetadatumSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata/vmha", expectedServer1.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + 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.StatusNoContent) + }) +} + +// HandleUpdateMetadataSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata update. +func HandleUpdateMetadataSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, UpdateMetadataRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateMetadataResponse) + }) +} + +// HandleResetMetadataSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata reset. +func HandleResetMetadataSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ResetMetadataRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ResetMetadataResponse) + }) +} + +// HandleResizeServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server resize action. +func HandleResizeServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ResizeRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleCreateImageSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests create server image. +func HandleCreateImageSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateImageRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/v3/ecl/compute/v2/servers/testing/requests_test.go b/v3/ecl/compute/v2/servers/testing/requests_test.go new file mode 100644 index 0000000..1f471e6 --- /dev/null +++ b/v3/ecl/compute/v2/servers/testing/requests_test.go @@ -0,0 +1,197 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/compute/v2/servers" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + count := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expectedServers, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListServersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + allPages, err := servers.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectedServers, actual) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetServerSuccessfully(t) + + actual, err := servers.Get(client.ServiceClient(), expectedServer2.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectedServer2, *actual) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerSuccessfully(t) + + configDrive := true + createOpts := servers.CreateOpts{ + Name: "Test Server1", + ImageRef: "c11a6d55-70e9-4d04-a086-4451f07da0d7", + FlavorRef: "1CPU-4GB", + UserData: []byte("user_data"), + AvailabilityZone: "zone1-groupa", + Networks: []servers.Network{ + { + UUID: "4d98b876-b5d1-4861-8650-b5a53024486a", + }, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + ConfigDrive: &configDrive, + } + + actual, err := servers.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedServer2.ID, actual.ID) + th.AssertDeepEquals(t, expectedServer2.Links, actual.Links) + th.AssertEquals(t, "aabbccddeeff", actual.AdminPass) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteServerSuccessfully(t) + + res := servers.Delete(client.ServiceClient(), expectedServer1.ID) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateServerSuccessfully(t) + + name := "Update Name" + updateOpts := servers.UpdateOpts{Name: &name} + + actual, err := servers.Update(client.ServiceClient(), expectedServer2.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, serverNameUpdated, *actual) +} + +func TestGetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetMetadataSuccessfully(t) + + actual, err := servers.Metadata(client.ServiceClient(), expectedServer2.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectMetadata, actual) +} + +func TestGetMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetMetadatumSuccessfully(t) + + actual, err := servers.Metadatum(client.ServiceClient(), expectedServer2.ID, "vmha").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectMetadatum, actual) +} + +func TestCreateMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateMetadatumSuccessfully(t) + + createOpts := servers.MetadatumOpts{"key1": "val1"} + + actual, err := servers.CreateMetadatum(client.ServiceClient(), expectedServer2.ID, createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectCreateMetadatum, actual) +} + +func TestDeleteMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteMetadatumSuccessfully(t) + + res := servers.DeleteMetadatum(client.ServiceClient(), expectedServer1.ID, "vmha") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateMetadataSuccessfully(t) + + updateOpts := servers.MetadataOpts{"key1": "update_val"} + + actual, err := servers.UpdateMetadata(client.ServiceClient(), expectedServer2.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ecpectUpdateMetadata, actual) +} + +func TestResetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleResetMetadataSuccessfully(t) + + createOpts := servers.MetadataOpts{ + "key1": "val1", + "key2": "val2", + } + + actual, err := servers.ResetMetadata(client.ServiceClient(), expectedServer2.ID, createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectResetMetadata, actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleResizeServerSuccessfully(t) + + resizeOpts := servers.ResizeOpts{FlavorRef: "2CPU-8GB"} + + err := servers.Resize(client.ServiceClient(), expectedServer2.ID, resizeOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestCreateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateImageSuccessfully(t) + + snapshotOpts := servers.CreateImageOpts{ + Name: "Test Create Image", + Metadata: map[string]string{"key": "create_image"}, + } + + result := servers.CreateImage(client.ServiceClient(), expectedServer2.ID, snapshotOpts) + th.AssertNoErr(t, result.Err) +} diff --git a/v3/ecl/compute/v2/servers/urls.go b/v3/ecl/compute/v2/servers/urls.go new file mode 100644 index 0000000..4650302 --- /dev/null +++ b/v3/ecl/compute/v2/servers/urls.go @@ -0,0 +1,39 @@ +package servers + +import "github.com/nttcom/eclcloud/v3" + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *eclcloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} diff --git a/v3/ecl/compute/v2/servers/util.go b/v3/ecl/compute/v2/servers/util.go new file mode 100644 index 0000000..15823b5 --- /dev/null +++ b/v3/ecl/compute/v2/servers/util.go @@ -0,0 +1,21 @@ +package servers + +import "github.com/nttcom/eclcloud/v3" + +// 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 *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v3/ecl/computevolume/extensions/volumeactions/doc.go b/v3/ecl/computevolume/extensions/volumeactions/doc.go new file mode 100644 index 0000000..5604976 --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/doc.go @@ -0,0 +1,86 @@ +/* +Package volumeactions provides information and interaction with volumes in the +Enterprise Cloud Block Storage service. A volume is a detachable block storage +device, akin to a USB hard drive. + +Example of Attaching a Volume to an Instance + + attachOpts := volumeactions.AttachOpts{ + MountPoint: "/mnt", + Mode: "rw", + InstanceUUID: server.ID, + } + + err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr() + if err != nil { + panic(err) + } + + detachOpts := volumeactions.DetachOpts{ + AttachmentID: volume.Attachments[0].AttachmentID, + } + + err = volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr() + if err != nil { + panic(err) + } + + +Example of Creating an Image from a Volume + + uploadImageOpts := volumeactions.UploadImageOpts{ + ImageName: "my_vol", + Force: true, + } + + volumeImage, err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", volumeImage) + +Example of Extending a Volume's Size + + extendOpts := volumeactions.ExtendSizeOpts{ + NewSize: 100, + } + + err := volumeactions.ExtendSize(client, volume.ID, extendOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Initializing a Volume Connection + + connectOpts := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: eclcloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + connectionInfo, err := volumeactions.InitializeConnection(client, volume.ID, connectOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", connectionInfo["data"]) + + terminateOpts := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: eclcloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + err = volumeactions.TerminateConnection(client, volume.ID, terminateOpts).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeactions diff --git a/v3/ecl/computevolume/extensions/volumeactions/requests.go b/v3/ecl/computevolume/extensions/volumeactions/requests.go new file mode 100644 index 0000000..9fe845d --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/requests.go @@ -0,0 +1,84 @@ +package volumeactions + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// ExtendSizeOptsBuilder allows extensions to add additional parameters to the +// ExtendSize request. +type ExtendSizeOptsBuilder interface { + ToVolumeExtendSizeMap() (map[string]interface{}, error) +} + +// ExtendSizeOpts contains options for extending the size of an existing Volume. +// This object is passed to the volumes.ExtendSize function. +type ExtendSizeOpts struct { + // NewSize is the new size of the volume, in GB. + NewSize int `json:"new_size" required:"true"` +} + +// ToVolumeExtendSizeMap assembles a request body based on the contents of an +// ExtendSizeOpts. +func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "os-extend") +} + +// ExtendSize will extend the size of the volume based on the provided information. +// This operation does not return a response body. +func ExtendSize(client *eclcloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) { + b, err := opts.ToVolumeExtendSizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// UploadImageOptsBuilder allows extensions to add additional parameters to the +// UploadImage request. +type UploadImageOptsBuilder interface { + ToVolumeUploadImageMap() (map[string]interface{}, error) +} + +// UploadImageOpts contains options for uploading a Volume to image storage. +type UploadImageOpts struct { + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format,omitempty"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format,omitempty"` + + // The name of image that will be stored in glance. + ImageName string `json:"image_name,omitempty"` + + // Force image creation, usable if volume attached to instance. + Force bool `json:"force,omitempty"` +} + +// ToVolumeUploadImageMap assembles a request body based on the contents of a +// UploadImageOpts. +func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "os-volume_upload_image") +} + +// UploadImage will upload an image based on the values in UploadImageOptsBuilder. +func UploadImage(client *eclcloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) { + b, err := opts.ToVolumeUploadImageMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// ForceDelete will delete the volume regardless of state. +func ForceDelete(client *eclcloud.ServiceClient, id string) (r ForceDeleteResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil) + return +} diff --git a/v3/ecl/computevolume/extensions/volumeactions/results.go b/v3/ecl/computevolume/extensions/volumeactions/results.go new file mode 100644 index 0000000..7a1cff1 --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/results.go @@ -0,0 +1,139 @@ +package volumeactions + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" +) + +// UploadImageResult contains the response body and error from an UploadImage +// request. +type UploadImageResult struct { + eclcloud.Result +} + +// ExtendSizeResult contains the response body and error from an ExtendSize request. +type ExtendSizeResult struct { + eclcloud.ErrResult +} + +// ImageVolumeType contains volume type information obtained from UploadImage +// action. +type ImageVolumeType struct { + // The ID of a volume type. + ID string `json:"id"` + + // Human-readable display name for the volume type. + Name string `json:"name"` + + // Human-readable description for the volume type. + Description string `json:"display_description"` + + // Flag for public access. + IsPublic bool `json:"is_public"` + + // Extra specifications for volume type. + ExtraSpecs map[string]interface{} `json:"extra_specs"` + + // ID of quality of service specs. + QosSpecsID string `json:"qos_specs_id"` + + // Flag for deletion status of volume type. + Deleted bool `json:"deleted"` + + // The date when volume type was deleted. + DeletedAt time.Time `json:"-"` + + // The date when volume type was created. + CreatedAt time.Time `json:"-"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` +} + +func (r *ImageVolumeType) UnmarshalJSON(b []byte) error { + type tmp ImageVolumeType + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + DeletedAt eclcloud.JSONRFC3339MilliNoZ `json:"deleted_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ImageVolumeType(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.DeletedAt = time.Time(s.DeletedAt) + + return err +} + +// VolumeImage contains information about volume uploaded to an image service. +type VolumeImage struct { + // The ID of a volume an image is created from. + VolumeID string `json:"id"` + + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format"` + + // Human-readable description for the volume. + Description string `json:"display_description"` + + // The ID of the created image. + ImageID string `json:"image_id"` + + // Human-readable display name for the image. + ImageName string `json:"image_name"` + + // Size of the volume in GB. + Size int `json:"size"` + + // Current status of the volume. + Status string `json:"status"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` + + // Volume type object of used volume. + VolumeType ImageVolumeType `json:"volume_type"` +} + +func (r *VolumeImage) UnmarshalJSON(b []byte) error { + type tmp VolumeImage + var s struct { + tmp + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VolumeImage(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// Extract will get an object with info about the uploaded image out of the +// UploadImageResult object. +func (r UploadImageResult) Extract() (VolumeImage, error) { + var s struct { + VolumeImage VolumeImage `json:"os-volume_upload_image"` + } + err := r.ExtractInto(&s) + return s.VolumeImage, err +} + +// ForceDeleteResult contains the response body and error from a ForceDelete request. +type ForceDeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/computevolume/extensions/volumeactions/testing/doc.go b/v3/ecl/computevolume/extensions/volumeactions/testing/doc.go new file mode 100644 index 0000000..336406d --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/testing/doc.go @@ -0,0 +1,2 @@ +// volumeactions unit tests +package testing diff --git a/v3/ecl/computevolume/extensions/volumeactions/testing/fixtures.go b/v3/ecl/computevolume/extensions/volumeactions/testing/fixtures.go new file mode 100644 index 0000000..c643b80 --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/testing/fixtures.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" +) + +const volumeID = "ff2ac0fd-ea58-4e15-bd71-aec0bc58c469" +const instanceID = "ff2ac0fd-ea58-4e15-bd71-aec0bc58c469" + +const uploadImageRequest = `{ + "os-volume_upload_image": { + "container_format": "bare", + "force": true, + "image_name": "imagetest", + "disk_format": "raw" + } +}` + +var uploadImageResponse = fmt.Sprintf(`{ + "os-volume_upload_image": { + "status": "uploading", + "image_id": "49d7efe7-975e-46d7-af0a-fd94fe8e62bf", + "image_name": "imagetest", + "volume_type": { + "name": "nfsdriver", + "qos_specs_id": null, + "deleted": false, + "created_at": "2018-06-04T08:05:09.000000", + "updated_at": null, + "deleted_at": null, + "id": "1f02ea8f-3823-4e69-a232-695adc39f5e0" + }, + "container_format": "bare", + "size": 40, + "disk_format": "raw", + "id": "%s", + "display_description": "test volume 2update", + "updated_at": "2019-02-06T22:06:27.000000" + } +}`, + volumeID, +) + +const extendRequest = `{ + "os-extend": + { + "new_size": 40 + } +}` diff --git a/v3/ecl/computevolume/extensions/volumeactions/testing/requests_test.go b/v3/ecl/computevolume/extensions/volumeactions/testing/requests_test.go new file mode 100644 index 0000000..2c86957 --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/testing/requests_test.go @@ -0,0 +1,88 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/computevolume/extensions/volumeactions" + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestVolumeUploadImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s/action", volumeID) + + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, uploadImageRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, uploadImageResponse) + }) + + options := &volumeactions.UploadImageOpts{ + ContainerFormat: "bare", + Force: true, + ImageName: "imagetest", + DiskFormat: "raw", + } + + actual, err := volumeactions.UploadImage(fakeclient.ServiceClient(), volumeID, options).Extract() + th.AssertNoErr(t, err) + + expected := volumeactions.VolumeImage{ + Status: "uploading", + ImageID: "49d7efe7-975e-46d7-af0a-fd94fe8e62bf", + ImageName: "imagetest", + VolumeType: volumeactions.ImageVolumeType{ + Name: "nfsdriver", + QosSpecsID: "", + Deleted: false, + CreatedAt: time.Date(2018, 6, 4, 8, 5, 9, 0, time.UTC), + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + ID: "1f02ea8f-3823-4e69-a232-695adc39f5e0", + }, + ContainerFormat: "bare", + Size: 40, + DiskFormat: "raw", + VolumeID: volumeID, + Description: "test volume 2update", + UpdatedAt: time.Date(2019, 2, 6, 22, 6, 27, 0, time.UTC), + } + th.AssertDeepEquals(t, expected, actual) +} + +func TestVolumeExtendSize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s/action", volumeID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, extendRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) + + options := &volumeactions.ExtendSizeOpts{ + NewSize: 40, + } + + err := volumeactions.ExtendSize(fakeclient.ServiceClient(), volumeID, options).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v3/ecl/computevolume/extensions/volumeactions/urls.go b/v3/ecl/computevolume/extensions/volumeactions/urls.go new file mode 100644 index 0000000..b6d2368 --- /dev/null +++ b/v3/ecl/computevolume/extensions/volumeactions/urls.go @@ -0,0 +1,7 @@ +package volumeactions + +import "github.com/nttcom/eclcloud/v3" + +func actionURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id, "action") +} diff --git a/v3/ecl/computevolume/v2/volumes/doc.go b/v3/ecl/computevolume/v2/volumes/doc.go new file mode 100644 index 0000000..98fdc6b --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// Enterprise Cloud 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/v3/ecl/computevolume/v2/volumes/requests.go b/v3/ecl/computevolume/v2/volumes/requests.go new file mode 100644 index 0000000..35dfeab --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/requests.go @@ -0,0 +1,207 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/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 { + // The size of the volume, in GB + Size int `json:"size" required:"true"` + // The availability zone + AvailabilityZone string `json:"availability_zone,omitempty"` + // ConsistencyGroupID is the ID of a consistency group + ConsistencyGroupID string `json:"consistencygroup_id,omitempty"` + // The volume description + Description string `json:"description,omitempty"` + // One or more metadata key and value pairs to associate with the volume + Metadata map[string]string `json:"metadata,omitempty"` + // The volume name + Name string `json:"name,omitempty"` + // the ID of the existing volume snapshot + SnapshotID string `json:"snapshot_id,omitempty"` + // SourceReplica is a UUID of an existing volume to replicate with + SourceReplica string `json:"source_replica,omitempty"` + // the ID of the existing volume + SourceVolID string `json:"source_volid,omitempty"` + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string `json:"imageRef,omitempty"` + // The associated volume type + VolumeType string `json:"volume_type,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// 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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// 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 { + // AllTenants will retrieve volumes of all tenants/projects. + AllTenants bool `q:"all_tenants"` + + // Metadata will filter results based on specified metadata. + Metadata map[string]string `q:"metadata"` + + // Name will filter by the specified volume name. + Name string `q:"name"` + + // Status will filter by the specified status. + Status string `q:"status"` + + // TenantID will filter by a specific tenant/project ID. + // Setting AllTenants is required for this. + TenantID string `q:"project_id"` + + // Comma-separated list of sort keys and optional sort directions in the + // form of [:]. + Sort string `q:"sort"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` + + // The ID of the last-seen item. + Marker string `q:"marker"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.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 + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// 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 { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Metadata *map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/v3/ecl/computevolume/v2/volumes/results.go b/v3/ecl/computevolume/v2/volumes/results.go new file mode 100644 index 0000000..e7066fa --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/results.go @@ -0,0 +1,169 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type Attachment struct { + AttachedAt time.Time `json:"-"` + AttachmentID string `json:"attachment_id"` + Device string `json:"device"` + HostName string `json:"host_name"` + ID string `json:"id"` + ServerID string `json:"server_id"` + VolumeID string `json:"volume_id"` +} + +func (r *Attachment) UnmarshalJSON(b []byte) error { + type tmp Attachment + var s struct { + tmp + AttachedAt eclcloud.JSONRFC3339MilliNoZ `json:"attached_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Attachment(s.tmp) + + r.AttachedAt = time.Time(s.AttachedAt) + + return err +} + +// Volume contains all the information associated with an Enterprise Cloud Volume. +type Volume struct { + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Size of the volume in GB. + Size int `json:"size"` + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Instances onto which the volume is attached. + Attachments []Attachment `json:"attachments"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // UserID is the id of the user who created the volume. + UserID string `json:"user_id"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // Encrypted denotes if the volume is encrypted. + Encrypted bool `json:"encrypted"` + // ReplicationStatus is the status of replication. + ReplicationStatus string `json:"replication_status"` + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `json:"consistencygroup_id"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach"` + // TenantID is the id of the project that owns the volume. + TenantID string `json:"os-vol-tenant-attr:tenant_id"` +} + +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r VolumePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"volumes_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// 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 +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/computevolume/v2/volumes/testing/doc.go b/v3/ecl/computevolume/v2/volumes/testing/doc.go new file mode 100644 index 0000000..100d586 --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/testing/doc.go @@ -0,0 +1,2 @@ +// volumes_v2 unittest +package testing diff --git a/v3/ecl/computevolume/v2/volumes/testing/fixtures.go b/v3/ecl/computevolume/v2/volumes/testing/fixtures.go new file mode 100644 index 0000000..130f261 --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/testing/fixtures.go @@ -0,0 +1,358 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/computevolume/v2/volumes" +) + +const idVolume1 = "251df9eb-c088-4e71-808b-75a690e8814b" +const idVolume2 = "7e0b432b-c922-49d7-b85a-28ac88164328" + +const sizeVolume1 = 40 + +const nameVolume1 = "volume1" +const nameVolume1Update = "volume1-update" + +const descriptionVolume1 = "test volume 1" +const descriptionVolume1Update = "test volume 1-update" + +const instanceID = "83ec2e3b-4321-422b-8706-a84185f52a0a" +const tenantID = "9ee80f2a926c49f88f166af47df4e9f5" +const az = "zone1-groupa" + +const createdAt = "2019-02-06T08:06:57.000000" + +var timeCreatedAt = time.Date(2019, 2, 6, 8, 6, 57, 0, time.UTC) + +var listResponse = fmt.Sprintf(`{ + "volumes": [{ + "id": "%s", + "name": "%s", + "status": "in-use", + "size": %d, + "availability_zone": "%s", + "created_at": "%s", + "os-vol-tenant-attr:tenant_id": "%s", + "description": "%s", + "attachments": [{ + "host_name": null, + "device": "/dev/vdb", + "server_id": "%s", + "id": "%s", + "volume_id": "%s" + }], + "links": [{ + "href": "dummy_self_link", + "rel": "self" + }, { + "href": "dummy_bookmark_link", + "rel": "bookmark" + }], + "encrypted": false, + "os-volume-replication:extended_status": null, + "volume_type": "nfsdriver", + "snapshot_id": null, + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "metadata": { + "readonly": "False", + "attached_mode": "rw" + }, + "volume_image_metadata": { + ".edition": "none", + ".major.version": "7", + ".official_image_template": "CentOS-7.1-1503_64_virtual-server_12", + "container_format": "bare", + "min_ram": "0", + "disk_format": "qcow2", + ".is_license": "False", + "image_name": "CentOS-7.1-1503_64_virtual-server_12", + "image_id": "df1944a7-ca45-4709-9ec6-e31664133650", + ".os.type": "centos", + ".enable.download": "True", + "checksum": "a828b6ba68b9d13d2da0a0cb3cfaa950", + "min_disk": "15", + ".service.type": "virtual-server", + ".virtual_server.os.pod": "other", + ".minor.version": "1-1503", + "size": "461504512" + }, + "source_volid": null, + "consistencygroup_id": null, + "bootable": "true", + "os-volume-replication:driver_data": null, + "replication_status": "disabled" + }, { + "id": "%s", + "name": "volume 2", + "status": "available", + "size": 40, + "availability_zone": "%s", + "created_at": "%s", + "os-vol-tenant-attr:tenant_id": "%s", + "description": "test volume 2", + "attachments": [], + "links": [{ + "href": "dummy_self_link", + "rel": "self" + }, { + "href": "dummy_bookmark_link", + "rel": "bookmark" + }], + "encrypted": false, + "os-volume-replication:extended_status": null, + "volume_type": "nfsdriver", + "snapshot_id": null, + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "metadata": {}, + "volume_image_metadata": { + ".edition": "none", + ".major.version": "7", + "container_format": "bare", + "min_ram": "0", + "disk_format": "qcow2", + ".is_license": "True", + "image_name": "RedHatEnterpriseLinux-7.1_64_include-license_virtual-server_42", + "image_id": "f304bc07-056a-406f-85fc-9f97c7b8ef95", + ".os.type": "rhel", + ".enable.download": "False", + "checksum": "85851188a680c5bddecb664914917a81", + "min_disk": "40", + ".service.type": "virtual-server", + ".virtual_server.os.pod": "rhel", + ".minor.version": "1", + "size": "515899392" + }, + "source_volid": null, + "consistencygroup_id": null, + "bootable": "true", + "os-volume-replication:driver_data": null, + "replication_status": "disabled" + }] +}`, + // For volume 1 + idVolume1, + nameVolume1, + sizeVolume1, + az, + createdAt, + tenantID, + descriptionVolume1, + instanceID, + idVolume1, + idVolume1, + // For volume 2 + idVolume2, + az, + createdAt, + tenantID, +) + +var structVolume1 = volumes.Volume{ + ID: idVolume1, + Status: "in-use", + Size: sizeVolume1, + AvailabilityZone: az, + CreatedAt: timeCreatedAt, + Attachments: []volumes.Attachment{{ + // AttachedAt: time.Date(2016, 8, 6, 14, 48, 20, 0, time.UTC), + // AttachmentID: idVolume1, + Device: "/dev/vdb", + HostName: "", + ID: idVolume1, + ServerID: instanceID, + VolumeID: idVolume1, + }}, + Name: nameVolume1, + Description: descriptionVolume1, + VolumeType: "nfsdriver", + SnapshotID: "", + SourceVolID: "", + Metadata: map[string]string{ + "readonly": "False", + "attached_mode": "rw", + }, + UserID: "2a5719084bc9457c93e659f4f13c6bfc", + Bootable: "true", + Encrypted: false, + ReplicationStatus: "disabled", + TenantID: tenantID, +} + +var structVolume2 = volumes.Volume{ + ID: idVolume2, + Status: "available", + Size: 40, + AvailabilityZone: az, + CreatedAt: timeCreatedAt, + Attachments: []volumes.Attachment{}, + Name: "volume 2", + Description: "test volume 2", + VolumeType: "nfsdriver", + SnapshotID: "", + SourceVolID: "", + Metadata: map[string]string{}, + UserID: "2a5719084bc9457c93e659f4f13c6bfc", + Bootable: "true", + Encrypted: false, + ReplicationStatus: "disabled", + TenantID: tenantID, +} + +var expectedVolumesSlice = []volumes.Volume{ + structVolume1, + structVolume2, +} + +var getResponse = fmt.Sprintf(`{ + "volume": { + "id": "%s", + "name": "%s", + "status": "in-use", + "size": %d, + "availability_zone": "%s", + "created_at": "%s", + "os-vol-tenant-attr:tenant_id": "%s", + "description": "%s", + "attachments": [{ + "host_name": null, + "device": "/dev/vdb", + "server_id": "%s", + "id": "%s", + "volume_id": "%s" + }], + "links": [{ + "href": "dummy_self_link", + "rel": "self" + }, { + "href": "dummy_bookmark_link", + "rel": "bookmark" + }], + "encrypted": false, + "os-volume-replication:extended_status": null, + "volume_type": "nfsdriver", + "snapshot_id": null, + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "metadata": { + "readonly": "False", + "attached_mode": "rw" + }, + "volume_image_metadata": { + ".edition": "none", + ".major.version": "7", + ".official_image_template": "CentOS-7.1-1503_64_virtual-server_12", + "container_format": "bare", + "min_ram": "0", + "disk_format": "qcow2", + ".is_license": "False", + "image_name": "CentOS-7.1-1503_64_virtual-server_12", + "image_id": "df1944a7-ca45-4709-9ec6-e31664133650", + ".os.type": "centos", + ".enable.download": "True", + "checksum": "a828b6ba68b9d13d2da0a0cb3cfaa950", + "min_disk": "15", + ".service.type": "virtual-server", + ".virtual_server.os.pod": "other", + ".minor.version": "1-1503", + "size": "461504512" + }, + "source_volid": null, + "consistencygroup_id": null, + "bootable": "true", + "os-volume-replication:driver_data": null, + "replication_status": "disabled" + } +}`, + idVolume1, + nameVolume1, + sizeVolume1, + az, + createdAt, + tenantID, + descriptionVolume1, + instanceID, + idVolume1, + idVolume1, +) + +var createRequest = fmt.Sprintf(`{ + "volume": { + "size": 15, + "availability_zone": "%s", + "description": "%s", + "name": "%s", + "imageRef": "dummyimage" + } +}`, + az, + descriptionVolume1, + nameVolume1, +) + +var createResponse = fmt.Sprintf(`{ + "volume": { + "status": "creating", + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "attachments": [], + "links": [{ + "href": "https://cinder-jp4-ecl.api.ntt.com/v2/9ee80f2a926c49f88f166af47df4e9f5/volumes/251df9eb-c088-4e71-808b-75a690e8814b", + "rel": "self" + }, { + "href": "https://cinder-jp4-ecl.api.ntt.com/9ee80f2a926c49f88f166af47df4e9f5/volumes/251df9eb-c088-4e71-808b-75a690e8814b", + "rel": "bookmark" + }], + "availability_zone": "%s", + "bootable": "false", + "encrypted": false, + "created_at": "2019-02-06T08:06:57.581271", + "description": "%s", + "volume_type": "nfsdriver", + "name": "%s", + "replication_status": "disabled", + "consistencygroup_id": null, + "source_volid": null, + "snapshot_id": null, + "metadata": {}, + "id": "%s", + "size": 15 + } +}`, az, + descriptionVolume1, + nameVolume1, + idVolume1, +) + +var updateResponse = fmt.Sprintf(`{ + "volume": { + "status": "available", + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "attachments": [], + "links": [{ + "href": "https://cinder-jp4-ecl.api.ntt.com/v2/9ee80f2a926c49f88f166af47df4e9f5/volumes/7e0b432b-c922-49d7-b85a-28ac88164328", + "rel": "self" + }, { + "href": "https://cinder-jp4-ecl.api.ntt.com/9ee80f2a926c49f88f166af47df4e9f5/volumes/7e0b432b-c922-49d7-b85a-28ac88164328", + "rel": "bookmark" + }], + "availability_zone": "%s", + "bootable": "true", + "encrypted": false, + "created_at": "2019-02-06T08:08:32.000000", + "description": "%s", + "volume_type": "nfsdriver", + "name": "%s", + "replication_status": "disabled", + "consistencygroup_id": null, + "source_volid": null, + "snapshot_id": null, + "metadata": {}, + "id": "%s", + "size": 40 + } +}`, + az, + descriptionVolume1Update, + nameVolume1Update, + idVolume1, +) diff --git a/v3/ecl/computevolume/v2/volumes/testing/requests_test.go b/v3/ecl/computevolume/v2/volumes/testing/requests_test.go new file mode 100644 index 0000000..c38c5fb --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/testing/requests_test.go @@ -0,0 +1,133 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/computevolume/v2/volumes" + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListVolumeAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listResponse) + }) + + allPages, err := volumes.List(fakeclient.ServiceClient(), &volumes.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := volumes.ExtractVolumes(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expectedVolumesSlice, actual) + +} + +func TestGetVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, getResponse) + }) + + v, err := volumes.Get(fakeclient.ServiceClient(), idVolume1).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &expectedVolumesSlice[0], v) +} + +func TestCreateVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, createResponse) + }) + + options := &volumes.CreateOpts{ + Size: 15, + AvailabilityZone: az, + Description: descriptionVolume1, + Name: nameVolume1, + ImageID: "dummyimage", + } + v, err := volumes.Create(fakeclient.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.AvailabilityZone, az) + th.AssertEquals(t, v.Size, 15) + th.AssertEquals(t, v.ID, idVolume1) + th.AssertEquals(t, v.Description, descriptionVolume1) +} + +func TestUpdatVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, updateResponse) + }) + + name := nameVolume1Update + description := descriptionVolume1Update + metadata := map[string]string{} + + updateOpts := volumes.UpdateOpts{ + Name: &name, + Description: &description, + Metadata: &metadata, + } + + v, err := volumes.Update(fakeclient.ServiceClient(), idVolume1, updateOpts).Extract() + + blankMeta := map[string]string{} + th.AssertNoErr(t, err) + th.CheckEquals(t, nameVolume1Update, v.Name) + th.CheckEquals(t, descriptionVolume1Update, v.Description) + th.CheckDeepEquals(t, &blankMeta, &v.Metadata) +} + +func TestDeleteVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusAccepted) + }) + + res := volumes.Delete(fakeclient.ServiceClient(), idVolume1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/computevolume/v2/volumes/urls.go b/v3/ecl/computevolume/v2/volumes/urls.go new file mode 100644 index 0000000..4865b34 --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/nttcom/eclcloud/v3" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/v3/ecl/computevolume/v2/volumes/util.go b/v3/ecl/computevolume/v2/volumes/util.go new file mode 100644 index 0000000..548329c --- /dev/null +++ b/v3/ecl/computevolume/v2/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v3/ecl/dedicated_hypervisor/v1/license_types/doc.go b/v3/ecl/dedicated_hypervisor/v1/license_types/doc.go new file mode 100644 index 0000000..f7ed6ce --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/license_types/doc.go @@ -0,0 +1,20 @@ +/* +Package license_types manages and retrieves license type in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List License types + + allPages, err := license_types.List(dhClient).AllPages() + if err != nil { + panic(err) + } + + allLicenseTypes, err := license_types.ExtractLicenseTypes(allPages) + if err != nil { + panic(err) + } + + for _, licenseType := range allLicenseTypes { + fmt.Printf("%+v\n", licenseType) + } +*/ +package license_types diff --git a/v3/ecl/dedicated_hypervisor/v1/license_types/requests.go b/v3/ecl/dedicated_hypervisor/v1/license_types/requests.go new file mode 100644 index 0000000..278aed7 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/license_types/requests.go @@ -0,0 +1,14 @@ +package license_types + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// List retrieves a list of LicenseTypes. +func List(client *eclcloud.ServiceClient) pagination.Pager { + url := listURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return LicenseTypePage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/license_types/results.go b/v3/ecl/dedicated_hypervisor/v1/license_types/results.go new file mode 100644 index 0000000..ad63b95 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/license_types/results.go @@ -0,0 +1,35 @@ +package license_types + +import ( + "github.com/nttcom/eclcloud/v3/pagination" +) + +// LicenseType represents guest image license information. +type LicenseType struct { + ID string `json:"id"` + Name string `json:"name"` + HasLicenseKey bool `json:"has_license_key"` + Unit string `json:"unit"` + LicenseSwitch bool `json:"license_switch"` + Description string `json:"description"` +} + +// LicenseTypePage is a single page of LicenseType results. +type LicenseTypePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of LicenseTypes contains any results. +func (r LicenseTypePage) IsEmpty() (bool, error) { + licenses, err := ExtractLicenseTypes(r) + return len(licenses) == 0, err +} + +// ExtractLicenseTypes returns a slice of LicenseTypes contained in a single page of results. +func ExtractLicenseTypes(r pagination.Page) ([]LicenseType, error) { + var s struct { + LicenseTypes []LicenseType `json:"license_types"` + } + err := (r.(LicenseTypePage)).ExtractInto(&s) + return s.LicenseTypes, err +} diff --git a/v3/ecl/dedicated_hypervisor/v1/license_types/testing/fixtures.go b/v3/ecl/dedicated_hypervisor/v1/license_types/testing/fixtures.go new file mode 100644 index 0000000..ba7cee0 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/license_types/testing/fixtures.go @@ -0,0 +1,73 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/license_types" + + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// ListResult provides a single page of LicenseType results. +const ListResult = ` +{ + "license_types": [ + { + "description": "Windows Server 2016 Standard Edition", + "has_license_key": false, + "id": "9c54c437-5f0f-46f5-8270-ddf450a44135", + "license_switch": true, + "name": "Windows Server 2016 Standard Edition", + "unit": "VM" + }, + { + "description": "vCenter Server 6.x Standard", + "has_license_key": true, + "id": "e37c05ba-8fd0-493e-93d2-688833363a74", + "license_switch": false, + "name": "vCenter Server 6.x Standard", + "unit": "License" + } + ] +} +` + +// FirstLicenseType is the first LicenseType in the List request. +var FirstLicenseType = license_types.LicenseType{ + ID: "9c54c437-5f0f-46f5-8270-ddf450a44135", + Name: "Windows Server 2016 Standard Edition", + HasLicenseKey: false, + Unit: "VM", + LicenseSwitch: true, + Description: "Windows Server 2016 Standard Edition", +} + +// SecondLicenseType is the second LicenseType in the List request. +var SecondLicenseType = license_types.LicenseType{ + ID: "e37c05ba-8fd0-493e-93d2-688833363a74", + Name: "vCenter Server 6.x Standard", + HasLicenseKey: true, + Unit: "License", + LicenseSwitch: false, + Description: "vCenter Server 6.x Standard", +} + +// ExpectedLicenseTypesSlice is the slice of LicenseTypes expected to be returned from ListResult. +var ExpectedLicenseTypesSlice = []license_types.LicenseType{FirstLicenseType, SecondLicenseType} + +// HandleListLicenseTypesSuccessfully creates an HTTP handler at `/license_types` on the +// test handler mux that responds with a list of two LicenseType. +func HandleListLicenseTypesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/license_types", 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, ListResult) + }) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/license_types/testing/requests_test.go b/v3/ecl/dedicated_hypervisor/v1/license_types/testing/requests_test.go new file mode 100644 index 0000000..3b999e1 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/license_types/testing/requests_test.go @@ -0,0 +1,43 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/license_types" + + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListLicenseTypes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicenseTypesSuccessfully(t) + + count := 0 + err := license_types.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := license_types.ExtractLicenseTypes(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedLicenseTypesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListLicenseTypesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicenseTypesSuccessfully(t) + + allPages, err := license_types.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + actual, err := license_types.ExtractLicenseTypes(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedLicenseTypesSlice, actual) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/license_types/urls.go b/v3/ecl/dedicated_hypervisor/v1/license_types/urls.go new file mode 100644 index 0000000..1768654 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/license_types/urls.go @@ -0,0 +1,7 @@ +package license_types + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("license_types") +} diff --git a/v3/ecl/dedicated_hypervisor/v1/licenses/doc.go b/v3/ecl/dedicated_hypervisor/v1/licenses/doc.go new file mode 100644 index 0000000..a35c2aa --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/licenses/doc.go @@ -0,0 +1,44 @@ +/* +Package licenses manages and retrieves license in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List Licenses + + listOpts := licenses.ListOpts{ + LicenseType: "vCenter Server 6.x Standard", + } + + allPages, err := licenses.List(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLicenses, err := licenses.ExtractLicenses(allPages) + if err != nil { + panic(err) + } + + for _, license := range allLicenses { + fmt.Printf("%+v\n", license) + } + +Example to Create a License + + createOpts := licenses.CreateOpts{ + LicenseType: "vCenter Server 6.x Standard", + } + + result := licenses.Create(dhClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a license + + licenseID := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + result := licenses.Delete(dhClient, licenseID) + if result.Err != nil { + panic(result.Err) + } +*/ +package licenses diff --git a/v3/ecl/dedicated_hypervisor/v1/licenses/requests.go b/v3/ecl/dedicated_hypervisor/v1/licenses/requests.go new file mode 100644 index 0000000..6f444dd --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/licenses/requests.go @@ -0,0 +1,76 @@ +package licenses + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToResourceListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + LicenseType string `q:"license_type"` +} + +// ToResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Licenses. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return LicensePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToResourceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a License. +type CreateOpts struct { + LicenseType string `json:"license_type"` +} + +// ToResourceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new License. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a License. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/v3/ecl/dedicated_hypervisor/v1/licenses/results.go b/v3/ecl/dedicated_hypervisor/v1/licenses/results.go new file mode 100644 index 0000000..4b81b89 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/licenses/results.go @@ -0,0 +1,63 @@ +package licenses + +import ( + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// License represents guest image license key information. +type License struct { + ID string `json:"id"` + Key string `json:"key"` + AssignedFrom time.Time `json:"assigned_from"` + ExpiresAt *time.Time `json:"expires_at"` + LicenseType string `json:"license_type"` +} + +type commonResult struct { + eclcloud.Result +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a License. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// LicensePage is a single page of License results. +type LicensePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Licenses contains any results. +func (r LicensePage) IsEmpty() (bool, error) { + licenses, err := ExtractLicenses(r) + return len(licenses) == 0, err +} + +// ExtractLicenses returns a slice of Licenses contained in a single page of +// results. +func ExtractLicenses(r pagination.Page) ([]License, error) { + var s struct { + Licenses []License `json:"licenses"` + } + err := (r.(LicensePage)).ExtractInto(&s) + return s.Licenses, err +} + +// ExtractLicenseInfo interprets any commonResult as a License. +func (r commonResult) ExtractLicenseInfo() (*License, error) { + var s struct { + License *License `json:"license"` + } + err := r.ExtractInto(&s) + return s.License, err +} diff --git a/v3/ecl/dedicated_hypervisor/v1/licenses/testing/fixtures.go b/v3/ecl/dedicated_hypervisor/v1/licenses/testing/fixtures.go new file mode 100644 index 0000000..0050613 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/licenses/testing/fixtures.go @@ -0,0 +1,114 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/licenses" + + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// ListResult provides a single page of License results. +const ListResult = ` +{ + "licenses": [ + { + "id": "02471b45-3de0-4fc8-8469-a7cc52c378df", + "key": "5H69L-8C3D7-K8292-03926-CREMN", + "assigned_from": "2017-04-27T09:20:47Z", + "expires_at": null, + "license_type": "vCenter Server 6.x Standard" + }, + { + "id": "0801a388-68e8-4e41-9158-73571117c915", + "key": "0021L-8CJ47-2829A-0A8K2-CXN4J", + "assigned_from": "2017-06-01T04:13:31Z", + "expires_at": null, + "license_type": "vCenter Server 6.x Standard" + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "license": { + "id": "0801a388-68e8-4e41-9158-73571117c915", + "key": "0021L-8CJ47-2829A-0A8K2-CXN4J", + "assigned_from": "2017-06-01T04:13:31Z", + "expires_at": null, + "license_type": "vCenter Server 6.x Standard" + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "license_type": "vCenter Server 6.x Standard" +} +` + +// FirstLicense is the first License in the List request. +var FirstLicense = licenses.License{ + ID: "02471b45-3de0-4fc8-8469-a7cc52c378df", + Key: "5H69L-8C3D7-K8292-03926-CREMN", + AssignedFrom: time.Date(2017, 4, 27, 9, 20, 47, 0, time.UTC), + ExpiresAt: nil, + LicenseType: "vCenter Server 6.x Standard", +} + +// SecondLicense is the second License in the List request. +var SecondLicense = licenses.License{ + ID: "0801a388-68e8-4e41-9158-73571117c915", + Key: "0021L-8CJ47-2829A-0A8K2-CXN4J", + AssignedFrom: time.Date(2017, 6, 1, 4, 13, 31, 0, time.UTC), + ExpiresAt: nil, + LicenseType: "vCenter Server 6.x Standard", +} + +// ExpectedLicensesSlice is the slice of Licenses expected to be returned from ListResult. +var ExpectedLicensesSlice = []licenses.License{FirstLicense, SecondLicense} + +// HandleListLicenseSuccessfully creates an HTTP handler at `/licenses` on the +// test handler mux that responds with a list of two Licenses. +func HandleListLicensesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/licenses", 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, ListResult) + }) +} + +// HandleCreateLicenseSuccessfully creates an HTTP handler at `/licenses` on the +// test handler mux that tests License creation. +func HandleCreateLicenseSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/licenses", 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, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetResult) + }) +} + +// HandleDeleteLicenseSuccessfully creates an HTTP handler at `/licenses` on the +// test handler mux that tests License deletion. +func HandleDeleteLicenseSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/licenses/%s", FirstLicense.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/licenses/testing/requests_test.go b/v3/ecl/dedicated_hypervisor/v1/licenses/testing/requests_test.go new file mode 100644 index 0000000..d417578 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/licenses/testing/requests_test.go @@ -0,0 +1,65 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/licenses" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListLicenses(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicensesSuccessfully(t) + + count := 0 + err := licenses.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := licenses.ExtractLicenses(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedLicensesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListLicensesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicensesSuccessfully(t) + + allPages, err := licenses.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := licenses.ExtractLicenses(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedLicensesSlice, actual) +} + +func TestCreateLicense(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateLicenseSuccessfully(t) + + createOpts := licenses.CreateOpts{ + LicenseType: SecondLicense.LicenseType, + } + + actual, err := licenses.Create(client.ServiceClient(), createOpts).ExtractLicenseInfo() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondLicense, *actual) +} + +func TestDeleteLicense(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteLicenseSuccessfully(t) + + res := licenses.Delete(client.ServiceClient(), FirstLicense.ID) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/licenses/urls.go b/v3/ecl/dedicated_hypervisor/v1/licenses/urls.go new file mode 100644 index 0000000..0fffd00 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/licenses/urls.go @@ -0,0 +1,15 @@ +package licenses + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("licenses") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("licenses") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("licenses", id) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/servers/doc.go b/v3/ecl/dedicated_hypervisor/v1/servers/doc.go new file mode 100644 index 0000000..5bc3134 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/servers/doc.go @@ -0,0 +1,131 @@ +/* +Package servers manages and retrieves servers in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List servers + + listOpts := servers.ListOpts{ + Limit: 10, + } + + allPages, err := servers.List(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to List servers details + + listOpts := servers.ListOpts{ + Limit: 10, + } + + allPages, err := servers.ListDetails(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Get a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + result := servers.Get(dhClient, serverID) + if result.Err != nil { + panic(result.Err) + } + + server, err := result.Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", server) + +Example to Create a server + + createOpts := servers.CreateOpts{ + Name: "test", + Networks: []servers.Network{ + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + }, + ImageRef: "dfd25820-b368-4012-997b-29a6d0cf8518", + FlavorRef: "a830b61c-3155-4a61-b7ed-c450862845e6", + } + + result := servers.Create(dhClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + result := servers.Delete(dhClient, serverID) + if result.Err != nil { + panic(result.Err) + } + +Example to Add license to a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + addLicenseOpts := servers.AddLicenseOpts{ + VmName: "Alice", + LicenseTypes: []string{ + "Windows Server", + "SQL Server Standard 2014", + }, + } + + result := servers.AddLicense(dhClient, serverID, addLicenseOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Get result for add license to a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + getAddLicenseResultOpts := servers.GetAddLicenseResultOpts{ + JobID: AddLicenseJob.JobID, + } + + result := servers.GetAddLicenseResult(dhClient, serverID, getAddLicenseResultOpts) + if result.Err != nil { + panic(result.Err) + } + + job, err := result.Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", job) +*/ +package servers diff --git a/v3/ecl/dedicated_hypervisor/v1/servers/requests.go b/v3/ecl/dedicated_hypervisor/v1/servers/requests.go new file mode 100644 index 0000000..7085741 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/servers/requests.go @@ -0,0 +1,160 @@ +package servers + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToResourceListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + ChangesSince string `q:"changes-since"` + Marker string `q:"marker"` + Limit int `q:"limit"` + Name string `q:"name"` + Image string `q:"image"` + Flavor string `q:"flavor"` + Status string `q:"status"` +} + +// ToResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Servers. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListDetails retrieves a list of Servers in details. +func ListDetails(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailsURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of a Server. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToResourceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a Server. +type CreateOpts struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Networks []Network `json:"networks"` + AdminPass string `json:"adminPass,omitempty"` + ImageRef string `json:"imageRef"` + FlavorRef string `json:"flavorRef"` + AvailabilityZone string `json:"availability_zone,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +type Network struct { + UUID string `json:"uuid"` + Port string `json:"port,omitempty"` + FixedIP string `json:"fixed_ip,omitempty"` + Plane string `json:"plane"` + SegmentationID int `json:"segmentation_id"` +} + +// ToResourceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "server") +} + +// Create creates a new Server. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a Server. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +type AddLicenseOpts struct { + VmName string `json:"vm_name,omitempty"` + VmID string `json:"vm_id,omitempty"` + LicenseTypes []string `json:"license_types"` +} + +func (opts AddLicenseOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "add-license-to-vm") +} + +func AddLicense(client *eclcloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r AddLicenseResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, serverID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +type GetAddLicenseResultOpts struct { + JobID string `json:"job_id"` +} + +func (opts GetAddLicenseResultOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "get-result-for-add-license-to-vm") +} + +func GetAddLicenseResult(client *eclcloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r AddLicenseResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, serverID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/dedicated_hypervisor/v1/servers/results.go b/v3/ecl/dedicated_hypervisor/v1/servers/results.go new file mode 100644 index 0000000..5d71676 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/servers/results.go @@ -0,0 +1,203 @@ +package servers + +import ( + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Server represents dedicated hypervisor server information. +type Server struct { + ID string `json:"id"` + Name string `json:"name"` + ImageRef string `json:"imageRef"` + Description *string `json:"description"` + Status string `json:"status"` + HypervisorType string `json:"hypervisor_type"` + BaremetalServer BaremetalServer `json:"baremetal_server"` + Links []Link `json:"links"` + AdminPass string `json:"adminPass"` +} + +type BaremetalServer struct { + PowerState string `json:"OS-EXT-STS:power_state"` + TaskState string `json:"OS-EXT-STS:task_state"` + VMState string `json:"OS-EXT-STS:vm_state"` + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` + Created time.Time `json:"created"` + Flavor Flavor `json:"flavor"` + ID string `json:"id"` + Image Image `json:"image"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Progress int `json:"progress"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + Updated time.Time `json:"updated"` + UserID string `json:"user_id"` + NicPhysicalPorts []NicPhysicalPort `json:"nic_physical_ports"` + ChassisStatus ChassisStatus `json:"chassis-status"` + Links []Link `json:"links"` + RaidArrays []RaidArray `json:"raid_arrays"` + LvmVolumeGroups []LvmVolumeGroup `json:"lvm_volume_groups"` + Filesystems []Filesystem `json:"filesystems"` + MediaAttachments []MediaAttachment `json:"media_attachments"` + ManagedByService string `json:"managed_by_service"` + ManagedServiceResourceID string `json:"managed_service_resource_id"` +} + +type Flavor struct { + ID string `json:"id"` + Links []Link `json:"links"` +} + +type Image struct { + ID string `json:"id"` + Links []Link `json:"links"` +} + +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +type NicPhysicalPort struct { + ID string `json:"id"` + MACAddr string `json:"mac_addr"` + NetworkPhysicalPortID string `json:"network_physical_port_id"` + Plane string `json:"plane"` + AttachedPorts []Port `json:"attached_ports"` + HardwareID string `json:"hardware_id"` +} + +type Port struct { + PortID string `json:"port_id"` + NetworkID string `json:"network_id"` + FixedIPs []FixedIP `json:"fixed_ips"` +} + +type FixedIP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +type ChassisStatus struct { + ChassisPower bool `json:"chassis-power"` + PowerSupply bool `json:"power-supply"` + CPU bool `json:"cpu"` + Memory bool `json:"memory"` + Fan bool `json:"fan"` + Disk int `json:"disk"` + Nic bool `json:"nic"` + SystemBoard bool `json:"system-board"` + Etc bool `json:"etc"` + Console bool `json:"console"` +} + +type RaidArray struct { + PrimaryStorage bool `json:"primary_storage"` + Partitions []Partition `json:"partitions"` + RaidCardHardwareID string `json:"raid_card_hardware_id"` + DiskHardwareIDs []string `json:"disk_hardware_ids"` +} + +type Partition struct { + Lvm bool `json:"lvm"` + Size string `json:"size"` + PartitionLabel string `json:"partition_label"` +} + +type LvmVolumeGroup struct { + VgLabel string `json:"vg_label"` + PhysicalVolumePartitionLabels []string `json:"physical_volume_partition_labels"` + LogicalVolumes []LogicalVolume `json:"logical_volumes"` +} + +type LogicalVolume struct { + LvLabel string `json:"lv_label"` + Size string `json:"size"` +} + +type Filesystem struct { + Label string `json:"label"` + MountPoint string `json:"mount_point"` + FsType string `json:"fs_type"` +} + +type MediaAttachment struct { + Image Image `json:"image"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Server. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Server. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ServerPage is a single page of Server results. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Servers contains any results. +func (r ServerPage) IsEmpty() (bool, error) { + servers, err := ExtractServers(r) + return len(servers) == 0, err +} + +// ExtractServers returns a slice of Servers contained in a single page of +// results. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s struct { + Servers []Server `json:"servers"` + } + err := (r.(ServerPage)).ExtractInto(&s) + return s.Servers, err +} + +// Extract interprets any commonResult as a Server. +func (r commonResult) Extract() (*Server, error) { + var s struct { + Server *Server `json:"server"` + } + err := r.ExtractInto(&s) + return s.Server, err +} + +type Job struct { + JobID string `json:"job_id"` + Status string `json:"status"` + RequestedParam RequestedParam `json:"requested_param"` +} + +type RequestedParam struct { + VmName string `json:"vm_name"` + LicenseTypes []string `json:"license_types"` +} + +type AddLicenseResult struct { + eclcloud.Result +} + +func (r AddLicenseResult) Extract() (*Job, error) { + var job Job + err := r.ExtractInto(&job) + return &job, err +} diff --git a/v3/ecl/dedicated_hypervisor/v1/servers/testing/fixtures.go b/v3/ecl/dedicated_hypervisor/v1/servers/testing/fixtures.go new file mode 100644 index 0000000..0e97c46 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/servers/testing/fixtures.go @@ -0,0 +1,1119 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/servers" + + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// ListResult provides a single page of Server results. +const ListResult = ` +{ + "servers": [ + { + "id": "194573e4-8f53-4ee4-806f-d9b2db74a380", + "name": "GP2v1", + "links": [ + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + "rel": "self" + }, + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + "rel": "bookmark" + } + ], + "baremetal_server": { + "id": "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "bookmark" + } + ], + "name": "GP2v1" + } + }, + { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "name": "test", + "links": [ + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "self" + }, + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "bookmark" + } + ], + "baremetal_server": { + "id": "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "bookmark" + } + ], + "name": "test" + } + } + ] +} +` + +// ListDetailsResult provides a single page of Server results in details. +const ListDetailsResult = ` +{ + "servers": [ + { + "id": "194573e4-8f53-4ee4-806f-d9b2db74a380", + "name": "GP2v1", + "imageRef": "293063f6-8986-4b79-becd-7a6d28794bb8", + "description": null, + "status": "ACTIVE", + "hypervisor_type": "vsphere_esxi", + "baremetal_server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "groupb", + "progress": 100, + "created": "2019-10-18T07:42:35Z", + "flavor": { + "id": "303b4993-cf29-4301-abd0-99512b5413a5", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + "rel": "bookmark" + } + ] + }, + "id": "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "image": { + "id": "02441adc-0d9a-4e9d-b359-ce23413e7ea7", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/02441adc-0d9a-4e9d-b359-ce23413e7ea7", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "GP2v1", + "status": "ACTIVE", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "updated": "2019-10-18T07:44:18Z", + "user_id": "55891ce6a3cb4bb0833514667d67288c", + "raid_arrays": [ + { + "primary_storage": true, + "partitions": null, + "raid_card_hardware_id": "24184dcf-dc76-4ea2-a34e-bccc6c11d5be", + "disk_hardware_ids": [ + "4de7d3df-8e2f-4193-98dc-145f78df29a2", + "de158fab-7bc2-49e3-98d1-9db5451d43e3", + "97087307-cab2-40cb-a84c-86d98da0f393" + ] + } + ], + "lvm_volume_groups": null, + "filesystems": null, + "nic_physical_ports": [ + { + "id": "b49c0624-e89a-469f-8c90-7a27ee1f61cb", + "mac_addr": "8C:DC:D4:B7:41:48", + "plane": "DATA", + "network_physical_port_id": "4cfbe3b2-a502-485f-82fa-a0949396e567", + "hardware_id": "8e1e2fe0-60a7-4211-a891-80e808426708", + "attached_ports": [ + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "8808acc2-d930-40fb-b382-ce7074baef83", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.11" + } + ] + }, + { + "network_id": "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + "port_id": "9d3baa16-e0e5-4e50-9677-08dd338e0c14", + "fixed_ips": [ + { + "subnet_id": "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + "ip_address": "192.168.4.3" + } + ] + } + ] + }, + { + "id": "8bea93c4-721b-480c-8713-f2a4b6e5dbad", + "mac_addr": "8C:DC:D4:B7:41:49", + "plane": "STORAGE", + "network_physical_port_id": "ab38075d-128f-4f3d-a16a-c6426375a380", + "hardware_id": "8e1e2fe0-60a7-4211-a891-80e808426708", + "attached_ports": [] + }, + { + "id": "a7fbca5e-ff49-4ddb-8659-02ec462f98ec", + "mac_addr": "8C:DC:D4:B7:45:89", + "plane": "STORAGE", + "network_physical_port_id": "9ef62803-7848-460c-9dae-17fd02606a26", + "hardware_id": "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + "attached_ports": [] + }, + { + "id": "71af4de1-0c9b-4870-8c95-c7b9b4115bb8", + "mac_addr": "8C:DC:D4:B7:45:88", + "plane": "DATA", + "network_physical_port_id": "ad92f33a-eac4-408d-bc27-22d91eccd465", + "hardware_id": "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + "attached_ports": [ + { + "network_id": "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + "port_id": "705bde94-7189-40b1-b8a2-188e6cc3c546", + "fixed_ips": [ + { + "subnet_id": "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + "ip_address": "192.168.4.4" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "7b860eb4-0eb6-4c2a-873e-ccefd6029d97", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.12" + } + ] + } + ] + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true, + "console": true + }, + "media_attachments": [], + "managed_by_service": "dedicated-hypervisor", + "managed_service_resource_id": "194573e4-8f53-4ee4-806f-d9b2db74a380" + } + }, + { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "name": "test", + "imageRef": "dfd25820-b368-4012-997b-29a6d0cf8518", + "description": "test", + "status": "ACTIVE", + "hypervisor_type": "vsphere_esxi", + "baremetal_server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "groupb", + "progress": 100, + "created": "2019-10-10T04:11:41Z", + "flavor": { + "id": "a830b61c-3155-4a61-b7ed-c450862845e6", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/a830b61c-3155-4a61-b7ed-c450862845e6", + "rel": "bookmark" + } + ] + }, + "id": "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "image": { + "id": "112a26a0-ff25-4513-afe1-407e41b0a48b", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/112a26a0-ff25-4513-afe1-407e41b0a48b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "test", + "status": "ACTIVE", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "updated": "2019-10-10T04:14:08Z", + "user_id": "55891ce6a3cb4bb0833514667d67288c", + "raid_arrays": [ + { + "primary_storage": true, + "partitions": null, + "raid_card_hardware_id": "bdfb75d1-194d-426d-b288-f588dfa5ac49", + "disk_hardware_ids": [ + "76649053-863e-4533-86e3-f194a79485a6", + "a25827e3-67da-47be-ba96-849ab4685a1d" + ] + } + ], + "lvm_volume_groups": null, + "filesystems": null, + "nic_physical_ports": [ + { + "id": "a2f63380-6c77-4cd5-8868-e3556ffd35ce", + "mac_addr": "48:DF:37:90:B4:58", + "plane": "DATA", + "network_physical_port_id": "d8e40a51-f1e2-4681-8953-9fe1e9992c42", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "30fc1c27-fb5f-4955-94d0-a56cd28d09e8", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.10" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "aa6c61f4-db8a-44c7-a91c-7e636dac1dc6", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.9" + } + ] + } + ] + }, + { + "id": "b01dfdb0-f247-47d8-8224-c257aa3265e9", + "mac_addr": "48:DF:37:90:B4:50", + "plane": "STORAGE", + "network_physical_port_id": "00dfea92-5c5b-4860-aa05-efef6c2bb2af", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [] + }, + { + "id": "f4355e8e-39fc-48bd-a283-a2dbef8a2e32", + "mac_addr": "48:DF:37:82:B0:A0", + "plane": "STORAGE", + "network_physical_port_id": "cf798cc0-c869-45d5-a5a7-bcc578a300b0", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [] + }, + { + "id": "5ef177fd-888c-4fae-9925-a8920beb07cb", + "mac_addr": "48:DF:37:82:B0:A8", + "plane": "DATA", + "network_physical_port_id": "2bbbb516-c75a-42b2-8a46-9cb5f26c219e", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "4e329a01-2cf4-4028-9259-03b7aa145cb6", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.20" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "a256b4a1-3ae3-4102-a14e-987ae1610f97", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.10" + } + ] + } + ] + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true, + "console": true + }, + "media_attachments": [], + "managed_by_service": "dedicated-hypervisor", + "managed_service_resource_id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + } + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "server": { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "name": "test", + "imageRef": "dfd25820-b368-4012-997b-29a6d0cf8518", + "description": "test", + "status": "ACTIVE", + "hypervisor_type": "vsphere_esxi", + "baremetal_server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "groupb", + "progress": 100, + "created": "2019-10-10T04:11:41Z", + "flavor": { + "id": "a830b61c-3155-4a61-b7ed-c450862845e6", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/a830b61c-3155-4a61-b7ed-c450862845e6", + "rel": "bookmark" + } + ] + }, + "id": "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "image": { + "id": "112a26a0-ff25-4513-afe1-407e41b0a48b", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/112a26a0-ff25-4513-afe1-407e41b0a48b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "test", + "status": "ACTIVE", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "updated": "2019-10-10T04:14:08Z", + "user_id": "55891ce6a3cb4bb0833514667d67288c", + "raid_arrays": [ + { + "primary_storage": true, + "partitions": null, + "raid_card_hardware_id": "bdfb75d1-194d-426d-b288-f588dfa5ac49", + "disk_hardware_ids": [ + "76649053-863e-4533-86e3-f194a79485a6", + "a25827e3-67da-47be-ba96-849ab4685a1d" + ] + } + ], + "lvm_volume_groups": null, + "filesystems": null, + "nic_physical_ports": [ + { + "id": "a2f63380-6c77-4cd5-8868-e3556ffd35ce", + "mac_addr": "48:DF:37:90:B4:58", + "plane": "DATA", + "network_physical_port_id": "d8e40a51-f1e2-4681-8953-9fe1e9992c42", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "30fc1c27-fb5f-4955-94d0-a56cd28d09e8", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.10" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "aa6c61f4-db8a-44c7-a91c-7e636dac1dc6", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.9" + } + ] + } + ] + }, + { + "id": "b01dfdb0-f247-47d8-8224-c257aa3265e9", + "mac_addr": "48:DF:37:90:B4:50", + "plane": "STORAGE", + "network_physical_port_id": "00dfea92-5c5b-4860-aa05-efef6c2bb2af", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [] + }, + { + "id": "f4355e8e-39fc-48bd-a283-a2dbef8a2e32", + "mac_addr": "48:DF:37:82:B0:A0", + "plane": "STORAGE", + "network_physical_port_id": "cf798cc0-c869-45d5-a5a7-bcc578a300b0", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [] + }, + { + "id": "5ef177fd-888c-4fae-9925-a8920beb07cb", + "mac_addr": "48:DF:37:82:B0:A8", + "plane": "DATA", + "network_physical_port_id": "2bbbb516-c75a-42b2-8a46-9cb5f26c219e", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "4e329a01-2cf4-4028-9259-03b7aa145cb6", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.20" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "a256b4a1-3ae3-4102-a14e-987ae1610f97", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.10" + } + ] + } + ] + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true, + "console": true + }, + "media_attachments": [], + "managed_by_service": "dedicated-hypervisor", + "managed_service_resource_id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + } + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "server": { + "imageRef": "dfd25820-b368-4012-997b-29a6d0cf8518", + "name": "test", + "networks": [ + { + "segmentation_id": 6, + "plane": "data", + "uuid": "94055904-6b2c-4839-a14a-c61c93a8bc48" + }, + { + "segmentation_id": 6, + "plane": "data", + "uuid": "94055904-6b2c-4839-a14a-c61c93a8bc48" + } + ], + "flavorRef": "a830b61c-3155-4a61-b7ed-c450862845e6" + } +} +` + +const CreateResponse = ` +{ + "server": { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "links": [ + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "self" + }, + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "bookmark" + } + ], + "adminPass": "aabbccddeeff" + } +} +` + +const AddLicenseRequest = ` +{ + "add-license-to-vm": { + "vm_name": "Alice", + "license_types": [ + "Windows Server", + "SQL Server Standard 2014" + ] + } +} +` + +const AddLicenseResponse = ` +{ + "job_id": "b4f888dc2b9d4c41bb769cbd" +} +` + +const GetAddLicenseResultRequest = ` +{ + "get-result-for-add-license-to-vm": { + "job_id": "b4f888dc2b9d4c41bb769cbd" + } +} +` + +const GetAddLicenseResultResponse = ` +{ + "job_id": "b4f888dc2b9d4c41bb769cbd", + "status": "COMPLETED", + "requested_param": { + "vm_name": "Alice", + "license_types": ["Windows Server", "SQL Server Standard 2014"] + } +} +` + +// FirstServer is the first resource in the List request. +var FirstServer = servers.Server{ + ID: "194573e4-8f53-4ee4-806f-d9b2db74a380", + Name: "GP2v1", + Links: []servers.Link{ + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + Rel: "self", + }, + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + Rel: "bookmark", + }, + }, + BaremetalServer: servers.BaremetalServer{ + ID: "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Name: "GP2v1", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "bookmark", + }, + }, + }, +} + +// SecondServer is the second resource in the List request. +var SecondServer = servers.Server{ + ID: "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Name: "test", + Links: []servers.Link{ + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Rel: "self", + }, + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Rel: "bookmark", + }, + }, + BaremetalServer: servers.BaremetalServer{ + ID: "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Name: "test", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "bookmark", + }, + }, + }, +} + +// ExpectedServersSlice is the slice of resources expected to be returned from ListResult. +var ExpectedServersSlice = []servers.Server{FirstServer, SecondServer} + +// FirstServerDetail is the first resource in the List details request. +var FirstServerDetail = servers.Server{ + ID: "194573e4-8f53-4ee4-806f-d9b2db74a380", + Name: "GP2v1", + ImageRef: "293063f6-8986-4b79-becd-7a6d28794bb8", + Description: nil, + Status: "ACTIVE", + HypervisorType: "vsphere_esxi", + BaremetalServer: servers.BaremetalServer{ + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "groupb", + Created: time.Date(2019, 10, 18, 7, 42, 35, 0, time.UTC), + Flavor: servers.Flavor{ + ID: "303b4993-cf29-4301-abd0-99512b5413a5", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + Rel: "bookmark", + }, + }, + }, + ID: "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Image: servers.Image{ + ID: "02441adc-0d9a-4e9d-b359-ce23413e7ea7", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/02441adc-0d9a-4e9d-b359-ce23413e7ea7", + Rel: "bookmark", + }, + }, + }, + Metadata: map[string]string{}, + Name: "GP2v1", + Progress: 100, + Status: "ACTIVE", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + Updated: time.Date(2019, 10, 18, 7, 44, 18, 0, time.UTC), + UserID: "55891ce6a3cb4bb0833514667d67288c", + NicPhysicalPorts: []servers.NicPhysicalPort{ + { + ID: "b49c0624-e89a-469f-8c90-7a27ee1f61cb", + MACAddr: "8C:DC:D4:B7:41:48", + Plane: "DATA", + NetworkPhysicalPortID: "4cfbe3b2-a502-485f-82fa-a0949396e567", + HardwareID: "8e1e2fe0-60a7-4211-a891-80e808426708", + AttachedPorts: []servers.Port{ + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "8808acc2-d930-40fb-b382-ce7074baef83", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.11", + }, + }, + }, + { + NetworkID: "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + PortID: "9d3baa16-e0e5-4e50-9677-08dd338e0c14", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + IPAddress: "192.168.4.3", + }, + }, + }, + }, + }, + { + ID: "8bea93c4-721b-480c-8713-f2a4b6e5dbad", + MACAddr: "8C:DC:D4:B7:41:49", + Plane: "STORAGE", + NetworkPhysicalPortID: "ab38075d-128f-4f3d-a16a-c6426375a380", + HardwareID: "8e1e2fe0-60a7-4211-a891-80e808426708", + AttachedPorts: []servers.Port{}, + }, + { + ID: "a7fbca5e-ff49-4ddb-8659-02ec462f98ec", + MACAddr: "8C:DC:D4:B7:45:89", + Plane: "STORAGE", + NetworkPhysicalPortID: "9ef62803-7848-460c-9dae-17fd02606a26", + HardwareID: "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + AttachedPorts: []servers.Port{}, + }, + { + ID: "71af4de1-0c9b-4870-8c95-c7b9b4115bb8", + MACAddr: "8C:DC:D4:B7:45:88", + Plane: "DATA", + NetworkPhysicalPortID: "ad92f33a-eac4-408d-bc27-22d91eccd465", + HardwareID: "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + AttachedPorts: []servers.Port{ + { + NetworkID: "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + PortID: "705bde94-7189-40b1-b8a2-188e6cc3c546", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + IPAddress: "192.168.4.4", + }, + }, + }, + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "7b860eb4-0eb6-4c2a-873e-ccefd6029d97", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.12", + }, + }, + }, + }, + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + Nic: true, + SystemBoard: true, + Etc: true, + Console: true, + }, + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + Partitions: nil, + RaidCardHardwareID: "24184dcf-dc76-4ea2-a34e-bccc6c11d5be", + DiskHardwareIDs: []string{ + "4de7d3df-8e2f-4193-98dc-145f78df29a2", + "de158fab-7bc2-49e3-98d1-9db5451d43e3", + "97087307-cab2-40cb-a84c-86d98da0f393", + }, + }, + }, + LvmVolumeGroups: nil, + Filesystems: nil, + MediaAttachments: []servers.MediaAttachment{}, + ManagedByService: "dedicated-hypervisor", + ManagedServiceResourceID: "194573e4-8f53-4ee4-806f-d9b2db74a380", + }, +} + +var SecondServerDescription = "test" + +// SecondServerDetail is the second resource in the List detail request. +var SecondServerDetail = servers.Server{ + ID: "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Name: "test", + ImageRef: "dfd25820-b368-4012-997b-29a6d0cf8518", + Description: &SecondServerDescription, + Status: "ACTIVE", + HypervisorType: "vsphere_esxi", + BaremetalServer: servers.BaremetalServer{ + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "groupb", + Created: time.Date(2019, 10, 10, 4, 11, 41, 0, time.UTC), + Flavor: servers.Flavor{ + ID: "a830b61c-3155-4a61-b7ed-c450862845e6", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/a830b61c-3155-4a61-b7ed-c450862845e6", + Rel: "bookmark", + }, + }, + }, + ID: "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Image: servers.Image{ + ID: "112a26a0-ff25-4513-afe1-407e41b0a48b", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/112a26a0-ff25-4513-afe1-407e41b0a48b", + Rel: "bookmark", + }, + }, + }, + Metadata: map[string]string{}, + Name: "test", + Progress: 100, + Status: "ACTIVE", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + Updated: time.Date(2019, 10, 10, 4, 14, 8, 0, time.UTC), + UserID: "55891ce6a3cb4bb0833514667d67288c", + NicPhysicalPorts: []servers.NicPhysicalPort{ + { + ID: "a2f63380-6c77-4cd5-8868-e3556ffd35ce", + MACAddr: "48:DF:37:90:B4:58", + Plane: "DATA", + NetworkPhysicalPortID: "d8e40a51-f1e2-4681-8953-9fe1e9992c42", + HardwareID: "be2d30d6-f891-4200-b827-95f229fb8c6b", + AttachedPorts: []servers.Port{ + { + NetworkID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + PortID: "30fc1c27-fb5f-4955-94d0-a56cd28d09e8", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + IPAddress: "2.1.1.10", + }, + }, + }, + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "aa6c61f4-db8a-44c7-a91c-7e636dac1dc6", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.9", + }, + }, + }, + }, + }, + { + ID: "b01dfdb0-f247-47d8-8224-c257aa3265e9", + MACAddr: "48:DF:37:90:B4:50", + Plane: "STORAGE", + NetworkPhysicalPortID: "00dfea92-5c5b-4860-aa05-efef6c2bb2af", + HardwareID: "be2d30d6-f891-4200-b827-95f229fb8c6b", + AttachedPorts: []servers.Port{}, + }, + { + ID: "f4355e8e-39fc-48bd-a283-a2dbef8a2e32", + MACAddr: "48:DF:37:82:B0:A0", + Plane: "STORAGE", + NetworkPhysicalPortID: "cf798cc0-c869-45d5-a5a7-bcc578a300b0", + HardwareID: "84c74a86-7045-4284-80f9-0e7aff5d27ad", + AttachedPorts: []servers.Port{}, + }, + { + ID: "5ef177fd-888c-4fae-9925-a8920beb07cb", + MACAddr: "48:DF:37:82:B0:A8", + Plane: "DATA", + NetworkPhysicalPortID: "2bbbb516-c75a-42b2-8a46-9cb5f26c219e", + HardwareID: "84c74a86-7045-4284-80f9-0e7aff5d27ad", + AttachedPorts: []servers.Port{ + { + NetworkID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + PortID: "4e329a01-2cf4-4028-9259-03b7aa145cb6", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + IPAddress: "2.1.1.20", + }, + }, + }, + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "a256b4a1-3ae3-4102-a14e-987ae1610f97", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.10", + }, + }, + }, + }, + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + Nic: true, + SystemBoard: true, + Etc: true, + Console: true, + }, + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + Partitions: nil, + RaidCardHardwareID: "bdfb75d1-194d-426d-b288-f588dfa5ac49", + DiskHardwareIDs: []string{ + "76649053-863e-4533-86e3-f194a79485a6", + "a25827e3-67da-47be-ba96-849ab4685a1d", + }, + }, + }, + LvmVolumeGroups: nil, + Filesystems: nil, + MediaAttachments: []servers.MediaAttachment{}, + ManagedByService: "dedicated-hypervisor", + ManagedServiceResourceID: "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + }, +} + +// ExpectedServersDetailsSlice is the slice of resources expected to be returned from ListDetailsResult. +var ExpectedServersDetailsSlice = []servers.Server{FirstServerDetail, SecondServerDetail} + +var AddLicenseJob = servers.Job{ + JobID: "b4f888dc2b9d4c41bb769cbd", + Status: "COMPLETED", + RequestedParam: servers.RequestedParam{ + VmName: "Alice", + LicenseTypes: []string{ + "Windows Server", + "SQL Server Standard 2014", + }, + }, +} + +// HandleListServersSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a list of two servers. +func HandleListServersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers", 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, ListResult) + }) +} + +// HandleListServersDetailsSuccessfully creates an HTTP handler at `/servers/detail` on the +// test handler mux that responds with a list of two servers. +func HandleListServersDetailsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", 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, ListDetailsResult) + }) +} + +// HandleGetServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a single server. +func HandleGetServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", SecondServer.ID), 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, GetResult) + }) +} + +// HandleCreateServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server creation. +func HandleCreateServerSuccessfully(t *testing.T) { + 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, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, CreateResponse) + }) +} + +// HandleDeleteServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server deletion. +func HandleDeleteServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", FirstServer.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +func HandleAddLicenseSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", SecondServer.ID), 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, AddLicenseRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, AddLicenseResponse) + }) +} + +func HandleGetAddLicenseResultSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", SecondServer.ID), 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, GetAddLicenseResultRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetAddLicenseResultResponse) + }) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/servers/testing/requests_test.go b/v3/ecl/dedicated_hypervisor/v1/servers/testing/requests_test.go new file mode 100644 index 0000000..5fa8ce8 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/servers/testing/requests_test.go @@ -0,0 +1,155 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/servers" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersSuccessfully(t) + + count := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedServersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListServersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersSuccessfully(t) + + allPages, err := servers.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedServersSlice, actual) +} + +func TestListServersDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + count := 0 + err := servers.ListDetails(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedServersDetailsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListServersDetailsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + allPages, err := servers.ListDetails(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedServersDetailsSlice, actual) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetServerSuccessfully(t) + + actual, err := servers.Get(client.ServiceClient(), SecondServer.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondServerDetail, *actual) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerSuccessfully(t) + + createOpts := servers.CreateOpts{ + Name: "test", + Networks: []servers.Network{ + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + }, + ImageRef: "dfd25820-b368-4012-997b-29a6d0cf8518", + FlavorRef: "a830b61c-3155-4a61-b7ed-c450862845e6", + } + + actual, err := servers.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, SecondServer.ID, actual.ID) + th.AssertDeepEquals(t, SecondServer.Links, actual.Links) + th.AssertEquals(t, "aabbccddeeff", actual.AdminPass) +} + +func TestDeleteResource(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteServerSuccessfully(t) + + res := servers.Delete(client.ServiceClient(), FirstServer.ID) + th.AssertNoErr(t, res.Err) +} + +func TestAddLicense(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAddLicenseSuccessfully(t) + + addLicenseOpts := servers.AddLicenseOpts{ + VmName: "Alice", + LicenseTypes: []string{ + "Windows Server", + "SQL Server Standard 2014", + }, + } + + actual, err := servers.AddLicense(client.ServiceClient(), SecondServer.ID, addLicenseOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, AddLicenseJob.JobID, actual.JobID) +} + +func TestGetAddLicenseResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetAddLicenseResultSuccessfully(t) + + getAddLicenseResultOpts := servers.GetAddLicenseResultOpts{ + JobID: AddLicenseJob.JobID, + } + + actual, err := servers.GetAddLicenseResult(client.ServiceClient(), SecondServer.ID, getAddLicenseResultOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, AddLicenseJob, *actual) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/servers/urls.go b/v3/ecl/dedicated_hypervisor/v1/servers/urls.go new file mode 100644 index 0000000..324aec0 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/servers/urls.go @@ -0,0 +1,27 @@ +package servers + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listDetailsURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func actionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} diff --git a/v3/ecl/dedicated_hypervisor/v1/usages/doc.go b/v3/ecl/dedicated_hypervisor/v1/usages/doc.go new file mode 100644 index 0000000..ce71466 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/usages/doc.go @@ -0,0 +1,39 @@ +/* +Package usages manages and retrieves usage in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List Usages + + listOpts := usages.ListOpts{ + From: "2019-10-10T00:00:00Z", + To: "2019-10-15T00:00:00Z", + LicenseType: "dedicated-hypervisor.guest-image.vcenter-server-6-0-standard", + } + + allPages, err := usages.List(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsages, err := usages.ExtractUsages(allPages) + if err != nil { + panic(err) + } + + for _, usage := range allUsages { + fmt.Printf("%+v\n", usage) + } + +Example to Get Usage Histories + + getHistoriesOpts := usages.GetHistoriesOpts{ + From: "2019-10-10T00:00:00Z", + To: "2019-10-15T00:00:00Z", + } + + usageID := "9ada4c06-a2a4-46d5-b969-72ac12433a79" + histories, err := usages.GetHistories(client, usageID, getHistoriesOpts).ExtractHistories() + if err != nil { + panic(err) + } +*/ +package usages diff --git a/v3/ecl/dedicated_hypervisor/v1/usages/requests.go b/v3/ecl/dedicated_hypervisor/v1/usages/requests.go new file mode 100644 index 0000000..bb46491 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/usages/requests.go @@ -0,0 +1,73 @@ +package usages + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToResourceListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + From string `q:"from"` + To string `q:"to"` + LicenseType string `q:"license_type"` +} + +// ToResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Usages. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UsagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetHistoriesOpts provides options to filter the GetHistories results. +type GetHistoriesOpts struct { + From string `q:"from"` + To string `q:"to"` +} + +// ToResourceListQuery formats a GetHistoriesOpts into a query string. +func (opts GetHistoriesOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// GetHistories retrieves details of usage histories. +func GetHistories(client *eclcloud.ServiceClient, usageID string, opts ListOptsBuilder) (r GetHistoriesResult) { + url := getHistoriesURL(client, usageID) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + r.Err = err + return + } + url += query + } + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/v3/ecl/dedicated_hypervisor/v1/usages/results.go b/v3/ecl/dedicated_hypervisor/v1/usages/results.go new file mode 100644 index 0000000..f1362c0 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/usages/results.go @@ -0,0 +1,89 @@ +package usages + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Usage represents guest image usage information. +type Usage struct { + ID string `json:"id"` + Type string `json:"type"` + Value string `json:"value"` + Unit string `json:"unit"` + Name string `json:"name"` + Description string `json:"description"` + HasLicenseKey bool `json:"has_license_key"` + ResourceID string `json:"resource_id"` +} + +type UsageHistories struct { + Unit string `json:"unit"` + ResourceID string `json:"resource_id"` + LicenseType string `json:"license_type"` + Histories []History `json:"histories"` +} + +type History struct { + Time time.Time `json:"-"` + Value string `json:"value"` +} + +func (h *History) UnmarshalJSON(b []byte) error { + type tmp History + var s struct { + tmp + Time eclcloud.JSONRFC3339ZNoTNoZ `json:"time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *h = History(s.tmp) + + h.Time = time.Time(s.Time) + + return err +} + +type commonResult struct { + eclcloud.Result +} + +// GetHistoriesResult is the response from a Get operation. Call its Extract method +// to interpret it as usage histories. +type GetHistoriesResult struct { + commonResult +} + +// UsagePage is a single page of Usage results. +type UsagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Usages contains any results. +func (r UsagePage) IsEmpty() (bool, error) { + usages, err := ExtractUsages(r) + return len(usages) == 0, err +} + +// ExtractUsages returns a slice of Usages contained in a single page of +// results. +func ExtractUsages(r pagination.Page) ([]Usage, error) { + var s struct { + Usages []Usage `json:"usages"` + } + err := (r.(UsagePage)).ExtractInto(&s) + return s.Usages, err +} + +// ExtractHistories interprets any commonResult as usage histories. +func (r commonResult) ExtractHistories() (*UsageHistories, error) { + var s UsageHistories + err := r.ExtractInto(&s) + return &s, err +} diff --git a/v3/ecl/dedicated_hypervisor/v1/usages/testing/fixtures.go b/v3/ecl/dedicated_hypervisor/v1/usages/testing/fixtures.go new file mode 100644 index 0000000..af8b354 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/usages/testing/fixtures.go @@ -0,0 +1,140 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/usages" + + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// ListResult provides a single page of Usage results. +const ListResult = ` +{ + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "usages": [ + { + "description": "vCenter Server 6.x Standard", + "has_license_key": true, + "id": "9ada4c06-a2a4-46d5-b969-72ac12433a79", + "name": "vCenter Server 6.x Standard", + "resource_id": "1bc271e7a8af4d988ff91612f5b122f8", + "type": "dedicated-hypervisor.guest-image.vcenter-server-6-0-standard", + "unit": "License", + "value": "2" + }, + { + "description": "SQL Server 2014 Standard Edition", + "has_license_key": false, + "id": "9da9116d-cc44-4ad8-aca5-7db398fcb478", + "name": "SQL Server 2014 Standard Edition", + "resource_id": "d-cc44-4ad8-aca5-7db398fcb477bbbbbb", + "type": "dedicated-hypervisor.guest-image.sql-server-2014-standard", + "unit": "vCPU", + "value": "6" + } + ] +} +` + +// FirstUsage is the first Usage in the List request. +var FirstUsage = usages.Usage{ + ID: "9ada4c06-a2a4-46d5-b969-72ac12433a79", + Type: "dedicated-hypervisor.guest-image.vcenter-server-6-0-standard", + Value: "2", + Unit: "License", + Name: "vCenter Server 6.x Standard", + Description: "vCenter Server 6.x Standard", + HasLicenseKey: true, + ResourceID: "1bc271e7a8af4d988ff91612f5b122f8", +} + +// SecondUsage is the second Usage in the List request. +var SecondUsage = usages.Usage{ + ID: "9da9116d-cc44-4ad8-aca5-7db398fcb478", + Type: "dedicated-hypervisor.guest-image.sql-server-2014-standard", + Value: "6", + Unit: "vCPU", + Name: "SQL Server 2014 Standard Edition", + Description: "SQL Server 2014 Standard Edition", + HasLicenseKey: false, + ResourceID: "d-cc44-4ad8-aca5-7db398fcb477bbbbbb", +} + +// ExpectedUsagesSlice is the slice of LicenseTypes expected to be returned from ListResult. +var ExpectedUsagesSlice = []usages.Usage{FirstUsage, SecondUsage} + +// HandleListUsagesSuccessfully creates an HTTP handler at `/usages` on the +// test handler mux that responds with a list of two Usages. +func HandleListUsagesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/usages", 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, ListResult) + }) +} + +const usageID = "9ada4c06-a2a4-46d5-b969-72ac12433a79" + +// GetHistoriesResult provides a single page of Usage results. +const GetHistoriesResult = ` +{ + "description": "vCenter Server 6.x Standard", + "histories": [ + { + "time": "2019-10-10 00:00:00", + "value": "1" + }, + { + "time": "2019-10-10 01:00:00", + "value": "1" + } + ], + "license_type": "vCenter Server 6.x Standard", + "resource_id": "1bc271e7a8af4d988ff91612f5b122f8", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "unit": "License" +} +` + +// FirstHistory is the first History in the Get histories request. +var FirstHistory = usages.History{ + Time: time.Date(2019, 10, 10, 0, 0, 0, 0, time.UTC), + Value: "1", +} + +// SecondHistory is the second History in the Get histories request. +var SecondHistory = usages.History{ + Time: time.Date(2019, 10, 10, 1, 0, 0, 0, time.UTC), + Value: "1", +} + +// ExpectedHistories is the UsageHistories expected to be returned from GetHistoriesResult. +var ExpectedHistories = &usages.UsageHistories{ + Unit: "License", + ResourceID: "1bc271e7a8af4d988ff91612f5b122f8", + LicenseType: "vCenter Server 6.x Standard", + Histories: []usages.History{FirstHistory, SecondHistory}, +} + +// HandleGetHistoriesSuccessfully creates an HTTP handler at `/usages//histories` on the +// test handler mux that responds with usage histories. +func HandleGetHistoriesSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/usages/%s/histories", usageID), 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, GetHistoriesResult) + }) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/usages/testing/requests_test.go b/v3/ecl/dedicated_hypervisor/v1/usages/testing/requests_test.go new file mode 100644 index 0000000..b05a940 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/usages/testing/requests_test.go @@ -0,0 +1,55 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/dedicated_hypervisor/v1/usages" + + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListUsages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsagesSuccessfully(t) + + count := 0 + err := usages.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := usages.ExtractUsages(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedUsagesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListUsagesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsagesSuccessfully(t) + + allPages, err := usages.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := usages.ExtractUsages(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedUsagesSlice, actual) +} + +func TestGetUsageHistories(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetHistoriesSuccessfully(t) + + result := usages.GetHistories(client.ServiceClient(), usageID, nil) + th.AssertNoErr(t, result.Err) + actual, err := result.ExtractHistories() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedHistories, actual) +} diff --git a/v3/ecl/dedicated_hypervisor/v1/usages/urls.go b/v3/ecl/dedicated_hypervisor/v1/usages/urls.go new file mode 100644 index 0000000..30f3ac8 --- /dev/null +++ b/v3/ecl/dedicated_hypervisor/v1/usages/urls.go @@ -0,0 +1,11 @@ +package usages + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("usages") +} + +func getHistoriesURL(client *eclcloud.ServiceClient, usageID string) string { + return client.ServiceURL("usages", usageID, "histories") +} diff --git a/v3/ecl/dns/v2/recordsets/doc.go b/v3/ecl/dns/v2/recordsets/doc.go new file mode 100644 index 0000000..cc65bed --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/doc.go @@ -0,0 +1,54 @@ +/* +Package recordsets provides information and interaction with the zone API +resource for the Enterprise Cloud DNS service. + +Example to List RecordSets by Zone + + listOpts := recordsets.ListOpts{ + Type: "A", + } + + zoneID := "fff121f5-c506-410a-a69e-2d73ef9cbdbd" + + allPages, err := recordsets.ListByZone(dnsClient, zoneID, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRRs, err := recordsets.ExtractRecordSets(allPages() + if err != nil { + panic(err) + } + + for _, rr := range allRRs { + fmt.Printf("%+v\n", rr) + } + +Example to Create a RecordSet + + createOpts := recordsets.CreateOpts{ + Name: "example.com.", + Type: "A", + TTL: 3600, + Description: "This is a recordset.", + Records: []string{"10.1.0.2"}, + } + + zoneID := "fff121f5-c506-410a-a69e-2d73ef9cbdbd" + + rr, err := recordsets.Create(dnsClient, zoneID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a RecordSet + + zoneID := "fff121f5-c506-410a-a69e-2d73ef9cbdbd" + recordsetID := "d96ed01a-b439-4eb8-9b90-7a9f71017f7b" + + err := recordsets.Delete(dnsClient, zoneID, recordsetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package recordsets diff --git a/v3/ecl/dns/v2/recordsets/requests.go b/v3/ecl/dns/v2/recordsets/requests.go new file mode 100644 index 0000000..b946464 --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/requests.go @@ -0,0 +1,162 @@ +package recordsets + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToRecordSetListQuery() (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 { + ZoneID string `q:"zone_id"` + + // Domain name of zone for partial-match search. + DomainName string `q:"data"` + + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + + // UUID of the recordset 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"` +} + +// ToRecordSetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRecordSetListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListByZone implements the recordset list request. +func ListByZone(client *eclcloud.ServiceClient, zoneID string, opts ListOptsBuilder) pagination.Pager { + url := baseURL(client, zoneID) + if opts != nil { + query, err := opts.ToRecordSetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RecordSetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get implements the recordset Get request. +func Get(client *eclcloud.ServiceClient, zoneID string, rrsetID string) (r GetResult) { + _, r.Err = client.Get(rrsetURL(client, zoneID, rrsetID), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional attributes to the +// Create request. +type CreateOptsBuilder interface { + ToRecordSetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies the base attributes that may be used to create a +// RecordSet. +type CreateOpts struct { + // Name is the name of the RecordSet. + Name string `json:"name" required:"true"` + + // Description is a description of the RecordSet. + Description string `json:"description,omitempty"` + + // Records are the DNS records of the RecordSet. + Records []string `json:"records,omitempty"` + + // TTL is the time to live of the RecordSet. + TTL int `json:"ttl,omitempty"` + + // Type is the record type of the RecordSet. + Type string `json:"type" required:"true"` +} + +// ToRecordSetCreateMap formats an CreateOpts structure into a request body. +func (opts CreateOpts) ToRecordSetCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create creates a recordset in a given zone. +func Create(client *eclcloud.ServiceClient, zoneID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRecordSetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client, zoneID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201, 202}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToRecordSetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// RecordSet. +type UpdateOpts struct { + // Name is the name of the RecordSet. + Name *string `json:"name,omitempty"` + + // Description is a description of the RecordSet. + Description *string `json:"description,omitempty"` + + // TTL is the time to live of the RecordSet. + TTL *int `json:"ttl,omitempty"` + + // Records are the DNS records of the RecordSet. + Records *[]string `json:"records,omitempty"` +} + +// ToRecordSetUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToRecordSetUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update updates a recordset in a given zone +func Update(client *eclcloud.ServiceClient, zoneID string, rrsetID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRecordSetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(rrsetURL(client, zoneID, rrsetID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete removes an existing RecordSet. +func Delete(client *eclcloud.ServiceClient, zoneID string, rrsetID string) (r DeleteResult) { + _, r.Err = client.Delete( + rrsetURL(client, zoneID, rrsetID), + &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} diff --git a/v3/ecl/dns/v2/recordsets/results.go b/v3/ecl/dns/v2/recordsets/results.go new file mode 100644 index 0000000..29572b1 --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/results.go @@ -0,0 +1,163 @@ +package recordsets + +import ( + "encoding/json" + "fmt" + // "log" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a RecordSet. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*RecordSet, error) { + var s *RecordSet + err := r.ExtractInto(&s) + return s, err +} + +func (r commonResult) ExtractCreatedRecordSet() (*RecordSet, error) { + var sl []*RecordSet + err := r.ExtractIntoSlicePtr(&sl, "recordsets") + if err != nil { + return nil, fmt.Errorf("[Error] Error in parsing result of recordset create %s", err) + } + return sl[0], nil +} + +// CreateResult is the result of a Create operation. Call its Extract method to +// interpret the result as a RecordSet. +type CreateResult struct { + commonResult +} + +// GetResult is the result of a Get operation. Call its Extract method to +// interpret the result as a RecordSet. +type GetResult struct { + commonResult +} + +// RecordSetPage is a single page of RecordSet results. +type RecordSetPage struct { + pagination.LinkedPageBase +} + +// UpdateResult is result of an Update operation. Call its Extract method to +// interpret the result as a RecordSet. +type UpdateResult struct { + commonResult +} + +// DeleteResult is result of a Delete operation. Call its ExtractErr method to +// determine if the operation succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// IsEmpty returns true if the page contains no results. +func (r RecordSetPage) IsEmpty() (bool, error) { + s, err := ExtractRecordSets(r) + return len(s) == 0, err +} + +// ExtractRecordSets extracts a slice of RecordSets from a List result. +func ExtractRecordSets(r pagination.Page) ([]RecordSet, error) { + var s struct { + RecordSets []RecordSet `json:"recordsets"` + } + err := (r.(RecordSetPage)).ExtractInto(&s) + return s.RecordSets, err +} + +// RecordSet represents a DNS Record Set. +type RecordSet struct { + // ID is the unique ID of the recordset + ID string `json:"id"` + + // ZoneID is the ID of the zone the recordset belongs to. + ZoneID string `json:"zone_id"` + + // ProjectID is the ID of the project that owns the recordset. + // ProjectID string `json:"project_id"` + + // Name is the name of the recordset. + Name string `json:"name"` + + // Type is the RRTYPE of the recordset. + Type string `json:"type"` + + // Records are the DNS records of the recordset. + // This is original code. + // Records []string `json:"records"` + // + // But in ECL2.0, record set will be returned as simple string + // e.g. + // Usual response(like creation) reccordset: "[10.0.0.1]" + // Update response(like creation) reccordset: "10.0.0.1]" + Records interface{} `json:"records"` + + // TTL is the time to live of the recordset. + TTL int `json:"ttl"` + + // Description is the description of the recordset. + Description string `json:"description"` + + // Version is the revision of the recordset. + Version int `json:"version"` + + // CreatedAt is the date when the recordset was created. + CreatedAt time.Time `json:"-"` + + // UpdatedAt is the date when the recordset was updated. + UpdatedAt time.Time `json:"-"` + + // Status is the current status of recordset. + Status string `json:"status"` + + // Current action in progress on the resource. + // This parameter is not currently supported. it always return an empty. + Action string `json:"action"` + + // Links includes HTTP references to the itself, + // useful for passing along to other APIs that might want a recordset + // reference. + Links []eclcloud.Link `json:"-"` +} + +func (r *RecordSet) UnmarshalJSON(b []byte) error { + type tmp RecordSet + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + Links map[string]interface{} `json:"links"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = RecordSet(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + if s.Links != nil { + for rel, href := range s.Links { + if v, ok := href.(string); ok { + link := eclcloud.Link{ + Rel: rel, + Href: v, + } + r.Links = append(r.Links, link) + } + } + } + + return err +} diff --git a/v3/ecl/dns/v2/recordsets/testing/doc.go b/v3/ecl/dns/v2/recordsets/testing/doc.go new file mode 100644 index 0000000..f4d91dc --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/testing/doc.go @@ -0,0 +1,2 @@ +// recordsets unit tests +package testing diff --git a/v3/ecl/dns/v2/recordsets/testing/fixtures.go b/v3/ecl/dns/v2/recordsets/testing/fixtures.go new file mode 100644 index 0000000..3190a8e --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/testing/fixtures.go @@ -0,0 +1,320 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/dns/v2/recordsets" +) + +const idZone = "4eb5c333-7031-48ff-a247-0eeccc57472e" + +const idRecordSet1 = "f1dcc528-6b94-47aa-9b13-b62a356ed44f" +const idRecordSet2 = "a0b9df1e-fa38-47a5-855d-f15927fea579" + +const nameRecordSet1 = "rs1.zone1.com." +const nameRecordSet1Update = "rs1update.zone1.com." + +const descriptionRecordSet1 = "a record set 1" +const descriptionRecordSet1Update = "a record set 1-updated" + +const ipRecordSet1 = "10.1.0.0" +const ipRecordSet1Update = "10.1.0.1" + +const TTLRecordSet1 = 3000 +const TTLRecordSet1Update = 3600 + +const recordSetCreatedAt = "2019-02-05T23:41:57" +const recordSetUpdatedAt = "2019-02-05T23:42:13" + +// ListResponse is a sample response to a TestListDNSRecordSet call. +var ListResponse = fmt.Sprintf(`{ + "recordsets": [{ + "id": "%s", + "action": "", + "name": "%s", + "ttl": %d, + "description": "%s", + "records": ["%s"], + "type": "A", + "version": 1, + "status": "ACTIVE", + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "links": { + "self": "dummylink" + } + }, { + "id": "%s", + "action": "", + "name": "rs2.zone1.com.", + "ttl": 3000, + "description": "a record set 2", + "records": ["20.1.0.0"], + "type": "A", + "version": 1, + "status": "ACTIVE", + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "links": { + "self": "dummylink" + } + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 2 + } +}`, + // For recordSet1 + idRecordSet1, + nameRecordSet1, + TTLRecordSet1, + descriptionRecordSet1, + ipRecordSet1, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, + // For recordSet2 + idRecordSet2, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, +) + +// ListResponseLimited is a sample response with limit query option. +var ListResponseLimited = fmt.Sprintf(`{ + "recordsets": [{ + "id": "%s", + "action": "", + "name": "rs2.zone1.com.", + "ttl": 3000, + "description": "a record set 2", + "records": ["20.1.0.0"], + "type": "A", + "version": 1, + "status": "ACTIVE", + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "links": { + "self": "dummylink" + } + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 1 + } +}`, + idRecordSet2, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, +) + +// RecordSetCreatedAt is mocked created time of each records. +var RecordSetCreatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, recordSetCreatedAt) + +// RecordSetUpdatedAt is mocked updated time of each records. +var RecordSetUpdatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, recordSetUpdatedAt) + +// FirstRecordSet is initialized struct as actual response +var FirstRecordSet = recordsets.RecordSet{ + ID: idRecordSet1, + Description: descriptionRecordSet1, + Records: []string{ipRecordSet1}, + TTL: TTLRecordSet1, + Name: nameRecordSet1, + ZoneID: idZone, + CreatedAt: RecordSetCreatedAt, + UpdatedAt: RecordSetUpdatedAt, + Version: 1, + Type: "A", + Status: "ACTIVE", + Action: "", + Links: []eclcloud.Link{ + { + Rel: "self", + Href: "dummylink", + }, + }, +} + +// SecondRecordSet is initialized struct as actual response +var SecondRecordSet = recordsets.RecordSet{ + ID: idRecordSet2, + Description: "a record set 2", + Records: []string{"20.1.0.0"}, + TTL: 3000, + Name: "rs2.zone1.com.", + ZoneID: idZone, + CreatedAt: RecordSetCreatedAt, + UpdatedAt: RecordSetUpdatedAt, + Version: 1, + Type: "A", + Status: "ACTIVE", + Action: "", + Links: []eclcloud.Link{ + { + Rel: "self", + Href: "dummylink", + }, + }, +} + +// ExpectedRecordSetSlice is the slice of results that should be parsed +// from ListByZoneOutput, in the expected order. +var ExpectedRecordSetSlice = []recordsets.RecordSet{FirstRecordSet, SecondRecordSet} + +// ExpectedRecordSetSliceLimited is the slice of limited results that should be parsed +// from ListByZoneOutput. +var ExpectedRecordSetSliceLimited = []recordsets.RecordSet{SecondRecordSet} + +// GetResponse is a sample response to a Get call. +var GetResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "ttl": %d, + "description": "%s", + "records": ["%s"], + "type": "A", + "version": 1, + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "status": "ACTIVE", + "links": { + "self": "dummylink" + } +}`, + idRecordSet1, + nameRecordSet1, + TTLRecordSet1, + descriptionRecordSet1, + ipRecordSet1, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, +) + +const selfURL = "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1" +const nextURL = "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1&marker=f7b10e9b-0cae-4a91-b162-562bc6096648" + +// NextPageRequest is a sample request to test pagination. +var NextPageRequest = fmt.Sprintf(` +{ + "links": { + "self": "%s", + "next": "%s" + } +}`, selfURL, nextURL) + +// CreateRequest is a sample request to create a resource record. +var CreateRequest = fmt.Sprintf(`{ + "name" : "%s", + "description" : "%s", + "type" : "A", + "ttl" : %d, + "records" : ["%s"] +}`, + nameRecordSet1, + descriptionRecordSet1, + TTLRecordSet1, + ipRecordSet1, +) + +// CreateResponse is a sample response to a create request. +var CreateResponse = fmt.Sprintf(`{ + "recordsets": [{ + "id": "%s", + "zone_id": "%s", + "records": ["%s"], + "ttl": %d, + "name": "%s", + "description": "%s", + "type": "A", + "version": 1, + "created_at": "", + "updated_at": null, + "links": { + "self": "dummylink" + } + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 1 + } +}`, idRecordSet1, + idZone, + ipRecordSet1, + TTLRecordSet1, + nameRecordSet1, + descriptionRecordSet1, +) + +// UpdateRequest is a sample request to update a record set. +var UpdateRequest = fmt.Sprintf(`{ + "name": "%s", + "description" : "%s", + "ttl" : %d, + "records" : ["%s"] +}`, + nameRecordSet1Update, + descriptionRecordSet1Update, + TTLRecordSet1Update, + ipRecordSet1Update, +) + +// UpdateResponse is a sample response to an update request. +var UpdateResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "ttl": %d, + "description": "%s", + "records": "%s", + "type": "A", + "version": 1, + "created_at": null, + "updated_at": null, + "zone_id": "%s", + "links": { + "self": "dummylink" + } +}`, + idRecordSet1, + nameRecordSet1Update, + TTLRecordSet1Update, + descriptionRecordSet1Update, + ipRecordSet1Update, + idZone, +) + +// UpdatedRecordSet is initialized struct as actual response of update +var UpdatedRecordSet = recordsets.RecordSet{ + ID: idRecordSet1, + Name: nameRecordSet1Update, + TTL: TTLRecordSet1Update, + Description: descriptionRecordSet1Update, + Records: ipRecordSet1Update, + Type: "A", + Version: 1, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + ZoneID: idZone, + // Status: "", + // Action: "", + Links: []eclcloud.Link{ + { + Rel: "self", + Href: "dummylink", + }, + }, +} diff --git a/v3/ecl/dns/v2/recordsets/testing/requests_test.go b/v3/ecl/dns/v2/recordsets/testing/requests_test.go new file mode 100644 index 0000000..df55fec --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/testing/requests_test.go @@ -0,0 +1,204 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/dns/v2/recordsets" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + prepareMuxForListResponse(t) + + count := 0 + err := recordsets.ListByZone(fakeclient.ServiceClient(), idZone, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := recordsets.ExtractRecordSets(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedRecordSetSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDNSRecordSetLimited(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + prepareMuxForListResponse(t) + + count := 0 + listOpts := recordsets.ListOpts{ + Limit: 1, + Marker: idRecordSet1, + } + err := recordsets.ListByZone(fakeclient.ServiceClient(), idZone, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := recordsets.ExtractRecordSets(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedRecordSetSliceLimited, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDNSRecordSetAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + prepareMuxForListResponse(t) + + allPages, err := recordsets.ListByZone(fakeclient.ServiceClient(), idZone, nil).AllPages() + th.AssertNoErr(t, err) + allRecordSets, err := recordsets.ExtractRecordSets(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allRecordSets)) +} + +func prepareMuxForListResponse(t *testing.T) { + url := fmt.Sprintf("/zones/%s/recordsets", idZone) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + + marker := r.Form.Get("marker") + switch marker { + case idRecordSet1: + fmt.Fprintf(w, ListResponseLimited) + case "": + fmt.Fprintf(w, ListResponse) + } + }) +} + +func TestGetDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets/%s", idZone, idRecordSet1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetResponse) + }) + + actual, err := recordsets.Get(fakeclient.ServiceClient(), idZone, idRecordSet1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstRecordSet, actual) +} + +func TestNextPageURL(t *testing.T) { + var page recordsets.RecordSetPage + var body map[string]interface{} + err := json.Unmarshal([]byte(NextPageRequest), &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + expected := nextURL + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} + +func TestCreateDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets", idZone) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := recordsets.CreateOpts{ + Name: nameRecordSet1, + Type: "A", + TTL: TTLRecordSet1, + Description: descriptionRecordSet1, + Records: []string{ipRecordSet1}, + } + + // Clone FirstRecord into CreatedRecordSet(Created result struct) + CreatedRecordSet := FirstRecordSet + CreatedRecordSet.CreatedAt = time.Time{} + CreatedRecordSet.UpdatedAt = time.Time{} + CreatedRecordSet.Status = "" + + actual, err := recordsets.Create(fakeclient.ServiceClient(), idZone, createOpts).ExtractCreatedRecordSet() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedRecordSet, actual) +} + +func TestUpdateDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets/%s", idZone, idRecordSet1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, UpdateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameRecordSet1Update + ttl := TTLRecordSet1Update + description := descriptionRecordSet1Update + records := []string{ipRecordSet1Update} + + updateOpts := recordsets.UpdateOpts{ + Name: &name, + TTL: &ttl, + Description: &description, + Records: &records, + } + + actual, err := recordsets.Update( + fakeclient.ServiceClient(), idZone, idRecordSet1, updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UpdatedRecordSet, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets/%s", idZone, idRecordSet1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + rs := recordsets.Delete(fakeclient.ServiceClient(), idZone, idRecordSet1) + th.AssertNoErr(t, rs.Err) +} diff --git a/v3/ecl/dns/v2/recordsets/urls.go b/v3/ecl/dns/v2/recordsets/urls.go new file mode 100644 index 0000000..7f63a05 --- /dev/null +++ b/v3/ecl/dns/v2/recordsets/urls.go @@ -0,0 +1,11 @@ +package recordsets + +import "github.com/nttcom/eclcloud/v3" + +func baseURL(c *eclcloud.ServiceClient, zoneID string) string { + return c.ServiceURL("zones", zoneID, "recordsets") +} + +func rrsetURL(c *eclcloud.ServiceClient, zoneID string, rrsetID string) string { + return c.ServiceURL("zones", zoneID, "recordsets", rrsetID) +} diff --git a/v3/ecl/dns/v2/zones/doc.go b/v3/ecl/dns/v2/zones/doc.go new file mode 100644 index 0000000..9a56cd5 --- /dev/null +++ b/v3/ecl/dns/v2/zones/doc.go @@ -0,0 +1,48 @@ +/* +Package zones provides information and interaction with the zone API +resource for the Enterprise Cloud DNS service. + +Example to List Zones + + listOpts := zones.ListOpts{ + Email: "jdoe@example.com", + } + + allPages, err := zones.List(dnsClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allZones, err := zones.ExtractZones(allPages) + if err != nil { + panic(err) + } + + for _, zone := range allZones { + fmt.Printf("%+v\n", zone) + } + +Example to Create a Zone + + createOpts := zones.CreateOpts{ + Name: "example.com.", + Email: "jdoe@example.com", + Type: "PRIMARY", + TTL: 7200, + Description: "This is a zone.", + } + + zone, err := zones.Create(dnsClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Zone + + zoneID := "99d10f68-5623-4491-91a0-6daafa32b60e" + err := zones.Delete(dnsClient, zoneID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package zones diff --git a/v3/ecl/dns/v2/zones/requests.go b/v3/ecl/dns/v2/zones/requests.go new file mode 100644 index 0000000..81d5065 --- /dev/null +++ b/v3/ecl/dns/v2/zones/requests.go @@ -0,0 +1,173 @@ +package zones + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add parameters to the List request. +type ListOptsBuilder interface { + ToZoneListQuery() (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 { + // Domain name of zone for partial-match search. + DomainName string `q:"domain_name"` + // Sorts the response by the attribute value. A valid value is only domain_name. + SortKey string `q:"sort_key"` + // Sorts the response by the requested sort direction. + // A valid value is asc (ascending) or desc (descending). Default is asc. + SortDir string `q:"sort_dir"` + // UUID of the zone 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"` + + // Following are original designate parameters. + // But can not be used in ECL2.0 + // TODO: Remove them at last of development. + // + // Description string `q:"description"` + // Email string `q:"email"` + // Name string `q:"name"` + // Status string `q:"status"` + // TTL int `q:"ttl"` + // Type string `q:"type"` +} + +// ToZoneListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToZoneListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List implements a zone List request. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := baseURL(client) + if opts != nil { + query, err := opts.ToZoneListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ZonePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns information about a zone, given its ID. +func Get(client *eclcloud.ServiceClient, zoneID string) (r GetResult) { + _, r.Err = client.Get(zoneURL(client, zoneID), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional attributes to the +// Create request. +type CreateOptsBuilder interface { + ToZoneCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies the attributes used to create a zone. +type CreateOpts struct { + // Description of the zone. + Description string `json:"description,omitempty"` + + // Email contact of the zone. + Email string `json:"email,omitempty"` + + // Name of the zone. + Name string `json:"name" required:"true"` + + // Masters specifies zone masters if this is a secondary zone. + Masters []string `json:"masters,omitempty"` + + // TTL is the time to live of the zone. + TTL int `json:"-"` + + // Type specifies if this is a primary or secondary zone. + Type string `json:"type,omitempty"` +} + +// ToZoneCreateMap formats an CreateOpts structure into a request body. +func (opts CreateOpts) ToZoneCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.TTL > 0 { + b["ttl"] = opts.TTL + } + + return b, nil +} + +// Create implements a zone create request. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToZoneCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201, 202}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToZoneUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the attributes to update a zone. +type UpdateOpts struct { + // Description of the zone. + Description *string `json:"description,omitempty"` + + // TTL is the time to live of the zone. + TTL *int `json:"ttl,omitempty"` + + // Masters specifies zone masters if this is a secondary zone. + Masters *[]string `json:"masters,omitempty"` + + // Email contact of the zone. + Email *string `json:"email,omitempty"` +} + +// ToZoneUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToZoneUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update implements a zone update request. +func Update(client *eclcloud.ServiceClient, zoneID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToZoneUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(zoneURL(client, zoneID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete implements a zone delete request. +func Delete(client *eclcloud.ServiceClient, zoneID string) (r DeleteResult) { + _, r.Err = client.Delete(zoneURL(client, zoneID), &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} diff --git a/v3/ecl/dns/v2/zones/results.go b/v3/ecl/dns/v2/zones/results.go new file mode 100644 index 0000000..8046ed2 --- /dev/null +++ b/v3/ecl/dns/v2/zones/results.go @@ -0,0 +1,166 @@ +package zones + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a Zone. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Zone, error) { + var s *Zone + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult is the result of a Create request. Call its Extract method +// to interpret the result as a Zone. +type CreateResult struct { + commonResult +} + +// GetResult is the result of a Get request. Call its Extract method +// to interpret the result as a Zone. +type GetResult struct { + commonResult +} + +// UpdateResult is the result of an Update request. Call its Extract method +// to interpret the result as a Zone. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method +// to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// ZonePage is a single page of Zone results. +type ZonePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (r ZonePage) IsEmpty() (bool, error) { + s, err := ExtractZones(r) + return len(s) == 0, err +} + +// ExtractZones extracts a slice of Zones from a List result. +func ExtractZones(r pagination.Page) ([]Zone, error) { + var s struct { + Zones []Zone `json:"zones"` + } + err := (r.(ZonePage)).ExtractInto(&s) + return s.Zones, err +} + +// Zone represents a DNS zone. +type Zone struct { + // ID uniquely identifies this zone amongst all other zones, including those + // not accessible to the current tenant. + ID string `json:"id"` + + // PoolID is the ID for the pool hosting this zone. + PoolID string `json:"pool_id"` + + // ProjectID identifies the project/tenant owning this resource. + ProjectID string `json:"project_id"` + + // Name is the DNS Name for the zone. + Name string `json:"name"` + + // Email for the zone. Used in SOA records for the zone. + Email string `json:"email"` + + // TTL is the Time to Live for the zone. + TTL int `json:"ttl"` + + // Serial is the current serial number for the zone. + Serial int `json:"-"` + + // Status is the status of the resource. + Status string `json:"status"` + + // Description for this zone. + Description string `json:"description"` + + // Masters is the servers for slave servers to get DNS information from. + Masters []string `json:"masters"` + + // Type of zone. Primary is controlled by Designate. + // Secondary zones are slaved from another DNS Server. + // Defaults to Primary. + Type string `json:"type"` + + // TransferredAt is the last time an update was retrieved from the + // master servers. + TransferredAt time.Time `json:"-"` + + // Version of the resource. + Version int `json:"version"` + + // CreatedAt is the date when the zone was created. + CreatedAt time.Time `json:"-"` + + // UpdatedAt is the date when the last change was made to the zone. + UpdatedAt time.Time `json:"-"` + + // Action for the zone. + Action string `json:"action"` + + // Attributes for the zone. + Attributes []string `json:"attributes"` + + // Links includes HTTP references to the itself, useful for passing along + // to other APIs that might want a server reference. + Links map[string]interface{} `json:"links"` +} + +func (r *Zone) UnmarshalJSON(b []byte) error { + type tmp Zone + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + TransferredAt eclcloud.JSONRFC3339MilliNoZ `json:"transferred_at"` + Serial interface{} `json:"serial"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Zone(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.TransferredAt = time.Time(s.TransferredAt) + + switch t := s.Serial.(type) { + case float64: + r.Serial = int(t) + case string: + switch t { + case "": + r.Serial = 0 + default: + serial, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Serial = int(serial) + } + } + + return err +} diff --git a/v3/ecl/dns/v2/zones/testing/doc.go b/v3/ecl/dns/v2/zones/testing/doc.go new file mode 100644 index 0000000..b9b6286 --- /dev/null +++ b/v3/ecl/dns/v2/zones/testing/doc.go @@ -0,0 +1,2 @@ +// zones unit tests +package testing diff --git a/v3/ecl/dns/v2/zones/testing/fixtures.go b/v3/ecl/dns/v2/zones/testing/fixtures.go new file mode 100644 index 0000000..797bd9d --- /dev/null +++ b/v3/ecl/dns/v2/zones/testing/fixtures.go @@ -0,0 +1,324 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/dns/v2/zones" +) + +const idZone1 = "dcbd3d17-26ce-461d-b77c-a8774cafee75" +const idZone2 = "db511e50-3b1d-4805-98c6-a00adeb6ded0" + +const nameZone1 = "myzone.com." + +const descriptionZone1 = "this is my zone" +const descriptionZone1Update = "this is my zone-update" + +const tenantID = "9b8f16df551e42f3b905859f28a33d55" + +const zoneCreatedAt = "2019-02-05T06:09:41" +const zoneUpdatedAt = "2019-02-05T06:09:45" + +// ListResponse is a sample response to a List call. +var ListResponse = fmt.Sprintf(` +{ + "api_result": "success", + "zones": [{ + "id": "%s", + "description": "%s", + "project_id": "%s", + "created_at": "%s", + "updated_at": "%s", + "name": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "links": { + "self": "dummylink" + }, + "action": "", + "attributes": [] + }, { + "id": "%s", + "description": "This is my zone 2", + "project_id": "%s", + "created_at": "%s", + "updated_at": "%s", + "name": "myzone2.com.", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "links": { + "self": "dummylink" + }, + "action": "", + "attributes": [] + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 2 + } +}`, + // for Zone1 + idZone1, + descriptionZone1, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, + nameZone1, + // for Zone2 + idZone2, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, +) + +// ExpectedZonesSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedZonesSlice = []zones.Zone{FirstZone, SecondZone} + +// ZoneCreatedAt is parsed zone creation time +var ZoneCreatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, zoneCreatedAt) + +// ZoneUpdatedAt is parsed zone update time +var ZoneUpdatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, zoneUpdatedAt) + +// FirstZone is the mock object of expected zone-1 +var FirstZone = zones.Zone{ + ID: idZone1, + PoolID: "", + ProjectID: tenantID, + Name: nameZone1, + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Description: descriptionZone1, + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + Action: "", + Attributes: []string{}, + Links: map[string]interface{}{ + "self": "dummylink", + }, +} + +// SecondZone is the mock object of expected zone-2 +var SecondZone = zones.Zone{ + ID: idZone2, + PoolID: "", + ProjectID: tenantID, + Name: "myzone2.com.", + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Description: "This is my zone 2", + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + Action: "", + Attributes: []string{}, + Links: map[string]interface{}{ + "self": "dummylink", + }} + +// GetResponse is a sample response to a Get call. +// This get result does not have action, attributes in ECL2.0 +var GetResponse = fmt.Sprintf(` +{ + "id": "%s", + "name": "%s", + "description": "%s", + "project_id": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "created_at": "%s", + "updated_at": "%s", + "links": { + "self": "dummylink" + } +}`, idZone1, + nameZone1, + descriptionZone1, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, +) + +// GetResponseStruct mocked actual +var GetResponseStruct = zones.Zone{ + ID: idZone1, + PoolID: "", + ProjectID: tenantID, + Name: nameZone1, + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Description: descriptionZone1, + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + Action: "", + Links: map[string]interface{}{ + "self": "dummylink", + }, +} + +// CreateZoneRequest is a sample request to create a zone. +var CreateZoneRequest = fmt.Sprintf(`{ + "description": "%s", + "email": "joe@example.org", + "name": "%s", + "ttl": 7200, + "type": "PRIMARY" +}`, + descriptionZone1, + nameZone1, +) + +// CreateZoneResponse is a sample response to a create request. +var CreateZoneResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "description": "%s", + "project_id": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "CREATING", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "created_at": "%s", + "updated_at": null, + "links": { + "self": "dummylink" + } +}`, idZone1, + nameZone1, + descriptionZone1, + tenantID, + zoneCreatedAt, +) + +// CreatedZone is the expected created zone +var CreatedZone = zones.Zone{ + ID: idZone1, + Name: nameZone1, + Description: descriptionZone1, + ProjectID: tenantID, + PoolID: "", + Email: "", + TTL: 0, + Serial: 0, + Status: "CREATING", + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: time.Time{}, + // Action: "", + Links: map[string]interface{}{ + "self": "dummylink", + }, +} + +// UpdateZoneRequest is a sample request to update a zone. +var UpdateZoneRequest = fmt.Sprintf(` +{ + "ttl": 600, + "description": "%s", + "masters": [], + "email": "" +}`, + descriptionZone1Update, +) + +// UpdateZoneResponse is a sample response to update a zone. +var UpdateZoneResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "description": "%s", + "project_id": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "created_at": "%s", + "updated_at": "%s", + "links": { + "self": "dummylink" + } +}`, idZone1, + nameZone1, + descriptionZone1Update, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, +) + +// UpdatedZone is the expected updated zone +var UpdatedZone = zones.Zone{ + ID: idZone1, + Name: nameZone1, + Description: descriptionZone1Update, + ProjectID: tenantID, + PoolID: "", + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + // Action: "", + Links: map[string]interface{}{ + "self": "dummylink", + }, +} diff --git a/v3/ecl/dns/v2/zones/testing/requests_test.go b/v3/ecl/dns/v2/zones/testing/requests_test.go new file mode 100644 index 0000000..6097237 --- /dev/null +++ b/v3/ecl/dns/v2/zones/testing/requests_test.go @@ -0,0 +1,151 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/dns/v2/zones" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, ListResponse) + }) + + count := 0 + err := zones.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := zones.ExtractZones(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedZonesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDNSZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, ListResponse) + }) + + allPages, err := zones.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := zones.ExtractZones(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s", idZone1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, GetResponse) + }) + + actual, err := zones.Get(fakeclient.ServiceClient(), idZone1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GetResponseStruct, actual) +} + +func TestCreateDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, CreateZoneRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, CreateZoneResponse) + }) + + createOpts := zones.CreateOpts{ + Description: descriptionZone1, + Email: "joe@example.org", + Name: nameZone1, + TTL: 7200, + Type: "PRIMARY", + } + + actual, err := zones.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedZone, actual) +} + +func TestUpdateDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s", idZone1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, UpdateZoneRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, UpdateZoneResponse) + }) + + description := descriptionZone1Update + ttl := 600 + masters := make([]string, 0) + email := "" + + updateOpts := zones.UpdateOpts{ + TTL: &ttl, + Description: &description, + Masters: &masters, + Email: &email, + } + + actual, err := zones.Update(fakeclient.ServiceClient(), idZone1, updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UpdatedZone, actual) +} + +func TestDeleteDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s", idZone1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + }) + + res := zones.Delete(fakeclient.ServiceClient(), idZone1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/dns/v2/zones/urls.go b/v3/ecl/dns/v2/zones/urls.go new file mode 100644 index 0000000..ed838dc --- /dev/null +++ b/v3/ecl/dns/v2/zones/urls.go @@ -0,0 +1,11 @@ +package zones + +import "github.com/nttcom/eclcloud/v3" + +func baseURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("zones") +} + +func zoneURL(c *eclcloud.ServiceClient, zoneID string) string { + return c.ServiceURL("zones", zoneID) +} diff --git a/v3/ecl/doc.go b/v3/ecl/doc.go new file mode 100644 index 0000000..c7024cf --- /dev/null +++ b/v3/ecl/doc.go @@ -0,0 +1,14 @@ +/* +Package ecl contains resources for the individual Enterprise Cloud projects +supported in eclcloud. It also includes functions to authenticate to an +Enterprise cloud and for provisioning various service-level clients. + +Example of Creating a Service Client + + ao, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(ao) + client, err := ecl.NewNetworkV2(client, eclcloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +package ecl diff --git a/v3/ecl/endpoint_location.go b/v3/ecl/endpoint_location.go new file mode 100644 index 0000000..c914fee --- /dev/null +++ b/v3/ecl/endpoint_location.go @@ -0,0 +1,52 @@ +package ecl + +import ( + "github.com/nttcom/eclcloud/v3" + tokens3 "github.com/nttcom/eclcloud/v3/ecl/identity/v3/tokens" +) + +/* +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 Enterprise Cloud deployment. +*/ +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts eclcloud.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 != eclcloud.AvailabilityPublic { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if (opts.Availability == eclcloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints} + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return eclcloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &eclcloud.ErrEndpointNotFound{} + return "", err +} diff --git a/v3/ecl/errors.go b/v3/ecl/errors.go new file mode 100644 index 0000000..3b749a2 --- /dev/null +++ b/v3/ecl/errors.go @@ -0,0 +1,58 @@ +package ecl + +import ( + "fmt" + "github.com/nttcom/eclcloud/v3" + tokens3 "github.com/nttcom/eclcloud/v3/ecl/identity/v3/tokens" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ eclcloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ eclcloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint +// for the given options is found in the v3 catalog +type ErrMultipleMatchingEndpointsV3 struct { + eclcloud.BaseError + Endpoints []tokens3.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV3) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ eclcloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ eclcloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ eclcloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/v3/ecl/identity/v3/endpoints/doc.go b/v3/ecl/identity/v3/endpoints/doc.go new file mode 100644 index 0000000..c0ea6b2 --- /dev/null +++ b/v3/ecl/identity/v3/endpoints/doc.go @@ -0,0 +1,66 @@ +/* +Package endpoints provides information and interaction with the service +endpoints API resource in the Enterprise Cloud Identity service. + +Example to List Endpoints + + serviceID := "e629d6e599d9489fb3ae5d9cc12eaea3" + + listOpts := endpoints.ListOpts{ + ServiceID: serviceID, + } + + allPages, err := endpoints.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allEndpoints, err := endpoints.ExtractEndpoints(allPages) + if err != nil { + panic(err) + } + + for _, endpoint := range allEndpoints { + fmt.Printf("%+v\n", endpoint) + } + +Example to Create an Endpoint + + serviceID := "e629d6e599d9489fb3ae5d9cc12eaea3" + + createOpts := endpoints.CreateOpts{ + Availability: eclcloud.AvailabilityPublic, + Name: "neutron", + Region: "RegionOne", + URL: "https://localhost:9696", + ServiceID: serviceID, + } + + endpoint, err := endpoints.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + + +Example to Update an Endpoint + + endpointID := "ad59deeec5154d1fa0dcff518596f499" + + updateOpts := endpoints.UpdateOpts{ + Region: "RegionTwo", + } + + endpoint, err := endpoints.Update(identityClient, endpointID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Endpoint + + endpointID := "ad59deeec5154d1fa0dcff518596f499" + err := endpoints.Delete(identityClient, endpointID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package endpoints diff --git a/v3/ecl/identity/v3/endpoints/requests.go b/v3/ecl/identity/v3/endpoints/requests.go new file mode 100644 index 0000000..d646a3c --- /dev/null +++ b/v3/ecl/identity/v3/endpoints/requests.go @@ -0,0 +1,136 @@ +package endpoints + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type CreateOptsBuilder interface { + ToEndpointCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains the subset of Endpoint attributes that should be used +// to create an Endpoint. +type CreateOpts struct { + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `json:"interface" required:"true"` + + // Name is the name of the Endpoint. + Name string `json:"name" required:"true"` + + // Region is the region the Endpoint is located in. + // This field can be omitted or left as a blank string. + Region string `json:"region,omitempty"` + + // URL is the url of the Endpoint. + URL string `json:"url" required:"true"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `json:"service_id" required:"true"` +} + +// ToEndpointCreateMap builds a request body from the Endpoint Create options. +func (opts CreateOpts) ToEndpointCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "endpoint") +} + +// Create inserts a new Endpoint into the service catalog. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToEndpointCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), &b, &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add parameters to the List request. +type ListOptsBuilder interface { + ToEndpointListParams() (string, error) +} + +// ListOpts allows finer control over the endpoints returned by a List call. +// All fields are optional. +type ListOpts struct { + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `q:"interface"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `q:"service_id"` + + // RegionID is the ID of the region the Endpoint refers to. + RegionID int `q:"region_id"` +} + +// ToEndpointListParams builds a list request from the List options. +func (opts ListOpts) ToEndpointListParams() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates endpoints in a paginated collection, optionally filtered +// by ListOpts criteria. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + u := listURL(client) + if opts != nil { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + } + return pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page { + return EndpointPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add parameters to the Update request. +type UpdateOptsBuilder interface { + ToEndpointUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the subset of Endpoint attributes that should be used to +// update an Endpoint. +type UpdateOpts struct { + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `json:"interface,omitempty"` + + // Name is the name of the Endpoint. + Name string `json:"name,omitempty"` + + // Region is the region the Endpoint is located in. + // This field can be omitted or left as a blank string. + Region string `json:"region,omitempty"` + + // URL is the url of the Endpoint. + URL string `json:"url,omitempty"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `json:"service_id,omitempty"` +} + +// ToEndpointUpdateMap builds an update request body from the Update options. +func (opts UpdateOpts) ToEndpointUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "endpoint") +} + +// Update changes an existing endpoint with new data. +func Update(client *eclcloud.ServiceClient, endpointID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToEndpointUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(endpointURL(client, endpointID), &b, &r.Body, nil) + return +} + +// Delete removes an endpoint from the service catalog. +func Delete(client *eclcloud.ServiceClient, endpointID string) (r DeleteResult) { + _, r.Err = client.Delete(endpointURL(client, endpointID), nil) + return +} diff --git a/v3/ecl/identity/v3/endpoints/results.go b/v3/ecl/identity/v3/endpoints/results.go new file mode 100644 index 0000000..66e81bb --- /dev/null +++ b/v3/ecl/identity/v3/endpoints/results.go @@ -0,0 +1,80 @@ +package endpoints + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.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) { + var s struct { + Endpoint *Endpoint `json:"endpoint"` + } + err := r.ExtractInto(&s) + return s.Endpoint, err +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as an Endpoint. +type CreateResult struct { + commonResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as an Endpoint. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Endpoint describes the entry point for another service's API. +type Endpoint struct { + // ID is the unique ID of the endpoint. + ID string `json:"id"` + + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `json:"interface"` + + // Name is the name of the Endpoint. + Name string `json:"name"` + + // Region is the region the Endpoint is located in. + Region string `json:"region"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `json:"service_id"` + + // URL is the url of the Endpoint. + URL string `json:"url"` +} + +// EndpointPage is a single page of Endpoint results. +type EndpointPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if no Endpoints were returned. +func (r EndpointPage) IsEmpty() (bool, error) { + es, err := ExtractEndpoints(r) + return len(es) == 0, err +} + +// ExtractEndpoints extracts an Endpoint slice from a Page. +func ExtractEndpoints(r pagination.Page) ([]Endpoint, error) { + var s struct { + Endpoints []Endpoint `json:"endpoints"` + } + err := (r.(EndpointPage)).ExtractInto(&s) + return s.Endpoints, err +} diff --git a/v3/ecl/identity/v3/endpoints/urls.go b/v3/ecl/identity/v3/endpoints/urls.go new file mode 100644 index 0000000..9bb5d64 --- /dev/null +++ b/v3/ecl/identity/v3/endpoints/urls.go @@ -0,0 +1,11 @@ +package endpoints + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("endpoints") +} + +func endpointURL(client *eclcloud.ServiceClient, endpointID string) string { + return client.ServiceURL("endpoints", endpointID) +} diff --git a/v3/ecl/identity/v3/groups/doc.go b/v3/ecl/identity/v3/groups/doc.go new file mode 100644 index 0000000..40afa1d --- /dev/null +++ b/v3/ecl/identity/v3/groups/doc.go @@ -0,0 +1,60 @@ +/* +Package groups manages and retrieves Groups in the Enterprise Cloud Identity Service. + +Example to List Groups + + listOpts := groups.ListOpts{ + DomainID: "default", + } + + allPages, err := groups.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + panic(err) + } + + for _, group := range allGroups { + fmt.Printf("%+v\n", group) + } + +Example to Create a Group + + createOpts := groups.CreateOpts{ + Name: "groupname", + DomainID: "default", + Extra: map[string]interface{}{ + "email": "groupname@example.com", + } + } + + group, err := groups.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Group + + groupID := "0fe36e73809d46aeae6705c39077b1b3" + + updateOpts := groups.UpdateOpts{ + Description: "Updated Description for group", + } + + group, err := groups.Update(identityClient, groupID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Group + + groupID := "0fe36e73809d46aeae6705c39077b1b3" + err := groups.Delete(identityClient, groupID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package groups diff --git a/v3/ecl/identity/v3/groups/errors.go b/v3/ecl/identity/v3/groups/errors.go new file mode 100644 index 0000000..98e6fe4 --- /dev/null +++ b/v3/ecl/identity/v3/groups/errors.go @@ -0,0 +1,17 @@ +package groups + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v3/ecl/identity/v3/groups/requests.go b/v3/ecl/identity/v3/groups/requests.go new file mode 100644 index 0000000..af35b1d --- /dev/null +++ b/v3/ecl/identity/v3/groups/requests.go @@ -0,0 +1,179 @@ +package groups + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" + "net/url" + "strings" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToGroupListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Name filters the response by group name. + Name string `q:"name"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToGroupListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the Groups to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return GroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single group, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a group. +type CreateOpts struct { + // Name is the name of the new group. + Name string `json:"name" required:"true"` + + // Description is a description of the group. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the group belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the group. + Extra map[string]interface{} `json:"-"` +} + +// ToGroupCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToGroupCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "group") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["group"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new Group. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToGroupUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a group. +type UpdateOpts struct { + // Name is the name of the new group. + Name string `json:"name,omitempty"` + + // Description is a description of the group. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the group belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the group. + Extra map[string]interface{} `json:"-"` +} + +// ToGroupUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToGroupUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "group") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["group"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing Group. +func Update(client *eclcloud.ServiceClient, groupID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToGroupUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, groupID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a group. +func Delete(client *eclcloud.ServiceClient, groupID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, groupID), nil) + return +} diff --git a/v3/ecl/identity/v3/groups/results.go b/v3/ecl/identity/v3/groups/results.go new file mode 100644 index 0000000..e633aa2 --- /dev/null +++ b/v3/ecl/identity/v3/groups/results.go @@ -0,0 +1,131 @@ +package groups + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/internal" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Group helps manage related users. +type Group struct { + // Description describes the group purpose. + Description string `json:"description"` + + // DomainID is the domain ID the group belongs to. + DomainID string `json:"domain_id"` + + // ID is the unique ID of the group. + ID string `json:"id"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // Links contains referencing links to the group. + Links map[string]interface{} `json:"links"` + + // Name is the name of the group. + Name string `json:"name"` +} + +func (r *Group) UnmarshalJSON(b []byte) error { + type tmp Group + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Group(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Group{}, resultMap) + } + } + + return err +} + +type groupResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Group. +type GetResult struct { + groupResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Group. +type CreateResult struct { + groupResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Group. +type UpdateResult struct { + groupResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// GroupPage is a single page of Group results. +type GroupPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Groups contains any results. +func (r GroupPage) IsEmpty() (bool, error) { + groups, err := ExtractGroups(r) + return len(groups) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r GroupPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractGroups returns a slice of Groups contained in a single page of results. +func ExtractGroups(r pagination.Page) ([]Group, error) { + var s struct { + Groups []Group `json:"groups"` + } + err := (r.(GroupPage)).ExtractInto(&s) + return s.Groups, err +} + +// Extract interprets any group results as a Group. +func (r groupResult) Extract() (*Group, error) { + var s struct { + Group *Group `json:"group"` + } + err := r.ExtractInto(&s) + return s.Group, err +} diff --git a/v3/ecl/identity/v3/groups/urls.go b/v3/ecl/identity/v3/groups/urls.go new file mode 100644 index 0000000..3af12ee --- /dev/null +++ b/v3/ecl/identity/v3/groups/urls.go @@ -0,0 +1,23 @@ +package groups + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("groups") +} + +func getURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("groups") +} + +func updateURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID) +} + +func deleteURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID) +} diff --git a/v3/ecl/identity/v3/projects/doc.go b/v3/ecl/identity/v3/projects/doc.go new file mode 100644 index 0000000..f2e73a8 --- /dev/null +++ b/v3/ecl/identity/v3/projects/doc.go @@ -0,0 +1,58 @@ +/* +Package projects manages and retrieves Projects in the Enterprise Cloud Identity +Service. + +Example to List Projects + + listOpts := projects.ListOpts{ + Enabled: eclcloud.Enabled, + } + + allPages, err := projects.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, project := range allProjects { + fmt.Printf("%+v\n", project) + } + +Example to Create a Project + + createOpts := projects.CreateOpts{ + Name: "project_name", + Description: "Project Description" + } + + project, err := projects.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + + updateOpts := projects.UpdateOpts{ + Enabled: eclcloud.Disabled, + } + + project, err := projects.Update(identityClient, projectID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + err := projects.Delete(identityClient, projectID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package projects diff --git a/v3/ecl/identity/v3/projects/errors.go b/v3/ecl/identity/v3/projects/errors.go new file mode 100644 index 0000000..7be97d8 --- /dev/null +++ b/v3/ecl/identity/v3/projects/errors.go @@ -0,0 +1,17 @@ +package projects + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v3/ecl/identity/v3/projects/requests.go b/v3/ecl/identity/v3/projects/requests.go new file mode 100644 index 0000000..1199de2 --- /dev/null +++ b/v3/ecl/identity/v3/projects/requests.go @@ -0,0 +1,173 @@ +package projects + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" + "net/url" + "strings" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToProjectListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Enabled filters the response by enabled projects. + Enabled *bool `q:"enabled"` + + // IsDomain filters the response by projects that are domains. + // Setting this to true is effectively listing domains. + IsDomain *bool `q:"is_domain"` + + // Name filters the response by project name. + Name string `q:"name"` + + // ParentID filters the response by projects of a given parent project. + ParentID string `q:"parent_id"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToProjectListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToProjectListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the Projects to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToProjectListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single project, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToProjectCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a project. +type CreateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the project status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // IsDomain indicates if this project is a domain. + IsDomain *bool `json:"is_domain,omitempty"` + + // Name is the name of the project. + Name string `json:"name" required:"true"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToProjectCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "project") +} + +// Create creates a new Project. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToProjectCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a project. +func Delete(client *eclcloud.ServiceClient, projectID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, projectID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToProjectUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a project. +type UpdateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the project status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // IsDomain indicates if this project is a domain. + IsDomain *bool `json:"is_domain,omitempty"` + + // Name is the name of the project. + Name string `json:"name,omitempty"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "project") +} + +// Update modifies the attributes of a project. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToProjectUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/identity/v3/projects/results.go b/v3/ecl/identity/v3/projects/results.go new file mode 100644 index 0000000..fdf3e35 --- /dev/null +++ b/v3/ecl/identity/v3/projects/results.go @@ -0,0 +1,103 @@ +package projects + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type projectResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a Project. +type GetResult struct { + projectResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Project. +type CreateResult struct { + projectResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Project. +type UpdateResult struct { + projectResult +} + +// Project represents an Enterprise Cloud Identity Project. +type Project struct { + // IsDomain indicates whether the project is a domain. + IsDomain bool `json:"is_domain"` + + // Description is the description of the project. + Description string `json:"description"` + + // DomainID is the domain ID the project belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the project is enabled. + Enabled bool `json:"enabled"` + + // ID is the unique ID of the project. + ID string `json:"id"` + + // Name is the name of the project. + Name string `json:"name"` + + // ParentID is the parent_id of the project. + ParentID string `json:"parent_id"` +} + +// ProjectPage is a single page of Project results. +type ProjectPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Projects contains any results. +func (r ProjectPage) IsEmpty() (bool, error) { + projects, err := ExtractProjects(r) + return len(projects) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r ProjectPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractProjects returns a slice of Projects contained in a single page of +// results. +func ExtractProjects(r pagination.Page) ([]Project, error) { + var s struct { + Projects []Project `json:"projects"` + } + err := (r.(ProjectPage)).ExtractInto(&s) + return s.Projects, err +} + +// Extract interprets any projectResults as a Project. +func (r projectResult) Extract() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} diff --git a/v3/ecl/identity/v3/projects/urls.go b/v3/ecl/identity/v3/projects/urls.go new file mode 100644 index 0000000..e9e9d4d --- /dev/null +++ b/v3/ecl/identity/v3/projects/urls.go @@ -0,0 +1,23 @@ +package projects + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func getURL(client *eclcloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func deleteURL(client *eclcloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func updateURL(client *eclcloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} diff --git a/v3/ecl/identity/v3/roles/doc.go b/v3/ecl/identity/v3/roles/doc.go new file mode 100644 index 0000000..2c0dfeb --- /dev/null +++ b/v3/ecl/identity/v3/roles/doc.go @@ -0,0 +1,135 @@ +/* +Package roles provides information and interaction with the roles API +resource for the Enterprise Cloud Identity service. + +Example to List Roles + + listOpts := roles.ListOpts{ + DomainID: "default", + } + + allPages, err := roles.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRoles, err := roles.ExtractRoles(allPages) + if err != nil { + panic(err) + } + + for _, role := range allRoles { + fmt.Printf("%+v\n", role) + } + +Example to Create a Role + + createOpts := roles.CreateOpts{ + Name: "read-only-admin", + DomainID: "default", + Extra: map[string]interface{}{ + "description": "this role grants read-only privilege cross tenant", + } + } + + role, err := roles.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Role + + roleID := "0fe36e73809d46aeae6705c39077b1b3" + + updateOpts := roles.UpdateOpts{ + Name: "read only admin", + } + + role, err := roles.Update(identityClient, roleID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Role + + roleID := "0fe36e73809d46aeae6705c39077b1b3" + err := roles.Delete(identityClient, roleID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Role Assignments + + listOpts := roles.ListAssignmentsOpts{ + UserID: "97061de2ed0647b28a393c36ab584f39", + ScopeProjectID: "9df1a02f5eb2416a9781e8b0c022d3ae", + } + + allPages, err := roles.ListAssignments(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRoles, err := roles.ExtractRoleAssignments(allPages) + if err != nil { + panic(err) + } + + for _, role := range allRoles { + fmt.Printf("%+v\n", role) + } + +Example to List Role Assignments for a User on a Project + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + userID := "9df1a02f5eb2416a9781e8b0c022d3ae" + listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{ + UserID: userID, + ProjectID: projectID, + } + + allPages, err := roles.ListAssignmentsOnResource(identityClient, listAssignmentsOnResourceOpts).AllPages() + if err != nil { + panic(err) + } + + allRoles, err := roles.ExtractRoles(allPages) + if err != nil { + panic(err) + } + + for _, role := range allRoles { + fmt.Printf("%+v\n", role) + } + +Example to Assign a Role to a User in a Project + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + userID := "9df1a02f5eb2416a9781e8b0c022d3ae" + roleID := "9fe2ff9ee4384b1894a90878d3e92bab" + + err := roles.Assign(identityClient, roleID, roles.AssignOpts{ + UserID: userID, + ProjectID: projectID, + }).ExtractErr() + + if err != nil { + panic(err) + } + +Example to Unassign a Role From a User in a Project + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + userID := "9df1a02f5eb2416a9781e8b0c022d3ae" + roleID := "9fe2ff9ee4384b1894a90878d3e92bab" + + err := roles.Unassign(identityClient, roleID, roles.UnassignOpts{ + UserID: userID, + ProjectID: projectID, + }).ExtractErr() + + if err != nil { + panic(err) + } +*/ +package roles diff --git a/v3/ecl/identity/v3/roles/errors.go b/v3/ecl/identity/v3/roles/errors.go new file mode 100644 index 0000000..b60d7d1 --- /dev/null +++ b/v3/ecl/identity/v3/roles/errors.go @@ -0,0 +1,17 @@ +package roles + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v3/ecl/identity/v3/roles/requests.go b/v3/ecl/identity/v3/roles/requests.go new file mode 100644 index 0000000..927a3ea --- /dev/null +++ b/v3/ecl/identity/v3/roles/requests.go @@ -0,0 +1,391 @@ +package roles + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" + "net/url" + "strings" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToRoleListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Name filters the response by role name. + Name string `q:"name"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToRoleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRoleListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the roles to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToRoleListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single role, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToRoleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a role. +type CreateOpts struct { + // Name is the name of the new role. + Name string `json:"name" required:"true"` + + // DomainID is the ID of the domain the role belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the role. + Extra map[string]interface{} `json:"-"` +} + +// ToRoleCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToRoleCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "role") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["role"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new Role. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRoleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToRoleUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a role. +type UpdateOpts struct { + // Name is the name of the new role. + Name string `json:"name,omitempty"` + + // Extra is free-form extra key/value pairs to describe the role. + Extra map[string]interface{} `json:"-"` +} + +// ToRoleUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToRoleUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "role") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["role"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing Role. +func Update(client *eclcloud.ServiceClient, roleID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRoleUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, roleID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a role. +func Delete(client *eclcloud.ServiceClient, roleID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, roleID), nil) + return +} + +// ListAssignmentsOptsBuilder allows extensions to add additional parameters to +// the ListAssignments request. +type ListAssignmentsOptsBuilder interface { + ToRolesListAssignmentsQuery() (string, error) +} + +// ListAssignmentsOpts allows you to query the ListAssignments method. +// Specify one of or a combination of GroupId, RoleId, ScopeDomainId, +// ScopeProjectId, and/or UserId to search for roles assigned to corresponding +// entities. +type ListAssignmentsOpts struct { + // GroupID is the group ID to query. + GroupID string `q:"group.id"` + + // RoleID is the specific role to query assignments to. + RoleID string `q:"role.id"` + + // ScopeDomainID filters the results by the given domain ID. + ScopeDomainID string `q:"scope.domain.id"` + + // ScopeProjectID filters the results by the given Project ID. + ScopeProjectID string `q:"scope.project.id"` + + // UserID filterst he results by the given User ID. + UserID string `q:"user.id"` + + // Effective lists effective assignments at the user, project, and domain + // level, allowing for the effects of group membership. + Effective *bool `q:"effective"` +} + +// ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string. +func (opts ListAssignmentsOpts) ToRolesListAssignmentsQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListAssignments enumerates the roles assigned to a specified resource. +func ListAssignments(client *eclcloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager { + url := listAssignmentsURL(client) + if opts != nil { + query, err := opts.ToRolesListAssignmentsQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RoleAssignmentPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAssignmentsOnResourceOpts provides options to list role assignments +// for a user/group on a project/domain +type ListAssignmentsOnResourceOpts struct { + // UserID is the ID of a user to assign a role + // Note: exactly one of UserID or GroupID must be provided + UserID string `xor:"GroupID"` + + // GroupID is the ID of a group to assign a role + // Note: exactly one of UserID or GroupID must be provided + GroupID string `xor:"UserID"` + + // ProjectID is the ID of a project to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + ProjectID string `xor:"DomainID"` + + // DomainID is the ID of a domain to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + DomainID string `xor:"ProjectID"` +} + +// AssignOpts provides options to assign a role +type AssignOpts struct { + // UserID is the ID of a user to assign a role + // Note: exactly one of UserID or GroupID must be provided + UserID string `xor:"GroupID"` + + // GroupID is the ID of a group to assign a role + // Note: exactly one of UserID or GroupID must be provided + GroupID string `xor:"UserID"` + + // ProjectID is the ID of a project to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + ProjectID string `xor:"DomainID"` + + // DomainID is the ID of a domain to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + DomainID string `xor:"ProjectID"` +} + +// UnassignOpts provides options to unassign a role +type UnassignOpts struct { + // UserID is the ID of a user to unassign a role + // Note: exactly one of UserID or GroupID must be provided + UserID string `xor:"GroupID"` + + // GroupID is the ID of a group to unassign a role + // Note: exactly one of UserID or GroupID must be provided + GroupID string `xor:"UserID"` + + // ProjectID is the ID of a project to unassign a role on + // Note: exactly one of ProjectID or DomainID must be provided + ProjectID string `xor:"DomainID"` + + // DomainID is the ID of a domain to unassign a role on + // Note: exactly one of ProjectID or DomainID must be provided + DomainID string `xor:"ProjectID"` +} + +// ListAssignmentsOnResource is the operation responsible for listing role +// assignments for a user/group on a project/domain. +func ListAssignmentsOnResource(client *eclcloud.ServiceClient, opts ListAssignmentsOnResourceOpts) pagination.Pager { + // Check xor conditions + _, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return pagination.Pager{Err: err} + } + + // Get corresponding URL + var targetID string + var targetType string + if opts.ProjectID != "" { + targetID = opts.ProjectID + targetType = "projects" + } else { + targetID = opts.DomainID + targetType = "domains" + } + + var actorID string + var actorType string + if opts.UserID != "" { + actorID = opts.UserID + actorType = "users" + } else { + actorID = opts.GroupID + actorType = "groups" + } + + url := listAssignmentsOnResourceURL(client, targetType, targetID, actorType, actorID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Assign is the operation responsible for assigning a role +// to a user/group on a project/domain. +func Assign(client *eclcloud.ServiceClient, roleID string, opts AssignOpts) (r AssignmentResult) { + // Check xor conditions + _, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + r.Err = err + return + } + + // Get corresponding URL + var targetID string + var targetType string + if opts.ProjectID != "" { + targetID = opts.ProjectID + targetType = "projects" + } else { + targetID = opts.DomainID + targetType = "domains" + } + + var actorID string + var actorType string + if opts.UserID != "" { + actorID = opts.UserID + actorType = "users" + } else { + actorID = opts.GroupID + actorType = "groups" + } + + _, r.Err = client.Put(assignURL(client, targetType, targetID, actorType, actorID, roleID), nil, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// Unassign is the operation responsible for unassigning a role +// from a user/group on a project/domain. +func Unassign(client *eclcloud.ServiceClient, roleID string, opts UnassignOpts) (r UnassignmentResult) { + // Check xor conditions + _, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + r.Err = err + return + } + + // Get corresponding URL + var targetID string + var targetType string + if opts.ProjectID != "" { + targetID = opts.ProjectID + targetType = "projects" + } else { + targetID = opts.DomainID + targetType = "domains" + } + + var actorID string + var actorType string + if opts.UserID != "" { + actorID = opts.UserID + actorType = "users" + } else { + actorID = opts.GroupID + actorType = "groups" + } + + _, r.Err = client.Delete(assignURL(client, targetType, targetID, actorType, actorID, roleID), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} diff --git a/v3/ecl/identity/v3/roles/results.go b/v3/ecl/identity/v3/roles/results.go new file mode 100644 index 0000000..6c3ad8b --- /dev/null +++ b/v3/ecl/identity/v3/roles/results.go @@ -0,0 +1,213 @@ +package roles + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/internal" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Role grants permissions to a user. +type Role struct { + // DomainID is the domain ID the role belongs to. + DomainID string `json:"domain_id"` + + // ID is the unique ID of the role. + ID string `json:"id"` + + // Links contains referencing links to the role. + Links map[string]interface{} `json:"links"` + + // Name is the role name + Name string `json:"name"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` +} + +func (r *Role) UnmarshalJSON(b []byte) error { + type tmp Role + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Role(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Role{}, resultMap) + } + } + + return err +} + +type roleResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Role. +type GetResult struct { + roleResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Role +type CreateResult struct { + roleResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Role. +type UpdateResult struct { + roleResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// RolePage is a single page of Role results. +type RolePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Roles contains any results. +func (r RolePage) IsEmpty() (bool, error) { + roles, err := ExtractRoles(r) + return len(roles) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r RolePage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractProjects returns a slice of Roles contained in a single page of +// results. +func ExtractRoles(r pagination.Page) ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := (r.(RolePage)).ExtractInto(&s) + return s.Roles, err +} + +// Extract interprets any roleResults as a Role. +func (r roleResult) Extract() (*Role, error) { + var s struct { + Role *Role `json:"role"` + } + err := r.ExtractInto(&s) + return s.Role, err +} + +// RoleAssignment is the result of a role assignments query. +type RoleAssignment struct { + Role AssignedRole `json:"role,omitempty"` + Scope Scope `json:"scope,omitempty"` + User User `json:"user,omitempty"` + Group Group `json:"group,omitempty"` +} + +// AssignedRole represents a Role in an assignment. +type AssignedRole struct { + ID string `json:"id,omitempty"` +} + +// Scope represents a scope in a Role assignment. +type Scope struct { + Domain Domain `json:"domain,omitempty"` + Project Project `json:"project,omitempty"` +} + +// Domain represents a domain in a role assignment scope. +type Domain struct { + ID string `json:"id,omitempty"` +} + +// Project represents a project in a role assignment scope. +type Project struct { + ID string `json:"id,omitempty"` +} + +// User represents a user in a role assignment scope. +type User struct { + ID string `json:"id,omitempty"` +} + +// Group represents a group in a role assignment scope. +type Group struct { + ID string `json:"id,omitempty"` +} + +// RoleAssignmentPage is a single page of RoleAssignments results. +type RoleAssignmentPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the RoleAssignmentPage contains no results. +func (r RoleAssignmentPage) IsEmpty() (bool, error) { + roleAssignments, err := ExtractRoleAssignments(r) + return len(roleAssignments) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r RoleAssignmentPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + } `json:"links"` + } + err := r.ExtractInto(&s) + return s.Links.Next, err +} + +// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection +// acquired from List. +func ExtractRoleAssignments(r pagination.Page) ([]RoleAssignment, error) { + var s struct { + RoleAssignments []RoleAssignment `json:"role_assignments"` + } + err := (r.(RoleAssignmentPage)).ExtractInto(&s) + return s.RoleAssignments, err +} + +// AssignmentResult represents the result of an assign operation. +// Call ExtractErr method to determine if the request succeeded or failed. +type AssignmentResult struct { + eclcloud.ErrResult +} + +// UnassignmentResult represents the result of an unassign operation. +// Call ExtractErr method to determine if the request succeeded or failed. +type UnassignmentResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/identity/v3/roles/urls.go b/v3/ecl/identity/v3/roles/urls.go new file mode 100644 index 0000000..c2c2141 --- /dev/null +++ b/v3/ecl/identity/v3/roles/urls.go @@ -0,0 +1,39 @@ +package roles + +import "github.com/nttcom/eclcloud/v3" + +const ( + rolePath = "roles" +) + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL(rolePath) +} + +func getURL(client *eclcloud.ServiceClient, roleID string) string { + return client.ServiceURL(rolePath, roleID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL(rolePath) +} + +func updateURL(client *eclcloud.ServiceClient, roleID string) string { + return client.ServiceURL(rolePath, roleID) +} + +func deleteURL(client *eclcloud.ServiceClient, roleID string) string { + return client.ServiceURL(rolePath, roleID) +} + +func listAssignmentsURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("role_assignments") +} + +func listAssignmentsOnResourceURL(client *eclcloud.ServiceClient, targetType, targetID, actorType, actorID string) string { + return client.ServiceURL(targetType, targetID, actorType, actorID, rolePath) +} + +func assignURL(client *eclcloud.ServiceClient, targetType, targetID, actorType, actorID, roleID string) string { + return client.ServiceURL(targetType, targetID, actorType, actorID, rolePath, roleID) +} diff --git a/v3/ecl/identity/v3/services/doc.go b/v3/ecl/identity/v3/services/doc.go new file mode 100644 index 0000000..07cd7b7 --- /dev/null +++ b/v3/ecl/identity/v3/services/doc.go @@ -0,0 +1,66 @@ +/* +Package services provides information and interaction with the services API +resource for the Enterprise Cloud Identity service. + +Example to List Services + + listOpts := services.ListOpts{ + ServiceType: "compute", + } + + allPages, err := services.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServices, err := services.ExtractServices(allPages) + if err != nil { + panic(err) + } + + for _, service := range allServices { + fmt.Printf("%+v\n", service) + } + +Example to Create a Service + + createOpts := services.CreateOpts{ + Type: "compute", + Extra: map[string]interface{}{ + "name": "compute-service", + "description": "Compute Service", + }, + } + + service, err := services.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Service + + serviceID := "3c7bbe9a6ecb453ca1789586291380ed" + + var iFalse bool = false + updateOpts := services.UpdateOpts{ + Enabled: &iFalse, + Extra: map[string]interface{}{ + "description": "Disabled Compute Service" + }, + } + + service, err := services.Update(identityClient, serviceID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Service + + serviceID := "3c7bbe9a6ecb453ca1789586291380ed" + err := services.Delete(identityClient, serviceID).ExtractErr() + if err != nil { + panic(err) + } + +*/ +package services diff --git a/v3/ecl/identity/v3/services/requests.go b/v3/ecl/identity/v3/services/requests.go new file mode 100644 index 0000000..32ef0e1 --- /dev/null +++ b/v3/ecl/identity/v3/services/requests.go @@ -0,0 +1,154 @@ +package services + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToServiceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a service. +type CreateOpts struct { + // Type is the type of the service. + Type string `json:"type"` + + // Enabled is whether or not the service is enabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the service. + Extra map[string]interface{} `json:"-"` +} + +// ToServiceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToServiceCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "service") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["service"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create adds a new service of the requested type to the catalog. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServiceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// ListOptsBuilder enables extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToServiceListMap() (string, error) +} + +// ListOpts provides options for filtering the List results. +type ListOpts struct { + // ServiceType filter the response by a type of service. + ServiceType string `q:"type"` + + // Name filters the response by a service name. + Name string `q:"name"` +} + +// ToServiceListMap builds a list query from the list options. +func (opts ListOpts) ToServiceListMap() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the services available to a specific user. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServiceListMap() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns additional information about a service, given its ID. +func Get(client *eclcloud.ServiceClient, serviceID string) (r GetResult) { + _, r.Err = client.Get(serviceURL(client, serviceID), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToServiceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a service. +type UpdateOpts struct { + // Type is the type of the service. + Type string `json:"type"` + + // Enabled is whether or not the service is enabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the service. + Extra map[string]interface{} `json:"-"` +} + +// ToServiceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToServiceUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "service") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["service"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing Service. +func Update(client *eclcloud.ServiceClient, serviceID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServiceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, serviceID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete removes an existing service. +// It either deletes all associated endpoints, or fails until all endpoints +// are deleted. +func Delete(client *eclcloud.ServiceClient, serviceID string) (r DeleteResult) { + _, r.Err = client.Delete(serviceURL(client, serviceID), nil) + return +} diff --git a/v3/ecl/identity/v3/services/results.go b/v3/ecl/identity/v3/services/results.go new file mode 100644 index 0000000..70c5134 --- /dev/null +++ b/v3/ecl/identity/v3/services/results.go @@ -0,0 +1,130 @@ +package services + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/internal" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type serviceResult struct { + eclcloud.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 serviceResult) Extract() (*Service, error) { + var s struct { + Service *Service `json:"service"` + } + err := r.ExtractInto(&s) + return s.Service, err +} + +// CreateResult is the response from a Create request. Call its Extract method +// to interpret it as a Service. +type CreateResult struct { + serviceResult +} + +// GetResult is the response from a Get request. Call its Extract method +// to interpret it as a Service. +type GetResult struct { + serviceResult +} + +// UpdateResult is the response from an Update request. Call its Extract method +// to interpret it as a Service. +type UpdateResult struct { + serviceResult +} + +// DeleteResult is the response from a Delete request. Call its ExtractErr +// method to interpret it as a Service. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Service represents an Enterprise Cloud Service. +type Service struct { + // ID is the unique ID of the service. + ID string `json:"id"` + + // Type is the type of the service. + Type string `json:"type"` + + // Enabled is whether or not the service is enabled. + Enabled bool `json:"enabled"` + + // Links contains referencing links to the service. + Links map[string]interface{} `json:"links"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` +} + +func (r *Service) UnmarshalJSON(b []byte) error { + type tmp Service + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Service(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Service{}, resultMap) + } + } + + return err +} + +// ServicePage is a single page of Service results. +type ServicePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the ServicePage contains no results. +func (r ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(r) + return len(services) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r ServicePage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractServices extracts a slice of Services from a Collection acquired +// from List. +func ExtractServices(r pagination.Page) ([]Service, error) { + var s struct { + Services []Service `json:"services"` + } + err := (r.(ServicePage)).ExtractInto(&s) + return s.Services, err +} diff --git a/v3/ecl/identity/v3/services/urls.go b/v3/ecl/identity/v3/services/urls.go new file mode 100644 index 0000000..62c89a6 --- /dev/null +++ b/v3/ecl/identity/v3/services/urls.go @@ -0,0 +1,19 @@ +package services + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func serviceURL(client *eclcloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} + +func updateURL(client *eclcloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} diff --git a/v3/ecl/identity/v3/tokens/doc.go b/v3/ecl/identity/v3/tokens/doc.go new file mode 100644 index 0000000..6c9f6b3 --- /dev/null +++ b/v3/ecl/identity/v3/tokens/doc.go @@ -0,0 +1,105 @@ +/* +Package tokens provides information and interaction with the token API +resource for the Enterprise Cloud Identity service. + +Example to Create a Token From a Username and Password + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Username, Password, and Domain + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainID: "default", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + + authOptions = tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainName: "default", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Token + + authOptions := tokens.AuthOptions{ + TokenID: "token_id", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project ID Scope + + scope := tokens.Scope{ + ProjectID: "0fe36e73809d46aeae6705c39077b1b3", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Domain ID Scope + + scope := tokens.Scope{ + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project Name Scope + + scope := tokens.Scope{ + ProjectName: "project_name", + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +*/ +package tokens diff --git a/v3/ecl/identity/v3/tokens/requests.go b/v3/ecl/identity/v3/tokens/requests.go new file mode 100644 index 0000000..3898a56 --- /dev/null +++ b/v3/ecl/identity/v3/tokens/requests.go @@ -0,0 +1,162 @@ +package tokens + +import "github.com/nttcom/eclcloud/v3" + +// 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 +} + +// AuthOptionsBuilder provides the ability for extensions to add additional +// parameters to AuthOptions. Extensions must satisfy all required methods. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool +} + +// AuthOptions represents options for authenticating a user. +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 `json:"-"` + + // 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 string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Eclcloud + // to cache your credentials in memory, and to allow Eclcloud 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 `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` + + Scope Scope `json:"-"` +} + +// ToTokenV3CreateMap builds a request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + eclcloudAuthOpts := eclcloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + ApplicationCredentialID: opts.ApplicationCredentialID, + ApplicationCredentialName: opts.ApplicationCredentialName, + ApplicationCredentialSecret: opts.ApplicationCredentialSecret, + } + + return eclcloudAuthOpts.ToTokenV3CreateMap(scope) +} + +// ToTokenV3CreateMap builds a scope request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + scope := eclcloud.AuthScope(opts.Scope) + + eclcloudAuthOpts := eclcloud.AuthOptions{ + Scope: &scope, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + } + + return eclcloudAuthOpts.ToTokenV3ScopeMap() +} + +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +func subjectTokenHeaders(c *eclcloud.ServiceClient, subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope +// of an existing token. +func Create(c *eclcloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(tokenURL(c), b, &r.Body, &eclcloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + }) + r.Err = err + if resp != nil { + r.Header = resp.Header + } + return +} + +// Get validates and retrieves information about another token. +func Get(c *eclcloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &eclcloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 203}, + }) + if resp != nil { + r.Err = err + r.Header = resp.Header + } + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *eclcloud.ServiceClient, token string) (bool, error) { + resp, err := c.Head(tokenURL(c), &eclcloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 200 || resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *eclcloud.ServiceClient, token string) (r RevokeResult) { + _, r.Err = c.Delete(tokenURL(c), &eclcloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + }) + return +} diff --git a/v3/ecl/identity/v3/tokens/results.go b/v3/ecl/identity/v3/tokens/results.go new file mode 100644 index 0000000..10f5498 --- /dev/null +++ b/v3/ecl/identity/v3/tokens/results.go @@ -0,0 +1,170 @@ +package tokens + +import ( + "github.com/nttcom/eclcloud/v3" + "time" +) + +// 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 `json:"id"` + Region string `json:"region"` + RegionID string `json:"region_id"` + Interface string `json:"interface"` + URL string `json:"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 `json:"id"` + + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if Enterprise Cloud defines a type for the + // service. Otherwise, for provider-specific services, the provider may + // assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// Domain provides information about the domain to which this token grants +// access. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// User represents a user resource that exists in the Identity Service. +type User struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// Role provides information about roles to which User is authorized. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Project provides information about project to which User is authorized. +type Project struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// commonResult is the response from a request. A commonResult has various +// methods which can be used to extract different details about the result. +type commonResult struct { + eclcloud.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) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractUser returns the User that is the owner of the Token. +func (r commonResult) ExtractUser() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ExtractRoles returns Roles to which User is authorized. +func (r commonResult) ExtractRoles() ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := r.ExtractInto(&s) + return s.Roles, err +} + +// ExtractProject returns Project to which User is authorized. +func (r commonResult) ExtractProject() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} + +// CreateResult is the response from a Create request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the response from a Get request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type GetResult struct { + commonResult +} + +// RevokeResult is response from a Revoke request. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services +// in an Enterprise Cloud provider. Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/v3/ecl/identity/v3/tokens/urls.go b/v3/ecl/identity/v3/tokens/urls.go new file mode 100644 index 0000000..9792cc6 --- /dev/null +++ b/v3/ecl/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/nttcom/eclcloud/v3" + +func tokenURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/v3/ecl/identity/v3/users/doc.go b/v3/ecl/identity/v3/users/doc.go new file mode 100644 index 0000000..f72ed45 --- /dev/null +++ b/v3/ecl/identity/v3/users/doc.go @@ -0,0 +1,172 @@ +/* +Package users manages and retrieves Users in the Enterprise Cloud Identity Service. + +Example to List Users + + listOpts := users.ListOpts{ + DomainID: "default", + } + + allPages, err := users.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +Example to Create a User + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + + createOpts := users.CreateOpts{ + Name: "username", + DomainID: "default", + DefaultProjectID: projectID, + Enabled: eclcloud.Enabled, + Password: "supersecret", + Extra: map[string]interface{}{ + "email": "username@example.com", + } + } + + user, err := users.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a User + + userID := "0fe36e73809d46aeae6705c39077b1b3" + + updateOpts := users.UpdateOpts{ + Enabled: eclcloud.Disabled, + } + + user, err := users.Update(identityClient, userID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Change Password of a User + + userID := "0fe36e73809d46aeae6705c39077b1b3" + originalPassword := "secretsecret" + password := "new_secretsecret" + + changePasswordOpts := users.ChangePasswordOpts{ + OriginalPassword: originalPassword, + Password: password, + } + + err := users.ChangePassword(identityClient, userID, changePasswordOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a User + + userID := "0fe36e73809d46aeae6705c39077b1b3" + err := users.Delete(identityClient, userID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Groups a User Belongs To + + userID := "0fe36e73809d46aeae6705c39077b1b3" + + allPages, err := users.ListGroups(identityClient, userID).AllPages() + if err != nil { + panic(err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + panic(err) + } + + for _, group := range allGroups { + fmt.Printf("%+v\n", group) + } + +Example to Add a User to a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + userID := "0fe36e73809d46aeae6705c39077b1b3" + err := users.AddToGroup(identityClient, groupID, userID).ExtractErr() + + if err != nil { + panic(err) + } + +Example to Check Whether a User Belongs to a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + userID := "0fe36e73809d46aeae6705c39077b1b3" + ok, err := users.IsMemberOfGroup(identityClient, groupID, userID).Extract() + if err != nil { + panic(err) + } + + if ok { + fmt.Printf("user %s is a member of group %s\n", userID, groupID) + } + +Example to Remove a User from a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + userID := "0fe36e73809d46aeae6705c39077b1b3" + err := users.RemoveFromGroup(identityClient, groupID, userID).ExtractErr() + + if err != nil { + panic(err) + } + +Example to List Projects a User Belongs To + + userID := "0fe36e73809d46aeae6705c39077b1b3" + + allPages, err := users.ListProjects(identityClient, userID).AllPages() + if err != nil { + panic(err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, project := range allProjects { + fmt.Printf("%+v\n", project) + } + +Example to List Users in a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + listOpts := users.ListOpts{ + DomainID: "default", + } + + allPages, err := users.ListInGroup(identityClient, groupID, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +*/ +package users diff --git a/v3/ecl/identity/v3/users/errors.go b/v3/ecl/identity/v3/users/errors.go new file mode 100644 index 0000000..0f0b798 --- /dev/null +++ b/v3/ecl/identity/v3/users/errors.go @@ -0,0 +1,17 @@ +package users + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v3/ecl/identity/v3/users/requests.go b/v3/ecl/identity/v3/users/requests.go new file mode 100644 index 0000000..915d7ba --- /dev/null +++ b/v3/ecl/identity/v3/users/requests.go @@ -0,0 +1,337 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/identity/v3/groups" + "github.com/nttcom/eclcloud/v3/ecl/identity/v3/projects" + "github.com/nttcom/eclcloud/v3/pagination" + "net/http" + "net/url" + "strings" +) + +// Option is a specific option defined at the API to enable features +// on a user account. +type Option string + +const ( + IgnoreChangePasswordUponFirstUse Option = "ignore_change_password_upon_first_use" + IgnorePasswordExpiry Option = "ignore_password_expiry" + IgnoreLockoutFailureAttempts Option = "ignore_lockout_failure_attempts" + MultiFactorAuthRules Option = "multi_factor_auth_rules" + MultiFactorAuthEnabled Option = "multi_factor_auth_enabled" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToUserListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Enabled filters the response by enabled users. + Enabled *bool `q:"enabled"` + + // IdpID filters the response by an Identity Provider ID. + IdPID string `q:"idp_id"` + + // Name filters the response by username. + Name string `q:"name"` + + // PasswordExpiresAt filters the response based on expiring passwords. + PasswordExpiresAt string `q:"password_expires_at"` + + // ProtocolID filters the response by protocol ID. + ProtocolID string `q:"protocol_id"` + + // UniqueID filters the response by unique ID. + UniqueID string `q:"unique_id"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToUserListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToUserListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the Users to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single user, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a user. +type CreateOpts struct { + // Name is the name of the new user. + Name string `json:"name" required:"true"` + + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id,omitempty"` + + // Description is a description of the user. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the user belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the user status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the user. + Extra map[string]interface{} `json:"-"` + + // Options are defined options in the API to enable certain features. + Options map[Option]interface{} `json:"options,omitempty"` + + // Password is the password of the new user. + Password string `json:"password,omitempty"` +} + +// ToUserCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["user"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new User. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a user account. +type UpdateOpts struct { + // Name is the name of the new user. + Name string `json:"name,omitempty"` + + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id,omitempty"` + + // Description is a description of the user. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the user belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the user status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the user. + Extra map[string]interface{} `json:"-"` + + // Options are defined options in the API to enable certain features. + Options map[Option]interface{} `json:"options,omitempty"` + + // Password is the password of the new user. + Password string `json:"password,omitempty"` +} + +// ToUserUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToUserUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["user"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing User. +func Update(client *eclcloud.ServiceClient, userID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToUserUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, userID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ChangePasswordOptsBuilder allows extensions to add additional parameters to +// the ChangePassword request. +type ChangePasswordOptsBuilder interface { + ToUserChangePasswordMap() (map[string]interface{}, error) +} + +// ChangePasswordOpts provides options for changing password for a user. +type ChangePasswordOpts struct { + // OriginalPassword is the original password of the user. + OriginalPassword string `json:"original_password"` + + // Password is the new password of the user. + Password string `json:"password"` +} + +// ToUserChangePasswordMap formats a ChangePasswordOpts into a ChangePassword request. +func (opts ChangePasswordOpts) ToUserChangePasswordMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + return b, nil +} + +// ChangePassword changes password for a user. +func ChangePassword(client *eclcloud.ServiceClient, userID string, opts ChangePasswordOptsBuilder) (r ChangePasswordResult) { + b, err := opts.ToUserChangePasswordMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(changePasswordURL(client, userID), &b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// Delete deletes a user. +func Delete(client *eclcloud.ServiceClient, userID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, userID), nil) + return +} + +// ListGroups enumerates groups user belongs to. +func ListGroups(client *eclcloud.ServiceClient, userID string) pagination.Pager { + url := listGroupsURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return groups.GroupPage{LinkedPageBase: pagination.LinkedPageBase{PageResult: r}} + }) +} + +// AddToGroup adds a user to a group. +func AddToGroup(client *eclcloud.ServiceClient, groupID, userID string) (r AddToGroupResult) { + url := addToGroupURL(client, groupID, userID) + _, r.Err = client.Put(url, nil, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// IsMemberOfGroup checks whether a user belongs to a group. +func IsMemberOfGroup(client *eclcloud.ServiceClient, groupID, userID string) (r IsMemberOfGroupResult) { + url := isMemberOfGroupURL(client, groupID, userID) + var response *http.Response + response, r.Err = client.Head(url, &eclcloud.RequestOpts{ + OkCodes: []int{204, 404}, + }) + if r.Err == nil && response != nil { + if (*response).StatusCode == 204 { + r.isMember = true + } + } + + return +} + +// RemoveFromGroup removes a user from a group. +func RemoveFromGroup(client *eclcloud.ServiceClient, groupID, userID string) (r RemoveFromGroupResult) { + url := removeFromGroupURL(client, groupID, userID) + _, r.Err = client.Delete(url, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// ListProjects enumerates groups user belongs to. +func ListProjects(client *eclcloud.ServiceClient, userID string) pagination.Pager { + url := listProjectsURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return projects.ProjectPage{LinkedPageBase: pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListInGroup enumerates users that belong to a group. +func ListInGroup(client *eclcloud.ServiceClient, groupID string, opts ListOptsBuilder) pagination.Pager { + url := listInGroupURL(client, groupID) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v3/ecl/identity/v3/users/results.go b/v3/ecl/identity/v3/users/results.go new file mode 100644 index 0000000..78106fc --- /dev/null +++ b/v3/ecl/identity/v3/users/results.go @@ -0,0 +1,178 @@ +package users + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/internal" + "github.com/nttcom/eclcloud/v3/pagination" + "time" +) + +// User represents a User in the Enterprise Cloud Identity Service. +type User struct { + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id"` + + // Description is the description of the user. + Description string `json:"description"` + + // DomainID is the domain ID the user belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the user is enabled. + Enabled bool `json:"enabled"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // ID is the unique ID of the user. + ID string `json:"id"` + + // Links contains referencing links to the user. + Links map[string]interface{} `json:"links"` + + // Name is the name of the user. + Name string `json:"name"` + + // Options are a set of defined options of the user. + Options map[string]interface{} `json:"options"` + + // PasswordExpiresAt is the timestamp when the user's password expires. + PasswordExpiresAt time.Time `json:"-"` +} + +func (r *User) UnmarshalJSON(b []byte) error { + type tmp User + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + PasswordExpiresAt eclcloud.JSONRFC3339MilliNoZ `json:"password_expires_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = User(s.tmp) + + r.PasswordExpiresAt = time.Time(s.PasswordExpiresAt) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "password_expires_at") + r.Extra = internal.RemainingKeys(User{}, resultMap) + } + } + + return err +} + +type userResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a User. +type GetResult struct { + userResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a User. +type CreateResult struct { + userResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a User. +type UpdateResult struct { + userResult +} + +// ChangePasswordResult is the response from a ChangePassword operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type ChangePasswordResult struct { + eclcloud.ErrResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// AddToGroupResult is the response from a AddToGroup operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type AddToGroupResult struct { + eclcloud.ErrResult +} + +// IsMemberOfGroupResult is the response from a IsMemberOfGroup operation. Call its +// Extract method to determine if the request succeeded or failed. +type IsMemberOfGroupResult struct { + isMember bool + eclcloud.Result +} + +// RemoveFromGroupResult is the response from a RemoveFromGroup operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RemoveFromGroupResult struct { + eclcloud.ErrResult +} + +// UserPage is a single page of User results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a UserPage contains any results. +func (r UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(r) + return len(users) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r UserPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractUsers returns a slice of Users contained in a single page of results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} + +// Extract interprets any user results as a User. +func (r userResult) Extract() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// Extract extracts IsMemberOfGroupResult as bool and error values +func (r IsMemberOfGroupResult) Extract() (bool, error) { + return r.isMember, r.Err +} diff --git a/v3/ecl/identity/v3/users/urls.go b/v3/ecl/identity/v3/users/urls.go new file mode 100644 index 0000000..697fa39 --- /dev/null +++ b/v3/ecl/identity/v3/users/urls.go @@ -0,0 +1,51 @@ +package users + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func updateURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func changePasswordURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "password") +} + +func deleteURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func listGroupsURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "groups") +} + +func addToGroupURL(client *eclcloud.ServiceClient, groupID, userID string) string { + return client.ServiceURL("groups", groupID, "users", userID) +} + +func isMemberOfGroupURL(client *eclcloud.ServiceClient, groupID, userID string) string { + return client.ServiceURL("groups", groupID, "users", userID) +} + +func removeFromGroupURL(client *eclcloud.ServiceClient, groupID, userID string) string { + return client.ServiceURL("groups", groupID, "users", userID) +} + +func listProjectsURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "projects") +} + +func listInGroupURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID, "users") +} diff --git a/v3/ecl/imagestorage/v2/imagedata/doc.go b/v3/ecl/imagestorage/v2/imagedata/doc.go new file mode 100644 index 0000000..0c12bf2 --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/doc.go @@ -0,0 +1,48 @@ +/* +Package imagedata enables management of image data. + +Example to Upload Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + imageData, err := os.Open("/path/to/image/file") + if err != nil { + panic(err) + } + defer imageData.Close() + + err = imagedata.Upload(imageClient, imageID, imageData).ExtractErr() + if err != nil { + panic(err) + } + +Example to Stage Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + imageData, err := os.Open("/path/to/image/file") + if err != nil { + panic(err) + } + defer imageData.Close() + + err = imagedata.Stage(imageClient, imageID, imageData).ExtractErr() + if err != nil { + panic(err) + } + +Example to Download Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + image, err := imagedata.Download(imageClient, imageID).Extract() + if err != nil { + panic(err) + } + + imageData, err := ioutil.ReadAll(image) + if err != nil { + panic(err) + } +*/ +package imagedata diff --git a/v3/ecl/imagestorage/v2/imagedata/requests.go b/v3/ecl/imagestorage/v2/imagedata/requests.go new file mode 100644 index 0000000..c80bfdc --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/requests.go @@ -0,0 +1,28 @@ +package imagedata + +import ( + "io" + "net/http" + + "github.com/nttcom/eclcloud/v3" +) + +// Upload uploads an image file. +func Upload(client *eclcloud.ServiceClient, id string, data io.Reader) (r UploadResult) { + _, r.Err = client.Put(uploadURL(client, id), data, nil, &eclcloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"}, + OkCodes: []int{204}, + }) + return +} + +// Download retrieves an image. +func Download(client *eclcloud.ServiceClient, id string) (r DownloadResult) { + var resp *http.Response + resp, r.Err = client.Get(downloadURL(client, id), nil, nil) + if resp != nil { + r.Body = resp.Body + r.Header = resp.Header + } + return +} diff --git a/v3/ecl/imagestorage/v2/imagedata/results.go b/v3/ecl/imagestorage/v2/imagedata/results.go new file mode 100644 index 0000000..a227351 --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/results.go @@ -0,0 +1,34 @@ +package imagedata + +import ( + "fmt" + "io" + + "github.com/nttcom/eclcloud/v3" +) + +// UploadResult is the result of an upload image operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type UploadResult struct { + eclcloud.ErrResult +} + +// StageResult is the result of a stage image operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StageResult struct { + eclcloud.ErrResult +} + +// DownloadResult is the result of a download image operation. Call its Extract +// method to gain access to the image data. +type DownloadResult struct { + eclcloud.Result +} + +// Extract builds images model from io.Reader +func (r DownloadResult) Extract() (io.Reader, error) { + if r, ok := r.Body.(io.Reader); ok { + return r, nil + } + return nil, fmt.Errorf("expected io.Reader but got: %T(%#v)", r.Body, r.Body) +} diff --git a/v3/ecl/imagestorage/v2/imagedata/testing/doc.go b/v3/ecl/imagestorage/v2/imagedata/testing/doc.go new file mode 100644 index 0000000..5a9db1b --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/testing/doc.go @@ -0,0 +1,2 @@ +// imagedata unit tests +package testing diff --git a/v3/ecl/imagestorage/v2/imagedata/testing/fixtures.go b/v3/ecl/imagestorage/v2/imagedata/testing/fixtures.go new file mode 100644 index 0000000..bfd216a --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/testing/fixtures.go @@ -0,0 +1,40 @@ +package testing + +import ( + "io/ioutil" + "net/http" + "testing" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// HandlePutImageDataSuccessfully setup +func HandlePutImageDataSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/0cb9328d-dd8c-41bb-b378-404b854b93b9/file", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read request body: %v", err) + } + + th.AssertByteArrayEquals(t, []byte{5, 3, 7, 24}, b) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleGetImageDataSuccessfully setup +func HandleGetImageDataSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/100f4d2d-dcb5-472e-b93f-b4e13d888604/file", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + + _, err := w.Write([]byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}) + th.AssertNoErr(t, err) + }) +} diff --git a/v3/ecl/imagestorage/v2/imagedata/testing/requests_test.go b/v3/ecl/imagestorage/v2/imagedata/testing/requests_test.go new file mode 100644 index 0000000..23f232d --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/testing/requests_test.go @@ -0,0 +1,103 @@ +package testing + +import ( + "fmt" + "io" + "io/ioutil" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/imagestorage/v2/imagedata" + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestUpload(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandlePutImageDataSuccessfully(t) + + err := imagedata.Upload( + fakeclient.ServiceClient(), + "0cb9328d-dd8c-41bb-b378-404b854b93b9", + readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr() + + th.AssertNoErr(t, err) +} + +/* +func TestStage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleStageImageDataSuccessfully(t) + + err := imagedata.Stage( + fakeclient.ServiceClient(), + "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr() + + th.AssertNoErr(t, err) +} +*/ + +func readSeekerOfBytes(bs []byte) io.ReadSeeker { + return &RS{bs: bs} +} + +// implements io.ReadSeeker +type RS struct { + bs []byte + offset int +} + +func (rs *RS) Read(p []byte) (int, error) { + leftToRead := len(rs.bs) - rs.offset + + if 0 < leftToRead { + bytesToWrite := min(leftToRead, len(p)) + for i := 0; i < bytesToWrite; i++ { + p[i] = rs.bs[rs.offset] + rs.offset++ + } + return bytesToWrite, nil + } + return 0, io.EOF +} + +func min(a int, b int) int { + if a < b { + return a + } + return b +} + +func (rs *RS) Seek(offset int64, whence int) (int64, error) { + var offsetInt = int(offset) + if whence == 0 { + rs.offset = offsetInt + } else if whence == 1 { + rs.offset = rs.offset + offsetInt + } else if whence == 2 { + rs.offset = len(rs.bs) - offsetInt + } else { + return 0, fmt.Errorf("for parameter `whence`, expected value in {0,1,2} but got: %#v", whence) + } + + return int64(rs.offset), nil +} + +func TestDownload(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetImageDataSuccessfully(t) + + rdr, err := imagedata.Download(fakeclient.ServiceClient(), "100f4d2d-dcb5-472e-b93f-b4e13d888604").Extract() + th.AssertNoErr(t, err) + + bs, err := ioutil.ReadAll(rdr) + th.AssertNoErr(t, err) + + th.AssertByteArrayEquals(t, []byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}, bs) +} diff --git a/v3/ecl/imagestorage/v2/imagedata/urls.go b/v3/ecl/imagestorage/v2/imagedata/urls.go new file mode 100644 index 0000000..e66db63 --- /dev/null +++ b/v3/ecl/imagestorage/v2/imagedata/urls.go @@ -0,0 +1,23 @@ +package imagedata + +import "github.com/nttcom/eclcloud/v3" + +const ( + rootPath = "images" + uploadPath = "file" + stagePath = "stage" +) + +// `imageDataURL(c,i)` is the URL for the binary image data for the +// image identified by ID `i` in the service `c`. +func uploadURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, uploadPath) +} + +func stageURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, stagePath) +} + +func downloadURL(c *eclcloud.ServiceClient, imageID string) string { + return uploadURL(c, imageID) +} diff --git a/v3/ecl/imagestorage/v2/images/doc.go b/v3/ecl/imagestorage/v2/images/doc.go new file mode 100644 index 0000000..cd3f5e2 --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/doc.go @@ -0,0 +1,60 @@ +/* +Package images enables management and retrieval of images from the Enterprise Cloud +Image Service. + +Example to List Images + + images.ListOpts{ + Owner: "a7509e1ae65945fda83f3e52c6296017", + } + + allPages, err := images.List(imagesClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } + +Example to Create an Image + + createOpts := images.CreateOpts{ + Name: "image_name", + Visibility: images.ImageVisibilityPrivate, + } + + image, err := images.Create(imageClient, createOpts) + if err != nil { + panic(err) + } + +Example to Update an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + + updateOpts := images.UpdateOpts{ + images.ReplaceImageName{ + NewName: "new_name", + }, + } + + image, err := images.Update(imageClient, imageID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + err := images.Delete(imageClient, imageID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package images diff --git a/v3/ecl/imagestorage/v2/images/requests.go b/v3/ecl/imagestorage/v2/images/requests.go new file mode 100644 index 0000000..78ff19f --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/requests.go @@ -0,0 +1,349 @@ +package images + +import ( + "fmt" + "net/url" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (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 { + // ID is the ID of the image. + // Multiple IDs can be specified by constructing a string + // such as "in:uuid1,uuid2,uuid3". + ID string `q:"id"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Name filters on the name of the image. + // Multiple names can be specified by constructing a string + // such as "in:name1,name2,name3". + Name string `q:"name"` + + // Visibility filters on the visibility of the image. + Visibility ImageVisibility `q:"visibility"` + + // MemberStatus filters on the member status of the image. + MemberStatus ImageMemberStatus `q:"member_status"` + + // Owner filters on the project ID of the image. + Owner string `q:"owner"` + + // Status filters on the status of the image. + // Multiple statuses can be specified by constructing a string + // such as "in:saving,queued". + Status ImageStatus `q:"status"` + + // SizeMin filters on the size_min image property. + SizeMin int64 `q:"size_min"` + + // SizeMax filters on the size_max image property. + SizeMax int64 `q:"size_max"` + + // Sort sorts the results using the new style of sorting. See the Enterprise Cloud + // Image API reference for the exact syntax. + // + // Sort cannot be used with the classic sort options (sort_key and sort_dir). + Sort string `q:"sort"` + + // SortKey will sort the results based on a specified image property. + SortKey string `q:"sort_key"` + + // SortDir will sort the list results either ascending or decending. + SortDir string `q:"sort_dir"` + + // Tags filters on specific image tags. + Tags []string `q:"tag"` + + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery + + // ContainerFormat filters images based on the container_format. + // Multiple container formats can be specified by constructing a + // string such as "in:bare,ami". + ContainerFormat string `q:"container_format"` + + // DiskFormat filters images based on the disk_format. + // Multiple disk formats can be specified by constructing a string + // such as "in:qcow2,iso". + DiskFormat string `q:"disk_format"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + + return q.String(), err +} + +// List implements image list request. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + imagePage := ImagePage{ + serviceURL: c.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create an image. +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name,omitempty"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{OkCodes: []int{201}}) + return +} + +// Delete implements image delete request. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get implements image get request. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Update implements image updated request. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches + // the patch schema. + ToImageUpdateMap() ([]interface{}, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap assembles a request body based on the contents of +// UpdateOpts. +func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) { + m := make([]interface{}, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates +// to an image can be submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]interface{} +} + +// UpdateVisibility represents an updated visibility property request. +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap assembles a request body based on UpdateVisibility. +func (r UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": r.Visibility, + } +} + +// ReplaceImageName represents an updated image_name property request. +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageName. +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum represents an updated checksum property request. +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum. +func (r ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": r.Checksum, + } +} + +// ReplaceImageTags represents an updated tags property request. +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} + +// UpdateOp represents a valid update operation. +type UpdateOp string + +const ( + AddOp UpdateOp = "add" + ReplaceOp UpdateOp = "replace" + RemoveOp UpdateOp = "remove" +) + +// UpdateImageProperty represents an update property request. +type UpdateImageProperty struct { + Op UpdateOp + Name string + Value string +} + +// ToImagePatchMap assembles a request body based on UpdateImageProperty. +func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} { + updateMap := map[string]interface{}{ + "op": r.Op, + "path": fmt.Sprintf("/%s", r.Name), + } + + if r.Value != "" { + updateMap["value"] = r.Value + } + + return updateMap +} diff --git a/v3/ecl/imagestorage/v2/images/results.go b/v3/ecl/imagestorage/v2/images/results.go new file mode 100644 index 0000000..f373fdc --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/results.go @@ -0,0 +1,201 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/internal" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Image represents an image found in the Enterprise Cloud Image service. +type Image struct { + // ID is the image UUID. + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See imageservice/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to + // boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to + // boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant ID the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Checksum is the checksum of the data that's associated with the image. + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"-"` + + // Metadata is a set of metadata associated with the image. + // Image metadata allow for meaningfully define the image properties + // and tags. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with + // the image. + Properties map[string]interface{} + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or + // it's properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the + // location of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image + // entity. + Schema string `json:"schema"` + + // VirtualSize is the virtual size of the image + VirtualSize int64 `json:"virtual_size"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes interface{} `json:"size"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + r.SizeBytes = 0 + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + // Bundle all other fields into Properties + var result interface{} + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "self") + delete(resultMap, "size") + r.Properties = internal.RemainingKeys(Image{}, resultMap) + } + + return err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as an Image. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as an Image. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret it as an Image. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to interpret it as an Image. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.serviceURL, s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, +// producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/v3/ecl/imagestorage/v2/images/testing/doc.go b/v3/ecl/imagestorage/v2/images/testing/doc.go new file mode 100644 index 0000000..d4bd4f3 --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains images unit tests +package testing diff --git a/v3/ecl/imagestorage/v2/images/testing/fixtures.go b/v3/ecl/imagestorage/v2/images/testing/fixtures.go new file mode 100644 index 0000000..1cb4411 --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/testing/fixtures.go @@ -0,0 +1,447 @@ +package testing + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "testing" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +type imageEntry struct { + ID string + JSON string +} + +// HandleImageListSuccessfully test setup +func HandleImageListSuccessfully(t *testing.T) { + + images := make([]imageEntry, 3) + + images[0] = imageEntry{"cirros-0.3.4-x86_64-uec", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec", + "tags": [], + "kernel_id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "container_format": "ami", + "created_at": "2015-07-15T11:43:35Z", + "ramdisk_id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "disk_format": "ami", + "updated_at": "2015-07-15T11:43:35Z", + "visibility": "public", + "self": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431", + "min_disk": 0, + "protected": false, + "id": "07aa21a9-fa1a-430e-9a33-185be5982431", + "size": 25165824, + "file": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431/file", + "checksum": "eb9139e4942121f22bbc2afc0400b2a4", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + images[1] = imageEntry{"cirros-0.3.4-x86_64-uec-ramdisk", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec-ramdisk", + "tags": [], + "container_format": "ari", + "created_at": "2015-07-15T11:43:32Z", + "size": 3740163, + "disk_format": "ari", + "updated_at": "2015-07-15T11:43:32Z", + "visibility": "public", + "self": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "min_disk": 0, + "protected": false, + "id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "file": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b/file", + "checksum": "be575a2b939972276ef675752936977f", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + images[2] = imageEntry{"cirros-0.3.4-x86_64-uec-kernel", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec-kernel", + "tags": [], + "container_format": "aki", + "created_at": "2015-07-15T11:43:29Z", + "size": 4979632, + "disk_format": "aki", + "updated_at": "2015-07-15T11:43:30Z", + "visibility": "public", + "self": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "min_disk": 0, + "protected": false, + "id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "file": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4/file", + "checksum": "8a40c862b5735975d82605c1dd395796", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + limit := 10 + var err error + if r.FormValue("limit") != "" { + limit, err = strconv.Atoi(r.FormValue("limit")) + if err != nil { + t.Errorf("Error value for 'limit' parameter %v (error: %v)", r.FormValue("limit"), err) + } + + } + + marker := "" + newMarker := "" + + if r.Form["marker"] != nil { + marker = r.Form["marker"][0] + } + + t.Logf("limit = %v marker = %v", limit, marker) + + selected := 0 + addNext := false + var imageJSON []string + + fmt.Fprintf(w, `{"images": [`) + + for _, i := range images { + if marker == "" || addNext { + t.Logf("Adding image %v to page", i.ID) + imageJSON = append(imageJSON, i.JSON) + newMarker = i.ID + selected++ + } else { + if strings.Contains(i.JSON, marker) { + addNext = true + } + } + + if selected == limit { + break + } + } + t.Logf("Writing out %v image(s)", len(imageJSON)) + fmt.Fprintf(w, strings.Join(imageJSON, ",")) + + fmt.Fprintf(w, `], + "next": "/images?marker=%s&limit=%v", + "schema": "/schemas/images", + "first": "/images?limit=%v"}`, newMarker, limit, limit) + + }) +} + +// HandleImageCreationSuccessfully test setup +func HandleImageCreationSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, `{ + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "name": "Ubuntu 12.10", + "architecture": "x86_64", + "tags": [ + "ubuntu", + "quantal" + ] + }`) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "queued", + "name": "Ubuntu 12.10", + "protected": false, + "tags": ["ubuntu","quantal"], + "container_format": "bare", + "created_at": "2014-11-11T20:47:55Z", + "disk_format": "qcow2", + "updated_at": "2014-11-11T20:47:55Z", + "visibility": "private", + "self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "min_disk": 0, + "protected": false, + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file", + "owner": "b4eedccc6fb74fa8a7ad6b08382b852b", + "min_ram": 0, + "schema": "/v2/schemas/image", + "size": 0, + "checksum": "", + "virtual_size": 0, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageCreationSuccessfullyNulls test setup +// JSON null values could be also returned according to behaviour https://bugs.launchpad.net/glance/+bug/1481512 +func HandleImageCreationSuccessfullyNulls(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, `{ + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "architecture": "x86_64", + "name": "Ubuntu 12.10", + "tags": [ + "ubuntu", + "quantal" + ] + }`) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "architecture": "x86_64", + "status": "queued", + "name": "Ubuntu 12.10", + "protected": false, + "tags": ["ubuntu","quantal"], + "container_format": "bare", + "created_at": "2014-11-11T20:47:55Z", + "disk_format": "qcow2", + "updated_at": "2014-11-11T20:47:55Z", + "visibility": "private", + "self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "min_disk": 0, + "protected": false, + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file", + "owner": "b4eedccc6fb74fa8a7ad6b08382b852b", + "min_ram": 0, + "schema": "/v2/schemas/image", + "size": null, + "checksum": null, + "virtual_size": null + }`) + }) +} + +// HandleImageGetSuccessfully test setup +func HandleImageGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "active", + "name": "cirros-0.3.2-x86_64-disk", + "tags": [], + "container_format": "bare", + "created_at": "2014-05-05T17:15:10Z", + "disk_format": "qcow2", + "updated_at": "2014-05-05T17:15:11Z", + "visibility": "public", + "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "min_disk": 0, + "protected": false, + "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", + "checksum": "64d7c1cd2b6f60c92c14662941cb7913", + "owner": "5ef70662f8b34079a6eddb8da9d75fe8", + "size": 13167616, + "min_ram": 0, + "schema": "/v2/schemas/image", + "virtual_size": null, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageDeleteSuccessfully test setup +func HandleImageDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleImageUpdateSuccessfully setup +func HandleImageUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `[ + { + "op": "replace", + "path": "/name", + "value": "Fedora 17" + }, + { + "op": "replace", + "path": "/tags", + "value": [ + "fedora", + "beefy" + ] + } + ]`) + + th.AssertEquals(t, "application/openstack-images-v2.1-json-patch", r.Header.Get("Content-Type")) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "name": "Fedora 17", + "status": "active", + "visibility": "public", + "size": 2254249, + "checksum": "2cec138d7dae2aa59038ef8c9aec2390", + "tags": [ + "fedora", + "beefy" + ], + "created_at": "2012-08-10T19:23:50Z", + "updated_at": "2012-08-12T11:11:33Z", + "self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", + "schema": "/v2/schemas/image", + "owner": "", + "min_ram": 0, + "min_disk": 0, + "disk_format": "", + "virtual_size": 0, + "container_format": "", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageListByTagsSuccessfully tests a list operation with tags. +func HandleImageListByTagsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "images": [ + { + "status": "active", + "name": "cirros-0.3.2-x86_64-disk", + "tags": ["foo", "bar"], + "container_format": "bare", + "created_at": "2014-05-05T17:15:10Z", + "disk_format": "qcow2", + "updated_at": "2014-05-05T17:15:11Z", + "visibility": "public", + "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "min_disk": 0, + "protected": false, + "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", + "checksum": "64d7c1cd2b6f60c92c14662941cb7913", + "owner": "5ef70662f8b34079a6eddb8da9d75fe8", + "size": 13167616, + "min_ram": 0, + "schema": "/v2/schemas/image", + "virtual_size": null, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + } + ] + }`) + }) +} + +// HandleImageUpdatePropertiesSuccessfully setup +func HandleImageUpdatePropertiesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `[ + { + "op": "add", + "path": "/hw_disk_bus", + "value": "scsi" + }, + { + "op": "add", + "path": "/hw_disk_bus_model", + "value": "virtio-scsi" + }, + { + "op": "add", + "path": "/hw_scsi_model", + "value": "virtio-scsi" + } + ]`) + + th.AssertEquals(t, "application/openstack-images-v2.1-json-patch", r.Header.Get("Content-Type")) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "name": "Fedora 17", + "status": "active", + "visibility": "public", + "size": 2254249, + "checksum": "2cec138d7dae2aa59038ef8c9aec2390", + "tags": [ + "fedora", + "beefy" + ], + "created_at": "2012-08-10T19:23:50Z", + "updated_at": "2012-08-12T11:11:33Z", + "self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", + "schema": "/v2/schemas/image", + "owner": "", + "min_ram": 0, + "min_disk": 0, + "disk_format": "", + "virtual_size": 0, + "container_format": "", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} diff --git a/v3/ecl/imagestorage/v2/images/testing/requests_test.go b/v3/ecl/imagestorage/v2/images/testing/requests_test.go new file mode 100644 index 0000000..741cefd --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/testing/requests_test.go @@ -0,0 +1,458 @@ +package testing + +import ( + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/imagestorage/v2/images" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListSuccessfully(t) + + t.Logf("Test setup %+v\n", th.Server) + + t.Logf("Id\tName\tOwner\tChecksum\tSizeBytes") + + pager := images.List(fakeclient.ServiceClient(), images.ListOpts{Limit: 1}) + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + images, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range images { + t.Logf("%s\t%s\t%s\t%s\t%v\t\n", i.ID, i.Name, i.Owner, i.Checksum, i.SizeBytes) + count++ + } + + return true, nil + }) + th.AssertNoErr(t, err) + + t.Logf("--------\n%d images listed on %d pages.\n", count, pages) + th.AssertEquals(t, 3, pages) + th.AssertEquals(t, 3, count) +} + +func TestAllPagesImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListSuccessfully(t) + + pages, err := images.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + images, err := images.ExtractImages(pages) + th.AssertNoErr(t, err) + th.AssertEquals(t, 3, len(images)) +} + +func TestCreateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageCreationSuccessfully(t) + + id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd" + name := "Ubuntu 12.10" + + actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{ + ID: id, + Name: name, + Properties: map[string]string{ + "architecture": "x86_64", + }, + Tags: []string{"ubuntu", "quantal"}, + }).Extract() + + th.AssertNoErr(t, err) + + containerFormat := "bare" + diskFormat := "qcow2" + owner := "b4eedccc6fb74fa8a7ad6b08382b852b" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + Name: "Ubuntu 12.10", + Tags: []string{"ubuntu", "quantal"}, + + Status: images.ImageStatusQueued, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Visibility: images.ImageVisibilityPrivate, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestCreateImageNulls(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageCreationSuccessfullyNulls(t) + + id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd" + name := "Ubuntu 12.10" + + actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{ + ID: id, + Name: name, + Tags: []string{"ubuntu", "quantal"}, + Properties: map[string]string{ + "architecture": "x86_64", + }, + }).Extract() + + th.AssertNoErr(t, err) + + containerFormat := "bare" + diskFormat := "qcow2" + owner := "b4eedccc6fb74fa8a7ad6b08382b852b" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + properties := map[string]interface{}{ + "architecture": "x86_64", + } + sizeBytes := int64(0) + + expectedImage := images.Image{ + ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + Name: "Ubuntu 12.10", + Tags: []string{"ubuntu", "quantal"}, + + Status: images.ImageStatusQueued, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Visibility: images.ImageVisibilityPrivate, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + Properties: properties, + SizeBytes: sizeBytes, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageGetSuccessfully(t) + + actualImage, err := images.Get(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27").Extract() + + th.AssertNoErr(t, err) + + checksum := "64d7c1cd2b6f60c92c14662941cb7913" + sizeBytes := int64(13167616) + containerFormat := "bare" + diskFormat := "qcow2" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + owner := "5ef70662f8b34079a6eddb8da9d75fe8" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + Name: "cirros-0.3.2-x86_64-disk", + Tags: []string{}, + + Status: images.ImageStatusActive, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Protected: false, + Visibility: images.ImageVisibilityPublic, + + Checksum: checksum, + SizeBytes: sizeBytes, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestDeleteImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageDeleteSuccessfully(t) + + result := images.Delete(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27") + th.AssertNoErr(t, result.Err) +} + +func TestUpdateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageUpdateSuccessfully(t) + + actualImage, err := images.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", images.UpdateOpts{ + images.ReplaceImageName{NewName: "Fedora 17"}, + images.ReplaceImageTags{NewTags: []string{"fedora", "beefy"}}, + }).Extract() + + th.AssertNoErr(t, err) + + sizebytes := int64(2254249) + checksum := "2cec138d7dae2aa59038ef8c9aec2390" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + Name: "Fedora 17", + Status: images.ImageStatusActive, + Visibility: images.ImageVisibilityPublic, + + SizeBytes: sizebytes, + Checksum: checksum, + + Tags: []string{ + "fedora", + "beefy", + }, + + Owner: "", + MinRAMMegabytes: 0, + MinDiskGigabytes: 0, + + DiskFormat: "", + ContainerFormat: "", + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestImageDateQuery(t *testing.T) { + date := time.Date(2014, 1, 1, 1, 1, 1, 0, time.UTC) + + listOpts := images.ListOpts{ + CreatedAtQuery: &images.ImageDateQuery{ + Date: date, + Filter: images.FilterGTE, + }, + UpdatedAtQuery: &images.ImageDateQuery{ + Date: date, + }, + } + + expectedQueryString := "?created_at=gte%3A2014-01-01T01%3A01%3A01Z&updated_at=2014-01-01T01%3A01%3A01Z" + actualQueryString, err := listOpts.ToImageListQuery() + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedQueryString, actualQueryString) +} + +func TestImageListByTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListByTagsSuccessfully(t) + + listOpts := images.ListOpts{ + Tags: []string{"foo", "bar"}, + } + + expectedQueryString := "?tag=foo&tag=bar" + actualQueryString, err := listOpts.ToImageListQuery() + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedQueryString, actualQueryString) + + pages, err := images.List(fakeclient.ServiceClient(), listOpts).AllPages() + th.AssertNoErr(t, err) + allImages, err := images.ExtractImages(pages) + th.AssertNoErr(t, err) + + checksum := "64d7c1cd2b6f60c92c14662941cb7913" + sizeBytes := int64(13167616) + containerFormat := "bare" + diskFormat := "qcow2" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + owner := "5ef70662f8b34079a6eddb8da9d75fe8" + file := allImages[0].File + createdDate := allImages[0].CreatedAt + lastUpdate := allImages[0].UpdatedAt + schema := "/v2/schemas/image" + tags := []string{"foo", "bar"} + + expectedImage := images.Image{ + ID: "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + Name: "cirros-0.3.2-x86_64-disk", + Tags: tags, + + Status: images.ImageStatusActive, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Protected: false, + Visibility: images.ImageVisibilityPublic, + + Checksum: checksum, + SizeBytes: sizeBytes, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, expectedImage, allImages[0]) +} + +func TestUpdateImageProperties(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageUpdatePropertiesSuccessfully(t) + + actualImage, err := images.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", images.UpdateOpts{ + images.UpdateImageProperty{ + Op: images.AddOp, + Name: "hw_disk_bus", + Value: "scsi", + }, + images.UpdateImageProperty{ + Op: images.AddOp, + Name: "hw_disk_bus_model", + Value: "virtio-scsi", + }, + images.UpdateImageProperty{ + Op: images.AddOp, + Name: "hw_scsi_model", + Value: "virtio-scsi", + }, + }).Extract() + + th.AssertNoErr(t, err) + + sizebytes := int64(2254249) + checksum := "2cec138d7dae2aa59038ef8c9aec2390" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + Name: "Fedora 17", + Status: images.ImageStatusActive, + Visibility: images.ImageVisibilityPublic, + + SizeBytes: sizebytes, + Checksum: checksum, + + Tags: []string{ + "fedora", + "beefy", + }, + + Owner: "", + MinRAMMegabytes: 0, + MinDiskGigabytes: 0, + + DiskFormat: "", + ContainerFormat: "", + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} diff --git a/v3/ecl/imagestorage/v2/images/types.go b/v3/ecl/imagestorage/v2/images/types.go new file mode 100644 index 0000000..5005b34 --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/types.go @@ -0,0 +1,101 @@ +package images + +import ( + "time" +) + +// ImageStatus image statuses +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image’s raw data is currently being + // uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet + // deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to + // any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size is explicitly +// set to zero on creation. +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with + // member_status == 'accepted' have this image in their default image-list. + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image +// member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} diff --git a/v3/ecl/imagestorage/v2/images/urls.go b/v3/ecl/imagestorage/v2/images/urls.go new file mode 100644 index 0000000..ecc40e4 --- /dev/null +++ b/v3/ecl/imagestorage/v2/images/urls.go @@ -0,0 +1,64 @@ +package images + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/utils" + "net/url" + "strings" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *eclcloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *eclcloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *eclcloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = eclcloud.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + + return nextURL.String(), nil +} diff --git a/v3/ecl/imagestorage/v2/members/doc.go b/v3/ecl/imagestorage/v2/members/doc.go new file mode 100644 index 0000000..acb4272 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/doc.go @@ -0,0 +1 @@ +package members diff --git a/v3/ecl/imagestorage/v2/members/requests.go b/v3/ecl/imagestorage/v2/members/requests.go new file mode 100644 index 0000000..753b682 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/requests.go @@ -0,0 +1,80 @@ +package members + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +/* + Create member for specific image + + Preconditions + + * The specified images must exist. + * You can only add a new member to an image which 'visibility' attribute is + private. + * You must be the owner of the specified image. + + Synchronous Postconditions + + With correct permissions, you can see the member status of the image as + pending through API calls. + +*/ + +func Create(client *eclcloud.ServiceClient, id string, member string) (r CreateResult) { + b := map[string]interface{}{"member": member} + _, r.Err = client.Post(createMemberURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// List members returns list of members for specifed image id. +func List(client *eclcloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listMembersURL(client, id), func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.SinglePageBase(r)} + }) +} + +// Get image member details. +func Get(client *eclcloud.ServiceClient, imageID string, memberID string) (r DetailsResult) { + _, r.Err = client.Get(getMemberURL(client, imageID, memberID), &r.Body, &eclcloud.RequestOpts{OkCodes: []int{200}}) + return +} + +// Delete membership for given image. Callee should be image owner. +func Delete(client *eclcloud.ServiceClient, imageID string, memberID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteMemberURL(client, imageID, memberID), &eclcloud.RequestOpts{OkCodes: []int{204}}) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToImageMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options to an Update request. +type UpdateOpts struct { + Status string `json:"status,omitempty" required:"true1"` +} + +// ToMemberUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToImageMemberUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "status": opts.Status, + }, nil +} + +// Update function updates member. +func Update(client *eclcloud.ServiceClient, imageID string, memberID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageMemberUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body, + &eclcloud.RequestOpts{OkCodes: []int{200}}) + return +} diff --git a/v3/ecl/imagestorage/v2/members/results.go b/v3/ecl/imagestorage/v2/members/results.go new file mode 100644 index 0000000..a033264 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/results.go @@ -0,0 +1,74 @@ +package members + +import ( + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Member represents a member of an Image. +type Member struct { + CreatedAt time.Time `json:"created_at"` + ImageID string `json:"image_id"` + MemberID string `json:"member_id"` + Schema string `json:"schema"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Extract Member model from a request. +func (r commonResult) Extract() (*Member, error) { + var s *Member + err := r.ExtractInto(&s) + return s, err +} + +// MemberPage is a single page of Members results. +type MemberPage struct { + pagination.SinglePageBase +} + +// ExtractMembers returns a slice of Members contained in a single page +// of results. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := r.(MemberPage).ExtractInto(&s) + return s.Members, err +} + +// IsEmpty determines whether or not a MemberPage contains any results. +func (r MemberPage) IsEmpty() (bool, error) { + members, err := ExtractMembers(r) + return len(members) == 0, err +} + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as a Member. +type CreateResult struct { + commonResult +} + +// DetailsResult represents the result of a Get operation. Call its Extract +// method to interpret it as a Member. +type DetailsResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as a Member. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/imagestorage/v2/members/testing/doc.go b/v3/ecl/imagestorage/v2/members/testing/doc.go new file mode 100644 index 0000000..1afbc43 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/testing/doc.go @@ -0,0 +1,2 @@ +// members unit tests +package testing diff --git a/v3/ecl/imagestorage/v2/members/testing/fixtures.go b/v3/ecl/imagestorage/v2/members/testing/fixtures.go new file mode 100644 index 0000000..69df704 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/testing/fixtures.go @@ -0,0 +1,138 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// HandleCreateImageMemberSuccessfully setup +func HandleCreateImageMemberSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `{"member": "f6a818c3d4aa458798ed86892e7150c0"}`) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "created_at": "2013-09-20T19:22:19Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member", + "status": "pending", + "updated_at": "2013-09-20T19:25:31Z" + }`) + + }) +} + +// HandleImageMemberList happy path setup +func HandleImageMemberList(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "members": [ + { + "created_at": "2013-10-07T17:58:03Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member", + "status": "pending", + "updated_at": "2013-10-07T17:58:03Z" + }, + { + "created_at": "2013-10-07T17:58:55Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "1efb79fe4437490aab966b57da5b9f05", + "schema": "/v2/schemas/member", + "status": "accepted", + "updated_at": "2013-10-08T12:08:55Z" + } + ], + "schema": "/v2/schemas/members" + }`) + }) +} + +// HandleImageMemberEmptyList happy path setup +func HandleImageMemberEmptyList(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "members": [], + "schema": "/v2/schemas/members" + }`) + }) +} + +// HandleImageMemberDetails setup +func HandleImageMemberDetails(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members/f6a818c3d4aa458798ed86892e7150c0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "status": "pending", + "created_at": "2013-11-26T07:21:21Z", + "updated_at": "2013-11-26T07:21:21Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member" + }`) + }) +} + +// HandleImageMemberDeleteSuccessfully setup +func HandleImageMemberDeleteSuccessfully(t *testing.T) *CallsCounter { + var counter CallsCounter + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members/f6a818c3d4aa458798ed86892e7150c0", func(w http.ResponseWriter, r *http.Request) { + counter.Counter = counter.Counter + 1 + + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + return &counter +} + +// HandleImageMemberUpdate setup +func HandleImageMemberUpdate(t *testing.T) *CallsCounter { + var counter CallsCounter + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members/f6a818c3d4aa458798ed86892e7150c0", func(w http.ResponseWriter, r *http.Request) { + counter.Counter = counter.Counter + 1 + + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `{"status": "accepted"}`) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "status": "accepted", + "created_at": "2013-11-26T07:21:21Z", + "updated_at": "2013-11-26T07:21:21Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member" + }`) + }) + return &counter +} + +// CallsCounter for checking if request handler was called at all +type CallsCounter struct { + Counter int +} diff --git a/v3/ecl/imagestorage/v2/members/testing/requests_test.go b/v3/ecl/imagestorage/v2/members/testing/requests_test.go new file mode 100644 index 0000000..9cb05f9 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/testing/requests_test.go @@ -0,0 +1,172 @@ +package testing + +import ( + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/imagestorage/v2/members" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +const createdAtString = "2013-09-20T19:22:19Z" +const updatedAtString = "2013-09-20T19:25:31Z" + +func TestCreateMemberSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateImageMemberSuccessfully(t) + im, err := members.Create(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0").Extract() + th.AssertNoErr(t, err) + + createdAt, err := time.Parse(time.RFC3339, createdAtString) + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, updatedAtString) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + MemberID: "f6a818c3d4aa458798ed86892e7150c0", + Schema: "/v2/schemas/member", + Status: "pending", + UpdatedAt: updatedAt, + }, *im) + +} + +func TestMemberListSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberList(t) + + pager := members.List(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb") + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + members, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, i := range members { + t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema) + count++ + } + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) + th.AssertEquals(t, 2, count) +} + +func TestMemberListEmpty(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberEmptyList(t) + + pager := members.List(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb") + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + members, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, i := range members { + t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema) + count++ + } + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 0, pages) + th.AssertEquals(t, 0, count) +} + +func TestShowMemberDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberDetails(t) + md, err := members.Get(fakeclient.ServiceClient(), + "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0").Extract() + + th.AssertNoErr(t, err) + if md == nil { + t.Errorf("Expected non-nil value for md") + } + + createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + MemberID: "f6a818c3d4aa458798ed86892e7150c0", + Schema: "/v2/schemas/member", + Status: "pending", + UpdatedAt: updatedAt, + }, *md) +} + +func TestDeleteMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + counter := HandleImageMemberDeleteSuccessfully(t) + + result := members.Delete(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0") + th.AssertEquals(t, 1, counter.Counter) + th.AssertNoErr(t, result.Err) +} + +func TestMemberUpdateSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + counter := HandleImageMemberUpdate(t) + im, err := members.Update(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0", + members.UpdateOpts{ + Status: "accepted", + }).Extract() + th.AssertEquals(t, 1, counter.Counter) + th.AssertNoErr(t, err) + + createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + MemberID: "f6a818c3d4aa458798ed86892e7150c0", + Schema: "/v2/schemas/member", + Status: "accepted", + UpdatedAt: updatedAt, + }, *im) + +} diff --git a/v3/ecl/imagestorage/v2/members/urls.go b/v3/ecl/imagestorage/v2/members/urls.go new file mode 100644 index 0000000..53a92c6 --- /dev/null +++ b/v3/ecl/imagestorage/v2/members/urls.go @@ -0,0 +1,31 @@ +package members + +import "github.com/nttcom/eclcloud/v3" + +func imageMembersURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID, "members") +} + +func listMembersURL(c *eclcloud.ServiceClient, imageID string) string { + return imageMembersURL(c, imageID) +} + +func createMemberURL(c *eclcloud.ServiceClient, imageID string) string { + return imageMembersURL(c, imageID) +} + +func imageMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return c.ServiceURL("images", imageID, "members", memberID) +} + +func getMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} + +func updateMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} + +func deleteMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} diff --git a/v3/ecl/network/v2/common/common_tests.go b/v3/ecl/network/v2/common/common_tests.go new file mode 100644 index 0000000..52db36b --- /dev/null +++ b/v3/ecl/network/v2/common/common_tests.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc +} diff --git a/v3/ecl/network/v2/common_function_gateways/doc.go b/v3/ecl/network/v2/common_function_gateways/doc.go new file mode 100644 index 0000000..766c6e0 --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/doc.go @@ -0,0 +1,57 @@ +/* +Package common_function_gateways contains functionality for working with +ECL Commnon Function Gateway resources. + +Example to List CommonFunctionGateways + + listOpts := common_function_gateways.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := common_function_gateways.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allCommonFunctionGateways, err := common_function_gateways.ExtractCommonFunctionGateways(allPages) + if err != nil { + panic(err) + } + + for _, common_function_gateways := range allCommonFunctionGateways { + fmt.Printf("%+v", common_function_gateways) + } + +Example to Create a common_function_gateways + + createOpts := common_function_gateways.CreateOpts{ + Name: "network_1", + } + + common_function_gateways, err := common_function_gateways.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a common_function_gateways + + commonFunctionGatewayID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := common_function_gateways.UpdateOpts{ + Name: "new_name", + } + + common_function_gateways, err := common_function_gateways.Update(networkClient, commonFunctionGatewayID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a common_function_gateways + + commonFunctionGatewayID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := common_function_gateways.Delete(networkClient, commonFunctionGatewayID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package common_function_gateways diff --git a/v3/ecl/network/v2/common_function_gateways/requests.go b/v3/ecl/network/v2/common_function_gateways/requests.go new file mode 100644 index 0000000..2fa70f7 --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/requests.go @@ -0,0 +1,169 @@ +package common_function_gateways + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToCommonFunctionGatewayListQuery() (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 common function gateway attributes you want to see returned. +type ListOpts struct { + CommonFunctionPoolID string `q:"common_function_pool_id"` + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + Status string `q:"status"` + SubnetID string `q:"subnet_id"` + TenantID string `q:"tenant_id"` +} + +// ToCommonFunctionGatewayListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCommonFunctionGatewayListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// common function gateways. +// It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToCommonFunctionGatewayListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CommonFunctionGatewayPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific common function gateway based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToCommonFunctionGatewayCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a common function gateway. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CommonFunctionPoolID string `json:"common_function_pool_id,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToCommonFunctionGatewayCreateMap builds a request body from CreateOpts. +// func (opts CreateOpts) ToCommonFunctionGatewayCreateMap() (map[string]interface{}, error) { +func (opts CreateOpts) ToCommonFunctionGatewayCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "common_function_gateway") +} + +// Create accepts a CreateOpts struct and creates a new common function gateway +// 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 +// common function gateway. +// An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToCommonFunctionGatewayCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToCommonFunctionGatewayUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a common function gateway. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` +} + +// ToCommonFunctionGatewayUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToCommonFunctionGatewayUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "common_function_gateway") +} + +// Update accepts a UpdateOpts struct and updates an existing common function gateway +// using the values provided. For more information, see the Create function. +func Update(c *eclcloud.ServiceClient, commonFunctionGatewayID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToCommonFunctionGatewayUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, commonFunctionGatewayID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the common function gateway associated with it. +func Delete(c *eclcloud.ServiceClient, commonFunctionGatewayID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, commonFunctionGatewayID), nil) + return +} + +// IDFromName is a convenience function that returns a common function gateway's +// ID, given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractCommonFunctionGateways(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "common_function_gateway"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "common_function_gateway"} + } +} diff --git a/v3/ecl/network/v2/common_function_gateways/results.go b/v3/ecl/network/v2/common_function_gateways/results.go new file mode 100644 index 0000000..997df56 --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/results.go @@ -0,0 +1,108 @@ +package common_function_gateways + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a common function gateway resource. +func (r commonResult) Extract() (*CommonFunctionGateway, error) { + var cfgw CommonFunctionGateway + err := r.ExtractInto(&cfgw) + return &cfgw, err +} + +// Extract interprets any commonResult as a Common Function Gateway, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "common_function_gateway") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Common Function Gateway. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Common Function Gateway. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Common Function Gateway. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CommonFunctionGateway represents, well, a common function gateway. +type CommonFunctionGateway struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + Description string `json:"description"` + + CommonFunctionPoolID string `json:"common_function_pool_id"` + + NetworkID string `json:"network_id"` + + SubnetID string `json:"subnet_id"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` +} + +// CommonFunctionGatewayPage is the page returned by a pager +// when traversing over a collection of common function gateway. +type CommonFunctionGatewayPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of common function gateway +// 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 (r CommonFunctionGatewayPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"common_function_gateways_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a CommonFunctionGatewayPage struct is empty. +func (r CommonFunctionGatewayPage) IsEmpty() (bool, error) { + is, err := ExtractCommonFunctionGateways(r) + return len(is) == 0, err +} + +// ExtractCommonFunctionGateways accepts a Page struct, +// specifically a NetworkPage struct, and extracts the elements +// into a slice of Common Function Gateway structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractCommonFunctionGateways(r pagination.Page) ([]CommonFunctionGateway, error) { + var s []CommonFunctionGateway + err := ExtractCommonFunctionGatewaysInto(r, &s) + return s, err +} + +// ExtractCommonFunctionGatewaysInto interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractCommonFunctionGatewaysInto(r pagination.Page, v interface{}) error { + return r.(CommonFunctionGatewayPage).Result.ExtractIntoSlicePtr(v, "common_function_gateways") +} diff --git a/v3/ecl/network/v2/common_function_gateways/testing/doc.go b/v3/ecl/network/v2/common_function_gateways/testing/doc.go new file mode 100644 index 0000000..c8a2e0c --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains common function gateways unit tests +package testing diff --git a/v3/ecl/network/v2/common_function_gateways/testing/fixtures.go b/v3/ecl/network/v2/common_function_gateways/testing/fixtures.go new file mode 100644 index 0000000..002ff66 --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/testing/fixtures.go @@ -0,0 +1,178 @@ +package testing + +import ( + "fmt" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/common_function_gateways" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idCommonFunctionGatway1 = "fb3efc23-ca8c-4eb5-b7f6-6fc66ff24f9c" +const idCommonFunctionGatway2 = "3535de20-192d-4f5a-a74a-cd1a9c1bf747" +const idCommonFunctionPool = "4f4971a5-899d-42b4-8442-24f17eac9683" + +const nameCommonFunctionGateway1 = "common_function_gateway_name_1" +const descriptionCommonFunctionGateway1 = "common_function_gateway_description_1" + +const nameCommonFunctionGateway1Update = "common_function_gateway_name_1-update" +const descriptionCommonFunctionGateway1Update = "common_function_gateway_description_1-update" + +const tenantID = "2d5b878c-147a-4d7c-87fd-90a8be9d255f" + +const networkID = "511f266e-a8bf-4547-ab2a-fc4d2bda9f81" +const subnetID = "9f3fd369-e4d4-4c3a-84f1-9c5ba7686297" + +// ListResponse is mocked response of common_function_gateways.List +var ListResponse = fmt.Sprintf(` +{ + "common_function_gateways": [ + { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + }, + { + "id": "%s", + "common_function_pool_id": "%s", + "tenant_id": "%s", + "name": "common_function_gateway_name_2", + "description": "common_function_gateway_description_2", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + } + ] +}`, + // for common function gateway1 + idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + tenantID, + networkID, + subnetID, + // for common function gateway2 + idCommonFunctionGatway2, + idCommonFunctionPool, + tenantID, + networkID, + subnetID) + +// GetResponse is mocked format of common_function_gateways.Get +var GetResponse = fmt.Sprintf(` +{ + "common_function_gateway": { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + } +}`, idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + tenantID, + networkID, + subnetID) + +// CreateRequest is mocked request for common_function_gateways.Create +var CreateRequest = fmt.Sprintf(` +{ + "common_function_gateway": { + "name": "%s", + "description": "%s", + "common_function_pool_id": "%s", + "tenant_id": "%s" + } +}`, nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + idCommonFunctionPool, + tenantID) + +// CreateResponse is mocked response of common_function_gateways.Create +var CreateResponse = fmt.Sprintf(` +{ + "common_function_gateway": { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + } +}`, idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + tenantID, + networkID, + subnetID) + +// UpdateRequest is mocked request of common_function_gateways.Update +var UpdateRequest = fmt.Sprintf(` +{ + "common_function_gateway": { + "name": "%s", + "description": "%s" + } +}`, nameCommonFunctionGateway1Update, + descriptionCommonFunctionGateway1Update) + +// UpdateResponse is mocked response of common_function_gateways.Update +var UpdateResponse = fmt.Sprintf(` +{ + "common_function_gateway": { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s" + } +}`, idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1Update, + descriptionCommonFunctionGateway1Update, + tenantID, + networkID, + subnetID) + +var commonFunctionGateway1 = common_function_gateways.CommonFunctionGateway{ + ID: idCommonFunctionGatway1, + CommonFunctionPoolID: idCommonFunctionPool, + TenantID: tenantID, + Name: nameCommonFunctionGateway1, + Description: descriptionCommonFunctionGateway1, + Status: "ACTIVE", + NetworkID: networkID, + SubnetID: subnetID, +} + +var commonFunctionGateway2 = common_function_gateways.CommonFunctionGateway{ + ID: idCommonFunctionGatway2, + CommonFunctionPoolID: idCommonFunctionPool, + TenantID: tenantID, + Name: "common_function_gateway_name_2", + Description: "common_function_gateway_description_2", + Status: "ACTIVE", + NetworkID: networkID, + SubnetID: subnetID, +} + +// ExpectedCommonFunctionGatewaysSlice is expected assertion target +var ExpectedCommonFunctionGatewaysSlice = []common_function_gateways.CommonFunctionGateway{ + commonFunctionGateway1, + commonFunctionGateway2, +} diff --git a/v3/ecl/network/v2/common_function_gateways/testing/requests_test.go b/v3/ecl/network/v2/common_function_gateways/testing/requests_test.go new file mode 100644 index 0000000..7529089 --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/testing/requests_test.go @@ -0,0 +1,147 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/common_function_gateways" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v2.0/common_function_gateways", + 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + common_function_gateways.List(client, common_function_gateways.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := common_function_gateways.ExtractCommonFunctionGateways(page) + if err != nil { + t.Errorf("Failed to extract common function gateways: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedCommonFunctionGatewaysSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v2.0/common_function_gateways/%s", idCommonFunctionGatway1) + 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, GetResponse) + }) + + cfGw, err := common_function_gateways.Get(fake.ServiceClient(), idCommonFunctionGatway1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &commonFunctionGateway1, cfGw) +} + +func TestCreateCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_gateways", + 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := common_function_gateways.CreateOpts{ + Name: nameCommonFunctionGateway1, + Description: descriptionCommonFunctionGateway1, + CommonFunctionPoolID: idCommonFunctionPool, + TenantID: tenantID, + } + cfGw, err := common_function_gateways.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, cfGw.Status, "ACTIVE") + th.AssertDeepEquals(t, &commonFunctionGateway1, cfGw) +} + +func TestUpdateCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v2.0/common_function_gateways/%s", idCommonFunctionGatway1) + 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.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameCommonFunctionGateway1Update + description := descriptionCommonFunctionGateway1Update + updateOpts := common_function_gateways.UpdateOpts{ + Name: &name, + Description: &description, + } + cfGw, err := common_function_gateways.Update( + fake.ServiceClient(), idCommonFunctionGatway1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, cfGw.Name, nameCommonFunctionGateway1Update) + th.AssertEquals(t, cfGw.Description, descriptionCommonFunctionGateway1Update) + th.AssertEquals(t, cfGw.ID, idCommonFunctionGatway1) +} + +func TestDeleteCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v2.0/common_function_gateways/%s", idCommonFunctionGatway1) + 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.StatusNoContent) + }) + + res := common_function_gateways.Delete(fake.ServiceClient(), idCommonFunctionGatway1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/common_function_gateways/urls.go b/v3/ecl/network/v2/common_function_gateways/urls.go new file mode 100644 index 0000000..22da343 --- /dev/null +++ b/v3/ecl/network/v2/common_function_gateways/urls.go @@ -0,0 +1,33 @@ +package common_function_gateways + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("common_function_gateways", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("common_function_gateways") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/common_function_pool/doc.go b/v3/ecl/network/v2/common_function_pool/doc.go new file mode 100644 index 0000000..1873e2a --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/doc.go @@ -0,0 +1,47 @@ +/* +Package common_function_pool contains functionality for working with +ECL Common Function Pool resources. + +Example to List Common Function Pools + + listOpts := common_function_pool.ListOpts{ + Description: "general", + } + + allPages, err := common_function_pool.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allCommonFunctionPools, err := common_function_pool.ExtractCommonFunctionPools(allPages) + if err != nil { + panic(err) + } + + for _, commonFunctionPool := range allCommonFunctionPools { + fmt.Printf("%+v\n", commonFunctionPool) + } + +Example to Show Common Function Pool + + commonFunctionPoolID := "c57066cc-9553-43a6-90de-asfdfesfffff" + + commonFunctionPool, err := common_function_pool.Get(networkClient, commonFunctionPoolID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", commonFunctionPool) + +Example to look for Common Function Pool's ID by its name + + commonFunctionPoolName := "CF_Pool1" + + commonFunctionPoolID, err := common_function_pool.IDFromName(networkClient, commonFunctionPoolName) + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", commonFunctionPoolID) +*/ +package common_function_pool diff --git a/v3/ecl/network/v2/common_function_pool/requests.go b/v3/ecl/network/v2/common_function_pool/requests.go new file mode 100644 index 0000000..6513f13 --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/requests.go @@ -0,0 +1,93 @@ +package common_function_pool + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToCommonFunctionPoolListQuery() (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 Common Function Pool attributes you want to see returned. SortKey allows you to sort +// by a particular Common Function Pool attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` +} + +// ToCommonFunctionPoolsListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCommonFunctionPoolListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Common Function 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 Common Function Pools that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToCommonFunctionPoolListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CommonFunctionPoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Common Function Pool based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// IDFromName is a convenience function that returns a Common Function Pool's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractCommonFunctionPools(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "common_function_pool"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "common_function_pool"} + } +} diff --git a/v3/ecl/network/v2/common_function_pool/results.go b/v3/ecl/network/v2/common_function_pool/results.go new file mode 100644 index 0000000..333e3a0 --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/results.go @@ -0,0 +1,62 @@ +package common_function_pool + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// CommonFunctionPool represents a Common Function Pool. See package documentation for a top-level +// description of what this is. +type CommonFunctionPool struct { + + // Description is description + Description string `json:"description"` + + // UUID representing the Common Function Pool. + ID string `json:"id"` + + // Name of Common Function Pool + Name string `json:"name"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Common Function Pool. +type GetResult struct { + commonResult +} + +// CommonFunctionPoolPage is the page returned by a pager when traversing over a collection +// of common function pools. +type CommonFunctionPoolPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a CommonFunctionPoolPage struct is empty. +func (r CommonFunctionPoolPage) IsEmpty() (bool, error) { + is, err := ExtractCommonFunctionPools(r) + return len(is) == 0, err +} + +// ExtractCommonFunctionPools accepts a Page struct, specifically a CommonFunctionPoolPage struct, +// and extracts the elements into a slice of Common Function Pool structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractCommonFunctionPools(r pagination.Page) ([]CommonFunctionPool, error) { + var s struct { + CommonFunctionPools []CommonFunctionPool `json:"common_function_pools"` + } + err := (r.(CommonFunctionPoolPage)).ExtractInto(&s) + return s.CommonFunctionPools, err +} + +// Extract is a function that accepts a result and extracts a Common Function Pool resource. +func (r commonResult) Extract() (*CommonFunctionPool, error) { + var s struct { + CommonFunctionPool *CommonFunctionPool `json:"common_function_pool"` + } + err := r.ExtractInto(&s) + return s.CommonFunctionPool, err +} diff --git a/v3/ecl/network/v2/common_function_pool/testing/doc.go b/v3/ecl/network/v2/common_function_pool/testing/doc.go new file mode 100644 index 0000000..16d9a64 --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/testing/doc.go @@ -0,0 +1,2 @@ +// Common Function Pool unit tests +package testing diff --git a/v3/ecl/network/v2/common_function_pool/testing/fixtures.go b/v3/ecl/network/v2/common_function_pool/testing/fixtures.go new file mode 100644 index 0000000..4ea96b6 --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/testing/fixtures.go @@ -0,0 +1,69 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/common_function_pool" +) + +const ListResponse = ` +{ + "common_function_pools": [ + { + "description": "Common Function Pool 1", + "id": "c57066cc-9553-43a6-90de-asfdfesfffff", + "name": "CF_Pool1" + }, + { + "description": "Common Function Pool 2", + "id": "fesg66cc-9553-43a6-90de-c8472fdsafedf", + "name": "CF_Pool2" + } + ] +} +` + +const GetResponse = ` +{ + "common_function_pool": { + "description": "Common Function Pool Description", + "id": "c57066cc-9553-43a6-90de-c847231bc70b", + "name": "CF_Pool1" + } +} +` + +var CommonFunctionPool1 = common_function_pool.CommonFunctionPool{ + Description: "Common Function Pool 1", + ID: "c57066cc-9553-43a6-90de-asfdfesfffff", + Name: "CF_Pool1", +} + +var CommonFunctionPool2 = common_function_pool.CommonFunctionPool{ + Description: "Common Function Pool 2", + ID: "fesg66cc-9553-43a6-90de-c8472fdsafedf", + Name: "CF_Pool2", +} + +var CommonFunctionDetail = common_function_pool.CommonFunctionPool{ + Description: "Common Function Pool Description", + ID: "c57066cc-9553-43a6-90de-c847231bc70b", + Name: "CF_Pool1", +} + +var ExpectedCommonFunctionPoolSlice = []common_function_pool.CommonFunctionPool{CommonFunctionPool1, CommonFunctionPool2} + +const ListResponseDuplicatedNames = ` +{ + "common_function_pools": [ + { + "description": "Common Function Pool Description 1", + "id": "c57066cc-9553-43a6-90de-asfdfesfffff", + "name": "CF_Pool1" + }, + { + "description": "Common Function Pool Description 2", + "id": "fesg66cc-9553-43a6-90de-c8472fdsafedf", + "name": "CF_Pool1" + } + ] +} +` diff --git a/v3/ecl/network/v2/common_function_pool/testing/requests_test.go b/v3/ecl/network/v2/common_function_pool/testing/requests_test.go new file mode 100644 index 0000000..ee0c59b --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/testing/requests_test.go @@ -0,0 +1,135 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/common_function_pool" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListCommonFunctionPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + common_function_pool.List(client, common_function_pool.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := common_function_pool.ExtractCommonFunctionPools(page) + if err != nil { + t.Errorf("Failed to extract Common Function Pools: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedCommonFunctionPoolSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetCommonFunctionPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_pools/c57066cc-9553-43a6-90de-c847231bc70b", 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, GetResponse) + }) + + s, err := common_function_pool.Get(fake.ServiceClient(), "c57066cc-9553-43a6-90de-c847231bc70b").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CommonFunctionDetail, s) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "c57066cc-9553-43a6-90de-asfdfesfffff" + actualID, err := common_function_pool.IDFromName(client, "CF_Pool1") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := common_function_pool.IDFromName(client, "CF_PoolX") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := common_function_pool.IDFromName(client, "CF_Pool1") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/network/v2/common_function_pool/urls.go b/v3/ecl/network/v2/common_function_pool/urls.go new file mode 100644 index 0000000..1cffd1d --- /dev/null +++ b/v3/ecl/network/v2/common_function_pool/urls.go @@ -0,0 +1,21 @@ +package common_function_pool + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("common_function_pools", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("common_function_pools") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} diff --git a/v3/ecl/network/v2/fic_gateways/doc.go b/v3/ecl/network/v2/fic_gateways/doc.go new file mode 100644 index 0000000..15f0fb6 --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/doc.go @@ -0,0 +1,35 @@ +/* +Package fic_gateways provides information of several service +in the Enterprise Cloud Compute service + +Example to List FIC Gateways + + listOpts := fic_gateways.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := fic_gateways.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFICGateways, err := fic_gateways.ExtractFICGateways(allPages) + if err != nil { + panic(err) + } + + for _, ficGateway := range allFICGateways { + fmt.Printf("%+v", ficGateway) + } + +Example to Show FIC Gateway + + id := "02dc9a22-129c-4b12-9936-4080f6a7ae44" + ficGateway, err := fic_gateways.Get(client, id).Extract() + if err != nil { + panic(err) + } + fmt.Print(ficGateway) + +*/ +package fic_gateways diff --git a/v3/ecl/network/v2/fic_gateways/requests.go b/v3/ecl/network/v2/fic_gateways/requests.go new file mode 100644 index 0000000..3ac78d2 --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/requests.go @@ -0,0 +1,66 @@ +package fic_gateways + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFICGatewaysListQuery() (string, error) +} + +// ListOpts allows the filtering of paginated collections through the API. +// Filtering is achieved by passing in struct field values that map to +// the FIC Gateway attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // Description of the FIC Gateway resource. + Description string `q:"description"` + + // FIC Service instantiated by this Gateway. + FICServiceID string `q:"fic_service_id"` + + //Unique ID of the FIC Gateway resource. + ID string `q:"id"` + + //Name of the FIC Gateway resource. + Name string `q:"name"` + + // Quality of Service options selected for this Gateway. + QoSOptionID string `q:"qos_option_id"` + + // The FIC Gateway status. + Status string `q:"status"` + + // Tenant ID of the owner (UUID). + TenantID string `q:"tenant_id"` +} + +// ToFICGatewaysListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFICGatewaysListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list FIC Gateways accessible to you. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFICGatewaysListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FICGatewayPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific FIC Gateway based on its unique ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/v3/ecl/network/v2/fic_gateways/results.go b/v3/ecl/network/v2/fic_gateways/results.go new file mode 100644 index 0000000..6f2f2e8 --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/results.go @@ -0,0 +1,53 @@ +package fic_gateways + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type FICGatewayPage struct { + pagination.LinkedPageBase +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a FICGateway. +type GetResult struct { + commonResult +} + +// FICGateway represents a FIC Gateway. +type FICGateway struct { + Description string `json:"description"` + FICServiceID string `json:"fic_service_id"` + ID string `json:"id"` + Name string `json:"name"` + QoSOptionID string `json:"qos_option_id"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` +} + +// IsEmpty checks whether a FICGatewayPage struct is empty. +func (r FICGatewayPage) IsEmpty() (bool, error) { + is, err := ExtractFICGateways(r) + return len(is) == 0, err +} + +// ExtractFICGateways accepts a Page struct, specifically a FICGatewayPage struct, +// and extracts the elements into a slice of ListOpts structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFICGateways(r pagination.Page) ([]FICGateway, error) { + var s []FICGateway + err := r.(FICGatewayPage).Result.ExtractIntoSlicePtr(&s, "fic_gateways") + return s, err +} + +// Extract is a function that accepts a result and extracts a FICGateway. +func (r GetResult) Extract() (*FICGateway, error) { + var l FICGateway + err := r.Result.ExtractIntoStructPtr(&l, "fic_gateway") + return &l, err +} diff --git a/v3/ecl/network/v2/fic_gateways/testing/doc.go b/v3/ecl/network/v2/fic_gateways/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v3/ecl/network/v2/fic_gateways/testing/fixtures.go b/v3/ecl/network/v2/fic_gateways/testing/fixtures.go new file mode 100644 index 0000000..8b4ba0f --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/testing/fixtures.go @@ -0,0 +1,64 @@ +package testing + +import "github.com/nttcom/eclcloud/v3/ecl/network/v2/fic_gateways" + +const ListResponse = ` +{ + "fic_gateways": [ + { + "description": "fic_gateway_inet_test, 10M-BE, member role", + "fic_service_id": "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "id": "07f97269-e616-4dff-a73f-ca80bc5682dc", + "name": "lab3-test-member-user-fic-gateway", + "qos_option_id": "e41f6a2f-e197-41c8-9f71-ef19cfd2a85a", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + }, + { + "description": "", + "fic_service_id": "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "id": "4c842674-60e4-48eb-b5a3-b902f832d0af", + "name": "N000001996_V15000001", + "qos_option_id": "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } + ] +} +` + +const GetResponse = ` +{ + "fic_gateway": { + "description": "fic_gateway_inet_test, 10M-BE, member role", + "fic_service_id": "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "id": "07f97269-e616-4dff-a73f-ca80bc5682dc", + "name": "lab3-test-member-user-fic-gateway", + "qos_option_id": "e41f6a2f-e197-41c8-9f71-ef19cfd2a85a", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` + +var ficgw1 = fic_gateways.FICGateway{ + Description: "fic_gateway_inet_test, 10M-BE, member role", + FICServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + ID: "07f97269-e616-4dff-a73f-ca80bc5682dc", + Name: "lab3-test-member-user-fic-gateway", + QoSOptionID: "e41f6a2f-e197-41c8-9f71-ef19cfd2a85a", + Status: "ACTIVE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var ficgw2 = fic_gateways.FICGateway{ + Description: "", + FICServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + ID: "4c842674-60e4-48eb-b5a3-b902f832d0af", + Name: "N000001996_V15000001", + QoSOptionID: "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + Status: "ACTIVE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var ExpectedFICGatewaySlice = []fic_gateways.FICGateway{ficgw1, ficgw2} diff --git a/v3/ecl/network/v2/fic_gateways/testing/request_test.go b/v3/ecl/network/v2/fic_gateways/testing/request_test.go new file mode 100644 index 0000000..25b27f7 --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/testing/request_test.go @@ -0,0 +1,69 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3/ecl/network/v2/fic_gateways" + "github.com/nttcom/eclcloud/v3/pagination" + + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListFICGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fic_gateways", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + fic_gateways.List(client, fic_gateways.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := fic_gateways.ExtractFICGateways(page) + if err != nil { + t.Errorf("Failed to extract FIC Gateways: %v", err) + return false, nil + } + th.CheckDeepEquals(t, ExpectedFICGatewaySlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetFICGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + id := "07f97269-e616-4dff-a73f-ca80bc5682dc" + th.Mux.HandleFunc(fmt.Sprintf("/v2.0/fic_gateways/%s", 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, GetResponse) + }) + + n, err := fic_gateways.Get(fake.ServiceClient(), id).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ficgw1, n) +} diff --git a/v3/ecl/network/v2/fic_gateways/urls.go b/v3/ecl/network/v2/fic_gateways/urls.go new file mode 100644 index 0000000..dbf6a3b --- /dev/null +++ b/v3/ecl/network/v2/fic_gateways/urls.go @@ -0,0 +1,11 @@ +package fic_gateways + +import "github.com/nttcom/eclcloud/v3" + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("fic_gateways", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("fic_gateways") +} diff --git a/v3/ecl/network/v2/gateway_interfaces/doc.go b/v3/ecl/network/v2/gateway_interfaces/doc.go new file mode 100644 index 0000000..2029b1c --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/doc.go @@ -0,0 +1 @@ +package gateway_interfaces diff --git a/v3/ecl/network/v2/gateway_interfaces/requests.go b/v3/ecl/network/v2/gateway_interfaces/requests.go new file mode 100644 index 0000000..dce7a6b --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/requests.go @@ -0,0 +1,162 @@ +package gateway_interfaces + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type ListOptsBuilder interface { + ToGatewayInterfaceListQuery() (string, error) +} + +type ListOpts struct { + AwsGwID string `q:"aws_gw_id"` + AzureGwID string `q:"azure_gw_id"` + Description string `q:"description"` + FICGatewayID string `q:"fic_gw_id"` + GcpGwID string `q:"gcp_gw_id"` + GwVipv4 string `q:"gw_vipv4"` + GwVipv6 string `q:"gw_vipv6"` + ID string `q:"id"` + InterdcGwID string `q:"interdc_gw_id"` + InternetGwID string `q:"internet_gw_id"` + Name string `q:"name"` + Netmask int `q:"netmask"` + NetworkID string `q:"network_id"` + PrimaryIpv4 string `q:"primary_ipv4"` + PrimaryIpv6 string `q:"primary_ipv6"` + SecondaryIpv4 string `q:"secondary_ipv4"` + SecondaryIpv6 string `q:"secondary_ipv6"` + ServiceType string `q:"service_type"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + VpnGwID string `q:"vpn_gw_id"` + VRID int `q:"vrid"` +} + +func (opts ListOpts) ToGatewayInterfaceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToGatewayInterfaceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return GatewayInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, gatewayInterfaceID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, gatewayInterfaceID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToGatewayInterfaceCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + AwsGwID string `json:"aws_gw_id,omitempty"` + AzureGwID string `json:"azure_gw_id,omitempty"` + Description string `json:"description"` + FICGatewayID string `json:"fic_gw_id,omitempty"` + GcpGwID string `json:"gcp_gw_id,omitempty"` + GwVipv4 string `json:"gw_vipv4" required:"true"` + InterdcGwID string `json:"interdc_gw_id,omitempty"` + InternetGwID string `json:"internet_gw_id,omitempty"` + Name string `json:"name"` + Netmask int `json:"netmask" required:"true"` + NetworkID string `json:"network_id" required:"true"` + PrimaryIpv4 string `json:"primary_ipv4" required:"true"` + SecondaryIpv4 string `json:"secondary_ipv4" required:"true"` + ServiceType string `json:"service_type" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + VpnGwID string `json:"vpn_gw_id,omitempty"` + VRID int `json:"vrid" required:"true"` +} + +func (opts CreateOpts) ToGatewayInterfaceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "gw_interface") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToGatewayInterfaceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToGatewayInterfaceUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` +} + +func (opts UpdateOpts) ToGatewayInterfaceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "gw_interface") +} + +func Update(c *eclcloud.ServiceClient, gatewayInterfaceID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToGatewayInterfaceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, gatewayInterfaceID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, gatewayInterfaceID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, gatewayInterfaceID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractGatewayInterfaces(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "gw_interface"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "gw_interface"} + } +} diff --git a/v3/ecl/network/v2/gateway_interfaces/results.go b/v3/ecl/network/v2/gateway_interfaces/results.go new file mode 100644 index 0000000..6b6ca65 --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/results.go @@ -0,0 +1,91 @@ +package gateway_interfaces + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*GatewayInterface, error) { + var s GatewayInterface + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "gw_interface") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type GatewayInterface struct { + AwsGwID string `json:"aws_gw_id"` + AzureGwID string `json:"azure_gw_id"` + Description string `json:"description"` + FICGatewayID string `json:"fic_gw_id"` + GcpGwID string `json:"gcp_gw_id"` + GwVipv4 string `json:"gw_vipv4"` + GwVipv6 string `json:"gw_vipv6"` + ID string `json:"id"` + InterdcGwID string `json:"interdc_gw_id"` + InternetGwID string `json:"internet_gw_id"` + Name string `json:"name"` + Netmask int `json:"netmask"` + NetworkID string `json:"network_id"` + PrimaryIpv4 string `json:"primary_ipv4"` + PrimaryIpv6 string `json:"primary_ipv6"` + SecondaryIpv4 string `json:"secondary_ipv4"` + SecondaryIpv6 string `json:"secondary_ipv6"` + ServiceType string `json:"service_type"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + VpnGwID string `json:"vpn_gw_id"` + VRID int `json:"vrid"` +} + +type GatewayInterfacePage struct { + pagination.LinkedPageBase +} + +func (r GatewayInterfacePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"gw_interfaces_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r GatewayInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractGatewayInterfaces(r) + return len(is) == 0, err +} + +func ExtractGatewayInterfaces(r pagination.Page) ([]GatewayInterface, error) { + var s []GatewayInterface + err := ExtractGatewayInterfacesInto(r, &s) + return s, err +} + +func ExtractGatewayInterfacesInto(r pagination.Page, v interface{}) error { + return r.(GatewayInterfacePage).Result.ExtractIntoSlicePtr(v, "gw_interfaces") +} diff --git a/v3/ecl/network/v2/gateway_interfaces/testing/docs.go b/v3/ecl/network/v2/gateway_interfaces/testing/docs.go new file mode 100644 index 0000000..37028c4 --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/testing/docs.go @@ -0,0 +1,2 @@ +// gateway_interfaces unit tests +package testing diff --git a/v3/ecl/network/v2/gateway_interfaces/testing/fixtures.go b/v3/ecl/network/v2/gateway_interfaces/testing/fixtures.go new file mode 100644 index 0000000..d4cc7f0 --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/testing/fixtures.go @@ -0,0 +1,202 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/gateway_interfaces" +) + +const ListResponse = ` +{ + "gw_interfaces": [ + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_CREATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + }, + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "lab3-test-user-fic-gateway-interface, role : member", + "fic_gw_id": "dd04adc4-459f-4fc4-83a5-47436c6aece5", + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.1", + "gw_vipv6": null, + "id": "165ed64c-b9d4-46b1-afc1-cbbdc356ddcb", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "lab3-hara-cfg-20151204", + "netmask": 29, + "network_id": "cce5c9a1-1ec3-40b1-bfc7-634bb914646b", + "primary_ipv4": "100.127.254.3", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.4", + "secondary_ipv6": null, + "service_type": "fic", + "status": "ACTIVE", + "tenant_id": "fe1f6fb95b0e48ba8c59be2121a58adc", + "vpn_gw_id": null, + "vrid": 10 + } + ] +}` + +const GetResponse = ` +{ + "gw_interface": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_CREATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + } +}` + +const CreateRequest = ` +{ + "gw_interface": { + "description": "", + "gw_vipv4": "100.127.254.49", + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "secondary_ipv4": "100.127.254.54", + "service_type": "internet", + "vrid": 1 + } +} +` + +const CreateResponse = ` +{ + "gw_interface": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_CREATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + } +}` + +const UpdateRequest = ` +{ + "gw_interface": { + "description": "Updated", + "name": "6_Gateway" + } +}` + +const UpdateResponse = ` +{ + "gw_interface": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "Updated", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "6_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_UPDATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + } +}` + +var GatewayInterface1 = gateway_interfaces.GatewayInterface{ + Description: "", + GwVipv4: "100.127.254.49", + ID: "09771fbb-6496-4ae1-9b53-226b6edcc1be", + InternetGwID: "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + Name: "5_Gateway", + Netmask: 29, + NetworkID: "0200a550-82cf-4d6d-b564-a87eb63e2b75", + PrimaryIpv4: "100.127.254.53", + SecondaryIpv4: "100.127.254.54", + ServiceType: "internet", + Status: "PENDING_CREATE", + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", + VRID: 1, +} + +var GatewayInterface2 = gateway_interfaces.GatewayInterface{ + Description: "lab3-test-user-fic-gateway-interface, role : member", + FICGatewayID: "dd04adc4-459f-4fc4-83a5-47436c6aece5", + GwVipv4: "100.127.254.1", + ID: "165ed64c-b9d4-46b1-afc1-cbbdc356ddcb", + Name: "lab3-hara-cfg-20151204", + Netmask: 29, + NetworkID: "cce5c9a1-1ec3-40b1-bfc7-634bb914646b", + PrimaryIpv4: "100.127.254.3", + SecondaryIpv4: "100.127.254.4", + ServiceType: "fic", + Status: "ACTIVE", + TenantID: "fe1f6fb95b0e48ba8c59be2121a58adc", + VRID: 10, +} + +var ExpectedGatewayInterfaceSlice = []gateway_interfaces.GatewayInterface{GatewayInterface1, GatewayInterface2} diff --git a/v3/ecl/network/v2/gateway_interfaces/testing/request_test.go b/v3/ecl/network/v2/gateway_interfaces/testing/request_test.go new file mode 100644 index 0000000..ab2f6f0 --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/testing/request_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/gateway_interfaces" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListGatewayInterfaces(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := gateway_interfaces.List(client, gateway_interfaces.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := gateway_interfaces.ExtractGatewayInterfaces(page) + if err != nil { + t.Errorf("Failed to extract gateway interfaces: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedGatewayInterfaceSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces/09771fbb-6496-4ae1-9b53-226b6edcc1be", 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, GetResponse) + }) + + i, err := gateway_interfaces.Get(fake.ServiceClient(), "09771fbb-6496-4ae1-9b53-226b6edcc1be").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GatewayInterface1, i) +} + +func TestCreateGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := gateway_interfaces.CreateOpts{ + Description: "", + GwVipv4: "100.127.254.49", + InternetGwID: "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + Name: "5_Gateway", + Netmask: 29, + NetworkID: "0200a550-82cf-4d6d-b564-a87eb63e2b75", + PrimaryIpv4: "100.127.254.53", + SecondaryIpv4: "100.127.254.54", + ServiceType: "internet", + VRID: 1, + } + i, err := gateway_interfaces.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &GatewayInterface1, i) +} + +func TestUpdateGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces/09771fbb-6496-4ae1-9b53-226b6edcc1be", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "Updated" + name := "6_Gateway" + options := gateway_interfaces.UpdateOpts{ + Description: &description, + Name: &name, + } + i, err := gateway_interfaces.Update(fake.ServiceClient(), "09771fbb-6496-4ae1-9b53-226b6edcc1be", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "6_Gateway") + th.AssertEquals(t, i.Description, "Updated") + th.AssertEquals(t, i.ID, "09771fbb-6496-4ae1-9b53-226b6edcc1be") +} + +func TestDeleteGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces/09771fbb-6496-4ae1-9b53-226b6edcc1be", 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 := gateway_interfaces.Delete(fake.ServiceClient(), "09771fbb-6496-4ae1-9b53-226b6edcc1be") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/gateway_interfaces/urls.go b/v3/ecl/network/v2/gateway_interfaces/urls.go new file mode 100644 index 0000000..c444099 --- /dev/null +++ b/v3/ecl/network/v2/gateway_interfaces/urls.go @@ -0,0 +1,31 @@ +package gateway_interfaces + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("gw_interfaces", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("gw_interfaces") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/internet_gateways/doc.go b/v3/ecl/network/v2/internet_gateways/doc.go new file mode 100644 index 0000000..9be4fab --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/doc.go @@ -0,0 +1 @@ +package internet_gateways diff --git a/v3/ecl/network/v2/internet_gateways/requests.go b/v3/ecl/network/v2/internet_gateways/requests.go new file mode 100644 index 0000000..dbf1997 --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/requests.go @@ -0,0 +1,136 @@ +package internet_gateways + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type ListOptsBuilder interface { + ToInternetGatewayListQuery() (string, error) +} + +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + InternetServiceID string `q:"internet_service_id"` + Name string `q:"name"` + QoSOptionID string `q:"qos_option_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +func (opts ListOpts) ToInternetGatewayListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToInternetGatewayListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return InternetGatewayPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, internetGatewayID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, internetGatewayID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToInternetGatewayCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + Description string `json:"description,omitempty"` + InternetServiceID string `json:"internet_service_id" required:"true"` + Name string `json:"name,omitempty"` + QoSOptionID string `json:"qos_option_id" required:"true"` + TenantID string `json:"tenant_id,omitempty"` +} + +func (opts CreateOpts) ToInternetGatewayCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "internet_gateway") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToInternetGatewayCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToInternetGatewayUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + QoSOptionID *string `json:"qos_option_id,omitempty"` +} + +func (opts UpdateOpts) ToInternetGatewayUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "internet_gateway") +} + +func Update(c *eclcloud.ServiceClient, internetGatewayID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToInternetGatewayUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, internetGatewayID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, internetGatewayID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, internetGatewayID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractInternetGateways(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "internet_gateway"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "internet_gateway"} + } +} diff --git a/v3/ecl/network/v2/internet_gateways/results.go b/v3/ecl/network/v2/internet_gateways/results.go new file mode 100644 index 0000000..01edec2 --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/results.go @@ -0,0 +1,76 @@ +package internet_gateways + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*InternetGateway, error) { + var s InternetGateway + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "internet_gateway") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type InternetGateway struct { + ID string `json:"id"` + Description string `json:"description"` + InternetServiceID string `json:"internet_service_id"` + Name string `json:"name"` + QoSOptionID string `json:"qos_option_id"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` +} + +type InternetGatewayPage struct { + pagination.LinkedPageBase +} + +func (r InternetGatewayPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"internet_gateways_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r InternetGatewayPage) IsEmpty() (bool, error) { + is, err := ExtractInternetGateways(r) + return len(is) == 0, err +} + +func ExtractInternetGateways(r pagination.Page) ([]InternetGateway, error) { + var s []InternetGateway + err := ExtractInternetGatewaysInto(r, &s) + return s, err +} + +func ExtractInternetGatewaysInto(r pagination.Page, v interface{}) error { + return r.(InternetGatewayPage).Result.ExtractIntoSlicePtr(v, "internet_gateways") +} diff --git a/v3/ecl/network/v2/internet_gateways/testing/doc.go b/v3/ecl/network/v2/internet_gateways/testing/doc.go new file mode 100644 index 0000000..79517b8 --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/testing/doc.go @@ -0,0 +1,2 @@ +// internet_gateways unit tests +package testing diff --git a/v3/ecl/network/v2/internet_gateways/testing/fixtures.go b/v3/ecl/network/v2/internet_gateways/testing/fixtures.go new file mode 100644 index 0000000..16e1692 --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/testing/fixtures.go @@ -0,0 +1,110 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/internet_gateways" +) + +const ListResponse = ` +{ + "internet_gateways": [ + { + "description": "test", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_CREATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + }, + { + "description": "", + "id": "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "6_performance", + "qos_option_id": "be985a60-e918-4cca-98f1-8886333f6f5e", + "status": "ACTIVE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } + ] +}` + +const GetResponse = `{ + "internet_gateway": { + "description": "test", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_CREATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } +}` + +const CreateRequest = ` +{ + "internet_gateway": { + "description": "test", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } +} +` + +const CreateResponse = ` +{ + "internet_gateway": { + "description": "test", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_CREATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } + }` + +const UpdateRequest = ` + { + "internet_gateway": { + "description": "test2", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40" + } +}` + +const UpdateResponse = ` +{ + "internet_gateway": { + "description": "test2", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_UPDATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } +}` + +var InternetGateway1 = internet_gateways.InternetGateway{ + Description: "test", + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + InternetServiceID: "5536154d-9a00-4b11-81fb-b185c9111d90", + Name: "Lab3-Internet-Service-Provider-01", + QoSOptionID: "e497bbc3-1127-4490-a51d-93582c40ab40", + Status: "PENDING_CREATE", + TenantID: "6c0bdafab1914ab2b2b6c415477defc7", +} + +var InternetGateway2 = internet_gateways.InternetGateway{ + Description: "", + ID: "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + InternetServiceID: "5536154d-9a00-4b11-81fb-b185c9111d90", + Name: "6_performance", + QoSOptionID: "be985a60-e918-4cca-98f1-8886333f6f5e", + Status: "ACTIVE", + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", +} + +var ExpectedInternetGatewaySlice = []internet_gateways.InternetGateway{InternetGateway1, InternetGateway2} diff --git a/v3/ecl/network/v2/internet_gateways/testing/request_test.go b/v3/ecl/network/v2/internet_gateways/testing/request_test.go new file mode 100644 index 0000000..dbe0a50 --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/testing/request_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/internet_gateways" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways", 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.Fprint(w, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := internet_gateways.List(client, internet_gateways.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := internet_gateways.ExtractInternetGateways(page) + if err != nil { + t.Errorf("Failed to extract internet gateways: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedInternetGatewaySlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + 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/internet_gateways/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.Fprint(w, GetResponse) + }) + + i, err := internet_gateways.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &InternetGateway1, i) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprint(w, CreateResponse) + }) + + options := internet_gateways.CreateOpts{ + Name: "Lab3-Internet-Service-Provider-01", + TenantID: "6c0bdafab1914ab2b2b6c415477defc7", + Description: "test", + InternetServiceID: "5536154d-9a00-4b11-81fb-b185c9111d90", + QoSOptionID: "e497bbc3-1127-4490-a51d-93582c40ab40", + } + i, err := internet_gateways.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Status, "PENDING_CREATE") + th.AssertDeepEquals(t, &InternetGateway1, i) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways/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) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, UpdateResponse) + }) + + name := "Lab3-Internet-Service-Provider-01" + description := "test2" + qosOptionId := "e497bbc3-1127-4490-a51d-93582c40ab40" + options := internet_gateways.UpdateOpts{ + Name: &name, + Description: &description, + QoSOptionID: &qosOptionId, + } + i, err := internet_gateways.Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "Lab3-Internet-Service-Provider-01") + th.AssertEquals(t, i.Description, "test2") + th.AssertEquals(t, i.QoSOptionID, "e497bbc3-1127-4490-a51d-93582c40ab40") + th.AssertEquals(t, i.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways/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) + }) + + res := internet_gateways.Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/internet_gateways/urls.go b/v3/ecl/network/v2/internet_gateways/urls.go new file mode 100644 index 0000000..1da0d07 --- /dev/null +++ b/v3/ecl/network/v2/internet_gateways/urls.go @@ -0,0 +1,31 @@ +package internet_gateways + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("internet_gateways", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("internet_gateways") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/internet_services/doc.go b/v3/ecl/network/v2/internet_services/doc.go new file mode 100644 index 0000000..1d5dd38 --- /dev/null +++ b/v3/ecl/network/v2/internet_services/doc.go @@ -0,0 +1 @@ +package internet_services diff --git a/v3/ecl/network/v2/internet_services/requests.go b/v3/ecl/network/v2/internet_services/requests.go new file mode 100644 index 0000000..7aaa84c --- /dev/null +++ b/v3/ecl/network/v2/internet_services/requests.go @@ -0,0 +1,42 @@ +package internet_services + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type ListOptsBuilder interface { + ToInternetServiceListQuery() (string, error) +} + +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + MinimalSubmaskLength int `q:"minimal_submask_length"` + Name string `q:"name"` + Zone string `q:"zone"` +} + +func (opts ListOpts) ToInternetServiceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToInternetServiceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return InternetServicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, internetServiceID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, internetServiceID), &r.Body, nil) + return +} diff --git a/v3/ecl/network/v2/internet_services/results.go b/v3/ecl/network/v2/internet_services/results.go new file mode 100644 index 0000000..681e475 --- /dev/null +++ b/v3/ecl/network/v2/internet_services/results.go @@ -0,0 +1,62 @@ +package internet_services + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*InternetService, error) { + var s InternetService + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "internet_service") +} + +type GetResult struct { + commonResult +} + +type InternetService struct { + Description string `json:"description"` + ID string `json:"id"` + MinimalSubmaskLength int `json:"minimal_submask_length"` + Name string `json:"name"` + Zone string `json:"zone"` +} + +type InternetServicePage struct { + pagination.LinkedPageBase +} + +func (r InternetServicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"internet_services_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r InternetServicePage) IsEmpty() (bool, error) { + is, err := ExtractInternetServices(r) + return len(is) == 0, err +} + +func ExtractInternetServices(r pagination.Page) ([]InternetService, error) { + var s []InternetService + err := ExtractInternetServicesInto(r, &s) + return s, err +} + +func ExtractInternetServicesInto(r pagination.Page, v interface{}) error { + return r.(InternetServicePage).Result.ExtractIntoSlicePtr(v, "internet_services") +} diff --git a/v3/ecl/network/v2/internet_services/testing/doc.go b/v3/ecl/network/v2/internet_services/testing/doc.go new file mode 100644 index 0000000..7603f83 --- /dev/null +++ b/v3/ecl/network/v2/internet_services/testing/doc.go @@ -0,0 +1 @@ +package testing diff --git a/v3/ecl/network/v2/internet_services/testing/fixtures.go b/v3/ecl/network/v2/internet_services/testing/fixtures.go new file mode 100644 index 0000000..000e788 --- /dev/null +++ b/v3/ecl/network/v2/internet_services/testing/fixtures.go @@ -0,0 +1,53 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/internet_services" +) + +const ListResponse = ` +{ + "internet_services": [ + { + "description": "Example internet_service 1 description", + "id": "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", + "minimal_submask_length": 26, + "name": "Internet-Service-01", + "zone": "jp1-zone1" + }, + { + "description": "Example internet_service 2 description.", + "id": "5d6eaf32-8c42-4187-973b-dcee142dcb9d", + "minimal_submask_length": 26, + "name": "Internet-Service-01", + "zone": "jp2-zone1" + } + ] +}` + +const GetResponse = `{ + "internet_service": { + "description": "Example internet_service 1 description", + "id": "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", + "minimal_submask_length": 26, + "name": "Internet-Service-01", + "zone": "jp1-zone1" + } +}` + +var InternetService1 = internet_services.InternetService{ + Description: "Example internet_service 1 description", + ID: "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", + MinimalSubmaskLength: 26, + Name: "Internet-Service-01", + Zone: "jp1-zone1", +} + +var InternetService2 = internet_services.InternetService{ + Description: "Example internet_service 2 description.", + ID: "5d6eaf32-8c42-4187-973b-dcee142dcb9d", + MinimalSubmaskLength: 26, + Name: "Internet-Service-01", + Zone: "jp2-zone1", +} + +var ExpectedInternetServiceSlice = []internet_services.InternetService{InternetService1, InternetService2} diff --git a/v3/ecl/network/v2/internet_services/testing/request_test.go b/v3/ecl/network/v2/internet_services/testing/request_test.go new file mode 100644 index 0000000..65fa18e --- /dev/null +++ b/v3/ecl/network/v2/internet_services/testing/request_test.go @@ -0,0 +1,71 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/internet_services" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_services", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := internet_services.List(client, internet_services.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := internet_services.ExtractInternetServices(page) + if err != nil { + t.Errorf("Failed to extract internet services: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedInternetServiceSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + 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/internet_services/a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", 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, GetResponse) + }) + + i, err := internet_services.Get(fake.ServiceClient(), "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &InternetService1, i) +} diff --git a/v3/ecl/network/v2/internet_services/urls.go b/v3/ecl/network/v2/internet_services/urls.go new file mode 100644 index 0000000..6cddfaf --- /dev/null +++ b/v3/ecl/network/v2/internet_services/urls.go @@ -0,0 +1,19 @@ +package internet_services + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("internet_services", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("internet_services") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} diff --git a/v3/ecl/network/v2/load_balancer_actions/doc.go b/v3/ecl/network/v2/load_balancer_actions/doc.go new file mode 100644 index 0000000..26b188d --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/doc.go @@ -0,0 +1,34 @@ +/* +Package load_balancer_actions contains functionality for working with +ECL Load Balancer/Actions resources. + +Example to reboot a Load Balancer + + loadBalancerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + + rebootOpts := load_balancer_actions.RebootOpts{ + Type: "HARD", + } + + err := load_balancer_actions.Reboot(networkClient, loadBalancerID, rebootOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to reset password of Load Balancer + + loadBalancerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + + resetPasswordOpts := load_balancer_actions.ResetPasswordOpts{ + Username: "user-read", + } + + resetPasswordResult, err := load_balancer_actions.ResetPassword(networkClient, loadBalancerID, resetPasswordOpts).ExtractResetPassword() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", resetPasswordResult) + +*/ +package load_balancer_actions diff --git a/v3/ecl/network/v2/load_balancer_actions/requests.go b/v3/ecl/network/v2/load_balancer_actions/requests.go new file mode 100644 index 0000000..7d95ee4 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/requests.go @@ -0,0 +1,67 @@ +package load_balancer_actions + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// RebootOpts represents the attributes used when rebooting a Load Balancer. +type RebootOpts struct { + + // should syslog record acl info + Type string `json:"type" required:"true"` +} + +// ToLoadBalancerActionRebootMap builds a request body from RebootOpts. +func (opts RebootOpts) ToLoadBalancerActionRebootMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Reboot accepts a RebootOpts struct and reboots an existing Load Balancer using the +// values provided. +func Reboot(c *eclcloud.ServiceClient, id string, opts RebootOpts) (r RebootResult) { + b, err := opts.ToLoadBalancerActionRebootMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rebootURL(c, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResetPasswordOpts represents the attributes used when resetting password of load_balancer instance. +type ResetPasswordOpts struct { + + // should syslog record acl info + Username string `json:"username" required:"true"` +} + +// ToLoadBalancerActionResetPasswordMap builds a request body from ResetPasswordOpts. +func (opts ResetPasswordOpts) ToLoadBalancerActionResetPasswordMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// ResetPassword accepts a ResetPasswordOpts struct and resets an existing Load Balancer password using the +// values provided. +func ResetPassword(c *eclcloud.ServiceClient, id string, opts ResetPasswordOpts) (r ResetPasswordResult) { + b, err := opts.ToLoadBalancerActionResetPasswordMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(resetPasswordURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/network/v2/load_balancer_actions/results.go b/v3/ecl/network/v2/load_balancer_actions/results.go new file mode 100644 index 0000000..025a972 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/results.go @@ -0,0 +1,38 @@ +package load_balancer_actions + +import ( + "github.com/nttcom/eclcloud/v3" +) + +type commonResult struct { + eclcloud.Result +} + +// ExtractResetPassword is a function that accepts a result and extracts a result of reset_password. +func (r commonResult) ExtractResetPassword() (*Password, error) { + var s Password + err := r.ExtractInto(&s) + return &s, err +} + +// RebootResult represents the result of a reboot operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RebootResult struct { + eclcloud.ErrResult +} + +// ResetPasswordResult represents the result of a Reset Password operation. Call its ExtractResetPassword +// method to interpret it as an action's result. +type ResetPasswordResult struct { + commonResult +} + +// Password represents a detail of a Reset Password operation. +type Password struct { + + // new password + NewPassword string `json:"new_password"` + + // username + Username string `json:"username"` +} diff --git a/v3/ecl/network/v2/load_balancer_actions/testing/doc.go b/v3/ecl/network/v2/load_balancer_actions/testing/doc.go new file mode 100644 index 0000000..b0c1fa6 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer/Actions unit tests +package testing diff --git a/v3/ecl/network/v2/load_balancer_actions/testing/fixtures.go b/v3/ecl/network/v2/load_balancer_actions/testing/fixtures.go new file mode 100644 index 0000000..909f092 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/testing/fixtures.go @@ -0,0 +1,27 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_actions" +) + +const RebootRequest = ` +{ + "type": "HARD" +} +` +const ResetPasswordResponse = ` +{ + "new_password": "ABCDabcd4321", + "username": "user-read" +} +` +const ResetPasswordRequest = ` +{ + "username": "user-read" +} +` + +var ResetPasswordDetail = load_balancer_actions.Password{ + NewPassword: "ABCDabcd4321", + Username: "user-read", +} diff --git a/v3/ecl/network/v2/load_balancer_actions/testing/request_test.go b/v3/ecl/network/v2/load_balancer_actions/testing/request_test.go new file mode 100644 index 0000000..483765b --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/testing/request_test.go @@ -0,0 +1,78 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_actions" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestRebootLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/6e9c7745-61f2-491f-9689-add8c5fc4b9a/reboot", 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, RebootRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + }) + + options := load_balancer_actions.RebootOpts{ + Type: "HARD", + } + res := load_balancer_actions.Reboot(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", options) + th.AssertNoErr(t, res.Err) +} + +func TestRequiredRebootOptsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancer_actions.Reboot(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", load_balancer_actions.RebootOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestResetPasswordLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/6e9c7745-61f2-491f-9689-add8c5fc4b9a/reset_password", 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, ResetPasswordRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ResetPasswordResponse) + }) + + options := load_balancer_actions.ResetPasswordOpts{ + Username: "user-read", + } + s, err := load_balancer_actions.ResetPassword(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", options).ExtractResetPassword() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &ResetPasswordDetail, s) +} + +func TestRequiredResetPasswordOptsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancer_actions.ResetPassword(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", load_balancer_actions.ResetPasswordOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/network/v2/load_balancer_actions/urls.go b/v3/ecl/network/v2/load_balancer_actions/urls.go new file mode 100644 index 0000000..9542c29 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_actions/urls.go @@ -0,0 +1,11 @@ +package load_balancer_actions + +import "github.com/nttcom/eclcloud/v3" + +func rebootURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id, "reboot") +} + +func resetPasswordURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id, "reset_password") +} diff --git a/v3/ecl/network/v2/load_balancer_interfaces/doc.go b/v3/ecl/network/v2/load_balancer_interfaces/doc.go new file mode 100644 index 0000000..973f0d8 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/doc.go @@ -0,0 +1,51 @@ +/* +Package load_balancer_interfaces contains functionality for working with +ECL Load Balancer Interface resources. + +Example to List Load Balancer Interfaces + + listOpts := load_balancer_interfaces.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := load_balancer_interfaces.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancerInterfaces, err := load_balancer_interfaces.ExtractLoadBalancerInterfaces(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancerInterface := range allLoadBalancerInterfaces { + fmt.Printf("%+v\n", loadBalancerInterface) + } + + +Example to Show Load Balancer Interface + + loadBalancerInterfaceID := "f44e063c-5fea-45b8-9124-956995eafe2a" + + loadBalancerInterface, err := load_balancer_interfaces.Get(networkClient, loadBalancerInterfaceID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerInterface) + + +Example to Update Load Balancer Interface + + loadBalancerInterfaceID := "f44e063c-5fea-45b8-9124-956995eafe2a" + + updateOpts := load_balancer_interfaces.UpdateOpts{ + Name: "new_name", + } + + loadBalancerInterface, err := load_balancer_interfaces.Update(networkClient, loadBalancerInterfaceID, updateOpts).Extract() + if err != nil { + panic(err) + } +*/ +package load_balancer_interfaces diff --git a/v3/ecl/network/v2/load_balancer_interfaces/requests.go b/v3/ecl/network/v2/load_balancer_interfaces/requests.go new file mode 100644 index 0000000..e3a3e20 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/requests.go @@ -0,0 +1,138 @@ +package load_balancer_interfaces + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/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 Load Balancer Interface attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer Interface attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + IPAddress string `q:"ip_address"` + LoadBalancerID string `q:"load_balancer_id"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + SlotNumber int `q:"slot_number"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + VirtualIPAddress string `q:"virtual_ip_address"` +} + +// ToLoadBalancerInterfacesListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerInterfacesListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancer Interfaces. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancer Interfaces that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancerInterfacesListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer Interface based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// UpdateOpts represents the attributes used when updating an existing Load Balancer Interface. +type UpdateOpts struct { + + // Description is description + Description *string `json:"description,omitempty"` + + // IP Address + IPAddress string `json:"ip_address,omitempty"` + + // Name of the Load Balancer Interface + Name *string `json:"name,omitempty"` + + // UUID of the parent network. + NetworkID *interface{} `json:"network_id,omitempty"` + + // Virtual IP Address + VirtualIPAddress *interface{} `json:"virtual_ip_address,omitempty"` + + // Properties used for virtual IP address + VirtualIPProperties *VirtualIPProperties `json:"virtual_ip_properties,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerInterfaceUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer_interface") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing Load Balancer Interface using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerInterfaceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convenience function that returns a Load Balancer Interface's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancerInterfaces(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer_interface"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer_interface"} + } +} diff --git a/v3/ecl/network/v2/load_balancer_interfaces/results.go b/v3/ecl/network/v2/load_balancer_interfaces/results.go new file mode 100644 index 0000000..c9f3421 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/results.go @@ -0,0 +1,101 @@ +package load_balancer_interfaces + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Load Balancer Interface. +type UpdateResult struct { + commonResult +} + +// Extract is a function that accepts a result and extracts a Load Balancer Interface resource. +func (r commonResult) Extract() (*LoadBalancerInterface, error) { + var s struct { + LoadBalancerInterface *LoadBalancerInterface `json:"load_balancer_interface"` + } + err := r.ExtractInto(&s) + return s.LoadBalancerInterface, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer Interface. +type GetResult struct { + commonResult +} + +// Properties used for virtual IP address +type VirtualIPProperties struct { + Protocol string `json:"protocol"` + Vrid int `json:"vrid"` +} + +// LoadBalancerInterface represents a Load Balancer Interface. See package documentation for a top-level +// description of what this is. +type LoadBalancerInterface struct { + + // Description is description + Description string `json:"description"` + + // UUID representing the Load Balancer Interface. + ID string `json:"id"` + + // IP Address + IPAddress *string `json:"ip_address"` + + // The ID of load_balancer this load_balancer_interface belongs to. + LoadBalancerID string `json:"load_balancer_id"` + + // Name of the Load Balancer Interface + Name string `json:"name"` + + // UUID of the parent network. + NetworkID *string `json:"network_id"` + + // Slot Number + SlotNumber int `json:"slot_number"` + + // Load Balancer Interface status + Status string `json:"status"` + + // Tenant ID of the owner (UUID) + TenantID string `json:"tenant_id"` + + // Load Balancer Interface type + Type string `json:"type"` + + // Virtual IP Address + VirtualIPAddress *string `json:"virtual_ip_address"` + + // Properties used for virtual IP address + VirtualIPProperties *VirtualIPProperties `json:"virtual_ip_properties"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a collection +// of load balancers. +type LoadBalancerInterfacePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerInterfacePage struct is empty. +func (r LoadBalancerInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancerInterfaces(r) + return len(is) == 0, err +} + +// ExtractLoadBalancerInterfaces accepts a Page struct, specifically a LoadBalancerPage struct, +// and extracts the elements into a slice of Load Balancer Interface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancerInterfaces(r pagination.Page) ([]LoadBalancerInterface, error) { + var s struct { + LoadBalancerInterfaces []LoadBalancerInterface `json:"load_balancer_interfaces"` + } + err := (r.(LoadBalancerInterfacePage)).ExtractInto(&s) + return s.LoadBalancerInterfaces, err +} diff --git a/v3/ecl/network/v2/load_balancer_interfaces/testing/doc.go b/v3/ecl/network/v2/load_balancer_interfaces/testing/doc.go new file mode 100644 index 0000000..d644bd6 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer Interfaces unit tests +package testing diff --git a/v3/ecl/network/v2/load_balancer_interfaces/testing/fixtures.go b/v3/ecl/network/v2/load_balancer_interfaces/testing/fixtures.go new file mode 100644 index 0000000..c8dadcb --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/testing/fixtures.go @@ -0,0 +1,183 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_interfaces" +) + +const ListResponse = ` +{ + "load_balancer_interfaces": [ + { + "description": "test1", + "id": "b409f68e-9307-4649-9073-bb3cb776bda5", + "ip_address": "100.64.64.34", + "load_balancer_id": "5a109f4a-ebd8-4998-8410-98629e2bd5cd", + "name": "Interface 1/2", + "network_id": "30b665e3-db2b-473b-a09a-8940148b6491", + "slot_number": 2, + "status": "ACTIVE", + "tenant_id": "8fe1cc29-ff7d4773bced6cb02fc8002f", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "description": "test2", + "id": "0aaef2e9-b4a0-4c31-bd98-496e0a8fed4f", + "ip_address": null, + "load_balancer_id": "12efe0b1-02b6-4e97-ad93-9dc1f7b5c0fc", + "name": "Interface 1/1", + "network_id": null, + "slot_number": 1, + "status": "DOWN", + "tenant_id": "44777b33f0ee474ab1466ebee9fa369f", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ] +} +` +const GetResponse = ` +{ + "load_balancer_interface": { + "description": "test3", + "id": "da3f99e8-a949-40e7-a0e4-4609b705a7c7", + "ip_address": "100.64.64.34", + "load_balancer_id": "79378a5d-bc2f-4a74-ab4b-ceae8693dca5", + "name": "Interface 1/2", + "network_id": "30b665e3-db2b-473b-a09a-8940148b6491", + "slot_number": 2, + "status": "ACTIVE", + "tenant_id": "401c9473a52b4ee486d17ea76f466f66", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + } +} + ` + +const UpdateRequest = ` +{ + "load_balancer_interface": { + "description": "test", + "ip_address": "100.64.64.34", + "name": "Interface 1/2", + "network_id": "e6106a35-d79b-44a3-bda0-6009b2f8775a", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + } +} +` +const UpdateResponse = ` +{ + "load_balancer_interface": { + "description": "test", + "id": "2897f333-3554-4099-a638-64d7022bf9ae", + "ip_address": "100.64.64.34", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "name": "Interface 1/2", + "network_id": "e6106a35-d79b-44a3-bda0-6009b2f8775a", + "slot_number": 2, + "status": "PENDING_UPDATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + } +} +` + +var LoadBalancerInterface1 = load_balancer_interfaces.LoadBalancerInterface{ + Description: "test1", + ID: "b409f68e-9307-4649-9073-bb3cb776bda5", + IPAddress: &DetailIPAddress, + LoadBalancerID: "5a109f4a-ebd8-4998-8410-98629e2bd5cd", + Name: "Interface 1/2", + NetworkID: &DetailNetworkID, + SlotNumber: 2, + Status: "ACTIVE", + TenantID: "8fe1cc29-ff7d4773bced6cb02fc8002f", + VirtualIPAddress: &DetailVirtualIPAddress, + VirtualIPProperties: &load_balancer_interfaces.VirtualIPProperties{ + Protocol: "vrrp", + Vrid: 10, + }, +} + +var DetailIPAddress = "100.64.64.34" +var DetailNetworkID = "30b665e3-db2b-473b-a09a-8940148b6491" +var DetailVirtualIPAddress = "100.64.64.101" + +var LoadBalancerInterface2 = load_balancer_interfaces.LoadBalancerInterface{ + Description: "test2", + ID: "0aaef2e9-b4a0-4c31-bd98-496e0a8fed4f", + LoadBalancerID: "12efe0b1-02b6-4e97-ad93-9dc1f7b5c0fc", + Name: "Interface 1/1", + SlotNumber: 1, + Status: "DOWN", + TenantID: "44777b33f0ee474ab1466ebee9fa369f", +} + +var LoadBalancerInterfaceDetail = load_balancer_interfaces.LoadBalancerInterface{ + Description: "test3", + ID: "da3f99e8-a949-40e7-a0e4-4609b705a7c7", + IPAddress: &DetailIPAddress, + LoadBalancerID: "79378a5d-bc2f-4a74-ab4b-ceae8693dca5", + Name: "Interface 1/2", + NetworkID: &DetailNetworkID, + SlotNumber: 2, + Status: "ACTIVE", + TenantID: "401c9473a52b4ee486d17ea76f466f66", + VirtualIPAddress: &DetailVirtualIPAddress, + VirtualIPProperties: &load_balancer_interfaces.VirtualIPProperties{ + Protocol: "vrrp", + Vrid: 10, + }, +} + +var ExpectedLoadBalancerInterfaceSlice = []load_balancer_interfaces.LoadBalancerInterface{LoadBalancerInterface1, LoadBalancerInterface2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancer_interfaces": [ + { + "description": "test1", + "id": "b409f68e-9307-4649-9073-bb3cb776bda5", + "ip_address": "100.64.64.34", + "load_balancer_id": "5a109f4a-ebd8-4998-8410-98629e2bd5cd", + "name": "Interface 1/2", + "network_id": "30b665e3-db2b-473b-a09a-8940148b6491", + "slot_number": 2, + "status": "ACTIVE", + "tenant_id": "8fe1cc29-ff7d4773bced6cb02fc8002f", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "description": "test2", + "id": "0aaef2e9-b4a0-4c31-bd98-496e0a8fed4f", + "ip_address": null, + "load_balancer_id": "12efe0b1-02b6-4e97-ad93-9dc1f7b5c0fc", + "name": "Interface 1/2", + "network_id": null, + "slot_number": 1, + "status": "DOWN", + "tenant_id": "44777b33f0ee474ab1466ebee9fa369f", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ] +} +` diff --git a/v3/ecl/network/v2/load_balancer_interfaces/testing/request_test.go b/v3/ecl/network/v2/load_balancer_interfaces/testing/request_test.go new file mode 100644 index 0000000..8048ebd --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/testing/request_test.go @@ -0,0 +1,197 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListLoadBalancerInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancer_interfaces.List(client, load_balancer_interfaces.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancer_interfaces.ExtractLoadBalancerInterfaces(page) + if err != nil { + t.Errorf("Failed to extract Load Balancer Interfaces: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerInterfaceSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancerInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces/5f3cae7c-58a5-4124-b622-9ca3cfbf2525", 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, GetResponse) + }) + + s, err := load_balancer_interfaces.Get(fake.ServiceClient(), "5f3cae7c-58a5-4124-b622-9ca3cfbf2525").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerInterfaceDetail, s) +} + +func TestUpdateLoadBalancerInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "test" + ipAddress := "100.64.64.34" + name := "Interface 1/2" + networkID := interface{}("e6106a35-d79b-44a3-bda0-6009b2f8775a") + virtualIPAddress := interface{}("100.64.64.101") + virtualIPProperties := load_balancer_interfaces.VirtualIPProperties{ + Protocol: "vrrp", + Vrid: 10, + } + + id := "2897f333-3554-4099-a638-64d7022bf9ae" + slotNumber := 2 + + status := "PENDING_UPDATE" + + tenantID := "6a156ddf2ecd497ca786ff2da6df5aa8" + + loadBalancerID := "9f872504-36ab-46af-83ce-a4991c669edd" + + options := load_balancer_interfaces.UpdateOpts{ + Description: &description, + IPAddress: ipAddress, + Name: &name, + NetworkID: &networkID, + VirtualIPAddress: &virtualIPAddress, + VirtualIPProperties: &virtualIPProperties, + } + + s, err := load_balancer_interfaces.Update(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, description, s.Description) + th.CheckEquals(t, id, s.ID) + th.CheckEquals(t, ipAddress, *s.IPAddress) + th.CheckEquals(t, loadBalancerID, s.LoadBalancerID) + th.CheckEquals(t, name, s.Name) + th.CheckEquals(t, networkID, *s.NetworkID) + th.CheckEquals(t, slotNumber, s.SlotNumber) + th.CheckEquals(t, status, s.Status) + th.CheckEquals(t, tenantID, s.TenantID) + th.CheckEquals(t, virtualIPAddress, *s.VirtualIPAddress) + th.CheckDeepEquals(t, virtualIPProperties, *s.VirtualIPProperties) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "b409f68e-9307-4649-9073-bb3cb776bda5" + actualID, err := load_balancer_interfaces.IDFromName(client, "Interface 1/2") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_interfaces.IDFromName(client, "Interface X") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_interfaces.IDFromName(client, "Interface 1/2") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/network/v2/load_balancer_interfaces/urls.go b/v3/ecl/network/v2/load_balancer_interfaces/urls.go new file mode 100644 index 0000000..e9ff953 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_interfaces/urls.go @@ -0,0 +1,23 @@ +package load_balancer_interfaces + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancer_interfaces", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancer_interfaces") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/load_balancer_plans/doc.go b/v3/ecl/network/v2/load_balancer_plans/doc.go new file mode 100644 index 0000000..f5f38bc --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/doc.go @@ -0,0 +1,37 @@ +/* +Package load_balancer_plans contains functionality for working with +ECL Load Balancer Plan resources. + +Example to List Load Balancer Plans + + listOpts := load_balancer_plans.ListOpts{ + Description: "general", + } + + allPages, err := load_balancer_plans.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancerPlans, err := load_balancer_plans.ExtractLoadBalancerPlans(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancerPlan := range allLoadBalancerPlans { + fmt.Printf("%+v\n", loadBalancerPlan) + } + +Example to Show Load Balancer Plan + + loadBalancerPlanID := "a46eeb5a-bc0a-40fa-b455-e5dc13b1220a" + + loadBalancerPlan, err := load_balancer_plans.Get(networkClient, loadBalancerPlanID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerPlan) + +*/ +package load_balancer_plans diff --git a/v3/ecl/network/v2/load_balancer_plans/requests.go b/v3/ecl/network/v2/load_balancer_plans/requests.go new file mode 100644 index 0000000..a8436ae --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/requests.go @@ -0,0 +1,88 @@ +package load_balancer_plans + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/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 Load Balancer Plan attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer Plan attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + MaximumSyslogServers int `q:"maximum_syslog_servers"` + Name string `q:"name"` + Vendor string `q:"vendor"` + Version string `q:"version"` +} + +// ToLoadBalancerPlansListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerPlansListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancer Plans. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancer Plans that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancerPlansListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPlanPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer Plan based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// IDFromName is a convenience function that returns a Load Balancer Plan's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancerPlans(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer_plan"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer_plan"} + } +} diff --git a/v3/ecl/network/v2/load_balancer_plans/results.go b/v3/ecl/network/v2/load_balancer_plans/results.go new file mode 100644 index 0000000..836741f --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/results.go @@ -0,0 +1,83 @@ +package load_balancer_plans + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Load Balancer Plan resource. +func (r commonResult) Extract() (*LoadBalancerPlan, error) { + var s struct { + LoadBalancerPlan *LoadBalancerPlan `json:"load_balancer_plan"` + } + err := r.ExtractInto(&s) + return s.LoadBalancerPlan, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer Plan. +type GetResult struct { + commonResult +} + +// Model of Load Balancer. +type Model struct { + Edition string `json:"edition"` + Size string `json:"size"` +} + +// LoadBalancerPlan represents a Load Balancer Plan. See package documentation for a top-level +// description of what this is. +type LoadBalancerPlan struct { + + // Description is description + Description string `json:"description"` + + // Is user allowed to create new load balancers with this plan. + Enabled bool `json:"enabled"` + + // UUID representing the Load Balancer Plan. + ID string `json:"id"` + + // Maximum number of syslog servers + MaximumSyslogServers int `json:"maximum_syslog_servers"` + + // Model of load balancer + Model Model `json:"model"` + + // Name of the Load Balancer Plan + Name string `json:"name"` + + // Load Balancer Type + Vendor string `json:"vendor"` + + // Version name + Version string `json:"version"` +} + +// LoadBalancerPlanPage is the page returned by a pager when traversing over a collection +// of load balancer plans. +type LoadBalancerPlanPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerPlanPage struct is empty. +func (r LoadBalancerPlanPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancerPlans(r) + return len(is) == 0, err +} + +// ExtractLoadBalancerPlans accepts a Page struct, specifically a LoadBalancerPage struct, +// and extracts the elements into a slice of Load Balancer Plan structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancerPlans(r pagination.Page) ([]LoadBalancerPlan, error) { + var s struct { + LoadBalancerPlans []LoadBalancerPlan `json:"load_balancer_plans"` + } + err := (r.(LoadBalancerPlanPage)).ExtractInto(&s) + return s.LoadBalancerPlans, err +} diff --git a/v3/ecl/network/v2/load_balancer_plans/testing/doc.go b/v3/ecl/network/v2/load_balancer_plans/testing/doc.go new file mode 100644 index 0000000..6a790dc --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer Plans unit tests +package testing diff --git a/v3/ecl/network/v2/load_balancer_plans/testing/fixtures.go b/v3/ecl/network/v2/load_balancer_plans/testing/fixtures.go new file mode 100644 index 0000000..9ffca0a --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/testing/fixtures.go @@ -0,0 +1,132 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_plans" +) + +const ListResponse = ` +{ + "load_balancer_plans": [ + { + "description": "Load Balancer Description 1", + "enabled": true, + "id": "58ab4df4-10f2-4fa0-b374-74b06dd648ee", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "50" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + }, + { + "description": "Load Balancer Description 2", + "enabled": false, + "id": "8b0cc5cc-b612-4810-ae45-7d6c5e806b3a", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "1000" + }, + "name": "LB_Plan2", + "vendor": "citrix", + "version": "10.5-57.7" + } + ] +} +` +const GetResponse = ` +{ + "load_balancer_plan": { + "description": "Load Balance Plan Description", + "enabled": true, + "id": "6e5faf0c-9361-4b98-bfc4-670497c9bde3", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "50" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + } +} + ` + +var LoadBalancerPlan1 = load_balancer_plans.LoadBalancerPlan{ + Description: "Load Balancer Description 1", + Enabled: true, + ID: "58ab4df4-10f2-4fa0-b374-74b06dd648ee", + MaximumSyslogServers: 10, + Model: load_balancer_plans.Model{ + Edition: "Standard", + Size: "50", + }, + Name: "LB_Plan1", + Vendor: "citrix", + Version: "10.5-57.7", +} + +var LoadBalancerPlan2 = load_balancer_plans.LoadBalancerPlan{ + Description: "Load Balancer Description 2", + Enabled: false, + ID: "8b0cc5cc-b612-4810-ae45-7d6c5e806b3a", + MaximumSyslogServers: 10, + Model: load_balancer_plans.Model{ + Edition: "Standard", + Size: "1000", + }, + Name: "LB_Plan2", + Vendor: "citrix", + Version: "10.5-57.7", +} + +var LoadBalancerDetail = load_balancer_plans.LoadBalancerPlan{ + Description: "Load Balance Plan Description", + Enabled: true, + ID: "6e5faf0c-9361-4b98-bfc4-670497c9bde3", + MaximumSyslogServers: 10, + Model: load_balancer_plans.Model{ + Edition: "Standard", + Size: "50", + }, + Name: "LB_Plan1", + Vendor: "citrix", + Version: "10.5-57.7", +} + +var ExpectedLoadBalancerPlanSlice = []load_balancer_plans.LoadBalancerPlan{LoadBalancerPlan1, LoadBalancerPlan2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancer_plans": [ + { + "description": "Load Balancer Description 1", + "enabled": true, + "id": "58ab4df4-10f2-4fa0-b374-74b06dd648ee", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "50" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + }, + { + "description": "Load Balancer Description 2", + "enabled": false, + "id": "8b0cc5cc-b612-4810-ae45-7d6c5e806b3a", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "1000" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + } + ] +} +` diff --git a/v3/ecl/network/v2/load_balancer_plans/testing/request_test.go b/v3/ecl/network/v2/load_balancer_plans/testing/request_test.go new file mode 100644 index 0000000..2c3f544 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/testing/request_test.go @@ -0,0 +1,136 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_plans" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListLoadBalancerPlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancer_plans.List(client, load_balancer_plans.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancer_plans.ExtractLoadBalancerPlans(page) + if err != nil { + t.Errorf("Failed to extract Load Balancer Plans: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerPlanSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancerPlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans/5f3cae7c-58a5-4124-b622-9ca3cfbf2525", 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, GetResponse) + }) + + s, err := load_balancer_plans.Get(fake.ServiceClient(), "5f3cae7c-58a5-4124-b622-9ca3cfbf2525").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerDetail, s) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "58ab4df4-10f2-4fa0-b374-74b06dd648ee" + actualID, err := load_balancer_plans.IDFromName(client, "LB_Plan1") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_plans.IDFromName(client, "LB_PlanX") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_plans.IDFromName(client, "LB_Plan1") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/network/v2/load_balancer_plans/urls.go b/v3/ecl/network/v2/load_balancer_plans/urls.go new file mode 100644 index 0000000..3046da8 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_plans/urls.go @@ -0,0 +1,19 @@ +package load_balancer_plans + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancer_plans", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancer_plans") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/doc.go b/v3/ecl/network/v2/load_balancer_syslog_servers/doc.go new file mode 100644 index 0000000..623ddd3 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/doc.go @@ -0,0 +1,88 @@ +/* +Package load_balancer_syslog_servers contains functionality for working with +ECL Load Balancer Syslog Server resources. + +Example to List Load Balancer Syslog Servers + + listOpts := load_balancer_syslog_servers.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := load_balancer_syslog_servers.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancerSyslogServers, err := load_balancer_syslog_servers.ExtractLoadBalancerSyslogServers(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancerSyslogServer := range allLoadBalancerSyslogServers { + fmt.Printf("%+v\n", loadBalancerSyslogServer) + } + + +Example to Show Load Balancer Syslog Server + + loadBalancerSyslogServerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + + loadBalancerSyslogServer, err := load_balancer_syslog_servers.Get(networkClient, loadBalancerSyslogServerID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerSyslogServer) + + +Example to Create a Load Balancer Syslog Server + + priority := 20 + + createOpts := load_balancer_syslog_servers.CreateOpts{ + AclLogging: "DISABLED", + AppflowLogging: "DISABLED", + DateFormat: "MMDDYYYY", + Description: "test", + IPAddress: "120.120.120.30", + LoadBalancerID: "4f6ebc24-f768-485b-99ef-f308063d0209", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Priority: &priority, + TcpLogging: "ALL", + TenantID: "b58531f716614e82a9bf001571c8bb15", + TimeZone: "LOCAL_TIME", + TransportType: "UDP", + UserConfigurableLogMessages: "NO", + } + + loadBalancerSyslogServer, err := load_balancer_syslog_servers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer Syslog Server + + loadBalancerSyslogServerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + description := "new_description" + + updateOpts := load_balancer_syslog_servers.UpdateOpts{ + Description: &description, + } + + loadBalancerSyslogServer, err := load_balancer_syslog_servers.Update(networkClient, loadBalancerSyslogServerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancer Syslog Server + + loadBalancerSyslogServerID := "13762eaf-9564-4c94-a106-98ece9fa189e" + err := load_balancer_syslog_servers.Delete(networkClient, loadBalancerSyslogServerID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package load_balancer_syslog_servers diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/requests.go b/v3/ecl/network/v2/load_balancer_syslog_servers/requests.go new file mode 100644 index 0000000..bf456d6 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/requests.go @@ -0,0 +1,233 @@ +package load_balancer_syslog_servers + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/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 Load Balancer Syslog Server attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer Syslog Server attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + IPAddress string `q:"ip_address"` + LoadBalancerID string `q:"load_balancer_id"` + LogFacility string `q:"log_facility"` + LogLevel string `q:"log_level"` + Name string `q:"name"` + PortNumber int `q:"port_number"` + Status string `q:"status"` + TransportType string `q:"transport_type"` +} + +// ToLoadBalancerSyslogServersListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerSyslogServersListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancer Syslog Servers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancer Syslog Servers that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancerSyslogServersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerSyslogServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer Syslog Server based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOpts represents the attributes used when creating a new Load Balancer Syslog Server. +type CreateOpts struct { + + // should syslog record acl info + AclLogging string `json:"acl_logging,omitempty"` + + // should syslog record appflow info + AppflowLogging string `json:"appflow_logging,omitempty"` + + // date format utilized by syslog + DateFormat string `json:"date_format,omitempty"` + + // Description is description + Description string `json:"description,omitempty"` + + // Ip address of syslog server + IPAddress string `json:"ip_address" required:"true"` + + // The ID of load_balancer this load_balancer_syslog_server belongs to. + LoadBalancerID string `json:"load_balancer_id" required:"true"` + + // Log facility for syslog + LogFacility string `json:"log_facility,omitempty"` + + // Log level for syslog + LogLevel string `json:"log_level,omitempty"` + + // Name is a human-readable name of the Load Balancer Syslog Server. + Name string `json:"name" required:"true"` + + // Port number of syslog server + PortNumber int `json:"port_number,omitempty"` + + // priority (0-255) + Priority *int `json:"priority,omitempty"` + + // should syslog record tcp protocol info + TcpLogging string `json:"tcp_logging,omitempty"` + + // The UUID of the project who owns the Load Balancer Syslog Server. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // time zone utilized by syslog + TimeZone string `json:"time_zone,omitempty"` + + // protocol for syslog transport + TransportType string `json:"transport_type,omitempty"` + + // can user configure log messages + UserConfigurableLogMessages string `json:"user_configurable_log_messages,omitempty"` +} + +// ToLoadBalancerSyslogServerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerSyslogServerCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer_syslog_server") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new Load Balancer Syslog Server using the values +// provided. You must remember to provide a valid LoadBalancerPlanID. +func Create(c *eclcloud.ServiceClient, opts CreateOpts) (r CreateResult) { + b, err := opts.ToLoadBalancerSyslogServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOpts represents the attributes used when updating an existing Load Balancer Syslog Server. +type UpdateOpts struct { + + // should syslog record acl info + AclLogging string `json:"acl_logging,omitempty"` + + // should syslog record appflow info + AppflowLogging string `json:"appflow_logging,omitempty"` + + // date format utilized by syslog + DateFormat string `json:"date_format,omitempty"` + + // Description is description + Description *string `json:"description,omitempty"` + + // Log facility for syslog + LogFacility string `json:"log_facility,omitempty"` + + // Log level for syslog + LogLevel string `json:"log_level,omitempty"` + + // priority (0-255) + Priority *int `json:"priority,omitempty"` + + // should syslog record tcp protocol info + TcpLogging string `json:"tcp_logging,omitempty"` + + // time zone utilized by syslog + TimeZone string `json:"time_zone,omitempty"` + + // can user configure log messages + UserConfigurableLogMessages string `json:"user_configurable_log_messages,omitempty"` +} + +// ToLoadBalancerSyslogServerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerSyslogServerUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer_syslog_server") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing Load Balancer Syslog Server using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerSyslogServerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the Load Balancer Syslog Server associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a Load Balancer Syslog Server's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancerSyslogServers(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer_syslog_server"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer_syslog_server"} + } +} diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/results.go b/v3/ecl/network/v2/load_balancer_syslog_servers/results.go new file mode 100644 index 0000000..262c3e1 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/results.go @@ -0,0 +1,125 @@ +package load_balancer_syslog_servers + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Load Balancer Syslog Server resource. +func (r commonResult) Extract() (*LoadBalancerSyslogServer, error) { + var s struct { + LoadBalancerSyslogServer *LoadBalancerSyslogServer `json:"load_balancer_syslog_server"` + } + err := r.ExtractInto(&s) + return s.LoadBalancerSyslogServer, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Load Balancer Syslog Server. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer Syslog Server. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Load Balancer Syslog Server. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// LoadBalancerSyslogServer represents a Load Balancer Syslog Server. See package documentation for a top-level +// description of what this is. +type LoadBalancerSyslogServer struct { + + // should syslog record acl info + AclLogging string `json:"acl_logging"` + + // should syslog record appflow info + AppflowLogging string `json:"appflow_logging"` + + // date format utilized by syslog + DateFormat string `json:"date_format"` + + // Description is description + Description string `json:"description"` + + // UUID representing the Load Balancer Syslog Server. + ID string `json:"id"` + + // Ip address of syslog server + IPAddress string `json:"ip_address"` + + // The ID of load_balancer this load_balancer_syslog_server belongs to. + LoadBalancerID string `json:"load_balancer_id"` + + // Log facility for syslog + LogFacility string `json:"log_facility"` + + // Log level for syslog + LogLevel string `json:"log_level"` + + // Name of the syslog resource + Name string `json:"name"` + + // Port number of syslog server + PortNumber int `json:"port_number"` + + // priority (0-255) + Priority int `json:"priority"` + + // Load balancer syslog server status + Status string `json:"status"` + + // should syslog record tcp protocol info + TcpLogging string `json:"tcp_logging"` + + // TenantID is the project owner of the Load Balancer Syslog Server. + TenantID string `json:"tenant_id"` + + // time zone utilized by syslog + TimeZone string `json:"time_zone"` + + // protocol for syslog transport + TransportType string `json:"transport_type"` + + // can user configure log messages + UserConfigurableLogMessages string `json:"user_configurable_log_messages"` +} + +// LoadBalancerSyslogServerPage is the page returned by a pager when traversing over a collection +// of load balancer Syslog Servers. +type LoadBalancerSyslogServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerSyslogServerPage struct is empty. +func (r LoadBalancerSyslogServerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancerSyslogServers(r) + return len(is) == 0, err +} + +// ExtractLoadBalancerSyslogServers accepts a Page struct, specifically a LoadBalancerSyslogServerPage struct, +// and extracts the elements into a slice of Load Balancer Syslog Server structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancerSyslogServers(r pagination.Page) ([]LoadBalancerSyslogServer, error) { + var s struct { + LoadBalancerSyslogServers []LoadBalancerSyslogServer `json:"load_balancer_syslog_servers"` + } + err := (r.(LoadBalancerSyslogServerPage)).ExtractInto(&s) + return s.LoadBalancerSyslogServers, err +} diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/testing/doc.go b/v3/ecl/network/v2/load_balancer_syslog_servers/testing/doc.go new file mode 100644 index 0000000..1593aa9 --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer Syslog Servers unit tests +package testing diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/testing/fixtures.go b/v3/ecl/network/v2/load_balancer_syslog_servers/testing/fixtures.go new file mode 100644 index 0000000..3da58ff --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/testing/fixtures.go @@ -0,0 +1,226 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_syslog_servers" +) + +const ListResponse = ` +{ + "load_balancer_syslog_servers": [ + { + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + }, + { + "description": "My second backup server", + "id": "c7de2dee-73a0-4a9b-acdf-8a348c242a30", + "ip_address": "120.120.122.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL2", + "log_level": "ERROR", + "name": "second_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + } + ] +} +` +const GetResponse = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "status": "ACTIVE", + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const CreateResponse = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "status": "ACTIVE", + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const CreateRequest = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const UpdateResponse = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test2", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "status": "PENDING_UPDATE", + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const UpdateRequest = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test2", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "priority": 20, + "tcp_logging": "ALL", + "time_zone": "LOCAL_TIME", + "user_configurable_log_messages": "NO" + } +} +` + +var LoadBalancerSyslogServer1 = load_balancer_syslog_servers.LoadBalancerSyslogServer{ + Description: "test", + ID: "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + IPAddress: "120.120.120.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Status: "ACTIVE", + TransportType: "UDP", +} + +var LoadBalancerSyslogServer2 = load_balancer_syslog_servers.LoadBalancerSyslogServer{ + Description: "My second backup server", + ID: "c7de2dee-73a0-4a9b-acdf-8a348c242a30", + IPAddress: "120.120.122.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL2", + LogLevel: "ERROR", + Name: "second_syslog_server", + PortNumber: 514, + Status: "ACTIVE", + TransportType: "UDP", +} + +var LoadBalancerSyslogServerDetail = load_balancer_syslog_servers.LoadBalancerSyslogServer{ + AclLogging: "DISABLED", + AppflowLogging: "DISABLED", + DateFormat: "MMDDYYYY", + Description: "test", + ID: "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + IPAddress: "120.120.120.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Priority: 20, + Status: "ACTIVE", + TcpLogging: "ALL", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + TimeZone: "LOCAL_TIME", + TransportType: "UDP", + UserConfigurableLogMessages: "NO", +} + +var ExpectedLoadBalancerSlice = []load_balancer_syslog_servers.LoadBalancerSyslogServer{LoadBalancerSyslogServer1, LoadBalancerSyslogServer2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancer_syslog_servers": [ + { + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + }, + { + "description": "My second backup server", + "id": "c7de2dee-73a0-4a9b-acdf-8a348c242a30", + "ip_address": "120.120.122.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL2", + "log_level": "ERROR", + "name": "first_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + } + ] +} +` diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/testing/request_test.go b/v3/ecl/network/v2/load_balancer_syslog_servers/testing/request_test.go new file mode 100644 index 0000000..8117e0a --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/testing/request_test.go @@ -0,0 +1,276 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancer_syslog_servers.List(client, load_balancer_syslog_servers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancer_syslog_servers.ExtractLoadBalancerSyslogServers(page) + if err != nil { + t.Errorf("Failed to extract Load Balancer Syslog Servers: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers/6e9c7745-61f2-491f-9689-add8c5fc4b9a", 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, GetResponse) + }) + + s, err := load_balancer_syslog_servers.Get(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerSyslogServerDetail, s) +} + +func TestCreateLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + priority := 20 + + options := load_balancer_syslog_servers.CreateOpts{ + AclLogging: "DISABLED", + AppflowLogging: "DISABLED", + DateFormat: "MMDDYYYY", + Description: "test", + IPAddress: "120.120.120.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Priority: &priority, + TcpLogging: "ALL", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + TimeZone: "LOCAL_TIME", + TransportType: "UDP", + UserConfigurableLogMessages: "NO", + } + s, err := load_balancer_syslog_servers.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &LoadBalancerSyslogServerDetail, s) +} + +func TestRequiredCreateOptsLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancer_syslog_servers.Create(fake.ServiceClient(), load_balancer_syslog_servers.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers/6e9c7745-61f2-491f-9689-add8c5fc4b9a", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + aclLogging := "DISABLED" + appflowLogging := "DISABLED" + dateFormat := "MMDDYYYY" + description := "test2" + logFacility := "LOCAL3" + logLevel := "DEBUG" + priority := 20 + tcpLogging := "ALL" + timeZone := "LOCAL_TIME" + userConfigurableLogMessages := "NO" + + id := "6e9c7745-61f2-491f-9689-add8c5fc4b9a" + + ipAddress := "120.120.120.30" + loadBalancerID := "9f872504-36ab-46af-83ce-a4991c669edd" + name := "first_syslog_server" + portNumber := 514 + status := "PENDING_UPDATE" + tenantID := "6a156ddf2ecd497ca786ff2da6df5aa8" + transportType := "UDP" + + options := load_balancer_syslog_servers.UpdateOpts{ + AclLogging: aclLogging, + AppflowLogging: appflowLogging, + DateFormat: dateFormat, + Description: &description, + LogFacility: logFacility, + LogLevel: logLevel, + Priority: &priority, + TcpLogging: tcpLogging, + TimeZone: timeZone, + UserConfigurableLogMessages: userConfigurableLogMessages, + } + + s, err := load_balancer_syslog_servers.Update(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, aclLogging, s.AclLogging) + th.CheckEquals(t, appflowLogging, s.AppflowLogging) + th.CheckEquals(t, dateFormat, s.DateFormat) + th.CheckEquals(t, description, s.Description) + th.CheckEquals(t, id, s.ID) + th.CheckEquals(t, logFacility, s.LogFacility) + th.CheckEquals(t, logLevel, s.LogLevel) + th.CheckEquals(t, priority, s.Priority) + th.CheckEquals(t, tcpLogging, s.TcpLogging) + th.CheckEquals(t, timeZone, s.TimeZone) + th.CheckEquals(t, userConfigurableLogMessages, s.UserConfigurableLogMessages) + th.CheckEquals(t, ipAddress, s.IPAddress) + th.CheckEquals(t, loadBalancerID, s.LoadBalancerID) + th.CheckEquals(t, name, s.Name) + th.CheckEquals(t, portNumber, s.PortNumber) + th.CheckEquals(t, status, s.Status) + th.CheckEquals(t, tenantID, s.TenantID) + th.CheckEquals(t, transportType, s.TransportType) + +} + +func TestDeleteLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers/6e9c7745-61f2-491f-9689-add8c5fc4b9a", 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 := load_balancer_syslog_servers.Delete(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a") + th.AssertNoErr(t, res.Err) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "6e9c7745-61f2-491f-9689-add8c5fc4b9a" + actualID, err := load_balancer_syslog_servers.IDFromName(client, "first_syslog_server") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_syslog_servers.IDFromName(client, "syslog_server X") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_syslog_servers.IDFromName(client, "first_syslog_server") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/network/v2/load_balancer_syslog_servers/urls.go b/v3/ecl/network/v2/load_balancer_syslog_servers/urls.go new file mode 100644 index 0000000..4fd634f --- /dev/null +++ b/v3/ecl/network/v2/load_balancer_syslog_servers/urls.go @@ -0,0 +1,31 @@ +package load_balancer_syslog_servers + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancer_syslog_servers", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancer_syslog_servers") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/load_balancers/doc.go b/v3/ecl/network/v2/load_balancers/doc.go new file mode 100644 index 0000000..e4b5a81 --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/doc.go @@ -0,0 +1,75 @@ +/* +Package load_balancers contains functionality for working with +ECL Load Balancer resources. + +Example to List Load Balancers + + listOpts := load_balancers.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := load_balancers.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancers, err := load_balancers.ExtractLoadBalancers(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancer := range allLoadBalancers { + fmt.Printf("%+v\n", loadBalancer) + } + + +Example to Show Load Balancer + + loadBalancerID := "f44e063c-5fea-45b8-9124-956995eafe2a" + + loadBalancer, err := load_balancers.Get(networkClient, loadBalancerID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancer) + + +Example to Create a Load Balancer + + createOpts := load_balancers.CreateOpts{ + AvailabilityZone: "zone1-groupa", + Description: "Load Balancer 1", + LoadBalancerPlanID: "69bf1e91-73f6-41d5-84c4-91de21a9af05", + Name: "abcdefghijklmnopqrstuvwxyz", + TenantID: "5cc454d62d8c4a0595134b2632bf2263", + } + + loadBalancer, err := load_balancers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer + + loadBalancerID := "f44e063c-5fea-45b8-9124-956995eafe2a" + name := "new_name" + + updateOpts := load_balancers.UpdateOpts{ + Name: &name, + } + + loadBalancer, err := load_balancers.Update(networkClient, loadBalancerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancer + + loadBalancerID := "165fb257-2365-4c05-b368-a7bed21bb927" + err := load_balancers.Delete(networkClient, loadBalancerID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package load_balancers diff --git a/v3/ecl/network/v2/load_balancers/requests.go b/v3/ecl/network/v2/load_balancers/requests.go new file mode 100644 index 0000000..06dab83 --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/requests.go @@ -0,0 +1,182 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/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 Load Balancer attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + AdminUsername string `q:"admin_username"` + AvailabilityZone string `q:"availability_zone"` + DefaultGateway string `q:"default_gateway"` + Description string `q:"description"` + ID string `q:"id"` + LoadBalancerPlanID string `q:"load_balancer_plan_id"` + Name string `q:"name"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + UserUsername string `q:"user_username"` +} + +// ToLoadBalancersListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancersListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancers that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOpts represents the attributes used when creating a new Load Balancer. +type CreateOpts struct { + + // AvailabilityZone is one of the Virtual Server (Nova)’s availability zone. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Description is description + Description string `json:"description,omitempty"` + + // LoadBalancerPlanID is the UUID of Load Balancer Plan. + LoadBalancerPlanID string `json:"load_balancer_plan_id" required:"true"` + + // Name is a human-readable name of the Load Balancer. + Name string `json:"name,omitempty"` + + // The UUID of the project who owns the Load Balancer. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` +} + +// ToLoadBalancerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new Load Balancer using the values +// provided. You must remember to provide a valid LoadBalancerPlanID. +func Create(c *eclcloud.ServiceClient, opts CreateOpts) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOpts represents the attributes used when updating an existing Load Balancer. +type UpdateOpts struct { + + // Description is description + DefaultGateway *interface{} `json:"default_gateway,omitempty"` + + // Description is description + Description *string `json:"description,omitempty"` + + // LoadBalancerPlanID is the UUID of Load Balancer Plan. + LoadBalancerPlanID string `json:"load_balancer_plan_id,omitempty"` + + // Name is a human-readable name of the Load Balancer. + Name *string `json:"name,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing Load Balancer using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the Load Balancer associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a Load Balancer's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancers(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer"} + } +} diff --git a/v3/ecl/network/v2/load_balancers/results.go b/v3/ecl/network/v2/load_balancers/results.go new file mode 100644 index 0000000..3f884af --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/results.go @@ -0,0 +1,115 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Load Balancer resource. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"load_balancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Load Balancer. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Load Balancer. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// LoadBalancer represents a Load Balancer. See package documentation for a top-level +// description of what this is. +type LoadBalancer struct { + + // AdminPassword is admin's password + AdminPassword string `json:"admin_password"` + + // AdminUsername is admin's username + AdminUsername string `json:"admin_username"` + + // AvailabilityZone is one of the Virtual Server (Nova)’s availability zone. + AvailabilityZone string `json:"availability_zone"` + + // Description is description + DefaultGateway *string `json:"default_gateway"` + + // Description is description + Description string `json:"description"` + + // UUID representing the Load Balancer. + ID string `json:"id"` + + // Attached interfaces by Load Balancer. + Interfaces []load_balancer_interfaces.LoadBalancerInterface `json:"interfaces"` + + // LoadBalancerPlanID is the UUID of Load Balancer Plan. + LoadBalancerPlanID string `json:"load_balancer_plan_id"` + + // Name of the Load Balancer. + Name string `json:"name"` + + // The Load Balancer status. + Status string `json:"status"` + + // Connected syslog servers. + SyslogServers []load_balancer_syslog_servers.LoadBalancerSyslogServer `json:"syslog_servers"` + + // TenantID is the project owner of the Load Balancer. + TenantID string `json:"tenant_id"` + + // User's password placeholder. + UserPassword string `json:"user_password"` + + // User's username placeholder. + UserUsername string `json:"user_username"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a collection +// of load balancers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (r LoadBalancerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancers(r) + return len(is) == 0, err +} + +// ExtractLoadBalancers accepts a Page struct, specifically a LoadBalancerPage struct, +// and extracts the elements into a slice of Load Balancer structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s struct { + LoadBalancers []LoadBalancer `json:"load_balancers"` + } + err := (r.(LoadBalancerPage)).ExtractInto(&s) + return s.LoadBalancers, err +} diff --git a/v3/ecl/network/v2/load_balancers/testing/doc.go b/v3/ecl/network/v2/load_balancers/testing/doc.go new file mode 100644 index 0000000..22e7d91 --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancers unit tests +package testing diff --git a/v3/ecl/network/v2/load_balancers/testing/fixtures.go b/v3/ecl/network/v2/load_balancers/testing/fixtures.go new file mode 100644 index 0000000..9b9a9a8 --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/testing/fixtures.go @@ -0,0 +1,370 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancers" +) + +const ListResponse = ` +{ + "load_balancers": [ + { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + }, + { + "admin_username": "user-admin", + "availability_zone": "zone1_groupa", + "default_gateway": null, + "description": "abcdefghijklmnopqrstuvwxyz", + "id": "601665cf-c161-4e80-87f0-a3c0925d07a0", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 2", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } + ] +} +` +const GetResponse = ` +{ + "load_balancer": { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "interfaces": [ + { + "id": "ee335c69-b50f-4a32-9d0f-f44cef84a456", + "ip_address": "100.127.253.173", + "name": "Interface 1/1", + "network_id": "c7f88fab-573e-47aa-b0b4-257db28dae23", + "slot_number": 1, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": "100.127.253.174", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "id": "b39b61e4-00b1-4698-aed0-1928beb90abe", + "ip_address": "192.168.110.1", + "name": "Interface 1/2", + "network_id": "1839d290-721c-49ba-99f1-3d7aa37811b0", + "slot_number": 2, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ], + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "syslog_servers": [ + { + "id": "11001101-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.215", + "log_facility": "LOCAL0", + "log_level": "ALERT|INFO|ERROR", + "name": "syslog_server_main", + "port_number": 514, + "status": "ACTIVE" + }, + { + "id": "22002202-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.211", + "log_facility": "LOCAL1", + "log_level": "ERROR", + "name": "syslog_server_backup_fst", + "port_number": 514, + "status": "ACTIVE" + } + ], + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } +} + ` +const CreateResponse = ` +{ + "load_balancer": { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "interfaces": [ + { + "id": "ee335c69-b50f-4a32-9d0f-f44cef84a456", + "ip_address": "100.127.253.173", + "name": "Interface 1/1", + "network_id": "c7f88fab-573e-47aa-b0b4-257db28dae23", + "slot_number": 1, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": "100.127.253.174", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "id": "b39b61e4-00b1-4698-aed0-1928beb90abe", + "ip_address": "192.168.110.1", + "name": "Interface 1/2", + "network_id": "1839d290-721c-49ba-99f1-3d7aa37811b0", + "slot_number": 2, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ], + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "syslog_servers": [ + { + "id": "11001101-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.215", + "log_facility": "LOCAL0", + "log_level": "ALERT|INFO|ERROR", + "name": "syslog_server_main", + "port_number": 514, + "status": "ACTIVE" + }, + { + "id": "22002202-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.211", + "log_facility": "LOCAL1", + "log_level": "ERROR", + "name": "syslog_server_backup_fst", + "port_number": 514, + "status": "ACTIVE" + } + ], + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } +} + ` +const CreateRequest = ` +{ + "load_balancer": { + "availability_zone": "zone1-groupa", + "description": "abcdefghijklmnopqrstuvwxyz", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "abcdefghijklmnopqrstuvwxyz", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` +const UpdateResponse = ` +{ + "load_balancer": { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "UPDATED", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "interfaces": [ + { + "id": "ee335c69-b50f-4a32-9d0f-f44cef84a456", + "ip_address": "100.127.253.173", + "name": "Interface 1/1", + "network_id": "c7f88fab-573e-47aa-b0b4-257db28dae23", + "slot_number": 1, + "status": "ACTIVE", + "virtual_ip_address": null, + "virtual_ip_properties": null + }, + { + "id": "b39b61e4-00b1-4698-aed0-1928beb90abe", + "ip_address": "192.168.110.1", + "name": "Interface 1/2", + "network_id": "1839d290-721c-49ba-99f1-3d7aa37811b0", + "slot_number": 2, + "status": "ACTIVE", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ], + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "abcdefghijklmnopqrstuvwxyz", + "status": "PENDING_UPDATE", + "syslog_servers": [ + { + "id": "11001101-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.215", + "log_facility": "LOCAL0", + "log_level": "ALERT|INFO|ERROR", + "name": "syslog_server_main", + "port_number": 514, + "status": "ACTIVE" + }, + { + "id": "22002202-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.211", + "log_facility": "LOCAL1", + "log_level": "ERROR", + "name": "syslog_server_backup_fst", + "port_number": 514, + "status": "ACTIVE" + } + ], + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } +} +` +const UpdateRequest = ` +{ + "load_balancer": { + "default_gateway": "100.127.253.1", + "description": "UPDATED", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "abcdefghijklmnopqrstuvwxyz" + } +} +` + +var LoadBalancer1 = load_balancers.LoadBalancer{ + ID: "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + AdminUsername: "user-admin", + AvailabilityZone: "zone1-groupa", + DefaultGateway: &DetailDefaultGateway, + Description: "Load Balancer 1 Description", + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "Load Balancer 1", + Status: "ACTIVE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + UserUsername: "user-read", +} + +var LoadBalancer2 = load_balancers.LoadBalancer{ + ID: "601665cf-c161-4e80-87f0-a3c0925d07a0", + AdminUsername: "user-admin", + AvailabilityZone: "zone1_groupa", + Description: "abcdefghijklmnopqrstuvwxyz", + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "Load Balancer 2", + Status: "PENDING_CREATE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + UserUsername: "user-read", +} + +var DetailDefaultGateway = "100.127.253.1" +var DetailIPAddress1 = "100.127.253.173" +var DetailNetworkID1 = "c7f88fab-573e-47aa-b0b4-257db28dae23" +var DetailVirtualIPAddress1 = "100.127.253.174" + +var DetailIPAddress2 = "192.168.110.1" +var DetailNetworkID2 = "1839d290-721c-49ba-99f1-3d7aa37811b0" + +var VirtualIPPropertiesProtocol = "vrrp" +var VirtualIPPropertiesVrid = 10 + +var LoadBalancerDetail = load_balancers.LoadBalancer{ + ID: "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + AdminUsername: "user-admin", + AvailabilityZone: "zone1-groupa", + DefaultGateway: &DetailDefaultGateway, + Description: "Load Balancer 1 Description", + Interfaces: []load_balancer_interfaces.LoadBalancerInterface{ + { + ID: "ee335c69-b50f-4a32-9d0f-f44cef84a456", + IPAddress: &DetailIPAddress1, + Name: "Interface 1/1", + NetworkID: &DetailNetworkID1, + SlotNumber: 1, + Status: "ACTIVE", + Type: "user", + VirtualIPAddress: &DetailVirtualIPAddress1, + VirtualIPProperties: &load_balancer_interfaces.VirtualIPProperties{ + Protocol: VirtualIPPropertiesProtocol, + Vrid: VirtualIPPropertiesVrid, + }, + }, + { + ID: "b39b61e4-00b1-4698-aed0-1928beb90abe", + IPAddress: &DetailIPAddress2, + Name: "Interface 1/2", + NetworkID: &DetailNetworkID2, + SlotNumber: 2, + Status: "ACTIVE", + Type: "user", + }, + }, + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "Load Balancer 1", + Status: "ACTIVE", + SyslogServers: []load_balancer_syslog_servers.LoadBalancerSyslogServer{ + { + ID: "11001101-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.215", + LogFacility: "LOCAL0", + LogLevel: "ALERT|INFO|ERROR", + Name: "syslog_server_main", + PortNumber: 514, + Status: "ACTIVE", + }, + { + ID: "22002202-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.211", + LogFacility: "LOCAL1", + LogLevel: "ERROR", + Name: "syslog_server_backup_fst", + PortNumber: 514, + Status: "ACTIVE", + }, + }, + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + UserUsername: "user-read", +} + +var ExpectedLoadBalancerSlice = []load_balancers.LoadBalancer{LoadBalancer1, LoadBalancer2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancers": [ + { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + }, + { + "admin_username": "user-admin", + "availability_zone": "zone1_groupa", + "default_gateway": null, + "description": "abcdefghijklmnopqrstuvwxyz", + "id": "601665cf-c161-4e80-87f0-a3c0925d07a0", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } + ] +} +` diff --git a/v3/ecl/network/v2/load_balancers/testing/request_test.go b/v3/ecl/network/v2/load_balancers/testing/request_test.go new file mode 100644 index 0000000..29663ab --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/testing/request_test.go @@ -0,0 +1,289 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/load_balancers" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancers.List(client, load_balancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancers.ExtractLoadBalancers(page) + if err != nil { + t.Errorf("Failed to extract Load Balancers: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/5f3cae7c-58a5-4124-b622-9ca3cfbf2525", 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, GetResponse) + }) + + s, err := load_balancers.Get(fake.ServiceClient(), "5f3cae7c-58a5-4124-b622-9ca3cfbf2525").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerDetail, s) +} + +func TestCreateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := load_balancers.CreateOpts{ + AvailabilityZone: "zone1-groupa", + Description: "abcdefghijklmnopqrstuvwxyz", + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "abcdefghijklmnopqrstuvwxyz", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + } + s, err := load_balancers.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &LoadBalancerDetail, s) +} + +func TestRequiredCreateOptsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancers.Create(fake.ServiceClient(), load_balancers.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + adminUsername := "user-admin" + availabilityZone := "zone1-groupa" + defaultGateway := interface{}("100.127.253.1") + description := "UPDATED" + id := "5f3cae7c-58a5-4124-b622-9ca3cfbf2525" + + ipAddress1 := "100.127.253.173" + networkID1 := "c7f88fab-573e-47aa-b0b4-257db28dae23" + ipAddress2 := "192.168.110.1" + networkID2 := "1839d290-721c-49ba-99f1-3d7aa37811b0" + + interfaces := []load_balancer_interfaces.LoadBalancerInterface{ + { + ID: "ee335c69-b50f-4a32-9d0f-f44cef84a456", + IPAddress: &ipAddress1, + Name: "Interface 1/1", + NetworkID: &networkID1, + SlotNumber: 1, + Status: "ACTIVE", + }, + { + ID: "b39b61e4-00b1-4698-aed0-1928beb90abe", + IPAddress: &ipAddress2, + Name: "Interface 1/2", + NetworkID: &networkID2, + SlotNumber: 2, + Status: "ACTIVE", + }, + } + + loadBalancerPlanID := "bd12784a-c66e-4f13-9f72-5143d64762b6" + name := "abcdefghijklmnopqrstuvwxyz" + status := "PENDING_UPDATE" + + syslogServers := []load_balancer_syslog_servers.LoadBalancerSyslogServer{ + { + ID: "11001101-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.215", + LogFacility: "LOCAL0", + LogLevel: "ALERT|INFO|ERROR", + Name: "syslog_server_main", + PortNumber: 514, + Status: "ACTIVE", + }, + { + ID: "22002202-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.211", + LogFacility: "LOCAL1", + LogLevel: "ERROR", + Name: "syslog_server_backup_fst", + PortNumber: 514, + Status: "ACTIVE", + }, + } + + tenantID := "6a156ddf2ecd497ca786ff2da6df5aa8" + userUsername := "user-read" + + options := load_balancers.UpdateOpts{ + DefaultGateway: &defaultGateway, + Description: &description, + LoadBalancerPlanID: loadBalancerPlanID, + Name: &name, + } + + s, err := load_balancers.Update(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, adminUsername, s.AdminUsername) + th.CheckEquals(t, availabilityZone, s.AvailabilityZone) + th.CheckEquals(t, defaultGateway, *s.DefaultGateway) + th.CheckEquals(t, description, s.Description) + th.CheckEquals(t, id, s.ID) + th.CheckDeepEquals(t, interfaces, s.Interfaces) + th.CheckEquals(t, loadBalancerPlanID, s.LoadBalancerPlanID) + th.CheckEquals(t, name, s.Name) + th.CheckEquals(t, status, s.Status) + th.CheckDeepEquals(t, syslogServers, s.SyslogServers) + th.CheckEquals(t, tenantID, s.TenantID) + th.CheckEquals(t, userUsername, s.UserUsername) +} + +func TestDeleteLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/ab49eb24-667f-4a4e-9421-b4d915bff416", 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 := load_balancers.Delete(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416") + th.AssertNoErr(t, res.Err) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "5f3cae7c-58a5-4124-b622-9ca3cfbf2525" + actualID, err := load_balancers.IDFromName(client, "Load Balancer 1") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancers.IDFromName(client, "Load Balancer X") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancers.IDFromName(client, "Load Balancer 1") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/network/v2/load_balancers/urls.go b/v3/ecl/network/v2/load_balancers/urls.go new file mode 100644 index 0000000..e39bbd4 --- /dev/null +++ b/v3/ecl/network/v2/load_balancers/urls.go @@ -0,0 +1,31 @@ +package load_balancers + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancers") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/networks/doc.go b/v3/ecl/network/v2/networks/doc.go new file mode 100644 index 0000000..e768b71 --- /dev/null +++ b/v3/ecl/network/v2/networks/doc.go @@ -0,0 +1,65 @@ +/* +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. + +Example to List Networks + + listOpts := networks.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := networks.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + panic(err) + } + + for _, network := range allNetworks { + fmt.Printf("%+v", network) + } + +Example to Create a Network + + iTrue := true + createOpts := networks.CreateOpts{ + Name: "network_1", + AdminStateUp: &iTrue, + } + + network, err := networks.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := networks.UpdateOpts{ + Name: "new_name", + } + + network, err := networks.Update(networkClient, networkID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := networks.Delete(networkClient, networkID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package networks diff --git a/v3/ecl/network/v2/networks/requests.go b/v3/ecl/network/v2/networks/requests.go new file mode 100644 index 0000000..cb0f079 --- /dev/null +++ b/v3/ecl/network/v2/networks/requests.go @@ -0,0 +1,170 @@ +package networks + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// 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 { + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + Plane string `q:"plane"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 *eclcloud.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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a network. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + Plane string `json:"plane,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToNetworkCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "network") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a network. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToNetworkUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "network") +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *eclcloud.ServiceClient, networkID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, networkID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *eclcloud.ServiceClient, networkID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, networkID), nil) + return +} + +// IDFromName is a convenience function that returns a network's ID, given +// its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractNetworks(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "network"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "network"} + } +} diff --git a/v3/ecl/network/v2/networks/results.go b/v3/ecl/network/v2/networks/results.go new file mode 100644 index 0000000..335ceb3 --- /dev/null +++ b/v3/ecl/network/v2/networks/results.go @@ -0,0 +1,120 @@ +package networks + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + var s Network + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "network") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Network. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Network. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Network. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // The administrative state of network. If false (down), the network does not + // forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Description is the description of the network. + Description string `json:"description"` + + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // Plane it the ype of the traffic for which network will be used. + Plane string `json:"plane"` + + // Specifies whether the network resource can be accessed by any tenant. + Shared bool `json:"shared"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // Tags optionally set via extensions/attributestags + Tags map[string]string `json:"tags"` + + // TenantID is the project owner of the network. + TenantID string `json:"tenant_id"` +} + +// 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 (r NetworkPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"networks_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r NetworkPage) IsEmpty() (bool, error) { + is, err := ExtractNetworks(r) + return len(is) == 0, err +} + +// 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(r pagination.Page) ([]Network, error) { + var s []Network + err := ExtractNetworksInto(r, &s) + return s, err +} + +func ExtractNetworksInto(r pagination.Page, v interface{}) error { + return r.(NetworkPage).Result.ExtractIntoSlicePtr(v, "networks") +} diff --git a/v3/ecl/network/v2/networks/testing/doc.go b/v3/ecl/network/v2/networks/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v3/ecl/network/v2/networks/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v3/ecl/network/v2/networks/testing/fixtures.go b/v3/ecl/network/v2/networks/testing/fixtures.go new file mode 100644 index 0000000..887c421 --- /dev/null +++ b/v3/ecl/network/v2/networks/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/networks" +) + +const ListResponse = ` +{ + "networks": [ + { + "admin_state_up": true, + "description": "", + "id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "name": "", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [ + "ab49eb24-667f-4a4e-9421-b4d915bff416", + "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678" + ], + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + }, + { + "admin_state_up": true, + "description": "Example Network 2", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "Example Network 2", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [], + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + ] + }` +const GetResponse = `{ + "network": { + "admin_state_up": true, + "description": "Example Network 2", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "Example Network 2", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [], + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const CreateResponse = ` +{ + "network": { + "admin_state_up": true, + "description": "Example Network 2", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "Example Network 2", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [], + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const CreateRequest = ` +{ + "network": { + "admin_state_up": true, + "description": "Example Network 2", + "name": "Example Network 2", + "plane": "data", + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` +const UpdateResponse = ` +{ + "network": { + "admin_state_up": false, + "description": "UPDATED", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "UPDATED", + "plane": "data", + "shared": false, + "status": "PENDING_UPDATE", + "subnets": [], + "tags": { + "keyword1": "UPDATED", + "keyword3": "CREATED" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const UpdateRequest = ` +{ + "network": { + "admin_state_up": false, + "description": "UPDATED", + "name": "UPDATED", + "tags": { + "keyword1": "UPDATED", + "keyword3": "CREATED" + } + } + }` + +var Network1 = networks.Network{ + AdminStateUp: true, + Description: "", + ID: "8f36b88a-443f-4d97-9751-34d34af9e782", + Name: "", + Plane: "data", + Shared: false, + Status: "ACTIVE", + Subnets: []string{ + "ab49eb24-667f-4a4e-9421-b4d915bff416", + "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + }, + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var Network2 = networks.Network{ + AdminStateUp: true, + Description: "Example Network 2", + ID: "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + Name: "Example Network 2", + Plane: "data", + Shared: false, + Status: "ACTIVE", + Subnets: []string{}, + Tags: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var ExpectedNetworkSlice = []networks.Network{Network1, Network2} diff --git a/v3/ecl/network/v2/networks/testing/request_test.go b/v3/ecl/network/v2/networks/testing/request_test.go new file mode 100644 index 0000000..76fa0a0 --- /dev/null +++ b/v3/ecl/network/v2/networks/testing/request_test.go @@ -0,0 +1,164 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/networks" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListNetwork(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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + networks.List(client, networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := networks.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extrace ports: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", 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, GetResponse) + }) + + n, err := networks.Get(fake.ServiceClient(), "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Network2, n) +} + +func TestCreateNetwork(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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + asu := true + + options := &networks.CreateOpts{ + AdminStateUp: &asu, + Description: "Example Network 2", + Name: "Example Network 2", + Plane: "data", + Tags: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", + } + n, err := networks.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &Network2, n) +} + +func TestRequiredCreateOptsNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := networks.Create(fake.ServiceClient(), networks.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + asu := false + description := "UPDATED" + name := "UPDATED" + tags := map[string]string{ + "keyword1": "UPDATED", + "keyword3": "CREATED", + } + + options := &networks.UpdateOpts{ + AdminStateUp: &asu, + Description: &description, + Name: &name, + Tags: &tags, + } + n, err := networks.Update(fake.ServiceClient(), "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, asu, n.AdminStateUp) + th.CheckEquals(t, description, n.Description) + th.CheckEquals(t, name, n.Name) + th.CheckDeepEquals(t, tags, n.Tags) +} + +func TestDeleteNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", 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 := networks.Delete(fake.ServiceClient(), "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/networks/urls.go b/v3/ecl/network/v2/networks/urls.go new file mode 100644 index 0000000..4dbf8e0 --- /dev/null +++ b/v3/ecl/network/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/ports/doc.go b/v3/ecl/network/v2/ports/doc.go new file mode 100644 index 0000000..cfb1774 --- /dev/null +++ b/v3/ecl/network/v2/ports/doc.go @@ -0,0 +1,73 @@ +/* +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. + +Example to List Ports + + listOpts := ports.ListOpts{ + DeviceID: "b0b89efe-82f8-461d-958b-adbf80f50c7d", + } + + allPages, err := ports.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPorts, err := ports.ExtractPorts(allPages) + if err != nil { + panic(err) + } + + for _, port := range allPorts { + fmt.Printf("%+v\n", port) + } + +Example to Create a Port + + createOtps := ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: &[]string{"foo"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + port, err := ports.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + + updateOpts := ports.UpdateOpts{ + Name: "new_name", + SecurityGroups: &[]string{}, + } + + port, err := ports.Update(networkClient, portID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + err := ports.Delete(networkClient, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package ports diff --git a/v3/ecl/network/v2/ports/requests.go b/v3/ecl/network/v2/ports/requests.go new file mode 100644 index 0000000..1cfcce2 --- /dev/null +++ b/v3/ecl/network/v2/ports/requests.go @@ -0,0 +1,186 @@ +package ports + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// 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 { + Description string `q:"description"` + DeviceID string `q:"device_id"` + DeviceOwner string `q:"device_owner"` + ID string `q:"id"` + MACAddress string `q:"mac_address"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + SegmentationID int `q:"segmentation_id"` + SegmentationType string `q:"segmentation_type"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 *eclcloud.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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + Description string `json:"description,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + MACAddress string `json:"mac_address,omitempty"` + Name string `json:"name,omitempty"` + NetworkID string `json:"network_id"` + SegmentationID int `json:"segmentation_id,omitempty"` + SegmentationType string `json:"segmentation_type,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToPortCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "port") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + AllowedAddressPairs *[]AddressPair `json:"allowed_address_pairs,omitempty"` + Description *string `json:"description,omitempty"` + DeviceID *string `json:"device_id,omitempty"` + DeviceOwner *string `json:"device_owner,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + Name *string `json:"name,omitempty"` + SegmentationID *int `json:"segmentation_id,omitempty"` + SegmentationType *string `json:"segmentation_type,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToPortUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "port") +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a port's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractPorts(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "port"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "port"} + } +} diff --git a/v3/ecl/network/v2/ports/results.go b/v3/ecl/network/v2/ports/results.go new file mode 100644 index 0000000..c2f50c8 --- /dev/null +++ b/v3/ecl/network/v2/ports/results.go @@ -0,0 +1,152 @@ +package ports + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s Port + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "port") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Port. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Port. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // Administrative state of port. If false (down), port does not forward + // packets. + AdminStateUp bool `json:"admin_state_up"` + + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` + + // Description is description + Description string `json:"description"` + + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `json:"device_id"` + + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `json:"device_owner"` + + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `json:"fixed_ips"` + + // UUID for the port. + ID string `json:"id"` + + // Mac address to use on this port. + MACAddress string `json:"mac_address"` + + // ManagedByService is set to true if only admin can modify it. Normal user has only read access. + ManagedByService bool `json:"managed_by_service"` + + // Human-readable name for the port. Might not be unique. + Name string `json:"name"` + + // Network that this port is associated with. + NetworkID string `json:"network_id"` + + // SegmentationID is the segmenation ID used for this port (i.e. for vlan type it is vlan tag) + SegmentationID int `json:"segmentation_id"` + + // SegmenationType is the segmentation type used for this port (i.e. vlan) + SegmentationType string `json:"segmentation_type"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Tags optionally set via extensions/attributestags + Tags map[string]string `json:"tags"` + + // TenantID is the project owner of the port. + TenantID string `json:"tenant_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 (r PortPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"ports_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (r PortPage) IsEmpty() (bool, error) { + is, err := ExtractPorts(r) + return len(is) == 0, err +} + +// 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(r pagination.Page) ([]Port, error) { + var s []Port + err := ExtractPortsInto(r, &s) + return s, err +} + +func ExtractPortsInto(r pagination.Page, v interface{}) error { + return r.(PortPage).Result.ExtractIntoSlicePtr(v, "ports") +} diff --git a/v3/ecl/network/v2/ports/testing/doc.go b/v3/ecl/network/v2/ports/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v3/ecl/network/v2/ports/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v3/ecl/network/v2/ports/testing/fixtures.go b/v3/ecl/network/v2/ports/testing/fixtures.go new file mode 100644 index 0000000..c622b71 --- /dev/null +++ b/v3/ecl/network/v2/ports/testing/fixtures.go @@ -0,0 +1,290 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/ports" +) + +const ListResponse = ` +{ + "ports": [ + { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "DHCP Server Port", + "device_id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "device_owner": "network:dhcp", + "fixed_ips": [ + { + "ip_address": "192.168.2.2", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "8db1ba30-be40-4943-a7be-ed5b98f053b3", + "mac_address": "00:00:5e:00:01:00", + "managed_by_service": false, + "name": "dhcp-server-port", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": null, + "segmentation_type": null, + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + }, + { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "managed_by_service": false, + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + ] + }` +const GetResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "managed_by_service": false, + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` + +const CreateResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const CreateRequest = ` +{ + "port": + { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1", + "segmentation_type": "flat" + } +}` +const UpdateResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + },{ + "ip_address": "192.168.2.255", + "mac_address": "26:8d:42:f6:c2:c4" + }], + "description": "UPDATED", + "device_id": "b269b8c0-1a42-4464-9314-4396e51e5107", + "device_owner": "UPDATED", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + }, { + "ip_address": "192.168.2.31", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff417" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "name": "UPDATED", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 2, + "segmentation_type": "vlan", + "status": "PENDING_CREATE", + "tags": { + "some-key":"UPDATED" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` + +const UpdateRequest = ` +{ + "port": { + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + },{ + "ip_address": "192.168.2.255", + "mac_address": "26:8d:42:f6:c2:c4" + }], + "description": "UPDATED", + "device_id": "b269b8c0-1a42-4464-9314-4396e51e5107", + "device_owner": "UPDATED", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + }, { + "ip_address": "192.168.2.31", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff417" + } + ], + "name": "UPDATED", + "segmentation_id": 2, + "segmentation_type": "vlan", + "tags": { + "some-key":"UPDATED" + } + } + }` + +const RemoveAllowedAddressPairsRequest = ` +{ + "port": { + "allowed_address_pairs": [], + "name": "new_port_name" + } + } +` + +const RemoveAllowedAddressPairsResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "name": "new_port_name", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` + +var Port1 = ports.Port{ + AdminStateUp: true, + AllowedAddressPairs: []ports.AddressPair{}, + Description: "DHCP Server Port", + DeviceID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + DeviceOwner: "network:dhcp", + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.2", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + ID: "8db1ba30-be40-4943-a7be-ed5b98f053b3", + MACAddress: "00:00:5e:00:01:00", + ManagedByService: false, + Name: "dhcp-server-port", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var Port2 = ports.Port{ + AdminStateUp: true, + AllowedAddressPairs: []ports.AddressPair{{ + IPAddress: "192.168.2.100", + MACAddress: "00:00:5e:00:01:01", + }}, + Description: "", + DeviceID: "", + DeviceOwner: "", + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.30", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + ID: "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + MACAddress: "fa:16:3e:b0:ca:f1", + ManagedByService: false, + Name: "port_12", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + SegmentationID: 0, + SegmentationType: "flat", + Status: "PENDING_CREATE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var ExpectedPortSlice = []ports.Port{Port1, Port2} diff --git a/v3/ecl/network/v2/ports/testing/request_test.go b/v3/ecl/network/v2/ports/testing/request_test.go new file mode 100644 index 0000000..0fb45d8 --- /dev/null +++ b/v3/ecl/network/v2/ports/testing/request_test.go @@ -0,0 +1,221 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/ports" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListPort(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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + ports.List(client, ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ports.ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extrace ports: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedPortSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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, GetResponse) + }) + + p, err := ports.Get(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Port2, p) +} + +func TestCreatePort(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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + asu := true + + options := &ports.CreateOpts{ + AdminStateUp: &asu, + AllowedAddressPairs: []ports.AddressPair{{ + IPAddress: "192.168.2.100", + MACAddress: "00:00:5e:00:01:01", + }}, + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.30", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + Name: "port_12", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", + SegmentationType: "flat", + } + p, err := ports.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &Port2, p) +} + +func TestRequiredCreateOptsPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := ports.Create(fake.ServiceClient(), ports.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} +func TestUpdatePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + aap := []ports.AddressPair{{ + IPAddress: "192.168.2.100", + MACAddress: "00:00:5e:00:01:01", + }, { + IPAddress: "192.168.2.255", + MACAddress: "26:8d:42:f6:c2:c4", + }} + description := "UPDATED" + deviceID := "b269b8c0-1a42-4464-9314-4396e51e5107" + deviceOwner := "UPDATED" + fip := []ports.IP{{ + IPAddress: "192.168.2.30", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }, { + IPAddress: "192.168.2.31", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff417", + }} + name := "UPDATED" + segmentationID := 2 + segmentationType := "vlan" + tags := map[string]string{"some-key": "UPDATED"} + + options := ports.UpdateOpts{ + AllowedAddressPairs: &aap, + Description: &description, + DeviceID: &deviceID, + DeviceOwner: &deviceOwner, + FixedIPs: fip, + Name: &name, + SegmentationID: &segmentationID, + SegmentationType: &segmentationType, + Tags: &tags, + } + p, err := ports.Update(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", options).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, aap, p.AllowedAddressPairs) + th.CheckEquals(t, description, p.Description) + th.CheckEquals(t, deviceID, p.DeviceID) + th.CheckEquals(t, deviceOwner, p.DeviceOwner) + th.CheckDeepEquals(t, fip, p.FixedIPs) + th.CheckEquals(t, name, p.Name) + th.CheckEquals(t, segmentationID, p.SegmentationID) + th.CheckEquals(t, segmentationType, p.SegmentationType) + th.CheckDeepEquals(t, tags, p.Tags) +} + +func TestRemoveAllowedAddressPairs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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, RemoveAllowedAddressPairsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, RemoveAllowedAddressPairsResponse) + }) + + name := "new_port_name" + options := ports.UpdateOpts{ + Name: &name, + AllowedAddressPairs: &[]ports.AddressPair{}, + } + + s, err := ports.Update(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{}) +} + +func TestDeletePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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 := ports.Delete(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/ports/urls.go b/v3/ecl/network/v2/ports/urls.go new file mode 100644 index 0000000..958f39a --- /dev/null +++ b/v3/ecl/network/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/public_ips/doc.go b/v3/ecl/network/v2/public_ips/doc.go new file mode 100644 index 0000000..eff1dea --- /dev/null +++ b/v3/ecl/network/v2/public_ips/doc.go @@ -0,0 +1 @@ +package public_ips diff --git a/v3/ecl/network/v2/public_ips/requests.go b/v3/ecl/network/v2/public_ips/requests.go new file mode 100644 index 0000000..a991f79 --- /dev/null +++ b/v3/ecl/network/v2/public_ips/requests.go @@ -0,0 +1,136 @@ +package public_ips + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type ListOptsBuilder interface { + ToPublicIPListQuery() (string, error) +} + +type ListOpts struct { + Cidr string `q:"cidr"` + Description string `q:"description"` + ID string `q:"id"` + InternetGwID string `q:"internet_gw_id"` + Name string `q:"name"` + Status string `q:"status"` + SubmaskLength int `q:"submask_length"` + TenantID string `q:"tenant_id"` +} + +func (opts ListOpts) ToPublicIPListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPublicIPListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PublicIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, publicIPID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, publicIPID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToPublicIPCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + Description string `json:"description,omitempty"` + InternetGwID string `json:"internet_gw_id" required:"true"` + Name string `json:"name,omitempty"` + SubmaskLength int `json:"submask_length" required:"true"` + TenantID string `json:"tenant_id,omitempty"` +} + +func (opts CreateOpts) ToPublicIPCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "public_ip") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPublicIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToPublicIPUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` +} + +func (opts UpdateOpts) ToPublicIPUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "public_ip") +} + +func Update(c *eclcloud.ServiceClient, publicIPID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPublicIPUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, publicIPID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, publicIPID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, publicIPID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractPublicIPs(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "public_ip"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "public_ip"} + } +} diff --git a/v3/ecl/network/v2/public_ips/results.go b/v3/ecl/network/v2/public_ips/results.go new file mode 100644 index 0000000..9861312 --- /dev/null +++ b/v3/ecl/network/v2/public_ips/results.go @@ -0,0 +1,77 @@ +package public_ips + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*PublicIP, error) { + var s PublicIP + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "public_ip") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type PublicIP struct { + Cidr string `json:"cidr"` + Description string `json:"description"` + ID string `json:"id"` + InternetGwID string `json:"internet_gw_id"` + Name string `json:"name"` + Status string `json:"status"` + SubmaskLength int `json:"submask_length"` + TenantID string `json:"tenant_id"` +} + +type PublicIPPage struct { + pagination.LinkedPageBase +} + +func (r PublicIPPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"public_ips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r PublicIPPage) IsEmpty() (bool, error) { + is, err := ExtractPublicIPs(r) + return len(is) == 0, err +} + +func ExtractPublicIPs(r pagination.Page) ([]PublicIP, error) { + var s []PublicIP + err := ExtractPublicIPsInto(r, &s) + return s, err +} + +func ExtractPublicIPsInto(r pagination.Page, v interface{}) error { + return r.(PublicIPPage).Result.ExtractIntoSlicePtr(v, "public_ips") +} diff --git a/v3/ecl/network/v2/public_ips/testing/doc.go b/v3/ecl/network/v2/public_ips/testing/doc.go new file mode 100644 index 0000000..ab98250 --- /dev/null +++ b/v3/ecl/network/v2/public_ips/testing/doc.go @@ -0,0 +1,2 @@ +// public_ips unit tests +package testing diff --git a/v3/ecl/network/v2/public_ips/testing/fixtures.go b/v3/ecl/network/v2/public_ips/testing/fixtures.go new file mode 100644 index 0000000..ac5a8b7 --- /dev/null +++ b/v3/ecl/network/v2/public_ips/testing/fixtures.go @@ -0,0 +1,121 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/public_ips" +) + +const ListResponse = ` +{ + "public_ips": [ + { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_CREATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + }, + { + "cidr": "100.127.254.56", + "description": "", + "id": "110846c3-3a20-42ff-ad3d-25ba7b0272bb", + "internet_gw_id": "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + "name": "6_Public", + "status": "ACTIVE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } + ] +} +` + +const GetResponse = ` +{ + "public_ip": { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_CREATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +const CreateRequest = ` +{ + "public_ip": { + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +const CreateResponse = ` +{ + "public_ip": { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_CREATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +const UpdateRequest = ` +{ + "public_ip": { + "name": "seinou-test-public", + "description": "" + } +} + ` + +const UpdateResponse = ` +{ + "public_ip": { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_UPDATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +var PublicIP1 = public_ips.PublicIP{ + Cidr: "100.127.255.80", + Description: "", + ID: "0718a31b-67be-4349-946b-61a0fc38e4cd", + InternetGwID: "2a75cfa6-89af-425b-bce5-2a85197ef04f", + Name: "seinou-test-public", + Status: "PENDING_CREATE", + SubmaskLength: 29, + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", +} + +var PublicIP2 = public_ips.PublicIP{ + Cidr: "100.127.254.56", + Description: "", + ID: "110846c3-3a20-42ff-ad3d-25ba7b0272bb", + InternetGwID: "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + Name: "6_Public", + Status: "ACTIVE", + SubmaskLength: 29, + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", +} + +var ExpectedPublicIPSlice = []public_ips.PublicIP{PublicIP1, PublicIP2} diff --git a/v3/ecl/network/v2/public_ips/testing/request_test.go b/v3/ecl/network/v2/public_ips/testing/request_test.go new file mode 100644 index 0000000..40efed5 --- /dev/null +++ b/v3/ecl/network/v2/public_ips/testing/request_test.go @@ -0,0 +1,143 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/public_ips" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/public_ips", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := public_ips.List(client, public_ips.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := public_ips.ExtractPublicIPs(page) + if err != nil { + t.Errorf("Failed to extract public ips: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedPublicIPSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + 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/public_ips/0718a31b-67be-4349-946b-61a0fc38e4cd", 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, GetResponse) + }) + + i, err := public_ips.Get(fake.ServiceClient(), "0718a31b-67be-4349-946b-61a0fc38e4cd").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &PublicIP1, i) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := public_ips.CreateOpts{ + Name: "seinou-test-public", + Description: "", + InternetGwID: "2a75cfa6-89af-425b-bce5-2a85197ef04f", + SubmaskLength: 29, + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", + } + i, err := public_ips.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Status, "PENDING_CREATE") + th.AssertDeepEquals(t, &PublicIP1, i) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/public_ips/0718a31b-67be-4349-946b-61a0fc38e4cd", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := "seinou-test-public" + description := "" + options := public_ips.UpdateOpts{Name: &name, Description: &description} + i, err := public_ips.Update(fake.ServiceClient(), "0718a31b-67be-4349-946b-61a0fc38e4cd", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "seinou-test-public") + th.AssertEquals(t, i.Description, "") + th.AssertEquals(t, i.ID, "0718a31b-67be-4349-946b-61a0fc38e4cd") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/public_ips/0718a31b-67be-4349-946b-61a0fc38e4cd", 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 := public_ips.Delete(fake.ServiceClient(), "0718a31b-67be-4349-946b-61a0fc38e4cd") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/public_ips/urls.go b/v3/ecl/network/v2/public_ips/urls.go new file mode 100644 index 0000000..7aca340 --- /dev/null +++ b/v3/ecl/network/v2/public_ips/urls.go @@ -0,0 +1,31 @@ +package public_ips + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("public_ips", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("public_ips") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/qos_options/doc.go b/v3/ecl/network/v2/qos_options/doc.go new file mode 100644 index 0000000..cd8de8c --- /dev/null +++ b/v3/ecl/network/v2/qos_options/doc.go @@ -0,0 +1,35 @@ +/* +Package qos_options provides information of several service +in the Enterprise Cloud Compute service + +Example to List QoS Options + + listOpts := qos_options.ListOpts{ + QoSType: "guarantee", + } + + allPages, err := qos_options.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allQoSOptions, err := qos_options.ExtractQoSOptions(allPages) + if err != nil { + panic(err) + } + + for _, qosOption := range allQoSOptions { + fmt.Printf("%+v", qosOption) + } + +Example to Show QoS Option + + id := "02dc9a22-129c-4b12-9936-4080f6a7ae44" + qosOption, err := qos_options.Get(client, id).Extract() + if err != nil { + panic(err) + } + fmt.Print(qosOption) + +*/ +package qos_options diff --git a/v3/ecl/network/v2/qos_options/requests.go b/v3/ecl/network/v2/qos_options/requests.go new file mode 100644 index 0000000..fef4f7d --- /dev/null +++ b/v3/ecl/network/v2/qos_options/requests.go @@ -0,0 +1,87 @@ +package qos_options + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToQosOptionsListQuery() (string, error) +} + +// ListOpts allows the filtering of paginated collections through the API. +// Filtering is achieved by passing in struct field values that map to +// the QoS option attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // Unique ID for the AWSService. + AWSServiceID string `q:"aws_service_id"` + + // Unique ID for the AzureService. + AzureServiceID string `q:"azure_service_id"` + + // Bandwidth assigned with this QoS Option + Bandwidth string `q:"bandwidth"` + + // Description is the description of the QoS Policy. + Description string `q:"description"` + + // Unique ID for the FICService. + FICServiceID string `q:"fic_service_id"` + + // Unique ID for the GCPService. + GCPServiceID string `q:"gcp_service_id"` + + // Unique ID for the QoS option. + ID string `q:"id"` + + // Unique ID for the InterDCService. + InterDCServiceID string `q:"interdc_service_id"` + + // Unique ID for the InternetService. + InternetServiceID string `q:"internet_service_id"` + + // Name is the name of the QoS option. + Name string `q:"name"` + + // Type of the QoS option.(guarantee or besteffort) + QoSType string `q:"qos_type"` + + // Service type of the QoS option.(aws, azure, fic, gcp, vpn, internet, interdc) + ServiceType string `q:"service_type"` + + // Indicates whether QoS option is currently operational. + Status string `q:"status"` + + // Unique ID for the VPNService. + VPNServiceID string `q:"vpn_service_id"` +} + +// ToQosOptionsListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToQosOptionsListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list QoS options accessible to you. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToQosOptionsListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return QosOptionPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific QoS option based on its unique ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/v3/ecl/network/v2/qos_options/results.go b/v3/ecl/network/v2/qos_options/results.go new file mode 100644 index 0000000..4fe3c61 --- /dev/null +++ b/v3/ecl/network/v2/qos_options/results.go @@ -0,0 +1,60 @@ +package qos_options + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type QosOptionPage struct { + pagination.LinkedPageBase +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a QoSOption. +type GetResult struct { + commonResult +} + +// QoSOption represents a QoS option. +type QoSOption struct { + AWSServiceID string `json:"aws_service_id"` + AzureServiceID string `json:"azure_service_id"` + Bandwidth string `json:"bandwidth"` + Description string `json:"description"` + FICServiceID string `json:"fic_service_id"` + GCPServiceID string `json:"gcp_service_id"` + ID string `json:"id"` + InterDCServiceID string `json:"interdc_service_id"` + InternetServiceID string `json:"internet_service_id"` + Name string `json:"name"` + QoSType string `json:"qos_type"` + ServiceType string `json:"service_type"` + Status string `json:"status"` + VPNServiceID string `json:"vpn_service_id"` +} + +// IsEmpty checks whether a QosOptionPage struct is empty. +func (r QosOptionPage) IsEmpty() (bool, error) { + is, err := ExtractQoSOptions(r) + return len(is) == 0, err +} + +// ExtractQoSOptions accepts a Page struct, specifically a QoSOptionPage struct, +// and extracts the elements into a slice of ListOpts structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractQoSOptions(r pagination.Page) ([]QoSOption, error) { + var s []QoSOption + err := r.(QosOptionPage).Result.ExtractIntoSlicePtr(&s, "qos_options") + return s, err +} + +// Extract is a function that accepts a result and extracts a QoSOption. +func (r GetResult) Extract() (*QoSOption, error) { + var l QoSOption + err := r.Result.ExtractIntoStructPtr(&l, "qos_option") + return &l, err +} diff --git a/v3/ecl/network/v2/qos_options/testing/doc.go b/v3/ecl/network/v2/qos_options/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v3/ecl/network/v2/qos_options/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v3/ecl/network/v2/qos_options/testing/fixtures.go b/v3/ecl/network/v2/qos_options/testing/fixtures.go new file mode 100644 index 0000000..fc9df64 --- /dev/null +++ b/v3/ecl/network/v2/qos_options/testing/fixtures.go @@ -0,0 +1,99 @@ +package testing + +import "github.com/nttcom/eclcloud/v3/ecl/network/v2/qos_options" + +const ListResponse = ` +{ + "qos_options": [ + { + "aws_service_id" : null, + "azure_service_id" : "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "bandwidth" : "20", + "description" : "20M-guarantee-menu-for-azure", + "fic_service_id" : null, + "gcp_service_id" : null, + "id" : "a6b91294-8870-4f2c-b9e9-a899acada723", + "interdc_service_id" : null, + "internet_service_id" : null, + "name" : "20M-GA-AZURE", + "qos_type" : "guarantee", + "service_type" : "azure", + "status" : "ACTIVE", + "vpn_service_id" : null + }, + { + "aws_service_id" : null, + "azure_service_id" : "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "bandwidth" : "500", + "description" : "500M-guarantee-menu-for-azure", + "fic_service_id" : null, + "gcp_service_id" : null, + "id" : "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + "interdc_service_id" : null, + "internet_service_id" : null, + "name" : "500M-GA-AZURE", + "qos_type" : "guarantee", + "service_type" : "azure", + "status" : "ACTIVE", + "vpn_service_id" : null + } + ] +} +` + +const GetResponse = ` +{ + "qos_option": { + "aws_service_id" : null, + "azure_service_id" : "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "bandwidth" : "20", + "description" : "20M-guarantee-menu-for-azure", + "fic_service_id" : null, + "gcp_service_id" : null, + "id" : "a6b91294-8870-4f2c-b9e9-a899acada723", + "interdc_service_id" : null, + "internet_service_id" : null, + "name" : "20M-GA-AZURE", + "qos_type" : "guarantee", + "service_type" : "azure", + "status" : "ACTIVE", + "vpn_service_id" : null + } +} +` + +var Qos1 = qos_options.QoSOption{ + AWSServiceID: "", + AzureServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + Bandwidth: "20", + Description: "20M-guarantee-menu-for-azure", + FICServiceID: "", + GCPServiceID: "", + ID: "a6b91294-8870-4f2c-b9e9-a899acada723", + InterDCServiceID: "", + InternetServiceID: "", + Name: "20M-GA-AZURE", + QoSType: "guarantee", + ServiceType: "azure", + Status: "ACTIVE", + VPNServiceID: "", +} + +var Qos2 = qos_options.QoSOption{ + AWSServiceID: "", + AzureServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + Bandwidth: "500", + Description: "500M-guarantee-menu-for-azure", + FICServiceID: "", + GCPServiceID: "", + ID: "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + InterDCServiceID: "", + InternetServiceID: "", + Name: "500M-GA-AZURE", + QoSType: "guarantee", + ServiceType: "azure", + Status: "ACTIVE", + VPNServiceID: "", +} + +var ExpectedQosSlice = []qos_options.QoSOption{Qos1, Qos2} diff --git a/v3/ecl/network/v2/qos_options/testing/request_test.go b/v3/ecl/network/v2/qos_options/testing/request_test.go new file mode 100644 index 0000000..8f3589a --- /dev/null +++ b/v3/ecl/network/v2/qos_options/testing/request_test.go @@ -0,0 +1,68 @@ +package testing + +import ( + "fmt" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/qos_options" + "github.com/nttcom/eclcloud/v3/pagination" + + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListQoS(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/qos_options", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + qos_options.List(client, qos_options.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := qos_options.ExtractQoSOptions(page) + if err != nil { + t.Errorf("Failed to extract QoS options: %v", err) + return false, nil + } + th.CheckDeepEquals(t, ExpectedQosSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetQoS(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + id := "2c649b8e-f007-4d90-b208-9b8710937a94" + th.Mux.HandleFunc(fmt.Sprintf("/v2.0/qos_options/%s", 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, GetResponse) + }) + + n, err := qos_options.Get(fake.ServiceClient(), id).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Qos1, n) +} diff --git a/v3/ecl/network/v2/qos_options/urls.go b/v3/ecl/network/v2/qos_options/urls.go new file mode 100644 index 0000000..b651e3e --- /dev/null +++ b/v3/ecl/network/v2/qos_options/urls.go @@ -0,0 +1,11 @@ +package qos_options + +import "github.com/nttcom/eclcloud/v3" + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("qos_options", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("qos_options") +} diff --git a/v3/ecl/network/v2/static_routes/doc.go b/v3/ecl/network/v2/static_routes/doc.go new file mode 100644 index 0000000..00ca023 --- /dev/null +++ b/v3/ecl/network/v2/static_routes/doc.go @@ -0,0 +1 @@ +package static_routes diff --git a/v3/ecl/network/v2/static_routes/requests.go b/v3/ecl/network/v2/static_routes/requests.go new file mode 100644 index 0000000..aa425ee --- /dev/null +++ b/v3/ecl/network/v2/static_routes/requests.go @@ -0,0 +1,151 @@ +package static_routes + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type ListOptsBuilder interface { + ToStaticRouteListQuery() (string, error) +} + +type ListOpts struct { + AwsGwID string `q:"aws_gw_id"` + AzureGwID string `q:"azure_gw_id"` + Description string `q:"description"` + Destination string `q:"destination"` + FICGatewayID string `q:"fic_gw_id"` + GcpGwID string `q:"gcp_gw_id"` + ID string `q:"id"` + InterdcGwID string `q:"inter_dc_id"` + InternetGwID string `q:"internet_gw_id"` + Name string `q:"name"` + Nexthop string `q:"nexthop"` + ServiceType string `q:"service_type"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + VpnGwID string `q:"vpn_gw_id"` +} + +func (opts ListOpts) ToStaticRouteListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToStaticRouteListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return StaticRoutePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, publicIPID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, publicIPID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToStaticRouteCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + AwsGwID string `json:"aws_gw_id,omitempty"` + AzureGwID string `json:"azure_gw_id,omitempty"` + Description string `json:"description"` + Destination string `json:"destination" required:"true"` + FICGatewayID string `json:"fic_gw_id,omitempty"` + GcpGwID string `json:"gcp_gw_id,omitempty"` + InterdcGwID string `json:"inter_dc_id,omitempty"` + InternetGwID string `json:"internet_gw_id,omitempty"` + Name string `json:"name"` + Nexthop string `json:"nexthop" required:"true"` + ServiceType string `json:"service_type" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + VpnGwID string `json:"vpn_gw_id,omitempty"` +} + +func (opts CreateOpts) ToStaticRouteCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "static_route") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToStaticRouteCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToStaticRouteUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` +} + +func (opts UpdateOpts) ToStaticRouteUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "static_route") +} + +func Update(c *eclcloud.ServiceClient, publicIPID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToStaticRouteUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, publicIPID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, publicIPID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, publicIPID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractStaticRoutes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "static_route"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "static_route"} + } +} diff --git a/v3/ecl/network/v2/static_routes/results.go b/v3/ecl/network/v2/static_routes/results.go new file mode 100644 index 0000000..b64afda --- /dev/null +++ b/v3/ecl/network/v2/static_routes/results.go @@ -0,0 +1,84 @@ +package static_routes + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*StaticRoute, error) { + var s StaticRoute + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "static_route") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type StaticRoute struct { + AwsGwID string `json:"aws_gw_id"` + AzureGwID string `json:"azure_gw_id"` + Description string `json:"description"` + Destination string `json:"destination"` + FICGatewayID string `json:"fic_gw_id"` + GcpGwID string `json:"gcp_gw_id"` + ID string `json:"id"` + InterdcGwID string `json:"interdc_gw_id"` + InternetGwID string `json:"internet_gw_id"` + Name string `json:"name"` + Nexthop string `json:"nexthop"` + ServiceType string `json:"service_type"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + VpnGwID string `json:"vpn_gw_id"` +} + +type StaticRoutePage struct { + pagination.LinkedPageBase +} + +func (r StaticRoutePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"static_routes_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r StaticRoutePage) IsEmpty() (bool, error) { + is, err := ExtractStaticRoutes(r) + return len(is) == 0, err +} + +func ExtractStaticRoutes(r pagination.Page) ([]StaticRoute, error) { + var s []StaticRoute + err := ExtractStaticRoutesInto(r, &s) + return s, err +} + +func ExtractStaticRoutesInto(r pagination.Page, v interface{}) error { + return r.(StaticRoutePage).Result.ExtractIntoSlicePtr(v, "static_routes") +} diff --git a/v3/ecl/network/v2/static_routes/testing/doc.go b/v3/ecl/network/v2/static_routes/testing/doc.go new file mode 100644 index 0000000..ab98250 --- /dev/null +++ b/v3/ecl/network/v2/static_routes/testing/doc.go @@ -0,0 +1,2 @@ +// public_ips unit tests +package testing diff --git a/v3/ecl/network/v2/static_routes/testing/fixtures.go b/v3/ecl/network/v2/static_routes/testing/fixtures.go new file mode 100644 index 0000000..8bf3357 --- /dev/null +++ b/v3/ecl/network/v2/static_routes/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/static_routes" +) + +const ListResponse = ` +{ + "static_routes": [ + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "gcp_gw_id": null, + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + }, + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "StaticRoute for Scenario-test.", + "destination": "100.127.255.184/29", + "fic_gw_id": "1331e6a7-2876-4d34-b12f-5aac9517b034", + "gcp_gw_id": null, + "id": "e58162ca-9fef-4f27-898f-af0d495b780c", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "StaticRoute_INGW_02_01", + "nexthop": "100.127.255.189", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + } + ] +} +` + +const GetResponse = ` +{ + "static_route": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "gcp_gw_id": null, + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + } +} +` + +const CreateRequest = ` +{ + "static_route": { + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` + +const CreateResponse = ` +{ + "static_route": { + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` + +const UpdateRequest = ` +{ + "static_route": { + "description": "SRT2", + "name": "SRT2" + } +} + ` + +const UpdateResponse = ` +{ + "static_route": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "gcp_gw_id": null, + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_UPDATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + } +} +` + +var StaticRoute1 = static_routes.StaticRoute{ + Description: "SRT2", + Destination: "100.127.254.116/30", + FICGatewayID: "5af4f343-91a7-4956-aabb-9ac678d215e5", + ID: "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + Name: "SRT2", + Nexthop: "100.127.254.117", + ServiceType: "fic", + Status: "PENDING_CREATE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var StaticRoute2 = static_routes.StaticRoute{ + Description: "StaticRoute for Scenario-test.", + Destination: "100.127.255.184/29", + FICGatewayID: "1331e6a7-2876-4d34-b12f-5aac9517b034", + ID: "e58162ca-9fef-4f27-898f-af0d495b780c", + Name: "StaticRoute_INGW_02_01", + Nexthop: "100.127.255.189", + ServiceType: "fic", + Status: "PENDING_CREATE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var ExpectedStaticRouteSlice = []static_routes.StaticRoute{StaticRoute1, StaticRoute2} diff --git a/v3/ecl/network/v2/static_routes/testing/request_test.go b/v3/ecl/network/v2/static_routes/testing/request_test.go new file mode 100644 index 0000000..cbcddda --- /dev/null +++ b/v3/ecl/network/v2/static_routes/testing/request_test.go @@ -0,0 +1,145 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/static_routes" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListStaticRoutes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := static_routes.List(client, static_routes.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := static_routes.ExtractStaticRoutes(page) + if err != nil { + t.Errorf("Failed to extract public ips: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedStaticRouteSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes/cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", 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, GetResponse) + }) + + i, err := static_routes.Get(fake.ServiceClient(), "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &StaticRoute1, i) +} + +func TestCreateStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := static_routes.CreateOpts{ + Name: "SRT2", + Description: "SRT2", + Destination: "100.127.254.116/30", + FICGatewayID: "5af4f343-91a7-4956-aabb-9ac678d215e5", + Nexthop: "100.127.254.117", + ServiceType: "fic", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + } + i, err := static_routes.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Status, "PENDING_CREATE") + th.AssertDeepEquals(t, &StaticRoute1, i) +} + +func TestUpdateStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes/cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := "SRT2" + description := "SRT2" + options := static_routes.UpdateOpts{Name: &name, Description: &description} + i, err := static_routes.Update(fake.ServiceClient(), "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "SRT2") + th.AssertEquals(t, i.Description, "SRT2") + th.AssertEquals(t, i.ID, "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a") +} + +func TestDeleteStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes/cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", 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 := static_routes.Delete(fake.ServiceClient(), "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/static_routes/urls.go b/v3/ecl/network/v2/static_routes/urls.go new file mode 100644 index 0000000..da27d18 --- /dev/null +++ b/v3/ecl/network/v2/static_routes/urls.go @@ -0,0 +1,31 @@ +package static_routes + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("static_routes", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("static_routes") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/subnets/doc.go b/v3/ecl/network/v2/subnets/doc.go new file mode 100644 index 0000000..d0ed8df --- /dev/null +++ b/v3/ecl/network/v2/subnets/doc.go @@ -0,0 +1,133 @@ +/* +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. + +Example to List Subnets + + listOpts := subnets.ListOpts{ + IPVersion: 4, + } + + allPages, err := subnets.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + panic(err) + } + + for _, subnet := range allSubnets { + fmt.Printf("%+v\n", subnet) + } + +Example to Create a Subnet With Specified Gateway + + var gatewayIP = "192.168.199.1" + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + GatewayIP: &gatewayIP, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With No Gateway + + var noGateway = "" + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With a Default Gateway + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + Name: "new_name", + DNSNameservers: []string{"8.8.8.8}, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove a Gateway From a Subnet + + var noGateway = "" + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + GatewayIP: &noGateway, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + err := subnets.Delete(networkClient, subnetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package subnets diff --git a/v3/ecl/network/v2/subnets/requests.go b/v3/ecl/network/v2/subnets/requests.go new file mode 100644 index 0000000..4fc6067 --- /dev/null +++ b/v3/ecl/network/v2/subnets/requests.go @@ -0,0 +1,238 @@ +package subnets + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// 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 { + CIDR string `q:"cidr"` + Description string `q:"description"` + GatewayIP string `q:"gateway_ip"` + ID string `q:"id"` + IPVersion int `q:"ip_version"` + IPv6AddressMode string `q:"ipv6_address_mode"` + IPv6RAMode string `q:"ipv6_ra_mode"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 *eclcloud.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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + // CIDR is the address CIDR of the subnet. + CIDR string `json:"cidr" required:"true"` + // Description is description + Description string `json:"description,omitempty"` + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes []HostRoute `json:"host_routes,omitempty"` + // IPVersion is the IP version for the subnet. + IPVersion eclcloud.IPVersion `json:"ip_version,omitempty"` + // Name is a human-readable name of the subnet. + Name string `json:"name,omitempty"` + // NetworkID is the UUID of the network the subnet will be associated with. + NetworkID string `json:"network_id" required:"true"` + // NTPServers are List of ntp servers. + NTPServers []string `json:"ntp_servers,omitempty"` + // Tags are tags + Tags map[string]string `json:"tags,omitempty"` + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSubnetCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSubnetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// 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 is a human-readable name of the subnet. + Name *string `json:"name,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // Description is description + Description *string `json:"description,omitempty"` + + // NTPServers are List of ntp servers. + NTPServers *[]string `json:"ntp_servers,omitempty"` + + // Tags are tags + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToSubnetUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSubnetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a subnet's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractSubnets(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "subnet"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "subnet"} + } +} diff --git a/v3/ecl/network/v2/subnets/results.go b/v3/ecl/network/v2/subnets/results.go new file mode 100644 index 0000000..0bf3654 --- /dev/null +++ b/v3/ecl/network/v2/subnets/results.go @@ -0,0 +1,152 @@ +package subnets + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + var s struct { + Subnet *Subnet `json:"subnet"` + } + err := r.ExtractInto(&s) + return s.Subnet, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Subnet. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Subnet. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Subnet. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.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 `json:"id"` + + // UUID of the parent network. + NetworkID string `json:"network_id"` + + // Human-readable name for the subnet. Might not be unique. + Name string `json:"name"` + + // IP version, either `4' or `6'. + IPVersion int `json:"ip_version"` + + // CIDR representing IP range for this subnet, based on IP version. + CIDR string `json:"cidr"` + + // Default gateway used by devices in this subnet. + GatewayIP string `json:"gateway_ip"` + + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `json:"dns_nameservers"` + + // Sub-ranges of CIDR available for dynamic allocation to ports. + // See AllocationPool. + AllocationPools []AllocationPool `json:"allocation_pools"` + + // Routes that should be used by devices with IPs from this subnet + // (not including local subnet route). + HostRoutes []HostRoute `json:"host_routes"` + + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `json:"enable_dhcp"` + + // TenantID is the project owner of the subnet. + TenantID string `json:"tenant_id"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode"` + + // Description is description + Description string `json:"description"` + + // NTPServers are List of ntp servers. + NTPServers []string `json:"ntp_servers"` + + // Status is Status + Status string `json:"status"` + + // Tags optionally set via extensions/attributestags + Tags map[string]string `json:"tags"` +} + +// 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 (r SubnetPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"subnets_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (r SubnetPage) IsEmpty() (bool, error) { + is, err := ExtractSubnets(r) + return len(is) == 0, err +} + +// 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(r pagination.Page) ([]Subnet, error) { + var s struct { + Subnets []Subnet `json:"subnets"` + } + err := (r.(SubnetPage)).ExtractInto(&s) + return s.Subnets, err +} diff --git a/v3/ecl/network/v2/subnets/testing/doc.go b/v3/ecl/network/v2/subnets/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v3/ecl/network/v2/subnets/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v3/ecl/network/v2/subnets/testing/fixtures.go b/v3/ecl/network/v2/subnets/testing/fixtures.go new file mode 100644 index 0000000..f1cecd3 --- /dev/null +++ b/v3/ecl/network/v2/subnets/testing/fixtures.go @@ -0,0 +1,246 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/subnets" +) + +const ListResponse = ` +{ + "subnets": [ + { + "allocation_pools": [ + { + "end": "192.168.2.254", + "start": "192.168.2.2" + } + ], + "cidr": "192.168.2.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.2.1", + "host_routes": [], + "id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + }, + { + "allocation_pools": [ + { + "end": "192.168.10.254", + "start": "192.168.10.2" + } + ], + "cidr": "192.168.10.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.10.1", + "host_routes": [], + "id": "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + ] + } +` +const GetResponse = ` +{ + "subnet": { + "allocation_pools": [ + { + "end": "192.168.2.254", + "start": "192.168.2.2" + } + ], + "cidr": "192.168.2.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.2.1", + "host_routes": [], + "id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } + ` +const CreateResponse = ` +{ + "subnet": { + "allocation_pools": [ + { + "end": "192.168.10.254", + "start": "192.168.10.2" + } + ], + "cidr": "192.168.10.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.10.1", + "host_routes": [], + "id": "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + "ip_version": 4, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } + ` +const CreateRequest = ` +{ + "subnet": { + "cidr": "192.168.10.0/24", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782" + } +} +` +const UpdateResponse = ` +{ + "subnet": { + "allocation_pools": [ + { + "end": "192.168.2.254", + "start": "192.168.2.2" + } + ], + "cidr": "192.168.2.0/24", + "description": "UPDATED", + "dns_nameservers": [ + "0.0.0.0", + "1.1.1.1" + ], + "enable_dhcp": false, + "gateway_ip": "192.168.10.1", + "host_routes": [ + { + "destination": "10.2.0.0/24", + "nexthop": "10.1.0.10" + } + ], + "id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "UPDATED", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [ + "2.2.2.2" + ], + "status": "PENDING_UPDATE", + "tags": { + "updated": "true" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` +const UpdateRequest = ` +{ + "subnet": { + "description": "UPDATED", + "dns_nameservers": [ + "0.0.0.0", + "1.1.1.1" + ], + "enable_dhcp": false, + "gateway_ip": "192.168.10.1", + "host_routes": [{ + "destination": "10.2.0.0/24", + "nexthop": "10.1.0.10" + }], + "name": "UPDATED", + "ntp_servers": [ + "2.2.2.2" + ], + "tags": { + "updated": "true" + } + } +} +` + +var Subnet1 = subnets.Subnet{ + AllocationPools: []subnets.AllocationPool{ + { + End: "192.168.2.254", + Start: "192.168.2.2", + }, + }, + CIDR: "192.168.2.0/24", + Description: "", + DNSNameservers: []string{ + "0.0.0.0", + }, + EnableDHCP: true, + GatewayIP: "192.168.2.1", + HostRoutes: []subnets.HostRoute{}, + ID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + IPVersion: 4, + Name: "", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + NTPServers: []string{}, + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var Subnet2 = subnets.Subnet{ + AllocationPools: []subnets.AllocationPool{ + { + End: "192.168.10.254", + Start: "192.168.10.2", + }, + }, + CIDR: "192.168.10.0/24", + Description: "", + DNSNameservers: []string{ + "0.0.0.0", + }, + EnableDHCP: true, + GatewayIP: "192.168.10.1", + HostRoutes: []subnets.HostRoute{}, + ID: "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + IPVersion: 4, + Name: "", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + NTPServers: []string{}, + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var ExpectedSubnetSlice = []subnets.Subnet{Subnet1, Subnet2} diff --git a/v3/ecl/network/v2/subnets/testing/request_test.go b/v3/ecl/network/v2/subnets/testing/request_test.go new file mode 100644 index 0000000..2b80064 --- /dev/null +++ b/v3/ecl/network/v2/subnets/testing/request_test.go @@ -0,0 +1,175 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/subnets" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListSubnet(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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + subnets.List(client, subnets.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := subnets.ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extrace ports: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedSubnetSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, GetResponse) + }) + + s, err := subnets.Get(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Subnet1, s) +} + +func TestCreateSubnet(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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := &subnets.CreateOpts{ + CIDR: "192.168.10.0/24", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + } + s, err := subnets.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &Subnet2, s) +} + +func TestRequiredCreateOptsSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := subnets.Create(fake.ServiceClient(), subnets.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "UPDATED" + dnsNameservers := []string{ + "0.0.0.0", + "1.1.1.1", + } + enableDHCP := false + gatewayIP := "192.168.10.1" + hostRoutes := []subnets.HostRoute{{ + DestinationCIDR: "10.2.0.0/24", + NextHop: "10.1.0.10", + }} + name := "UPDATED" + ntpServers := []string{ + "2.2.2.2", + } + tags := map[string]string{ + "updated": "true", + } + + options := subnets.UpdateOpts{ + Description: &description, + DNSNameservers: dnsNameservers, + EnableDHCP: &enableDHCP, + GatewayIP: &gatewayIP, + HostRoutes: &hostRoutes, + Name: &name, + NTPServers: &ntpServers, + Tags: &tags, + } + + s, err := subnets.Update(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, description, s.Description) + th.CheckDeepEquals(t, dnsNameservers, s.DNSNameservers) + th.CheckEquals(t, enableDHCP, s.EnableDHCP) + th.CheckEquals(t, gatewayIP, s.GatewayIP) + th.CheckDeepEquals(t, hostRoutes, s.HostRoutes) + th.CheckEquals(t, name, s.Name) + th.CheckDeepEquals(t, ntpServers, s.NTPServers) + th.CheckDeepEquals(t, tags, s.Tags) +} + +func TestDeleteSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/ab49eb24-667f-4a4e-9421-b4d915bff416", 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 := subnets.Delete(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/subnets/urls.go b/v3/ecl/network/v2/subnets/urls.go new file mode 100644 index 0000000..6f86bdc --- /dev/null +++ b/v3/ecl/network/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/doc.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/doc.go new file mode 100644 index 0000000..1058a9b --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/doc.go @@ -0,0 +1,79 @@ +/* +Package tenant_connection_requests manages and retrieves Tenant Connection Request in the Enterprise Cloud Provider Connectivity Service. + +Example to List Tenant Connection Request + + allPages, err := tenant_connection_requests.List(tcrClient).AllPages() + if err != nil { + panic(err) + } + + allTenantConnectionRequests, err := tenant_connection_requests.ExtractTenantConnectionRequests(allPages) + if err != nil { + panic(err) + } + + for _, tenantConnectionRequest := range allTenantConnectionRequests { + fmt.Printf("%+v\n", tenantConnectionRequest) + } + +Example to Get a Tenant Connection Request + + tenant_connection_request_id := "85a1dc30-2e48-11ea-9e55-525403060300" + + tenantConnectionRequest, err := tenant_connection_requests.Get(tcrClient, tenant_connection_request_id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", tenantConnectionRequest) + +Example to Create a Tenant Connection Request + + createOpts := tenant_connection_requests.CreateOpts{ + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + Name: "create_test_name", + Description: "create_test_desc", + Tags: map[string]string{"foo", "bar"}, + } + + result := tenant_connection_requests.Create(tcrClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a Tenant Connection Request + + tenant_connection_request_id := "85a1dc30-2e48-11ea-9e55-525403060300" + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: "update_test_name", + Description: "update_test_desc", + Tags: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + NameOther: "update_test_name_other", + DescriptionOther: "update_test_desc_other", + TagsOther: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + } + + result := tenant_connection_requests.Update(tcrClient, tenant_connection_request_id, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a Tenant Connection Request + + tenant_connection_request_id := "85a1dc30-2e48-11ea-9e55-525403060300" + + result := tenant_connection_requests.Delete(tcrClient, tenant_connection_request_id) + if result.Err != nil { + panic(result.Err) + } + +*/ +package tenant_connection_requests diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/requests.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/requests.go new file mode 100644 index 0000000..6cc6e3c --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/requests.go @@ -0,0 +1,129 @@ +package tenant_connection_requests + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToTenantConnectionRequestListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + TenantConnectionRequestID string `q:"tenant_connection_request_id"` + Status string `q:"status"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + NameOther string `q:"name_other"` + TenantIDOther string `q:"tenant_id_other"` + NetworkID string `q:"network_id"` + ApprovalRequestID string `q:"approval_request_id"` +} + +// ToTenantConnectionRequestListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantConnectionRequestListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Tenant Connection Requests. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantConnectionRequestListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantConnectionRequestPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of an Tenant Connection Request. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToTenantConnectionRequestCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a Tenant Connection Request. +type CreateOpts struct { + TenantIDOther string `json:"tenant_id_other" required:"true"` + NetworkID string `json:"network_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +// ToTenantConnectionRequestCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToTenantConnectionRequestCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection_request") +} + +// Create creates a new Tenant Connection Request. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantConnectionRequestCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a Tenant Connection Request. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToTenantConnectionRequestUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a Tenant Connection Request. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` + NameOther *string `json:"name_other,omitempty"` + DescriptionOther *string `json:"description_other,omitempty"` + TagsOther *map[string]string `json:"tags_other,omitempty"` +} + +// ToResourceUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToTenantConnectionRequestUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection_request") +} + +// Update modifies the attributes of a Tenant Connection Request. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantConnectionRequestUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/results.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/results.go new file mode 100644 index 0000000..78cd9ba --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/results.go @@ -0,0 +1,80 @@ +package tenant_connection_requests + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// TenantConnectionRequest represents Tenant Connection Request. +type TenantConnectionRequest struct { + ID string `json:"id"` + Status string `json:"status"` + Name string `json:"name"` + Description string `json:"description"` + Tags map[string]string `json:"tags"` + TenantID string `json:"tenant_id"` + NameOther string `json:"name_other"` + DescriptionOther string `json:"description_other"` + TagsOther map[string]string `json:"tags_other"` + TenantIDOther string `json:"tenant_id_other"` + NetworkID string `json:"network_id"` + ApprovalRequestID string `json:"approval_request_id"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Tenant Connection Request. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Tenant Connection Request. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Tenant Connection Request. +type UpdateResult struct { + commonResult +} + +// TenantConnectionRequestPage is a single page of Tenant Connection Request results. +type TenantConnectionRequestPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenant Connection Request contains any results. +func (r TenantConnectionRequestPage) IsEmpty() (bool, error) { + resources, err := ExtractTenantConnectionRequests(r) + return len(resources) == 0, err +} + +// ExtractTenantConnectionRequests returns a slice of Tenant Connection Requests contained in a +// single page of results. +func ExtractTenantConnectionRequests(r pagination.Page) ([]TenantConnectionRequest, error) { + var s struct { + TenantConnectionRequest []TenantConnectionRequest `json:"tenant_connection_requests"` + } + err := (r.(TenantConnectionRequestPage)).ExtractInto(&s) + return s.TenantConnectionRequest, err +} + +// Extract interprets any commonResult as a Tenant Connection Request. +func (r commonResult) Extract() (*TenantConnectionRequest, error) { + var s struct { + TenantConnectionRequest *TenantConnectionRequest `json:"tenant_connection_request"` + } + err := r.ExtractInto(&s) + return s.TenantConnectionRequest, err +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/doc.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/doc.go new file mode 100644 index 0000000..7370972 --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/doc.go @@ -0,0 +1,2 @@ +// Tenant Connection Request unit tests +package testing diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/fixtures.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/fixtures.go new file mode 100644 index 0000000..5f13f6d --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/fixtures.go @@ -0,0 +1,457 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/provider_connectivity/v2/tenant_connection_requests" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +// ListResult provides a single page of tenant_connection_request results. +const ListResult = ` +{ + "tenant_connection_requests": [ + { + "id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "name": "test_name1", + "description": "test_desc1", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "", + "description_other": "", + "tags_other": {}, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registering", + "approval_request_id": "req0000010454" + }, + { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "created_name", + "description": "created_desc", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": { + "test_tags_other2": "test2" + }, + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "created_name", + "description": "created_desc", + "tags": { + "test_tags2":"test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": { + "test_tags_other2":"test2" + }, + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "tenant_connection_request": { + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "name": "test_name1", + "description": "test_desc1", + "tags": { + "test_tags1": "test1" + } + } +} +` + +// CreateResponse provides the output from a Create request. +const CreateResponse = ` +{ + "tenant_connection_request": { + "id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "name": "test_name1", + "description": "test_desc1", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "", + "description_other": "", + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "tags_other": {}, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registering", + "approval_request_id": "req0000010454" + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "tenant_connection_request":{ + "name": "updated_name", + "description": "updated_desc", + "tags": { + "k2":"v2" + } + } +} +` + +// UpdateResult provides an update result. +const UpdateResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "updated_name", + "description": "updated_desc", + "tags": { + "k2": "v2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "test_tags_other2": "test2" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// UpdateOtherMetadataRequest provides the input to as Update to other metadata request. +const UpdateOtherMetadataRequest = ` +{ + "tenant_connection_request":{ + "name_other": "updated_name_other", + "description_other": "updated_desc_other", + "tags_other": { + "k3":"v3" + } + } +} +` + +// UpdateOtherMetadataResult provides an update to other metadata result. +const UpdateOtherMetadataResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "created_name", + "description": "created_desc", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "updated_name_other", + "description_other": "updated_desc_other", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "k3": "v3" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// UpdateBlankRequest provides the input to as Update with blank request. +const UpdateBlankRequest = ` +{ + "tenant_connection_request":{ + "name": "", + "description": "", + "tags": {} + } +} +` + +// UpdateBlankResult provides an update with blank result. +const UpdateBlankResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "", + "description": "", + "tags": {}, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "test_tags_other2": "test2" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// UpdateNilRequest provides the input to as Update with nil request. +const UpdateNilRequest = ` +{ + "tenant_connection_request":{ + "name": "nilupdate" + } +} +` + +// UpdateNilResult provides an update with nil result. +const UpdateNilResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "nilupdate", + "description": "created_desc", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "test_tags_other2": "test2" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// FirstTenantConnectionRequest is the first tenant_connection_request in the List request. +var FirstTenantConnectionRequest = tenant_connection_requests.TenantConnectionRequest{ + ID: "5fbcc350-bd33-11e7-afb6-0050569c850d", + Status: "registering", + Name: "test_name1", + Description: "test_desc1", + Tags: map[string]string{"test_tags1": "test1"}, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{}, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010454", +} + +// SecondTenantConnectionRequest is the second tenant_connection_request in the List request. +var SecondTenantConnectionRequest = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "created_name", + Description: "created_desc", + Tags: map[string]string{"test_tags2": "test2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestUpdated is how second tenant_connection_request should look after an Update. +var SecondTenantConnectionRequestUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "updated_name", + Description: "updated_desc", + Tags: map[string]string{"k2": "v2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestOtherMetadataUpdated is how second tenant_connection_request should look after an Update to other metadata. +var SecondTenantConnectionRequestOtherMetadataUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "created_name", + Description: "created_desc", + Tags: map[string]string{"test_tags2": "test2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "updated_name_other", + DescriptionOther: "updated_desc_other", + TagsOther: map[string]string{"k3": "v3"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestBlankUpdated is how second tenant_connection_request should look after an Update with blank. +var SecondTenantConnectionRequestBlankUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "", + Description: "", + Tags: map[string]string{}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestNilUpdated is how second tenant_connection_request should look after an Update with nil. +var SecondTenantConnectionRequestNilUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "nilupdate", + Description: "created_desc", + Tags: map[string]string{"test_tags2": "test2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// ExpectedTenantConnectionRequestsSlice is the slice of tenant_connection_request expected to be returned from ListResult. +var ExpectedTenantConnectionRequestsSlice = []tenant_connection_requests.TenantConnectionRequest{FirstTenantConnectionRequest, SecondTenantConnectionRequest} + +// HandleListTenantConnectionRequestsSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that responds with a list of two tenant_connection_requests. +func HandleListTenantConnectionRequestsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connection_requests", 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.Fprint(w, ListResult) + }) +} + +// HandleGetTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that responds with a single tenant_connection_request. +func HandleGetTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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.Fprint(w, GetResult) + }) +} + +// HandleCreateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request creation. +func HandleCreateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connection_requests", 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, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateResponse) + }) +} + +// HandleDeleteTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request deletion. +func HandleDeleteTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", FirstTenantConnectionRequest.ID), 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) + }) +} + +// HandleUpdateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update. +func HandleUpdateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateResult) + }) +} + +// HandleUpdateOtherMetadataTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update to other metadata result. +func HandleUpdateOtherMetadataTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateOtherMetadataRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateOtherMetadataResult) + }) +} + +// HandleBlankUpdateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update with blank. +func HandleBlankUpdateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateBlankRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateBlankResult) + }) +} + +// HandleNilUpdateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update with nil. +func HandleNilUpdateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateNilRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateNilResult) + }) +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/requests_test.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/requests_test.go new file mode 100644 index 0000000..f96b44c --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/testing/requests_test.go @@ -0,0 +1,155 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/provider_connectivity/v2/tenant_connection_requests" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListTenantConnectionRequests(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionRequestsSuccessfully(t) + + count := 0 + err := tenant_connection_requests.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := tenant_connection_requests.ExtractTenantConnectionRequests(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedTenantConnectionRequestsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListTenantConnectionRequestsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionRequestsSuccessfully(t) + + allPages, err := tenant_connection_requests.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := tenant_connection_requests.ExtractTenantConnectionRequests(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedTenantConnectionRequestsSlice, actual) +} + +func TestGetTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetTenantConnectionRequestSuccessfully(t) + + actual, err := tenant_connection_requests.Get(client.ServiceClient(), SecondTenantConnectionRequest.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequest, *actual) +} + +func TestCreateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionRequestSuccessfully(t) + + createOpts := tenant_connection_requests.CreateOpts{ + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + Name: "test_name1", + Description: "test_desc1", + Tags: map[string]string{"test_tags1": "test1"}, + } + + actual, err := tenant_connection_requests.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &FirstTenantConnectionRequest, actual) +} + +func TestDeleteTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteTenantConnectionRequestSuccessfully(t) + + res := tenant_connection_requests.Delete(client.ServiceClient(), FirstTenantConnectionRequest.ID) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateTenantConnectionRequestSuccessfully(t) + + name := "updated_name" + description := "updated_desc" + tags := map[string]string{"k2": "v2"} + + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestUpdated, *actual) +} + +func TestUpdateOtherMetadataTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateOtherMetadataTenantConnectionRequestSuccessfully(t) + + nameOther := "updated_name_other" + descriptionOther := "updated_desc_other" + tagsOther := map[string]string{"k3": "v3"} + + updateOpts := tenant_connection_requests.UpdateOpts{ + NameOther: &nameOther, + DescriptionOther: &descriptionOther, + TagsOther: &tagsOther, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestOtherMetadataUpdated, *actual) +} + +func TestBlankUpdateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleBlankUpdateTenantConnectionRequestSuccessfully(t) + + name := "" + description := "" + tags := map[string]string{} + + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestBlankUpdated, *actual) +} + +func TestNilUpdateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleNilUpdateTenantConnectionRequestSuccessfully(t) + + name := "nilupdate" + + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: &name, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestNilUpdated, *actual) +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connection_requests/urls.go b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/urls.go new file mode 100644 index 0000000..b04b1a8 --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connection_requests/urls.go @@ -0,0 +1,23 @@ +package tenant_connection_requests + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connection_requests") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connection_requests", id) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connection_requests") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connection_requests", id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connection_requests", id) +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/doc.go b/v3/ecl/provider_connectivity/v2/tenant_connections/doc.go new file mode 100644 index 0000000..3d82c48 --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/doc.go @@ -0,0 +1,87 @@ +/* +Package tenant_connections manages and retrieves Tenant Connection in the Enterprise Cloud Provider Connectivity Service. + +Example to List Tenant Connection + + allPages, err := tenant_connections.List(tcClient).AllPages() + if err != nil { + panic(err) + } + + allTenantConnections, err := tenant_connections.ExtractTenantConnections(allPages) + if err != nil { + panic(err) + } + + for _, tenantConnection := range allTenantConnections { + fmt.Printf("%+v\n", tenantConnection) + } + +Example to Get a Tenant Connection + + tenant_connection_id := "ea5d975c-bd31-11e7-bcac-0050569c850d" + + tenantConnection, err := tenant_connections.Get(tcClient, tenant_connection_id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", tenantConnection) + +Example to Create a Tenant Connection + + createOpts := tenant_connections.CreateOpts{ + Name: "create_test_name", + Description: "create_test_desc", + Tags: map[string]string{ + "test_tags": "test", + }, + TenantConnectionRequestID: "21b344d8-be11-11e7-bf3c-0050569c850d", + DeviceType: "ECL::VirtualNetworkAppliance::VSRX", + DeviceID: "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + DeviceInterfaceID: "interface_2", + AttachmentOpts: tenant_connections.Vna{ + FixedIPs: []tenant_connections.VnaFixedIPs{ + IPAddress: "192.168.1.3", + }, + }, + } + + result := tenant_connections.Create(tcClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a Tenant Connection + + tenant_connection_id := "ea5d975c-bd31-11e7-bcac-0050569c850d" + + updateOpts := tenant_connections.UpdateOpts{ + Name: "test_name", + Description: "test_desc", + Tags: map[string]string{ + "test_tags": "test", + }, + NameOther: "test_name_other", + DescriptionOther: "test_desc_other", + TagsOther: map[string]string{ + "test_tags_other": "test_other", + }, + } + + result := tenant_connections.Update(tcClient, tenant_connection_id, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a Tenant Connection + + tenant_connection_id := "ea5d975c-bd31-11e7-bcac-0050569c850d" + + result := tenant_connections.Delete(tcClient, tenant_connection_id) + if result.Err != nil { + panic(result.Err) + } + +*/ +package tenant_connections diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/requests.go b/v3/ecl/provider_connectivity/v2/tenant_connections/requests.go new file mode 100644 index 0000000..77a8be1 --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/requests.go @@ -0,0 +1,171 @@ +package tenant_connections + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToTenantConnectionListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + TenantConnectionRequestID string `q:"tenant_connection_request_id"` + Status string `q:"status"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + NameOther string `q:"name_other"` + TenantIDOther string `q:"tenant_id_other"` + NetworkID string `q:"network_id"` + DeviceType string `q:"device_type"` + DeviceID string `q:"device_id"` + DeviceInterfaceID string `q:"device_interface_id"` + PortID string `q:"port_id"` +} + +// ToTenantConnectionListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantConnectionListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Tenant Connection. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantConnectionListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantConnectionPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of an Tenant Connection. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToTenantConnectionCreateMap() (map[string]interface{}, error) +} + +// ServerFixedIPs contains the IP Address and the SubnetID. +type ServerFixedIPs struct { + SubnetID string `json:"subnet_id,omitempty"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// VnaFixedIPs represents ip address part of virtual network appliance. +type VnaFixedIPs struct { + IPAddress string `json:"ip_address,omitempty"` +} + +// Vna represents the parameter when device_type is VSRX. +type Vna struct { + FixedIPs []VnaFixedIPs `json:"fixed_ips,omitempty"` +} + +// ComputeServer represents the parameter when device_type is Compute Server. +type ComputeServer struct { + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + FixedIPs []ServerFixedIPs `json:"fixed_ips,omitempty"` +} + +// BaremetalServer represents the parameter when device_type is Baremetal Server. +type BaremetalServer struct { + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + FixedIPs []ServerFixedIPs `json:"fixed_ips,omitempty"` + SegmentationID int `json:"segmentation_id,omitempty"` + SegmentationType string `json:"segmentation_type,omitempty"` +} + +// CreateOpts provides options used to create a Tenant Connection. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantConnectionRequestID string `json:"tenant_connection_request_id" required:"true"` + DeviceType string `json:"device_type" required:"true"` + DeviceID string `json:"device_id" required:"true"` + DeviceInterfaceID string `json:"device_interface_id,omitempty"` + AttachmentOpts interface{} `json:"attachment_opts,omitempty"` +} + +// ToTenantConnectionCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToTenantConnectionCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection") +} + +// Create creates a new Tenant Connection. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantConnectionCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a Tenant Connection. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToTenantConnectionUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a Tenant Connection. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` + NameOther *string `json:"name_other,omitempty"` + DescriptionOther *string `json:"description_other,omitempty"` + TagsOther *map[string]string `json:"tags_other,omitempty"` +} + +// ToTenantConnectionUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToTenantConnectionUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection") +} + +// Update modifies the attributes of a Tenant Connection. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantConnectionUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/results.go b/v3/ecl/provider_connectivity/v2/tenant_connections/results.go new file mode 100644 index 0000000..0492643 --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/results.go @@ -0,0 +1,87 @@ +package tenant_connections + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// TenantConnection represents Tenant Connection. +// TagsOther is interface{} because the data type returned by Create API depends on the value of device_type. +// When the device_type of Create Request is ECL::Compute::Server, the data type of tags_other is map[]. +// When the device_type of Create Request is ECL::Baremetal::Server or ECL::VirtualNetworkAppliance::VSRX, the data type of tags_other is string. +type TenantConnection struct { + ID string `json:"id"` + TenantConnectionRequestID string `json:"tenant_connection_request_id"` + Name string `json:"name"` + Description string `json:"description"` + Tags map[string]string `json:"tags"` + TenantID string `json:"tenant_id"` + NameOther string `json:"name_other"` + DescriptionOther string `json:"description_other"` + TagsOther interface{} `json:"tags_other"` + TenantIDOther string `json:"tenant_id_other"` + NetworkID string `json:"network_id"` + DeviceType string `json:"device_type"` + DeviceID string `json:"device_id"` + DeviceInterfaceID string `json:"device_interface_id"` + PortID string `json:"port_id"` + Status string `json:"status"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Tenant Connection. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Tenant Connection. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Tenant Connection. +type UpdateResult struct { + commonResult +} + +// TenantConnectionPage is a single page of Tenant Connection results. +type TenantConnectionPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenant Connection contains any results. +func (r TenantConnectionPage) IsEmpty() (bool, error) { + resources, err := ExtractTenantConnections(r) + return len(resources) == 0, err +} + +// ExtractTenantConnections returns a slice of Tenant Connections contained in a +// single page of results. +func ExtractTenantConnections(r pagination.Page) ([]TenantConnection, error) { + var s struct { + TenantConnection []TenantConnection `json:"tenant_connections"` + } + err := (r.(TenantConnectionPage)).ExtractInto(&s) + return s.TenantConnection, err +} + +// Extract interprets any commonResult as a Tenant Connection. +func (r commonResult) Extract() (*TenantConnection, error) { + var s struct { + TenantConnection *TenantConnection `json:"tenant_connection"` + } + err := r.ExtractInto(&s) + return s.TenantConnection, err +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/testing/doc.go b/v3/ecl/provider_connectivity/v2/tenant_connections/testing/doc.go new file mode 100644 index 0000000..fb04ea5 --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/testing/doc.go @@ -0,0 +1,2 @@ +// Tenant Connection unit tests +package testing diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/testing/fixtures.go b/v3/ecl/provider_connectivity/v2/tenant_connections/testing/fixtures.go new file mode 100644 index 0000000..a729dbb --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/testing/fixtures.go @@ -0,0 +1,722 @@ +package testing + +import ( + "fmt" + "github.com/nttcom/eclcloud/v3/ecl/provider_connectivity/v2/tenant_connections" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" + "net/http" + "testing" +) + +// ListResult provides a single page of tenant_connection results. +const ListResult = ` +{ + "tenant_connections": [ + { + "id": "2a23e5a6-bd34-11e7-afb6-0050569c850d", + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tenant_connection_request_id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "device_type": "ECL::Compute::Server", + "device_id": "8c235a3b-8dee-41a1-b81a-64e06edc0986", + "device_interface_id": "", + "port_id": "b404ed73-9438-41a1-91ed-49d0e403be64", + "status": "creating", + "name": "test_name_1", + "description": "test_desc_1", + "tags": { + "test_tags1": "test1" + }, + "name_other": "", + "description_other": "", + "tags_other": {} + }, + { + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down", + "name": "test_name_2", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + } + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "tenant_connection": { + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down", + "name": "test_name_2", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + } + } +} +` + +// CreateAttachComputeServerRequest provides the input to a Create request. +const CreateAttachComputeServerRequest = ` +{ + "tenant_connection": { + "name": "test_name_1", + "description": "test_desc_1", + "tags": { + "test_tags1": "test1" + }, + "tenant_connection_request_id": "21b344d8-be11-11e7-bf3c-0050569c850d", + "device_type": "ECL::Compute::Server", + "device_id": "8c235a3b-8dee-41a1-b81a-64e06edc0986", + "attachment_opts": { + "fixed_ips": [ + { + "ip_address": "192.168.1.1", + "subnet_id": "1f424165-2202-4022-ad70-0fa6f9ec99e1" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "192.168.1.2", + "mac_address": "11:22:33:aa:bb:cc" + } + ] + } + } +} +` + +// CreateAttachComputeServerResponse provides the output from a Create request. +const CreateAttachComputeServerResponse = ` +{ + "tenant_connection":{ + "id": "2a23e5a6-bd34-11e7-afb6-0050569c850d", + "tenant_connection_request_id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "name": "test_name_1", + "description": "test_desc_1", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": {}, + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "device_type": "ECL::Compute::Server", + "device_id": "8c235a3b-8dee-41a1-b81a-64e06edc0986", + "device_interface_id": "", + "port_id": "b404ed73-9438-41a1-91ed-49d0e403be64", + "status": "creating" + } +} +` + +// CreateAttachBaremetalServerRequest provides the input to a Create request. +const CreateAttachBaremetalServerRequest = ` +{ + "tenant_connection": { + "name": "attach_bare_name", + "description": "attach_bare_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_connection_request_id": "147c4ffa-481e-11ea-8088-525400060300", + "device_type": "ECL::Baremetal::Server", + "device_interface_id": "46eb7624-d462-46c2-8ac7-f988a15d3280", + "device_id": "0acab22f-8993-451c-8a6b-398b0244f578", + "attachment_opts": { + "segmentation_id": 10, + "segmentation_type": "flat", + "fixed_ips": [ + { + "ip_address": "192.168.1.1", + "subnet_id": "1f424165-2202-4022-ad70-0fa6f9ec99e1" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "192.168.1.2", + "mac_address": "11:22:33:aa:bb:cc" + } + ] + } + } +} +` + +// CreateAttachBaremetalServerResponse provides the output from a Create request. +const CreateAttachBaremetalServerResponse = ` +{ + "tenant_connection":{ + "id": "0d956a2e-4958-11ea-8088-525400060300", + "tenant_connection_request_id": "147c4ffa-481e-11ea-8088-525400060300", + "name": "attach_bare_name", + "description": "attach_bare_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": "{}", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "061dbaa9-a3e0-4343-b3fc-0a619db66854", + "device_type": "ECL::Baremetal::Server", + "device_id": "0acab22f-8993-451c-8a6b-398b0244f578", + "device_interface_id": "46eb7624-d462-46c2-8ac7-f988a15d3280", + "port_id": "87449d66-4e99-4cf7-9b93-9f153548ccc7", + "status": "creating" + } +} +` + +// CreateAttachVnaRequest provides the input to a Create request. +const CreateAttachVnaRequest = ` +{ + "tenant_connection": { + "name": "attach_vna_name", + "description": "attach_vna_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_connection_request_id": "67d76b00-3804-11ea-8088-525400060300", + "device_type": "ECL::VirtualNetworkAppliance::VSRX", + "device_interface_id": "interface_2", + "device_id": "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + "attachment_opts": { + "fixed_ips": [ + { + "ip_address": "192.168.1.3" + } + ] + } + } +} +` + +// CreateAttachVnaResponse provides the output from a Create request. +const CreateAttachVnaResponse = ` +{ + "tenant_connection":{ + "id": "f6331886-3804-11ea-95a8-525400060400", + "tenant_connection_request_id": "67d76b00-3804-11ea-8088-525400060300", + "name": "attach_vna_name", + "description": "attach_vna_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": "{}", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "061dbaa9-a3e0-4343-b3fc-0a619db66854", + "device_interface_id": "interface_2", + "device_type": "ECL::VirtualNetworkAppliance::VSRX", + "device_id": "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + "port_id": "", + "status": "active" + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "tenant_connection": { + "name": "update_name", + "description": "update_desc", + "tags": { + "update_tags": "update" + } + } +} +` + +// UpdateResult provides an update result. +const UpdateResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "update_name", + "description": "update_desc", + "tags": { + "update_tags": "update" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + }, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// UpdateOtherMetadataRequest provides the input to as Update to other metadata request. +const UpdateOtherMetadataRequest = ` +{ + "tenant_connection": { + "name_other": "update_name_other", + "description_other": "update_desc_other", + "tags_other": { + "test_tags_other": "update" + } + } +} +` + +// UpdateOtherMetadataResult provides an update to other metadata result. +const UpdateOtherMetadataResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "test_name_2", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "update_name_other", + "description_other": "update_desc_other", + "tags_other": { + "test_tags_other": "update" + }, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// UpdateBlankRequest provides the input to as Update with blank request. +const UpdateBlankRequest = ` +{ + "tenant_connection": { + "name": "", + "description": "", + "tags": {} + } +} +` + +// UpdateBlankResult provides an update with blank result. +const UpdateBlankResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "", + "description": "", + "tags": {}, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": {"test_tags_other2": "test2"}, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// UpdateNilRequest provides the input to as Update with nil request. +const UpdateNilRequest = ` +{ + "tenant_connection": { + "name": "nilupdate" + } +} +` + +// UpdateNilResult provides an update with blank with nil result. +const UpdateNilResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "nilupdate", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + }, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// FirstTenantConnection is the first tenant_connection in the List request. +var FirstTenantConnection = tenant_connections.TenantConnection{ + ID: "2a23e5a6-bd34-11e7-afb6-0050569c850d", + TenantConnectionRequestID: "5fbcc350-bd33-11e7-afb6-0050569c850d", + Name: "test_name_1", + Description: "test_desc_1", + Tags: map[string]string{ + "test_tags1": "test1", + }, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + DeviceType: "ECL::Compute::Server", + DeviceID: "8c235a3b-8dee-41a1-b81a-64e06edc0986", + DeviceInterfaceID: "", + PortID: "b404ed73-9438-41a1-91ed-49d0e403be64", + Status: "creating", +} + +// SecondTenantConnection is the second tenant_connection in the List request. +var SecondTenantConnection = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "test_name_2", + Description: "test_desc_2", + Tags: map[string]string{ + "test_tags2": "test2", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{ + "test_tags_other2": "test2", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// CreateTenantConnectionAttachBaremetalServer is the tenant_connection in the Create Attach Baremetal Server request. +var CreateTenantConnectionAttachBaremetalServer = tenant_connections.TenantConnection{ + ID: "0d956a2e-4958-11ea-8088-525400060300", + TenantConnectionRequestID: "147c4ffa-481e-11ea-8088-525400060300", + Name: "attach_bare_name", + Description: "attach_bare_desc", + Tags: map[string]string{ + "test_tags1": "test1", + }, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: "{}", + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "061dbaa9-a3e0-4343-b3fc-0a619db66854", + DeviceType: "ECL::Baremetal::Server", + DeviceID: "0acab22f-8993-451c-8a6b-398b0244f578", + DeviceInterfaceID: "46eb7624-d462-46c2-8ac7-f988a15d3280", + PortID: "87449d66-4e99-4cf7-9b93-9f153548ccc7", + Status: "creating", +} + +// CreateTenantConnectionAttachVna is the tenant_connection in the Create Attach Vna request. +var CreateTenantConnectionAttachVna = tenant_connections.TenantConnection{ + ID: "f6331886-3804-11ea-95a8-525400060400", + TenantConnectionRequestID: "67d76b00-3804-11ea-8088-525400060300", + Name: "attach_vna_name", + Description: "attach_vna_desc", + Tags: map[string]string{ + "test_tags1": "test1", + }, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: "{}", + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "061dbaa9-a3e0-4343-b3fc-0a619db66854", + DeviceType: "ECL::VirtualNetworkAppliance::VSRX", + DeviceID: "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + DeviceInterfaceID: "interface_2", + PortID: "", + Status: "active", +} + +// SecondTenantConnectionUpdated is how second tenant_connection should look after an Update. +var SecondTenantConnectionUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "update_name", + Description: "update_desc", + Tags: map[string]string{ + "update_tags": "update", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{ + "test_tags_other2": "test2", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// SecondTenantConnectionOtherMetadataUpdated is how second tenant_connection should look after an Update to other metadata. +var SecondTenantConnectionOtherMetadataUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "test_name_2", + Description: "test_desc_2", + Tags: map[string]string{ + "test_tags2": "test2", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "update_name_other", + DescriptionOther: "update_desc_other", + TagsOther: map[string]string{ + "test_tags_other": "update", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// SecondTenantConnectionBlankUpdated is how second tenant_connection should look after an Update with blank. +var SecondTenantConnectionBlankUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "", + Description: "", + Tags: map[string]string{}, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// SecondTenantConnectionNilUpdated is how second tenant_connection should look after an Update with nil. +var SecondTenantConnectionNilUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "nilupdate", + Description: "test_desc_2", + Tags: map[string]string{ + "test_tags2": "test2", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{ + "test_tags_other2": "test2", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// ExpectedTenantConnectionsSlice is the slice of tenant_connection expected to be returned from ListResult. +var ExpectedTenantConnectionsSlice = []tenant_connections.TenantConnection{FirstTenantConnection, SecondTenantConnection} + +// HandleListTenantConnectionsSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that responds with a list of two tenant_connections. +func HandleListTenantConnectionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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.Fprint(w, ListResult) + }) +} + +// HandleGetTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that responds with a single tenant_connection. +func HandleGetTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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.Fprint(w, GetResult) + }) +} + +// HandleCreateTenantConnectionAttachComputeServerSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests creation of tenant_connection with Compute Server attached. +func HandleCreateTenantConnectionAttachComputeServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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, CreateAttachComputeServerRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateAttachComputeServerResponse) + }) +} + +// HandleCreateTenantConnectionAttachBaremetalServerSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests creation of tenant_connection with Baremetal Server attached. +func HandleCreateTenantConnectionAttachBaremetalServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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, CreateAttachBaremetalServerRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateAttachBaremetalServerResponse) + }) +} + +// HandleCreateTenantConnectionAttachVnaSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that that tests creation of tenant_connection with Vna attached. +func HandleCreateTenantConnectionAttachVnaSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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, CreateAttachVnaRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateAttachVnaResponse) + }) +} + +// HandleDeleteTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection deletion. +func HandleDeleteTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", FirstTenantConnection.ID), 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) + }) +} + +// HandleUpdateTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update. +func HandleUpdateTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateResult) + }) +} + +// HandleUpdateOtherMetadataTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update to other metadata. +func HandleUpdateOtherMetadataTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateOtherMetadataRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateOtherMetadataResult) + }) +} + +// HandleBlankUpdateTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update with blank. +func HandleBlankUpdateTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateBlankRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateBlankResult) + }) +} + +// HandleNilUpdateTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update with nil. +func HandleNilUpdateTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateNilRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateNilResult) + }) +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/testing/requests_test.go b/v3/ecl/provider_connectivity/v2/tenant_connections/testing/requests_test.go new file mode 100644 index 0000000..fae6c5a --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/testing/requests_test.go @@ -0,0 +1,234 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/provider_connectivity/v2/tenant_connections" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListTenantConnections(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionsSuccessfully(t) + + count := 0 + err := tenant_connections.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := tenant_connections.ExtractTenantConnections(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedTenantConnectionsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListTenantConnectionsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionsSuccessfully(t) + + allPages, err := tenant_connections.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := tenant_connections.ExtractTenantConnections(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedTenantConnectionsSlice, actual) +} + +func TestGetTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetTenantConnectionSuccessfully(t) + + actual, err := tenant_connections.Get(client.ServiceClient(), SecondTenantConnection.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnection, *actual) +} + +func TestCreateTenantConnectionAttachComputeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionAttachComputeServerSuccessfully(t) + + createOpts := tenant_connections.CreateOpts{ + Name: "test_name_1", + Description: "test_desc_1", + Tags: map[string]string{"test_tags1": "test1"}, + TenantConnectionRequestID: "21b344d8-be11-11e7-bf3c-0050569c850d", + DeviceType: "ECL::Compute::Server", + DeviceID: "8c235a3b-8dee-41a1-b81a-64e06edc0986", + DeviceInterfaceID: "", + AttachmentOpts: tenant_connections.ComputeServer{ + AllowedAddressPairs: []tenant_connections.AddressPair{ + { + IPAddress: "192.168.1.2", + MACAddress: "11:22:33:aa:bb:cc", + }, + }, + FixedIPs: []tenant_connections.ServerFixedIPs{ + { + SubnetID: "1f424165-2202-4022-ad70-0fa6f9ec99e1", + IPAddress: "192.168.1.1", + }, + }, + }, + } + + actual, err := tenant_connections.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &FirstTenantConnection, actual) +} + +func TestCreateTenantConnectionAttachBaremetalServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionAttachBaremetalServerSuccessfully(t) + + createOpts := tenant_connections.CreateOpts{ + Name: "attach_bare_name", + Description: "attach_bare_desc", + Tags: map[string]string{"test_tags1": "test1"}, + TenantConnectionRequestID: "147c4ffa-481e-11ea-8088-525400060300", + DeviceType: "ECL::Baremetal::Server", + DeviceID: "0acab22f-8993-451c-8a6b-398b0244f578", + DeviceInterfaceID: "46eb7624-d462-46c2-8ac7-f988a15d3280", + AttachmentOpts: tenant_connections.BaremetalServer{ + AllowedAddressPairs: []tenant_connections.AddressPair{ + { + IPAddress: "192.168.1.2", + MACAddress: "11:22:33:aa:bb:cc", + }, + }, + FixedIPs: []tenant_connections.ServerFixedIPs{ + { + SubnetID: "1f424165-2202-4022-ad70-0fa6f9ec99e1", + IPAddress: "192.168.1.1", + }, + }, + SegmentationID: 10, + SegmentationType: "flat", + }, + } + + actual, err := tenant_connections.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &CreateTenantConnectionAttachBaremetalServer, actual) +} + +func TestCreateTenantConnectionAttachVna(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionAttachVnaSuccessfully(t) + + createOpts := tenant_connections.CreateOpts{ + Name: "attach_vna_name", + Description: "attach_vna_desc", + Tags: map[string]string{"test_tags1": "test1"}, + TenantConnectionRequestID: "67d76b00-3804-11ea-8088-525400060300", + DeviceType: "ECL::VirtualNetworkAppliance::VSRX", + DeviceID: "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + DeviceInterfaceID: "interface_2", + AttachmentOpts: tenant_connections.Vna{ + FixedIPs: []tenant_connections.VnaFixedIPs{ + { + IPAddress: "192.168.1.3", + }, + }, + }, + } + + actual, err := tenant_connections.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &CreateTenantConnectionAttachVna, actual) +} + +func TestDeleteTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteTenantConnectionSuccessfully(t) + + res := tenant_connections.Delete(client.ServiceClient(), FirstTenantConnection.ID) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateTenantConnectionSuccessfully(t) + + name := "update_name" + description := "update_desc" + tags := map[string]string{"update_tags": "update"} + + updateOpts := tenant_connections.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionUpdated, *actual) +} + +func TestUpdateOtherMetadataTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateOtherMetadataTenantConnectionSuccessfully(t) + + nameOther := "update_name_other" + descriptionOther := "update_desc_other" + tagsOther := map[string]string{"test_tags_other": "update"} + + updateOpts := tenant_connections.UpdateOpts{ + NameOther: &nameOther, + DescriptionOther: &descriptionOther, + TagsOther: &tagsOther, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionOtherMetadataUpdated, *actual) +} + +func TestBlankUpdateTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleBlankUpdateTenantConnectionSuccessfully(t) + + name := "" + description := "" + tags := map[string]string{} + + updateOpts := tenant_connections.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionBlankUpdated, *actual) +} + +func TestNilUpdateTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleNilUpdateTenantConnectionSuccessfully(t) + + name := "nilupdate" + + updateOpts := tenant_connections.UpdateOpts{ + Name: &name, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionNilUpdated, *actual) +} diff --git a/v3/ecl/provider_connectivity/v2/tenant_connections/urls.go b/v3/ecl/provider_connectivity/v2/tenant_connections/urls.go new file mode 100644 index 0000000..1d6c10a --- /dev/null +++ b/v3/ecl/provider_connectivity/v2/tenant_connections/urls.go @@ -0,0 +1,23 @@ +package tenant_connections + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connections") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connections", id) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connections") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connections", id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connections", id) +} diff --git a/v3/ecl/rca/v1/users/doc.go b/v3/ecl/rca/v1/users/doc.go new file mode 100644 index 0000000..97ce3dd --- /dev/null +++ b/v3/ecl/rca/v1/users/doc.go @@ -0,0 +1,64 @@ +/* +Package users manages and retrieves users in the Enterprise Cloud Remote Console Access Service. + +Example to List users + + allPages, err := users.List(rcaClient).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +Example to Get a user + + username := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + user, err := users.Get(rcaClient, username).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", user) + +Example to Create a user + + createOpts := users.CreateOpts{ + Password: "dummy_passw@rd", + } + + result := users.Create(rcaClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a user + + username := "02471b45-3de0-4fc8-8469-a7cc52c378df" + updateOpts := users.UpdateOpts{ + Password: "dummy_passw@rd", + } + + result := users.Update(rcaClient, username, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a user + + username := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + result := users.Delete(rcaClient, username) + if result.Err != nil { + panic(result.Err) + } + +*/ +package users diff --git a/v3/ecl/rca/v1/users/requests.go b/v3/ecl/rca/v1/users/requests.go new file mode 100644 index 0000000..471ace0 --- /dev/null +++ b/v3/ecl/rca/v1/users/requests.go @@ -0,0 +1,86 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// List retrieves a list of users. +func List(client *eclcloud.ServiceClient) pagination.Pager { + url := listURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of a user. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToResourceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a user. +type CreateOpts struct { + Password string `json:"password"` +} + +// ToResourceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "user") +} + +// Create creates a new user. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a user. +func Delete(client *eclcloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToResourceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a user. +type UpdateOpts struct { + Password string `json:"password"` +} + +// ToResourceUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToResourceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "user") +} + +// Update modifies the attributes of a user. +func Update(client *eclcloud.ServiceClient, name string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToResourceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, name), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/rca/v1/users/results.go b/v3/ecl/rca/v1/users/results.go new file mode 100644 index 0000000..a4067c6 --- /dev/null +++ b/v3/ecl/rca/v1/users/results.go @@ -0,0 +1,77 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// User represents VPN user. +type User struct { + Name string `json:"name"` + Password string `json:"password"` + VPNEndpoints []VPNEndpoint `json:"vpn_endpoints"` +} + +// VPNEndpoint represents VPN Endpoint. +type VPNEndpoint struct { + Endpoint string `json:"endpoint"` + Type string `json:"type"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a user. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a user. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a user. +type UpdateResult struct { + commonResult +} + +// UserPage is a single page of user results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of users contains any results. +func (r UserPage) IsEmpty() (bool, error) { + resources, err := ExtractUsers(r) + return len(resources) == 0, err +} + +// ExtractUsers returns a slice of users contained in a single page of +// results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} + +// Extract interprets any commonResult as a user. +func (r commonResult) Extract() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} diff --git a/v3/ecl/rca/v1/users/testing/fixtures.go b/v3/ecl/rca/v1/users/testing/fixtures.go new file mode 100644 index 0000000..b530ef5 --- /dev/null +++ b/v3/ecl/rca/v1/users/testing/fixtures.go @@ -0,0 +1,196 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/rca/v1/users" + + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +var ( + password = "dummy_passw@rd" + passwordUpdated = "dummy_passw@rd_updated" +) + +// ListResult provides a single page of user results. +const ListResult = ` +{ + "users": [ + { + "name": "ef5778e553a24d789c15c689e30adf5d", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + }, + { + "name": "8bbe05d4bec747189e0dab81e486969f-1005", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "user": { + "name": "8bbe05d4bec747189e0dab81e486969f-1005", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "user": { + "password": "dummy_passw@rd" + } +} +` + +// CreateResponse provides the output from a Create request. +const CreateResponse = ` +{ + "user": { + "name": "8bbe05d4bec747189e0dab81e486969f-1005", + "password": "dummy_passw@rd", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "user": { + "password": "dummy_passw@rd_updated" + } +} +` + +// UpdateResult provides an update result. +const UpdateResult = GetResult + +// FirstUser is the first user in the List request. +var FirstUser = users.User{ + Name: "ef5778e553a24d789c15c689e30adf5d", + VPNEndpoints: []users.VPNEndpoint{ + { + Endpoint: "https://rca-sslvpn1-jp1.ecl.ntt.com", + Type: "SSL-VPN", + }, + }, +} + +// SecondUser is the second user in the List request. +var SecondUser = users.User{ + Name: "8bbe05d4bec747189e0dab81e486969f-1005", + VPNEndpoints: []users.VPNEndpoint{ + { + Endpoint: "https://rca-sslvpn1-jp1.ecl.ntt.com", + Type: "SSL-VPN", + }, + }, +} + +// SecondUserUpdated is how SecondUser should look after an Update. +var SecondUserUpdated = users.User{ + Name: "8bbe05d4bec747189e0dab81e486969f-1005", + VPNEndpoints: []users.VPNEndpoint{ + { + Endpoint: "https://rca-sslvpn1-jp1.ecl.ntt.com", + Type: "SSL-VPN", + }, + }, +} + +// ExpectedUsersSlice is the slice of users expected to be returned from ListResult. +var ExpectedUsersSlice = []users.User{FirstUser, SecondUser} + +// HandleListUsersSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that responds with a list of two users. +func HandleListUsersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users", 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, ListResult) + }) +} + +// HandleGetUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that responds with a single user. +func HandleGetUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/users/%s", SecondUser.Name), 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, GetResult) + }) +} + +// HandleCreateUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user creation. +func HandleCreateUserSuccessfully(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", client.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, CreateResponse) + }) +} + +// HandleDeleteUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user deletion. +func HandleDeleteUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/users/%s", FirstUser.Name), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusOK) + }) +} + +// HandleUpdateUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user update. +func HandleUpdateUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/users/%s", SecondUser.Name), 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, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateResult) + }) +} diff --git a/v3/ecl/rca/v1/users/testing/requests_test.go b/v3/ecl/rca/v1/users/testing/requests_test.go new file mode 100644 index 0000000..686cf50 --- /dev/null +++ b/v3/ecl/rca/v1/users/testing/requests_test.go @@ -0,0 +1,91 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/rca/v1/users" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListUsers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsersSuccessfully(t) + + count := 0 + err := users.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedUsersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListUsersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsersSuccessfully(t) + + allPages, err := users.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + actual, err := users.ExtractUsers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedUsersSlice, actual) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetUserSuccessfully(t) + + actual, err := users.Get(client.ServiceClient(), SecondUser.Name).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondUser, *actual) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateUserSuccessfully(t) + + createOpts := users.CreateOpts{ + Password: password, + } + + actual, err := users.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, SecondUser.Name, actual.Name) + th.AssertEquals(t, password, actual.Password) + th.AssertDeepEquals(t, SecondUser.VPNEndpoints, actual.VPNEndpoints) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteUserSuccessfully(t) + + res := users.Delete(client.ServiceClient(), FirstUser.Name) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateUserSuccessfully(t) + + updateOpts := users.UpdateOpts{ + Password: passwordUpdated, + } + + actual, err := users.Update(client.ServiceClient(), SecondUser.Name, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondUserUpdated, *actual) +} diff --git a/v3/ecl/rca/v1/users/urls.go b/v3/ecl/rca/v1/users/urls.go new file mode 100644 index 0000000..8335c66 --- /dev/null +++ b/v3/ecl/rca/v1/users/urls.go @@ -0,0 +1,23 @@ +package users + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *eclcloud.ServiceClient, name string) string { + return client.ServiceURL("users", name) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func deleteURL(client *eclcloud.ServiceClient, name string) string { + return client.ServiceURL("users", name) +} + +func updateURL(client *eclcloud.ServiceClient, name string) string { + return client.ServiceURL("users", name) +} diff --git a/v3/ecl/security_order/v3/host_based/doc.go b/v3/ecl/security_order/v3/host_based/doc.go new file mode 100644 index 0000000..75eaef7 --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/doc.go @@ -0,0 +1,2 @@ +// Package host_based contains Host Based Security functionality. +package host_based diff --git a/v3/ecl/security_order/v3/host_based/requests.go b/v3/ecl/security_order/v3/host_based/requests.go new file mode 100644 index 0000000..4ddc9b1 --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/requests.go @@ -0,0 +1,139 @@ +package host_based + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// GetOptsBuilder allows extensions to add additional parameters to +// the order progress API request +type GetOptsBuilder interface { + ToServiceOrderQuery() (string, error) +} + +// GetOpts represents result of host based security API response. +type GetOpts struct { + TenantID string `q:"tenant_id"` +} + +// ToServiceOrderQuery formats a GetOpts into a query string. +func (opts GetOpts) ToServiceOrderQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves details of an order progress, by SoId. +func Get(client *eclcloud.ServiceClient, opts GetOptsBuilder) (r GetResult) { + url := getURL(client) + if opts != nil { + query, _ := opts.ToServiceOrderQuery() + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToHostBasedCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a Host based security. +type CreateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + ServiceOrderService string `json:"service_order_service" required:"true"` + MaxAgentValue int `json:"max_agent_value" required:"true"` + MailAddress string `json:"mailaddress" required:"true"` + DSMLang string `json:"dsm_lang" required:"true"` + TimeZone string `json:"time_zone" required:"true"` +} + +// ToHostBasedCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToHostBasedCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new Host based security. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToHostBasedCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to +// the Delete request. +type DeleteOptsBuilder interface { + ToHostBasedDeleteMap() (map[string]interface{}, error) +} + +// DeleteOpts represents parameters used to cancel Host Based Security. +type DeleteOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + MailAddress string `json:"mailaddress" required:"true"` +} + +// ToHostBasedDeleteMap formats a DeleteOpts into a delete request. +func (opts DeleteOpts) ToHostBasedDeleteMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Delete deletes a device. +func Delete(client *eclcloud.ServiceClient, opts DeleteOptsBuilder) (r DeleteResult) { + b, err := opts.ToHostBasedDeleteMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return + +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToHostBasedUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a Host Based Security. +type UpdateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + MailAddress string `json:"mailaddress" required:"true"` + // Set this in case of Type M1 Change + ServiceOrderService *string `json:"service_order_service,omitempty"` + // Set this in case of Type M2 Change + MaxAgentValue *int `json:"max_agent_value,omitempty"` +} + +// ToHostBasedUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToHostBasedUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a Host Based Security. +func Update(client *eclcloud.ServiceClient, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToHostBasedUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(updateURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/security_order/v3/host_based/results.go b/v3/ecl/security_order/v3/host_based/results.go new file mode 100644 index 0000000..7943dd7 --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/results.go @@ -0,0 +1,85 @@ +package host_based + +import ( + "github.com/nttcom/eclcloud/v3" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Host Based Security resource. +func (r commonResult) Extract() (*HostBasedOrder, error) { + var hbo HostBasedOrder + err := r.ExtractInto(&hbo) + return &hbo, err +} + +// Extract interprets any commonResult as a Host Based Security if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Host Based Security. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Host Based Security. +type GetResult struct { + commonResult +} + +// HostBasedSecurity represents a Host Based Security's each order. +type HostBasedSecurity struct { + Code string `json:"code"` + Message string `json:"message"` + Region string `json:"region"` + TenantName string `json:"tenant_name"` + TenantDescription string `json:"tenant_description"` + ContractID string `json:"contract_id"` + ServiceOrderService string `json:"service_order_service"` + MaxAgentValue interface{} `json:"max_agent_value"` + TimeZone string `json:"time_zone"` + CustomerName string `json:"customer_name"` + MailAddress string `json:"mailaddress"` + DSMLang string `json:"dsm_lang"` + TenantFlg bool `json:"tenant_flg"` + Status int `json:"status"` +} + +// Extract is a function that accepts a result +// and extracts a Host Based Security resource. +func (r GetResult) Extract() (*HostBasedSecurity, error) { + var h HostBasedSecurity + err := r.ExtractInto(&h) + return &h, err +} + +// ExtractInto interprets any commonResult as a Host Based Security if possible. +func (r GetResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Host Based Security. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// HostBasedOrder represents a Host Based Security's each order. +type HostBasedOrder struct { + ID string `json:"soId"` + Code string `json:"code"` + Message string `json:"message"` + Status int `json:"status"` +} diff --git a/v3/ecl/security_order/v3/host_based/testing/doc.go b/v3/ecl/security_order/v3/host_based/testing/doc.go new file mode 100644 index 0000000..085b2c7 --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains host based security unittests +package testing diff --git a/v3/ecl/security_order/v3/host_based/testing/fixtures.go b/v3/ecl/security_order/v3/host_based/testing/fixtures.go new file mode 100644 index 0000000..fbfd134 --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/testing/fixtures.go @@ -0,0 +1,139 @@ +package testing + +import ( + security "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/host_based" +) + +const getResponse = ` +{ + "code": "DEP-01", + "message": "Successful completion", + "region": "jp4", + "tenant_name": "Test Tenant", + "tenant_description": "Test Tenant", + "contract_id": "econ9999999999", + "service_order_service": "Managed Anti-Virus", + "max_agent_value": 1, + "customer_name": "Customer", + "time_zone": "Asia/Tokyo", + "mailaddress": "terraform@example.com", + "dsm_lang": "ja", + "tenant_flg": true, + "status": 1 +} +` + +var expectedResult = security.HostBasedSecurity{ + Code: "DEP-01", + Message: "Successful completion", + Region: "jp4", + TenantName: "Test Tenant", + TenantDescription: "Test Tenant", + ContractID: "econ9999999999", + ServiceOrderService: "Managed Anti-Virus", + MaxAgentValue: float64(1), + TimeZone: "Asia/Tokyo", + CustomerName: "Customer", + MailAddress: "terraform@example.com", + DSMLang: "ja", + TenantFlg: true, + Status: 1, +} + +var createRequest = ` +{ + "sokind": "N", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "service_order_service": "Managed Anti-Virus", + "max_agent_value": 1, + "mailaddress": "terraform@example.com", + "dsm_lang": "ja", + "time_zone": "Asia/Tokyo" +}` + +var createResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var createResult = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequestM1 = ` +{ + "sokind": "M1", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "mailaddress": "terraform@example.com", + "service_order_service": "Managed Anti-Virus" +}` + +var updateResponseM1 = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResultM1 = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequestM2 = ` +{ + "sokind": "M2", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "mailaddress": "terraform@example.com", + "max_agent_value": 10 +}` + +var updateResponseM2 = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResultM2 = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var deleteRequest = ` +{ + "sokind": "C", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "mailaddress": "terraform@example.com" +}` + +var deleteResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var deleteResult = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} diff --git a/v3/ecl/security_order/v3/host_based/testing/requests_test.go b/v3/ecl/security_order/v3/host_based/testing/requests_test.go new file mode 100644 index 0000000..fc0969e --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/testing/requests_test.go @@ -0,0 +1,146 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + security "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/host_based" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestGetHostBasedSecurity(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/ScreenEventHBSOrderInfoGet" + fmt.Println(url) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := security.Get(fakeclient.ServiceClient(), nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} + +func TestCreateHostBasedSecurity(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/SoEntryHBS", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + createOpts := security.CreateOpts{ + SOKind: "N", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + ServiceOrderService: "Managed Anti-Virus", + MaxAgentValue: 1, + MailAddress: "terraform@example.com", + DSMLang: "ja", + TimeZone: "Asia/Tokyo", + } + + actual, err := security.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createResult, actual) +} + +func TestUpdateHostBasedSecurityTypeM1(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryHBS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequestM1) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponseM1) + }) + + updateOpts := security.UpdateOpts{ + SOKind: "M1", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + MailAddress: "terraform@example.com", + } + + serviceOrderService := "Managed Anti-Virus" + updateOpts.ServiceOrderService = &serviceOrderService + actual, err := security.Update(fakeclient.ServiceClient(), updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResultM1, actual) +} + +func TestUpdateHostBasedSecurityTypeM2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryHBS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequestM2) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponseM2) + }) + + updateOpts := security.UpdateOpts{ + SOKind: "M2", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + MailAddress: "terraform@example.com", + } + + maxAgentValue := 10 + updateOpts.MaxAgentValue = &maxAgentValue + actual, err := security.Update(fakeclient.ServiceClient(), updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResultM2, actual) +} + +func TestDeleteDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryHBS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, deleteRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, deleteResponse) + }) + + deleteOpts := security.DeleteOpts{ + SOKind: "C", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + MailAddress: "terraform@example.com", + } + + actual, err := security.Delete(fakeclient.ServiceClient(), deleteOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &deleteResult, actual) +} diff --git a/v3/ecl/security_order/v3/host_based/urls.go b/v3/ecl/security_order/v3/host_based/urls.go new file mode 100644 index 0000000..0cdb692 --- /dev/null +++ b/v3/ecl/security_order/v3/host_based/urls.go @@ -0,0 +1,21 @@ +package host_based + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/ScreenEventHBSOrderInfoGet") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryHBS") +} + +func deleteURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryHBS") +} + +func updateURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryHBS") +} diff --git a/v3/ecl/security_order/v3/network_based_device_ha/doc.go b/v3/ecl/security_order/v3/network_based_device_ha/doc.go new file mode 100644 index 0000000..e5f191e --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/doc.go @@ -0,0 +1,2 @@ +// Package network_based_device_ha contains HA device functionality on security. +package network_based_device_ha diff --git a/v3/ecl/security_order/v3/network_based_device_ha/requests.go b/v3/ecl/security_order/v3/network_based_device_ha/requests.go new file mode 100644 index 0000000..07f2216 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/requests.go @@ -0,0 +1,162 @@ +package network_based_device_ha + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToHADeviceQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Locale string `q:"locale"` +} + +// ToHADeviceQuery formats a ListOpts into a query string. +func (opts ListOpts) ToHADeviceQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Devices to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToHADeviceQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return HADevicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToHADeviceCreateMap() (map[string]interface{}, error) +} + +// GtHostInCreate represents parameters used to create a HA Device. +type GtHostInCreate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + AZGroup string `json:"azgroup" required:"true"` + + HALink1NetworkID string `json:"halink1networkid" required:"true"` + HALink1SubnetID string `json:"halink1subnetid" required:"true"` + HALink1IPAddress string `json:"halink1ipaddress" required:"true"` + + HALink2NetworkID string `json:"halink2networkid" required:"true"` + HALink2SubnetID string `json:"halink2subnetid" required:"true"` + HALink2IPAddress string `json:"halink2ipaddress" required:"true"` +} + +// CreateOpts represents parameters used to create a device. +type CreateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + GtHost [2]GtHostInCreate `json:"gt_host" required:"true"` +} + +// ToHADeviceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToHADeviceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new device. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToHADeviceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to +// the Delete request. +type DeleteOptsBuilder interface { + ToHADeviceDeleteMap() (map[string]interface{}, error) +} + +// GtHostInDelete represents parameters used to delete a HA Device. +type GtHostInDelete struct { + HostName string `json:"hostname" required:"true"` +} + +// DeleteOpts represents parameters used to delete a device. +type DeleteOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [2]GtHostInDelete `json:"gt_host" required:"true"` +} + +// ToHADeviceDeleteMap formats a DeleteOpts into a delete request. +func (opts DeleteOpts) ToHADeviceDeleteMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Delete deletes a device. +func Delete(client *eclcloud.ServiceClient, deviceType string, opts DeleteOptsBuilder) (r DeleteResult) { + b, err := opts.ToHADeviceDeleteMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return + +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToHADeviceUpdateMap() (map[string]interface{}, error) +} + +// GtHostInUpdate represents parameters used to update a HA Device. +type GtHostInUpdate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + HostName string `json:"hostname" required:"true"` +} + +// UpdateOpts represents parameters to update a HA Device. +type UpdateOpts struct { + SOKind string `json:"sokind" required:"true"` + Locale string `json:"locale,omitempty"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [2]GtHostInUpdate `json:"gt_host" required:"true"` +} + +// ToHADeviceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToHADeviceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a device. +func Update(client *eclcloud.ServiceClient, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToHADeviceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(updateURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/security_order/v3/network_based_device_ha/results.go b/v3/ecl/security_order/v3/network_based_device_ha/results.go new file mode 100644 index 0000000..28db67e --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/results.go @@ -0,0 +1,104 @@ +package network_based_device_ha + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a HA Device resource. +func (r commonResult) Extract() (*HADeviceOrder, error) { + var sdo HADeviceOrder + err := r.ExtractInto(&sdo) + return &sdo, err +} + +// Extract interprets any commonResult as a HA Device if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a HA Device. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a HA Device. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a HA Device. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// HADevice represents the result of a each element in +// response of HA Device api result. +type HADevice struct { + ID int `json:"id"` + Cell []string `json:"cell"` +} + +// HADeviceOrder represents a HA Device's each order. +type HADeviceOrder struct { + ID string `json:"soId"` + Code string `json:"code"` + Message string `json:"message"` + Status int `json:"status"` +} + +// HADevicePage is the page returned by a pager +// when traversing over a collection of HA Device. +type HADevicePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of HA Device +// 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 (r HADevicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"ha_device_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a HADevicePage struct is empty. +func (r HADevicePage) IsEmpty() (bool, error) { + is, err := ExtractHADevices(r) + return len(is) == 0, err +} + +// ExtractHADevices accepts a Page struct, +// specifically a HADevicePage struct, and extracts the elements +// into a slice of HA Device structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractHADevices(r pagination.Page) ([]HADevice, error) { + var s []HADevice + err := ExtractHADevicesInto(r, &s) + return s, err +} + +// ExtractHADevicesInto interprets the results of a single page from a List() call, +// producing a slice of Device entities. +func ExtractHADevicesInto(r pagination.Page, v interface{}) error { + return r.(HADevicePage).Result.ExtractIntoSlicePtr(v, "rows") +} diff --git a/v3/ecl/security_order/v3/network_based_device_ha/testing/doc.go b/v3/ecl/security_order/v3/network_based_device_ha/testing/doc.go new file mode 100644 index 0000000..499badf --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains network based security HA device unittests +package testing diff --git a/v3/ecl/security_order/v3/network_based_device_ha/testing/fixtures.go b/v3/ecl/security_order/v3/network_based_device_ha/testing/fixtures.go new file mode 100644 index 0000000..e35d76a --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/testing/fixtures.go @@ -0,0 +1,149 @@ +package testing + +import ( + security "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/network_based_device_ha" +) + +const listResponse = ` +{ + "status": 1, + "code": "FOV-01", + "message": "Successful completion", + "records": 2, + "rows": [ + { + "cell": ["false", "1", "1902F60E", "CES12085", "FW_HA", "02", "ha", "zone1-groupa", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.3", "dummyNetworkID2", "dummySubnetID2", "192.168.2.3"], + "id": 1 + }, + { + "cell": ["false", "2", "1902F60E", "CES12086", "FW_HA", "02", "ha", "zone1-groupb", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.4", "dummyNetworkID2", "dummySubnetID2", "192.168.2.4"], + "id": 2 + } + ] +} +` + +var expectedDevicesSlice = []security.HADevice{firstDevice, secondDevice} + +var firstDevice = security.HADevice{ + ID: 1, + Cell: []string{ + "false", "1", "1902F60E", "CES12085", "FW_HA", "02", "ha", "zone1-groupa", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.3", "dummyNetworkID2", "dummySubnetID2", "192.168.2.3", + }, +} + +var secondDevice = security.HADevice{ + ID: 2, + Cell: []string{ + "false", "2", "1902F60E", "CES12086", "FW_HA", "02", "ha", "zone1-groupb", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.4", "dummyNetworkID2", "dummySubnetID2", "192.168.2.4", + }, +} + +var createRequest = ` +{ + "gt_host": [ + { + "azgroup": "zone1-groupa", + "licensekind": "02", + "operatingmode": "FW_HA", + "halink1ipaddress": "192.168.1.3", + "halink1networkid": "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + "halink1subnetid": "9a2116e2-52be-439c-9587-506a1a5d288d", + "halink2ipaddress": "192.168.2.3", + "halink2networkid": "a8df4d5f-8752-4574-a255-dc749acd458f", + "halink2subnetid": "a2ff5669-8422-421c-bb85-a6d691ecf223" + }, + { + "azgroup": "zone1-groupb", + "licensekind": "02", + "operatingmode": "FW_HA", + "halink1ipaddress": "192.168.1.4", + "halink1networkid": "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + "halink1subnetid": "9a2116e2-52be-439c-9587-506a1a5d288d", + "halink2ipaddress": "192.168.2.4", + "halink2networkid": "a8df4d5f-8752-4574-a255-dc749acd458f", + "halink2subnetid": "a2ff5669-8422-421c-bb85-a6d691ecf223" + } + ], + "locale": "ja", + "sokind": "AH", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var createResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var createResult = security.HADeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811", + "licensekind": "08", + "operatingmode": "UTM_HA" + }, + { + "hostname": "CES11812", + "licensekind": "08", + "operatingmode": "UTM_HA" + } + ], + "locale": "en", + "sokind": "MH", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var updateResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResult = security.HADeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var deleteRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811" + }, + { + "hostname": "CES11812" + } + ], + "sokind": "DH", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var deleteResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var deleteResult = security.HADeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} diff --git a/v3/ecl/security_order/v3/network_based_device_ha/testing/requests_test.go b/v3/ecl/security_order/v3/network_based_device_ha/testing/requests_test.go new file mode 100644 index 0000000..c2cc2b2 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/testing/requests_test.go @@ -0,0 +1,180 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + security "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/network_based_device_ha" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGHADeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := security.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security.ExtractHADevices(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDevicesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGHADeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := security.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allDevices, err := security.ExtractHADevices(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDevices)) +} + +func TestCreateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/SoEntryFGHA", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + gtHost1 := security.GtHostInCreate{ + AZGroup: "zone1-groupa", + LicenseKind: "02", + OperatingMode: "FW_HA", + HALink1IPAddress: "192.168.1.3", + HALink1NetworkID: "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + HALink1SubnetID: "9a2116e2-52be-439c-9587-506a1a5d288d", + HALink2IPAddress: "192.168.2.3", + HALink2NetworkID: "a8df4d5f-8752-4574-a255-dc749acd458f", + HALink2SubnetID: "a2ff5669-8422-421c-bb85-a6d691ecf223", + } + + gtHost2 := security.GtHostInCreate{ + AZGroup: "zone1-groupb", + LicenseKind: "02", + OperatingMode: "FW_HA", + HALink1IPAddress: "192.168.1.4", + HALink1NetworkID: "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + HALink1SubnetID: "9a2116e2-52be-439c-9587-506a1a5d288d", + HALink2IPAddress: "192.168.2.4", + HALink2NetworkID: "a8df4d5f-8752-4574-a255-dc749acd458f", + HALink2SubnetID: "a2ff5669-8422-421c-bb85-a6d691ecf223", + } + + createOpts := security.CreateOpts{ + SOKind: "AH", + Locale: "ja", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [2]security.GtHostInCreate{gtHost1, gtHost2}, + } + + actual, err := security.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createResult, actual) +} + +func TestUpdateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGHA" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponse) + }) + + gtHost1 := security.GtHostInUpdate{ + OperatingMode: "UTM_HA", + LicenseKind: "08", + HostName: "CES11811", + } + + gtHost2 := security.GtHostInUpdate{ + OperatingMode: "UTM_HA", + LicenseKind: "08", + HostName: "CES11812", + } + + updateOpts := security.UpdateOpts{ + SOKind: "MH", + Locale: "en", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [2]security.GtHostInUpdate{gtHost1, gtHost2}, + } + + actual, err := security.Update(fakeclient.ServiceClient(), updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResult, actual) +} + +func TestDeleteDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGHA" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, deleteRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, deleteResponse) + }) + + gtHost1 := security.GtHostInDelete{ + HostName: "CES11811", + } + + gtHost2 := security.GtHostInDelete{ + HostName: "CES11812", + } + + deleteOpts := security.DeleteOpts{ + SOKind: "DH", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [2]security.GtHostInDelete{gtHost1, gtHost2}, + } + + actual, err := security.Delete(fakeclient.ServiceClient(), "CES11811", deleteOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &deleteResult, actual) +} diff --git a/v3/ecl/security_order/v3/network_based_device_ha/urls.go b/v3/ecl/security_order/v3/network_based_device_ha/urls.go new file mode 100644 index 0000000..2e6b015 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_ha/urls.go @@ -0,0 +1,21 @@ +package network_based_device_ha + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/ScreenEventFGHADeviceGet") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryFGHA") +} + +func deleteURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryFGHA") +} + +func updateURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryFGHA") +} diff --git a/v3/ecl/security_order/v3/network_based_device_single/doc.go b/v3/ecl/security_order/v3/network_based_device_single/doc.go new file mode 100644 index 0000000..b752fd5 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/doc.go @@ -0,0 +1,2 @@ +// Package network_based_device_single contains single device functionality on security. +package network_based_device_single diff --git a/v3/ecl/security_order/v3/network_based_device_single/requests.go b/v3/ecl/security_order/v3/network_based_device_single/requests.go new file mode 100644 index 0000000..5c37b03 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/requests.go @@ -0,0 +1,154 @@ +package network_based_device_single + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToSingleDeviceQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Locale string `q:"locale"` +} + +// ToSingleDeviceQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSingleDeviceQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Devices to which the current token has access. +func List(client *eclcloud.ServiceClient, deviceType string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, deviceType) + if opts != nil { + query, err := opts.ToSingleDeviceQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SingleDevicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToSingleDeviceCreateMap() (map[string]interface{}, error) +} + +// GtHostInCreate represents parameters used to create a Single Device. +type GtHostInCreate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + AZGroup string `json:"azgroup" required:"true"` +} + +// CreateOpts represents parameters used to create a device. +type CreateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + GtHost [1]GtHostInCreate `json:"gt_host" required:"true"` +} + +// ToSingleDeviceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToSingleDeviceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new device. +func Create(client *eclcloud.ServiceClient, deviceType string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSingleDeviceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, deviceType), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to +// the Delete request. +type DeleteOptsBuilder interface { + ToSingleDeviceDeleteMap() (map[string]interface{}, error) +} + +// GtHostInDelete represents parameters used to delete a Single Device. +type GtHostInDelete struct { + HostName string `json:"hostname" required:"true"` +} + +// DeleteOpts represents parameters used to delete a device. +type DeleteOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [1]GtHostInDelete `json:"gt_host" required:"true"` +} + +// ToSingleDeviceDeleteMap formats a DeleteOpts into a delete request. +func (opts DeleteOpts) ToSingleDeviceDeleteMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Delete deletes a device. +func Delete(client *eclcloud.ServiceClient, deviceType string, opts DeleteOptsBuilder) (r DeleteResult) { + b, err := opts.ToSingleDeviceDeleteMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, deviceType), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return + +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToSingleDeviceUpdateMap() (map[string]interface{}, error) +} + +// GtHostInUpdate represents parameters used to update a Single Device. +type GtHostInUpdate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + HostName string `json:"hostname" required:"true"` +} + +// UpdateOpts represents parameters to update a Single Device. +type UpdateOpts struct { + SOKind string `json:"sokind" required:"true"` + Locale string `json:"locale,omitempty"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [1]GtHostInUpdate `json:"gt_host" required:"true"` +} + +// ToSingleDeviceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToSingleDeviceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a device. +func Update(client *eclcloud.ServiceClient, deviceType string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSingleDeviceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(updateURL(client, deviceType), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/security_order/v3/network_based_device_single/results.go b/v3/ecl/security_order/v3/network_based_device_single/results.go new file mode 100644 index 0000000..48ded15 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/results.go @@ -0,0 +1,104 @@ +package network_based_device_single + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Single Device resource. +func (r commonResult) Extract() (*SingleDeviceOrder, error) { + var sdo SingleDeviceOrder + err := r.ExtractInto(&sdo) + return &sdo, err +} + +// Extract interprets any commonResult as a Single Device if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Single Device. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Single Device. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Single Device. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// SingleDevice represents the result of a each element in +// response of single device api result. +type SingleDevice struct { + ID int `json:"id"` + Cell []string `json:"cell"` +} + +// SingleDeviceOrder represents a Single Device's each order. +type SingleDeviceOrder struct { + ID string `json:"soId"` + Code string `json:"code"` + Message string `json:"message"` + Status int `json:"status"` +} + +// SingleDevicePage is the page returned by a pager +// when traversing over a collection of Single Device. +type SingleDevicePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of Single Device +// 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 (r SingleDevicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"single_device_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SingleDevicePage struct is empty. +func (r SingleDevicePage) IsEmpty() (bool, error) { + is, err := ExtractSingleDevices(r) + return len(is) == 0, err +} + +// ExtractSingleDevices accepts a Page struct, +// specifically a SingleDevicePage struct, and extracts the elements +// into a slice of Single Device structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractSingleDevices(r pagination.Page) ([]SingleDevice, error) { + var s []SingleDevice + err := ExtractSingleDevicesInto(r, &s) + return s, err +} + +// ExtractSingleDevicesInto interprets the results of a single page from a List() call, +// producing a slice of Device entities. +func ExtractSingleDevicesInto(r pagination.Page, v interface{}) error { + return r.(SingleDevicePage).Result.ExtractIntoSlicePtr(v, "rows") +} diff --git a/v3/ecl/security_order/v3/network_based_device_single/testing/doc.go b/v3/ecl/security_order/v3/network_based_device_single/testing/doc.go new file mode 100644 index 0000000..dd8abff --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains network based security single device unittests +package testing diff --git a/v3/ecl/security_order/v3/network_based_device_single/testing/fixtures.go b/v3/ecl/security_order/v3/network_based_device_single/testing/fixtures.go new file mode 100644 index 0000000..5961be1 --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/testing/fixtures.go @@ -0,0 +1,124 @@ +package testing + +import ( + security "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/network_based_device_single" +) + +const listResponse = ` +{ + "status": 1, + "code": "FOV-01", + "message": "Successful completion", + "records": 2, + "rows": [ + { + "id": 1, + "cell": ["false", "1", "CES11810", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1"] + }, + { + "id": 2, + "cell": ["false", "1", "CES11811", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1"] + } + ] +} +` + +var expectedDevicesSlice = []security.SingleDevice{firstDevice, secondDevice} + +var firstDevice = security.SingleDevice{ + ID: 1, + Cell: []string{ + "false", "1", "CES11810", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1", + }, +} + +var secondDevice = security.SingleDevice{ + ID: 2, + Cell: []string{ + "false", "1", "CES11811", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1", + }, +} + +var createRequest = ` +{ + "gt_host":[ + { + "azgroup": "zone1-groupb", + "licensekind":"02", + "operatingmode":"FW" + } + ], + "locale": "ja", + "sokind": "A", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var createResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var createResult = security.SingleDeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811", + "licensekind": "08", + "operatingmode": "UTM" + } + ], + "locale": "en", + "sokind": "M", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var updateResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResult = security.SingleDeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var deleteRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811" + } + ], + "sokind": "D", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var deleteResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var deleteResult = security.SingleDeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} diff --git a/v3/ecl/security_order/v3/network_based_device_single/testing/requests_test.go b/v3/ecl/security_order/v3/network_based_device_single/testing/requests_test.go new file mode 100644 index 0000000..f48de9b --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/testing/requests_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + security "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/network_based_device_single" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGSDeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := security.List(fakeclient.ServiceClient(), "UTM", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security.ExtractSingleDevices(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDevicesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGSDeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := security.List(fakeclient.ServiceClient(), "UTM", nil).AllPages() + th.AssertNoErr(t, err) + allDevices, err := security.ExtractSingleDevices(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDevices)) +} + +func TestCreateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/SoEntryFGS", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + gtHost := security.GtHostInCreate{ + AZGroup: "zone1-groupb", + LicenseKind: "02", + OperatingMode: "FW", + } + createOpts := security.CreateOpts{ + SOKind: "A", + Locale: "ja", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [1]security.GtHostInCreate{gtHost}, + } + + actual, err := security.Create(fakeclient.ServiceClient(), "UTM", createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createResult, actual) +} + +func TestUpdateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponse) + }) + + gtHost := security.GtHostInUpdate{ + OperatingMode: "UTM", + LicenseKind: "08", + HostName: "CES11811", + } + updateOpts := security.UpdateOpts{ + SOKind: "M", + Locale: "en", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [1]security.GtHostInUpdate{gtHost}, + } + + actual, err := security.Update(fakeclient.ServiceClient(), "CES11811", updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResult, actual) +} + +func TestDeleteDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, deleteRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, deleteResponse) + }) + + gtHost := security.GtHostInDelete{ + HostName: "CES11811", + } + deleteOpts := security.DeleteOpts{ + SOKind: "D", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [1]security.GtHostInDelete{gtHost}, + } + + actual, err := security.Delete(fakeclient.ServiceClient(), "CES11811", deleteOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &deleteResult, actual) +} diff --git a/v3/ecl/security_order/v3/network_based_device_single/urls.go b/v3/ecl/security_order/v3/network_based_device_single/urls.go new file mode 100644 index 0000000..1f2ce5b --- /dev/null +++ b/v3/ecl/security_order/v3/network_based_device_single/urls.go @@ -0,0 +1,38 @@ +package network_based_device_single + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +func getURLPartFromDeviceType(deviceType string) string { + if deviceType == "WAF" { + return "WAF" + } + return "S" +} + +func listURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/ScreenEventFG%sDeviceGet", part) + return client.ServiceURL(url) +} + +func createURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/SoEntryFG%s", part) + return client.ServiceURL(url) +} + +func deleteURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/SoEntryFG%s", part) + return client.ServiceURL(url) +} + +func updateURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/SoEntryFG%s", part) + return client.ServiceURL(url) +} diff --git a/v3/ecl/security_order/v3/service_order_status/doc.go b/v3/ecl/security_order/v3/service_order_status/doc.go new file mode 100644 index 0000000..f55fa76 --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/doc.go @@ -0,0 +1,2 @@ +// Package service_order_status contains order management functionality on security +package service_order_status diff --git a/v3/ecl/security_order/v3/service_order_status/requests.go b/v3/ecl/security_order/v3/service_order_status/requests.go new file mode 100644 index 0000000..83834de --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/requests.go @@ -0,0 +1,36 @@ +package service_order_status + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// GetOptsBuilder allows extensions to add additional parameters to +// the order progress API request +type GetOptsBuilder interface { + ToServiceOrderQuery() (string, error) +} + +// GetOpts represents result of order progress API response. +type GetOpts struct { + TenantID string `q:"tenant_id"` + Locale string `q:"locale"` + SoID string `q:"soid"` +} + +// ToServiceOrderQuery formats a GetOpts into a query string. +func (opts GetOpts) ToServiceOrderQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves details of an order progress, by SoId. +func Get(client *eclcloud.ServiceClient, deviceType string, opts GetOptsBuilder) (r GetResult) { + url := getURL(client, deviceType) + if opts != nil { + query, _ := opts.ToServiceOrderQuery() + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/v3/ecl/security_order/v3/service_order_status/results.go b/v3/ecl/security_order/v3/service_order_status/results.go new file mode 100644 index 0000000..88b8196 --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/results.go @@ -0,0 +1,36 @@ +package service_order_status + +import ( + "github.com/nttcom/eclcloud/v3" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts an Order Progress resource. +func (r commonResult) Extract() (*OrderProgress, error) { + var sd OrderProgress + err := r.ExtractInto(&sd) + return &sd, err +} + +// Extract interprets any commonResult as an Order Progress, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as an Order. +type GetResult struct { + commonResult +} + +// OrderProgress represents an Order Progress response. +type OrderProgress struct { + Status int `json:"status"` + Code string `json:"code"` + Message string `json:"message"` + ProgressRate int `json:"progressRate"` +} diff --git a/v3/ecl/security_order/v3/service_order_status/testing/doc.go b/v3/ecl/security_order/v3/service_order_status/testing/doc.go new file mode 100644 index 0000000..6b493c6 --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains service order status unit tests +package testing diff --git a/v3/ecl/security_order/v3/service_order_status/testing/fixtures.go b/v3/ecl/security_order/v3/service_order_status/testing/fixtures.go new file mode 100644 index 0000000..eccb01d --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/testing/fixtures.go @@ -0,0 +1,21 @@ +package testing + +import ( + order "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/service_order_status" +) + +const getResponse = ` +{ + "status": 1, + "code": "FOV-05", + "message": "We accepted the order. Please wait", + "progressRate": 45 +} +` + +var expectedResult = order.OrderProgress{ + Status: 1, + Code: "FOV-05", + Message: "We accepted the order. Please wait", + ProgressRate: 45, +} diff --git a/v3/ecl/security_order/v3/service_order_status/testing/requests_test.go b/v3/ecl/security_order/v3/service_order_status/testing/requests_test.go new file mode 100644 index 0000000..e1b7d7d --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/testing/requests_test.go @@ -0,0 +1,31 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + order "github.com/nttcom/eclcloud/v3/ecl/security_order/v3/service_order_status" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestGetOrder(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/ScreenEventFGSOrderProgressRate" + fmt.Println(url) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := order.Get(fakeclient.ServiceClient(), "UTM", nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} diff --git a/v3/ecl/security_order/v3/service_order_status/urls.go b/v3/ecl/security_order/v3/service_order_status/urls.go new file mode 100644 index 0000000..a4d5005 --- /dev/null +++ b/v3/ecl/security_order/v3/service_order_status/urls.go @@ -0,0 +1,24 @@ +package service_order_status + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient, deviceType string) string { + var part string + switch deviceType { + case "WAF": + part = "FGWAF" + break + case "HostBased": + part = "HBS" + break + default: + part = "FGS" + } + + url := fmt.Sprintf("API/ScreenEvent%sOrderProgressRate", part) + return client.ServiceURL(url) +} diff --git a/v3/ecl/security_portal/v3/device_interfaces/doc.go b/v3/ecl/security_portal/v3/device_interfaces/doc.go new file mode 100644 index 0000000..4ae5098 --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/doc.go @@ -0,0 +1,2 @@ +// Package device_interfaces contains device management functionality in security portal API +package device_interfaces diff --git a/v3/ecl/security_portal/v3/device_interfaces/requests.go b/v3/ecl/security_portal/v3/device_interfaces/requests.go new file mode 100644 index 0000000..f8071df --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/requests.go @@ -0,0 +1,40 @@ +package device_interfaces + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToDeviceInterfaceQuery() (string, error) +} + +// ListOpts converts tenant id and token as query string +type ListOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToDeviceInterfaceQuery formats a ListOpts into a query string. +func (opts ListOpts) ToDeviceInterfaceQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// device interfaces. +func List(client *eclcloud.ServiceClient, serverUUID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, serverUUID) + if opts != nil { + query, err := opts.ToDeviceInterfaceQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return DeviceInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v3/ecl/security_portal/v3/device_interfaces/results.go b/v3/ecl/security_portal/v3/device_interfaces/results.go new file mode 100644 index 0000000..0da8c63 --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/results.go @@ -0,0 +1,65 @@ +package device_interfaces + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// DeviceInterface represents the result of a each element in +// response of device interface api result. +type DeviceInterface struct { + OSIPAddress string `json:"os_ip_address"` + MSAPortID string `json:"msa_port_id"` + OSPortName string `json:"os_port_name"` + OSPortID string `json:"os_port_id"` + OSNetworkID string `json:"os_network_id"` + OSPortStatus string `json:"os_port_status"` + OSMACAddress string `json:"os_mac_address"` + OSSubnetID string `json:"os_subnet_id"` +} + +// DeviceInterfacePage is the page returned by a pager +// when traversing over a collection of Device Interface. +type DeviceInterfacePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of Single Device Interface +// 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 (r DeviceInterfacePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"device_interfaces"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a DeviceInterfacePage struct is empty. +func (r DeviceInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractDeviceInterfaces(r) + return len(is) == 0, err +} + +// ExtractDeviceInterfaces accepts a Page struct, +// specifically a DeviceInterfacePage struct, and extracts the elements +// into a slice of Device Interface structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractDeviceInterfaces(r pagination.Page) ([]DeviceInterface, error) { + var d []DeviceInterface + err := ExtractDeviceInterfacesInto(r, &d) + return d, err +} + +// ExtractDeviceInterfacesInto interprets the results of a single page from a List() call, +// producing a slice of Device Interface entities. +func ExtractDeviceInterfacesInto(r pagination.Page, v interface{}) error { + return r.(DeviceInterfacePage).Result.ExtractIntoSlicePtr(v, "device_interfaces") +} diff --git a/v3/ecl/security_portal/v3/device_interfaces/testing/doc.go b/v3/ecl/security_portal/v3/device_interfaces/testing/doc.go new file mode 100644 index 0000000..7c9a653 --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains device interfaces unit tests +package testing diff --git a/v3/ecl/security_portal/v3/device_interfaces/testing/fixtures.go b/v3/ecl/security_portal/v3/device_interfaces/testing/fixtures.go new file mode 100644 index 0000000..6950b58 --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/testing/fixtures.go @@ -0,0 +1,60 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/device_interfaces" +) + +const deviceUUID = "cad6e00a-2af9-491c-b732-ca5688d147f5" + +const listResponse = ` +{ + "device_interfaces": [ + { + "os_ip_address": "192.168.1.50", + "msa_port_id": "port4", + "os_port_name": "port4-CES11892", + "os_port_id": "82ebe045-9c9a-4088-8b33-cb0d590079aa", + "os_network_id": "5ef9c597-15fe-431c-84b9-88d00d567202", + "os_port_status": "ACTIVE", + "os_mac_address": "fa:16:3e:05:ff:66", + "os_subnet_id": "48ea24c7-fe48-4a54-9ed0-528aa09cebc7" + }, + { + "os_ip_address": "192.168.2.50", + "msa_port_id": "port7", + "os_port_name": "port7-CES11892", + "os_port_id": "82ebe045-9c9a-4088-8b33-cb0d590079aa", + "os_network_id": "5ef9c597-15fe-431c-84b9-88d00d567203", + "os_port_status": "ACTIVE", + "os_mac_address": "fa:16:3e:05:ff:67", + "os_subnet_id": "48ea24c7-fe48-4a54-9ed0-528aa09cebc8" + } + ] +} +` + +var expectedDeviceInterfacesSlice = []device_interfaces.DeviceInterface{ + firstDeviceInterface, secondDeviceInterface, +} + +var firstDeviceInterface = device_interfaces.DeviceInterface{ + OSIPAddress: "192.168.1.50", + MSAPortID: "port4", + OSPortName: "port4-CES11892", + OSPortID: "82ebe045-9c9a-4088-8b33-cb0d590079aa", + OSNetworkID: "5ef9c597-15fe-431c-84b9-88d00d567202", + OSPortStatus: "ACTIVE", + OSMACAddress: "fa:16:3e:05:ff:66", + OSSubnetID: "48ea24c7-fe48-4a54-9ed0-528aa09cebc7", +} + +var secondDeviceInterface = device_interfaces.DeviceInterface{ + OSIPAddress: "192.168.2.50", + MSAPortID: "port7", + OSPortName: "port7-CES11892", + OSPortID: "82ebe045-9c9a-4088-8b33-cb0d590079aa", + OSNetworkID: "5ef9c597-15fe-431c-84b9-88d00d567203", + OSPortStatus: "ACTIVE", + OSMACAddress: "fa:16:3e:05:ff:67", + OSSubnetID: "48ea24c7-fe48-4a54-9ed0-528aa09cebc8", +} diff --git a/v3/ecl/security_portal/v3/device_interfaces/testing/requests_test.go b/v3/ecl/security_portal/v3/device_interfaces/testing/requests_test.go new file mode 100644 index 0000000..45b873d --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/testing/requests_test.go @@ -0,0 +1,57 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/device_interfaces" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListDeviceInterfaces(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/ecl-api/devices/%s/interfaces", deviceUUID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := device_interfaces.List(fakeclient.ServiceClient(), deviceUUID, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := device_interfaces.ExtractDeviceInterfaces(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDeviceInterfacesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceInterfaceAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/ecl-api/devices/%s/interfaces", deviceUUID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := device_interfaces.List(fakeclient.ServiceClient(), deviceUUID, nil).AllPages() + th.AssertNoErr(t, err) + allDeviceInterfaces, err := device_interfaces.ExtractDeviceInterfaces(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDeviceInterfaces)) +} diff --git a/v3/ecl/security_portal/v3/device_interfaces/urls.go b/v3/ecl/security_portal/v3/device_interfaces/urls.go new file mode 100644 index 0000000..987c4d4 --- /dev/null +++ b/v3/ecl/security_portal/v3/device_interfaces/urls.go @@ -0,0 +1,12 @@ +package device_interfaces + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +func listURL(client *eclcloud.ServiceClient, serverUUID string) string { + url := fmt.Sprintf("ecl-api/devices/%s/interfaces", serverUUID) + return client.ServiceURL(url) +} diff --git a/v3/ecl/security_portal/v3/devices/doc.go b/v3/ecl/security_portal/v3/devices/doc.go new file mode 100644 index 0000000..6a88b4d --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/doc.go @@ -0,0 +1,2 @@ +// Package devices contains device management functionality in security portal API +package devices diff --git a/v3/ecl/security_portal/v3/devices/requests.go b/v3/ecl/security_portal/v3/devices/requests.go new file mode 100644 index 0000000..b6f5140 --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/requests.go @@ -0,0 +1,40 @@ +package devices + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToDevicesQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToDevicesQuery formats a ListOpts into a query string. +func (opts ListOpts) ToDevicesQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over +// a collection of devices. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToDevicesQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return DevicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v3/ecl/security_portal/v3/devices/results.go b/v3/ecl/security_portal/v3/devices/results.go new file mode 100644 index 0000000..03c9ee7 --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/results.go @@ -0,0 +1,64 @@ +package devices + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Device represents the result of a each element in +// response of device api result. +type Device struct { + MSADeviceID string `json:"msa_device_id"` + OSServerID string `json:"os_server_id"` + OSServerName string `json:"os_server_name"` + OSAvailabilityZone string `json:"os_availability_zone"` + OSAdminUserName string `json:"os_admin_username"` + MSADeviceType string `json:"msa_device_type"` + OSServerStatus string `json:"os_server_status"` +} + +// DevicePage is the page returned by a pager +// when traversing over a collection of Device. +type DevicePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of Device +// 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 (r DevicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"devices"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a Device struct is empty. +func (r DevicePage) IsEmpty() (bool, error) { + is, err := ExtractDevices(r) + return len(is) == 0, err +} + +// ExtractDevices accepts a Page struct, +// specifically a DevicePage struct, and extracts the elements +// into a slice of Device structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractDevices(r pagination.Page) ([]Device, error) { + var d []Device + err := ExtractDevicesInto(r, &d) + return d, err +} + +// ExtractDevicesInto interprets the results of a single page from a List() call, +// producing a slice of Device entities. +func ExtractDevicesInto(r pagination.Page, v interface{}) error { + return r.(DevicePage).Result.ExtractIntoSlicePtr(v, "devices") +} diff --git a/v3/ecl/security_portal/v3/devices/testing/doc.go b/v3/ecl/security_portal/v3/devices/testing/doc.go new file mode 100644 index 0000000..d541ffa --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains devices unit tests +package testing diff --git a/v3/ecl/security_portal/v3/devices/testing/fixtures.go b/v3/ecl/security_portal/v3/devices/testing/fixtures.go new file mode 100644 index 0000000..0ea1c38 --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/testing/fixtures.go @@ -0,0 +1,51 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/devices" +) + +const listResponse = ` +{ + "devices": [ + { + "msa_device_id": "CES11810", + "os_server_id": "392a90bf-2c1b-45fd-8221-096894fff39d", + "os_server_name": "UTM-CES11878", + "os_availability_zone": "zone1-groupb", + "os_admin_username": "jp4_sdp_mss_utm_admin", + "msa_device_type": "FW", + "os_server_status": "ACTIVE" + }, + { + "msa_device_id": "CES11811", + "os_server_id": "12768064-e7c9-44d1-b01d-e66f138a278e", + "os_server_name": "WAF-CES11816", + "os_availability_zone": "zone1-groupb", + "os_admin_username": "jp4_sdp_mss_utm_admin", + "msa_device_type": "WAF", + "os_server_status": "ACTIVE" + } + ] +}` + +var expectedDevicesSlice = []devices.Device{firstDevice, secondDevice} + +var firstDevice = devices.Device{ + MSADeviceID: "CES11810", + OSServerID: "392a90bf-2c1b-45fd-8221-096894fff39d", + OSServerName: "UTM-CES11878", + OSAvailabilityZone: "zone1-groupb", + OSAdminUserName: "jp4_sdp_mss_utm_admin", + MSADeviceType: "FW", + OSServerStatus: "ACTIVE", +} + +var secondDevice = devices.Device{ + MSADeviceID: "CES11811", + OSServerID: "12768064-e7c9-44d1-b01d-e66f138a278e", + OSServerName: "WAF-CES11816", + OSAvailabilityZone: "zone1-groupb", + OSAdminUserName: "jp4_sdp_mss_utm_admin", + MSADeviceType: "WAF", + OSServerStatus: "ACTIVE", +} diff --git a/v3/ecl/security_portal/v3/devices/testing/requests_test.go b/v3/ecl/security_portal/v3/devices/testing/requests_test.go new file mode 100644 index 0000000..0be8900 --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/testing/requests_test.go @@ -0,0 +1,55 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/devices" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListDevices(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ecl-api/devices", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := devices.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := devices.ExtractDevices(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDevicesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ecl-api/devices", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := devices.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allDevices, err := devices.ExtractDevices(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDevices)) +} diff --git a/v3/ecl/security_portal/v3/devices/urls.go b/v3/ecl/security_portal/v3/devices/urls.go new file mode 100644 index 0000000..da31626 --- /dev/null +++ b/v3/ecl/security_portal/v3/devices/urls.go @@ -0,0 +1,9 @@ +package devices + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("ecl-api/devices") +} diff --git a/v3/ecl/security_portal/v3/ha_ports/doc.go b/v3/ecl/security_portal/v3/ha_ports/doc.go new file mode 100644 index 0000000..6412766 --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/doc.go @@ -0,0 +1,2 @@ +// Package ha_ports contains port management functionality in security portal API +package ha_ports diff --git a/v3/ecl/security_portal/v3/ha_ports/requests.go b/v3/ecl/security_portal/v3/ha_ports/requests.go new file mode 100644 index 0000000..d1f538e --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/requests.go @@ -0,0 +1,78 @@ +package ha_ports + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// SinglePort represents parameters to update a Single Port. +type SinglePort struct { + EnablePort string `json:"enable_port" required:"true"` + IPAddress []string `json:"ip_address,omitempty"` + NetworkID string `json:"network_id,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` + MTU string `json:"mtu,omitempty"` + Comment string `json:"comment,omitempty"` + + EnablePing string `json:"enable_ping,omitempty"` + VRRPGroupID string `json:"vrrp_grp_id,omitempty"` + VRRPID string `json:"vrrp_id,omitempty"` + VRRPIPAddress string `json:"vrrp_ip,omitempty"` + Preempt string `json:"preempt,omitempty"` +} + +// UpdateOpts represents options used to update a port. +type UpdateOpts struct { + Port []SinglePort `json:"port" required:"true"` +} + +// ToPortUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// UpdateQueryOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateQueryOptsBuilder interface { + ToUpdateQuery() (string, error) +} + +// UpdateQueryOpts represents query strings for updating port. +type UpdateQueryOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToUpdateQuery formats a ListOpts into a query string. +func (opts UpdateQueryOpts) ToUpdateQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Update modifies the attributes of a port. +func Update(client *eclcloud.ServiceClient, + hostName string, + opts UpdateOptsBuilder, + qOpts UpdateQueryOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + + url := updateURL(client, hostName) + if qOpts != nil { + query, _ := qOpts.ToUpdateQuery() + url += query + } + + _, r.Err = client.Put(url, &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/security_portal/v3/ha_ports/results.go b/v3/ecl/security_portal/v3/ha_ports/results.go new file mode 100644 index 0000000..dd992f5 --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/results.go @@ -0,0 +1,69 @@ +package ha_ports + +import ( + "encoding/json" + "strconv" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Port resource. +func (r commonResult) Extract() (*UpdateProcess, error) { + var p UpdateProcess + err := r.ExtractInto(&p) + return &p, err +} + +// Extract interprets any commonResult as a Port if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// UpdateProcess represents the result of a each element in +// response of port api result. +type UpdateProcess struct { + Message string `json:"message"` + ProcessID int `json:"processId"` + ID string `json:"-"` +} + +// ProcessPage is the page returned by a pager +// when traversing over a collection of Single Port. +type ProcessPage struct { + pagination.LinkedPageBase +} + +// UnmarshalJSON function overrides original functionality, +// to parse processId as unique identifier of process. +// Note: +// ID parameter in each struct must be string, +// but in api result of process polling API, +// processId is returned as integer value. +// This function solves this problem. +func (r *UpdateProcess) UnmarshalJSON(b []byte) error { + type tmp UpdateProcess + var s struct { + tmp + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = UpdateProcess(s.tmp) + + r.ID = strconv.Itoa(r.ProcessID) + + return err +} diff --git a/v3/ecl/security_portal/v3/ha_ports/testing/doc.go b/v3/ecl/security_portal/v3/ha_ports/testing/doc.go new file mode 100644 index 0000000..134142d --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains ports unit tests +package testing diff --git a/v3/ecl/security_portal/v3/ha_ports/testing/fixtures.go b/v3/ecl/security_portal/v3/ha_ports/testing/fixtures.go new file mode 100644 index 0000000..e68924b --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/testing/fixtures.go @@ -0,0 +1,29 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/ports" +) + +const updateRequest = `{ + "port": [ + { + "comment": "port 0 comment", + "enable_port":"true", + "ip_address": "192.168.1.50/24", + "mtu":"1500", + "network_id": "32314bd2-3583-4fb9-b622-9b121e04e007", + "subnet_id": "7fd77711-abae-4828-93f1-f3d682a8771f" + } + ] +}` + +const updateResponse = `{ + "message": "The process launch request has been accepted", + "processId": 85385 +}` + +var expectedResult = ports.UpdateProcess{ + Message: "The process launch request has been accepted", + ProcessID: 85385, + ID: "85385", +} diff --git a/v3/ecl/security_portal/v3/ha_ports/testing/requests_test.go b/v3/ecl/security_portal/v3/ha_ports/testing/requests_test.go new file mode 100644 index 0000000..99ed1d8 --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/testing/requests_test.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/ports" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestUpdatePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/ecl-api/ports/utm/CES11995" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, updateResponse) + }) + + updateOpts := ports.UpdateOpts{ + Port: []ports.SinglePort{ + { + Comment: "port 0 comment", + EnablePort: "true", + IPAddress: "192.168.1.50/24", + MTU: "1500", + NetworkID: "32314bd2-3583-4fb9-b622-9b121e04e007", + SubnetID: "7fd77711-abae-4828-93f1-f3d682a8771f", + }, + }, + } + + actual, err := ports.Update( + fakeclient.ServiceClient(), + "utm", + "CES11995", + updateOpts, + nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} diff --git a/v3/ecl/security_portal/v3/ha_ports/urls.go b/v3/ecl/security_portal/v3/ha_ports/urls.go new file mode 100644 index 0000000..3e54b13 --- /dev/null +++ b/v3/ecl/security_portal/v3/ha_ports/urls.go @@ -0,0 +1,12 @@ +package ha_ports + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +func updateURL(client *eclcloud.ServiceClient, hostName string) string { + url := fmt.Sprintf("ecl-api/ports/utm/ha/%s", hostName) + return client.ServiceURL(url) +} diff --git a/v3/ecl/security_portal/v3/ports/doc.go b/v3/ecl/security_portal/v3/ports/doc.go new file mode 100644 index 0000000..6c06631 --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/doc.go @@ -0,0 +1,2 @@ +// Package ports contains port management functionality in security portal API +package ports diff --git a/v3/ecl/security_portal/v3/ports/requests.go b/v3/ecl/security_portal/v3/ports/requests.go new file mode 100644 index 0000000..9428d5f --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/requests.go @@ -0,0 +1,73 @@ +package ports + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// SinglePort represents parameters to update a Single Port. +type SinglePort struct { + EnablePort string `json:"enable_port" required:"true"` + IPAddress string `json:"ip_address,omitempty"` + NetworkID string `json:"network_id,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` + MTU string `json:"mtu,omitempty"` + Comment string `json:"comment,omitempty"` +} + +// UpdateOpts represents options used to update a port. +type UpdateOpts struct { + Port []SinglePort `json:"port" required:"true"` +} + +// ToPortUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// UpdateQueryOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateQueryOptsBuilder interface { + ToUpdateQuery() (string, error) +} + +// UpdateQueryOpts represents query strings for updating port. +type UpdateQueryOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToUpdateQuery formats a ListOpts into a query string. +func (opts UpdateQueryOpts) ToUpdateQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Update modifies the attributes of a port. +func Update(client *eclcloud.ServiceClient, + serviceType string, + hostName string, + opts UpdateOptsBuilder, + qOpts UpdateQueryOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + + url := updateURL(client, serviceType, hostName) + if qOpts != nil { + query, _ := qOpts.ToUpdateQuery() + url += query + } + + _, r.Err = client.Put(url, &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v3/ecl/security_portal/v3/ports/results.go b/v3/ecl/security_portal/v3/ports/results.go new file mode 100644 index 0000000..1adf04f --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/results.go @@ -0,0 +1,69 @@ +package ports + +import ( + "encoding/json" + "strconv" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Port resource. +func (r commonResult) Extract() (*UpdateProcess, error) { + var p UpdateProcess + err := r.ExtractInto(&p) + return &p, err +} + +// Extract interprets any commonResult as a Port if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// UpdateProcess represents the result of a each element in +// response of port api result. +type UpdateProcess struct { + Message string `json:"message"` + ProcessID int `json:"processId"` + ID string `json:"-"` +} + +// ProcessPage is the page returned by a pager +// when traversing over a collection of Single Port. +type ProcessPage struct { + pagination.LinkedPageBase +} + +// UnmarshalJSON function overrides original functionality, +// to parse processId as unique identifier of process. +// Note: +// ID parameter in each struct must be string, +// but in api result of process polling API, +// processId is returned as integer value. +// This function solves this problem. +func (r *UpdateProcess) UnmarshalJSON(b []byte) error { + type tmp UpdateProcess + var s struct { + tmp + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = UpdateProcess(s.tmp) + + r.ID = strconv.Itoa(r.ProcessID) + + return err +} diff --git a/v3/ecl/security_portal/v3/ports/testing/doc.go b/v3/ecl/security_portal/v3/ports/testing/doc.go new file mode 100644 index 0000000..134142d --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains ports unit tests +package testing diff --git a/v3/ecl/security_portal/v3/ports/testing/fixtures.go b/v3/ecl/security_portal/v3/ports/testing/fixtures.go new file mode 100644 index 0000000..e68924b --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/testing/fixtures.go @@ -0,0 +1,29 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/ports" +) + +const updateRequest = `{ + "port": [ + { + "comment": "port 0 comment", + "enable_port":"true", + "ip_address": "192.168.1.50/24", + "mtu":"1500", + "network_id": "32314bd2-3583-4fb9-b622-9b121e04e007", + "subnet_id": "7fd77711-abae-4828-93f1-f3d682a8771f" + } + ] +}` + +const updateResponse = `{ + "message": "The process launch request has been accepted", + "processId": 85385 +}` + +var expectedResult = ports.UpdateProcess{ + Message: "The process launch request has been accepted", + ProcessID: 85385, + ID: "85385", +} diff --git a/v3/ecl/security_portal/v3/ports/testing/requests_test.go b/v3/ecl/security_portal/v3/ports/testing/requests_test.go new file mode 100644 index 0000000..99ed1d8 --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/testing/requests_test.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/ports" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestUpdatePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/ecl-api/ports/utm/CES11995" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, updateResponse) + }) + + updateOpts := ports.UpdateOpts{ + Port: []ports.SinglePort{ + { + Comment: "port 0 comment", + EnablePort: "true", + IPAddress: "192.168.1.50/24", + MTU: "1500", + NetworkID: "32314bd2-3583-4fb9-b622-9b121e04e007", + SubnetID: "7fd77711-abae-4828-93f1-f3d682a8771f", + }, + }, + } + + actual, err := ports.Update( + fakeclient.ServiceClient(), + "utm", + "CES11995", + updateOpts, + nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} diff --git a/v3/ecl/security_portal/v3/ports/urls.go b/v3/ecl/security_portal/v3/ports/urls.go new file mode 100644 index 0000000..0d53d60 --- /dev/null +++ b/v3/ecl/security_portal/v3/ports/urls.go @@ -0,0 +1,12 @@ +package ports + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +func updateURL(client *eclcloud.ServiceClient, deviceType string, hostName string) string { + url := fmt.Sprintf("ecl-api/ports/%s/%s", deviceType, hostName) + return client.ServiceURL(url) +} diff --git a/v3/ecl/security_portal/v3/processes/doc.go b/v3/ecl/security_portal/v3/processes/doc.go new file mode 100644 index 0000000..160ef42 --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/doc.go @@ -0,0 +1,2 @@ +// Package process contains port management functionality on security +package processes diff --git a/v3/ecl/security_portal/v3/processes/requests.go b/v3/ecl/security_portal/v3/processes/requests.go new file mode 100644 index 0000000..913370f --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/requests.go @@ -0,0 +1,35 @@ +package processes + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// GetOptsBuilder allows extensions to add additional parameters to +// the order API request +type GetOptsBuilder interface { + ToProcessQuery() (string, error) +} + +// GetOpts represents result of order API response. +type GetOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToProcessQuery formats a GetOpts into a query string. +func (opts GetOpts) ToProcessQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves details on a single order, by ID. +func Get(client *eclcloud.ServiceClient, processID string, opts GetOptsBuilder) (r GetResult) { + url := getURL(client, processID) + if opts != nil { + query, _ := opts.ToProcessQuery() + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/v3/ecl/security_portal/v3/processes/results.go b/v3/ecl/security_portal/v3/processes/results.go new file mode 100644 index 0000000..c805e4e --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/results.go @@ -0,0 +1,36 @@ +package processes + +import ( + "github.com/nttcom/eclcloud/v3" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Process. +func (r commonResult) Extract() (*ProcessInstance, error) { + var pr ProcessInstance + err := r.ExtractInto(&pr) + return &pr, err +} + +// Extract interprets any commonResult as a Process, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "processInstance") +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Process. +type GetResult struct { + commonResult +} + +type ProcessStatus struct { + Status string `json:"status"` +} + +type ProcessInstance struct { + Status ProcessStatus `json:"status"` +} diff --git a/v3/ecl/security_portal/v3/processes/testing/doc.go b/v3/ecl/security_portal/v3/processes/testing/doc.go new file mode 100644 index 0000000..d64fb4a --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains processes unit tests +package testing diff --git a/v3/ecl/security_portal/v3/processes/testing/fixtures.go b/v3/ecl/security_portal/v3/processes/testing/fixtures.go new file mode 100644 index 0000000..1823247 --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/testing/fixtures.go @@ -0,0 +1,186 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/processes" +) + +const processID = "85385" + +const getResponse = ` +{ + "processInstance": { + "processId": { + "id": 85385, + "lastExecNumber": 1, + "name": "ntt/FortiVA_Port_Management/Process_Manage_UTM_Interfaces/Process_Manage_UTM_Interfaces", + "submissionType": "RUN" + }, + "serviceId": { + "id": 19382, + "name": "FortiVA_Port_Management", + "serviceReference": "PORT_MNGT_CES11892", + "state": null + }, + "status": { + "comment": "Ping Monitoring started for the device 11892.", + "duration": 0, + "endingDate": "2019-07-26 04:34:56.0", + "execNumber": 1, + "processInstanceId": 85385, + "processName": "ntt/FortiVA_Port_Management/Process_Manage_UTM_Interfaces/Process_Manage_UTM_Interfaces", + "startingDate": "2019-07-26 04:24:45.0", + "status": "RUNNING", + "taskStatusList": [ + { + "comment": "IP Address inputs verified successfully.", + "endingDate": "2019-07-26 04:24:48.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:24:45.0", + "status": "ENDED", + "taskId": 1, + "taskName": "Verify IP Address, MTU Inputs" + }, + { + "comment": "Ping Monitoring stopped for the device 11892.", + "endingDate": "2019-07-26 04:26:49.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:24:48.0", + "status": "ENDED", + "taskId": 2, + "taskName": "Stop Ping Monitoring" + }, + { + "comment": "Openstack Server 158eb01a-8d45-45c8-a9ff-1fba8f1ab7e3 stopped successfully.\nServer Status : SHUTOFF\nTask State : -\nPower State : Shutdown\n", + "endingDate": "2019-07-26 04:27:03.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:26:49.0", + "status": "ENDED", + "taskId": 3, + "taskName": "Stop the UTM" + }, + { + "comment": "IP Address 100.76.96.230 is now unreachable from MSA.\nPING Status : Destination Host Unreachable\n", + "endingDate": "2019-07-26 04:27:13.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:27:03.0", + "status": "ENDED", + "taskId": 4, + "taskName": "Wait for UTM Ping unreachability from MSA" + }, + { + "comment": "Ports deleted successfully.", + "endingDate": "2019-07-26 04:28:29.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:27:13.0", + "status": "ENDED", + "taskId": 5, + "taskName": "Delete Ports" + }, + { + "comment": "Ports created successfully.\nPort Id : 34c7389d-1428-4f98-a37c-9c2e32aab255\nPort Id : 3d09053b-fad8-45c4-bf71-501c0fc2b58a\nPort Id : 0262d90c-6056-4308-8b76-8e851f0132f5\nPort Id : 5fcabdf2-8a20-4337-bd10-02f5c5000ca1\nPort Id : 53211b09-f82b-40d5-bf5b-7289a298cbdf\nPort Id : 9ce2d3b7-7ae0-400d-8e41-16dc9b94f95e\nPort Id : a36493fe-43d2-4dc1-a39e-c96898e9c0be\n", + "endingDate": "2019-07-26 04:29:50.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:28:29.0", + "status": "ENDED", + "taskId": 6, + "taskName": "Create Ports" + }, + { + "comment": "Ports attached successfully to the Server 158eb01a-8d45-45c8-a9ff-1fba8f1ab7e3.", + "endingDate": "2019-07-26 04:31:33.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:29:50.0", + "status": "ENDED", + "taskId": 7, + "taskName": "Attach Ports" + }, + { + "comment": "Openstack Server 158eb01a-8d45-45c8-a9ff-1fba8f1ab7e3 started successfully.\nServer Status : ACTIVE\nTask State : -\nPower State : Running\n", + "endingDate": "2019-07-26 04:31:47.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:31:33.0", + "status": "ENDED", + "taskId": 8, + "taskName": "Start the UTM" + }, + { + "comment": "IP Address 100.76.96.230 is now reachable from MSA.\nPING Status : OK\n", + "endingDate": "2019-07-26 04:32:30.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:31:47.0", + "status": "ENDED", + "taskId": 9, + "taskName": "Wait for UTM Ping reachability from MSA" + }, + { + "comment": "OK LICENSE IS VALID", + "endingDate": "2019-07-26 04:32:56.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:32:30.0", + "status": "ENDED", + "taskId": 10, + "taskName": "Verify License Validity" + }, + { + "comment": "Ports updated successfully on Fortigate Device 11892.\n", + "endingDate": "2019-07-26 04:33:17.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:32:56.0", + "status": "ENDED", + "taskId": 11, + "taskName": "Update UTM" + }, + { + "comment": "Device 11892 Backup completed successfully.\nBackup Status : ENDED\nBackup Message : BACKUP processed\n\nBackup Revision Id : 209408\n", + "endingDate": "2019-07-26 04:33:28.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:33:17.0", + "status": "ENDED", + "taskId": 12, + "taskName": "Device Backup" + }, + { + "comment": "Ping Monitoring started for the device 11892.", + "endingDate": "2019-07-26 04:34:56.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:33:28.0", + "status": "ENDED", + "taskId": 13, + "taskName": "Start Ping Monitoring" + } + ] + } + } +}` + +var expectedProcess = processes.ProcessInstance{ + Status: processes.ProcessStatus{ + Status: "RUNNING", + }, +} diff --git a/v3/ecl/security_portal/v3/processes/testing/requests_test.go b/v3/ecl/security_portal/v3/processes/testing/requests_test.go new file mode 100644 index 0000000..4b7339b --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/testing/requests_test.go @@ -0,0 +1,30 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/security_portal/v3/processes" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestGetProcess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/ecl-api/process/%s/status", processID) + fmt.Println(url) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := processes.Get(fakeclient.ServiceClient(), processID, nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedProcess, actual) +} diff --git a/v3/ecl/security_portal/v3/processes/urls.go b/v3/ecl/security_portal/v3/processes/urls.go new file mode 100644 index 0000000..deb93d7 --- /dev/null +++ b/v3/ecl/security_portal/v3/processes/urls.go @@ -0,0 +1,12 @@ +package processes + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient, processID string) string { + url := fmt.Sprintf("ecl-api/process/%s/status", processID) + return client.ServiceURL(url) +} diff --git a/v3/ecl/sss/v1/approval_requests/doc.go b/v3/ecl/sss/v1/approval_requests/doc.go new file mode 100644 index 0000000..49196e5 --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/doc.go @@ -0,0 +1,44 @@ +/* +Package approval_requests manages and retrieves approval requests in the Enterprise Cloud. + +Example to List approval requests + + allPages, err := approval_requests.List(client).AllPages() + if err != nil { + panic(err) + } + + allApprovalRequests, err := approval_requests.ExtractApprovalRequests(allPages) + if err != nil { + panic(err) + } + + for _, approvalRequest := range allApprovalRequests { + fmt.Printf("%+v\n", approvalRequest) + } + +Example to Get an approval requests + + requestID := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + approvalRequest, err := approval_requests.Get(client, requestID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", approvalRequest) + +Example to Update an approval request + + requestID := "02471b45-3de0-4fc8-8469-a7cc52c378df" + updateOpts := approval_requests.UpdateOpts{ + Status: "approved", + } + + result := approval_requests.Update(client, requestID, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +*/ +package approval_requests diff --git a/v3/ecl/sss/v1/approval_requests/requests.go b/v3/ecl/sss/v1/approval_requests/requests.go new file mode 100644 index 0000000..38f053b --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/requests.go @@ -0,0 +1,77 @@ +package approval_requests + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToApprovalRequestListQuery() (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 approval request attributes you want to see returned. +type ListOpts struct { + Status string `q:"status"` + Service string `q:"service"` +} + +// ToApprovalRequestListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToApprovalRequestListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List retrieves a list of approval requests. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToApprovalRequestListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ApprovalRequestPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of an approval request. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToResourceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update an approval request. +type UpdateOpts struct { + Status string `json:"status" required:"true"` +} + +// ToResourceUpdateCreateMap formats a UpdateOpts to update approval request. +func (opts UpdateOpts) ToResourceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of an approval request. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToResourceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} diff --git a/v3/ecl/sss/v1/approval_requests/results.go b/v3/ecl/sss/v1/approval_requests/results.go new file mode 100644 index 0000000..4dc4275 --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/results.go @@ -0,0 +1,95 @@ +package approval_requests + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type Action struct { + Service string `json:"service"` + Region string `json:"region"` + APIPath string `json:"api_path"` + Method string `json:"method"` + // Basically JSON is passed to Action.Body, + // but depending on the value of the service, it may be a String, so it is set to interface{}. + // If service is "provider-connectivity", body's type is JSON. + // If service is "network", body's type is String. + Body interface{} `json:"body"` +} + +type Description struct { + Lang string `json:"lang"` + Text string `json:"text"` +} + +// ApprovalRequest represents an ECL SSS Approval Request. +type ApprovalRequest struct { + RequestID string `json:"request_id"` + ExternalRequestID string `json:"external_request_id"` + ApproverType string `json:"approver_type"` + ApproverID string `json:"approver_id"` + RequestUserID string `json:"request_user_id"` + Service string `json:"service"` + Actions []Action `json:"actions"` + Descriptions []Description `json:"descriptions"` + RequestUser interface{} `json:"request_user"` + Approver bool `json:"approver"` + ApprovalDeadLine interface{} `json:"approval_deadline"` + ApprovalExpire interface{} `json:"approval_expire"` + RegisteredTime interface{} `json:"registered_time"` + UpdatedTime interface{} `json:"updated_time"` + Status string `json:"status"` +} + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*ApprovalRequest, error) { + var ar ApprovalRequest + err := r.ExtractInto(&ar) + return &ar, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as an approval request. +type GetResult struct { + commonResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as an approval request. +type UpdateResult struct { + commonResult +} + +// ApprovalRequestPage is a single page of approval request results. +type ApprovalRequestPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of approval requests contains any results. +func (r ApprovalRequestPage) IsEmpty() (bool, error) { + resources, err := ExtractApprovalRequests(r) + return len(resources) == 0, err +} + +// ExtractApprovalRequests returns a slice of approval requests +// contained in a single page of results. +func ExtractApprovalRequests(r pagination.Page) ([]ApprovalRequest, error) { + var s struct { + ApprovalRequests []ApprovalRequest `json:"approval_requests"` + } + err := (r.(ApprovalRequestPage)).ExtractInto(&s) + return s.ApprovalRequests, err +} + +// ExtractApprovalRequestsInto interprets the results of a single page from a List() call, +// producing a slice of Approval Request entities. +func ExtractApprovalRequestsInto(r pagination.Page, v interface{}) error { + return r.(ApprovalRequestPage).Result.ExtractIntoSlicePtr(v, "") +} diff --git a/v3/ecl/sss/v1/approval_requests/testing/doc.go b/v3/ecl/sss/v1/approval_requests/testing/doc.go new file mode 100644 index 0000000..c513bc0 --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/testing/doc.go @@ -0,0 +1,2 @@ +// sss approval request unit tests +package testing diff --git a/v3/ecl/sss/v1/approval_requests/testing/fixtures.go b/v3/ecl/sss/v1/approval_requests/testing/fixtures.go new file mode 100644 index 0000000..a78084c --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/testing/fixtures.go @@ -0,0 +1,203 @@ +package testing + +import ( + "fmt" + + ar "github.com/nttcom/eclcloud/v3/ecl/sss/v1/approval_requests" +) + +const idApprovalRequest1 = "9a76dca6-d8cd-4391-aac6f-2ea052f10f4" +const idApprovalRequest2 = "fc578e8b-dea2-4f8c-aa7e-9026fa173632" + +var listResponse = fmt.Sprintf(` +{ + "approval_requests": [ + { + "request_id": "%s", + "external_request_id": "test007", + "approver_type":"tenant_owner", + "approver_id":"11a98bf9cb144af5a204c9da566d2bd0", + "request_user_id":"ecid9999888881", + "service":"provider-connectivity", + "actions" : [ + { + "service": "provider-connectivity", + "region": "jp1", + "api_path": "/v2.0/tenant_connections_requests", + "method": "POST", + "body": { + "tenant_connection_request": { + "tenant_id_other": "d2f19c353e6d4c519e530c6a78438b33", + "network_id": "ea7eea8c-0d91-4553-9ecb-01f81e2c3989" + } + } + } + ], + "descriptions": [ + { + "lang": "en", + "text": "approval resquest test" + } + ], + "request_user": false, + "approver": true, + "approval_deadline": "2017-02-05 09:45:22", + "approval_expire": null, + "registered_time": "2017-01-31 07:43:13", + "updated_time": null, + "status": "registered" + }, + { + "request_id": "%s", + "external_request_id": "test006", + "approver_type":"tenant_owner", + "approver_id":"66a98bf9cb1238192a204c9da566dbd0", + "request_user_id":"ecid9999888882", + "service":"network", + "actions" : [ + { + "service": "network", + "region": "jp1", + "api_path": "/network/v1/firewall", + "method": "POST", + "body": "{\n\t\"firewall\": {\n\t\t\"availability_zone\": \"zone1-groupa\",\n\t\t\"default_gateway\": \"\",\n\t\t\"description\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"firewall_plan_id\": \"bd12784a-c66e-4f13-9f72-5143d64762b6\",\n\t\t\"name\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"tenant_id\": \"6a156ddf2ecd497ca786ff2da6df5aa8\"\n\t}\n}" + } + ], + "descriptions": [ + { + "lang": "en", + "text": "approval resquest test" + } + ], + "request_user": false, + "approver": true, + "approval_deadline": "2016-12-25 09:45:22", + "approval_expire": null, + "registered_time": "2016-12-13 02:20:21", + "updated_time": null, + "status": "expired" + } + ] +} +`, + idApprovalRequest1, + idApprovalRequest2, +) + +var expectedApprovalRequestsSlice = []ar.ApprovalRequest{ + firstApprovalRequest, + secondApprovalRequest, +} + +var firstApprovalRequest = ar.ApprovalRequest{ + RequestID: idApprovalRequest1, + ExternalRequestID: "test007", + ApproverType: "tenant_owner", + ApproverID: "11a98bf9cb144af5a204c9da566d2bd0", + RequestUserID: "ecid9999888881", + Service: "provider-connectivity", + Actions: []ar.Action{ + { + Service: "provider-connectivity", + Region: "jp1", + APIPath: "/v2.0/tenant_connections_requests", + Method: "POST", + Body: map[string]interface{}{ + "tenant_connection_request": map[string]string{ + "tenant_id_other": "d2f19c353e6d4c519e530c6a78438b33", + "network_id": "ea7eea8c-0d91-4553-9ecb-01f81e2c3989", + }, + }, + }, + }, + Descriptions: []ar.Description{ + { + Lang: "en", + Text: "approval resquest test", + }, + }, + RequestUser: false, + Approver: true, + ApprovalDeadLine: interface{}("2017-02-05 09:45:22"), + ApprovalExpire: interface{}(nil), + RegisteredTime: interface{}("2017-01-31 07:43:13"), + UpdatedTime: interface{}(nil), + Status: "registered", +} + +var secondApprovalRequest = ar.ApprovalRequest{ + RequestID: idApprovalRequest2, + ExternalRequestID: "test006", + ApproverType: "tenant_owner", + ApproverID: "66a98bf9cb1238192a204c9da566dbd0", + RequestUserID: "ecid9999888882", + Service: "network", + Actions: []ar.Action{ + { + Service: "network", + Region: "jp1", + APIPath: "/network/v1/firewall", + Method: "POST", + Body: "{\n\t\"firewall\": {\n\t\t\"availability_zone\": \"zone1-groupa\",\n\t\t\"default_gateway\": \"\",\n\t\t\"description\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"firewall_plan_id\": \"bd12784a-c66e-4f13-9f72-5143d64762b6\",\n\t\t\"name\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"tenant_id\": \"6a156ddf2ecd497ca786ff2da6df5aa8\"\n\t}\n}", + }, + }, + Descriptions: []ar.Description{ + { + Lang: "en", + Text: "approval resquest test", + }, + }, + RequestUser: false, + Approver: true, + ApprovalDeadLine: interface{}("2016-12-25 09:45:22"), + ApprovalExpire: interface{}(nil), + RegisteredTime: interface{}("2016-12-13 02:20:21"), + UpdatedTime: interface{}(nil), + Status: "expired", +} + +var getResponse = fmt.Sprintf(` + { + "request_id": "%s", + "external_request_id": "test007", + "approver_type":"tenant_owner", + "approver_id":"11a98bf9cb144af5a204c9da566d2bd0", + "request_user_id":"ecid9999888881", + "service":"provider-connectivity", + "actions" : [ + { + "service": "provider-connectivity", + "region": "jp1", + "api_path": "/v2.0/tenant_connections_requests", + "method": "POST", + "body": { + "tenant_connection_request": { + "tenant_id_other": "d2f19c353e6d4c519e530c6a78438b33", + "network_id": "ea7eea8c-0d91-4553-9ecb-01f81e2c3989" + } + } + } + ], + "descriptions": [ + { + "lang": "en", + "text": "approval resquest test" + } + ], + "request_user": false, + "approver": true, + "approval_deadline": "2017-02-05 09:45:22", + "approval_expire": null, + "registered_time": "2017-01-31 07:43:13", + "updated_time": null, + "status": "registered" + } +`, + idApprovalRequest1, +) + +const updateRequest = ` +{ + "status": "approved" +} +` diff --git a/v3/ecl/sss/v1/approval_requests/testing/requests_test.go b/v3/ecl/sss/v1/approval_requests/testing/requests_test.go new file mode 100644 index 0000000..6fe6518 --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/testing/requests_test.go @@ -0,0 +1,96 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + ar "github.com/nttcom/eclcloud/v3/ecl/sss/v1/approval_requests" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListApprovalRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/approval-requests", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := ar.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ar.ExtractApprovalRequests(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedApprovalRequestsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListApprovalRequestAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/approval-requests", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := ar.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allRequests, err := ar.ExtractApprovalRequests(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allRequests)) +} + +func TestGetApprovalRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/approval-requests/%s", idApprovalRequest1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := ar.Get(fakeclient.ServiceClient(), idApprovalRequest1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &firstApprovalRequest, actual) +} + +func TestUpdateApprovalRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/approval-requests/%s", idApprovalRequest1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusNoContent) + }) + + updateOpts := ar.UpdateOpts{ + Status: "approved", + } + + res := ar.Update(fakeclient.ServiceClient(), idApprovalRequest1, updateOpts) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/sss/v1/approval_requests/urls.go b/v3/ecl/sss/v1/approval_requests/urls.go new file mode 100644 index 0000000..80acd40 --- /dev/null +++ b/v3/ecl/sss/v1/approval_requests/urls.go @@ -0,0 +1,15 @@ +package approval_requests + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("approval-requests") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("approval-requests", id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("approval-requests", id) +} diff --git a/v3/ecl/sss/v1/tenants/doc.go b/v3/ecl/sss/v1/tenants/doc.go new file mode 100644 index 0000000..65ee932 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/doc.go @@ -0,0 +1,57 @@ +/* +Package projects manages and retrieves Projects in the ECL SSS Service. + +Example to List Tenants + + listOpts := tenants.ListOpts{ + Enabled: eclcloud.Enabled, + } + + allPages, err := tenants.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allProjects, err := tenants.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, tenant := range allTenants { + fmt.Printf("%+v\n", tenant) + } + +Example to Create a Tenant + + createOpts := projects.CreateOpts{ + Name: "tenant_name", + Description: "Tenant Description" + } + + tenant, err := tenants.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Tenant + + tenantID := "966b3c7d36a24facaf20b7e458bf2192" + + updateOpts := tenants.UpdateOpts{ + Description: "Tenant Description - New", + } + + tenant, err := tenants.Update(identityClient, tenantID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Tenant + + tenantID := "966b3c7d36a24facaf20b7e458bf2192" + err := projects.Delete(identityClient, projectID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package tenants diff --git a/v3/ecl/sss/v1/tenants/errors.go b/v3/ecl/sss/v1/tenants/errors.go new file mode 100644 index 0000000..08cfa34 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/errors.go @@ -0,0 +1,17 @@ +package tenants + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v3/ecl/sss/v1/tenants/requests.go b/v3/ecl/sss/v1/tenants/requests.go new file mode 100644 index 0000000..6c103e0 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/requests.go @@ -0,0 +1,124 @@ +package tenants + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToTenantListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +// Currently SSS Tenant API does not support any of query parameters. +type ListOpts struct { +} + +// ToTenantListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Projects to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single tenant, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToTenantCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a tenant. +type CreateOpts struct { + // Name of this tenant. + TenantName string `json:"tenant_name" required:"true"` + + // Description of the tenant. + Description string `json:"description" required:"true"` + + // TenantRegion of the tenant. + TenantRegion string `json:"region" required:"true"` + + // ID of contract which new tenant belongs. + ContractID string `json:"contract_id,omitempty"` +} + +// ToTenantCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToTenantCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new Project. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a tenant. +func Delete(client *eclcloud.ServiceClient, tenantID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, tenantID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToTenantUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a tenant. +type UpdateOpts struct { + // Description of the tenant. + Description *string `json:"description,omitempty"` +} + +// ToTenantUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToTenantUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a tenant. +// SSS Tenant PUT API does not have response body, +// so set JSONResponse option as nil. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put( + updateURL(client, id), + b, + nil, + &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }, + ) + return +} diff --git a/v3/ecl/sss/v1/tenants/results.go b/v3/ecl/sss/v1/tenants/results.go new file mode 100644 index 0000000..300559a --- /dev/null +++ b/v3/ecl/sss/v1/tenants/results.go @@ -0,0 +1,125 @@ +package tenants + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type tenantResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a Tenant. +type GetResult struct { + tenantResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Tenant. +type CreateResult struct { + tenantResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Tenant. +type UpdateResult struct { + tenantResult +} + +// Tenant represents an ECL SSS Tenant. +type Tenant struct { + // ID of contract which owns these tenants. + ContractID string `json:"contract_id"` + // ID is the unique ID of the tenant. + TenantID string `json:"tenant_id"` + // Name of the tenant. + TenantName string `json:"tenant_name"` + // Description of the tenant. + Description string `json:"description"` + // TenantRegion the tenant blongs. + TenantRegion string `json:"region"` + // Time that the tenant is created. + StartTime time.Time `json:"-"` +} + +// UnmarshalJSON creates JSON format of tenant +func (r *Tenant) UnmarshalJSON(b []byte) error { + type tmp Tenant + var s struct { + tmp + StartTime eclcloud.JSONRFC3339ZNoTNoZ `json:"start_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Tenant(s.tmp) + + r.StartTime = time.Time(s.StartTime) + + return err +} + +// 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 (r TenantPage) IsEmpty() (bool, error) { + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractTenants returns a slice of Tenants contained in a single page of +// results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + ContractID string `json:"contract_id"` + Tenants []Tenant `json:"tenants"` + } + + // In list response case, each json element does not have contract_id. + // It is set at out layer of each element. + // So following logic set contract_id into inside of tenants slice forcibly. + // In "show(get with ID of tennat)" case, this does not occur. + err := (r.(TenantPage)).ExtractInto(&s) + contractID := s.ContractID + + for i := 0; i < len(s.Tenants); i++ { + s.Tenants[i].ContractID = contractID + } + return s.Tenants, err +} + +// Extract interprets any projectResults as a Tenant. +func (r tenantResult) Extract() (*Tenant, error) { + var s *Tenant + err := r.ExtractInto(&s) + return s, err +} diff --git a/v3/ecl/sss/v1/tenants/testing/doc.go b/v3/ecl/sss/v1/tenants/testing/doc.go new file mode 100644 index 0000000..020c8c3 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/testing/doc.go @@ -0,0 +1,2 @@ +// sss tenant unit tests +package testing diff --git a/v3/ecl/sss/v1/tenants/testing/fixtures.go b/v3/ecl/sss/v1/tenants/testing/fixtures.go new file mode 100644 index 0000000..a983984 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/testing/fixtures.go @@ -0,0 +1,146 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/sss/v1/tenants" +) + +const contractID = "econ8000008888" + +const idTenant1 = "9a76dca6d8cd4391aac6f2ea052f10f4" +const idTenant2 = "27a58d42769141ff8e94920a99aeb44b" + +const nameTenant1 = "jp1_tenant01" + +const descriptionTenant1 = "jp1 tenant01" +const descriptionTenant1Update = "jp1 tenant01-update" + +const startTime = "2018-07-26 08:40:01" + +// ListResponse is a sample response to a List call. +var ListResponse = fmt.Sprintf(` +{ + "contract_id": "%s", + "tenants": [{ + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp1", + "start_time": "%s" + }, { + "tenant_id": "%s", + "tenant_name": "jp2_tenant01", + "description": "jp2 tenant", + "region": "jp2", + "start_time": "%s" + }] +} +`, + contractID, + // fot tenant 1 + idTenant1, + nameTenant1, + descriptionTenant1, + startTime, + // for tenant 2 + idTenant2, + startTime, +) + +// ExpectedTenantsSlice is the slice of results that should be parsed +// from ListResponse in the expected order. +var ExpectedTenantsSlice = []tenants.Tenant{FirstTenant, SecondTenant} + +// TenantStartTime is parsed tenant start time +var TenantStartTime, _ = time.Parse(eclcloud.RFC3339ZNoTNoZ, startTime) + +// FirstTenant is the mock object of expected tenant-1 +var FirstTenant = tenants.Tenant{ + ContractID: contractID, + TenantID: idTenant1, + TenantName: nameTenant1, + Description: descriptionTenant1, + TenantRegion: "jp1", + StartTime: TenantStartTime, +} + +// SecondTenant is the mock object of expected tenant-2 +var SecondTenant = tenants.Tenant{ + ContractID: contractID, + TenantID: idTenant2, + TenantName: "jp2_tenant01", + Description: "jp2 tenant", + TenantRegion: "jp2", + StartTime: TenantStartTime, +} + +// GetResponse is a sample response to a Get call. +// This get result does not have action, attributes in ECL2.0 +var GetResponse = fmt.Sprintf(` +{ + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp1", + "contract_id": "%s", + "region_api_endpoint": "https://example.com:443/api", + "start_time": "%s", + "users": [{ + "user_id": "ecid8000008888", + "contract_id": "%s", + "contract_owner": true + }], + "brand_id": "ecl2" +}`, idTenant1, + nameTenant1, + descriptionTenant1, + contractID, + startTime, + contractID, +) + +// GetResponseStruct mocked actual tenant +var GetResponseStruct = tenants.Tenant{ + ContractID: contractID, + TenantID: idTenant1, + TenantName: nameTenant1, + Description: descriptionTenant1, + TenantRegion: "jp1", + StartTime: TenantStartTime, +} + +// CreateRequest is a sample request to create a tenant. +var CreateRequest = fmt.Sprintf(`{ + "tenant_name": "%s", + "region": "jp1", + "description": "%s", + "contract_id": "%s" +}`, + nameTenant1, + descriptionTenant1, + contractID, +) + +// CreateTenantResponse is a sample response to a create request. +var CreateResponse = fmt.Sprintf(`{ + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp1", + "contract_id": "%s" +}`, idTenant1, + nameTenant1, + descriptionTenant1, + contractID, +) + +// UpdateRequest is a sample request to update a zone. +var UpdateRequest = fmt.Sprintf(` +{ + "description": "%s" +}`, + descriptionTenant1Update, +) diff --git a/v3/ecl/sss/v1/tenants/testing/requests_test.go b/v3/ecl/sss/v1/tenants/testing/requests_test.go new file mode 100644 index 0000000..bd21e52 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/testing/requests_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3/ecl/sss/v1/tenants" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListResponse) + }) + + count := 0 + err := tenants.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := tenants.ExtractTenants(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedTenantsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListTenantAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListResponse) + }) + + allPages, err := tenants.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := tenants.ExtractTenants(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/tenants/%s", idTenant1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetResponse) + }) + + actual, err := tenants.Get(fakeclient.ServiceClient(), idTenant1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GetResponseStruct, actual) +} + +func TestCreateTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := tenants.CreateOpts{ + TenantName: nameTenant1, + Description: descriptionTenant1, + TenantRegion: "jp1", + ContractID: contractID, + } + + // clone FirstTenant into createdTenant(Used as assertion target) + // and initialize StartTime + createdTenant := FirstTenant + createdTenant.StartTime = time.Time{} + + actual, err := tenants.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdTenant, actual) +} + +func TestUpdateTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/tenants/%s", idTenant1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, UpdateRequest) + + w.WriteHeader(http.StatusNoContent) + }) + + description := descriptionTenant1Update + + updateOpts := tenants.UpdateOpts{ + Description: &description, + } + + // In ECL2.0 tennat update API returns + // - StatusNoContent + // - No response body as PUT response + res := tenants.Update(fakeclient.ServiceClient(), idTenant1, updateOpts) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/tenants/%s", idTenant1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := tenants.Delete(fakeclient.ServiceClient(), idTenant1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/sss/v1/tenants/urls.go b/v3/ecl/sss/v1/tenants/urls.go new file mode 100644 index 0000000..13114d8 --- /dev/null +++ b/v3/ecl/sss/v1/tenants/urls.go @@ -0,0 +1,23 @@ +package tenants + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func getURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func deleteURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func updateURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} diff --git a/v3/ecl/sss/v1/users/doc.go b/v3/ecl/sss/v1/users/doc.go new file mode 100644 index 0000000..a2ac48e --- /dev/null +++ b/v3/ecl/sss/v1/users/doc.go @@ -0,0 +1,2 @@ +// Package users contains user management functionality on SSS +package users diff --git a/v3/ecl/sss/v1/users/requests.go b/v3/ecl/sss/v1/users/requests.go new file mode 100644 index 0000000..5319448 --- /dev/null +++ b/v3/ecl/sss/v1/users/requests.go @@ -0,0 +1,132 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToUserListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +// Currently SSS User API does not support any of query parameters. +type ListOpts struct { +} + +// ToUserListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToUserListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Users to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single user, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a user. +type CreateOpts struct { + // Login id of new user. + LoginID string `json:"login_id" required:"true"` + + // Mail address of new user. + MailAddress string `json:"mail_address" required:"true"` + + // Initial password of new user. + // If this parameter is not designated, + // random initial password is generated and applied to new user. + Password string `json:"password,omitempty"` + + // If this flag is set 'true', notification e-mail will be sent to new user's email. + NotifyPassword string `json:"notify_password" required:"true"` +} + +// ToUserCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new user. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a user. +func Delete(client *eclcloud.ServiceClient, userID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, userID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a user. +type UpdateOpts struct { + // New login id of the user. + LoginID *string `json:"login_id" required:"true"` + + // New email address of the user + MailAddress *string `json:"mail_address" required:"true"` + + // New password of the user + NewPassword *string `json:"new_password" required:"true"` +} + +// ToUserUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToUserUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a user. +// SSS User PUT API does not have response body, +// so set JSONResponse option as nil. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToUserUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put( + updateURL(client, id), + b, + nil, + &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }, + ) + return +} diff --git a/v3/ecl/sss/v1/users/results.go b/v3/ecl/sss/v1/users/results.go new file mode 100644 index 0000000..2b950c1 --- /dev/null +++ b/v3/ecl/sss/v1/users/results.go @@ -0,0 +1,127 @@ +package users + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type userResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a User. +type GetResult struct { + userResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a User. +type CreateResult struct { + userResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a User. +type UpdateResult struct { + userResult +} + +// User represents an ECL SSS User. +type User struct { + LoginID string `json:"login_id"` + MailAddress string `json:"mail_address"` + UserID string `json:"user_id"` + ContractOwner bool `json:"contract_owner"` + KeystoneName string `json:"keystone_name"` + KeystonePassword string `json:"keystone_password"` + KeystoneEndpoint string `json:"keystone_endpoint"` + SSSEndpoint string `json:"sss_endpoint"` + ContractID string `json:"contract_id"` + LoginIntegration string `json:"login_integration"` + ExternalReferenceID string `json:"external_reference_id"` + BrandID string `json:"brand_id"` + Password string `json:"password"` + StartTime time.Time `json:"-"` +} + +// UnmarshalJSON creates JSON format of user +func (r *User) UnmarshalJSON(b []byte) error { + type tmp User + var s struct { + tmp + StartTime eclcloud.JSONRFC3339ZNoTNoZ `json:"start_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = User(s.tmp) + + r.StartTime = time.Time(s.StartTime) + + return err +} + +// UserPage is a single page of User results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of User contains any results. +func (r UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(r) + return len(users) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r UserPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractUsers returns a slice of Users contained in a single page of +// results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + ContractID string `json:"contract_id"` + Users []User `json:"users"` + } + + // In list response case, each json element does not have contract_id. + // It is set at out layer of each element. + // So following logic set contract_id into inside of users slice forcibly. + // In "show(get with ID of tennat)" case, this does not occur. + err := (r.(UserPage)).ExtractInto(&s) + contractID := s.ContractID + + for i := 0; i < len(s.Users); i++ { + s.Users[i].ContractID = contractID + } + return s.Users, err +} + +// Extract interprets any projectResults as a User. +func (r userResult) Extract() (*User, error) { + var u *User + err := r.ExtractInto(&u) + return u, err +} diff --git a/v3/ecl/sss/v1/users/testing/doc.go b/v3/ecl/sss/v1/users/testing/doc.go new file mode 100644 index 0000000..5bf4d42 --- /dev/null +++ b/v3/ecl/sss/v1/users/testing/doc.go @@ -0,0 +1,2 @@ +// sss user unit tests +package testing diff --git a/v3/ecl/sss/v1/users/testing/fixtures.go b/v3/ecl/sss/v1/users/testing/fixtures.go new file mode 100644 index 0000000..4a5783a --- /dev/null +++ b/v3/ecl/sss/v1/users/testing/fixtures.go @@ -0,0 +1,134 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/sss/v1/users" +) + +const contractID = "econ8000008888" + +const idUser1 = "ecid1000000001" +const idUser2 = "ecid1000000002" + +const startTime = "2018-07-26 08:40:01" + +var listResponse = fmt.Sprintf(` +{ + "contract_id": "%s", + "users": [{ + "user_id": "%s", + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "contract_id": "%s", + "start_time": "%s" + }, { + "user_id": "%s", + "login_id": "login_id_2", + "mail_address": "user2@example.com", + "contract_id": "%s", + "start_time": "%s" + }] +}`, contractID, + idUser1, contractID, startTime, + idUser2, contractID, startTime) + +var expectedUsersSlice = []users.User{firstUser, secondUser} + +var userStartTime, _ = time.Parse(eclcloud.RFC3339ZNoTNoZ, startTime) + +var firstUser = users.User{ + UserID: idUser1, + LoginID: "login_id_1", + MailAddress: "user1@example.com", + ContractID: contractID, + StartTime: userStartTime, +} + +var secondUser = users.User{ + UserID: idUser2, + LoginID: "login_id_2", + MailAddress: "user2@example.com", + ContractID: contractID, + StartTime: userStartTime, +} + +var getResponse = fmt.Sprintf(` +{ + "user_id": "%s", + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "contract_owner": false, + "super_user": false, + "sss_endpoint": "http://sss.com", + "keystone_endpoint": "http://keystone.com", + "keystone_name": "keystonename1", + "keystone_password": "keystonepassword1", + "start_time": "%s", + "contract_id": "%s", + "login_integration": "", + "external_reference_id": "econ0000009999", + "brand_id": "ecl2", + "otp_activation": false +}`, idUser1, + startTime, + contractID, +) + +var getResponseStruct = users.User{ + UserID: idUser1, + LoginID: "login_id_1", + MailAddress: "user1@example.com", + ContractOwner: false, + SSSEndpoint: "http://sss.com", + KeystoneEndpoint: "http://keystone.com", + KeystoneName: "keystonename1", + KeystonePassword: "keystonepassword1", + StartTime: userStartTime, + ContractID: contractID, + LoginIntegration: "", + ExternalReferenceID: "econ0000009999", + BrandID: "ecl2", +} + +var createRequest = `{ + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "notify_password": "false", + "password": "Passw0rd" +}` + +var createResponse = fmt.Sprintf(`{ + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "user_id": "%s", + "contract_id": "%s", + "keystone_name": "keystonename1", + "keystone_password": "keystonepassword1", + "keystone_endpoint": "http://keystone.com", + "sss_endpoint": "http://sss.com", + "password": "Passw0rd" +} +`, idUser1, + contractID, +) + +var createdUser = users.User{ + LoginID: "login_id_1", + UserID: idUser1, + ContractID: contractID, + MailAddress: "user1@example.com", + KeystoneName: "keystonename1", + KeystonePassword: "keystonepassword1", + KeystoneEndpoint: "http://keystone.com", + SSSEndpoint: "http://sss.com", + Password: "Passw0rd", +} + +var updateRequest = `{ + "login_id": "login_id_1_update", + "mail_address": "user1_update@example.com", + "new_password": "NewPassw0rd" +}` diff --git a/v3/ecl/sss/v1/users/testing/requests_test.go b/v3/ecl/sss/v1/users/testing/requests_test.go new file mode 100644 index 0000000..d2d5b88 --- /dev/null +++ b/v3/ecl/sss/v1/users/testing/requests_test.go @@ -0,0 +1,152 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/sss/v1/users" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := users.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedUsersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListUserAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := users.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := users.ExtractUsers(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/users/%s", idUser1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := users.Get(fakeclient.ServiceClient(), idUser1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &getResponseStruct, actual) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + createOpts := users.CreateOpts{ + LoginID: "login_id_1", + MailAddress: "user1@example.com", + NotifyPassword: "false", + Password: "Passw0rd", + } + + // clone FirstTenant into createdUser(Used as assertion target) + // and initialize StartTime + // createdUser := firstUser + // createdUser.StartTime = time.Time{} + + actual, err := users.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdUser, actual) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/users/%s", idUser1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusNoContent) + }) + + loginID := "login_id_1_update" + mailAddress := "user1_update@example.com" + newPassword := "NewPassw0rd" + + updateOpts := users.UpdateOpts{ + LoginID: &loginID, + MailAddress: &mailAddress, + NewPassword: &newPassword, + } + + // In ECL2.0 user update API returns + // - StatusNoContent + // - No response body as PUT response + res := users.Update(fakeclient.ServiceClient(), idUser1, updateOpts) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/users/%s", idUser1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := users.Delete(fakeclient.ServiceClient(), idUser1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/sss/v1/users/urls.go b/v3/ecl/sss/v1/users/urls.go new file mode 100644 index 0000000..8e9c498 --- /dev/null +++ b/v3/ecl/sss/v1/users/urls.go @@ -0,0 +1,23 @@ +package users + +import "github.com/nttcom/eclcloud/v3" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("users", tenantID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func deleteURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("users", tenantID) +} + +func updateURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("users", tenantID) +} diff --git a/v3/ecl/storage/v1/virtualstorages/doc.go b/v3/ecl/storage/v1/virtualstorages/doc.go new file mode 100644 index 0000000..0f6dd42 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/doc.go @@ -0,0 +1,5 @@ +// Package virtualstorages provides information and interaction with virtualstorage in the +// Enterprise Cloud 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 virtualstorages diff --git a/v3/ecl/storage/v1/virtualstorages/requests.go b/v3/ecl/storage/v1/virtualstorages/requests.go new file mode 100644 index 0000000..35b2bd1 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/requests.go @@ -0,0 +1,179 @@ +package virtualstorages + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVirtualStorageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a VirtualStorage. This object is passed to +// the virtualstorages.Create function. For more information about these parameters, +// see the VirtualStorage object. +type CreateOpts struct { + // The virtual storage name + Name string `json:"name" required:"true"` + // The virtual storage description + Description string `json:"description,omitempty"` + // The network_id to connect virtual storage + NetworkID string `json:"network_id" required:"true"` + // The subnet_id to connect virtual storage + SubnetID string `json:"subnet_id" required:"true"` + // The virtual storage volume_type_id + VolumeTypeID string `json:"volume_type_id" required:"true"` + // The ip address pool of virtual storage + IPAddrPool IPAddressPool `json:"ip_addr_pool" required:"true"` + // The virtual storage host_routes + HostRoutes []HostRoute `json:"host_routes,omitempty"` +} + +// ToVirtualStorageCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVirtualStorageCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_storage") +} + +// Create will create a new VirtualStorage based on the values in CreateOpts. +// To extract the VirtualStorage object from the response, call the Extract method on the +// CreateResult. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVirtualStorageCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing VirtualStorage with the provided ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete( + deleteURL(client, id), + &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get retrieves the VirtualStorage with the provided ID. +// To extract the VirtualStorage object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVirtualStorageListQuery() (string, error) +} + +// ListOpts holds options for listing VirtualStorages. +// It is passed to the virtualstorages.List function. +type ListOpts struct { + // Now there are no definiton as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToVirtualStorageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVirtualStorageListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns VirtualStorage optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVirtualStorageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VirtualStoragePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVirtualStorageUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing VirtualStorage. +// This object is passed to the virtual_storage.Update function. +// For more information about the parameters, see the VirtualStorage object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + IPAddrPool *IPAddressPool `json:"ip_addr_pool,omitempty"` + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` +} + +// ToVirtualStorageUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVirtualStorageUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_storage") +} + +// Update will update the VirtualStorage with provided information. +// To extract the updated VirtualStorage from the response, +// call the Extract method on the UpdateResult. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVirtualStorageUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// IDFromName is a convenience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + // Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVirtualStorages(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "virtual_storage"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_storage"} + } +} diff --git a/v3/ecl/storage/v1/virtualstorages/results.go b/v3/ecl/storage/v1/virtualstorages/results.go new file mode 100644 index 0000000..4012f31 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/results.go @@ -0,0 +1,130 @@ +package virtualstorages + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// IPAddressPool is struct which corresponds to ip_addr_pool object. +type IPAddressPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute is struct which corresponds to host_routes object. +type HostRoute struct { + Destination string `json:"destination"` + Nexthop string `json:"nexthop"` +} + +// VirtualStorage contains all the information associated with a Virtual Storage. +type VirtualStorage struct { + // API error in virtual storage creation. + APIErrorMessage string `json:"api_error_message"` + // Unique identifier for the virtual storage. + ID string `json:"id"` + // network_id which this virtual storage is connected. + NetworkID string `json:"network_id"` + // subnet_id which this virtual storage is connected. + SubnetID string `json:"subnet_id"` + // ip_address_pool object for virtual storage. + IPAddrPool IPAddressPool `json:"ip_addr_pool"` + // List of host routes of virtual storage. + HostRoutes []HostRoute `json:"host_routes"` + // volume_type_id of virtual storage + VolumeTypeID string `json:"volume_type_id"` + // Human-readable display name for the virtual storage. + Name string `json:"name"` + // Human-readable description for the virtual storage. + Description string `json:"description"` + // Current status of the virtual storage. + Status string `json:"status"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Error in virtual storage creation. + ErrorMessage string `json:"error_message"` +} + +// UnmarshalJSON creates JSON format of virtual storage +func (r *VirtualStorage) UnmarshalJSON(b []byte) error { + type tmp VirtualStorage + var s struct { + tmp + CreatedAt eclcloud.JSONISO8601 `json:"created_at"` + UpdatedAt eclcloud.JSONISO8601 `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VirtualStorage(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VirtualStoragePage is a pagination.pager that is returned from a call to the List function. +type VirtualStoragePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no VirtualStorages. +func (r VirtualStoragePage) IsEmpty() (bool, error) { + vss, err := ExtractVirtualStorages(r) + return len(vss) == 0, err +} + +// ExtractVirtualStorages extracts and returns VirtualStorages. +// It is used while iterating over a virtualstorages.List call. +func ExtractVirtualStorages(r pagination.Page) ([]VirtualStorage, error) { + var s []VirtualStorage + err := ExtractVirtualStoragesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the VirtualStorage object out of the commonResult object. +func (r commonResult) Extract() (*VirtualStorage, error) { + var s VirtualStorage + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "virtual_storage") +} + +// ExtractVirtualStoragesInto is information expander for virtual storage +func ExtractVirtualStoragesInto(r pagination.Page, v interface{}) error { + return r.(VirtualStoragePage).Result.ExtractIntoSlicePtr(v, "virtual_storages") +} + +// 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 +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/storage/v1/virtualstorages/testing/doc.go b/v3/ecl/storage/v1/virtualstorages/testing/doc.go new file mode 100644 index 0000000..2b09490 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains virtual storage unit tests +package testing diff --git a/v3/ecl/storage/v1/virtualstorages/testing/fixtures.go b/v3/ecl/storage/v1/virtualstorages/testing/fixtures.go new file mode 100644 index 0000000..268a122 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/testing/fixtures.go @@ -0,0 +1,435 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/storage/v1/virtualstorages" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idVirtualStorage1 = "fb3efc23-ca8c-4eb5-b7f6-6fc66ff24f9c" +const idVirtualStorage2 = "3535de20-192d-4f5a-a74a-cd1a9c1bf747" + +const idVolumeType = "4f4971a5-899d-42b4-8442-24f17eac9683" + +const nameVirtualStorage1 = "virtual_storage_name_1" +const descriptionVirtualStorage1 = "virtual_storage_description_1" + +const nameVirtualStorage1Update = "virtual_storage_name_1-update" +const descriptionVirtualStorage1Update = "virtual_storage_description_1-update" + +const tenantID = "2d5b878c-147a-4d7c-87fd-90a8be9d255f" + +const networkID = "511f266e-a8bf-4547-ab2a-fc4d2bda9f81" +const subnetID = "9f3fd369-e4d4-4c3a-84f1-9c5ba7686297" + +const storageTime = "2015-05-17T18:14:34+0000" + +const hostRoute1Destination = "0.0.0.0/0" +const hostRoute1Nexthop = "123.123.123.1" +const hostRoute2Destination = "192.168.0.0/24" +const hostRoute2Nexthop = "123.123.123.1" +const hostRoute3Destination = "192.168.1.0/24" +const hostRoute3Nexthop = "123.123.123.1" + +const ipAddrPoolStart = "192.168.1.10" +const ipAddrPoolEnd = "192.168.1.20" + +const ipAddrPoolStartUpdate = "192.168.1.9" +const ipAddrPoolEndUpdate = "192.168.1.21" + +// ListResponse is mocked response of virtualstorages.List +var ListResponse = fmt.Sprintf(` +{ + "virtual_storages": [ + { + "id" : "%s", + "volume_type_id" : "%s", + "name" : "%s", + "description" : "%s", + "tenant_id" : "%s", + "network_id" : "%s", + "subnet_id" : "%s", + "ip_addr_pool" : { + "start" : "%s", + "end" : "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination":"%s", + "nexthop": "%s" + }], + "status" : "available", + "links": [{ + "href": "http://storage.sdp.url:port/v1.0/virtual_storages/440cf918-3ee0-4143-b289-f63e1d2000e6", + "rel": "self" + }], + "created_at" : "%s", + "updated_at" : "%s" + }, + { + "id" : "%s", + "volume_type_id" : "%s", + "name" : "virtual_storage_name_2", + "description" : "virtual_storage_description_2", + "tenant_id" : "%s", + "network_id" : "%s", + "subnet_id" : "%s", + "ip_addr_pool" : { + "start" : "%s", + "end" : "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination":"%s", + "nexthop": "%s" + }], + "status": "available", + "links": [{ + "href": "http://storage.sdp.url:port/v1.0/virtual_storages/440cf918-3ee0-4143-b289-f63e1d2000e6", + "rel": "self" + }], + "created_at" : "%s", + "updated_at" : "%s" + } + ] +}`, + // for virtual storage 1 + idVirtualStorage1, + idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + tenantID, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + storageTime, + storageTime, + // for virtual storage 2 + idVirtualStorage1, + idVolumeType, + tenantID, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + storageTime, + storageTime) + +// GetResponse is mocked format of virtualstorages.Get +var GetResponse = fmt.Sprintf(` +{ + "virtual_storage": { + "id": "%s", + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }], + "status": "available", + "created_at": "%s", + "updated_at" : "%s", + "error_message": "" + } +}`, idVirtualStorage1, + idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + storageTime, + storageTime) + +// CreateRequest is mocked request for virtualstorages.Create +var CreateRequest = fmt.Sprintf(` +{ + "virtual_storage": { + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }] + } +}`, idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, +) + +// CreateResponse is mocked response of virtualstorages.Create +var CreateResponse = fmt.Sprintf(` +{ + "virtual_storage": { + "id": "%s", + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }], + "status": "creating", + "created_at": "null", + "error_message": "" + } +}`, idVirtualStorage1, + idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, +) + +// UpdateRequest is mocked request of virtualstorages.Update +var UpdateRequest = fmt.Sprintf(` +{ + "virtual_storage": { + "name": "%s", + "description": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }] + } +}`, nameVirtualStorage1Update, + descriptionVirtualStorage1Update, + ipAddrPoolStartUpdate, + ipAddrPoolEndUpdate, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + hostRoute3Destination, + hostRoute3Nexthop, +) + +// UpdateResponse is mocked response of virtualstorages.Update +var UpdateResponse = fmt.Sprintf(` +{ + "virtual_storage": { + "id": "%s", + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }], + "status": "available", + "created_at": "%s", + "updated_at" : "%s", + "error_message": "" + } +}`, idVirtualStorage1, + idVolumeType, + nameVirtualStorage1Update, + descriptionVirtualStorage1Update, + networkID, + subnetID, + ipAddrPoolStartUpdate, + ipAddrPoolEndUpdate, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + hostRoute3Destination, + hostRoute3Nexthop, + storageTime, + storageTime) + +func getExpectedVirtualStoragesSlice() []virtualstorages.VirtualStorage { + storageParsedTime, _ := time.Parse(eclcloud.ISO8601, storageTime) + + var virtualStorage1 = virtualstorages.VirtualStorage{ + ID: idVirtualStorage1, + VolumeTypeID: idVolumeType, + Name: nameVirtualStorage1, + Description: descriptionVirtualStorage1, + NetworkID: networkID, + SubnetID: subnetID, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + Status: "available", + } + + var virtualStorage2 = virtualstorages.VirtualStorage{ + ID: idVirtualStorage1, + VolumeTypeID: idVolumeType, + Name: "virtual_storage_name_2", + Description: "virtual_storage_description_2", + NetworkID: networkID, + SubnetID: subnetID, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + Status: "available", + } + + // ExpectedVirtualStoragesSlice is expected assertion target + ExpectedVirtualStoragesSlice := []virtualstorages.VirtualStorage{ + virtualStorage1, + virtualStorage2, + } + + return ExpectedVirtualStoragesSlice +} + +func getHostRoutes(isUpdate bool) []virtualstorages.HostRoute { + hostRoutes := []virtualstorages.HostRoute{ + { + Destination: hostRoute1Destination, + Nexthop: hostRoute1Nexthop, + }, + { + Destination: hostRoute2Destination, + Nexthop: hostRoute2Nexthop, + }, + } + + if isUpdate { + hostRoutes = append( + hostRoutes, + virtualstorages.HostRoute{ + Destination: hostRoute3Destination, + Nexthop: hostRoute3Nexthop, + }, + ) + } + + return hostRoutes +} + +func getIPAddrPool(isUpdate bool) virtualstorages.IPAddressPool { + var ipAddrPool virtualstorages.IPAddressPool + + if isUpdate { + ipAddrPool = virtualstorages.IPAddressPool{ + Start: ipAddrPoolStartUpdate, + End: ipAddrPoolEndUpdate, + } + return ipAddrPool + } + + ipAddrPool = virtualstorages.IPAddressPool{ + Start: ipAddrPoolStart, + End: ipAddrPoolEnd, + } + return ipAddrPool +} + +func getExpectedCreateVirtualStorage() virtualstorages.VirtualStorage { + + result := virtualstorages.VirtualStorage{ + ID: idVirtualStorage1, + VolumeTypeID: idVolumeType, + Name: nameVirtualStorage1, + Description: descriptionVirtualStorage1, + NetworkID: networkID, + SubnetID: subnetID, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + Status: "creating", + ErrorMessage: "", + } + return result +} diff --git a/v3/ecl/storage/v1/virtualstorages/testing/requests_test.go b/v3/ecl/storage/v1/virtualstorages/testing/requests_test.go new file mode 100644 index 0000000..ec66cda --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/testing/requests_test.go @@ -0,0 +1,167 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/storage/v1/virtualstorages" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/virtual_storages/detail", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fakeclient.ServiceClient() + count := 0 + + virtualstorages.List(client, virtualstorages.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := virtualstorages.ExtractVirtualStorages(page) + if err != nil { + t.Errorf("Failed to extract virtual storages: %v", err) + return false, err + } + + th.CheckDeepEquals(t, getExpectedVirtualStoragesSlice(), actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } + +} + +func TestGetVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/virtual_storages/%s", idVirtualStorage1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + vsActual, err := virtualstorages.Get( + fakeclient.ServiceClient(), idVirtualStorage1).Extract() + th.AssertNoErr(t, err) + vsExpected := getExpectedVirtualStoragesSlice()[0] + th.CheckDeepEquals(t, &vsExpected, vsActual) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/virtual_storages", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) // 202 + + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := virtualstorages.CreateOpts{ + VolumeTypeID: idVolumeType, + Name: nameVirtualStorage1, + Description: descriptionVirtualStorage1, + NetworkID: networkID, + SubnetID: subnetID, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + } + vsActual, err := virtualstorages.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, vsActual.Status, "creating") + + vsExpected := getExpectedCreateVirtualStorage() + th.AssertDeepEquals(t, &vsExpected, vsActual) +} + +func TestUpdateVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/virtual_storages/%s", idVirtualStorage1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameVirtualStorage1Update + description := descriptionVirtualStorage1Update + ipAddrPool := getIPAddrPool(true) + hostRoutes := getHostRoutes(true) + + updateOpts := virtualstorages.UpdateOpts{ + Name: &name, + Description: &description, + IPAddrPool: &ipAddrPool, + HostRoutes: &hostRoutes, + } + vsActual, err := virtualstorages.Update( + fakeclient.ServiceClient(), idVirtualStorage1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, vsActual.Name, nameVirtualStorage1Update) + th.AssertEquals(t, vsActual.Description, descriptionVirtualStorage1Update) + th.AssertEquals(t, vsActual.ID, idVirtualStorage1) + + th.AssertEquals(t, vsActual.IPAddrPool.Start, ipAddrPoolStartUpdate) + th.AssertEquals(t, vsActual.IPAddrPool.End, ipAddrPoolEndUpdate) + + th.AssertEquals(t, vsActual.HostRoutes[2].Destination, hostRoute3Destination) + th.AssertEquals(t, vsActual.HostRoutes[2].Nexthop, hostRoute3Nexthop) +} + +func TestDeleteVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/virtual_storages/%s", idVirtualStorage1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusOK) + }) + + res := virtualstorages.Delete(fakeclient.ServiceClient(), idVirtualStorage1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/storage/v1/virtualstorages/urls.go b/v3/ecl/storage/v1/virtualstorages/urls.go new file mode 100644 index 0000000..de09fa9 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/urls.go @@ -0,0 +1,23 @@ +package virtualstorages + +import "github.com/nttcom/eclcloud/v3" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_storages") +} + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_storages", "detail") +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("virtual_storages", id) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/v3/ecl/storage/v1/virtualstorages/util.go b/v3/ecl/storage/v1/virtualstorages/util.go new file mode 100644 index 0000000..b65d9e5 --- /dev/null +++ b/v3/ecl/storage/v1/virtualstorages/util.go @@ -0,0 +1,22 @@ +package virtualstorages + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v3/ecl/storage/v1/volumes/doc.go b/v3/ecl/storage/v1/volumes/doc.go new file mode 100644 index 0000000..e75c49c --- /dev/null +++ b/v3/ecl/storage/v1/volumes/doc.go @@ -0,0 +1,3 @@ +// Package volume provides information and interaction with volume in the +// Storage service. A volume is a detachable block storage device. +package volumes diff --git a/v3/ecl/storage/v1/volumes/requests.go b/v3/ecl/storage/v1/volumes/requests.go new file mode 100644 index 0000000..2d40d3f --- /dev/null +++ b/v3/ecl/storage/v1/volumes/requests.go @@ -0,0 +1,187 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v3" + // "github.com/nttcom/eclcloud/v3/ecl/storage/v1/virtualstorages" + // "github.com/nttcom/eclcloud/v3/ecl/storage/v1/volumetypes" + "github.com/nttcom/eclcloud/v3/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 { + // The volume name + Name string `json:"name" required:"true"` + // The volume description + Description string `json:"description,omitempty"` + // The volume size + Size int `json:"size" required:"true"` + // The volume IOPS as IOPS/GB + IOPSPerGB string `json:"iops_per_gb,omitempty"` + // The volume Throughput + Throughput string `json:"throughput,omitempty"` + // The initiator_iqns for volume (in case ISCSI) + InitiatorIQNs []string `json:"initiator_iqns,omitempty"` + // The availability zone of volume + AvailabilityZone string `json:"availability_zone,omitempty"` + // The parent virtual storage ID to connect volume + VirtualStorageID string `json:"virtual_storage_id" required:"true"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete( + deleteURL(client, id), + &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// 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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// 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 { + // Now there are no definitions as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volume optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.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 + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// 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 volume.Update function. +// For more information about the parameters, see the Volume object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + InitiatorIQNs *[]string `json:"initiator_iqns,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +// Volume of Storage SDP only allows to send "initiator_iqns" when +// the service type is "File Storage" type +// So in "ToVolumeUpdateMap" function, check volume type of virtual storage +// related to volume first +// And if service type is "File Storage's one", add initiator_iqns as request parameter +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// IDFromName is a convenience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + // Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/v3/ecl/storage/v1/volumes/results.go b/v3/ecl/storage/v1/volumes/results.go new file mode 100644 index 0000000..19344ea --- /dev/null +++ b/v3/ecl/storage/v1/volumes/results.go @@ -0,0 +1,130 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Volume contains all the information associated with a Volume. +type Volume struct { + // API error in volume creation. + APIErrorMessage string `json:"api_error_message"` + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The volume size + Size int `json:"size"` + // The volume IOPS GB + IOPSPerGB string `json:"iops_per_gb"` + // The volume Throughput + Throughput string `json:"throughput"` + // The initiator_iqns for volume (in case ISCSI) + InitiatorIQNs []string `json:"initiator_iqns"` + // Relevant snapshot's IDs of this volume + SnapshotIDs []string `json:"snapshot_ids"` + // IP Addresses to connect this volume as target device. + TargetIPs []string `json:"target_ips"` + // The metadata of volume + Metadata map[string]string `json:"metadata"` + // The parent virtual storage ID to connect volume + VirtualStorageID string `json:"virtual_storage_id"` + // The availability zone of volume + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Export rule of the volum + ExportRules []string `json:"export_rules"` + // Reservation parcentage about snapshot reservation capacity of the volume + PercentSnapshotReserveUsed int `json:"percent_snapshot_reserve_used"` + // Error in volume creation. + ErrorMessage string `json:"error_message"` +} + +// UnmarshalJSON creates JSON format of volume +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt eclcloud.JSONISO8601 `json:"created_at"` + UpdatedAt eclcloud.JSONISO8601 `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + vss, err := ExtractVolumes(r) + return len(vss) == 0, err +} + +// ExtractVolumes extracts and returns Volumes. +// It is used while iterating over a Volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +// ExtractVolumesInto is information expander for volume +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// 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 +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v3/ecl/storage/v1/volumes/testing/doc.go b/v3/ecl/storage/v1/volumes/testing/doc.go new file mode 100644 index 0000000..d083d20 --- /dev/null +++ b/v3/ecl/storage/v1/volumes/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains volume unit tests +package testing diff --git a/v3/ecl/storage/v1/volumes/testing/fixtures.go b/v3/ecl/storage/v1/volumes/testing/fixtures.go new file mode 100644 index 0000000..792cc24 --- /dev/null +++ b/v3/ecl/storage/v1/volumes/testing/fixtures.go @@ -0,0 +1,371 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/storage/v1/volumes" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idVolume1 = "fb3efc23-ca8c-4eb5-b7f6-6fc66ff24f9c" +const idVolume2 = "3535de20-192d-4f5a-a74a-cd1a9c1bf747" + +const idVirtualStorage = "4f4971a5-899d-42b4-8442-24f17eac9683" + +const nameVolume1 = "virtual_storage_name_1" +const descriptionVolume1 = "virtual_storage_description_1" + +const nameVolume1Update = "virtual_storage_name_1-update" +const descriptionVolume1Update = "virtual_storage_description_1-update" + +const storageTime = "2015-05-17T18:14:34+0000" + +const idVolumeType = "3f4971a5-899d-42b4-8442-24f17eac9684" + +const IQN1 = "iqn.1986-03.com.nttcom:iscsihost.0" +const IQN2 = "iqn.1986-03.com.nttcom:iscsihost.1" + +// ListResponse is mocked response of volumes.List +var ListResponse = fmt.Sprintf(` +{ + "volumes": [ + { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "available" + }, + { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "virtual_storage_name_2", + "description": "virtual_storage_description_2", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "available" + } + ] +}`, + // for volume 1 + idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, + storageTime, + storageTime, + // for volume 2 + idVolume2, + idVirtualStorage, + IQN1, + storageTime, + storageTime, +) + +// GetResponse is mocked format of volumes.Get +var GetResponse = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "available" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, + storageTime, + storageTime, +) + +// CreateRequestBlock is mocked request for volumes.Create +var CreateRequestBlock = fmt.Sprintf(` +{ + "volume": { + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "availability_zone": "zone1_groupa" + } +}`, idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, +) + +// CreateResponseBlock is mocked response of volumes.Create +var CreateResponseBlock = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "null", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "creating" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, +) + +// CreateRequestFile is mocked request for volumes.Create +var CreateRequestFile = fmt.Sprintf(` +{ + "volume": { + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 256, + "throughput": "50", + "availability_zone": "zone1_groupa" + } +}`, idVirtualStorage, + nameVolume1, + descriptionVolume1, +) + +// CreateResponseFile is mocked response of volumes.Create +var CreateResponseFile = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 256, + "throughput": "50", + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "null", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "creating" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, +) + +// UpdateRequest is mocked request of volumes.Update +var UpdateRequest = fmt.Sprintf(` +{ + "volume": { + "name": "%s", + "description": "%s", + "initiator_iqns": [ + "%s", + "%s" + ] + } +}`, nameVolume1Update, + descriptionVolume1Update, + IQN1, + IQN2, +) + +// UpdateResponse is mocked response of volumes.Update +var UpdateResponse = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s", + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "updating" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1Update, + descriptionVolume1Update, + IQN1, + IQN2, + storageTime, + storageTime, +) + +func getExpectedVolumesSlice() []volumes.Volume { + storageParsedTime, _ := time.Parse(eclcloud.ISO8601, storageTime) + + var volume1 = volumes.Volume{ + ID: idVolume1, + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + AvailabilityZone: "zone1_groupa", + Status: "available", + ErrorMessage: "", + } + + var volume2 = volumes.Volume{ + ID: idVolume2, + VirtualStorageID: idVirtualStorage, + Name: "virtual_storage_name_2", + Description: "virtual_storage_description_2", + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + AvailabilityZone: "zone1_groupa", + Status: "available", + ErrorMessage: "", + } + + // ExpectedVolumesSlice is expected assertion target + ExpectedVolumesSlice := []volumes.Volume{ + volume1, + volume2, + } + + return ExpectedVolumesSlice +} + +func getExpectedCreateBlockStorageTypeVolume() volumes.Volume { + + result := volumes.Volume{ + ID: idVolume1, + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + AvailabilityZone: "zone1_groupa", + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + Status: "creating", + ErrorMessage: "", + } + return result +} + +func getExpectedCreateFileStorageTypeVolume() volumes.Volume { + + result := volumes.Volume{ + ID: idVolume1, + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 256, + Throughput: "50", + AvailabilityZone: "zone1_groupa", + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + Status: "creating", + ErrorMessage: "", + } + return result +} diff --git a/v3/ecl/storage/v1/volumes/testing/requests_test.go b/v3/ecl/storage/v1/volumes/testing/requests_test.go new file mode 100644 index 0000000..5e514da --- /dev/null +++ b/v3/ecl/storage/v1/volumes/testing/requests_test.go @@ -0,0 +1,199 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/storage/v1/volumes" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/volumes/detail", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fakeclient.ServiceClient() + count := 0 + + volumes.List(client, volumes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumes.ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + th.CheckDeepEquals(t, getExpectedVolumesSlice(), actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } + +} + +func TestGetVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + volActual, err := volumes.Get( + fakeclient.ServiceClient(), idVolume1).Extract() + th.AssertNoErr(t, err) + volExpected := getExpectedVolumesSlice()[0] + th.CheckDeepEquals(t, &volExpected, volActual) +} + +func TestCreateBlockStorageTypeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequestBlock) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) // 202 + + fmt.Fprintf(w, CreateResponseBlock) + }) + + createOpts := volumes.CreateOpts{ + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + AvailabilityZone: "zone1_groupa", + } + volActual, err := volumes.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, volActual.Status, "creating") + + volExpected := getExpectedCreateBlockStorageTypeVolume() + th.AssertDeepEquals(t, &volExpected, volActual) +} + +func TestCreateFileStorageTypeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequestFile) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) // 202 + + fmt.Fprintf(w, CreateResponseFile) + }) + + createOpts := volumes.CreateOpts{ + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 256, + Throughput: "50", + AvailabilityZone: "zone1_groupa", + } + volActual, err := volumes.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, volActual.Status, "creating") + + volExpected := getExpectedCreateFileStorageTypeVolume() + th.AssertDeepEquals(t, &volExpected, volActual) +} + +// TestUpdateBlockStorageTypeVolume covers file storage type's codes +// So, contrary to creation tests, tests for file storage type updating is not implemented +func TestUpdateBlockStorageTypeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(fmt.Sprintf("/volumes/%s", idVolume1), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameVolume1Update + description := descriptionVolume1Update + initiatorIQNs := []string{IQN1, IQN2} + + updateOpts := volumes.UpdateOpts{ + Name: &name, + Description: &description, + InitiatorIQNs: &initiatorIQNs, + } + + volActual, err := volumes.Update( + fakeclient.ServiceClient(), idVolume1, updateOpts).Extract() + + th.AssertNoErr(t, err) + + th.AssertEquals(t, volActual.Name, nameVolume1Update) + th.AssertEquals(t, volActual.Description, descriptionVolume1Update) + + th.AssertEquals(t, volActual.InitiatorIQNs[0], IQN1) + th.AssertEquals(t, volActual.InitiatorIQNs[1], IQN2) +} + +func TestDeleteVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusOK) + }) + + res := volumes.Delete(fakeclient.ServiceClient(), idVolume1) + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/storage/v1/volumes/urls.go b/v3/ecl/storage/v1/volumes/urls.go new file mode 100644 index 0000000..4865b34 --- /dev/null +++ b/v3/ecl/storage/v1/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/nttcom/eclcloud/v3" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/v3/ecl/storage/v1/volumes/util.go b/v3/ecl/storage/v1/volumes/util.go new file mode 100644 index 0000000..548329c --- /dev/null +++ b/v3/ecl/storage/v1/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v3" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v3/ecl/storage/v1/volumetypes/doc.go b/v3/ecl/storage/v1/volumetypes/doc.go new file mode 100644 index 0000000..ad96f88 --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/doc.go @@ -0,0 +1 @@ +package volumetypes diff --git a/v3/ecl/storage/v1/volumetypes/requests.go b/v3/ecl/storage/v1/volumetypes/requests.go new file mode 100644 index 0000000..5461fbe --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/requests.go @@ -0,0 +1,85 @@ +package volumetypes + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// Get retrieves the VolumeType with the provided ID. +// To extract the VolumeType object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeTypeListQuery() (string, error) +} + +// ListOpts holds options for listing ToVolumeTypes. +// It is passed to the volumetypes.List function. +type ListOpts struct { + // Now there are no definiton as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToVolumeTypeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeTypeListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns VolumeType optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeTypeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumeTypePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + // Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumeTypes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "volume_type"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_storage"} + } +} diff --git a/v3/ecl/storage/v1/volumetypes/results.go b/v3/ecl/storage/v1/volumetypes/results.go new file mode 100644 index 0000000..bad2d23 --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/results.go @@ -0,0 +1,71 @@ +package volumetypes + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ExtraSpec is struct which corresponds to extra_specs object. +type ExtraSpec struct { + AvailableVolumeSize []int `json:"available_volume_size"` + AvailableVolumeThroughput []string `json:"available_volume_throughput"` + AvailableIOPSPerGB []string `json:"available_iops_per_gb"` +} + +// VolumeType contains all the information associated with a Virtual Storage. +type VolumeType struct { + // API error in virtual storage creation. + APIErrorMessage string `json:"api_error_message"` + // Unique identifier for the volume type. + ID string `json:"id"` + // Human-readable display name for the volume type. + Name string `json:"name"` + // Extra specification of volume type. + // This includes available_volume_size, and available_iops_per_gb, + // or available_throughput depending on storage service type. + ExtraSpecs ExtraSpec `json:"extra_specs"` +} + +// VolumeTypePage is a pagination.pager that is returned from a call to the List function. +type VolumeTypePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no VirtualStorages. +func (r VolumeTypePage) IsEmpty() (bool, error) { + vtypes, err := ExtractVolumeTypes(r) + return len(vtypes) == 0, err +} + +// ExtractVolumeTypes extracts and returns VolumeTypes. +// It is used while iterating over a volumetypes.List call. +func ExtractVolumeTypes(r pagination.Page) ([]VolumeType, error) { + var s []VolumeType + err := ExtractVolumeTypesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the VolumeType object out of the commonResult object. +func (r commonResult) Extract() (*VolumeType, error) { + var s VolumeType + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume_type") +} + +// ExtractVolumeTypesInto is information expander for volume types +func ExtractVolumeTypesInto(r pagination.Page, v interface{}) error { + return r.(VolumeTypePage).Result.ExtractIntoSlicePtr(v, "volume_types") +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} diff --git a/v3/ecl/storage/v1/volumetypes/testing/doc.go b/v3/ecl/storage/v1/volumetypes/testing/doc.go new file mode 100644 index 0000000..fc6042c --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains volume type unit tests +package testing diff --git a/v3/ecl/storage/v1/volumetypes/testing/fixtures.go b/v3/ecl/storage/v1/volumetypes/testing/fixtures.go new file mode 100644 index 0000000..a7235b1 --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/testing/fixtures.go @@ -0,0 +1,170 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3/ecl/storage/v1/volumetypes" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idVolumeType1 = "6328d234-7939-4d61-9216-736de66d15f9" +const idVolumeType2 = "bf33db2a-d13e-11e5-8949-005056ab5d30" +const idVolumeType3 = "704db6e5-8a93-41a5-850d-405913600341" + +// ListResponse is mocked response of volumetypes.List +var ListResponse = fmt.Sprintf(` +{ + "volume_types": [ + { + "extra_specs": { + "available_volume_size": [ + 100, + 250, + 500, + 1000, + 2000, + 4000, + 8000, + 12000 + ], + "available_iops_per_gb": [ + "2", + "4" + ] + }, + "id": "%s", + "name": "piops_iscsi_na" + }, + { + "extra_specs": { + "available_volume_size": [ + 256, + 512 + ], + "available_volume_throughput": [ + "50", + "100", + "250", + "400" + ] + }, + "id": "%s", + "name": "pre_nfs_na" + }, + { + "extra_specs": { + "available_volume_size": [ + 1024, + 2048, + 3072, + 4096, + 5120, + 10240, + 15360, + 20480, + 25600, + 30720, + 35840, + 40960, + 46080, + 51200, + 56320, + 61440, + 66560, + 71680, + 76800, + 81920, + 87040, + 92160, + 97280, + 102400 + ] + }, + "id": "%s", + "name": "standard_nfs_na" + } + ] +}`, + idVolumeType1, + idVolumeType2, + idVolumeType3, +) + +// GetResponse is mocked format of volumetypes.Get +var GetResponse = fmt.Sprintf(` +{ + "volume_type": { + "extra_specs": { + "available_volume_size": [ + 100, + 250, + 500, + 1000, + 2000, + 4000, + 8000, + 12000 + ], + "available_iops_per_gb": [ + "2", + "4" + ] + }, + "id": "%s", + "name": "piops_iscsi_na" + } +}`, idVolumeType1, +) + +func getExpectedVolumeTypesSlice() []volumetypes.VolumeType { + + // For Block Storage Type + var volumetype1 = volumetypes.VolumeType{ + ID: idVolumeType1, + Name: "piops_iscsi_na", + ExtraSpecs: volumetypes.ExtraSpec{ + AvailableVolumeSize: []int{ + 100, 250, 500, 1000, 2000, 4000, 8000, 12000, + }, + AvailableIOPSPerGB: []string{"2", "4"}, + }, + } + + // For File Storage(Premium) Type + var volumetype2 = volumetypes.VolumeType{ + ID: idVolumeType2, + Name: "pre_nfs_na", + ExtraSpecs: volumetypes.ExtraSpec{ + AvailableVolumeSize: []int{ + 256, 512, + }, + AvailableVolumeThroughput: []string{ + "50", "100", "250", "400", + }, + }, + } + + // For File Storage(Standard) Type + var volumetype3 = volumetypes.VolumeType{ + ID: idVolumeType3, + Name: "standard_nfs_na", + ExtraSpecs: volumetypes.ExtraSpec{ + AvailableVolumeSize: []int{ + 1024, 2048, 3072, 4096, 5120, 10240, + 15360, 20480, 25600, 30720, 35840, 40960, + 46080, 51200, 56320, 61440, 66560, 71680, + 76800, 81920, 87040, 92160, 97280, 102400, + }, + }, + } + + // ExpectedVolumeTypesSlice is expected assertion target + ExpectedVolumeTypesSlice := []volumetypes.VolumeType{ + volumetype1, + volumetype2, + volumetype3, + } + + return ExpectedVolumeTypesSlice +} diff --git a/v3/ecl/storage/v1/volumetypes/testing/requests_test.go b/v3/ecl/storage/v1/volumetypes/testing/requests_test.go new file mode 100644 index 0000000..ce8135b --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/testing/requests_test.go @@ -0,0 +1,73 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/ecl/storage/v1/volumetypes" + "github.com/nttcom/eclcloud/v3/pagination" + + th "github.com/nttcom/eclcloud/v3/testhelper" + fakeclient "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestListVolumeType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/volume_types/detail", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fakeclient.ServiceClient() + count := 0 + + volumetypes.List(client, volumetypes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumetypes.ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + th.CheckDeepEquals(t, getExpectedVolumeTypesSlice(), actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } + +} + +func TestGetVolumeType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volume_types/%s", idVolumeType1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + vtActual, err := volumetypes.Get( + fakeclient.ServiceClient(), idVolumeType1).Extract() + th.AssertNoErr(t, err) + vtExpected := getExpectedVolumeTypesSlice()[0] + th.CheckDeepEquals(t, &vtExpected, vtActual) +} diff --git a/v3/ecl/storage/v1/volumetypes/urls.go b/v3/ecl/storage/v1/volumetypes/urls.go new file mode 100644 index 0000000..93967c3 --- /dev/null +++ b/v3/ecl/storage/v1/volumetypes/urls.go @@ -0,0 +1,13 @@ +package volumetypes + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("volume_types", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("volume_types", "detail") +} diff --git a/v3/ecl/utils/base_endpoint.go b/v3/ecl/utils/base_endpoint.go new file mode 100644 index 0000000..40080f7 --- /dev/null +++ b/v3/ecl/utils/base_endpoint.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", err + } + + u.RawQuery, u.Fragment = "", "" + + path := u.Path + versionRe := regexp.MustCompile("v[0-9.]+/?") + + if version := versionRe.FindString(path); version != "" { + versionIndex := strings.Index(path, version) + u.Path = path[:versionIndex] + } + + return u.String(), nil +} diff --git a/v3/ecl/utils/choose_version.go b/v3/ecl/utils/choose_version.go new file mode 100644 index 0000000..09b9524 --- /dev/null +++ b/v3/ecl/utils/choose_version.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/nttcom/eclcloud/v3" +) + +// 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 *eclcloud.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, &eclcloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + 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) + } + } + + for _, version := range recognized { + if strings.Contains(value.ID, version.ID) { + // 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 version, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || version.Priority > highest.Priority { + highest = version + 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/v3/ecl/vna/v1/appliance_plans/doc.go b/v3/ecl/vna/v1/appliance_plans/doc.go new file mode 100644 index 0000000..2b9d5dd --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/doc.go @@ -0,0 +1,37 @@ +/* +Package appliance_plans contains functionality for working with +ECL Virtual Network Appliance Plan resources. + +Example to List Virtual Network Appliance Plans + + listOpts := appliance_plans.ListOpts{ + Description: "general", + } + + allPages, err := appliance_plans.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allVirtualNetworkAppliancePlans, err := appliance_plans.ExtractVirtualNetworkAppliancePlans(allPages) + if err != nil { + panic(err) + } + + for _, virtualNetworkAppliancePlan := range allVirtualNetworkAppliancePlans { + fmt.Printf("%+v\n", virtualNetworkAppliancePlan) + } + +Example to Show Virtual Network Appliance Plan + + virtualNetworkAppliancePlanID := "37556569-87f2-4699-b5ff-bf38e7cbf8a7" + + virtualNetworkAppliancePlan, err := appliance_plans.Get(networkClient, virtualNetworkAppliancePlanID, nil).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", virtualNetworkAppliancePlan) + +*/ +package appliance_plans diff --git a/v3/ecl/vna/v1/appliance_plans/requests.go b/v3/ecl/vna/v1/appliance_plans/requests.go new file mode 100644 index 0000000..50411e6 --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/requests.go @@ -0,0 +1,111 @@ +package appliance_plans + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/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 Virtual Network Appliance Plan attributes you want to see returned. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + ApplianceType string `q:"appliance_type"` + Version string `q:"version"` + Flavor string `q:"flavor"` + NumberOfInterfaces int `q:"number_of_interfaces"` + Enabled bool `q:"enabled"` + MaxNumberOfAap int `q:"max_number_of_aap"` + Details bool `q:"details"` + AvailabilityZone string `q:"availability_zone"` + AvailabilityZoneAvailable bool `q:"availability_zone.available"` +} + +// ToVirtualNetworkAppliancePlanListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVirtualNetworkAppliancePlanListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Virtual Network Appliance Plans. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToVirtualNetworkAppliancePlanListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return VirtualNetworkAppliancePlanPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetOptsBuilder allows extensions to add additional parameters to +// the Virtual Network Appliance Plan API request +type GetOptsBuilder interface { + ToProcessQuery() (string, error) +} + +// GetOpts represents result of Virtual Network Appliance Plan API response. +type GetOpts struct { + VirtualNetworkAppliancePlanId string `q:"virtual_network_appliance_plan_id"` + Details bool `q:"details"` +} + +// ToProcessQuery formats a GetOpts into a query string. +func (opts GetOpts) ToProcessQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves a specific Virtual Network Appliance Plan based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string, opts GetOptsBuilder) (r GetResult) { + url := getURL(c, id) + if opts != nil { + query, _ := opts.ToProcessQuery() + url += query + } + _, r.Err = c.Get(url, &r.Body, nil) + return +} + +// IDFromName is a convenience function that returns a Virtual Network Appliance Plan's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVirtualNetworkAppliancePlans(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "virtual_network_appliance_plan"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_network_appliance_plan"} + } +} diff --git a/v3/ecl/vna/v1/appliance_plans/results.go b/v3/ecl/vna/v1/appliance_plans/results.go new file mode 100644 index 0000000..06a8853 --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/results.go @@ -0,0 +1,98 @@ +package appliance_plans + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Virtual Network Appliance Plan resource. +func (r commonResult) Extract() (*VirtualNetworkAppliancePlan, error) { + var s struct { + VirtualNetworkAppliancePlan *VirtualNetworkAppliancePlan `json:"virtual_network_appliance_plan"` + } + err := r.ExtractInto(&s) + return s.VirtualNetworkAppliancePlan, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Virtual Network Appliance Plan. +type GetResult struct { + commonResult +} + +// License of Virtual Network Appliance +type License struct { + LicenseType string `json:"license_type"` +} + +// Availability Zone of Virtual Network Appliance +type AvailabilityZone struct { + AvailabilityZone string `json:"availability_zone"` + Available bool `json:"available"` + Rank int `json:"rank"` +} + +// VirtualNetworkAppliancePlan represents a Virtual Network Appliance Plan. +// See package documentation for a top-level description of what this is. +type VirtualNetworkAppliancePlan struct { + + // UUID representing the Virtual Network Appliance Plan. + ID string `json:"id"` + + // Name of the Virtual Network Appliance Plan. + Name string `json:"name"` + + // Description is description + Description string `json:"description"` + + // Type of appliance + ApplianceType string `json:"appliance_type"` + + // Version name + Version string `json:"version"` + + // Nova flavor + Flavor string `json:"flavor"` + + // Number of Interfaces + NumberOfInterfaces int `json:"number_of_interfaces"` + + // Is user allowed to create new firewalls with this plan. + Enabled bool `json:"enabled"` + + // Max Number of allowed_address_pairs + MaxNumberOfAap int `json:"max_number_of_aap"` + + // Licenses + Licenses []License `json:"licenses"` + + // AvailabilityZones + AvailabilityZones []AvailabilityZone `json:"availability_zones"` +} + +// VirtualNetworkAppliancePlanPage is the page returned by a pager when traversing over a collection +// of virtual network appliance plans. +type VirtualNetworkAppliancePlanPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a VirtualNetworkAppliancePlanPage struct is empty. +func (r VirtualNetworkAppliancePlanPage) IsEmpty() (bool, error) { + is, err := ExtractVirtualNetworkAppliancePlans(r) + return len(is) == 0, err +} + +// ExtractVirtualNetworkAppliancePlans accepts a Page struct, specifically a VirtualNetworkAppliancePlanPage struct, +// and extracts the elements into a slice of Virtual Network Appliance Plan structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVirtualNetworkAppliancePlans(r pagination.Page) ([]VirtualNetworkAppliancePlan, error) { + var s struct { + VirtualNetworkAppliancePlans []VirtualNetworkAppliancePlan `json:"virtual_network_appliance_plans"` + } + err := (r.(VirtualNetworkAppliancePlanPage)).ExtractInto(&s) + return s.VirtualNetworkAppliancePlans, err +} diff --git a/v3/ecl/vna/v1/appliance_plans/testing/doc.go b/v3/ecl/vna/v1/appliance_plans/testing/doc.go new file mode 100644 index 0000000..d17d407 --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/testing/doc.go @@ -0,0 +1,2 @@ +// Virtual Network Appliance Plans unit tests +package testing diff --git a/v3/ecl/vna/v1/appliance_plans/testing/fixtures.go b/v3/ecl/vna/v1/appliance_plans/testing/fixtures.go new file mode 100644 index 0000000..046de18 --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/testing/fixtures.go @@ -0,0 +1,195 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/vna/v1/appliance_plans" +) + +const ListResponse = ` +{ + "virtual_network_appliance_plans": [ + { + "id": "37556569-87f2-4699-b5ff-bf38e7cbf8a7", + "name": "appliance_plans_name", + "description": "appliance_plans_description", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "", + "flavor": "2CPU-8GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + } + ] +} +` +const GetResponse = ` +{ + "virtual_network_appliance_plan": { + "id": "6589b37a-cf82-4918-96fe-255683f78e76", + "name": "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + "description": "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "15.1X49-D100", + "flavor": "VSRX-2CPU-4GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + } +} +` + +var VirtualNetworkAppliancePlan1 = appliance_plans.VirtualNetworkAppliancePlan{ + ID: "37556569-87f2-4699-b5ff-bf38e7cbf8a7", + Name: "appliance_plans_name", + Description: "appliance_plans_description", + ApplianceType: "ECL::VirtualNetworkAppliance::VSRX", + Version: "", + Flavor: "2CPU-8GB", + NumberOfInterfaces: 8, + Enabled: true, + MaxNumberOfAap: 1, + Licenses: []appliance_plans.License{ + { + LicenseType: "STD", + }, + }, + AvailabilityZones: []appliance_plans.AvailabilityZone{ + { + AvailabilityZone: "zone1_groupa", + Available: true, + Rank: 1, + }, + { + AvailabilityZone: "zone1_groupb", + Available: false, + Rank: 2, + }, + }, +} + +var VirtualNetworkApplianceDetail = appliance_plans.VirtualNetworkAppliancePlan{ + ID: "6589b37a-cf82-4918-96fe-255683f78e76", + Name: "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + Description: "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + ApplianceType: "ECL::VirtualNetworkAppliance::VSRX", + Version: "15.1X49-D100", + Flavor: "VSRX-2CPU-4GB", + NumberOfInterfaces: 8, + Enabled: true, + MaxNumberOfAap: 1, + Licenses: []appliance_plans.License{ + { + LicenseType: "STD", + }, + }, + AvailabilityZones: []appliance_plans.AvailabilityZone{ + { + AvailabilityZone: "zone1_groupa", + Available: true, + Rank: 1, + }, + { + AvailabilityZone: "zone1_groupb", + Available: false, + Rank: 2, + }, + }, +} + +var ExpectedVirtualNetworkAppliancePlanSlice = []appliance_plans.VirtualNetworkAppliancePlan{VirtualNetworkAppliancePlan1} + +const ListResponseDuplicatedNames = ` +{ + "virtual_network_appliance_plans": [ + { + "id": "37556569-87f2-4699-b5ff-bf38e7cbf8a7", + "name": "appliance_plans_name", + "description": "appliance_plans_description", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "", + "flavor": "2CPU-8GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + }, + { + "id": "6589b37a-cf82-4918-96fe-255683f78e76", + "name": "appliance_plans_name", + "description": "appliance_plans_description", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "", + "flavor": "2CPU-8GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + } + ] +} +` diff --git a/v3/ecl/vna/v1/appliance_plans/testing/request_test.go b/v3/ecl/vna/v1/appliance_plans/testing/request_test.go new file mode 100644 index 0000000..b9340da --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/testing/request_test.go @@ -0,0 +1,147 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/vna/v1/appliance_plans" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" + cli "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +const TokenID = cli.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := cli.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc +} + +func TestListAppliancePlans(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/virtual_network_appliance_plans", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, ListResponse) + }) + + client := ServiceClient() + count := 0 + + appliance_plans.List(client, appliance_plans.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := appliance_plans.ExtractVirtualNetworkAppliancePlans(page) + if err != nil { + t.Errorf("Failed to extract Virtual Network Appliance Plans: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedVirtualNetworkAppliancePlanSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetAppliancePlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans/6589b37a-cf82-4918-96fe-255683f78e76", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + s, err := appliance_plans.Get(ServiceClient(), "6589b37a-cf82-4918-96fe-255683f78e76", appliance_plans.GetOpts{}).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &VirtualNetworkApplianceDetail, s) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := ServiceClient() + + expectedID := "37556569-87f2-4699-b5ff-bf38e7cbf8a7" + actualID, err := appliance_plans.IDFromName(client, "appliance_plans_name") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := ServiceClient() + + _, err := appliance_plans.IDFromName(client, "appliance_plans_nameX") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponseDuplicatedNames) + }) + + client := ServiceClient() + + _, err := appliance_plans.IDFromName(client, "appliance_plans_name") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v3/ecl/vna/v1/appliance_plans/urls.go b/v3/ecl/vna/v1/appliance_plans/urls.go new file mode 100644 index 0000000..5e430b8 --- /dev/null +++ b/v3/ecl/vna/v1/appliance_plans/urls.go @@ -0,0 +1,19 @@ +package appliance_plans + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("virtual_network_appliance_plans", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_network_appliance_plans") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/vna/v1/appliances/doc.go b/v3/ecl/vna/v1/appliances/doc.go new file mode 100644 index 0000000..dca0884 --- /dev/null +++ b/v3/ecl/vna/v1/appliances/doc.go @@ -0,0 +1,57 @@ +/* +Package appliances contains functionality for working with +ECL Commnon Function Gateway resources. + +Example to List VirtualNetworkAppliances + + listOpts := virtual_network_appliances.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := virtual_network_appliances.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allVirtualNetworkAppliances, err := virtual_network_appliances.ExtractVirtualNetworkAppliances(allPages) + if err != nil { + panic(err) + } + + for _, virtual_network_appliances := range allVirtualNetworkAppliances { + fmt.Printf("%+v", virtual_network_appliances) + } + +Example to Create a virtual_network_appliances + + createOpts := virtual_network_appliances.CreateOpts{ + Name: "network_1", + } + + virtual_network_appliances, err := virtual_network_appliances.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a virtual_network_appliances + + virtualNetworkApplianceID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := virtual_network_appliances.UpdateOpts{ + Name: "new_name", + } + + virtual_network_appliances, err := virtual_network_appliances.Update(networkClient, virtualNetworkApplianceID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a virtual_network_appliances + + virtualNetworkApplianceID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := virtual_network_appliances.Delete(networkClient, virtualNetworkApplianceID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package appliances diff --git a/v3/ecl/vna/v1/appliances/requests.go b/v3/ecl/vna/v1/appliances/requests.go new file mode 100644 index 0000000..65debde --- /dev/null +++ b/v3/ecl/vna/v1/appliances/requests.go @@ -0,0 +1,326 @@ +package appliances + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToVirtualNetworkApplianceListQuery() (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 virtual network appliance attributes you want to see returned. +type ListOpts struct { + Name string `q:"name"` + ID string `q:"id"` + ApplianceType string `q:"appliance_type"` + Description string `q:"description"` + AvailabilityZone string `q:"availability_zone"` + OSMonitoringStatus string `q:"os_monitoring_status"` + OSLoginStatus string `q:"os_login_status"` + VMStatus string `q:"vm_status"` + OperationStatus string `q:"operation_status"` + VirtualNetworkAppliancePlanID string `q:"virtual_network_appliance_plan_id"` + TenantID string `q:"tenant_id"` +} + +// ToVirtualNetworkApplianceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVirtualNetworkApplianceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// virtual network appliances. +// It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToVirtualNetworkApplianceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return AppliancePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific virtual network appliance based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToApplianceCreateMap() (map[string]interface{}, error) +} + +/* +Parameters for Create +*/ + +// CreateOptsFixedIP represents fixed ip information in virtual network appliance creation. +type CreateOptsFixedIP struct { + IPAddress string `json:"ip_address" required:"true"` +} + +// CreateOptsInterface represents each parameters in virtual network appliance creation. +type CreateOptsInterface struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + NetworkID string `json:"network_id" required:"true"` + Tags map[string]string `json:"tags,omitempty"` + FixedIPs *[]CreateOptsFixedIP `json:"fixed_ips,omitempty"` +} + +// CreateOptsInterfaces represents 1st interface in virtual network appliance creation. +type CreateOptsInterfaces struct { + Interface1 *CreateOptsInterface `json:"interface_1,omitempty"` + Interface2 *CreateOptsInterface `json:"interface_2,omitempty"` + Interface3 *CreateOptsInterface `json:"interface_3,omitempty"` + Interface4 *CreateOptsInterface `json:"interface_4,omitempty"` + Interface5 *CreateOptsInterface `json:"interface_5,omitempty"` + Interface6 *CreateOptsInterface `json:"interface_6,omitempty"` + Interface7 *CreateOptsInterface `json:"interface_7,omitempty"` + Interface8 *CreateOptsInterface `json:"interface_8,omitempty"` +} + +// CreateOpts represents options used to create a virtual network appliance. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + DefaultGateway string `json:"default_gateway,omitempty"` + AvailabilityZone string `json:"availability_zone,omitempty"` + VirtualNetworkAppliancePlanID string `json:"virtual_network_appliance_plan_id" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Interfaces *CreateOptsInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToApplianceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +// Create accepts a CreateOpts struct and creates a new virtual network appliance +// using the values provided. +// This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToApplianceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToApplianceUpdateMap() (map[string]interface{}, error) +} + +/* +Update for Allowed Address Pairs +*/ + +// UpdateAllowedAddressPairAddressInfo represents options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairAddressInfo struct { + IPAddress string `json:"ip_address" required:"true"` + MACAddress *string `json:"mac_address" required:"true"` + Type *string `json:"type" required:"true"` + VRID *interface{} `json:"vrid" required:"true"` +} + +// UpdateAllowedAddressPairInterface represents +// allowed address pairs list in update options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairInterface struct { + AllowedAddressPairs *[]UpdateAllowedAddressPairAddressInfo `json:"allowed_address_pairs,omitempty"` +} + +// UpdateAllowedAddressPairInterfaces represents +// interface list of update options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairInterfaces struct { + Interface1 *UpdateAllowedAddressPairInterface `json:"interface_1,omitempty"` + Interface2 *UpdateAllowedAddressPairInterface `json:"interface_2,omitempty"` + Interface3 *UpdateAllowedAddressPairInterface `json:"interface_3,omitempty"` + Interface4 *UpdateAllowedAddressPairInterface `json:"interface_4,omitempty"` + Interface5 *UpdateAllowedAddressPairInterface `json:"interface_5,omitempty"` + Interface6 *UpdateAllowedAddressPairInterface `json:"interface_6,omitempty"` + Interface7 *UpdateAllowedAddressPairInterface `json:"interface_7,omitempty"` + Interface8 *UpdateAllowedAddressPairInterface `json:"interface_8,omitempty"` +} + +// UpdateAllowedAddressPairOpts represents +// parent element of interfaces in update options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairOpts struct { + Interfaces *UpdateAllowedAddressPairInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceUpdateMap builds a request body from UpdateAllowedAddressPairOpts. +func (opts UpdateAllowedAddressPairOpts) ToApplianceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +/* +Update for FixedIP (includes network_id) +*/ + +// UpdateFixedIPAddressInfo represents ip address part +// of virtual network appliance update. +type UpdateFixedIPAddressInfo struct { + IPAddress string `json:"ip_address" required:"true"` +} + +// UpdateFixedIPInterface represents each interface information +// in updating network connection and fixed ip address +// of virtual network appliance. +type UpdateFixedIPInterface struct { + NetworkID *string `json:"network_id,omitempty"` + FixedIPs *[]UpdateFixedIPAddressInfo `json:"fixed_ips,omitempty"` +} + +// UpdateFixedIPInterfaces represents +// interface list of update options used to +// update virtual network appliance network connection and fixed ips. +type UpdateFixedIPInterfaces struct { + Interface1 *UpdateFixedIPInterface `json:"interface_1,omitempty"` + Interface2 *UpdateFixedIPInterface `json:"interface_2,omitempty"` + Interface3 *UpdateFixedIPInterface `json:"interface_3,omitempty"` + Interface4 *UpdateFixedIPInterface `json:"interface_4,omitempty"` + Interface5 *UpdateFixedIPInterface `json:"interface_5,omitempty"` + Interface6 *UpdateFixedIPInterface `json:"interface_6,omitempty"` + Interface7 *UpdateFixedIPInterface `json:"interface_7,omitempty"` + Interface8 *UpdateFixedIPInterface `json:"interface_8,omitempty"` +} + +// UpdateFixedIPOpts represents +// parent element of interfaces in update options used to +// update virtual network appliance network connection and fixed ips. +type UpdateFixedIPOpts struct { + Interfaces *UpdateFixedIPInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceUpdateMap builds a request body from UpdateFixedIPOpts. +func (opts UpdateFixedIPOpts) ToApplianceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +/* +Update for Metadata +*/ + +// UpdateMetadataInterface represents options used to +// update virtual network appliance metadata of interface. +type UpdateMetadataInterface struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// UpdateMetadataInterfaces represents +// list of interfaces for updating virtual network appliance metadata. +type UpdateMetadataInterfaces struct { + Interface1 *UpdateMetadataInterface `json:"interface_1,omitempty"` + Interface2 *UpdateMetadataInterface `json:"interface_2,omitempty"` + Interface3 *UpdateMetadataInterface `json:"interface_3,omitempty"` + Interface4 *UpdateMetadataInterface `json:"interface_4,omitempty"` + Interface5 *UpdateMetadataInterface `json:"interface_5,omitempty"` + Interface6 *UpdateMetadataInterface `json:"interface_6,omitempty"` + Interface7 *UpdateMetadataInterface `json:"interface_7,omitempty"` + Interface8 *UpdateMetadataInterface `json:"interface_8,omitempty"` +} + +// UpdateMetadataOpts represents +// metadata of virtual network appliance itself and +// pararent element for list of interfaces +// which are used by virtual network appliance metadata update. +type UpdateMetadataOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` + Interfaces *UpdateMetadataInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceUpdateMap builds a request body from UpdateOpts. +func (opts UpdateMetadataOpts) ToApplianceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +/* +Update Common +*/ + +// Update accepts a UpdateOpts struct and updates an existing virtual network appliance +// using the values provided. For more information, see the Create function. +func Update(c *eclcloud.ServiceClient, virtualNetworkApplianceID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToApplianceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Patch(updateURL(c, virtualNetworkApplianceID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the virtual network appliance associated with it. +func Delete(c *eclcloud.ServiceClient, virtualNetworkApplianceID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, virtualNetworkApplianceID), nil) + return +} + +// IDFromName is a convenience function that returns a virtual network appliance's +// ID, given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractAppliances(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "virtual_network_appliance"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_network_appliance"} + } +} diff --git a/v3/ecl/vna/v1/appliances/results.go b/v3/ecl/vna/v1/appliances/results.go new file mode 100644 index 0000000..05def8a --- /dev/null +++ b/v3/ecl/vna/v1/appliances/results.go @@ -0,0 +1,150 @@ +package appliances + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a virtual network appliance resource. +func (r commonResult) Extract() (*Appliance, error) { + var vna Appliance + err := r.ExtractInto(&vna) + return &vna, err +} + +// Extract interprets any commonResult as a Virtual Network Appliance, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "virtual_network_appliance") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Virtual Network Appliance. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Virtual Network Appliance. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Virtual Network Appliance. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// FixedIPInResponse represents each element of fixed ips +// of virtual network appliance. +type FixedIPInResponse struct { + IPAddress string `json:"ip_address"` + SubnetID string `json:"subnet_id"` +} + +// AllowedAddressPairInResponse represents each element of +// allowed address pair of virtual network appliance. +type AllowedAddressPairInResponse struct { + IPAddress string `json:"ip_address"` + MACAddress string `json:"mac_address"` + Type string `json:"type"` + VRID interface{} `json:"vrid"` +} + +// InterfaceInResponse works as parent element of +// each interface of virtual network appliance. +type InterfaceInResponse struct { + Name string `json:"name"` + Description string `json:"description"` + NetworkID string `json:"network_id"` + Updatable bool `json:"updatable"` + Tags map[string]string `json:"tags"` + FixedIPs []FixedIPInResponse `json:"fixed_ips"` + AllowedAddressPairs []AllowedAddressPairInResponse `json:"allowed_address_pairs"` +} + +// InterfacesInResponse works as list of interfaces +// of virtual network appliance. +type InterfacesInResponse struct { + Interface1 InterfaceInResponse `json:"interface_1"` + Interface2 InterfaceInResponse `json:"interface_2"` + Interface3 InterfaceInResponse `json:"interface_3"` + Interface4 InterfaceInResponse `json:"interface_4"` + Interface5 InterfaceInResponse `json:"interface_5"` + Interface6 InterfaceInResponse `json:"interface_6"` + Interface7 InterfaceInResponse `json:"interface_7"` + Interface8 InterfaceInResponse `json:"interface_8"` +} + +// Appliance represents, well, a virtual network appliance. +type Appliance struct { + Name string `json:"name"` + ID string `json:"id"` + ApplianceType string `json:"appliance_type"` + Description string `json:"description"` + DefaultGateway string `json:"default_gateway"` + AvailabilityZone string `json:"availability_zone"` + OSMonitoringStatus string `json:"os_monitoring_status"` + OSLoginStatus string `json:"os_login_status"` + VMStatus string `json:"vm_status"` + OperationStatus string `json:"operation_status"` + AppliancePlanID string `json:"virtual_network_appliance_plan_id"` + TenantID string `json:"tenant_id"` + Username string `json:"username"` + Password string `json:"password"` + Tags map[string]string `json:"tags"` + Interfaces InterfacesInResponse `json:"interfaces"` +} + +// AppliancePage is the page returned by a pager +// when traversing over a collection of virtual network appliance. +type AppliancePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of virtual network appliance +// 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 (r AppliancePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"appliances_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a AppliancePage struct is empty. +func (r AppliancePage) IsEmpty() (bool, error) { + is, err := ExtractAppliances(r) + return len(is) == 0, err +} + +// ExtractAppliances accepts a Page struct, +// specifically a NetworkPage struct, and extracts the elements +// into a slice of Virtual Network Appliance structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractAppliances(r pagination.Page) ([]Appliance, error) { + var s []Appliance + err := ExtractAppliancesInto(r, &s) + return s, err +} + +// ExtractAppliancesInto interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractAppliancesInto(r pagination.Page, v interface{}) error { + return r.(AppliancePage).Result.ExtractIntoSlicePtr(v, "virtual_network_appliances") +} diff --git a/v3/ecl/vna/v1/appliances/testing/doc.go b/v3/ecl/vna/v1/appliances/testing/doc.go new file mode 100644 index 0000000..f740d23 --- /dev/null +++ b/v3/ecl/vna/v1/appliances/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains virtual network appliance unit tests +package testing diff --git a/v3/ecl/vna/v1/appliances/testing/fixtures.go b/v3/ecl/vna/v1/appliances/testing/fixtures.go new file mode 100644 index 0000000..65ada27 --- /dev/null +++ b/v3/ecl/vna/v1/appliances/testing/fixtures.go @@ -0,0 +1,1061 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v3/ecl/vna/v1/appliances" +) + +const applianceType = "ECL::VirtualNetworkAppliance::VSRX" +const idAppliance1 = "45db3e66-31af-45a6-8ad2-d01521726141" +const idAppliance2 = "45db3e66-31af-45a6-8ad2-d01521726142" +const idAppliance3 = "45db3e66-31af-45a6-8ad2-d01521726143" + +const idVirtualNetworkAppliancePlan = "6589b37a-cf82-4918-96fe-255683f78e76" + +var listResponse = fmt.Sprintf(` +{ + "virtual_network_appliances": [ + { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + }, + { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_2_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "2.2.2.2", + "mac_address": "aa:bb:cc:dd:ee:f2", + "type": "", + "vrid": null + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.52", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_2", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } + ] +}`, + // for appliance1 + idAppliance1, + idVirtualNetworkAppliancePlan, + // for appliance2 + idAppliance2, + idVirtualNetworkAppliancePlan, +) + +var defaultInterface = appliances.InterfaceInResponse{ + Name: "", + Description: "", + NetworkID: "", + Updatable: true, + Tags: map[string]string{}, + FixedIPs: []appliances.FixedIPInResponse{}, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{}, +} + +var appliance1 = appliances.Appliance{ + ID: idAppliance1, + Name: "appliance_1", + ApplianceType: applianceType, + Description: "appliance_1_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + OSMonitoringStatus: "ACTIVE", + OSLoginStatus: "ACTIVE", + VMStatus: "ACTIVE", + OperationStatus: "COMPLETE", + AppliancePlanID: idVirtualNetworkAppliancePlan, + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Tags: map[string]string{"k1": "v1"}, + Interfaces: appliances.InterfacesInResponse{ + Interface1: appliances.InterfaceInResponse{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + Updatable: true, + FixedIPs: []appliances.FixedIPInResponse{ + { + IPAddress: "192.168.1.51", + SubnetID: "dummySubnetID", + }, + }, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{ + { + IPAddress: "1.1.1.1", + MACAddress: "aa:bb:cc:dd:ee:f1", + Type: "vrrp", + VRID: float64(123), + }, + }, + }, + Interface2: defaultInterface, + Interface3: defaultInterface, + Interface4: defaultInterface, + Interface5: defaultInterface, + Interface6: defaultInterface, + Interface7: defaultInterface, + Interface8: defaultInterface, + }, +} + +var appliance2 = appliances.Appliance{ + ID: idAppliance2, + Name: "appliance_2", + ApplianceType: applianceType, + Description: "appliance_2_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + OSMonitoringStatus: "ACTIVE", + OSLoginStatus: "ACTIVE", + VMStatus: "ACTIVE", + OperationStatus: "COMPLETE", + AppliancePlanID: idVirtualNetworkAppliancePlan, + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Tags: map[string]string{"k1": "v1"}, + Interfaces: appliances.InterfacesInResponse{ + Interface1: appliances.InterfaceInResponse{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + Updatable: true, + FixedIPs: []appliances.FixedIPInResponse{ + { + IPAddress: "192.168.1.52", + SubnetID: "dummySubnetID", + }, + }, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{ + { + IPAddress: "2.2.2.2", + MACAddress: "aa:bb:cc:dd:ee:f2", + Type: "", + VRID: interface{}(nil), + }, + }, + }, + Interface2: defaultInterface, + Interface3: defaultInterface, + Interface4: defaultInterface, + Interface5: defaultInterface, + Interface6: defaultInterface, + Interface7: defaultInterface, + Interface8: defaultInterface, + }, +} + +var appliance3 = appliances.Appliance{ + ID: idAppliance3, + Name: "appliance_3", + ApplianceType: applianceType, + Description: "appliance_3_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + OSMonitoringStatus: "ACTIVE", + OSLoginStatus: "ACTIVE", + VMStatus: "ACTIVE", + OperationStatus: "COMPLETE", + AppliancePlanID: idVirtualNetworkAppliancePlan, + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Username: "root", + Password: "Passw0rd", + Tags: map[string]string{"k1": "v1"}, + Interfaces: appliances.InterfacesInResponse{ + Interface1: appliances.InterfaceInResponse{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + Updatable: true, + FixedIPs: []appliances.FixedIPInResponse{ + { + IPAddress: "192.168.1.53", + SubnetID: "dummySubnetID", + }, + }, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{ + { + IPAddress: "3.3.3.3", + MACAddress: "aa:bb:cc:dd:ee:f3", + Type: "vrrp", + VRID: float64(123), + }, + }, + }, + Interface2: defaultInterface, + Interface3: defaultInterface, + Interface4: defaultInterface, + Interface5: defaultInterface, + Interface6: defaultInterface, + Interface7: defaultInterface, + Interface8: defaultInterface, + }, +} + +var expectedAppliancesSlice = []appliances.Appliance{ + appliance1, + appliance2, +} + +var getResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) + +var createRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "name": "appliance_1", + "description": "appliance_1_description", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "interfaces": { + "interface_1": { + "name": "interface_1", + "description": "interface_1_description", + "fixed_ips": [{ + "ip_address": "192.168.1.51" + }], + "network_id": "dummyNetworkID" + } + }, + "tags": { + "k1": "v1" + }, + "virtual_network_appliance_plan_id": "%s" + } + } +`, + idVirtualNetworkAppliancePlan, +) + +var createResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_3_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "3.3.3.3", + "mac_address": "aa:bb:cc:dd:ee:f3", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.53", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_3", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance3, + idVirtualNetworkAppliancePlan, +) + +var updateMetadataRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "name": "appliance_1-update", + "description": "appliance_1_description-update", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "interfaces": { + "interface_1": { + "name": "interface_1", + "description": "interface_1_description", + "tags": { + "k1": "v1", + "k2": "v2" + } + } + } + } + }`, +) + +var updateMetadataResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description-update", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1-update", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) + +var updateNetworkIDAndFixedIPRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "interfaces": { + "interface_1": { + "network_id": "dummyNetworkID2", + "fixed_ips": [ + { + "ip_address": "192.168.1.51" + }, + { + "ip_address": "192.168.1.52" + } + ] + } + } + } + }`, +) + +var updateNetworkIDAndFixedIPResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + }, + { + "ip_address": "192.168.1.52", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID2", + "tags": { + "k1": "v1" + }, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) + +var updateAllowedAddressPairsRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + }, + { + "ip_address": "2.2.2.2", + "mac_address": "aa:bb:cc:dd:ee:f2", + "type": "", + "vrid": null + } + ] + } + } + } + }`, +) + +var updateAllowedAddressPairsResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + }, + { + "ip_address": "2.2.2.2", + "mac_address": "aa:bb:cc:dd:ee:f2", + "type": "", + "vrid": null + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": { + "k1": "v1" + }, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) diff --git a/v3/ecl/vna/v1/appliances/testing/requests_test.go b/v3/ecl/vna/v1/appliances/testing/requests_test.go new file mode 100644 index 0000000..19adfae --- /dev/null +++ b/v3/ecl/vna/v1/appliances/testing/requests_test.go @@ -0,0 +1,313 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/ecl/vna/v1/appliances" + "github.com/nttcom/eclcloud/v3/pagination" + "github.com/nttcom/eclcloud/v3/testhelper/client" + + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc +} + +func TestListAppliances(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/virtual_network_appliances", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + + err := appliances.List(cli, appliances.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := appliances.ExtractAppliances(page) + if err != nil { + t.Errorf("Failed to extract virtual network appliances: %v", err) + return false, err + } + + th.CheckDeepEquals(t, expectedAppliancesSlice, actual) + + return true, nil + }) + + if err != nil { + t.Errorf("Failed to get virtual network appliance list: %v", err) + } + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetAppliance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, getResponse) + }) + + ap, err := appliances.Get(ServiceClient(), idAppliance1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &appliance1, ap) +} + +func TestCreateAppliance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliances", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, createResponse) + }) + + createOpts := appliances.CreateOpts{ + Name: "appliance_1", + Description: "appliance_1_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + VirtualNetworkAppliancePlanID: idVirtualNetworkAppliancePlan, + Tags: map[string]string{"k1": "v1"}, + Interfaces: &appliances.CreateOptsInterfaces{ + Interface1: &appliances.CreateOptsInterface{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + FixedIPs: &[]appliances.CreateOptsFixedIP{ + { + IPAddress: "192.168.1.51", + }, + }, + }, + }, + } + ap, err := appliances.Create(ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.OperationStatus, "COMPLETE") + th.AssertDeepEquals(t, &appliance3, ap) +} + +func TestDeleteAppliance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := appliances.Delete(ServiceClient(), idAppliance1) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateApplianceMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateMetadataRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateMetadataResponse) + }) + + name := "appliance_1-update" + description := "appliance_1_description-update" + tags := map[string]string{"k1": "v1", "k2": "v2"} + + interface1Name := "interface_1" + interface1Description := "interface_1_description" + interface1Tags := map[string]string{"k1": "v1", "k2": "v2"} + + updateOptsInterface1 := appliances.UpdateMetadataInterface{ + Name: &interface1Name, + Description: &interface1Description, + Tags: &interface1Tags, + } + updateOpts := appliances.UpdateMetadataOpts{ + Name: &name, + Description: &description, + Tags: &tags, + Interfaces: &appliances.UpdateMetadataInterfaces{ + Interface1: &updateOptsInterface1, + }, + } + ap, err := appliances.Update( + ServiceClient(), idAppliance1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.Name, "appliance_1-update") + th.AssertEquals(t, ap.Description, "appliance_1_description-update") + th.AssertEquals(t, ap.ID, idAppliance1) +} + +func TestUpdateApplianceNetworkIDAndFixedIP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateNetworkIDAndFixedIPRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateNetworkIDAndFixedIPResponse) + }) + + networkID := "dummyNetworkID2" + + updateAddressInfo1 := appliances.UpdateFixedIPAddressInfo{ + IPAddress: "192.168.1.51", + } + + updateAddressInfo2 := appliances.UpdateFixedIPAddressInfo{ + IPAddress: "192.168.1.52", + } + updateFixedIPs := []appliances.UpdateFixedIPAddressInfo{ + updateAddressInfo1, + updateAddressInfo2, + } + + updateOptsInterface1 := appliances.UpdateFixedIPInterface{ + NetworkID: &networkID, + FixedIPs: &updateFixedIPs, + } + updateOpts := appliances.UpdateFixedIPOpts{ + Interfaces: &appliances.UpdateFixedIPInterfaces{ + Interface1: &updateOptsInterface1, + }, + } + ap, err := appliances.Update( + ServiceClient(), idAppliance1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.Interfaces.Interface1.NetworkID, "dummyNetworkID2") + th.AssertEquals(t, ap.Interfaces.Interface1.FixedIPs[0].IPAddress, "192.168.1.51") + th.AssertEquals(t, ap.Interfaces.Interface1.FixedIPs[1].IPAddress, "192.168.1.52") + th.AssertEquals(t, ap.ID, idAppliance1) +} +func TestUpdateApplianceAllowedAddressPairs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateAllowedAddressPairsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateAllowedAddressPairsResponse) + }) + + mac1 := "aa:bb:cc:dd:ee:f1" + type1 := "vrrp" + + var vrid1 interface{} = 123 + + UpdateAllowedAddressPairAddressInfo1 := appliances.UpdateAllowedAddressPairAddressInfo{ + IPAddress: "1.1.1.1", + MACAddress: &mac1, + Type: &type1, + VRID: &vrid1, + } + + mac2 := "aa:bb:cc:dd:ee:f2" + type2 := "" + + var vrid2 interface{} = nil + + UpdateAllowedAddressPairAddressInfo2 := appliances.UpdateAllowedAddressPairAddressInfo{ + IPAddress: "2.2.2.2", + MACAddress: &mac2, + Type: &type2, + VRID: &vrid2, + } + + updateAllowedAddressPairs := []appliances.UpdateAllowedAddressPairAddressInfo{ + UpdateAllowedAddressPairAddressInfo1, + UpdateAllowedAddressPairAddressInfo2, + } + + updateOptsInterface1 := appliances.UpdateAllowedAddressPairInterface{ + AllowedAddressPairs: &updateAllowedAddressPairs, + } + + updateOpts := appliances.UpdateAllowedAddressPairOpts{ + Interfaces: &appliances.UpdateAllowedAddressPairInterfaces{ + Interface1: &updateOptsInterface1, + }, + } + ap, err := appliances.Update( + ServiceClient(), idAppliance1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].IPAddress, "1.1.1.1") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].MACAddress, "aa:bb:cc:dd:ee:f1") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].Type, "vrrp") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].VRID, float64(123)) + + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].IPAddress, "2.2.2.2") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].MACAddress, "aa:bb:cc:dd:ee:f2") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].Type, "") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].VRID, interface{}(nil)) + + th.AssertEquals(t, ap.ID, idAppliance1) +} diff --git a/v3/ecl/vna/v1/appliances/urls.go b/v3/ecl/vna/v1/appliances/urls.go new file mode 100644 index 0000000..2dd749d --- /dev/null +++ b/v3/ecl/vna/v1/appliances/urls.go @@ -0,0 +1,33 @@ +package appliances + +import ( + "github.com/nttcom/eclcloud/v3" +) + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("virtual_network_appliances", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_network_appliances") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/endpoint_search.go b/v3/endpoint_search.go new file mode 100644 index 0000000..a106f6a --- /dev/null +++ b/v3/endpoint_search.go @@ -0,0 +1,74 @@ +package eclcloud + +// 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 +// Enterprise Cloud 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 "ecl.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 + } + eo.Availability = AvailabilityPublic +} diff --git a/v3/errors.go b/v3/errors.go new file mode 100644 index 0000000..d4edadf --- /dev/null +++ b/v3/errors.go @@ -0,0 +1,474 @@ +package eclcloud + +import ( + "fmt" + "strings" +) + +// BaseError is an error type that all other error types embed. +type BaseError struct { + DefaultErrString string + Info string +} + +func (e BaseError) Error() string { + e.DefaultErrString = "An error occurred while executing a Eclcloud request." + return e.choseErrString() +} + +func (e BaseError) choseErrString() string { + if e.Info != "" { + return e.Info + } + return e.DefaultErrString +} + +// ErrMissingInput is the error when input is required in a particular +// situation but not provided by the user +type ErrMissingInput struct { + BaseError + Argument string +} + +func (e ErrMissingInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument) + return e.choseErrString() +} + +// ErrInvalidInput is an error type used for most non-HTTP Eclcloud errors. +type ErrInvalidInput struct { + ErrMissingInput + Value interface{} +} + +func (e ErrInvalidInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value) + return e.choseErrString() +} + +// ErrMissingEnvironmentVariable is the error when environment variable is required +// in a particular situation but not provided by the user +type ErrMissingEnvironmentVariable struct { + BaseError + EnvironmentVariable string +} + +func (e ErrMissingEnvironmentVariable) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing environment variable [%s]", e.EnvironmentVariable) + return e.choseErrString() +} + +// ErrMissingAnyoneOfEnvironmentVariables is the error when anyone of the environment variables +// is required in a particular situation but not provided by the user +type ErrMissingAnyoneOfEnvironmentVariables struct { + BaseError + EnvironmentVariables []string +} + +func (e ErrMissingAnyoneOfEnvironmentVariables) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Missing one of the following environment variables [%s]", + strings.Join(e.EnvironmentVariables, ", "), + ) + return e.choseErrString() +} + +// ErrUnexpectedResponseCode is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type ErrUnexpectedResponseCode struct { + BaseError + URL string + Method string + Expected []int + Actual int + Body []byte +} + +func (e ErrUnexpectedResponseCode) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + e.Expected, e.Method, e.URL, e.Actual, e.Body, + ) + return e.choseErrString() +} + +// ErrDefault400 is the default error type returned on a 400 HTTP response code. +type ErrDefault400 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault401 is the default error type returned on a 401 HTTP response code. +type ErrDefault401 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault403 is the default error type returned on a 403 HTTP response code. +type ErrDefault403 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault404 is the default error type returned on a 404 HTTP response code. +type ErrDefault404 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault405 is the default error type returned on a 405 HTTP response code. +type ErrDefault405 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault408 is the default error type returned on a 408 HTTP response code. +type ErrDefault408 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault409 is the default error type returned on a 409 HTTP response code. +type ErrDefault409 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault429 is the default error type returned on a 429 HTTP response code. +type ErrDefault429 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault500 is the default error type returned on a 500 HTTP response code. +type ErrDefault500 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault503 is the default error type returned on a 503 HTTP response code. +type ErrDefault503 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Bad request with: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault401) Error() string { + return "Authentication failed" +} +func (e ErrDefault403) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Request forbidden: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault404) Error() string { + return "Resource not found" +} +func (e ErrDefault405) Error() string { + return "Method not allowed" +} +func (e ErrDefault408) Error() string { + return "The server timed out waiting for the request" +} +func (e ErrDefault409) Error() string { + return "Request conflicted" +} +func (e ErrDefault429) Error() string { + return "Too many requests have been sent in a given amount of time. Pause" + + " requests, wait up to one minute, and try again." +} +func (e ErrDefault500) Error() string { + return "Internal Server Error" +} +func (e ErrDefault503) Error() string { + return "The service is currently unable to handle the request due to a temporary" + + " overloading or maintenance. This is a temporary condition. Try again later." +} + +// Err400er is the interface resource error types implement to override the error message +// from a 400 error. +type Err400er interface { + Error400(ErrUnexpectedResponseCode) error +} + +// Err401er is the interface resource error types implement to override the error message +// from a 401 error. +type Err401er interface { + Error401(ErrUnexpectedResponseCode) error +} + +// Err403er is the interface resource error types implement to override the error message +// from a 403 error. +type Err403er interface { + Error403(ErrUnexpectedResponseCode) error +} + +// Err404er is the interface resource error types implement to override the error message +// from a 404 error. +type Err404er interface { + Error404(ErrUnexpectedResponseCode) error +} + +// Err405er is the interface resource error types implement to override the error message +// from a 405 error. +type Err405er interface { + Error405(ErrUnexpectedResponseCode) error +} + +// Err408er is the interface resource error types implement to override the error message +// from a 408 error. +type Err408er interface { + Error408(ErrUnexpectedResponseCode) error +} + +// Err409er is the interface resource error types implement to override the error message +// from a 409 error. +type Err409er interface { + Error409(ErrUnexpectedResponseCode) error +} + +// Err429er is the interface resource error types implement to override the error message +// from a 429 error. +type Err429er interface { + Error429(ErrUnexpectedResponseCode) error +} + +// Err500er is the interface resource error types implement to override the error message +// from a 500 error. +type Err500er interface { + Error500(ErrUnexpectedResponseCode) error +} + +// Err503er is the interface resource error types implement to override the error message +// from a 503 error. +type Err503er interface { + Error503(ErrUnexpectedResponseCode) error +} + +// ErrTimeOut is the error type returned when an operations times out. +type ErrTimeOut struct { + BaseError +} + +func (e ErrTimeOut) Error() string { + e.DefaultErrString = "A time out occurred" + return e.choseErrString() +} + +// ErrUnableToReauthenticate is the error type returned when reauthentication fails. +type ErrUnableToReauthenticate struct { + BaseError + ErrOriginal error +} + +func (e ErrUnableToReauthenticate) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrErrorAfterReauthentication is the error type returned when reauthentication +// succeeds, but an error occurs afterword (usually an HTTP error). +type ErrErrorAfterReauthentication struct { + BaseError + ErrOriginal error +} + +func (e ErrErrorAfterReauthentication) Error() string { + e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal) + return e.choseErrString() +} + +// 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. +type ErrServiceNotFound struct { + BaseError +} + +func (e ErrServiceNotFound) Error() string { + e.DefaultErrString = "No suitable service could be found in the service catalog." + return e.choseErrString() +} + +// 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. +type ErrEndpointNotFound struct { + BaseError +} + +func (e ErrEndpointNotFound) Error() string { + e.DefaultErrString = "No suitable endpoint could be found in the service catalog." + return e.choseErrString() +} + +// ErrResourceNotFound is the error when trying to retrieve a resource's +// ID by name and the resource doesn't exist. +type ErrResourceNotFound struct { + BaseError + Name string + ResourceType string +} + +func (e ErrResourceNotFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrMultipleResourcesFound is the error when trying to retrieve a resource's +// ID by name and multiple resources have the user-provided name. +type ErrMultipleResourcesFound struct { + BaseError + Name string + Count int + ResourceType string +} + +func (e ErrMultipleResourcesFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrUnexpectedType is the error when an unexpected type is encountered +type ErrUnexpectedType struct { + BaseError + Expected string + Actual string +} + +func (e ErrUnexpectedType) Error() string { + e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual) + return e.choseErrString() +} + +func unacceptedAttributeErr(attribute string) string { + return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute) +} + +// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. +type ErrAPIKeyProvided struct{ BaseError } + +func (e ErrAPIKeyProvided) Error() string { + return unacceptedAttributeErr("APIKey") +} + +// ErrTenantIDProvided indicates that a TenantID was provided but can't be used. +type ErrTenantIDProvided struct{ BaseError } + +func (e ErrTenantIDProvided) Error() string { + return unacceptedAttributeErr("TenantID") +} + +// ErrTenantNameProvided indicates that a TenantName was provided but can't be used. +type ErrTenantNameProvided struct{ BaseError } + +func (e ErrTenantNameProvided) Error() string { + return unacceptedAttributeErr("TenantName") +} + +// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. +type ErrUsernameWithToken struct{ BaseError } + +func (e ErrUsernameWithToken) Error() string { + return redundantWithTokenErr("Username") +} + +// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. +type ErrUserIDWithToken struct{ BaseError } + +func (e ErrUserIDWithToken) Error() string { + return redundantWithTokenErr("UserID") +} + +// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. +type ErrDomainIDWithToken struct{ BaseError } + +func (e ErrDomainIDWithToken) Error() string { + return redundantWithTokenErr("DomainID") +} + +// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s +type ErrDomainNameWithToken struct{ BaseError } + +func (e ErrDomainNameWithToken) Error() string { + return redundantWithTokenErr("DomainName") +} + +// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. +type ErrUsernameOrUserID struct{ BaseError } + +func (e ErrUsernameOrUserID) Error() string { + return "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. +type ErrDomainIDWithUserID struct{ BaseError } + +func (e ErrDomainIDWithUserID) Error() string { + return redundantWithUserID("DomainID") +} + +// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. +type ErrDomainNameWithUserID struct{ BaseError } + +func (e ErrDomainNameWithUserID) Error() string { + return 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. +type ErrDomainIDOrDomainName struct{ BaseError } + +func (e ErrDomainIDOrDomainName) Error() string { + return "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. +type ErrMissingPassword struct{ BaseError } + +func (e ErrMissingPassword) Error() string { + return "You must provide a password to authenticate" +} + +// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. +type ErrScopeDomainIDOrDomainName struct{ BaseError } + +func (e ErrScopeDomainIDOrDomainName) Error() string { + return "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. +type ErrScopeProjectIDOrProjectName struct{ BaseError } + +func (e ErrScopeProjectIDOrProjectName) Error() string { + return "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. +type ErrScopeProjectIDAlone struct{ BaseError } + +func (e ErrScopeProjectIDAlone) Error() string { + return "ProjectID must be supplied alone in a Scope" +} + +// ErrScopeEmpty indicates that no credentials were provided in a Scope. +type ErrScopeEmpty struct{ BaseError } + +func (e ErrScopeEmpty) Error() string { + return "You must provide either a Project or Domain in a Scope" +} + +// ErrAppCredMissingSecret indicates that no Application Credential Secret was provided with Application Credential ID or Name +type ErrAppCredMissingSecret struct{ BaseError } + +func (e ErrAppCredMissingSecret) Error() string { + return "You must provide an Application Credential Secret" +} diff --git a/v3/go.mod b/v3/go.mod index 63b9bf7..9da2c1f 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -1,3 +1,3 @@ module github.com/nttcom/eclcloud/v3 -go 1.13 +go 1.17 diff --git a/v3/internal/pkg.go b/v3/internal/pkg.go new file mode 100644 index 0000000..5bf0569 --- /dev/null +++ b/v3/internal/pkg.go @@ -0,0 +1 @@ +package internal diff --git a/v3/internal/testing/pkg.go b/v3/internal/testing/pkg.go new file mode 100644 index 0000000..7603f83 --- /dev/null +++ b/v3/internal/testing/pkg.go @@ -0,0 +1 @@ +package testing diff --git a/v3/internal/testing/util_test.go b/v3/internal/testing/util_test.go new file mode 100644 index 0000000..b26b32b --- /dev/null +++ b/v3/internal/testing/util_test.go @@ -0,0 +1,42 @@ +package testing + +import ( + "reflect" + "testing" + + "github.com/nttcom/eclcloud/v3/internal" +) + +func TestRemainingKeys(t *testing.T) { + type User struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Location string `json:"-"` + CreatedAt string `json:"-"` + Status string + IsAdmin bool + } + + userResponse := map[string]interface{}{ + "user_id": "abcd1234", + "username": "jdoe", + "location": "Hawaii", + "created_at": "2017-06-08T02:49:03.000000", + "status": "active", + "is_admin": "true", + "custom_field": "foo", + } + + expected := map[string]interface{}{ + "created_at": "2017-06-08T02:49:03.000000", + "is_admin": "true", + "custom_field": "foo", + } + + actual := internal.RemainingKeys(User{}, userResponse) + + isEqual := reflect.DeepEqual(expected, actual) + if !isEqual { + t.Fatalf("expected %s but got %s", expected, actual) + } +} diff --git a/v3/internal/util.go b/v3/internal/util.go new file mode 100644 index 0000000..8efb283 --- /dev/null +++ b/v3/internal/util.go @@ -0,0 +1,34 @@ +package internal + +import ( + "reflect" + "strings" +) + +// RemainingKeys will inspect a struct and compare it to a map. Any struct +// field that does not have a JSON tag that matches a key in the map or +// a matching lower-case field in the map will be returned as an extra. +// +// This is useful for determining the extra fields returned in response bodies +// for resources that can contain an arbitrary or dynamic number of fields. +func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) { + extras = make(map[string]interface{}) + for k, v := range m { + extras[k] = v + } + + valueOf := reflect.ValueOf(s) + typeOf := reflect.TypeOf(s) + for i := 0; i < valueOf.NumField(); i++ { + field := typeOf.Field(i) + + lowerField := strings.ToLower(field.Name) + delete(extras, lowerField) + + if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" { + delete(extras, tagValue) + } + } + + return +} diff --git a/v3/pagination/http.go b/v3/pagination/http.go new file mode 100644 index 0000000..56cc6fb --- /dev/null +++ b/v3/pagination/http.go @@ -0,0 +1,59 @@ +package pagination + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v3" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + eclcloud.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 PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { + return PageResult{ + Result: eclcloud.Result{ + Body: body, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *eclcloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &eclcloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + }) +} diff --git a/v3/pagination/linked.go b/v3/pagination/linked.go new file mode 100644 index 0000000..201006f --- /dev/null +++ b/v3/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/nttcom/eclcloud/v3" +) + +// 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 { + err := eclcloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + err := eclcloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := eclcloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := eclcloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// 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/v3/pagination/marker.go b/v3/pagination/marker.go new file mode 100644 index 0000000..7721cf0 --- /dev/null +++ b/v3/pagination/marker.go @@ -0,0 +1,57 @@ +package pagination + +import ( + "fmt" + "github.com/nttcom/eclcloud/v3" + "reflect" +) + +// 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 +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := eclcloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// 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/v3/pagination/pager.go b/v3/pagination/pager.go new file mode 100644 index 0000000..71f9599 --- /dev/null +++ b/v3/pagination/pager.go @@ -0,0 +1,250 @@ +package pagination + +import ( + "errors" + "fmt" + "github.com/nttcom/eclcloud/v3" + "net/http" + "reflect" + "strings" +) + +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 *eclcloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + firstPage 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 *eclcloud.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 { + var currentPage Page + + // if first page has already been fetched, no need to fetch it again + if p.firstPage != nil { + currentPage = p.firstPage + p.firstPage = nil + } else { + var err error + 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 first page to ascertain the page body type. + firstPage, 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(firstPage) + + // if it's a single page, just return the firstPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return firstPage, nil + } + + // store the first page to avoid getting it twice + p.firstPage = firstPage + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch pb := firstPage.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, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []interface{} (which is really []map[string]interface{}) + switch vt := v.(type) { + case []interface{}: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + 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 + } + if len(pagesSlice) > 0 { + // 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: + err := eclcloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}/[]byte/[]interface{}" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // 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/v3/pagination/pkg.go b/v3/pagination/pkg.go new file mode 100644 index 0000000..90cb4b2 --- /dev/null +++ b/v3/pagination/pkg.go @@ -0,0 +1,5 @@ +/* +Package pagination contains utilities and convenience structs +that implement common pagination idioms within Enterprise Cloud APIs. +*/ +package pagination diff --git a/v3/pagination/single.go b/v3/pagination/single.go new file mode 100644 index 0000000..e016a49 --- /dev/null +++ b/v3/pagination/single.go @@ -0,0 +1,32 @@ +package pagination + +import ( + "fmt" + "github.com/nttcom/eclcloud/v3" + "reflect" +) + +// 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 +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := eclcloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// 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/v3/pagination/testing/doc.go b/v3/pagination/testing/doc.go new file mode 100644 index 0000000..0bc1eb3 --- /dev/null +++ b/v3/pagination/testing/doc.go @@ -0,0 +1,2 @@ +// pagination +package testing diff --git a/v3/pagination/testing/linked_test.go b/v3/pagination/testing/linked_test.go new file mode 100644 index 0000000..ae5c4c4 --- /dev/null +++ b/v3/pagination/testing/linked_test.go @@ -0,0 +1,112 @@ +package testing + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/nttcom/eclcloud/v3/pagination" + "github.com/nttcom/eclcloud/v3/testhelper" +) + +// LinkedPager sample and test cases. + +type LinkedPageResult struct { + pagination.LinkedPageBase +} + +func (r LinkedPageResult) IsEmpty() (bool, error) { + is, err := ExtractLinkedInts(r) + return len(is) == 0, err +} + +func ExtractLinkedInts(r pagination.Page) ([]int, error) { + var s struct { + Ints []int `json:"ints"` + } + err := (r.(LinkedPageResult)).ExtractInto(&s) + return s.Ints, err +} + +func createLinked(t *testing.T) pagination.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 pagination.PageResult) pagination.Page { + return LinkedPageResult{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.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 pagination.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/v3/pagination/testing/marker_test.go b/v3/pagination/testing/marker_test.go new file mode 100644 index 0000000..a57f6cf --- /dev/null +++ b/v3/pagination/testing/marker_test.go @@ -0,0 +1,127 @@ +package testing + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/nttcom/eclcloud/v3/pagination" + "github.com/nttcom/eclcloud/v3/testhelper" +) + +// MarkerPager sample and test cases. + +type MarkerPageResult struct { + pagination.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) pagination.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 pagination.PageResult) pagination.Page { + p := MarkerPageResult{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return pagination.NewPager(client, testhelper.Server.URL+"/page", createPage) +} + +func ExtractMarkerStrings(page pagination.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 pagination.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/v3/pagination/testing/pagination_test.go b/v3/pagination/testing/pagination_test.go new file mode 100644 index 0000000..15c9e93 --- /dev/null +++ b/v3/pagination/testing/pagination_test.go @@ -0,0 +1,13 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/testhelper" +) + +func createClient() *eclcloud.ServiceClient { + return &eclcloud.ServiceClient{ + ProviderClient: &eclcloud.ProviderClient{TokenID: "abc123"}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/v3/pagination/testing/single_test.go b/v3/pagination/testing/single_test.go new file mode 100644 index 0000000..b71a3a1 --- /dev/null +++ b/v3/pagination/testing/single_test.go @@ -0,0 +1,79 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3/pagination" + "github.com/nttcom/eclcloud/v3/testhelper" +) + +// SinglePage sample and test cases. + +type SinglePageResult struct { + pagination.SinglePageBase +} + +func (r SinglePageResult) IsEmpty() (bool, error) { + is, err := ExtractSingleInts(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +func ExtractSingleInts(r pagination.Page) ([]int, error) { + var s struct { + Ints []int `json:"ints"` + } + err := (r.(SinglePageResult)).ExtractInto(&s) + return s.Ints, err +} + +func setupSinglePaged() pagination.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 pagination.PageResult) pagination.Page { + return SinglePageResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, testhelper.Server.URL+"/only", createPage) +} + +func TestEnumerateSinglePaged(t *testing.T) { + callCount := 0 + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + err := pager.EachPage(func(page pagination.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/v3/params.go b/v3/params.go new file mode 100644 index 0000000..88b06c3 --- /dev/null +++ b/v3/params.go @@ -0,0 +1,488 @@ +package eclcloud + +import ( + "encoding/json" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +/* +BuildRequestBody builds a map[string]interface from the given `struct`. If +parent is not an empty string, the final map[string]interface returned will +encapsulate the built one. For example: + + disk := 1 + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: &disk, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + body, err := eclcloud.BuildRequestBody(createOpts, "flavor") + +The above example can be run as-is, however it is recommended to look at how +BuildRequestBody is used within eclcloud to more fully understand how it +fits within the request process as a whole rather than use it directly as shown +above. +*/ +func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, 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]interface{}) + if optsValue.Kind() == reflect.Struct { + //fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind()) + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + + if f.Name != strings.Title(f.Name) { + //fmt.Printf("Skipping field: %s...\n", f.Name) + continue + } + + //fmt.Printf("Starting on field: %s...\n", f.Name) + + zero := isZero(v) + //fmt.Printf("v is zero?: %v\n", zero) + + // if the field has a required tag that's set to "true" + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + //fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero) + // if the field's value is zero, return a missing-argument error + if zero { + // if the field has a 'required' tag, it can't have a zero-value + err := ErrMissingInput{} + err.Argument = f.Name + return nil, err + } + } + + if xorTag := f.Tag.Get("xor"); xorTag != "" { + //fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag) + xorField := optsValue.FieldByName(xorTag) + var xorFieldIsZero bool + if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) { + xorFieldIsZero = true + } else { + if xorField.Kind() == reflect.Ptr { + xorField = xorField.Elem() + } + xorFieldIsZero = isZero(xorField) + } + if !(zero != xorFieldIsZero) { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag) + err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag) + return nil, err + } + } + + if orTag := f.Tag.Get("or"); orTag != "" { + //fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag) + //fmt.Printf("field is zero?: %v\n", zero) + if zero { + orField := optsValue.FieldByName(orTag) + var orFieldIsZero bool + if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) { + orFieldIsZero = true + } else { + if orField.Kind() == reflect.Ptr { + orField = orField.Elem() + } + orFieldIsZero = isZero(orField) + } + if orFieldIsZero { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag) + err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag) + return nil, err + } + } + } + + jsonTag := f.Tag.Get("json") + if jsonTag == "-" { + continue + } + + if v.Kind() == reflect.Slice || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice) { + sliceValue := v + if sliceValue.Kind() == reflect.Ptr { + sliceValue = sliceValue.Elem() + } + + for i := 0; i < sliceValue.Len(); i++ { + element := sliceValue.Index(i) + if element.Kind() == reflect.Struct || (element.Kind() == reflect.Ptr && element.Elem().Kind() == reflect.Struct) { + _, err := BuildRequestBody(element.Interface(), "") + if err != nil { + return nil, err + } + } + } + } + if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { + if zero { + //fmt.Printf("value before change: %+v\n", optsValue.Field(i)) + if jsonTag != "" { + jsonTagPieces := strings.Split(jsonTag, ",") + if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { + if v.CanSet() { + if !v.IsNil() { + if v.Kind() == reflect.Ptr { + v.Set(reflect.Zero(v.Type())) + } + } + //fmt.Printf("value after change: %+v\n", optsValue.Field(i)) + } + } + } + continue + } + + //fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name) + _, err := BuildRequestBody(v.Interface(), f.Name) + if err != nil { + return nil, err + } + } + } + + //fmt.Printf("opts: %+v \n", opts) + + b, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + //fmt.Printf("string(b): %s\n", string(b)) + + err = json.Unmarshal(b, &optsMap) + if err != nil { + return nil, err + } + + //fmt.Printf("optsMap: %+v\n", optsMap) + + if parent != "" { + optsMap = map[string]interface{}{parent: optsMap} + } + //fmt.Printf("optsMap after parent added: %+v\n", optsMap) + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("options type is not a struct") +} + +// 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 +) + +// IPVersion is a type for the possible IP address versions. Valid instances +// are IPv4 and IPv6 +type IPVersion int + +const ( + // IPv4 is used for IP version 4 addresses + IPv4 IPVersion = 4 + // IPv6 is used for IP version 6 addresses + IPv6 IPVersion = 6 +) + +// 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 +} + +/* +func isUnderlyingStructZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr: + return isUnderlyingStructZero(v.Elem()) + default: + return isZero(v) + } +} +*/ + +var t time.Time + +func isZero(v reflect.Value) bool { + //fmt.Printf("\n\nchecking isZero for value: %+v\n", v) + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return true + } + return false + 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) { + return v.Interface().(time.Time).IsZero() + } + 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()) + //fmt.Printf("zero type for value: %+v\n\n\n", z) + 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) { + loop: + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + goto loop + 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()) + } + } + case reflect.Map: + if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String { + var s []string + for _, k := range v.MapKeys() { + value := v.MapIndex(k).String() + s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) + } + params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", "))) + } + } + } else { + // if the field has a 'required' tag, it can't have a zero-value + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + return &url.URL{}, 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 { + // if the field has a 'required' tag, it can't have a zero-value + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + return optsMap, fmt.Errorf("required header [%s] not set", f.Name) + } + } + } + + } + 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/v3/provider_client.go b/v3/provider_client.go new file mode 100644 index 0000000..5188c75 --- /dev/null +++ b/v3/provider_client.go @@ -0,0 +1,402 @@ +package eclcloud + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "log" + "net/http" + "strings" + "sync" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const DefaultUserAgent = "eclcloud/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 +// Eclcloud 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. + // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. + // To safely read or write this value, call `Token` or `SetToken`, respectively + 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 + + mut *sync.RWMutex + + reauthmut *reauthlock +} + +type reauthlock struct { + sync.RWMutex + reauthing bool +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { + if client.reauthmut != nil { + client.reauthmut.RLock() + if client.reauthmut.reauthing { + client.reauthmut.RUnlock() + return + } + client.reauthmut.RUnlock() + } + t := client.Token() + if t == "" { + return + } + return map[string]string{"X-Auth-Token": t} +} + +// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. +// If the application's ProviderClient is not used concurrently, this doesn't need to be called. +func (client *ProviderClient) UseTokenLock() { + client.mut = new(sync.RWMutex) + client.reauthmut = new(reauthlock) +} + +// Token safely reads the value of the auth token from the ProviderClient. Applications should +// call this method to access the token instead of the TokenID field +func (client *ProviderClient) Token() string { + if client.mut != nil { + client.mut.RLock() + defer client.mut.RUnlock() + } + return client.TokenID +} + +// SetToken safely sets the value of the auth token in the ProviderClient. Applications may +// use this method in a custom ReauthFunc +func (client *ProviderClient) SetToken(t string) { + if client.mut != nil { + client.mut.Lock() + defer client.mut.Unlock() + } + client.TokenID = t +} + +//Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is +//called because of a 401 response, the caller may pass the previous token. In +//this case, the reauthentication can be skipped if another thread has already +//reauthenticated in the meantime. If no previous token is known, an empty +//string should be passed instead to force unconditional reauthentication. +func (client *ProviderClient) Reauthenticate(previousToken string) (err error) { + if client.ReauthFunc == nil { + return nil + } + + if client.mut == nil { + return client.ReauthFunc() + } + client.mut.Lock() + defer client.mut.Unlock() + + client.reauthmut.Lock() + client.reauthmut.reauthing = true + client.reauthmut.Unlock() + + if previousToken == "" || client.TokenID == previousToken { + err = client.ReauthFunc() + } + + client.reauthmut.Lock() + client.reauthmut.reauthing = false + client.reauthmut.Unlock() + return +} + +// 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 + // ErrorContext specifies the resource error type to return if an error is encountered. + // This lets resources override default error messages based on the response status code. + ErrorContext error +} + +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 + + log.Printf("[DEBUG] Request: %s %s", method, url) + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.ReadSeeker 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 eclcloud.Request().") + } + + rendered, err := json.Marshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + log.Printf("[DEBUG] Request body: %s", body) + + } + + if options.RawBody != nil { + body = options.RawBody + log.Printf("[DEBUG] Request body: %s", body) + } + + // 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) + + // 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) + } + } + } + + // get latest token from client + for k, v := range client.AuthenticatedHeaders() { + req.Header.Set(k, v) + } + + // Set connection parameter to close the connection immediately when we've got the response + req.Close = true + + prereqtok := req.Header.Get("X-Auth-Token") + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, 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() + respErr := ErrUnexpectedResponseCode{ + URL: url, + Method: method, + Expected: options.OkCodes, + Actual: resp.StatusCode, + Body: body, + } + + errType := options.ErrorContext + switch resp.StatusCode { + case http.StatusBadRequest: + err = ErrDefault400{respErr} + if error400er, ok := errType.(Err400er); ok { + err = error400er.Error400(respErr) + } + case http.StatusUnauthorized: + if client.ReauthFunc != nil { + err = client.Reauthenticate(prereqtok) + if err != nil { + e := &ErrUnableToReauthenticate{} + e.ErrOriginal = respErr + return nil, e + } + if options.RawBody != nil { + if seeker, ok := options.RawBody.(io.Seeker); ok { + seeker.Seek(0, 0) + } + } + // make a new call to request with a nil reauth func in order to avoid infinite loop + reauthFunc := client.ReauthFunc + client.ReauthFunc = nil + resp, err = client.Request(method, url, options) + client.ReauthFunc = reauthFunc + if err != nil { + switch err.(type) { + case *ErrUnexpectedResponseCode: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err.(*ErrUnexpectedResponseCode) + return nil, e + default: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err + return nil, e + } + } + return resp, nil + } + err = ErrDefault401{respErr} + if error401er, ok := errType.(Err401er); ok { + err = error401er.Error401(respErr) + } + case http.StatusForbidden: + err = ErrDefault403{respErr} + if error403er, ok := errType.(Err403er); ok { + err = error403er.Error403(respErr) + } + case http.StatusNotFound: + err = ErrDefault404{respErr} + if error404er, ok := errType.(Err404er); ok { + err = error404er.Error404(respErr) + } + case http.StatusMethodNotAllowed: + err = ErrDefault405{respErr} + if error405er, ok := errType.(Err405er); ok { + err = error405er.Error405(respErr) + } + case http.StatusRequestTimeout: + err = ErrDefault408{respErr} + if error408er, ok := errType.(Err408er); ok { + err = error408er.Error408(respErr) + } + case http.StatusConflict: + err = ErrDefault409{respErr} + if error409er, ok := errType.(Err409er); ok { + err = error409er.Error409(respErr) + } + case 429: + err = ErrDefault429{respErr} + if error429er, ok := errType.(Err429er); ok { + err = error429er.Error429(respErr) + } + case http.StatusInternalServerError: + err = ErrDefault500{respErr} + if error500er, ok := errType.(Err500er); ok { + err = error500er.Error500(respErr) + } + case http.StatusServiceUnavailable: + err = ErrDefault503{respErr} + if error503er, ok := errType.(Err503er); ok { + err = error503er.Error503(respErr) + } + } + + if err == nil { + err = respErr + } + + return resp, err + } + + // Parse the response body as JSON, if requested to do so. + if options.JSONResponse != nil { + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + return nil, err + } + } + + 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 == "PATCH": + return []int{200, 202, 204} + case method == "DELETE": + return []int{202, 204} + } + + return []int{} +} diff --git a/v3/results.go b/v3/results.go new file mode 100644 index 0000000..30e93ce --- /dev/null +++ b/v3/results.go @@ -0,0 +1,473 @@ +package eclcloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + // "log" + "log" + "net/http" + "reflect" + "strconv" + "time" +) + +/* +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 +} + +// ExtractInto allows users to provide an object into which `Extract` will extract +// the `Result.Body`. This would be useful for Enterprise Cloud providers that have +// different fields in the response object than Enterprise Cloud proper. +func (r Result) ExtractInto(to interface{}) error { + if r.Err != nil { + log.Printf("[DEBUG] Response body: %s", r.Err) + return r.Err + } + + if reader, ok := r.Body.(io.Reader); ok { + if readCloser, ok := reader.(io.Closer); ok { + defer readCloser.Close() + } + return json.NewDecoder(reader).Decode(to) + } + + b, err := json.Marshal(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + log.Printf("[DEBUG] Response body: %s", b) + + return err +} + +func (r Result) extractIntoPtr(to interface{}, label string) error { + if label == "" { + return r.ExtractInto(&to) + } + + var m map[string]interface{} + err := r.ExtractInto(&m) + if err != nil { + return err + } + + b, err := json.Marshal(m[label]) + if err != nil { + return err + } + + toValue := reflect.ValueOf(to) + if toValue.Kind() == reflect.Ptr { + toValue = toValue.Elem() + } + + switch toValue.Kind() { + case reflect.Slice: + typeOfV := toValue.Type().Elem() + if typeOfV.Kind() == reflect.Struct { + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0) + + for _, v := range m[label].([]interface{}) { + // For each iteration of the slice, we create a new struct. + // This is to work around a bug where elements of a slice + // are reused and not overwritten when the same copy of the + // struct is used: + // + // https://github.com/golang/go/issues/21092 + // https://github.com/golang/go/issues/24155 + // https://play.golang.org/p/NHo3ywlPZli + newType := reflect.New(typeOfV).Elem() + + b, err := json.Marshal(v) + if err != nil { + return err + } + + // This is needed for structs with an UnmarshalJSON method. + // Technically this is just unmarshalling the response into + // a struct that is never used, but it's good enough to + // trigger the UnmarshalJSON method. + for i := 0; i < newType.NumField(); i++ { + s := newType.Field(i).Addr().Interface() + + // Unmarshal is used rather than NewDecoder to also work + // around the above-mentioned bug. + err = json.Unmarshal(b, s) + if err != nil { + return err + } + } + + newSlice = reflect.Append(newSlice, newType) + } + + // "to" should now be properly modeled to receive the + // JSON response body and unmarshal into all the correct + // fields of the struct or composed extension struct + // at the end of this method. + toValue.Set(newSlice) + } + } + case reflect.Struct: + typeOfV := toValue.Type() + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + for i := 0; i < toValue.NumField(); i++ { + toField := toValue.Field(i) + if toField.Kind() == reflect.Struct { + s := toField.Addr().Interface() + err = json.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return err + } + } + } + } + } + + err = json.Unmarshal(b, &to) + return err +} + +// ExtractIntoStructPtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying struct type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoStructPtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Struct: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("expected pointer to struct, got: %v", t) + } +} + +// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying slice type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Slice: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("expected pointer to slice, got: %v", t) + } +} + +// 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 +Enterprise Cloud, because most of the operations don't return response bodies, +but do have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractInto allows users to provide an object into which `Extract` will +// extract the http.Header headers of the result. +func (r HeaderResult) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + tmpHeaderMap := map[string]string{} + for k, v := range r.Header { + if len(v) > 0 { + tmpHeaderMap[k] = v[0] + } + } + + b, err := json.Marshal(tmpHeaderMap) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +// ISO8601 describes a common time format used by some API responses. +// Expecially in storage SDP of Enterprise Cloud 2.0 +const ISO8601 = "2006-01-02T15:04:05+0000" + +type JSONISO8601 time.Time + +func (jt *JSONISO8601) UnmarshalJSON(data []byte) error { + // log.Printf("[DEBUG] ISO8601::UnmarshalJSON") + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + + var s string + if err := dec.Decode(&s); err != nil { + return err + } + + t, _ := time.Parse(ISO8601, s) + *jt = JSONISO8601(t) + return nil +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +type JSONRFC3339Milli time.Time + +func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + t, err := time.Parse(RFC3339Milli, s) + if err != nil { + return err + } + *jt = JSONRFC3339Milli(t) + return nil +} + +const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" + +type JSONRFC3339MilliNoZ time.Time + +func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339MilliNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339MilliNoZ(t) + return nil +} + +type JSONRFC1123 time.Time + +func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(time.RFC1123, s) + if err != nil { + return err + } + *jt = JSONRFC1123(t) + return nil +} + +type JSONUnix time.Time + +func (jt *JSONUnix) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + unix, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t = time.Unix(unix, 0) + *jt = JSONUnix(t) + return nil +} + +// RFC3339NoZ is the time format used in Heat (Orchestration). +const RFC3339NoZ = "2006-01-02T15:04:05" + +type JSONRFC3339NoZ time.Time + +func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339NoZ(t) + return nil +} + +// RFC3339ZNoT is the time format used in Zun (Containers Service). +const RFC3339ZNoT = "2006-01-02 15:04:05-07:00" + +type JSONRFC3339ZNoT time.Time + +func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoT, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoT(t) + return nil +} + +// RFC3339ZNoTNoZ is another time format used in Zun (Containers Service). +const RFC3339ZNoTNoZ = "2006-01-02 15:04:05" + +type JSONRFC3339ZNoTNoZ time.Time + +func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoTNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoTNoZ(t) + return nil +} + +/* +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 `json:"href"` + Rel string `json:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It 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/v3/service_client.go b/v3/service_client.go new file mode 100644 index 0000000..9cddab7 --- /dev/null +++ b/v3/service_client.go @@ -0,0 +1,146 @@ +package eclcloud + +import ( + "io" + "net/http" + "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 + + // This is the service client type (e.g. compute, network). + Type string + + // The microversion of the service to use. Set this to use a particular microversion. + Microversion string + + // MoreHeaders allows users (or Eclcloud) to set service-wide headers on requests. Put another way, + // values set in this field will be set on all the HTTP requests the service client sends. + MoreHeaders map[string]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, "/") +} + +func (client *ServiceClient) initReqOpts(url string, JSONBody interface{}, JSONResponse interface{}, 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 + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + + if client.Microversion != "" { + client.setMicroversionHeader(opts) + } +} + +// Get calls `Request` with the "GET" HTTP verb. +func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("GET", url, opts) +} + +// Post calls `Request` with the "POST" HTTP verb. +func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("POST", url, opts) +} + +// Put calls `Request` with the "PUT" HTTP verb. +func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PUT", url, opts) +} + +// Patch calls `Request` with the "PATCH" HTTP verb. +func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PATCH", url, opts) +} + +// Delete calls `Request` with the "DELETE" HTTP verb. +func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("DELETE", url, opts) +} + +// Head calls `Request` with the "HEAD" HTTP verb. +func (client *ServiceClient) Head(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("HEAD", url, opts) +} + +func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + switch client.Type { + case "compute": + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + case "volume": + opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion + } + + if client.Type != "" { + opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + } +} + +// Request carries out the HTTP operation for the service client +func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + if len(client.MoreHeaders) > 0 { + if options == nil { + options = new(RequestOpts) + } + for k, v := range client.MoreHeaders { + options.MoreHeaders[k] = v + } + } + return client.ProviderClient.Request(method, url, options) +} diff --git a/v3/testhelper/client/fake.go b/v3/testhelper/client/fake.go new file mode 100644 index 0000000..1eee3ca --- /dev/null +++ b/v3/testhelper/client/fake.go @@ -0,0 +1,17 @@ +package client + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/testhelper" +) + +// Fake token to use. +const TokenID = "cbc36478b0bd8e67e89469c7749d4127" + +// ServiceClient returns a generic service client for use in tests. +func ServiceClient() *eclcloud.ServiceClient { + return &eclcloud.ServiceClient{ + ProviderClient: &eclcloud.ProviderClient{TokenID: TokenID}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/v3/testhelper/convenience.go b/v3/testhelper/convenience.go new file mode 100644 index 0000000..25f6720 --- /dev/null +++ b/v3/testhelper/convenience.go @@ -0,0 +1,348 @@ +package testhelper + +import ( + "bytes" + "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 := actual.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)) + }) +} + +func isByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) bool { + return bytes.Equal(expectedBytes, actualBytes) +} + +// AssertByteArrayEquals a convenience function for checking whether two byte arrays are equal +func AssertByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) { + if !isByteArrayEquals(t, expectedBytes, actualBytes) { + logFatal(t, "The bytes differed.") + } +} + +// CheckByteArrayEquals a convenience function for silent checking whether two byte arrays are equal +func CheckByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) { + if !isByteArrayEquals(t, expectedBytes, actualBytes) { + logError(t, "The bytes differed.") + } +} + +// 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/v3/testhelper/doc.go b/v3/testhelper/doc.go new file mode 100644 index 0000000..25b4dfe --- /dev/null +++ b/v3/testhelper/doc.go @@ -0,0 +1,4 @@ +/* +Package testhelper container methods that are useful for writing unit tests. +*/ +package testhelper diff --git a/v3/testhelper/fixture/helper.go b/v3/testhelper/fixture/helper.go new file mode 100644 index 0000000..5b9265d --- /dev/null +++ b/v3/testhelper/fixture/helper.go @@ -0,0 +1,31 @@ +package fixture + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func SetupHandler(t *testing.T, url, method, requestBody, responseBody string, status int) { + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, method) + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + if requestBody != "" { + th.TestJSONRequest(t, r, requestBody) + } + + if responseBody != "" { + w.Header().Add("Content-Type", "application/json") + } + + w.WriteHeader(status) + + if responseBody != "" { + fmt.Fprintf(w, responseBody) + } + }) +} diff --git a/v3/testhelper/http_responses.go b/v3/testhelper/http_responses.go new file mode 100644 index 0000000..e1f1f9a --- /dev/null +++ b/v3/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/v3/testing/doc.go b/v3/testing/doc.go new file mode 100644 index 0000000..6336d11 --- /dev/null +++ b/v3/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains eclcloud tests. +package testing diff --git a/v3/testing/endpoint_search_test.go b/v3/testing/endpoint_search_test.go new file mode 100644 index 0000000..f3d26a1 --- /dev/null +++ b/v3/testing/endpoint_search_test.go @@ -0,0 +1,20 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestApplyDefaultsToEndpointOpts(t *testing.T) { + eo := eclcloud.EndpointOpts{Availability: eclcloud.AvailabilityPublic} + eo.ApplyDefaults("compute") + expected := eclcloud.EndpointOpts{Availability: eclcloud.AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) + + eo = eclcloud.EndpointOpts{Type: "compute"} + eo.ApplyDefaults("object-store") + expected = eclcloud.EndpointOpts{Availability: eclcloud.AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) +} diff --git a/v3/testing/params_test.go b/v3/testing/params_test.go new file mode 100644 index 0000000..f61389e --- /dev/null +++ b/v3/testing/params_test.go @@ -0,0 +1,276 @@ +package testing + +import ( + "net/url" + "reflect" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestMaybeString(t *testing.T) { + testString := "" + var expected *string + actual := eclcloud.MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) + + testString = "carol" + expected = &testString + actual = eclcloud.MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) +} + +func TestMaybeInt(t *testing.T) { + testInt := 0 + var expected *int + actual := eclcloud.MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) + + testInt = 4 + expected = &testInt + actual = eclcloud.MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) +} + +func TestBuildQueryString(t *testing.T) { + type testVar string + iFalse := false + opts := struct { + J int `q:"j"` + R string `q:"r" required:"true"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + F *bool `q:"f"` + M map[string]string `q:"m"` + }{ + J: 2, + R: "red", + C: true, + S: []string{"one", "two", "three"}, + TS: []testVar{"a", "b"}, + TI: []int{1, 2}, + F: &iFalse, + M: map[string]string{"k1": "success1"}, + } + expected := &url.URL{RawQuery: "c=true&f=false&j=2&m=%7B%27k1%27%3A%27success1%27%7D&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"} + actual, err := eclcloud.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:"true"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + F *bool `q:"f"` + M map[string]string `q:"m"` + }{ + J: 2, + C: true, + } + _, err = eclcloud.BuildQueryString(&opts) + if err == nil { + t.Errorf("Expected error: 'Required field not set'") + } + th.CheckDeepEquals(t, expected, actual) + + _, err = eclcloud.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:"true"` + Style bool `h:"Style"` + }{ + Accept: "application/json", + Num: 4, + Style: true, + } + expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"} + actual, err := eclcloud.BuildHeaders(&testStruct) + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) + + testStruct.Num = 0 + _, err = eclcloud.BuildHeaders(&testStruct) + if err == nil { + t.Errorf("Expected error: 'Required header not set'") + } + + _, err = eclcloud.BuildHeaders(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +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 := eclcloud.BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"}) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestBuildRequestBody(t *testing.T) { + type PasswordCredentials struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + } + + type TokenCredentials struct { + ID string `json:"id,omitempty" required:"true"` + } + + type orFields struct { + Filler int `json:"filler,omitempty"` + F1 int `json:"f1,omitempty" or:"F2"` + F2 int `json:"f2,omitempty" or:"F1"` + } + + // AuthOptions wraps a eclcloud AuthOptions in order to adhere to the AuthOptionsBuilder + // interface. + type AuthOptions struct { + PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // 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 string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenCredentials *TokenCredentials `json:"token,omitempty" xor:"PasswordCredentials"` + + OrFields *orFields `json:"or_fields,omitempty"` + } + + var successCases = []struct { + opts AuthOptions + expected map[string]interface{} + }{ + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + }, + map[string]interface{}{ + "auth": map[string]interface{}{ + "passwordCredentials": map[string]interface{}{ + "password": "swordfish", + "username": "me", + }, + }, + }, + }, + { + AuthOptions{ + TokenCredentials: &TokenCredentials{ + ID: "1234567", + }, + }, + map[string]interface{}{ + "auth": map[string]interface{}{ + "token": map[string]interface{}{ + "id": "1234567", + }, + }, + }, + }, + } + + for _, successCase := range successCases { + actual, err := eclcloud.BuildRequestBody(successCase.opts, "auth") + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, successCase.expected, actual) + } + + var failCases = []struct { + opts AuthOptions + expected error + }{ + { + AuthOptions{ + TenantID: "987654321", + TenantName: "me", + }, + eclcloud.ErrMissingInput{}, + }, + { + AuthOptions{ + TokenCredentials: &TokenCredentials{ + ID: "1234567", + }, + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + }, + eclcloud.ErrMissingInput{}, + }, + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Password: "swordfish", + }, + }, + eclcloud.ErrMissingInput{}, + }, + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + OrFields: &orFields{ + Filler: 2, + }, + }, + eclcloud.ErrMissingInput{}, + }, + } + + for _, failCase := range failCases { + _, err := eclcloud.BuildRequestBody(failCase.opts, "auth") + th.AssertDeepEquals(t, reflect.TypeOf(failCase.expected), reflect.TypeOf(err)) + } + + createdAt := time.Date(2018, 1, 4, 10, 00, 12, 0, time.UTC) + var complexFields = struct { + Username string `json:"username" required:"true"` + CreatedAt *time.Time `json:"-"` + }{ + Username: "jdoe", + CreatedAt: &createdAt, + } + + expectedComplexFields := map[string]interface{}{ + "username": "jdoe", + } + + actual, err := eclcloud.BuildRequestBody(complexFields, "") + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectedComplexFields, actual) + +} diff --git a/v3/testing/provider_client_test.go b/v3/testing/provider_client_test.go new file mode 100644 index 0000000..ec4ae05 --- /dev/null +++ b/v3/testing/provider_client_test.go @@ -0,0 +1,155 @@ +package testing + +import ( + "fmt" + "io/ioutil" + "net/http" + "reflect" + "sync" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" + "github.com/nttcom/eclcloud/v3/testhelper/client" +) + +func TestAuthenticatedHeaders(t *testing.T) { + p := &eclcloud.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 := &eclcloud.ProviderClient{} + + p.UserAgent.Prepend("custom-user-agent/2.4.0") + expected := "custom-user-agent/2.4.0 eclcloud/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 eclcloud/1.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) + + p.UserAgent = eclcloud.UserAgent{} + expected = "eclcloud/1.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) +} + +func TestConcurrentReauth(t *testing.T) { + var info = struct { + numreauths int + mut *sync.RWMutex + }{ + 0, + new(sync.RWMutex), + } + + numconc := 20 + + prereauthTok := client.TokenID + postreauthTok := "12345678" + + p := new(eclcloud.ProviderClient) + p.UseTokenLock() + p.SetToken(prereauthTok) + p.ReauthFunc = func() error { + time.Sleep(1 * time.Second) + p.AuthenticatedHeaders() + info.mut.Lock() + info.numreauths++ + info.mut.Unlock() + p.TokenID = postreauthTok + return nil + } + + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("X-Auth-Token") != postreauthTok { + w.WriteHeader(http.StatusUnauthorized) + return + } + info.mut.RLock() + hasReauthed := info.numreauths != 0 + info.mut.RUnlock() + + if hasReauthed { + th.CheckEquals(t, p.Token(), postreauthTok) + } + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{}`) + }) + + wg := new(sync.WaitGroup) + reqopts := new(eclcloud.RequestOpts) + reqopts.MoreHeaders = map[string]string{ + "X-Auth-Token": prereauthTok, + } + + for i := 0; i < numconc; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := p.Request("GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts) + th.CheckNoErr(t, err) + if resp == nil { + t.Errorf("got a nil response") + return + } + if resp.Body == nil { + t.Errorf("response body was nil") + return + } + defer resp.Body.Close() + actual, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("error reading response body: %s", err) + return + } + th.CheckByteArrayEquals(t, []byte(`{}`), actual) + }() + } + + wg.Wait() + + th.AssertEquals(t, 1, info.numreauths) +} + +func TestReauthEndLoop(t *testing.T) { + + p := new(eclcloud.ProviderClient) + p.UseTokenLock() + p.SetToken(client.TokenID) + p.ReauthFunc = func() error { + // Reauth func is working and returns no error + return nil + } + + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + // route always return 401 + w.WriteHeader(http.StatusUnauthorized) + }) + + reqopts := new(eclcloud.RequestOpts) + _, err := p.Request("GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts) + if err == nil { + t.Errorf("request ends with a nil error") + return + } + + if reflect.TypeOf(err) != reflect.TypeOf(&eclcloud.ErrErrorAfterReauthentication{}) { + t.Errorf("error is not an ErrErrorAfterReauthentication") + } +} diff --git a/v3/testing/results_test.go b/v3/testing/results_test.go new file mode 100644 index 0000000..7d4e2da --- /dev/null +++ b/v3/testing/results_test.go @@ -0,0 +1,208 @@ +package testing + +import ( + "encoding/json" + "testing" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +var singleResponse = ` +{ + "person": { + "name": "Bill", + "email": "bill@example.com", + "location": "Canada" + } +} +` + +var multiResponse = ` +{ + "people": [ + { + "name": "Bill", + "email": "bill@example.com", + "location": "Canada" + }, + { + "name": "Ted", + "email": "ted@example.com", + "location": "Mexico" + } + ] +} +` + +type TestPerson struct { + Name string `json:"-"` + Email string `json:"email"` +} + +func (r *TestPerson) UnmarshalJSON(b []byte) error { + type tmp TestPerson + var s struct { + tmp + Name string `json:"name"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = TestPerson(s.tmp) + r.Name = s.Name + " unmarshalled" + + return nil +} + +type TestPersonExt struct { + Location string `json:"-"` +} + +func (r *TestPersonExt) UnmarshalJSON(b []byte) error { + type tmp TestPersonExt + var s struct { + tmp + Location string `json:"location"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = TestPersonExt(s.tmp) + r.Location = s.Location + " unmarshalled" + + return nil +} + +type TestPersonWithExtensions struct { + TestPerson + TestPersonExt +} + +type TestPersonWithExtensionsNamed struct { + TestPerson TestPerson + TestPersonExt TestPersonExt +} + +// TestUnmarshalAnonymousStruct tests if UnmarshalJSON is called on each +// of the anonymous structs contained in an overarching struct. +func TestUnmarshalAnonymousStructs(t *testing.T) { + var actual TestPersonWithExtensions + + var dejson interface{} + sejson := []byte(singleResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var singleResult = eclcloud.Result{ + Body: dejson, + } + + err = singleResult.ExtractIntoStructPtr(&actual, "person") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "Bill unmarshalled", actual.Name) + th.AssertEquals(t, "Canada unmarshalled", actual.Location) +} + +// TestUnmarshalSliceofAnonymousStructs tests if UnmarshalJSON is called on each +// of the anonymous structs contained in an overarching struct slice. +func TestUnmarshalSliceOfAnonymousStructs(t *testing.T) { + var actual []TestPersonWithExtensions + + var dejson interface{} + sejson := []byte(multiResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var multiResult = eclcloud.Result{ + Body: dejson, + } + + err = multiResult.ExtractIntoSlicePtr(&actual, "people") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "Bill unmarshalled", actual[0].Name) + th.AssertEquals(t, "Canada unmarshalled", actual[0].Location) + th.AssertEquals(t, "Ted unmarshalled", actual[1].Name) + th.AssertEquals(t, "Mexico unmarshalled", actual[1].Location) +} + +// TestUnmarshalSliceOfStruct tests if extracting results from a "normal" +// struct still works correctly. +func TestUnmarshalSliceofStruct(t *testing.T) { + var actual []TestPerson + + var dejson interface{} + sejson := []byte(multiResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var multiResult = eclcloud.Result{ + Body: dejson, + } + + err = multiResult.ExtractIntoSlicePtr(&actual, "people") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "Bill unmarshalled", actual[0].Name) + th.AssertEquals(t, "Ted unmarshalled", actual[1].Name) +} + +// TestUnmarshalNamedStruct tests if the result is empty. +func TestUnmarshalNamedStructs(t *testing.T) { + var actual TestPersonWithExtensionsNamed + + var dejson interface{} + sejson := []byte(singleResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var singleResult = eclcloud.Result{ + Body: dejson, + } + + err = singleResult.ExtractIntoStructPtr(&actual, "person") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", actual.TestPerson.Name) + th.AssertEquals(t, "", actual.TestPersonExt.Location) +} + +// TestUnmarshalSliceofNamedStructs tests if the result is empty. +func TestUnmarshalSliceOfNamedStructs(t *testing.T) { + var actual []TestPersonWithExtensionsNamed + + var dejson interface{} + sejson := []byte(multiResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var multiResult = eclcloud.Result{ + Body: dejson, + } + + err = multiResult.ExtractIntoSlicePtr(&actual, "people") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", actual[0].TestPerson.Name) + th.AssertEquals(t, "", actual[0].TestPersonExt.Location) + th.AssertEquals(t, "", actual[1].TestPerson.Name) + th.AssertEquals(t, "", actual[1].TestPersonExt.Location) +} diff --git a/v3/testing/service_client_test.go b/v3/testing/service_client_test.go new file mode 100644 index 0000000..b98f399 --- /dev/null +++ b/v3/testing/service_client_test.go @@ -0,0 +1,34 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestServiceURL(t *testing.T) { + c := &eclcloud.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) +} + +func TestMoreHeaders(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + c := new(eclcloud.ServiceClient) + c.MoreHeaders = map[string]string{ + "custom": "header", + } + c.ProviderClient = new(eclcloud.ProviderClient) + resp, err := c.Get(fmt.Sprintf("%s/route", th.Endpoint()), nil, nil) + th.AssertNoErr(t, err) + th.AssertEquals(t, resp.Request.Header.Get("custom"), "header") +} diff --git a/v3/testing/util_test.go b/v3/testing/util_test.go new file mode 100644 index 0000000..dfd08a7 --- /dev/null +++ b/v3/testing/util_test.go @@ -0,0 +1,121 @@ +package testing + +import ( + "errors" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nttcom/eclcloud/v3" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestWaitFor(t *testing.T) { + err := eclcloud.WaitFor(2, func() (bool, error) { + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestWaitForTimeout(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := eclcloud.WaitFor(1, func() (bool, error) { + return false, nil + }) + th.AssertEquals(t, "A timeout occurred", err.Error()) +} + +func TestWaitForError(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := eclcloud.WaitFor(2, func() (bool, error) { + return false, errors.New("error has occurred") + }) + th.AssertEquals(t, "error has occurred", err.Error()) +} + +func TestWaitForPredicateExceed(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := eclcloud.WaitFor(1, func() (bool, error) { + time.Sleep(4 * time.Second) + return false, errors.New("just wasting time") + }) + th.AssertEquals(t, "A timeout occurred", err.Error()) +} + +func TestNormalizeURL(t *testing.T) { + urls := []string{ + "NoSlashAtEnd", + "SlashAtEnd/", + } + expected := []string{ + "NoSlashAtEnd/", + "SlashAtEnd/", + } + for i := 0; i < len(expected); i++ { + th.CheckEquals(t, expected[i], eclcloud.NormalizeURL(urls[i])) + } + +} + +func TestNormalizePathURL(t *testing.T) { + baseDir := "/test/path" + + rawPath := "template.yaml" + basePath := "/test/path" + result, _ := eclcloud.NormalizePathURL(basePath, rawPath) + expected := strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "template.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "http://www.google.com" + basePath = "/test/path" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "/test/path" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "http://www.google.com" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml/" + basePath = "http://www.google.com/" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "http://www.google.com/even/more" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/even/more/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/") + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml/" + basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/") + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + +} diff --git a/v3/util.go b/v3/util.go new file mode 100644 index 0000000..5726475 --- /dev/null +++ b/v3/util.go @@ -0,0 +1,99 @@ +package eclcloud + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// This is useful to wait for a resource to transition to a certain state. +// To handle situations when the predicate might hang indefinitely, the +// predicate will be prematurely cancelled after the timeout. +// 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 { + type WaitForResult struct { + Success bool + Error error + } + + start := time.Now().Unix() + + for { + // If a timeout is set, and that's been exceeded, shut it down. + if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) { + return fmt.Errorf("A timeout occurred") + } + + time.Sleep(1 * time.Second) + + var result WaitForResult + ch := make(chan bool, 1) + go func() { + defer close(ch) + satisfied, err := predicate() + result.Success = satisfied + result.Error = err + }() + + select { + case <-ch: + if result.Error != nil { + return result.Error + } + if result.Success { + return nil + } + // If the predicate has not finished by the timeout, cancel it. + case <-time.After(time.Duration(timeout) * time.Second): + return fmt.Errorf("A timeout occurred") + } + } +} + +// 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 +} + +// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as +// a reference in the filesystem, if necessary. basePath is assumed to contain +// either '.' when first used, or the file:// type fqdn of the parent resource. +// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml +func NormalizePathURL(basePath, rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", err + } + // if a scheme is defined, it must be a fqdn already + if u.Scheme != "" { + return u.String(), nil + } + // if basePath is a url, then child resources are assumed to be relative to it + bu, err := url.Parse(basePath) + if err != nil { + return "", err + } + var basePathSys, absPathSys string + if bu.Scheme != "" { + basePathSys = filepath.FromSlash(bu.Path) + absPathSys = filepath.Join(basePathSys, rawPath) + bu.Path = filepath.ToSlash(absPathSys) + return bu.String(), nil + } + + absPathSys = filepath.Join(basePath, rawPath) + u.Path = filepath.ToSlash(absPathSys) + u.Scheme = "file" + return u.String(), nil + +} diff --git a/v4/auth_options.go b/v4/auth_options.go new file mode 100644 index 0000000..9f442bf --- /dev/null +++ b/v4/auth_options.go @@ -0,0 +1,418 @@ +package eclcloud + +/* +AuthOptions stores information needed to authenticate to an Enterprise Cloud. +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. + +An example of manually providing authentication information: + + opts := eclcloud.AuthOptions{ + IdentityEndpoint: "https://keystone-{your_region}-ecl.api.ntt.com/v3/", + Username: "{api key}", + Password: "{api secret key}", + TenantID: "{tenant id}", + } + + provider, err := ecl.AuthenticatedClient(opts) + +An example of using AuthOptionsFromEnv(), where the environment variables can +be read from a file, such as a standard openrc file: + + opts, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(opts) +*/ +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. + // + // The IdentityEndpoint is typically referred to as the "auth_url" or + // "OS_AUTH_URL" in the information provided by the cloud operator. + IdentityEndpoint string `json:"-"` + + // 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 string `json:"username,omitempty"` + UserID string `json:"-"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // The same fields are known as project_id and project_name in the Identity + // V3 API, but are collected as TenantID and TenantName here in both cases. + // 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. + // If DomainID or DomainName are provided, they will also apply to TenantName. + // It is not currently possible to authenticate with Username and a Domain + // and scope to a Project in a different Domain by using TenantName. To + // accomplish that, the ProjectID will need to be provided as the TenantID + // option. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // AllowReauth should be set to true if you grant permission for Eclcloud to + // cache your credentials in memory, and to allow Eclcloud 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. + // + // NOTE: The reauth function will try to re-authenticate endlessly if left + // unchecked. The way to limit the number of attempts is to provide a custom + // HTTP client to the provider client and provide a transport that implements + // the RoundTripper interface and stores the number of failed retries. For an + // example of this, see here: + // https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Scope determines the scoping of the authentication request. + Scope *AuthScope `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` +} + +// AuthScope allows a created token to be limited to a specific domain or project. +type AuthScope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + // Populate the request map. + authMap := make(map[string]interface{}) + + if opts.Username != "" { + if opts.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": opts.Username, + "password": opts.Password, + } + } else { + return nil, ErrMissingInput{Argument: "Password"} + } + } else if opts.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": opts.TokenID, + } + } else { + return nil, ErrMissingInput{Argument: "Username"} + } + + if opts.TenantID != "" { + authMap["tenantId"] = opts.TenantID + } + if opts.TenantName != "" { + authMap["tenantName"] = opts.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + 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,omitempty"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type applicationCredentialReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + User *userReq `json:"user,omitempty"` + Secret *string `json:"secret,omitempty"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + 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 + var userRequest userReq + + if opts.Password == "" { + if opts.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if opts.Username != "" { + return nil, ErrUsernameWithToken{} + } + if opts.UserID != "" { + return nil, ErrUserIDWithToken{} + } + if opts.DomainID != "" { + return nil, ErrDomainIDWithToken{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithToken{} + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: opts.TokenID, + } + + } else if opts.ApplicationCredentialID != "" { + // Configure the request for ApplicationCredentialID authentication. + // There are three kinds of possible application_credential requests + // 1. application_credential id + secret + // 2. application_credential name + secret + user_id + // 3. application_credential name + secret + username + domain_id / domain_name + if opts.ApplicationCredentialSecret == "" { + return nil, ErrAppCredMissingSecret{} + } + req.Auth.Identity.Methods = []string{"application_credential"} + req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ + ID: &opts.ApplicationCredentialID, + Secret: &opts.ApplicationCredentialSecret, + } + } else if opts.ApplicationCredentialName != "" { + if opts.ApplicationCredentialSecret == "" { + return nil, ErrAppCredMissingSecret{} + } + // make sure that only one of DomainName or DomainID were provided + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + req.Auth.Identity.Methods = []string{"application_credential"} + if opts.DomainID != "" { + userRequest = userReq{ + Name: &opts.Username, + Domain: &domainReq{ID: &opts.DomainID}, + } + } else if opts.DomainName != "" { + userRequest = userReq{ + Name: &opts.Username, + Domain: &domainReq{Name: &opts.DomainName}, + } + } + req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ + Name: &opts.ApplicationCredentialName, + User: &userRequest, + Secret: &opts.ApplicationCredentialSecret, + } + } else { + // If no password or token ID or ApplicationCredential are available, authentication can't continue. + return nil, ErrMissingPassword{} + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if opts.Username == "" && opts.UserID == "" { + return nil, ErrUsernameOrUserID{} + } + + if opts.Username != "" { + // If Username is provided, UserID may not be provided. + if opts.UserID != "" { + return nil, ErrUsernameOrUserID{} + } + + // Either DomainID or DomainName must also be specified. + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + + if opts.DomainID != "" { + if opts.DomainName != "" { + return nil, ErrDomainIDOrDomainName{} + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + + if opts.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + } + + if opts.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if opts.DomainID != "" { + return nil, ErrDomainIDWithUserID{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithUserID{} + } + + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &opts.UserID, Password: opts.Password}, + } + } + } + + b, err := BuildRequestBody(req, "") + if err != nil { + return nil, err + } + + if len(scope) != 0 { + b["auth"].(map[string]interface{})["scope"] = scope + } + + return b, nil +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + // For backwards compatibility. + // If AuthOptions.Scope was not set, try to determine it. + // This works well for common scenarios. + if opts.Scope == nil { + opts.Scope = new(AuthScope) + if opts.TenantID != "" { + opts.Scope.ProjectID = opts.TenantID + } else { + if opts.TenantName != "" { + opts.Scope.ProjectName = opts.TenantName + opts.Scope.DomainID = opts.DomainID + opts.Scope.DomainName = opts.DomainName + } + } + } + + if opts.Scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + if opts.Scope.ProjectID != "" { + return nil, ErrScopeProjectIDOrProjectName{} + } + + if opts.Scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"id": &opts.Scope.DomainID}, + }, + }, nil + } + + if opts.Scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"name": &opts.Scope.DomainName}, + }, + }, nil + } + } else if opts.Scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if opts.Scope.DomainID != "" { + return nil, ErrScopeProjectIDAlone{} + } + if opts.Scope.DomainName != "" { + return nil, ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &opts.Scope.ProjectID, + }, + }, nil + } else if opts.Scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if opts.Scope.DomainName != "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &opts.Scope.DomainID, + }, + }, nil + } else if opts.Scope.DomainName != "" { + // DomainName + return map[string]interface{}{ + "domain": map[string]interface{}{ + "name": &opts.Scope.DomainName, + }, + }, nil + } + + return nil, nil +} + +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} diff --git a/v4/doc.go b/v4/doc.go new file mode 100644 index 0000000..58918f9 --- /dev/null +++ b/v4/doc.go @@ -0,0 +1,93 @@ +/* +Package eclcloud provides interface to Enterprise Cloud. +The library has a three-level hierarchy: providers, services, and +resources. + +Authenticating with Providers + +Provider structs represent the cloud providers that offer and manage a +collection of services. You will generally want to create one Provider +client per Enterprise Cloud. + +Use your Enterprise Cloud credentials to create a Provider client. The +IdentityEndpoint is typically referred to as "auth_url" or "OS_AUTH_URL" in +information provided by the cloud operator. Additionally, the cloud may refer to +TenantID or TenantName as project_id and project_name. Credentials are +specified like so: + + opts := eclcloud.AuthOptions{ + IdentityEndpoint: "https://keystone-{region}-ecl.api.ntt.com/v3/", + Username: "{api key}", + Password: "{api secret key}", + TenantID: "{tenant_id}", + } + + provider, err := ecl.AuthenticatedClient(opts) + +You may also use the ecl.AuthOptionsFromEnv() helper function. This +function reads in standard environment variables frequently found in an +Enterprise Cloud `openrc` file. Again note that Gophercloud currently uses "tenant" +instead of "project". + + opts, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(opts) + +Service Clients + +Service structs are specific to a provider and handle all of the logic and +operations for a particular Enterprise Cloud 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 := eclcloud.EndpointOpts{Region: "RegionOne"} + + client := ecl.NewComputeV2(provider, opts) + +Resources + +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 + }) + +If you want to obtain the entire collection of pages without doing any +intermediary processing on each page, you can use the AllPages method: + + allPages, err := servers.List(client, nil).AllPages() + allServers, err := servers.ExtractServers(allPages) + +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 eclcloud diff --git a/v4/ecl/auth_env.go b/v4/ecl/auth_env.go new file mode 100644 index 0000000..a6adb5a --- /dev/null +++ b/v4/ecl/auth_env.go @@ -0,0 +1,97 @@ +package ecl + +import ( + "github.com/nttcom/eclcloud/v4" + "os" +) + +var nilOptions = eclcloud.AuthOptions{} + +/* +AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +settings found on the various Enterprise Cloud 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, OS_TENANT_NAME, OS_PROJECT_ID, and +OS_PROJECT_NAME are optional. + +OS_TENANT_ID and OS_TENANT_NAME are mutually exclusive to OS_PROJECT_ID and +OS_PROJECT_NAME. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will +still be referred as "tenant" in eclcloud. + +To use this function, first set the OS_* environment variables (for example, +by sourcing an `openrc` file), then: + + opts, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(opts) +*/ +func AuthOptionsFromEnv() (eclcloud.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") + applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID") + applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME") + applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET") + + // If OS_PROJECT_ID is set, overwrite tenantID with the value. + if v := os.Getenv("OS_PROJECT_ID"); v != "" { + tenantID = v + } + + // If OS_PROJECT_NAME is set, overwrite tenantName with the value. + if v := os.Getenv("OS_PROJECT_NAME"); v != "" { + tenantName = v + } + + if authURL == "" { + err := eclcloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_AUTH_URL", + } + return nilOptions, err + } + + if username == "" && userID == "" { + err := eclcloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERNAME", "OS_USERID"}, + } + return nilOptions, err + } + + if password == "" && applicationCredentialID == "" && applicationCredentialName == "" { + err := eclcloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_PASSWORD", + } + return nilOptions, err + } + + if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" { + err := eclcloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET", + } + return nilOptions, err + } + + ao := eclcloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + ApplicationCredentialID: applicationCredentialID, + ApplicationCredentialName: applicationCredentialName, + ApplicationCredentialSecret: applicationCredentialSecret, + } + + return ao, nil +} diff --git a/v4/ecl/baremetal/v2/availabilityzones/doc.go b/v4/ecl/baremetal/v2/availabilityzones/doc.go new file mode 100644 index 0000000..6619157 --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/doc.go @@ -0,0 +1,22 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(client).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } +*/ +package availabilityzones diff --git a/v4/ecl/baremetal/v2/availabilityzones/requests.go b/v4/ecl/baremetal/v2/availabilityzones/requests.go new file mode 100644 index 0000000..08f8be9 --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/requests.go @@ -0,0 +1,13 @@ +package availabilityzones + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// List will return the existing availability zones. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/v4/ecl/baremetal/v2/availabilityzones/results.go b/v4/ecl/baremetal/v2/availabilityzones/results.go new file mode 100644 index 0000000..d603b19 --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/results.go @@ -0,0 +1,37 @@ +package availabilityzones + +import ( + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an ECL +// AvailabilityZone. +type AvailabilityZone struct { + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` + Hosts interface{} `json:"hosts"` +} + +// AvailabilityZonePage stores a single page of all AvailabilityZone results +// from a List call. +// Use the ExtractAvailabilityZones function to convert the results to a slice of +// AvailabilityZones. +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/v4/ecl/baremetal/v2/availabilityzones/testing/doc.go b/v4/ecl/baremetal/v2/availabilityzones/testing/doc.go new file mode 100644 index 0000000..59fc76c --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains baremetal availability zone unit tests +package testing diff --git a/v4/ecl/baremetal/v2/availabilityzones/testing/fixtures.go b/v4/ecl/baremetal/v2/availabilityzones/testing/fixtures.go new file mode 100644 index 0000000..d1594e0 --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/testing/fixtures.go @@ -0,0 +1,36 @@ +package testing + +import ( + az "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/availabilityzones" +) + +const getResponse = ` +{ + "availabilityZoneInfo": [{ + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupa" + }, { + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupb" + }] +} +` + +var azResult = []az.AvailabilityZone{ + { + Hosts: nil, + ZoneName: "zone1-groupa", + ZoneState: az.ZoneState{Available: true}, + }, + { + Hosts: nil, + ZoneName: "zone1-groupb", + ZoneState: az.ZoneState{Available: true}, + }, +} diff --git a/v4/ecl/baremetal/v2/availabilityzones/testing/requests_test.go b/v4/ecl/baremetal/v2/availabilityzones/testing/requests_test.go new file mode 100644 index 0000000..d5a1f11 --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/testing/requests_test.go @@ -0,0 +1,33 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + az "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/availabilityzones" + th "github.com/nttcom/eclcloud/v4/testhelper" + + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListAvailabilityZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + allPages, err := az.List(fakeclient.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := az.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, azResult, actual) +} diff --git a/v4/ecl/baremetal/v2/availabilityzones/urls.go b/v4/ecl/baremetal/v2/availabilityzones/urls.go new file mode 100644 index 0000000..a2cc8ba --- /dev/null +++ b/v4/ecl/baremetal/v2/availabilityzones/urls.go @@ -0,0 +1,7 @@ +package availabilityzones + +import "github.com/nttcom/eclcloud/v4" + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} diff --git a/v4/ecl/baremetal/v2/flavors/doc.go b/v4/ecl/baremetal/v2/flavors/doc.go new file mode 100644 index 0000000..23dbe2b --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/doc.go @@ -0,0 +1,25 @@ +/* +Package flavors contains functionality for working with +ECL Baremetal Server's flavor resources. + +Example to list flavors + + listOpts := flavors.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := flavors.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v", flavor) + } +*/ +package flavors diff --git a/v4/ecl/baremetal/v2/flavors/requests.go b/v4/ecl/baremetal/v2/flavors/requests.go new file mode 100644 index 0000000..63dfa09 --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/requests.go @@ -0,0 +1,88 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Get retrieves the flavor with the provided ID. +// To extract the Flavor object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// ListOpts holds options for listing flavors. +// It is passed to the flavors.List function. +type ListOpts struct { + // Now there are no definition as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Flavor optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.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 + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// IDFromName is a convenience function that returns a flavor's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &eclcloud.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &eclcloud.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/v4/ecl/baremetal/v2/flavors/results.go b/v4/ecl/baremetal/v2/flavors/results.go new file mode 100644 index 0000000..747a379 --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/results.go @@ -0,0 +1,79 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// Extract provides access to the individual Flavor returned by +// the Get and functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} diff --git a/v4/ecl/baremetal/v2/flavors/testing/doc.go b/v4/ecl/baremetal/v2/flavors/testing/doc.go new file mode 100644 index 0000000..159b3e8 --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains baremetal flavor unit tests +package testing diff --git a/v4/ecl/baremetal/v2/flavors/testing/fixtures.go b/v4/ecl/baremetal/v2/flavors/testing/fixtures.go new file mode 100644 index 0000000..0185036 --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/testing/fixtures.go @@ -0,0 +1,86 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/flavors" +) + +var listResponse = fmt.Sprintf(` +{ + "flavors": [ + { + "id": "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "name": "General Purpose 1", + "vcpus": 4, + "ram": 32768, + "disk": 550, + "links": [ + { + "href": "https://baremetal-server.ntt/v2/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "self" + }, + { + "href": "https://baremetal-server.ntt/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "bookmark" + } + ] + }, + { + "id": "303b4993-cf29-4301-abd0-99512b5413a5", + "name": "General Purpose 2", + "vcpus": 8, + "ram": 262144, + "disk": 3950, + "links": [ + { + "href": "https://baremetal-server.ntt/v2/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + "rel": "self" + }, + { + "href": "https://baremetal-server.ntt/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + "rel": "bookmark" + } + ] + } + ] +}`) + +var getResponse = fmt.Sprintf(` +{ + "flavor": { + "id": "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "links": [ + { + "href": "https://baremetal-server.ntt/v2/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "self" + }, + { + "href": "https://baremetal-server.ntt/1bc271e7a8af4d988ff91612f5b122f8/flavors/cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + "rel": "bookmark" + } + ], + "name": "General Purpose 1", + "vcpus": 4, + "ram": 32768, + "disk": 550 + } +}`) + +var expectedFlavors = []flavors.Flavor{expectedFlavor1, expectedFlavor2} + +var expectedFlavor1 = flavors.Flavor{ + ID: "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79", + Name: "General Purpose 1", + Disk: 550, + RAM: 32768, + VCPUs: 4, +} + +var expectedFlavor2 = flavors.Flavor{ + ID: "303b4993-cf29-4301-abd0-99512b5413a5", + Name: "General Purpose 2", + Disk: 3950, + RAM: 262144, + VCPUs: 8, +} diff --git a/v4/ecl/baremetal/v2/flavors/testing/requests_test.go b/v4/ecl/baremetal/v2/flavors/testing/requests_test.go new file mode 100644 index 0000000..1564760 --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/testing/requests_test.go @@ -0,0 +1,76 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/flavors" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := flavors.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := flavors.ExtractFlavors(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedFlavors, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListFlavorsAllPages(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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := flavors.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + + allFlavors, err := flavors.ExtractFlavors(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allFlavors)) +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/flavors/%s", "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := flavors.Get(fakeclient.ServiceClient(), "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedFlavor1, actual) +} diff --git a/v4/ecl/baremetal/v2/flavors/urls.go b/v4/ecl/baremetal/v2/flavors/urls.go new file mode 100644 index 0000000..18abce3 --- /dev/null +++ b/v4/ecl/baremetal/v2/flavors/urls.go @@ -0,0 +1,13 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} diff --git a/v4/ecl/baremetal/v2/keypairs/doc.go b/v4/ecl/baremetal/v2/keypairs/doc.go new file mode 100644 index 0000000..dd95992 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/doc.go @@ -0,0 +1,52 @@ +/* +Package keypairs provides the ability to manage key pairs. + +Example to List Key Pairs + + allPages, err := keypairs.List(client).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to Create a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + } + + keypair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", keypair) + +Example to Import a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + PublicKey: "public-key", + } + + keypair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair + + err := keypairs.Delete(client, "keypair-name").ExtractErr() + if err != nil { + panic(err) + } +*/ +package keypairs diff --git a/v4/ecl/baremetal/v2/keypairs/requests.go b/v4/ecl/baremetal/v2/keypairs/requests.go new file mode 100644 index 0000000..aa80718 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/requests.go @@ -0,0 +1,60 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies KeyPair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. + // If provided, this key will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new KeyPair on the server, or to import a +// pre-existing keypair. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *eclcloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), nil) + return +} diff --git a/v4/ecl/baremetal/v2/keypairs/results.go b/v4/ecl/baremetal/v2/keypairs/results.go new file mode 100644 index 0000000..9bd19f3 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/results.go @@ -0,0 +1,84 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// KeyPair is an SSH key known to the Enterprise Cloud 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 `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate + // or validate a longer public key. + Fingerprint string `json:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. + // "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"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 `json:"private_key"` + + // UserID is the user who owns this KeyPair. + UserID string `json:"user_id"` +} + +// KeyPairPage stores a single page of all KeyPair results from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + var s struct { + KeyPairs []KeyPair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + return s.KeyPairs, err +} + +type keyPairResult struct { + eclcloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response +// as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/baremetal/v2/keypairs/testing/doc.go b/v4/ecl/baremetal/v2/keypairs/testing/doc.go new file mode 100644 index 0000000..bf23f88 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains keypairs unit tests +package testing diff --git a/v4/ecl/baremetal/v2/keypairs/testing/fixtures.go b/v4/ecl/baremetal/v2/keypairs/testing/fixtures.go new file mode 100644 index 0000000..3ea7925 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/testing/fixtures.go @@ -0,0 +1,92 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/keypairs" +) + +const listOutput = ` +{ + "keypairs": [ + { + "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" + }, + { + "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" + } + ] +} +` + +const createRequest = `{ "keypair": { "name": "createdkey" } }` +const createResponse = ` +{ + "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" + } +} +` + +const getResponse = ` +{ + "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" + } +} +` + +const importRequest = ` +{ + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } +}` +const importResponse = ` +{ + "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" + } +} +` + +var firstKeyPair = keypairs.KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +var secondKeyPair = keypairs.KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +var expectedKeyPairSlice = []keypairs.KeyPair{firstKeyPair, secondKeyPair} + +var createdKeyPair = keypairs.KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + 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", +} + +var importedKeyPair = keypairs.KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} diff --git a/v4/ecl/baremetal/v2/keypairs/testing/requests_test.go b/v4/ecl/baremetal/v2/keypairs/testing/requests_test.go new file mode 100644 index 0000000..57e4f12 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/testing/requests_test.go @@ -0,0 +1,111 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/keypairs" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listOutput) + }) + + count := 0 + err := keypairs.List(fakeclient.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdKeyPair, actual) +} + +func TestImportKeypair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, importRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, importResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &importedKeyPair, actual) +} + +func TestGetKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := keypairs.Get(fakeclient.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &firstKeyPair, actual) +} + +func TestDeleteKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) + + err := keypairs.Delete(fakeclient.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/baremetal/v2/keypairs/urls.go b/v4/ecl/baremetal/v2/keypairs/urls.go new file mode 100644 index 0000000..7353b20 --- /dev/null +++ b/v4/ecl/baremetal/v2/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/nttcom/eclcloud/v4" + +const resourcePath = "os-keypairs" + +func resourceURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *eclcloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *eclcloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/v4/ecl/baremetal/v2/servers/doc.go b/v4/ecl/baremetal/v2/servers/doc.go new file mode 100644 index 0000000..0ab1220 --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/doc.go @@ -0,0 +1,120 @@ +/* +Package servers contains functionality for working with +ECL Baremetal Server resources. + +Example to create server + + createOpts := servers.CreateOpts{ + Name: "server-test-1", + Networks: []servers.CreateOptsNetwork{ + { + UUID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + FixedIP: "10.0.0.100", + }, + }, + AdminPass: "aabbccddeeff", + ImageRef: "b5660a6e-4b46-4be3-9707-6b47221b454f", + FlavorRef: "05184ba3-00ba-4fbc-b7a2-03b62b884931", + AvailabilityZone: "zone1-groupa", + UserData: "IyEvYmluL2Jhc2gKZWNobyAiS3VtYSBQb3N0IEluc3RhbGwgU2NyaXB0IiA+PiAvaG9tZS9iaWcvcG9zdC1pbnN0YWxsLXNjcmlwdA==", + RaidArrays: []servers.CreateOptsRaidArray{ + { + PrimaryStorage: true, + Partitions: []map[string]interface{}{ + { + "lvm": true, + "partition_label": "primary-part1", + }, + { + "lvm": false, + "size": "100G", + "partition_label": "var", + }, + }, + }, + { + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + "disk4_uuid", + }, + Partitions: []map[string]interface{}{ + { + "lvm": true, + "partition_label": "secondary-part1", + }, + }, + }, + }, + LVMVolumeGroups: []servers.CreateOptsLVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []map[string]string{ + { + "size": "300G", + "lv_label": "LV_root", + }, + { + "size": "2G", + "lv_label": "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.CreateOptsFilesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + } + server, err := servers.Create(client, createOpts).Extract() + +Example to list servers + + listOpts := servers.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := servers.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v", server) + } + +Example to delete server + + err = servers.Delete(client, "server-id"").ExtractErr() + if err != nil { + panic(err) + } + +*/ +package servers diff --git a/v4/ecl/baremetal/v2/servers/requests.go b/v4/ecl/baremetal/v2/servers/requests.go new file mode 100644 index 0000000..aebdac3 --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/requests.go @@ -0,0 +1,186 @@ +package servers + +import ( + "encoding/base64" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Get retrieves the server with the provided ID. +// To extract the Server object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts holds options for listing servers. +// It is passed to the servers.List function. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Server optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// CreateOptsNetwork represents networks information in server creation. +type CreateOptsNetwork struct { + UUID string `json:"uuid,omitempty"` + Port string `json:"port,omitempty"` + FixedIP string `json:"fixed_ip,omitempty"` + Plane string `json:"plane,omitempty"` +} + +// CreateOptsRaidArray represents raid configuration for the server resource. +type CreateOptsRaidArray struct { + PrimaryStorage bool `json:"primary_storage,omitempty"` + RaidCardHardwareID string `json:"raid_card_hardware_id,omitempty"` + DiskHardwareIDs []string `json:"disk_hardware_ids,omitempty"` + RaidLevel int `json:"raid_level,omitempty"` + Partitions []CreateOptsPartition `json:"partitions,omitempty"` +} + +// CreateOptsPartition represents partition configuration for the server resource. +type CreateOptsPartition struct { + LVM bool `json:"lvm,omitempty"` + Size string `json:"size,omitempty"` + PartitionLabel string `json:"partition_label,omitempty"` +} + +// CreateOptsLVMVolumeGroup represents LVM volume group configuration for the server resource. +type CreateOptsLVMVolumeGroup struct { + VGLabel string `json:"vg_label,omitempty"` + PhysicalVolumePartitionLabels []string `json:"physical_volume_partition_labels,omitempty"` + LogicalVolumes []CreateOptsLogicalVolume `json:"logical_volumes,omitempty"` +} + +// CreateOptsLogicalVolume represents logical volume configuration for the server resource. +type CreateOptsLogicalVolume struct { + LVLabel string `json:"lv_label,omitempty"` + Size string `json:"size,omitempty"` +} + +// CreateOptsFilesystem represents file system configuration for the server resource. +type CreateOptsFilesystem struct { + Label string `json:"label,omitempty"` + FSType string `json:"fs_type,omitempty"` + MountPoint string `json:"mount_point,omitempty"` +} + +// CreateOptsPersonality represents personal files configuration for the server resource. +type CreateOptsPersonality struct { + Path string `json:"path,omitempty"` + Contents string `json:"contents,omitempty"` +} + +// CreateOpts represents options used to create a server. +type CreateOpts struct { + Name string `json:"name" required:"true"` + Networks []CreateOptsNetwork `json:"networks" required:"true"` + AdminPass string `json:"adminPass,omitempty"` + ImageRef string `json:"imageRef,omitempty"` + FlavorRef string `json:"flavorRef" required:"true"` + AvailabilityZone string `json:"availability_zone,omitempty"` + KeyName string `json:"key_name,omitempty"` + UserData []byte `json:"-"` + RaidArrays []CreateOptsRaidArray `json:"raid_arrays,omitempty"` + LVMVolumeGroups []CreateOptsLVMVolumeGroup `json:"lvm_volume_groups,omitempty"` + Filesystems []CreateOptsFilesystem `json:"filesystems,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Personality []CreateOptsPersonality `json:"personality,omitempty"` +} + +// ToServerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + return map[string]interface{}{"server": b}, nil +} + +// Create accepts a CreateOpts struct and creates a new server +// using the values provided. +// This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/v4/ecl/baremetal/v2/servers/result.go b/v4/ecl/baremetal/v2/servers/result.go new file mode 100644 index 0000000..00f23d3 --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/result.go @@ -0,0 +1,208 @@ +package servers + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a Server. +type GetResult struct { + commonResult +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Extract provides access to the individual Server returned by +// the Get and functions. +func (r commonResult) Extract() (*Server, error) { + var s struct { + Server *Server `json:"server"` + } + err := r.ExtractInto(&s) + return s.Server, err +} + +// RaidArray represents raid configuration for the server resource. +type RaidArray struct { + PrimaryStorage bool `json:"primary_storage"` + RaidCardHardwareID string `json:"raid_card_hardware_id"` + DiskHardwareIDs []string `json:"disk_hardware_ids"` + RaidLevel int `json:"raid_level"` + Partitions []Partition `json:"partitions"` +} + +// Partition represents partition configuration for the server resource. +type Partition struct { + LVM bool `json:"lvm"` + Size int `json:"size"` + PartitionLabel string `json:"partition_label"` +} + +// LVMVolumeGroup represents LVM volume group configuration for the server resource. +type LVMVolumeGroup struct { + VGLabel string `json:"vg_label"` + PhysicalVolumePartitionLabels []string `json:"physical_volume_partition_labels"` + LogicalVolumes []LogicalVolume `json:"logical_volumes"` +} + +// LogicalVolume represents logical volume configuration for the server resource. +type LogicalVolume struct { + LVLabel string `json:"lv_label"` + Size int `json:"size"` +} + +// Filesystem represents file system configuration for the server resource. +type Filesystem struct { + Label string `json:"label"` + FSType string `json:"fs_type"` + MountPoint string `json:"mount_point"` +} + +// NICPhysicalPort represents port configuraion for the server resource. +type NICPhysicalPort struct { + ID string `json:"id"` + MacAddr string `json:"mac_addr"` + NetworkPhysicalPortID string `json:"network_physical_port_id"` + Plane string `json:"plane"` + AttachedPorts []AttachedPort `json:"attached_ports"` + HardwareID string `json:"hardware_id"` +} + +// AttachedPort represents attached port configuration for the server resource. +type AttachedPort struct { + PortID string `json:"port_id"` + NetworkID string `json:"network_id"` + FixedIPs []FixedIP `json:"fixed_ips"` +} + +// FixedIP represents fixed IP configuration for the server resource. +type FixedIP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +// ChassisStatus represents chassis status for the server resource +type ChassisStatus struct { + ChassisPower bool `json:"chassis-power"` + PowerSupply bool `json:"power-supply"` + CPU bool `json:"cpu"` + Memory bool `json:"memory"` + Fan bool `json:"fan"` + Disk int `json:"disk"` + NIC bool `json:"nic"` + SystemBoard bool `json:"system-board"` + Etc bool `json:"etc"` +} + +// Personality represents personal files configuration for the server resource. +type Personality struct { + Path string `json:"path"` + Contents string `json:"contents"` +} + +// Server represents hardware configurations for server resources +// in a region. +type Server struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Updated time.Time `json:"-"` + Created time.Time `json:"-"` + Status string `json:"status"` + AdminPass string `json:"adminPass"` + PowerState string `json:"OS-EXT-STS:power_state"` + TaskState string `json:"OS-EXT-STS:task_state"` + VMState string `json:"OS-EXT-STS:vm_state"` + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` + Progress int `json:"progress"` + Image map[string]interface{} `json:"image"` + Flavor map[string]interface{} `json:"flavor"` + Metadata map[string]string `json:"metadata"` + Links []eclcloud.Link `json:"links"` + RaidArrays []RaidArray `json:"raid_arrays"` + LVMVolumeGroups []LVMVolumeGroup `json:"lvm_volume_groups"` + Filesystems []Filesystem `json:"filesystems"` + NICPhysicalPorts []NICPhysicalPort `json:"nic_physical_ports"` + ChassisStatus ChassisStatus `json:"chassis-status"` + MediaAttachments []map[string]interface{} `json:"media_attachments"` + Personality []Personality `json:"personality"` +} + +// UnmarshalJSON to override default +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Created eclcloud.JSONRFC3339Milli `json:"created"` + Updated eclcloud.JSONRFC3339Milli `json:"updated"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Server(s.tmp) + + r.Created = time.Time(s.Created) + r.Updated = time.Time(s.Updated) + + return err +} + +// ServerPage contains a single page of all servers from a ListDetails call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page ServerPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"servers_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page ServerPage) IsEmpty() (bool, error) { + flavors, err := ExtractServers(page) + return len(flavors) == 0, err +} + +// ExtractServers provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s struct { + Servers []Server `json:"servers"` + } + err := (r.(ServerPage)).ExtractInto(&s) + return s.Servers, err +} diff --git a/v4/ecl/baremetal/v2/servers/testing/doc.go b/v4/ecl/baremetal/v2/servers/testing/doc.go new file mode 100644 index 0000000..d255096 --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains baremetal server unit tests +package testing diff --git a/v4/ecl/baremetal/v2/servers/testing/fixtures.go b/v4/ecl/baremetal/v2/servers/testing/fixtures.go new file mode 100644 index 0000000..e37a97e --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/testing/fixtures.go @@ -0,0 +1,898 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/servers" +) + +var listResponse = fmt.Sprintf(` +{ + "servers": [ + { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "zone1-groupa", + "created": "2012-09-07T16:56:37Z", + "flavor": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ] + }, + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "Test Server1", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-09-07T16:56:37Z", + "user_id": "fake", + "raid_arrays": [ + { + "primary_storage": true, + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "lvm": false, + "size": 100, + "partition_label": "var" + } + ] + }, + { + "primary_storage": false, + "raid_card_hardware_id": "raid_card_uuid", + "internal_disk_ids": [ + "disk4_uuid", + "disk5_uuid", + "disk6_uuid", + "disk7_uuid" + ], + "raid_level": 10, + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ] + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", + "secondary-part1" + ], + "logical_volumes": [ + { + "lv_label": "LV_root" + }, + { + "size": 2, + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "nic_physical_ports": [ + { + "id": "39285bf9-12fb-4064-b98b-a552efc51cfc", + "mac_addr": "0a:31:c1:d5:6d:9c", + "network_physical_port_id": "38268d94-584a-4f14-96ff-732a68aa7301", + "plane": "data", + "attached_ports": [ + { + "port_id": "61b7da1e-9571-4d63-b779-e003a56b8105", + "network_id": "9aa93722-1ec4-4912-b813-b975c21460a5", + "fixed_ips": [ + { + "subnet_id": "0419bbde-2b82-4107-9d8a-6bba76e364af", + "ip_address": "192.168.10.2" + } + ] + } + ], + "hardware_id": "063468e8-61ab-4afd-be38-c937254aeb9a" + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true + }, + "media_attachments": [], + "personality": [ + { + "path": "/home/big/banner.txt", + "contents": "ZWNobyAiS3VtYSBQZXJzb25hbGl0eSIgPj4gL2hvbWUvYmlnL3BlcnNvbmFsaXR5" + } + ] + }, + { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "zone1-groupa", + "created": "2012-09-07T16:56:37Z", + "flavor": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884932", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "16d193736a5cfdb60c697ca27ad071d6126fa13baeb670fc9d10645e", + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884932", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "Test Server2", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-09-07T16:56:37Z", + "user_id": "fake", + "raid_arrays": [ + { + "primary_storage": true, + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "lvm": false, + "size": 100, + "partition_label": "var" + } + ] + }, + { + "primary_storage": false, + "raid_card_hardware_id": "raid_card_uuid", + "internal_disk_ids": [ + "disk4_uuid", + "disk5_uuid", + "disk6_uuid", + "disk7_uuid" + ], + "raid_level": 10, + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ] + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", + "secondary-part1" + ], + "logical_volumes": [ + { + "lv_label": "LV_root" + }, + { + "size": 2, + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "nic_physical_ports": [ + { + "id": "f4732cd9-31f7-408e-9f27-cc9b0ee17457", + "mac_addr": "0a:31:c1:d5:6d:9d", + "network_physical_port_id": "ab17a82d-e9a5-4e95-9b18-de3f8a47670f", + "plane": "storage", + "attached_ports": [ + { + "port_id": "6fb0d979-f05b-466c-b50c-64d5ae4c4ef6", + "network_id": "99babdfc-79eb-470a-b0d4-df02482cc509", + "fixed_ips": [ + { + "subnet_id": "9632ce5d-8750-40bf-871d-968aa3324367", + "ip_address": "192.168.10.8" + } + ] + } + ], + "hardware_id": "ab36f541-b854-46c3-8891-e9484a1ba1ac" + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true + }, + "media_attachments": [ + { + "image": { + "id": "3339fd5f-ec06-4ef8-9337-c1c70218a748", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/3339fd5f-ec06-4ef8-9337-c1c70218a748", + "rel": "bookmark" + } + ] + } + } + ] + } + ] +}`) + +var getResponse = fmt.Sprintf(` +{ + "server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "zone1-groupa", + "created": "2012-09-07T16:56:37Z", + "flavor": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ] + }, + "hostId": "16d193736a5cfdb60c697ca27ad071d6126fa13baeb670fc9d10645e", + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "Test Server1", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-09-07T16:56:37Z", + "user_id": "fake", + "raid_arrays": [ + { + "primary_storage": true, + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "lvm": false, + "size": 100, + "partition_label": "var" + } + ] + }, + { + "primary_storage": false, + "raid_card_hardware_id": "raid_card_uuid", + "internal_disk_ids": [ + "disk4_uuid", + "disk5_uuid", + "disk6_uuid", + "disk7_uuid" + ], + "raid_level": 10, + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ] + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", + "secondary-part1" + ], + "logical_volumes": [ + { + "lv_label": "LV_root" + }, + { + "size": 2, + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "nic_physical_ports": [ + { + "id": "39285bf9-12fb-4064-b98b-a552efc51cfc", + "mac_addr": "0a:31:c1:d5:6d:9c", + "network_physical_port_id": "38268d94-584a-4f14-96ff-732a68aa7301", + "plane": "data", + "attached_ports": [ + { + "port_id": "61b7da1e-9571-4d63-b779-e003a56b8105", + "network_id": "9aa93722-1ec4-4912-b813-b975c21460a5", + "fixed_ips": [ + { + "subnet_id": "0419bbde-2b82-4107-9d8a-6bba76e364af", + "ip_address": "192.168.10.2" + } + ] + } + ], + "hardware_id": "063468e8-61ab-4afd-be38-c937254aeb9a" + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true + }, + "media_attachments": [ + { + "image": { + "id": "3339fd5f-ec06-4ef8-9337-c1c70218a748", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/3339fd5f-ec06-4ef8-9337-c1c70218a748", + "rel": "bookmark" + } + ] + } + } + ], + "personality": [ + { + "path": "/home/big/banner.txt", + "contents": "ZWNobyAiS3VtYSBQZXJzb25hbGl0eSIgPj4gL2hvbWUvYmlnL3BlcnNvbmFsaXR5" + } + ] + } +}`) + +var createRequest = fmt.Sprintf(` + { + "server": { + "name": "server-test-1", + "adminPass": "aabbccddeeff", + "imageRef": "b5660a6e-4b46-4be3-9707-6b47221b454f", + "flavorRef": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "availability_zone": "zone1-groupa", + "networks": [ + { + "uuid": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "fixed_ip": "10.0.0.100" + } + ], + "raid_arrays": [ + { + "primary_storage": true, + "partitions": [ + { + "lvm": true, + "partition_label": "primary-part1" + }, + { + "size": "100G", + "partition_label": "var" + } + ] + }, + { + "raid_card_hardware_id": "raid_card_uuid", + "disk_hardware_ids": [ + "disk1_uuid", "disk2_uuid", "disk3_uuid", "disk4_uuid" + ], + "partitions": [ + { + "lvm": true, + "partition_label": "secondary-part1" + } + ], + "raid_level": 10 + } + ], + "lvm_volume_groups": [ + { + "vg_label": "VG_root", + "physical_volume_partition_labels": [ + "primary-part1", "secondary-part1" + ], + "logical_volumes": [ + { + "size": "300G", + "lv_label": "LV_root" + }, + { + "size": "2G", + "lv_label": "LV_swap" + } + ] + } + ], + "filesystems": [ + { + "label": "LV_root", + "mount_point": "/", + "fs_type": "xfs" + }, + { + "label": "var", + "mount_point": "/var", + "fs_type": "xfs" + }, + { + "label": "LV_swap", + "fs_type": "swap" + } + ], + "user_data": "dXNlcl9kYXRh", + "metadata": { + "foo": "bar" + } + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "server": { + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark" + } + ], + "adminPass": "aabbccddeeff" + } +}`) + +var expectedServers = []servers.Server{expectedServer1, expectedServer2} + +var expectedCreated, _ = time.Parse(eclcloud.RFC3339Milli, "2012-09-07T16:56:37Z") +var expectedUpdated, _ = time.Parse(eclcloud.RFC3339Milli, "2012-09-07T16:56:37Z") + +var expectedServer1 = servers.Server{ + ID: "05184ba3-00ba-4fbc-b7a2-03b62b884931", + TenantID: "openstack", + UserID: "fake", + Name: "Test Server1", + Updated: expectedUpdated, + Created: expectedCreated, + Status: "ACTIVE", + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "zone1-groupa", + Progress: 0, + Image: map[string]interface{}{ + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884931", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/flavors/05184ba3-00ba-4fbc-b7a2-03b62b884931", + "rel": "bookmark", + }, + }, + }, + Metadata: map[string]string{ + "My Server Name": "Apache1", + }, + Links: []eclcloud.Link{ + { + Href: "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "self", + }, + { + Href: "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + }, + Partitions: []servers.Partition{ + { + LVM: true, + PartitionLabel: "primary-part1", + }, + { + LVM: false, + Size: 100, + PartitionLabel: "var", + }, + }, + }, + }, + LVMVolumeGroups: []servers.LVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []servers.LogicalVolume{ + { + LVLabel: "LV_root", + }, + { + Size: 2, + LVLabel: "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.Filesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + NICPhysicalPorts: []servers.NICPhysicalPort{ + { + ID: "39285bf9-12fb-4064-b98b-a552efc51cfc", + MacAddr: "0a:31:c1:d5:6d:9c", + NetworkPhysicalPortID: "38268d94-584a-4f14-96ff-732a68aa7301", + Plane: "data", + AttachedPorts: []servers.AttachedPort{ + { + PortID: "61b7da1e-9571-4d63-b779-e003a56b8105", + NetworkID: "9aa93722-1ec4-4912-b813-b975c21460a5", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "0419bbde-2b82-4107-9d8a-6bba76e364af", + IPAddress: "192.168.10.2", + }, + }, + }, + }, + HardwareID: "063468e8-61ab-4afd-be38-c937254aeb9a", + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + NIC: true, + SystemBoard: true, + Etc: true, + }, + MediaAttachments: []map[string]interface{}{}, + Personality: []servers.Personality{ + { + Path: "/home/big/banner.txt", + Contents: "ZWNobyAiS3VtYSBQZXJzb25hbGl0eSIgPj4gL2hvbWUvYmlnL3BlcnNvbmFsaXR5", + }, + }, +} + +var expectedServer2 = servers.Server{ + ID: "05184ba3-00ba-4fbc-b7a2-03b62b884932", + TenantID: "openstack", + UserID: "fake", + Name: "Test Server2", + Updated: expectedUpdated, + Created: expectedCreated, + Status: "ACTIVE", + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "zone1-groupa", + Progress: 0, + Image: map[string]interface{}{ + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "05184ba3-00ba-4fbc-b7a2-03b62b884932", + "links": []map[string]interface{}{ + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark", + }, + }, + }, + Metadata: map[string]string{ + "My Server Name": "Apache1", + }, + Links: []eclcloud.Link{ + { + Href: "http://openstack.example.com/v2/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "self", + }, + { + Href: "http://openstack.example.com/openstack/servers/05184ba3-00ba-4fbc-b7a2-03b62b884931", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk0_uuid", + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + }, + Partitions: []servers.Partition{ + { + LVM: true, + PartitionLabel: "primary-part1", + }, + { + LVM: false, + Size: 100, + PartitionLabel: "var", + }, + }, + }, + }, + LVMVolumeGroups: []servers.LVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []servers.LogicalVolume{ + { + LVLabel: "LV_root", + }, + { + Size: 2, + LVLabel: "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.Filesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + NICPhysicalPorts: []servers.NICPhysicalPort{ + { + ID: "f4732cd9-31f7-408e-9f27-cc9b0ee17457", + MacAddr: "0a:31:c1:d5:6d:9d", + NetworkPhysicalPortID: "ab17a82d-e9a5-4e95-9b18-de3f8a47670f", + Plane: "storage", + AttachedPorts: []servers.AttachedPort{ + { + PortID: "6fb0d979-f05b-466c-b50c-64d5ae4c4ef6", + NetworkID: "99babdfc-79eb-470a-b0d4-df02482cc509", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "9632ce5d-8750-40bf-871d-968aa3324367", + IPAddress: "192.168.10.8", + }, + }, + }, + }, + HardwareID: "ab36f541-b854-46c3-8891-e9484a1ba1ac", + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + NIC: true, + SystemBoard: true, + Etc: true, + }, + MediaAttachments: []map[string]interface{}{}, + Personality: []servers.Personality(nil), +} diff --git a/v4/ecl/baremetal/v2/servers/testing/requests_test.go b/v4/ecl/baremetal/v2/servers/testing/requests_test.go new file mode 100644 index 0000000..72b3638 --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/testing/requests_test.go @@ -0,0 +1,176 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/baremetal/v2/servers" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := servers.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + fmt.Printf("person[%%#v] -> %#v\n", actual) + th.CheckDeepEquals(t, expectedServers, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s", "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := servers.Get(fakeclient.ServiceClient(), "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedServer1, actual) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, createResponse) + }) + + createOpts := servers.CreateOpts{ + Name: "server-test-1", + Networks: []servers.CreateOptsNetwork{ + { + UUID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + FixedIP: "10.0.0.100", + }, + }, + AdminPass: "aabbccddeeff", + ImageRef: "b5660a6e-4b46-4be3-9707-6b47221b454f", + FlavorRef: "05184ba3-00ba-4fbc-b7a2-03b62b884931", + AvailabilityZone: "zone1-groupa", + UserData: []byte("user_data"), + RaidArrays: []servers.CreateOptsRaidArray{ + { + PrimaryStorage: true, + Partitions: []servers.CreateOptsPartition{ + { + LVM: true, + PartitionLabel: "primary-part1", + }, + { + Size: "100G", + PartitionLabel: "var", + }, + }, + }, + { + RaidCardHardwareID: "raid_card_uuid", + DiskHardwareIDs: []string{ + "disk1_uuid", + "disk2_uuid", + "disk3_uuid", + "disk4_uuid", + }, + Partitions: []servers.CreateOptsPartition{ + { + LVM: true, + PartitionLabel: "secondary-part1", + }, + }, + RaidLevel: 10, + }, + }, + LVMVolumeGroups: []servers.CreateOptsLVMVolumeGroup{ + { + VGLabel: "VG_root", + PhysicalVolumePartitionLabels: []string{ + "primary-part1", + "secondary-part1", + }, + LogicalVolumes: []servers.CreateOptsLogicalVolume{ + { + Size: "300G", + LVLabel: "LV_root", + }, + { + Size: "2G", + LVLabel: "LV_swap", + }, + }, + }, + }, + Filesystems: []servers.CreateOptsFilesystem{ + { + Label: "LV_root", + FSType: "xfs", + MountPoint: "/", + }, + { + Label: "var", + FSType: "xfs", + MountPoint: "/var", + }, + { + Label: "LV_swap", + FSType: "swap", + }, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + } + server, err := servers.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, server.AdminPass, "aabbccddeeff") +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s", "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := servers.Delete(fakeclient.ServiceClient(), "cebf8bb5-74cf-4a53-bca5-b90d4bbe8d79") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/baremetal/v2/servers/urls.go b/v4/ecl/baremetal/v2/servers/urls.go new file mode 100644 index 0000000..662800b --- /dev/null +++ b/v4/ecl/baremetal/v2/servers/urls.go @@ -0,0 +1,21 @@ +package servers + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} diff --git a/v4/ecl/client.go b/v4/ecl/client.go new file mode 100644 index 0000000..d2b2250 --- /dev/null +++ b/v4/ecl/client.go @@ -0,0 +1,390 @@ +package ecl + +import ( + "fmt" + "reflect" + + "github.com/nttcom/eclcloud/v4" + tokens3 "github.com/nttcom/eclcloud/v4/ecl/identity/v3/tokens" + "github.com/nttcom/eclcloud/v4/ecl/utils" +) + +const ( + // v3 represents Keystone v3. + // The version can be anything from v3 to v3.x. + v3 = "v3" +) + +/* +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. + +A basic example of using this would be: + + ao, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.NewClient(ao.IdentityEndpoint) + client, err := ecl.NewIdentityV3(provider, eclcloud.EndpointOpts{}) +*/ +func NewClient(endpoint string) (*eclcloud.ProviderClient, error) { + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + endpoint = eclcloud.NormalizeURL(endpoint) + base = eclcloud.NormalizeURL(base) + + p := new(eclcloud.ProviderClient) + p.IdentityBase = base + p.IdentityEndpoint = endpoint + p.UseTokenLock() + + return p, nil +} + +/* +AuthenticatedClient logs in to an Enterprise Cloud found at the identity endpoint +specified by the options, acquires a token, and returns a Provider Client +instance that's ready to operate. + +If the full path to a versioned identity endpoint was specified (example: +http://example.com:5000/v3), that path will be used as the endpoint to query. + +If a versionless endpoint was specified (example: http://example.com:5000/), +the endpoint will be queried to determine which versions of the identity service +are available, then chooses the most recent or most supported version. + +Example: + + ao, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(ao) + client, err := ecl.NewNetworkV2(client, eclcloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +func AuthenticatedClient(options eclcloud.AuthOptions) (*eclcloud.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 *eclcloud.ProviderClient, options eclcloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v3, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v3: + return v3auth(client, endpoint, &options, eclcloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *eclcloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo eclcloud.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +func v3auth(client *eclcloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo eclcloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + result := tokens3.Create(v3Client, opts) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + + if opts.CanReauth() { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.ReauthFunc = nil + tac.TokenID = "" + var tao tokens3.AuthOptionsBuilder + switch ot := opts.(type) { + case *eclcloud.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *tokens3.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + default: + tao = opts + } + client.ReauthFunc = func() error { + err := v3auth(&tac, endpoint, tao, eo) + if err != nil { + return err + } + client.TokenID = tac.TokenID + return nil + } + } + client.EndpointLocator = func(opts eclcloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 +// identity service. +func NewIdentityV3(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, eclcloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + // Ensure endpoint still has a suffix of v3. + // This is because EndpointLocator might have found a versionless + // endpoint or the published endpoint is still /v2.0. In both + // cases, we need to fix the endpoint to point to /v3. + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + base = eclcloud.NormalizeURL(base) + + endpoint = base + "v3/" + + return &eclcloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +func initClientOpts(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts, clientType string) (*eclcloud.ServiceClient, error) { + sc := new(eclcloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +func initSSSClientOptsForced(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts, clientType string, sssURL string) (*eclcloud.ServiceClient, error) { + sc := new(eclcloud.ServiceClient) + eo.ApplyDefaults(clientType) + url := sssURL + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 +// object storage package. +func NewObjectStorageV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "object-store") +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute +// package. +func NewComputeV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "compute") +} + +// NewBaremetalV2 creates a ServiceClient that may be used with the v2 baremetal +// package. +func NewBaremetalV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal-server") +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network +// package. +func NewNetworkV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewComputeVolumeV2 creates a ServiceClient that may be used to access the v2 +// block storage service. +func NewComputeVolumeV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev2") +} + +// NewSSSV2 creates ServiceClient that may be used to access the v2 +// SSS API service. +func NewSSSV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "sssv2") +} + +// NewSSSV2 creates ServiceClient that may be used to access the v2 +// SSS API service with Unscoped Token. +func NewSSSV2Forced(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts, sssURL string) (*eclcloud.ServiceClient, error) { + return initSSSClientOptsForced(client, eo, "sssv2", sssURL) +} + +// NewStorageV1 creates ServiceClient that may be used to access the v1 +// storage API service. +func NewStorageV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "storage") +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 +// orchestration service. +func NewOrchestrationV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration") +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS +// service. +func NewDNSV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 +// image service. +func NewImageServiceV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewVNAV1 creates a ServiceClient that may be used with the v1 virtual network appliance management package. +func NewVNAV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "virtual-network-appliance") + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc, err +} + +// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2 +// load balancer service. +func NewLoadBalancerV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "load-balancer") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewManagedLoadBalancerV1 creates a ServiceClient that may be used to access the v1 +// managed load balancer service. +func NewManagedLoadBalancerV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "managed-load-balancer") + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc, err +} + +// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering +// package. +func NewClusteringV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "clustering") +} + +// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging +// service. +func NewMessagingV2(client *eclcloud.ProviderClient, clientID string, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "messaging") + sc.MoreHeaders = map[string]string{"Client-ID": clientID} + return sc, err +} + +// NewContainerV1 creates a ServiceClient that may be used with v1 container package +func NewContainerV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "container") +} + +// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key +// manager service. +func NewKeyManagerV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "key-manager") + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management +// package. +func NewContainerInfraV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "container-infra") +} + +// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. +func NewWorkflowV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "workflowv2") +} + +// NewSecurityOrderV3 creates a ServiceClient that may be used to access the v3 Security +// Order API service. +func NewSecurityOrderV3(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "security-order-th") + // sc.ResourceBase = sc.Endpoint + "v3/" + return sc, err +} + +// NewSecurityPortalV3 creates a ServiceClient that may be used to access the v3 Security +// Portal API service. +func NewSecurityPortalV3(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "security-operation-th") + // sc.ResourceBase = sc.Endpoint + "v3/" + return sc, err +} + +// NewDedicatedHypervisorV1 creates a ServiceClient that may be used to access the v1 Dedicated Hypervisor service. +func NewDedicatedHypervisorV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "dedicated-hypervisor") +} + +// NewRCAV1 creates a ServiceClient that may be used to access the v1 Remote Console Access service. +func NewRCAV1(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + return initClientOpts(client, eo, "rca") +} + +// NewProviderConnectivityV2 creates a ServiceClient that may be used to access the v2 Provider Connectivity service. +func NewProviderConnectivityV2(client *eclcloud.ProviderClient, eo eclcloud.EndpointOpts) (*eclcloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "provider-connectivity") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/doc.go b/v4/ecl/compute/v2/extensions/availabilityzones/doc.go new file mode 100644 index 0000000..6fbc22f --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/doc.go @@ -0,0 +1,46 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Extend server result with Availability Zone Information: + + type ServerWithAZ struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + } + + var allServers []ServerWithAZ + + allPages, err := servers.List(client, nil).AllPages() + if err != nil { + panic("Unable to retrieve servers: %s", err) + } + + err = servers.ExtractServersInto(allPages, &allServers) + if err != nil { + panic("Unable to extract servers: %s", err) + } + + for _, server := range allServers { + fmt.Println(server.AvailabilityZone) + } + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } + +*/ +package availabilityzones diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/requests.go b/v4/ecl/compute/v2/extensions/availabilityzones/requests.go new file mode 100644 index 0000000..08f8be9 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/requests.go @@ -0,0 +1,13 @@ +package availabilityzones + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// List will return the existing availability zones. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/results.go b/v4/ecl/compute/v2/extensions/availabilityzones/results.go new file mode 100644 index 0000000..8fbfe61 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/results.go @@ -0,0 +1,80 @@ +package availabilityzones + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ServerAvailabilityZoneExt is an extension to the base Server object. +type ServerAvailabilityZoneExt struct { + // AvailabilityZone is the availability zone the server is in. + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` +} + +// ServiceState represents the state of a service in an AvailabilityZone. +type ServiceState struct { + Active bool `json:"active"` + Available bool `json:"available"` + UpdatedAt time.Time `json:"-"` +} + +// UnmarshalJSON to override default +func (r *ServiceState) UnmarshalJSON(b []byte) error { + type tmp ServiceState + var s struct { + tmp + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ServiceState(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +// Services is a map of services contained in an AvailabilityZone. +type Services map[string]ServiceState + +// Hosts is map of hosts/nodes contained in an AvailabilityZone. +// Each host can have multiple services. +type Hosts map[string]Services + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an ECL +// AvailabilityZone. +type AvailabilityZone struct { + Hosts Hosts `json:"hosts"` + // The availability zone name + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` +} + +// AvailabilityZonePage stores a single page of all AvailabilityZone results +// from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/testing/doc.go b/v4/ecl/compute/v2/extensions/availabilityzones/testing/doc.go new file mode 100644 index 0000000..a4408d7 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/testing/doc.go @@ -0,0 +1,2 @@ +// availabilityzones unittests +package testing diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/testing/fixtures.go b/v4/ecl/compute/v2/extensions/availabilityzones/testing/fixtures.go new file mode 100644 index 0000000..02b4d1f --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/testing/fixtures.go @@ -0,0 +1,31 @@ +package testing + +import ( + az "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/availabilityzones" +) + +const getResponse = ` +{ + "availabilityZoneInfo": [{ + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupa" + }, { + "zoneState": { + "available": true + }, + "hosts": null, + "zoneName": "zone1-groupb" + }] +} +` + +var azResult = []az.AvailabilityZone{ + { + Hosts: nil, + ZoneName: "zone1-groupa", + ZoneState: az.ZoneState{Available: true}, + }, +} diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/testing/requests_test.go b/v4/ecl/compute/v2/extensions/availabilityzones/testing/requests_test.go new file mode 100644 index 0000000..7d82395 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/testing/requests_test.go @@ -0,0 +1,33 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + az "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/availabilityzones" + th "github.com/nttcom/eclcloud/v4/testhelper" + + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListAvailabilityZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + allPages, err := az.List(fakeclient.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := az.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, azResult, actual) +} diff --git a/v4/ecl/compute/v2/extensions/availabilityzones/urls.go b/v4/ecl/compute/v2/extensions/availabilityzones/urls.go new file mode 100644 index 0000000..a2cc8ba --- /dev/null +++ b/v4/ecl/compute/v2/extensions/availabilityzones/urls.go @@ -0,0 +1,7 @@ +package availabilityzones + +import "github.com/nttcom/eclcloud/v4" + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/doc.go b/v4/ecl/compute/v2/extensions/bootfromvolume/doc.go new file mode 100644 index 0000000..3f16cda --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/doc.go @@ -0,0 +1,145 @@ +/* +Package bootfromvolume extends a server create request with the ability to +specify block device options. This can be used to boot a server from a block +storage volume as well as specify multiple ephemeral disks upon creation. + +Example of Creating a Server From an Image + +This example will boot a server from an image and use a standard ephemeral +disk as the server's root disk. This is virtually no different than creating +a server without using block device mappings. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example of Creating a Server From a New Volume + +This example will create a block storage volume based on the given Image. The +server will use this volume as its root disk. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + VolumeSize: 2, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example of Creating a Server From an Existing Volume + +This example will create a server with an existing volume as its root disk. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: "volume-uuid", + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example of Creating a Server with Multiple Ephemeral Disks + +This example will create a server with multiple ephemeral disks. The first +block device will be based off of an existing Image. Each additional +ephemeral disks must have an index of -1. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + VolumeSize: 5, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package bootfromvolume diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/requests.go b/v4/ecl/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 0000000..9d500ce --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,129 @@ +package bootfromvolume + +import ( + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/servers" + + "github.com/nttcom/eclcloud/v4" +) + +type ( + // DestinationType represents the type of medium being used as the + // destination of the bootable device. + DestinationType string + + // SourceType represents the type of medium being used as the source of the + // bootable device. + SourceType string +) + +const ( + // DestinationLocal DestinationType is for using an ephemeral disk as the + // destination. + DestinationLocal DestinationType = "local" + + // DestinationVolume DestinationType is for using a volume as the destination. + DestinationVolume DestinationType = "volume" + + // SourceBlank SourceType is for a "blank" or empty source. + SourceBlank SourceType = "blank" + + // SourceImage SourceType is for using images as the source of a block device. + SourceImage SourceType = "image" + + // SourceSnapshot SourceType is for using a volume snapshot as the source of + // a block device. + SourceSnapshot SourceType = "snapshot" + + // SourceVolume SourceType is for using a volume as the source of block + // device. + SourceVolume SourceType = "volume" +) + +// BlockDevice is a structure with options for creating block devices in a +// server. The block device may be created from an image, snapshot, new volume, +// or existing volume. The destination may be a new volume, existing volume +// which will be attached to the instance, ephemeral disk, or boot device. +type BlockDevice struct { + // SourceType must be one of: "volume", "snapshot", "image", or "blank". + SourceType SourceType `json:"source_type" required:"true"` + + // UUID is the unique identifier for the existing volume, snapshot, or + // image (see above). + UUID string `json:"uuid,omitempty"` + + // BootIndex is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType is the type that gets created. Possible values are "volume" + // and "local". + DestinationType DestinationType `json:"destination_type,omitempty"` + + // GuestFormat specifies the format of the block device. + GuestFormat string `json:"guest_format,omitempty"` + + // VolumeSize is the size of the volume to create (in gigabytes). This can be + // omitted for existing volumes. + VolumeSize int `json:"volume_size,omitempty"` + + // DeviceType specifies the device type of the block devices. + // Examples of this are disk, cdrom, floppy, lun, etc. + DeviceType string `json:"device_type,omitempty"` + + // DiskBus is the bus type of the block devices. + // Examples of this are ide, usb, virtio, scsi, etc. + DiskBus string `json:"disk_bus,omitempty"` +} + +// 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 { + err := eclcloud.ErrMissingInput{} + err.Argument = "bootfromvolume.CreateOptsExt.BlockDevice" + return nil, err + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + b, err := eclcloud.BuildRequestBody(bd, "") + if err != nil { + return nil, err + } + blockDevice[i] = b + } + 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 *eclcloud.ServiceClient, opts servers.CreateOptsBuilder) (r servers.CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/results.go b/v4/ecl/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 0000000..37bf6f4 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,12 @@ +package bootfromvolume + +import ( + os "github.com/nttcom/eclcloud/v4/ecl/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +// It embeds the standard servers.CreateResults type and so can be used the +// same way as a standard server request result. +type CreateResult struct { + os.CreateResult +} diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/testing/doc.go b/v4/ecl/compute/v2/extensions/bootfromvolume/testing/doc.go new file mode 100644 index 0000000..2b6699f --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains bootfromvolume unit tests +package testing diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/testing/fixtures.go b/v4/ecl/compute/v2/extensions/bootfromvolume/testing/fixtures.go new file mode 100644 index 0000000..aac20df --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/testing/fixtures.go @@ -0,0 +1,275 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/bootfromvolume" + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/servers" +) + +var baseCreateOpts = servers.CreateOpts{ + Name: "createdserver", + FlavorRef: "performance1-1", +} + +var baseCreateOptsWithImageRef = servers.CreateOpts{ + Name: "createdserver", + FlavorRef: "performance1-1", + ImageRef: "asdfasdfasdf", +} + +const expectedNewVolumeRequest = ` +{ + "server": { + "name":"createdserver", + "flavorRef":"performance1-1", + "imageRef":"", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": 0, + "delete_on_termination": true, + "volume_size": 10 + } + ] + } +} +` + +var newVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOpts, + BlockDevice: []bootfromvolume.BlockDevice{ + { + UUID: "123456", + SourceType: bootfromvolume.SourceImage, + DestinationType: bootfromvolume.DestinationVolume, + VolumeSize: 10, + DeleteOnTermination: true, + }, + }, +} + +const expectedExistingVolumeRequest = ` +{ + "server": { + "name":"createdserver", + "flavorRef":"performance1-1", + "imageRef":"", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"volume", + "destination_type":"volume", + "boot_index": 0, + "delete_on_termination": true + } + ] + } +} +` + +var existingVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOpts, + BlockDevice: []bootfromvolume.BlockDevice{ + { + UUID: "123456", + SourceType: bootfromvolume.SourceVolume, + DestinationType: bootfromvolume.DestinationVolume, + DeleteOnTermination: true, + }, + }, +} + +const expectedImageRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + } + ] + } +} +` + +var imageRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + }, +} + +const expectedMultiEphemeralRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": -1, + "delete_on_termination": true, + "destination_type":"local", + "guest_format":"ext4", + "source_type":"blank", + "volume_size": 1 + }, + { + "boot_index": -1, + "delete_on_termination": true, + "destination_type":"local", + "guest_format":"ext4", + "source_type":"blank", + "volume_size": 1 + } + ] + } +} +` + +var multiEphemeralRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: -1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + { + BootIndex: -1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + }, +} + +const expectedImageAndNewVolumeRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": 1, + "delete_on_termination": true, + "destination_type":"volume", + "source_type":"blank", + "volume_size": 1, + "device_type": "disk", + "disk_bus": "scsi" + } + ] + } +} +` + +var imageAndNewVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + DeviceType: "disk", + DiskBus: "scsi", + }, + }, +} + +const expectedImageAndExistingVolumeRequest = ` +{ + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": 1, + "delete_on_termination": true, + "destination_type":"volume", + "source_type":"volume", + "uuid":"123456", + "volume_size": 1 + } + ] + } +} +` + +var imageAndExistingVolumeRequest = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: baseCreateOptsWithImageRef, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: "123456", + VolumeSize: 1, + }, + }, +} diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/testing/requests_test.go b/v4/ecl/compute/v2/extensions/bootfromvolume/testing/requests_test.go new file mode 100644 index 0000000..efeeff6 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/testing/requests_test.go @@ -0,0 +1,44 @@ +package testing + +import ( + "testing" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestBootFromNewVolume(t *testing.T) { + + actual, err := newVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedNewVolumeRequest, actual) +} + +func TestBootFromExistingVolume(t *testing.T) { + actual, err := existingVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedExistingVolumeRequest, actual) +} + +func TestBootFromImage(t *testing.T) { + actual, err := imageRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedImageRequest, actual) +} + +func TestCreateMultiEphemeralOpts(t *testing.T) { + actual, err := multiEphemeralRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedMultiEphemeralRequest, actual) +} + +func TestAttachNewVolume(t *testing.T) { + actual, err := imageAndNewVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedImageAndNewVolumeRequest, actual) +} + +func TestAttachExistingVolume(t *testing.T) { + actual, err := imageAndExistingVolumeRequest.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expectedImageAndExistingVolumeRequest, actual) +} diff --git a/v4/ecl/compute/v2/extensions/bootfromvolume/urls.go b/v4/ecl/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 0000000..0160bc4 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/nttcom/eclcloud/v4" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("os-volumes_boot") +} diff --git a/v4/ecl/compute/v2/extensions/keypairs/doc.go b/v4/ecl/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 0000000..24c4607 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,71 @@ +/* +Package keypairs provides the ability to manage key pairs as well as create +servers with a specified key pair. + +Example to List Key Pairs + + allPages, err := keypairs.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to Create a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", keypair) + +Example to Import a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + PublicKey: "public-key", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair + + err := keypairs.Delete(computeClient, "keypair-name").ExtractErr() + if err != nil { + panic(err) + } + +Example to Create a Server With a Key Pair + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := keypairs.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + KeyName: "keypair-name", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package keypairs diff --git a/v4/ecl/compute/v2/extensions/keypairs/requests.go b/v4/ecl/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 0000000..c5b45f1 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,86 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/servers" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // KeyName is the name of the key pair. + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if opts.KeyName == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyName + + return base, nil +} + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *eclcloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies KeyPair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. + // If provided, this key will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new KeyPair on the server, or to import a +// pre-existing keypair. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *eclcloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), nil) + return +} diff --git a/v4/ecl/compute/v2/extensions/keypairs/results.go b/v4/ecl/compute/v2/extensions/keypairs/results.go new file mode 100644 index 0000000..b8708a4 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,91 @@ +package keypairs + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// KeyPair is an SSH key known to the Enterprise Cloud 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 `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate + // or validate a longer public key. + Fingerprint string `json:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. + // "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"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 `json:"private_key"` + + // UserID is the user who owns this KeyPair. + UserID string `json:"user_id"` +} + +// KeyPairPage stores a single page of all KeyPair results from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `json:"keypair"` + } + var s struct { + KeyPairs []pair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + results := make([]KeyPair, len(s.KeyPairs)) + for i, pair := range s.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + eclcloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response +// as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/compute/v2/extensions/keypairs/testing/doc.go b/v4/ecl/compute/v2/extensions/keypairs/testing/doc.go new file mode 100644 index 0000000..bf23f88 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains keypairs unit tests +package testing diff --git a/v4/ecl/compute/v2/extensions/keypairs/testing/fixtures.go b/v4/ecl/compute/v2/extensions/keypairs/testing/fixtures.go new file mode 100644 index 0000000..3706caa --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/testing/fixtures.go @@ -0,0 +1,96 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/keypairs" +) + +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" + } + } + ] +} +` + +const createRequest = `{ "keypair": { "name": "createdkey" } }` +const createResponse = ` +{ + "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" + } +} +` + +const getResponse = ` +{ + "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" + } +} +` + +const importRequest = ` +{ + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } +}` +const importResponse = ` +{ + "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" + } +} +` + +var firstKeyPair = keypairs.KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +var secondKeyPair = keypairs.KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +var expectedKeyPairSlice = []keypairs.KeyPair{firstKeyPair, secondKeyPair} + +var createdKeyPair = keypairs.KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + 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", +} + +var importedKeyPair = keypairs.KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} diff --git a/v4/ecl/compute/v2/extensions/keypairs/testing/requests_test.go b/v4/ecl/compute/v2/extensions/keypairs/testing/requests_test.go new file mode 100644 index 0000000..58d860f --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/testing/requests_test.go @@ -0,0 +1,111 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/keypairs" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listOutput) + }) + + count := 0 + err := keypairs.List(fakeclient.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdKeyPair, actual) +} + +func TestImportKeypair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, importRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, importResponse) + }) + + actual, err := keypairs.Create(fakeclient.ServiceClient(), keypairs.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &importedKeyPair, actual) +} + +func TestGetKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := keypairs.Get(fakeclient.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &firstKeyPair, actual) +} + +func TestDeleteKeyPair(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) + + err := keypairs.Delete(fakeclient.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/compute/v2/extensions/keypairs/urls.go b/v4/ecl/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 0000000..7353b20 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/nttcom/eclcloud/v4" + +const resourcePath = "os-keypairs" + +func resourceURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *eclcloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *eclcloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/v4/ecl/compute/v2/extensions/startstop/doc.go b/v4/ecl/compute/v2/extensions/startstop/doc.go new file mode 100644 index 0000000..65c4c5a --- /dev/null +++ b/v4/ecl/compute/v2/extensions/startstop/doc.go @@ -0,0 +1,19 @@ +/* +Package startstop provides functionality to start and stop servers that have +been provisioned by the Enterprise Cloud Compute service. + +Example to Stop and Start a Server + + serverID := "47b6b7b7-568d-40e4-868c-d5c41735532e" + + err := startstop.Stop(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + + err := startstop.Start(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package startstop diff --git a/v4/ecl/compute/v2/extensions/startstop/requests.go b/v4/ecl/compute/v2/extensions/startstop/requests.go new file mode 100644 index 0000000..7e11e07 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,19 @@ +package startstop + +import "github.com/nttcom/eclcloud/v4" + +func actionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Start is the operation responsible for starting a Compute server. +func Start(client *eclcloud.ServiceClient, id string) (r StartResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil) + return +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(client *eclcloud.ServiceClient, id string) (r StopResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil) + return +} diff --git a/v4/ecl/compute/v2/extensions/startstop/results.go b/v4/ecl/compute/v2/extensions/startstop/results.go new file mode 100644 index 0000000..7135023 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/startstop/results.go @@ -0,0 +1,15 @@ +package startstop + +import "github.com/nttcom/eclcloud/v4" + +// StartResult is the response from a Start operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StartResult struct { + eclcloud.ErrResult +} + +// StopResult is the response from Stop operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StopResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/compute/v2/extensions/startstop/testing/doc.go b/v4/ecl/compute/v2/extensions/startstop/testing/doc.go new file mode 100644 index 0000000..ee45964 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/startstop/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains startstop unit tests +package testing diff --git a/v4/ecl/compute/v2/extensions/startstop/testing/requests_test.go b/v4/ecl/compute/v2/extensions/startstop/testing/requests_test.go new file mode 100644 index 0000000..14acbed --- /dev/null +++ b/v4/ecl/compute/v2/extensions/startstop/testing/requests_test.go @@ -0,0 +1,44 @@ +package testing + +import ( + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/startstop" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +const serverID = "645b787e-7fbb-4111-a217-63a2882930f2" + +func TestServerStart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/servers/" + serverID + "/action" + th.Mux.HandleFunc(url, 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) + }) + + err := startstop.Start(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestServerStop(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/servers/" + serverID + "/action" + th.Mux.HandleFunc(url, 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) + }) + + err := startstop.Stop(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/compute/v2/extensions/volumeattach/doc.go b/v4/ecl/compute/v2/extensions/volumeattach/doc.go new file mode 100644 index 0000000..484eb20 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/doc.go @@ -0,0 +1,30 @@ +/* +Package volumeattach provides the ability to attach and detach volumes +from servers. + +Example to Attach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + volumeID := "87463836-f0e2-4029-abf6-20c8892a3103" + + createOpts := volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: volumeID, + } + + result, err := volumeattach.Create(computeClient, serverID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Detach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + attachmentID := "ed081613-1c9b-4231-aa5e-ebfd4d87f983" + + err := volumeattach.Delete(computeClient, serverID, attachmentID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeattach diff --git a/v4/ecl/compute/v2/extensions/volumeattach/requests.go b/v4/ecl/compute/v2/extensions/volumeattach/requests.go new file mode 100644 index 0000000..0b0a9b2 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/requests.go @@ -0,0 +1,60 @@ +package volumeattach + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of +// VolumeAttachments. +func List(client *eclcloud.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return VolumeAttachmentPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + ToVolumeAttachmentCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies volume attachment creation or import parameters. +type CreateOpts struct { + // Device is the device that the volume will attach to the instance as. + // Omit for "auto". + Device string `json:"device,omitempty"` + + // VolumeID is the ID of the volume to attach to the instance. + VolumeID string `json:"volumeId" required:"true"` +} + +// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volumeAttachment") +} + +// Create requests the creation of a new volume attachment on the server. +func Create(client *eclcloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeAttachmentCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, serverID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *eclcloud.ServiceClient, serverID, attachmentID string) (r GetResult) { + _, r.Err = client.Get(getURL(client, serverID, attachmentID), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored VolumeAttachment from +// the server. +func Delete(client *eclcloud.ServiceClient, serverID, attachmentID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, serverID, attachmentID), nil) + return +} diff --git a/v4/ecl/compute/v2/extensions/volumeattach/results.go b/v4/ecl/compute/v2/extensions/volumeattach/results.go new file mode 100644 index 0000000..67ce3b9 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/results.go @@ -0,0 +1,77 @@ +package volumeattach + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// VolumeAttachment contains attachment information between a volume +// and server. +type VolumeAttachment struct { + // ID is a unique id of the attachment. + ID string `json:"id"` + + // Device is what device the volume is attached as. + Device string `json:"device"` + + // VolumeID is the ID of the attached volume. + VolumeID string `json:"volumeId"` + + // ServerID is the ID of the instance that has the volume attached. + ServerID string `json:"serverId"` +} + +// VolumeAttachmentPage stores a single page all of VolumeAttachment +// results from a List call. +type VolumeAttachmentPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a VolumeAttachmentPage is empty. +func (page VolumeAttachmentPage) IsEmpty() (bool, error) { + va, err := ExtractVolumeAttachments(page) + return len(va) == 0, err +} + +// ExtractVolumeAttachments interprets a page of results as a slice of +// VolumeAttachment. +func ExtractVolumeAttachments(r pagination.Page) ([]VolumeAttachment, error) { + var s struct { + VolumeAttachments []VolumeAttachment `json:"volumeAttachments"` + } + err := (r.(VolumeAttachmentPage)).ExtractInto(&s) + return s.VolumeAttachments, err +} + +// VolumeAttachmentResult is the result from a volume attachment operation. +type VolumeAttachmentResult struct { + eclcloud.Result +} + +// Extract is a method that attempts to interpret any VolumeAttachment resource +// response as a VolumeAttachment struct. +func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) { + var s struct { + VolumeAttachment *VolumeAttachment `json:"volumeAttachment"` + } + err := r.ExtractInto(&s) + return s.VolumeAttachment, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a VolumeAttachment. +type CreateResult struct { + VolumeAttachmentResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a VolumeAttachment. +type GetResult struct { + VolumeAttachmentResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/compute/v2/extensions/volumeattach/testing/doc.go b/v4/ecl/compute/v2/extensions/volumeattach/testing/doc.go new file mode 100644 index 0000000..7d35174 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains volumeattach unit tests +package testing diff --git a/v4/ecl/compute/v2/extensions/volumeattach/testing/fixtures.go b/v4/ecl/compute/v2/extensions/volumeattach/testing/fixtures.go new file mode 100644 index 0000000..82b6af5 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/testing/fixtures.go @@ -0,0 +1,86 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/volumeattach" +) + +const serverID = "4d8c3732-a248-40ed-bebc-539a6ffd25c0" +const volumeID = "a26887c6-c47b-4654-abb5-dfadf7d3f803" +const attachID = volumeID + +var listResponse = fmt.Sprintf(` +{ + "volumeAttachments": [ + { + "device": "/dev/vdd", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + }, + { + "device": "/dev/vdc", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + } + ] +}`, + volumeID, serverID, volumeID, + volumeID, serverID, volumeID, +) + +var expectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{ + firstVolumeAttachment, + secondVolumeAttachment, +} + +var firstVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdd", + ID: volumeID, + ServerID: serverID, + VolumeID: volumeID, +} + +var secondVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: volumeID, + ServerID: serverID, + VolumeID: volumeID, +} + +var getResponse = fmt.Sprintf(` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + } +}`, volumeID, serverID, volumeID) + +var createRequest = fmt.Sprintf(` +{ + "volumeAttachment": { + "volumeId": "%s", + "device": "/dev/vdc" + } +}`, volumeID) + +var createResponse = fmt.Sprintf(` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "%s", + "serverId": "%s", + "volumeId": "%s" + } +}`, volumeID, serverID, volumeID) + +var createdVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: volumeID, + ServerID: serverID, + VolumeID: volumeID, +} diff --git a/v4/ecl/compute/v2/extensions/volumeattach/testing/requests_test.go b/v4/ecl/compute/v2/extensions/volumeattach/testing/requests_test.go new file mode 100644 index 0000000..aa9cf1f --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/testing/requests_test.go @@ -0,0 +1,95 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/extensions/volumeattach" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := volumeattach.List(fakeclient.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumeattach.ExtractVolumeAttachments(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedVolumeAttachmentSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + actual, err := volumeattach.Create(fakeclient.ServiceClient(), serverID, volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: volumeID, + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdVolumeAttachment, actual) +} + +func TestGetVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments/%s", serverID, volumeID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := volumeattach.Get(fakeclient.ServiceClient(), serverID, attachID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &secondVolumeAttachment, actual) +} + +func TestDeleteVolumeAttachment(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/servers/%s/os-volume_attachments/%s", serverID, volumeID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) + + err := volumeattach.Delete(fakeclient.ServiceClient(), serverID, attachID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/compute/v2/extensions/volumeattach/urls.go b/v4/ecl/compute/v2/extensions/volumeattach/urls.go new file mode 100644 index 0000000..36928f5 --- /dev/null +++ b/v4/ecl/compute/v2/extensions/volumeattach/urls.go @@ -0,0 +1,25 @@ +package volumeattach + +import "github.com/nttcom/eclcloud/v4" + +const resourcePath = "os-volume_attachments" + +func resourceURL(c *eclcloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, resourcePath) +} + +func listURL(c *eclcloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func createURL(c *eclcloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func getURL(c *eclcloud.ServiceClient, serverID, aID string) string { + return c.ServiceURL("servers", serverID, resourcePath, aID) +} + +func deleteURL(c *eclcloud.ServiceClient, serverID, aID string) string { + return getURL(c, serverID, aID) +} diff --git a/v4/ecl/compute/v2/flavors/doc.go b/v4/ecl/compute/v2/flavors/doc.go new file mode 100644 index 0000000..e4e2959 --- /dev/null +++ b/v4/ecl/compute/v2/flavors/doc.go @@ -0,0 +1,137 @@ +/* +Package flavors provides information and interaction with the flavor API +in the Enterprise Cloud Compute service. + +A flavor is an available hardware configuration for a server. Each flavor +has a unique combination of disk space, memory capacity and priority for CPU +time. + +Example to List Flavors + + listOpts := flavors.ListOpts{ + AccessType: flavors.PublicAccess, + } + + allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: eclcloud.IntToPointer(1), + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + flavor, err := flavors.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List Flavor Access + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages() + if err != nil { + panic(err) + } + + allAccesses, err := flavors.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.AddAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.RemoveAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Update Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/v4/ecl/compute/v2/flavors/requests.go b/v4/ecl/compute/v2/flavors/requests.go new file mode 100644 index 0000000..282f20e --- /dev/null +++ b/v4/ecl/compute/v2/flavors/requests.go @@ -0,0 +1,357 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +/* + AccessType maps to Enterprise Cloud's Flavor.is_public field. Although the is_public + field is boolean, the request options are ternary, which is why AccessType is + a string. The following values are allowed: + + The AccessType arguement is optional, and if it is not supplied, Enterprise Cloud + returns the PublicAccess flavors. +*/ +type AccessType string + +const ( + // PublicAccess returns public flavors and private flavors associated with + // that project. + PublicAccess AccessType = "true" + + // PrivateAccess (admin only) returns private flavors, across all projects. + PrivateAccess AccessType = "false" + + // AllAccess (admin only) returns public and private flavors across all + // projects. + AllAccess AccessType = "None" +) + +/* + ListOpts filters the results returned by the List() function. + For example, a flavor with a minDisk field of 10 will not be returned if you + specify MinDisk set to 20. + + Typically, software will use the last ID of the previous call to List to set + the Marker for the current call. +*/ +type 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"` + + // SortDir allows to select sort direction. + // It can be "asc" or "desc" (default). + SortDir string `q:"sort_dir"` + + // SortKey allows to sort by one of the flavors attributes. + // Default is flavorid. + SortKey string `q:"sort_key"` + + // 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"` + + // AccessType, if provided, instructs List which set of flavors to return. + // If IsPublic not provided, flavors for the current project are returned. + AccessType AccessType `q:"is_public"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs Enterprise Cloud to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier +// processing. +func ListDetail(client *eclcloud.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 + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// type CreateOptsBuilder interface { +// ToFlavorCreateMap() (map[string]interface{}, error) +// } + +// // CreateOpts specifies parameters used for creating a flavor. +// type CreateOpts struct { +// // Name is the name of the flavor. +// Name string `json:"name" required:"true"` + +// // RAM is the memory of the flavor, measured in MB. +// RAM int `json:"ram" required:"true"` + +// // VCPUs is the number of vcpus for the flavor. +// VCPUs int `json:"vcpus" required:"true"` + +// // Disk the amount of root disk space, measured in GB. +// Disk *int `json:"disk" required:"true"` + +// // ID is a unique ID for the flavor. +// ID string `json:"id,omitempty"` + +// // Swap is the amount of swap space for the flavor, measured in MB. +// Swap *int `json:"swap,omitempty"` + +// // RxTxFactor alters the network bandwidth of a flavor. +// RxTxFactor float64 `json:"rxtx_factor,omitempty"` + +// // IsPublic flags a flavor as being available to all projects or not. +// IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + +// // Ephemeral is the amount of ephemeral disk space, measured in GB. +// Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` +// } + +// // ToFlavorCreateMap constructs a request body from CreateOpts. +// func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { +// return eclcloud.BuildRequestBody(opts, "flavor") +// } + +// // Create requests the creation of a new flavor. +// func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { +// b, err := opts.ToFlavorCreateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200, 201}, +// }) +// return +// } + +// Get retrieves details of a single flavor. Use ExtractFlavor to convert its +// result into a Flavor. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// // Delete deletes the specified flavor ID. +// func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { +// _, r.Err = client.Delete(deleteURL(client, id), nil) +// return +// } + +// // ListAccesses retrieves the tenants which have access to a flavor. +// func ListAccesses(client *eclcloud.ServiceClient, id string) pagination.Pager { +// url := accessURL(client, id) + +// return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { +// return AccessPage{pagination.SinglePageBase(r)} +// }) +// } + +// // AddAccessOptsBuilder allows extensions to add additional parameters to the +// // AddAccess requests. +// type AddAccessOptsBuilder interface { +// ToFlavorAddAccessMap() (map[string]interface{}, error) +// } + +// // AddAccessOpts represents options for adding access to a flavor. +// type AddAccessOpts struct { +// // Tenant is the project/tenant ID to grant access. +// Tenant string `json:"tenant"` +// } + +// // ToFlavorAddAccessMap constructs a request body from AddAccessOpts. +// func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) { +// return eclcloud.BuildRequestBody(opts, "addTenantAccess") +// } + +// // AddAccess grants a tenant/project access to a flavor. +// func AddAccess(client *eclcloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { +// b, err := opts.ToFlavorAddAccessMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// // RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// // RemoveAccess requests. +// type RemoveAccessOptsBuilder interface { +// ToFlavorRemoveAccessMap() (map[string]interface{}, error) +// } + +// // RemoveAccessOpts represents options for removing access to a flavor. +// type RemoveAccessOpts struct { +// // Tenant is the project/tenant ID to grant access. +// Tenant string `json:"tenant"` +// } + +// // ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts. +// func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) { +// return eclcloud.BuildRequestBody(opts, "removeTenantAccess") +// } + +// // RemoveAccess removes/revokes a tenant/project access to a flavor. +// func RemoveAccess(client *eclcloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { +// b, err := opts.ToFlavorRemoveAccessMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// // ExtraSpecs requests all the extra-specs for the given flavor ID. +// func ListExtraSpecs(client *eclcloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { +// _, r.Err = client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil) +// return +// } + +// func GetExtraSpec(client *eclcloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { +// _, r.Err = client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil) +// return +// } + +// // CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// // CreateExtraSpecs requests. +// type CreateExtraSpecsOptsBuilder interface { +// ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) +// } + +// // ExtraSpecsOpts is a map that contains key-value pairs. +// type ExtraSpecsOpts map[string]string + +// // ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on +// // the contents of ExtraSpecsOpts. +// func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) { +// return map[string]interface{}{"extra_specs": opts}, nil +// } + +// // CreateExtraSpecs will create or update the extra-specs key-value pairs for +// // the specified Flavor. +// func CreateExtraSpecs(client *eclcloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { +// b, err := opts.ToFlavorExtraSpecsCreateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// // UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// // the Update request. +// type UpdateExtraSpecOptsBuilder interface { +// ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) +// } + +// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on +// the contents of a ExtraSpecOpts. +// func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) { +// if len(opts) != 1 { +// err := eclcloud.ErrInvalidInput{} +// err.Argument = "flavors.ExtraSpecOpts" +// err.Info = "Must have 1 and only one key-value pair" +// return nil, "", err +// } + +// var key string +// for k := range opts { +// key = k +// } + +// return opts, key, nil +// } + +// // UpdateExtraSpec will updates the value of the specified flavor's extra spec +// // for the key in opts. +// func UpdateExtraSpec(client *eclcloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { +// b, key, err := opts.ToFlavorExtraSpecUpdateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// DeleteExtraSpec will delete the key-value pair with the given key for the given +// flavor ID. +// func DeleteExtraSpec(client *eclcloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { +// _, r.Err = client.Delete(extraSpecDeleteURL(client, flavorID, key), &eclcloud.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// IDFromName is a convienience function that returns a flavor's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &eclcloud.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &eclcloud.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/v4/ecl/compute/v2/flavors/results.go b/v4/ecl/compute/v2/flavors/results.go new file mode 100644 index 0000000..4f64f69 --- /dev/null +++ b/v4/ecl/compute/v2/flavors/results.go @@ -0,0 +1,252 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// // CreateResult is the response of a Get operations. Call its Extract method to +// // interpret it as a Flavor. +// type CreateResult struct { +// commonResult +// } + +// GetResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// // DeleteResult is the result from a Delete operation. Call its ExtractErr +// // method to determine if the call succeeded or failed. +// type DeleteResult struct { +// eclcloud.ErrResult +// } + +// Extract provides access to the individual Flavor returned by the Get and +// Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // RxTxFactor describes bandwidth alterations of the flavor. + RxTxFactor float64 `json:"rxtx_factor"` + + // Swap is the amount of swap space, measured in MB. + Swap int `json:"-"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"os-flavor-access:is_public"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// // AccessPage contains a single page of all FlavorAccess entries for a flavor. +// type AccessPage struct { +// pagination.SinglePageBase +// } + +// // IsEmpty indicates whether an AccessPage is empty. +// func (page AccessPage) IsEmpty() (bool, error) { +// v, err := ExtractAccesses(page) +// return len(v) == 0, err +// } + +// // ExtractAccesses interprets a page of results as a slice of FlavorAccess. +// func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) { +// var s struct { +// FlavorAccesses []FlavorAccess `json:"flavor_access"` +// } +// err := (r.(AccessPage)).ExtractInto(&s) +// return s.FlavorAccesses, err +// } + +// type accessResult struct { +// eclcloud.Result +// } + +// // AddAccessResult is the response of an AddAccess operation. Call its +// // Extract method to interpret it as a slice of FlavorAccess. +// type AddAccessResult struct { +// accessResult +// } + +// // RemoveAccessResult is the response of a RemoveAccess operation. Call its +// // Extract method to interpret it as a slice of FlavorAccess. +// type RemoveAccessResult struct { +// accessResult +// } + +// // Extract provides access to the result of an access create or delete. +// // The result will be all accesses that the flavor has. +// func (r accessResult) Extract() ([]FlavorAccess, error) { +// var s struct { +// FlavorAccesses []FlavorAccess `json:"flavor_access"` +// } +// err := r.ExtractInto(&s) +// return s.FlavorAccesses, err +// } + +// // FlavorAccess represents an ACL of tenant access to a specific Flavor. +// type FlavorAccess struct { +// // FlavorID is the unique ID of the flavor. +// FlavorID string `json:"flavor_id"` + +// // TenantID is the unique ID of the tenant. +// TenantID string `json:"tenant_id"` +// } + +// // Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +// func (r extraSpecsResult) Extract() (map[string]string, error) { +// var s struct { +// ExtraSpecs map[string]string `json:"extra_specs"` +// } +// err := r.ExtractInto(&s) +// return s.ExtraSpecs, err +// } + +// // extraSpecsResult contains the result of a call for (potentially) multiple +// // key-value pairs. Call its Extract method to interpret it as a +// // map[string]interface. +// type extraSpecsResult struct { +// eclcloud.Result +// } + +// // ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// // method to interpret it as a map[string]interface. +// type ListExtraSpecsResult struct { +// extraSpecsResult +// } + +// // // CreateExtraSpecResult contains the result of a Create operation. Call its +// // // Extract method to interpret it as a map[string]interface. +// // type CreateExtraSpecsResult struct { +// // extraSpecsResult +// // } + +// // extraSpecResult contains the result of a call for individual a single +// // key-value pair. +// type extraSpecResult struct { +// eclcloud.Result +// } + +// // GetExtraSpecResult contains the result of a Get operation. Call its Extract +// // method to interpret it as a map[string]interface. +// type GetExtraSpecResult struct { +// extraSpecResult +// } + +// // // UpdateExtraSpecResult contains the result of an Update operation. Call its +// // // Extract method to interpret it as a map[string]interface. +// // type UpdateExtraSpecResult struct { +// // extraSpecResult +// // } + +// // // DeleteExtraSpecResult contains the result of a Delete operation. Call its +// // // ExtractErr method to determine if the call succeeded or failed. +// // type DeleteExtraSpecResult struct { +// // eclcloud.ErrResult +// // } + +// // Extract interprets any extraSpecResult as an ExtraSpec, if possible. +// func (r extraSpecResult) Extract() (map[string]string, error) { +// var s map[string]string +// err := r.ExtractInto(&s) +// return s, err +// } diff --git a/v4/ecl/compute/v2/flavors/urls.go b/v4/ecl/compute/v2/flavors/urls.go new file mode 100644 index 0000000..1d51d32 --- /dev/null +++ b/v4/ecl/compute/v2/flavors/urls.go @@ -0,0 +1,49 @@ +package flavors + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("flavors") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func accessURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-flavor-access") +} + +func accessActionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "action") +} + +func extraSpecsListURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecsGetURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecsCreateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecUpdateURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecDeleteURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} diff --git a/v4/ecl/compute/v2/images/doc.go b/v4/ecl/compute/v2/images/doc.go new file mode 100644 index 0000000..1ebc445 --- /dev/null +++ b/v4/ecl/compute/v2/images/doc.go @@ -0,0 +1,32 @@ +/* +Package images provides information and interaction with the images through +the Enterprise Cloud Compute service. + +This API is deprecated and will be removed from a future version of the Nova +API service. + +An image is a collection of files used to create or rebuild a server. +Operators provide a number of pre-built OS images by default. You may also +create custom images from cloud servers you have launched. + +Example to List Images + + listOpts := images.ListOpts{ + Limit: 2, + } + + allPages, err := images.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } +*/ +package images diff --git a/v4/ecl/compute/v2/images/requests.go b/v4/ecl/compute/v2/images/requests.go new file mode 100644 index 0000000..d9201fa --- /dev/null +++ b/v4/ecl/compute/v2/images/requests.go @@ -0,0 +1,109 @@ +package images + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// ListDetail request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options filtering Images returned from a call to ListDetail. +type ListOpts struct { + // ChangesSince filters Images based on the last changed status (in date-time + // format). + ChangesSince string `q:"changes-since"` + + // Limit limits the number of Images to return. + Limit int `q:"limit"` + + // Mark is an Image UUID at which to set a marker. + Marker string `q:"marker"` + + // Name is the name of the Image. + Name string `q:"name"` + + // Server is the name of the Server (in URL format). + Server string `q:"server"` + + // Status is the current status of the Image. + Status string `q:"status"` + + // Type is the type of image (e.g. BASE, SERVER, ALL). + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail enumerates the available images. +func ListDetail(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns data about a specific image by its ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete deletes the specified image ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// IDFromName is a convienience function that returns an image's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractImages(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &eclcloud.ErrResourceNotFound{} + err.ResourceType = "image" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &eclcloud.ErrMultipleResourcesFound{} + err.ResourceType = "image" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/v4/ecl/compute/v2/images/results.go b/v4/ecl/compute/v2/images/results.go new file mode 100644 index 0000000..21e7a78 --- /dev/null +++ b/v4/ecl/compute/v2/images/results.go @@ -0,0 +1,95 @@ +package images + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as an Image. +type GetResult struct { + eclcloud.Result +} + +// DeleteResult is the result from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Extract interprets a GetResult as an Image. +func (r GetResult) Extract() (*Image, error) { + var s struct { + Image *Image `json:"image"` + } + err := r.ExtractInto(&s) + return s.Image, err +} + +// Image represents an Image returned by the Compute API. +type Image struct { + // ID is the unique ID of an image. + ID string + + // Created is the date when the image was created. + Created string + + // MinDisk is the minimum amount of disk a flavor must have to be able + // to create a server based on the image, measured in GB. + MinDisk int + + // MinRAM is the minimum amount of RAM a flavor must have to be able + // to create a server based on the image, measured in MB. + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + Progress int + + // Status is the current status of the image. + Status string + + // Update is the date when the image was updated. + Updated string + + // Metadata provides free-form key/value pairs that further describe the + // image. + Metadata map[string]interface{} +} + +// ImagePage contains a single page of all Images returne from a ListDetail +// operation. Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page ImagePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"images_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image +// structs. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/v4/ecl/compute/v2/images/urls.go b/v4/ecl/compute/v2/images/urls.go new file mode 100644 index 0000000..e08102f --- /dev/null +++ b/v4/ecl/compute/v2/images/urls.go @@ -0,0 +1,15 @@ +package images + +import "github.com/nttcom/eclcloud/v4" + +func listDetailURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/v4/ecl/compute/v2/servers/doc.go b/v4/ecl/compute/v2/servers/doc.go new file mode 100644 index 0000000..c68acb6 --- /dev/null +++ b/v4/ecl/compute/v2/servers/doc.go @@ -0,0 +1,168 @@ +/* +Package servers provides information and interaction with the server API +resource in the Enterprise Cloud Compute service. + +A server is a virtual machine instance in the compute system. In order for +one to be provisioned, a valid flavor and image are required. + +Example to List Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.List(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Get a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + server, err := servers.Get(client, serverID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", server) + +Example to Create a Server + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + result := servers.Create(computeClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a Server + + name := "update_name" + updateOpts := servers.UpdateOpts{Name: &name} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.Update(client, serverID, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.Delete(computeClient, serverID) + if result.Err != nil { + panic(err) + } + +Example to Show Metadata a server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + metadata, err := servers.Metadata(client, serverID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", metadata) + +Example to Show details for a Metadata item by key for a Server + + key := "key" + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + metadatum, err := servers.Metadatum(client, serverID, key).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", metadatum) + +Example to Create Metadata a Server + + createMetadatumOpts := servers.MetadatumOpts{"key": "value"} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.CreateMetadatum(client, serverID, createMetadatumOpts) + if err != nil { + panic(result.Err) + } + +Example to Update Metadata a Server + + updateMetadataOpts := servers.MetadataOpts{"key": "update"} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.UpdateMetadata(client, serverID, updateMetadataOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete Metadata a Server + + key := "key" + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.DeleteMetadatum(client, serverID, key) + if result.Err != nil { + panic(result.Err) + } + +Example to Reset Metadata a Server + + resetMetadataOpts := servers.MetadataOpts{"key2": "val2"} + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.ResetMetadata(client, serverID, resetMetadataOpts) + if result.Err != nil { + panic(nil) + } + +Example to Resize a Server + + resizeOpts := servers.ResizeOpts{ + FlavorRef: "flavor-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.Resize(computeClient, serverID, resizeOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Snapshot a Server + + snapshotOpts := servers.CreateImageOpts{ + Name: "snapshot_name", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + result := servers.CreateImage(computeClient, serverID, snapshotOpts) + if result.Err != nil { + panic(result.Err) + } + +*/ +package servers diff --git a/v4/ecl/compute/v2/servers/errors.go b/v4/ecl/compute/v2/servers/errors.go new file mode 100644 index 0000000..18b885c --- /dev/null +++ b/v4/ecl/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ eclcloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ eclcloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ eclcloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ eclcloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ eclcloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ eclcloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ eclcloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + eclcloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e eclcloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/v4/ecl/compute/v2/servers/requests.go b/v4/ecl/compute/v2/servers/requests.go new file mode 100644 index 0000000..4091834 --- /dev/null +++ b/v4/ecl/compute/v2/servers/requests.go @@ -0,0 +1,573 @@ +package servers + +import ( + "encoding/base64" + // "encoding/json" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/flavors" + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/images" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Host is the name of the host as a string. + Host string `q:"host"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // AllTenants is a bool to show all tenants. + AllTenants bool `q:"all_tenants"` + + // TenantID lists servers for a particular tenant. + // Setting "AllTenants = true" is required. + TenantID string `q:"tenant_id"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list servers accessible to you. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network +// attachments. +type Network struct { + // UUID of a network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// Personality is an array of files that are injected into the server at launch. +// type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the +// server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild +// operation is requested, json.Marshal will call File's MarshalJSON method. +// type File struct { +// // Path of the file. +// Path string + +// // Contents of the file. Maximum content size is 255 bytes. +// Contents []byte +// } + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +// func (f *File) MarshalJSON() ([]byte, error) { +// file := struct { +// Path string `json:"path"` +// Contents string `json:"contents"` +// }{ +// Path: f.Path, +// Contents: base64.StdEncoding.EncodeToString(f.Contents), +// } +// return json.Marshal(file) +// } + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef [optional; required if ImageName is not provided] is the ID or + // full URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // ImageName [optional; required if ImageRef is not provided] is the name of + // the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string `json:"-"` + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string `json:"-"` + + // SecurityGroups lists the names of the security groups to which this server + // should belong. + // SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the + // tenant. + Networks []Network `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the + // server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + // Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + // AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 pecifies an IPv6 address for the instance. + // AccessIPv6 string `json:"accessIPv6,omitempty"` + + // ServiceClient will allow calls to be made to retrieve an image or + // flavor ID by name. + ServiceClient *eclcloud.ServiceClient `json:"-"` +} + +// ToServerCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + sc := opts.ServiceClient + opts.ServiceClient = nil + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + // if len(opts.SecurityGroups) > 0 { + // securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + // for i, groupName := range opts.SecurityGroups { + // securityGroups[i] = map[string]interface{}{"name": groupName} + // } + // b["security_groups"] = securityGroups + // } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + b["networks"] = networks + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageRef == "" { + if opts.ImageName != "" { + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(sc, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if opts.FlavorRef == "" { + if opts.FlavorName == "" { + err := ErrNeitherFlavorIDNorFlavorNameProvided{} + err.Argument = "FlavorRef/FlavorName" + return nil, err + } + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + flavorID, err := flavors.IDFromName(sc, opts.FlavorName) + if err != nil { + return nil, err + } + b["flavorRef"] = flavorID + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get requests details on a single server, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name *string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + // AccessIPv4 *string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + // AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResizeOptsBuilder allows extensions to add additional parameters to the +// resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize +// operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON +// request body for the Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// +// Note that this implies rebuilding it. +// +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in VERIFY_RESIZE state. +// While in this state, you can explore the use of the new server's +// configuration. If you like it, call ConfirmResize() to commit the resize +// permanently. Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *eclcloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// 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 *eclcloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *eclcloud.ServiceClient, id string) (r GetMetadataResult) { + _, r.Err = client.Get(metadataURL(client, id), &r.Body, nil) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for +// the given server ID. This operation does not affect already-existing metadata +// that is not specified by opts. +func UpdateMetadata(client *eclcloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the +// contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := eclcloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key +// for the given server ID. +func CreateMetadatum(client *eclcloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadatum requests the key-value pair with the given key for the given +// server ID. +func Metadatum(client *eclcloud.ServiceClient, id, key string) (r GetMetadatumResult) { + _, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the +// given server ID. +func DeleteMetadatum(client *eclcloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + _, r.Err = client.Delete(metadatumURL(client, id, key), nil) + return +} + +// CreateImageOptsBuilder allows extensions to add additional parameters to the +// CreateImage request. +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts provides options to pass to the CreateImage request. +type CreateImageOpts struct { + // Name of the image/snapshot. + Name string `json:"name" required:"true"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to + // the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request +// body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be +// created of the server +func CreateImage(client *eclcloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + r.Err = err + r.Header = resp.Header + return +} + +// IDFromName is a convienience function that returns a server's ID given its +// name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + allPages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractServers(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "server"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"} + } +} diff --git a/v4/ecl/compute/v2/servers/results.go b/v4/ecl/compute/v2/servers/results.go new file mode 100644 index 0000000..bdd9f3c --- /dev/null +++ b/v4/ecl/compute/v2/servers/results.go @@ -0,0 +1,295 @@ +package servers + +import ( + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type serverResult struct { + eclcloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + serverResult +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Server. +type GetResult struct { + serverResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + serverResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ActionResult represents the result of server action operations, like reboot. +// Call its ExtractErr method to determine if the action succeeded or failed. +type ActionResult struct { + eclcloud.ErrResult +} + +// CreateImageResult is the response from a CreateImage operation. Call its +// ExtractImageID method to retrieve the ID of the newly created image. +type CreateImageResult struct { + eclcloud.Result +} + +// ExtractImageID gets the ID of the newly created server image from the header. +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Server represents a server/instance in the Enterprise Cloud. +type Server struct { + // ID uniquely identifies this server amongst all other servers, + // including those not accessible to the current tenant. + ID string `json:"id"` + + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + + // Name contains the human-readable name for the server. + Name string `json:"name"` + + // Updated and Created contain ISO-8601 timestamps of when the state of the + // server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + + // HostID is the host where the server is located in the cloud. + HostID string `json:"hostid"` + + // Status contains the current operational status of the server, + // such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, + // suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + // AccessIPv6 string `json:"accessIPv6"` + + // Image refers to a JSON object, which itself indicates the OS image used to + // deploy the server. + Image map[string]interface{} `json:"-"` + + // Flavor refers to a JSON object, which itself indicates the hardware + // configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + + // Addresses includes a list of all IP addresses assigned to the server, + // keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the server. + Metadata map[string]string `json:"metadata"` + + // Links includes HTTP references to the itself, useful for passing along to + // other APIs that might want a server reference. + Links []interface{} `json:"links"` + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the + // administrative password chosen when provisioning a new server without a + // set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied + // to it. + SecurityGroups []map[string]interface{} `json:"security_groups"` + + // Fault contains failure information about a server. + Fault Fault `json:"fault"` + + // ConfigDrive is the name of the server's config drive. + ConfigDrive string `json:"config_drive"` +} + +type Fault struct { + Code int `json:"code"` + Created time.Time `json:"created"` + Details string `json:"details"` + Message string `json:"message"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against +// the API. As Enterprise Cloud extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type MetadataResult struct { + eclcloud.Result +} + +// GetMetadataResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult contains the result of a Reset operation. Call its +// Extract method to interpret it as a map[string]interface. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single +// key-value pair. +type MetadatumResult struct { + eclcloud.Result +} + +// GetMetadatumResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteMetadatumResult struct { + eclcloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} diff --git a/v4/ecl/compute/v2/servers/testing/doc.go b/v4/ecl/compute/v2/servers/testing/doc.go new file mode 100644 index 0000000..29b7613 --- /dev/null +++ b/v4/ecl/compute/v2/servers/testing/doc.go @@ -0,0 +1,2 @@ +// Compute Server unit tests +package testing diff --git a/v4/ecl/compute/v2/servers/testing/fixtures.go b/v4/ecl/compute/v2/servers/testing/fixtures.go new file mode 100644 index 0000000..89ec719 --- /dev/null +++ b/v4/ecl/compute/v2/servers/testing/fixtures.go @@ -0,0 +1,836 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/servers" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + "time" +) + +// ListResult provides a single page of Server results. +const ListResult = ` +{ + "servers": [ + { + "id": "707dbd55-b6bf-439d-804c-3002f49ac898", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "bookmark" + } + ], + "name": "Test Server2" + }, + { + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "name": "Test Server1" + } + ] +} +` + +// ListDetailsResult provides a single page of Server results in details. +const ListDetailsResult = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:f3:ed:05", + "version": 4, + "addr": "192.168.1.103", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2020-05-11T06:25:56.000000", + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "707dbd55-b6bf-439d-804c-3002f49ac898", + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "zone1_groupa", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Test Server2", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "True", + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + }, + { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:49:78:28", + "version": 4, + "addr": "192.168.1.101", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2020-05-11T03:36:27.000000", + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "zone1_groupa", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Test Server1", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:49:78:28", + "version": 4, + "addr": "192.168.1.101", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2020-05-11T03:36:27.000000", + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "zone1_groupa", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Test Server1", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "server": { + "flavorRef": "1CPU-4GB", + "imageRef": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "name": "Test Server1", + "availability_zone": "zone1-groupa", + "config_drive": true, + "user_data": "dXNlcl9kYXRh", + "metadata": { + "foo": "bar" + }, + "networks": [ + { + "uuid": "4d98b876-b5d1-4861-8650-b5a53024486a" + } + ] + } +} +` + +const CreateResponse = ` +{ + "server": { + "security_groups": [ + { + "name": "default" + } + ], + "OS-DCF:diskConfig": "MANUAL", + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "adminPass": "aabbccddeeff" + } +} +` + +const UpdateRequest = ` +{ + "server": { + "name": "Update Name" + } +} +` + +const UpdateResponse = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2020-05-18T01:51:41Z", + "hostId": "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + "addresses": { + "IF-4831": [ + { + "version": 4, + "addr": "192.168.1.101" + } + ] + }, + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self" + }, + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "bookmark" + } + ], + "image": { + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark" + } + ] + }, + "flavor": { + "id": "1CPU-4GB", + "links": [ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark" + } + ] + }, + "id": "8e69a092-53f9-4225-bae6-57cfbb5d6857", + "user_id": "5e86848fbc63403daaeffc1b76b3a784", + "name": "Update Name", + "created": "2020-05-11T06:25:53Z", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "OS-DCF:diskConfig": "MANUAL", + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } + } +} +` + +var MetadataResult = ` +{ + "metadata": { + "vmha": "false", + "HA_Enabled": "false" + } +} +` + +var MetadatumResult = ` +{ + "meta": { + "vmha": "false" + } +} +` + +var CreateMetadatumRequest = ` +{ + "meta": { + "key1": "val1" + } +} +` + +var CreateMetadatumResponse = CreateMetadatumRequest + +var UpdateMetadataRequest = ` +{ + "metadata": { + "key1": "update_val" + } +} +` + +var UpdateMetadataResponse = UpdateMetadataRequest + +var ResetMetadataRequest = ` +{ + "metadata": { + "key1": "val1", + "key2": "val2" + } +} +` + +var ResetMetadataResponse = ResetMetadataRequest + +var ResizeRequest = ` +{ + "resize": { + "flavorRef": "2CPU-8GB" + } +} +` + +var CreateImageRequest = ` +{ + "createImage": { + "metadata": { + "key": "create_image" + }, + "name": "Test Create Image" + } +} +` + +var expectedServers = []servers.Server{expectedServer1, expectedServer2} + +var expectedCreated, _ = time.Parse(eclcloud.RFC3339Milli, "2020-05-11T06:25:53Z") +var expectedUpdated, _ = time.Parse(eclcloud.RFC3339Milli, "2020-05-18T01:51:41Z") + +var expectedServer1 = servers.Server{ + ID: "707dbd55-b6bf-439d-804c-3002f49ac898", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + UserID: "5e86848fbc63403daaeffc1b76b3a784", + Name: "Test Server2", + Updated: expectedUpdated, + Created: expectedCreated, + HostID: "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + Status: "ACTIVE", + Progress: 0, + AccessIPv4: "", + Image: map[string]interface{}{ + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1CPU-4GB", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "IF-4831": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:f3:ed:05", + "OS-EXT-IPS:type": "fixed", + "addr": "192.168.1.103", + "version": float64(4), + }, + }, + }, + Metadata: map[string]string{ + "vmha": "false", + "HA_Enabled": "false", + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/707dbd55-b6bf-439d-804c-3002f49ac898", + "rel": "self", + }, + }, + AdminPass: "", + SecurityGroups: nil, + Fault: servers.Fault{}, + ConfigDrive: "True", +} + +var expectedServer2 = servers.Server{ + ID: "8e69a092-53f9-4225-bae6-57cfbb5d6857", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + UserID: "5e86848fbc63403daaeffc1b76b3a784", + Name: "Test Server1", + Updated: expectedUpdated, + Created: expectedCreated, + HostID: "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + Status: "ACTIVE", + Progress: 0, + AccessIPv4: "", + Image: map[string]interface{}{ + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1CPU-4GB", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "IF-4831": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:49:78:28", + "OS-EXT-IPS:type": "fixed", + "addr": "192.168.1.101", + "version": float64(4), + }, + }, + }, + Metadata: map[string]string{ + "vmha": "false", + "HA_Enabled": "false", + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self", + }, + }, + //KeyName: nil, + AdminPass: "", + SecurityGroups: nil, + Fault: servers.Fault{}, + ConfigDrive: "", +} + +var serverNameUpdated = servers.Server{ + ID: "8e69a092-53f9-4225-bae6-57cfbb5d6857", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + UserID: "5e86848fbc63403daaeffc1b76b3a784", + Name: "Update Name", + Updated: expectedUpdated, + Created: expectedCreated, + HostID: "d7961f8a2cde3e49a3f5d3a0a95c6c1d9ce28a342285d4118a936247", + Status: "ACTIVE", + Progress: 0, + AccessIPv4: "", + Image: map[string]interface{}{ + "id": "c11a6d55-70e9-4d04-a086-4451f07da0d7", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/c11a6d55-70e9-4d04-a086-4451f07da0d7", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1CPU-4GB", + "links": []map[string]interface{}{ + { + "href": "https://nova-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/1CPU-4GB", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "IF-4831": []interface{}{ + map[string]interface{}{ + "addr": "192.168.1.101", + "version": float64(4), + }, + }, + }, + Metadata: map[string]string{ + "vmha": "false", + "HA_Enabled": "false", + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://nova-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/8e69a092-53f9-4225-bae6-57cfbb5d6857", + "rel": "self", + }, + }, + AdminPass: "", + SecurityGroups: nil, + Fault: servers.Fault{}, + ConfigDrive: "", +} + +var expectMetadata = map[string]string{ + "vmha": "false", + "HA_Enabled": "false", +} + +var expectMetadatum = map[string]string{ + "vmha": "false", +} + +var expectCreateMetadatum = map[string]string{ + "key1": "val1", +} + +var ecpectUpdateMetadata = map[string]string{ + "key1": "update_val", +} + +var expectResetMetadata = map[string]string{ + "key1": "val1", + "key2": "val2", +} + +// HandleListServersSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a list of two servers. +func HandleListServersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers", 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, ListResult) + }) +} + +// HandleListServersDetailsSuccessfully creates an HTTP handler at `/servers/detail` on the +// test handler mux that responds with a list of two servers. +func HandleListServersDetailsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", 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, ListDetailsResult) + }) +} + +// HandleGetServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a single server. +func HandleGetServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", expectedServer2.ID), 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, GetResult) + }) +} + +// HandleCreateServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server creation. +func HandleCreateServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, CreateResponse) + }) +} + +// HandleDeleteServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server deletion. +func HandleDeleteServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", expectedServer1.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + 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.StatusNoContent) + }) +} + +// HandleUpdateServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server update. +func HandleUpdateServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateResponse) + }) +} + +// HandleGetMetadataSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a server metadata. +func HandleGetMetadataSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata", expectedServer2.ID), 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.WriteHeader(http.StatusOK) + fmt.Fprintf(w, MetadataResult) + }) +} + +// HandleGetMetadatumSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a server metadatum. +func HandleGetMetadatumSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata/vmha", expectedServer2.ID), 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.WriteHeader(http.StatusOK) + fmt.Fprintf(w, MetadatumResult) + }) +} + +// HandleCreateMetadatumSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata creation. +func HandleCreateMetadatumSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata/key1", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateMetadatumRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, CreateMetadatumResponse) + }) +} + +// HandleDeleteMetadatumSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata deletion. +func HandleDeleteMetadatumSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata/vmha", expectedServer1.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + 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.StatusNoContent) + }) +} + +// HandleUpdateMetadataSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata update. +func HandleUpdateMetadataSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, UpdateMetadataRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateMetadataResponse) + }) +} + +// HandleResetMetadataSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server metadata reset. +func HandleResetMetadataSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/metadata", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ResetMetadataRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ResetMetadataResponse) + }) +} + +// HandleResizeServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server resize action. +func HandleResizeServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ResizeRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleCreateImageSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests create server image. +func HandleCreateImageSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", expectedServer2.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateImageRequest) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/v4/ecl/compute/v2/servers/testing/requests_test.go b/v4/ecl/compute/v2/servers/testing/requests_test.go new file mode 100644 index 0000000..2edd074 --- /dev/null +++ b/v4/ecl/compute/v2/servers/testing/requests_test.go @@ -0,0 +1,197 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/compute/v2/servers" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + count := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expectedServers, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListServersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + allPages, err := servers.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectedServers, actual) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetServerSuccessfully(t) + + actual, err := servers.Get(client.ServiceClient(), expectedServer2.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectedServer2, *actual) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerSuccessfully(t) + + configDrive := true + createOpts := servers.CreateOpts{ + Name: "Test Server1", + ImageRef: "c11a6d55-70e9-4d04-a086-4451f07da0d7", + FlavorRef: "1CPU-4GB", + UserData: []byte("user_data"), + AvailabilityZone: "zone1-groupa", + Networks: []servers.Network{ + { + UUID: "4d98b876-b5d1-4861-8650-b5a53024486a", + }, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + ConfigDrive: &configDrive, + } + + actual, err := servers.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedServer2.ID, actual.ID) + th.AssertDeepEquals(t, expectedServer2.Links, actual.Links) + th.AssertEquals(t, "aabbccddeeff", actual.AdminPass) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteServerSuccessfully(t) + + res := servers.Delete(client.ServiceClient(), expectedServer1.ID) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateServerSuccessfully(t) + + name := "Update Name" + updateOpts := servers.UpdateOpts{Name: &name} + + actual, err := servers.Update(client.ServiceClient(), expectedServer2.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, serverNameUpdated, *actual) +} + +func TestGetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetMetadataSuccessfully(t) + + actual, err := servers.Metadata(client.ServiceClient(), expectedServer2.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectMetadata, actual) +} + +func TestGetMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetMetadatumSuccessfully(t) + + actual, err := servers.Metadatum(client.ServiceClient(), expectedServer2.ID, "vmha").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectMetadatum, actual) +} + +func TestCreateMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateMetadatumSuccessfully(t) + + createOpts := servers.MetadatumOpts{"key1": "val1"} + + actual, err := servers.CreateMetadatum(client.ServiceClient(), expectedServer2.ID, createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectCreateMetadatum, actual) +} + +func TestDeleteMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteMetadatumSuccessfully(t) + + res := servers.DeleteMetadatum(client.ServiceClient(), expectedServer1.ID, "vmha") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateMetadataSuccessfully(t) + + updateOpts := servers.MetadataOpts{"key1": "update_val"} + + actual, err := servers.UpdateMetadata(client.ServiceClient(), expectedServer2.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ecpectUpdateMetadata, actual) +} + +func TestResetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleResetMetadataSuccessfully(t) + + createOpts := servers.MetadataOpts{ + "key1": "val1", + "key2": "val2", + } + + actual, err := servers.ResetMetadata(client.ServiceClient(), expectedServer2.ID, createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectResetMetadata, actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleResizeServerSuccessfully(t) + + resizeOpts := servers.ResizeOpts{FlavorRef: "2CPU-8GB"} + + err := servers.Resize(client.ServiceClient(), expectedServer2.ID, resizeOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestCreateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateImageSuccessfully(t) + + snapshotOpts := servers.CreateImageOpts{ + Name: "Test Create Image", + Metadata: map[string]string{"key": "create_image"}, + } + + result := servers.CreateImage(client.ServiceClient(), expectedServer2.ID, snapshotOpts) + th.AssertNoErr(t, result.Err) +} diff --git a/v4/ecl/compute/v2/servers/urls.go b/v4/ecl/compute/v2/servers/urls.go new file mode 100644 index 0000000..48db63a --- /dev/null +++ b/v4/ecl/compute/v2/servers/urls.go @@ -0,0 +1,39 @@ +package servers + +import "github.com/nttcom/eclcloud/v4" + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *eclcloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *eclcloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} diff --git a/v4/ecl/compute/v2/servers/util.go b/v4/ecl/compute/v2/servers/util.go new file mode 100644 index 0000000..1b305c4 --- /dev/null +++ b/v4/ecl/compute/v2/servers/util.go @@ -0,0 +1,21 @@ +package servers + +import "github.com/nttcom/eclcloud/v4" + +// 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 *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v4/ecl/computevolume/extensions/volumeactions/doc.go b/v4/ecl/computevolume/extensions/volumeactions/doc.go new file mode 100644 index 0000000..5604976 --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/doc.go @@ -0,0 +1,86 @@ +/* +Package volumeactions provides information and interaction with volumes in the +Enterprise Cloud Block Storage service. A volume is a detachable block storage +device, akin to a USB hard drive. + +Example of Attaching a Volume to an Instance + + attachOpts := volumeactions.AttachOpts{ + MountPoint: "/mnt", + Mode: "rw", + InstanceUUID: server.ID, + } + + err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr() + if err != nil { + panic(err) + } + + detachOpts := volumeactions.DetachOpts{ + AttachmentID: volume.Attachments[0].AttachmentID, + } + + err = volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr() + if err != nil { + panic(err) + } + + +Example of Creating an Image from a Volume + + uploadImageOpts := volumeactions.UploadImageOpts{ + ImageName: "my_vol", + Force: true, + } + + volumeImage, err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", volumeImage) + +Example of Extending a Volume's Size + + extendOpts := volumeactions.ExtendSizeOpts{ + NewSize: 100, + } + + err := volumeactions.ExtendSize(client, volume.ID, extendOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Initializing a Volume Connection + + connectOpts := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: eclcloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + connectionInfo, err := volumeactions.InitializeConnection(client, volume.ID, connectOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", connectionInfo["data"]) + + terminateOpts := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: eclcloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + err = volumeactions.TerminateConnection(client, volume.ID, terminateOpts).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeactions diff --git a/v4/ecl/computevolume/extensions/volumeactions/requests.go b/v4/ecl/computevolume/extensions/volumeactions/requests.go new file mode 100644 index 0000000..fa4d051 --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/requests.go @@ -0,0 +1,84 @@ +package volumeactions + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// ExtendSizeOptsBuilder allows extensions to add additional parameters to the +// ExtendSize request. +type ExtendSizeOptsBuilder interface { + ToVolumeExtendSizeMap() (map[string]interface{}, error) +} + +// ExtendSizeOpts contains options for extending the size of an existing Volume. +// This object is passed to the volumes.ExtendSize function. +type ExtendSizeOpts struct { + // NewSize is the new size of the volume, in GB. + NewSize int `json:"new_size" required:"true"` +} + +// ToVolumeExtendSizeMap assembles a request body based on the contents of an +// ExtendSizeOpts. +func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "os-extend") +} + +// ExtendSize will extend the size of the volume based on the provided information. +// This operation does not return a response body. +func ExtendSize(client *eclcloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) { + b, err := opts.ToVolumeExtendSizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// UploadImageOptsBuilder allows extensions to add additional parameters to the +// UploadImage request. +type UploadImageOptsBuilder interface { + ToVolumeUploadImageMap() (map[string]interface{}, error) +} + +// UploadImageOpts contains options for uploading a Volume to image storage. +type UploadImageOpts struct { + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format,omitempty"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format,omitempty"` + + // The name of image that will be stored in glance. + ImageName string `json:"image_name,omitempty"` + + // Force image creation, usable if volume attached to instance. + Force bool `json:"force,omitempty"` +} + +// ToVolumeUploadImageMap assembles a request body based on the contents of a +// UploadImageOpts. +func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "os-volume_upload_image") +} + +// UploadImage will upload an image based on the values in UploadImageOptsBuilder. +func UploadImage(client *eclcloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) { + b, err := opts.ToVolumeUploadImageMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// ForceDelete will delete the volume regardless of state. +func ForceDelete(client *eclcloud.ServiceClient, id string) (r ForceDeleteResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil) + return +} diff --git a/v4/ecl/computevolume/extensions/volumeactions/results.go b/v4/ecl/computevolume/extensions/volumeactions/results.go new file mode 100644 index 0000000..6e31d37 --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/results.go @@ -0,0 +1,139 @@ +package volumeactions + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" +) + +// UploadImageResult contains the response body and error from an UploadImage +// request. +type UploadImageResult struct { + eclcloud.Result +} + +// ExtendSizeResult contains the response body and error from an ExtendSize request. +type ExtendSizeResult struct { + eclcloud.ErrResult +} + +// ImageVolumeType contains volume type information obtained from UploadImage +// action. +type ImageVolumeType struct { + // The ID of a volume type. + ID string `json:"id"` + + // Human-readable display name for the volume type. + Name string `json:"name"` + + // Human-readable description for the volume type. + Description string `json:"display_description"` + + // Flag for public access. + IsPublic bool `json:"is_public"` + + // Extra specifications for volume type. + ExtraSpecs map[string]interface{} `json:"extra_specs"` + + // ID of quality of service specs. + QosSpecsID string `json:"qos_specs_id"` + + // Flag for deletion status of volume type. + Deleted bool `json:"deleted"` + + // The date when volume type was deleted. + DeletedAt time.Time `json:"-"` + + // The date when volume type was created. + CreatedAt time.Time `json:"-"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` +} + +func (r *ImageVolumeType) UnmarshalJSON(b []byte) error { + type tmp ImageVolumeType + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + DeletedAt eclcloud.JSONRFC3339MilliNoZ `json:"deleted_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ImageVolumeType(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.DeletedAt = time.Time(s.DeletedAt) + + return err +} + +// VolumeImage contains information about volume uploaded to an image service. +type VolumeImage struct { + // The ID of a volume an image is created from. + VolumeID string `json:"id"` + + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format"` + + // Human-readable description for the volume. + Description string `json:"display_description"` + + // The ID of the created image. + ImageID string `json:"image_id"` + + // Human-readable display name for the image. + ImageName string `json:"image_name"` + + // Size of the volume in GB. + Size int `json:"size"` + + // Current status of the volume. + Status string `json:"status"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` + + // Volume type object of used volume. + VolumeType ImageVolumeType `json:"volume_type"` +} + +func (r *VolumeImage) UnmarshalJSON(b []byte) error { + type tmp VolumeImage + var s struct { + tmp + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VolumeImage(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// Extract will get an object with info about the uploaded image out of the +// UploadImageResult object. +func (r UploadImageResult) Extract() (VolumeImage, error) { + var s struct { + VolumeImage VolumeImage `json:"os-volume_upload_image"` + } + err := r.ExtractInto(&s) + return s.VolumeImage, err +} + +// ForceDeleteResult contains the response body and error from a ForceDelete request. +type ForceDeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/computevolume/extensions/volumeactions/testing/doc.go b/v4/ecl/computevolume/extensions/volumeactions/testing/doc.go new file mode 100644 index 0000000..336406d --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/testing/doc.go @@ -0,0 +1,2 @@ +// volumeactions unit tests +package testing diff --git a/v4/ecl/computevolume/extensions/volumeactions/testing/fixtures.go b/v4/ecl/computevolume/extensions/volumeactions/testing/fixtures.go new file mode 100644 index 0000000..c643b80 --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/testing/fixtures.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" +) + +const volumeID = "ff2ac0fd-ea58-4e15-bd71-aec0bc58c469" +const instanceID = "ff2ac0fd-ea58-4e15-bd71-aec0bc58c469" + +const uploadImageRequest = `{ + "os-volume_upload_image": { + "container_format": "bare", + "force": true, + "image_name": "imagetest", + "disk_format": "raw" + } +}` + +var uploadImageResponse = fmt.Sprintf(`{ + "os-volume_upload_image": { + "status": "uploading", + "image_id": "49d7efe7-975e-46d7-af0a-fd94fe8e62bf", + "image_name": "imagetest", + "volume_type": { + "name": "nfsdriver", + "qos_specs_id": null, + "deleted": false, + "created_at": "2018-06-04T08:05:09.000000", + "updated_at": null, + "deleted_at": null, + "id": "1f02ea8f-3823-4e69-a232-695adc39f5e0" + }, + "container_format": "bare", + "size": 40, + "disk_format": "raw", + "id": "%s", + "display_description": "test volume 2update", + "updated_at": "2019-02-06T22:06:27.000000" + } +}`, + volumeID, +) + +const extendRequest = `{ + "os-extend": + { + "new_size": 40 + } +}` diff --git a/v4/ecl/computevolume/extensions/volumeactions/testing/requests_test.go b/v4/ecl/computevolume/extensions/volumeactions/testing/requests_test.go new file mode 100644 index 0000000..b72f85f --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/testing/requests_test.go @@ -0,0 +1,88 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/computevolume/extensions/volumeactions" + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestVolumeUploadImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s/action", volumeID) + + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, uploadImageRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, uploadImageResponse) + }) + + options := &volumeactions.UploadImageOpts{ + ContainerFormat: "bare", + Force: true, + ImageName: "imagetest", + DiskFormat: "raw", + } + + actual, err := volumeactions.UploadImage(fakeclient.ServiceClient(), volumeID, options).Extract() + th.AssertNoErr(t, err) + + expected := volumeactions.VolumeImage{ + Status: "uploading", + ImageID: "49d7efe7-975e-46d7-af0a-fd94fe8e62bf", + ImageName: "imagetest", + VolumeType: volumeactions.ImageVolumeType{ + Name: "nfsdriver", + QosSpecsID: "", + Deleted: false, + CreatedAt: time.Date(2018, 6, 4, 8, 5, 9, 0, time.UTC), + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + ID: "1f02ea8f-3823-4e69-a232-695adc39f5e0", + }, + ContainerFormat: "bare", + Size: 40, + DiskFormat: "raw", + VolumeID: volumeID, + Description: "test volume 2update", + UpdatedAt: time.Date(2019, 2, 6, 22, 6, 27, 0, time.UTC), + } + th.AssertDeepEquals(t, expected, actual) +} + +func TestVolumeExtendSize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s/action", volumeID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, extendRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) + + options := &volumeactions.ExtendSizeOpts{ + NewSize: 40, + } + + err := volumeactions.ExtendSize(fakeclient.ServiceClient(), volumeID, options).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/computevolume/extensions/volumeactions/urls.go b/v4/ecl/computevolume/extensions/volumeactions/urls.go new file mode 100644 index 0000000..5790964 --- /dev/null +++ b/v4/ecl/computevolume/extensions/volumeactions/urls.go @@ -0,0 +1,7 @@ +package volumeactions + +import "github.com/nttcom/eclcloud/v4" + +func actionURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id, "action") +} diff --git a/v4/ecl/computevolume/v2/volumes/doc.go b/v4/ecl/computevolume/v2/volumes/doc.go new file mode 100644 index 0000000..98fdc6b --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// Enterprise Cloud 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/v4/ecl/computevolume/v2/volumes/requests.go b/v4/ecl/computevolume/v2/volumes/requests.go new file mode 100644 index 0000000..dda79c1 --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/requests.go @@ -0,0 +1,207 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/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 { + // The size of the volume, in GB + Size int `json:"size" required:"true"` + // The availability zone + AvailabilityZone string `json:"availability_zone,omitempty"` + // ConsistencyGroupID is the ID of a consistency group + ConsistencyGroupID string `json:"consistencygroup_id,omitempty"` + // The volume description + Description string `json:"description,omitempty"` + // One or more metadata key and value pairs to associate with the volume + Metadata map[string]string `json:"metadata,omitempty"` + // The volume name + Name string `json:"name,omitempty"` + // the ID of the existing volume snapshot + SnapshotID string `json:"snapshot_id,omitempty"` + // SourceReplica is a UUID of an existing volume to replicate with + SourceReplica string `json:"source_replica,omitempty"` + // the ID of the existing volume + SourceVolID string `json:"source_volid,omitempty"` + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string `json:"imageRef,omitempty"` + // The associated volume type + VolumeType string `json:"volume_type,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// 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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// 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 { + // AllTenants will retrieve volumes of all tenants/projects. + AllTenants bool `q:"all_tenants"` + + // Metadata will filter results based on specified metadata. + Metadata map[string]string `q:"metadata"` + + // Name will filter by the specified volume name. + Name string `q:"name"` + + // Status will filter by the specified status. + Status string `q:"status"` + + // TenantID will filter by a specific tenant/project ID. + // Setting AllTenants is required for this. + TenantID string `q:"project_id"` + + // Comma-separated list of sort keys and optional sort directions in the + // form of [:]. + Sort string `q:"sort"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` + + // The ID of the last-seen item. + Marker string `q:"marker"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.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 + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// 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 { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Metadata *map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/v4/ecl/computevolume/v2/volumes/results.go b/v4/ecl/computevolume/v2/volumes/results.go new file mode 100644 index 0000000..147e55a --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/results.go @@ -0,0 +1,169 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type Attachment struct { + AttachedAt time.Time `json:"-"` + AttachmentID string `json:"attachment_id"` + Device string `json:"device"` + HostName string `json:"host_name"` + ID string `json:"id"` + ServerID string `json:"server_id"` + VolumeID string `json:"volume_id"` +} + +func (r *Attachment) UnmarshalJSON(b []byte) error { + type tmp Attachment + var s struct { + tmp + AttachedAt eclcloud.JSONRFC3339MilliNoZ `json:"attached_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Attachment(s.tmp) + + r.AttachedAt = time.Time(s.AttachedAt) + + return err +} + +// Volume contains all the information associated with an Enterprise Cloud Volume. +type Volume struct { + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Size of the volume in GB. + Size int `json:"size"` + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Instances onto which the volume is attached. + Attachments []Attachment `json:"attachments"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // UserID is the id of the user who created the volume. + UserID string `json:"user_id"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // Encrypted denotes if the volume is encrypted. + Encrypted bool `json:"encrypted"` + // ReplicationStatus is the status of replication. + ReplicationStatus string `json:"replication_status"` + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `json:"consistencygroup_id"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach"` + // TenantID is the id of the project that owns the volume. + TenantID string `json:"os-vol-tenant-attr:tenant_id"` +} + +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r VolumePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"volumes_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// 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 +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/computevolume/v2/volumes/testing/doc.go b/v4/ecl/computevolume/v2/volumes/testing/doc.go new file mode 100644 index 0000000..100d586 --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/testing/doc.go @@ -0,0 +1,2 @@ +// volumes_v2 unittest +package testing diff --git a/v4/ecl/computevolume/v2/volumes/testing/fixtures.go b/v4/ecl/computevolume/v2/volumes/testing/fixtures.go new file mode 100644 index 0000000..67a0e62 --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/testing/fixtures.go @@ -0,0 +1,358 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/computevolume/v2/volumes" +) + +const idVolume1 = "251df9eb-c088-4e71-808b-75a690e8814b" +const idVolume2 = "7e0b432b-c922-49d7-b85a-28ac88164328" + +const sizeVolume1 = 40 + +const nameVolume1 = "volume1" +const nameVolume1Update = "volume1-update" + +const descriptionVolume1 = "test volume 1" +const descriptionVolume1Update = "test volume 1-update" + +const instanceID = "83ec2e3b-4321-422b-8706-a84185f52a0a" +const tenantID = "9ee80f2a926c49f88f166af47df4e9f5" +const az = "zone1-groupa" + +const createdAt = "2019-02-06T08:06:57.000000" + +var timeCreatedAt = time.Date(2019, 2, 6, 8, 6, 57, 0, time.UTC) + +var listResponse = fmt.Sprintf(`{ + "volumes": [{ + "id": "%s", + "name": "%s", + "status": "in-use", + "size": %d, + "availability_zone": "%s", + "created_at": "%s", + "os-vol-tenant-attr:tenant_id": "%s", + "description": "%s", + "attachments": [{ + "host_name": null, + "device": "/dev/vdb", + "server_id": "%s", + "id": "%s", + "volume_id": "%s" + }], + "links": [{ + "href": "dummy_self_link", + "rel": "self" + }, { + "href": "dummy_bookmark_link", + "rel": "bookmark" + }], + "encrypted": false, + "os-volume-replication:extended_status": null, + "volume_type": "nfsdriver", + "snapshot_id": null, + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "metadata": { + "readonly": "False", + "attached_mode": "rw" + }, + "volume_image_metadata": { + ".edition": "none", + ".major.version": "7", + ".official_image_template": "CentOS-7.1-1503_64_virtual-server_12", + "container_format": "bare", + "min_ram": "0", + "disk_format": "qcow2", + ".is_license": "False", + "image_name": "CentOS-7.1-1503_64_virtual-server_12", + "image_id": "df1944a7-ca45-4709-9ec6-e31664133650", + ".os.type": "centos", + ".enable.download": "True", + "checksum": "a828b6ba68b9d13d2da0a0cb3cfaa950", + "min_disk": "15", + ".service.type": "virtual-server", + ".virtual_server.os.pod": "other", + ".minor.version": "1-1503", + "size": "461504512" + }, + "source_volid": null, + "consistencygroup_id": null, + "bootable": "true", + "os-volume-replication:driver_data": null, + "replication_status": "disabled" + }, { + "id": "%s", + "name": "volume 2", + "status": "available", + "size": 40, + "availability_zone": "%s", + "created_at": "%s", + "os-vol-tenant-attr:tenant_id": "%s", + "description": "test volume 2", + "attachments": [], + "links": [{ + "href": "dummy_self_link", + "rel": "self" + }, { + "href": "dummy_bookmark_link", + "rel": "bookmark" + }], + "encrypted": false, + "os-volume-replication:extended_status": null, + "volume_type": "nfsdriver", + "snapshot_id": null, + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "metadata": {}, + "volume_image_metadata": { + ".edition": "none", + ".major.version": "7", + "container_format": "bare", + "min_ram": "0", + "disk_format": "qcow2", + ".is_license": "True", + "image_name": "RedHatEnterpriseLinux-7.1_64_include-license_virtual-server_42", + "image_id": "f304bc07-056a-406f-85fc-9f97c7b8ef95", + ".os.type": "rhel", + ".enable.download": "False", + "checksum": "85851188a680c5bddecb664914917a81", + "min_disk": "40", + ".service.type": "virtual-server", + ".virtual_server.os.pod": "rhel", + ".minor.version": "1", + "size": "515899392" + }, + "source_volid": null, + "consistencygroup_id": null, + "bootable": "true", + "os-volume-replication:driver_data": null, + "replication_status": "disabled" + }] +}`, + // For volume 1 + idVolume1, + nameVolume1, + sizeVolume1, + az, + createdAt, + tenantID, + descriptionVolume1, + instanceID, + idVolume1, + idVolume1, + // For volume 2 + idVolume2, + az, + createdAt, + tenantID, +) + +var structVolume1 = volumes.Volume{ + ID: idVolume1, + Status: "in-use", + Size: sizeVolume1, + AvailabilityZone: az, + CreatedAt: timeCreatedAt, + Attachments: []volumes.Attachment{{ + // AttachedAt: time.Date(2016, 8, 6, 14, 48, 20, 0, time.UTC), + // AttachmentID: idVolume1, + Device: "/dev/vdb", + HostName: "", + ID: idVolume1, + ServerID: instanceID, + VolumeID: idVolume1, + }}, + Name: nameVolume1, + Description: descriptionVolume1, + VolumeType: "nfsdriver", + SnapshotID: "", + SourceVolID: "", + Metadata: map[string]string{ + "readonly": "False", + "attached_mode": "rw", + }, + UserID: "2a5719084bc9457c93e659f4f13c6bfc", + Bootable: "true", + Encrypted: false, + ReplicationStatus: "disabled", + TenantID: tenantID, +} + +var structVolume2 = volumes.Volume{ + ID: idVolume2, + Status: "available", + Size: 40, + AvailabilityZone: az, + CreatedAt: timeCreatedAt, + Attachments: []volumes.Attachment{}, + Name: "volume 2", + Description: "test volume 2", + VolumeType: "nfsdriver", + SnapshotID: "", + SourceVolID: "", + Metadata: map[string]string{}, + UserID: "2a5719084bc9457c93e659f4f13c6bfc", + Bootable: "true", + Encrypted: false, + ReplicationStatus: "disabled", + TenantID: tenantID, +} + +var expectedVolumesSlice = []volumes.Volume{ + structVolume1, + structVolume2, +} + +var getResponse = fmt.Sprintf(`{ + "volume": { + "id": "%s", + "name": "%s", + "status": "in-use", + "size": %d, + "availability_zone": "%s", + "created_at": "%s", + "os-vol-tenant-attr:tenant_id": "%s", + "description": "%s", + "attachments": [{ + "host_name": null, + "device": "/dev/vdb", + "server_id": "%s", + "id": "%s", + "volume_id": "%s" + }], + "links": [{ + "href": "dummy_self_link", + "rel": "self" + }, { + "href": "dummy_bookmark_link", + "rel": "bookmark" + }], + "encrypted": false, + "os-volume-replication:extended_status": null, + "volume_type": "nfsdriver", + "snapshot_id": null, + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "metadata": { + "readonly": "False", + "attached_mode": "rw" + }, + "volume_image_metadata": { + ".edition": "none", + ".major.version": "7", + ".official_image_template": "CentOS-7.1-1503_64_virtual-server_12", + "container_format": "bare", + "min_ram": "0", + "disk_format": "qcow2", + ".is_license": "False", + "image_name": "CentOS-7.1-1503_64_virtual-server_12", + "image_id": "df1944a7-ca45-4709-9ec6-e31664133650", + ".os.type": "centos", + ".enable.download": "True", + "checksum": "a828b6ba68b9d13d2da0a0cb3cfaa950", + "min_disk": "15", + ".service.type": "virtual-server", + ".virtual_server.os.pod": "other", + ".minor.version": "1-1503", + "size": "461504512" + }, + "source_volid": null, + "consistencygroup_id": null, + "bootable": "true", + "os-volume-replication:driver_data": null, + "replication_status": "disabled" + } +}`, + idVolume1, + nameVolume1, + sizeVolume1, + az, + createdAt, + tenantID, + descriptionVolume1, + instanceID, + idVolume1, + idVolume1, +) + +var createRequest = fmt.Sprintf(`{ + "volume": { + "size": 15, + "availability_zone": "%s", + "description": "%s", + "name": "%s", + "imageRef": "dummyimage" + } +}`, + az, + descriptionVolume1, + nameVolume1, +) + +var createResponse = fmt.Sprintf(`{ + "volume": { + "status": "creating", + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "attachments": [], + "links": [{ + "href": "https://cinder-jp4-ecl.api.ntt.com/v2/9ee80f2a926c49f88f166af47df4e9f5/volumes/251df9eb-c088-4e71-808b-75a690e8814b", + "rel": "self" + }, { + "href": "https://cinder-jp4-ecl.api.ntt.com/9ee80f2a926c49f88f166af47df4e9f5/volumes/251df9eb-c088-4e71-808b-75a690e8814b", + "rel": "bookmark" + }], + "availability_zone": "%s", + "bootable": "false", + "encrypted": false, + "created_at": "2019-02-06T08:06:57.581271", + "description": "%s", + "volume_type": "nfsdriver", + "name": "%s", + "replication_status": "disabled", + "consistencygroup_id": null, + "source_volid": null, + "snapshot_id": null, + "metadata": {}, + "id": "%s", + "size": 15 + } +}`, az, + descriptionVolume1, + nameVolume1, + idVolume1, +) + +var updateResponse = fmt.Sprintf(`{ + "volume": { + "status": "available", + "user_id": "2a5719084bc9457c93e659f4f13c6bfc", + "attachments": [], + "links": [{ + "href": "https://cinder-jp4-ecl.api.ntt.com/v2/9ee80f2a926c49f88f166af47df4e9f5/volumes/7e0b432b-c922-49d7-b85a-28ac88164328", + "rel": "self" + }, { + "href": "https://cinder-jp4-ecl.api.ntt.com/9ee80f2a926c49f88f166af47df4e9f5/volumes/7e0b432b-c922-49d7-b85a-28ac88164328", + "rel": "bookmark" + }], + "availability_zone": "%s", + "bootable": "true", + "encrypted": false, + "created_at": "2019-02-06T08:08:32.000000", + "description": "%s", + "volume_type": "nfsdriver", + "name": "%s", + "replication_status": "disabled", + "consistencygroup_id": null, + "source_volid": null, + "snapshot_id": null, + "metadata": {}, + "id": "%s", + "size": 40 + } +}`, + az, + descriptionVolume1Update, + nameVolume1Update, + idVolume1, +) diff --git a/v4/ecl/computevolume/v2/volumes/testing/requests_test.go b/v4/ecl/computevolume/v2/volumes/testing/requests_test.go new file mode 100644 index 0000000..2c9568a --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/testing/requests_test.go @@ -0,0 +1,133 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/computevolume/v2/volumes" + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListVolumeAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listResponse) + }) + + allPages, err := volumes.List(fakeclient.ServiceClient(), &volumes.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := volumes.ExtractVolumes(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expectedVolumesSlice, actual) + +} + +func TestGetVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, getResponse) + }) + + v, err := volumes.Get(fakeclient.ServiceClient(), idVolume1).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &expectedVolumesSlice[0], v) +} + +func TestCreateVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, createResponse) + }) + + options := &volumes.CreateOpts{ + Size: 15, + AvailabilityZone: az, + Description: descriptionVolume1, + Name: nameVolume1, + ImageID: "dummyimage", + } + v, err := volumes.Create(fakeclient.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.AvailabilityZone, az) + th.AssertEquals(t, v.Size, 15) + th.AssertEquals(t, v.ID, idVolume1) + th.AssertEquals(t, v.Description, descriptionVolume1) +} + +func TestUpdatVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, updateResponse) + }) + + name := nameVolume1Update + description := descriptionVolume1Update + metadata := map[string]string{} + + updateOpts := volumes.UpdateOpts{ + Name: &name, + Description: &description, + Metadata: &metadata, + } + + v, err := volumes.Update(fakeclient.ServiceClient(), idVolume1, updateOpts).Extract() + + blankMeta := map[string]string{} + th.AssertNoErr(t, err) + th.CheckEquals(t, nameVolume1Update, v.Name) + th.CheckEquals(t, descriptionVolume1Update, v.Description) + th.CheckDeepEquals(t, &blankMeta, &v.Metadata) +} + +func TestDeleteVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusAccepted) + }) + + res := volumes.Delete(fakeclient.ServiceClient(), idVolume1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/computevolume/v2/volumes/urls.go b/v4/ecl/computevolume/v2/volumes/urls.go new file mode 100644 index 0000000..56d1b8b --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/nttcom/eclcloud/v4" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/v4/ecl/computevolume/v2/volumes/util.go b/v4/ecl/computevolume/v2/volumes/util.go new file mode 100644 index 0000000..5ea926e --- /dev/null +++ b/v4/ecl/computevolume/v2/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v4/ecl/dedicated_hypervisor/v1/license_types/doc.go b/v4/ecl/dedicated_hypervisor/v1/license_types/doc.go new file mode 100644 index 0000000..f7ed6ce --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/license_types/doc.go @@ -0,0 +1,20 @@ +/* +Package license_types manages and retrieves license type in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List License types + + allPages, err := license_types.List(dhClient).AllPages() + if err != nil { + panic(err) + } + + allLicenseTypes, err := license_types.ExtractLicenseTypes(allPages) + if err != nil { + panic(err) + } + + for _, licenseType := range allLicenseTypes { + fmt.Printf("%+v\n", licenseType) + } +*/ +package license_types diff --git a/v4/ecl/dedicated_hypervisor/v1/license_types/requests.go b/v4/ecl/dedicated_hypervisor/v1/license_types/requests.go new file mode 100644 index 0000000..f787015 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/license_types/requests.go @@ -0,0 +1,14 @@ +package license_types + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// List retrieves a list of LicenseTypes. +func List(client *eclcloud.ServiceClient) pagination.Pager { + url := listURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return LicenseTypePage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/license_types/results.go b/v4/ecl/dedicated_hypervisor/v1/license_types/results.go new file mode 100644 index 0000000..ff1bf99 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/license_types/results.go @@ -0,0 +1,35 @@ +package license_types + +import ( + "github.com/nttcom/eclcloud/v4/pagination" +) + +// LicenseType represents guest image license information. +type LicenseType struct { + ID string `json:"id"` + Name string `json:"name"` + HasLicenseKey bool `json:"has_license_key"` + Unit string `json:"unit"` + LicenseSwitch bool `json:"license_switch"` + Description string `json:"description"` +} + +// LicenseTypePage is a single page of LicenseType results. +type LicenseTypePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of LicenseTypes contains any results. +func (r LicenseTypePage) IsEmpty() (bool, error) { + licenses, err := ExtractLicenseTypes(r) + return len(licenses) == 0, err +} + +// ExtractLicenseTypes returns a slice of LicenseTypes contained in a single page of results. +func ExtractLicenseTypes(r pagination.Page) ([]LicenseType, error) { + var s struct { + LicenseTypes []LicenseType `json:"license_types"` + } + err := (r.(LicenseTypePage)).ExtractInto(&s) + return s.LicenseTypes, err +} diff --git a/v4/ecl/dedicated_hypervisor/v1/license_types/testing/fixtures.go b/v4/ecl/dedicated_hypervisor/v1/license_types/testing/fixtures.go new file mode 100644 index 0000000..65a5558 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/license_types/testing/fixtures.go @@ -0,0 +1,73 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/license_types" + + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// ListResult provides a single page of LicenseType results. +const ListResult = ` +{ + "license_types": [ + { + "description": "Windows Server 2016 Standard Edition", + "has_license_key": false, + "id": "9c54c437-5f0f-46f5-8270-ddf450a44135", + "license_switch": true, + "name": "Windows Server 2016 Standard Edition", + "unit": "VM" + }, + { + "description": "vCenter Server 6.x Standard", + "has_license_key": true, + "id": "e37c05ba-8fd0-493e-93d2-688833363a74", + "license_switch": false, + "name": "vCenter Server 6.x Standard", + "unit": "License" + } + ] +} +` + +// FirstLicenseType is the first LicenseType in the List request. +var FirstLicenseType = license_types.LicenseType{ + ID: "9c54c437-5f0f-46f5-8270-ddf450a44135", + Name: "Windows Server 2016 Standard Edition", + HasLicenseKey: false, + Unit: "VM", + LicenseSwitch: true, + Description: "Windows Server 2016 Standard Edition", +} + +// SecondLicenseType is the second LicenseType in the List request. +var SecondLicenseType = license_types.LicenseType{ + ID: "e37c05ba-8fd0-493e-93d2-688833363a74", + Name: "vCenter Server 6.x Standard", + HasLicenseKey: true, + Unit: "License", + LicenseSwitch: false, + Description: "vCenter Server 6.x Standard", +} + +// ExpectedLicenseTypesSlice is the slice of LicenseTypes expected to be returned from ListResult. +var ExpectedLicenseTypesSlice = []license_types.LicenseType{FirstLicenseType, SecondLicenseType} + +// HandleListLicenseTypesSuccessfully creates an HTTP handler at `/license_types` on the +// test handler mux that responds with a list of two LicenseType. +func HandleListLicenseTypesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/license_types", 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, ListResult) + }) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/license_types/testing/requests_test.go b/v4/ecl/dedicated_hypervisor/v1/license_types/testing/requests_test.go new file mode 100644 index 0000000..63017e3 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/license_types/testing/requests_test.go @@ -0,0 +1,43 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/license_types" + + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListLicenseTypes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicenseTypesSuccessfully(t) + + count := 0 + err := license_types.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := license_types.ExtractLicenseTypes(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedLicenseTypesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListLicenseTypesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicenseTypesSuccessfully(t) + + allPages, err := license_types.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + actual, err := license_types.ExtractLicenseTypes(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedLicenseTypesSlice, actual) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/license_types/urls.go b/v4/ecl/dedicated_hypervisor/v1/license_types/urls.go new file mode 100644 index 0000000..89e07be --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/license_types/urls.go @@ -0,0 +1,7 @@ +package license_types + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("license_types") +} diff --git a/v4/ecl/dedicated_hypervisor/v1/licenses/doc.go b/v4/ecl/dedicated_hypervisor/v1/licenses/doc.go new file mode 100644 index 0000000..a35c2aa --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/licenses/doc.go @@ -0,0 +1,44 @@ +/* +Package licenses manages and retrieves license in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List Licenses + + listOpts := licenses.ListOpts{ + LicenseType: "vCenter Server 6.x Standard", + } + + allPages, err := licenses.List(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLicenses, err := licenses.ExtractLicenses(allPages) + if err != nil { + panic(err) + } + + for _, license := range allLicenses { + fmt.Printf("%+v\n", license) + } + +Example to Create a License + + createOpts := licenses.CreateOpts{ + LicenseType: "vCenter Server 6.x Standard", + } + + result := licenses.Create(dhClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a license + + licenseID := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + result := licenses.Delete(dhClient, licenseID) + if result.Err != nil { + panic(result.Err) + } +*/ +package licenses diff --git a/v4/ecl/dedicated_hypervisor/v1/licenses/requests.go b/v4/ecl/dedicated_hypervisor/v1/licenses/requests.go new file mode 100644 index 0000000..5588dca --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/licenses/requests.go @@ -0,0 +1,76 @@ +package licenses + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToResourceListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + LicenseType string `q:"license_type"` +} + +// ToResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Licenses. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return LicensePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToResourceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a License. +type CreateOpts struct { + LicenseType string `json:"license_type"` +} + +// ToResourceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new License. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a License. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/v4/ecl/dedicated_hypervisor/v1/licenses/results.go b/v4/ecl/dedicated_hypervisor/v1/licenses/results.go new file mode 100644 index 0000000..dfe8068 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/licenses/results.go @@ -0,0 +1,63 @@ +package licenses + +import ( + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// License represents guest image license key information. +type License struct { + ID string `json:"id"` + Key string `json:"key"` + AssignedFrom time.Time `json:"assigned_from"` + ExpiresAt *time.Time `json:"expires_at"` + LicenseType string `json:"license_type"` +} + +type commonResult struct { + eclcloud.Result +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a License. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// LicensePage is a single page of License results. +type LicensePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Licenses contains any results. +func (r LicensePage) IsEmpty() (bool, error) { + licenses, err := ExtractLicenses(r) + return len(licenses) == 0, err +} + +// ExtractLicenses returns a slice of Licenses contained in a single page of +// results. +func ExtractLicenses(r pagination.Page) ([]License, error) { + var s struct { + Licenses []License `json:"licenses"` + } + err := (r.(LicensePage)).ExtractInto(&s) + return s.Licenses, err +} + +// ExtractLicenseInfo interprets any commonResult as a License. +func (r commonResult) ExtractLicenseInfo() (*License, error) { + var s struct { + License *License `json:"license"` + } + err := r.ExtractInto(&s) + return s.License, err +} diff --git a/v4/ecl/dedicated_hypervisor/v1/licenses/testing/fixtures.go b/v4/ecl/dedicated_hypervisor/v1/licenses/testing/fixtures.go new file mode 100644 index 0000000..bd55612 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/licenses/testing/fixtures.go @@ -0,0 +1,114 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/licenses" + + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// ListResult provides a single page of License results. +const ListResult = ` +{ + "licenses": [ + { + "id": "02471b45-3de0-4fc8-8469-a7cc52c378df", + "key": "5H69L-8C3D7-K8292-03926-CREMN", + "assigned_from": "2017-04-27T09:20:47Z", + "expires_at": null, + "license_type": "vCenter Server 6.x Standard" + }, + { + "id": "0801a388-68e8-4e41-9158-73571117c915", + "key": "0021L-8CJ47-2829A-0A8K2-CXN4J", + "assigned_from": "2017-06-01T04:13:31Z", + "expires_at": null, + "license_type": "vCenter Server 6.x Standard" + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "license": { + "id": "0801a388-68e8-4e41-9158-73571117c915", + "key": "0021L-8CJ47-2829A-0A8K2-CXN4J", + "assigned_from": "2017-06-01T04:13:31Z", + "expires_at": null, + "license_type": "vCenter Server 6.x Standard" + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "license_type": "vCenter Server 6.x Standard" +} +` + +// FirstLicense is the first License in the List request. +var FirstLicense = licenses.License{ + ID: "02471b45-3de0-4fc8-8469-a7cc52c378df", + Key: "5H69L-8C3D7-K8292-03926-CREMN", + AssignedFrom: time.Date(2017, 4, 27, 9, 20, 47, 0, time.UTC), + ExpiresAt: nil, + LicenseType: "vCenter Server 6.x Standard", +} + +// SecondLicense is the second License in the List request. +var SecondLicense = licenses.License{ + ID: "0801a388-68e8-4e41-9158-73571117c915", + Key: "0021L-8CJ47-2829A-0A8K2-CXN4J", + AssignedFrom: time.Date(2017, 6, 1, 4, 13, 31, 0, time.UTC), + ExpiresAt: nil, + LicenseType: "vCenter Server 6.x Standard", +} + +// ExpectedLicensesSlice is the slice of Licenses expected to be returned from ListResult. +var ExpectedLicensesSlice = []licenses.License{FirstLicense, SecondLicense} + +// HandleListLicenseSuccessfully creates an HTTP handler at `/licenses` on the +// test handler mux that responds with a list of two Licenses. +func HandleListLicensesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/licenses", 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, ListResult) + }) +} + +// HandleCreateLicenseSuccessfully creates an HTTP handler at `/licenses` on the +// test handler mux that tests License creation. +func HandleCreateLicenseSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/licenses", 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, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetResult) + }) +} + +// HandleDeleteLicenseSuccessfully creates an HTTP handler at `/licenses` on the +// test handler mux that tests License deletion. +func HandleDeleteLicenseSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/licenses/%s", FirstLicense.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/licenses/testing/requests_test.go b/v4/ecl/dedicated_hypervisor/v1/licenses/testing/requests_test.go new file mode 100644 index 0000000..0410aab --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/licenses/testing/requests_test.go @@ -0,0 +1,65 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/licenses" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListLicenses(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicensesSuccessfully(t) + + count := 0 + err := licenses.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := licenses.ExtractLicenses(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedLicensesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListLicensesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListLicensesSuccessfully(t) + + allPages, err := licenses.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := licenses.ExtractLicenses(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedLicensesSlice, actual) +} + +func TestCreateLicense(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateLicenseSuccessfully(t) + + createOpts := licenses.CreateOpts{ + LicenseType: SecondLicense.LicenseType, + } + + actual, err := licenses.Create(client.ServiceClient(), createOpts).ExtractLicenseInfo() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondLicense, *actual) +} + +func TestDeleteLicense(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteLicenseSuccessfully(t) + + res := licenses.Delete(client.ServiceClient(), FirstLicense.ID) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/licenses/urls.go b/v4/ecl/dedicated_hypervisor/v1/licenses/urls.go new file mode 100644 index 0000000..168d1d0 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/licenses/urls.go @@ -0,0 +1,15 @@ +package licenses + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("licenses") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("licenses") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("licenses", id) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/servers/doc.go b/v4/ecl/dedicated_hypervisor/v1/servers/doc.go new file mode 100644 index 0000000..5bc3134 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/servers/doc.go @@ -0,0 +1,131 @@ +/* +Package servers manages and retrieves servers in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List servers + + listOpts := servers.ListOpts{ + Limit: 10, + } + + allPages, err := servers.List(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to List servers details + + listOpts := servers.ListOpts{ + Limit: 10, + } + + allPages, err := servers.ListDetails(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Get a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + result := servers.Get(dhClient, serverID) + if result.Err != nil { + panic(result.Err) + } + + server, err := result.Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", server) + +Example to Create a server + + createOpts := servers.CreateOpts{ + Name: "test", + Networks: []servers.Network{ + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + }, + ImageRef: "dfd25820-b368-4012-997b-29a6d0cf8518", + FlavorRef: "a830b61c-3155-4a61-b7ed-c450862845e6", + } + + result := servers.Create(dhClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + result := servers.Delete(dhClient, serverID) + if result.Err != nil { + panic(result.Err) + } + +Example to Add license to a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + addLicenseOpts := servers.AddLicenseOpts{ + VmName: "Alice", + LicenseTypes: []string{ + "Windows Server", + "SQL Server Standard 2014", + }, + } + + result := servers.AddLicense(dhClient, serverID, addLicenseOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Get result for add license to a server + + serverID := "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + + getAddLicenseResultOpts := servers.GetAddLicenseResultOpts{ + JobID: AddLicenseJob.JobID, + } + + result := servers.GetAddLicenseResult(dhClient, serverID, getAddLicenseResultOpts) + if result.Err != nil { + panic(result.Err) + } + + job, err := result.Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", job) +*/ +package servers diff --git a/v4/ecl/dedicated_hypervisor/v1/servers/requests.go b/v4/ecl/dedicated_hypervisor/v1/servers/requests.go new file mode 100644 index 0000000..214e7ab --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/servers/requests.go @@ -0,0 +1,160 @@ +package servers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToResourceListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + ChangesSince string `q:"changes-since"` + Marker string `q:"marker"` + Limit int `q:"limit"` + Name string `q:"name"` + Image string `q:"image"` + Flavor string `q:"flavor"` + Status string `q:"status"` +} + +// ToResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Servers. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListDetails retrieves a list of Servers in details. +func ListDetails(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailsURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of a Server. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToResourceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a Server. +type CreateOpts struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Networks []Network `json:"networks"` + AdminPass string `json:"adminPass,omitempty"` + ImageRef string `json:"imageRef"` + FlavorRef string `json:"flavorRef"` + AvailabilityZone string `json:"availability_zone,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +type Network struct { + UUID string `json:"uuid"` + Port string `json:"port,omitempty"` + FixedIP string `json:"fixed_ip,omitempty"` + Plane string `json:"plane"` + SegmentationID int `json:"segmentation_id"` +} + +// ToResourceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "server") +} + +// Create creates a new Server. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a Server. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +type AddLicenseOpts struct { + VmName string `json:"vm_name,omitempty"` + VmID string `json:"vm_id,omitempty"` + LicenseTypes []string `json:"license_types"` +} + +func (opts AddLicenseOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "add-license-to-vm") +} + +func AddLicense(client *eclcloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r AddLicenseResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, serverID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +type GetAddLicenseResultOpts struct { + JobID string `json:"job_id"` +} + +func (opts GetAddLicenseResultOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "get-result-for-add-license-to-vm") +} + +func GetAddLicenseResult(client *eclcloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r AddLicenseResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, serverID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/dedicated_hypervisor/v1/servers/results.go b/v4/ecl/dedicated_hypervisor/v1/servers/results.go new file mode 100644 index 0000000..e5e3fc4 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/servers/results.go @@ -0,0 +1,203 @@ +package servers + +import ( + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Server represents dedicated hypervisor server information. +type Server struct { + ID string `json:"id"` + Name string `json:"name"` + ImageRef string `json:"imageRef"` + Description *string `json:"description"` + Status string `json:"status"` + HypervisorType string `json:"hypervisor_type"` + BaremetalServer BaremetalServer `json:"baremetal_server"` + Links []Link `json:"links"` + AdminPass string `json:"adminPass"` +} + +type BaremetalServer struct { + PowerState string `json:"OS-EXT-STS:power_state"` + TaskState string `json:"OS-EXT-STS:task_state"` + VMState string `json:"OS-EXT-STS:vm_state"` + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` + Created time.Time `json:"created"` + Flavor Flavor `json:"flavor"` + ID string `json:"id"` + Image Image `json:"image"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Progress int `json:"progress"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + Updated time.Time `json:"updated"` + UserID string `json:"user_id"` + NicPhysicalPorts []NicPhysicalPort `json:"nic_physical_ports"` + ChassisStatus ChassisStatus `json:"chassis-status"` + Links []Link `json:"links"` + RaidArrays []RaidArray `json:"raid_arrays"` + LvmVolumeGroups []LvmVolumeGroup `json:"lvm_volume_groups"` + Filesystems []Filesystem `json:"filesystems"` + MediaAttachments []MediaAttachment `json:"media_attachments"` + ManagedByService string `json:"managed_by_service"` + ManagedServiceResourceID string `json:"managed_service_resource_id"` +} + +type Flavor struct { + ID string `json:"id"` + Links []Link `json:"links"` +} + +type Image struct { + ID string `json:"id"` + Links []Link `json:"links"` +} + +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +type NicPhysicalPort struct { + ID string `json:"id"` + MACAddr string `json:"mac_addr"` + NetworkPhysicalPortID string `json:"network_physical_port_id"` + Plane string `json:"plane"` + AttachedPorts []Port `json:"attached_ports"` + HardwareID string `json:"hardware_id"` +} + +type Port struct { + PortID string `json:"port_id"` + NetworkID string `json:"network_id"` + FixedIPs []FixedIP `json:"fixed_ips"` +} + +type FixedIP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +type ChassisStatus struct { + ChassisPower bool `json:"chassis-power"` + PowerSupply bool `json:"power-supply"` + CPU bool `json:"cpu"` + Memory bool `json:"memory"` + Fan bool `json:"fan"` + Disk int `json:"disk"` + Nic bool `json:"nic"` + SystemBoard bool `json:"system-board"` + Etc bool `json:"etc"` + Console bool `json:"console"` +} + +type RaidArray struct { + PrimaryStorage bool `json:"primary_storage"` + Partitions []Partition `json:"partitions"` + RaidCardHardwareID string `json:"raid_card_hardware_id"` + DiskHardwareIDs []string `json:"disk_hardware_ids"` +} + +type Partition struct { + Lvm bool `json:"lvm"` + Size string `json:"size"` + PartitionLabel string `json:"partition_label"` +} + +type LvmVolumeGroup struct { + VgLabel string `json:"vg_label"` + PhysicalVolumePartitionLabels []string `json:"physical_volume_partition_labels"` + LogicalVolumes []LogicalVolume `json:"logical_volumes"` +} + +type LogicalVolume struct { + LvLabel string `json:"lv_label"` + Size string `json:"size"` +} + +type Filesystem struct { + Label string `json:"label"` + MountPoint string `json:"mount_point"` + FsType string `json:"fs_type"` +} + +type MediaAttachment struct { + Image Image `json:"image"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Server. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Server. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ServerPage is a single page of Server results. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Servers contains any results. +func (r ServerPage) IsEmpty() (bool, error) { + servers, err := ExtractServers(r) + return len(servers) == 0, err +} + +// ExtractServers returns a slice of Servers contained in a single page of +// results. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s struct { + Servers []Server `json:"servers"` + } + err := (r.(ServerPage)).ExtractInto(&s) + return s.Servers, err +} + +// Extract interprets any commonResult as a Server. +func (r commonResult) Extract() (*Server, error) { + var s struct { + Server *Server `json:"server"` + } + err := r.ExtractInto(&s) + return s.Server, err +} + +type Job struct { + JobID string `json:"job_id"` + Status string `json:"status"` + RequestedParam RequestedParam `json:"requested_param"` +} + +type RequestedParam struct { + VmName string `json:"vm_name"` + LicenseTypes []string `json:"license_types"` +} + +type AddLicenseResult struct { + eclcloud.Result +} + +func (r AddLicenseResult) Extract() (*Job, error) { + var job Job + err := r.ExtractInto(&job) + return &job, err +} diff --git a/v4/ecl/dedicated_hypervisor/v1/servers/testing/fixtures.go b/v4/ecl/dedicated_hypervisor/v1/servers/testing/fixtures.go new file mode 100644 index 0000000..5350fd4 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/servers/testing/fixtures.go @@ -0,0 +1,1119 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/servers" + + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// ListResult provides a single page of Server results. +const ListResult = ` +{ + "servers": [ + { + "id": "194573e4-8f53-4ee4-806f-d9b2db74a380", + "name": "GP2v1", + "links": [ + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + "rel": "self" + }, + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + "rel": "bookmark" + } + ], + "baremetal_server": { + "id": "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "bookmark" + } + ], + "name": "GP2v1" + } + }, + { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "name": "test", + "links": [ + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "self" + }, + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "bookmark" + } + ], + "baremetal_server": { + "id": "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "bookmark" + } + ], + "name": "test" + } + } + ] +} +` + +// ListDetailsResult provides a single page of Server results in details. +const ListDetailsResult = ` +{ + "servers": [ + { + "id": "194573e4-8f53-4ee4-806f-d9b2db74a380", + "name": "GP2v1", + "imageRef": "293063f6-8986-4b79-becd-7a6d28794bb8", + "description": null, + "status": "ACTIVE", + "hypervisor_type": "vsphere_esxi", + "baremetal_server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "groupb", + "progress": 100, + "created": "2019-10-18T07:42:35Z", + "flavor": { + "id": "303b4993-cf29-4301-abd0-99512b5413a5", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + "rel": "bookmark" + } + ] + }, + "id": "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "image": { + "id": "02441adc-0d9a-4e9d-b359-ce23413e7ea7", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/02441adc-0d9a-4e9d-b359-ce23413e7ea7", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "GP2v1", + "status": "ACTIVE", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "updated": "2019-10-18T07:44:18Z", + "user_id": "55891ce6a3cb4bb0833514667d67288c", + "raid_arrays": [ + { + "primary_storage": true, + "partitions": null, + "raid_card_hardware_id": "24184dcf-dc76-4ea2-a34e-bccc6c11d5be", + "disk_hardware_ids": [ + "4de7d3df-8e2f-4193-98dc-145f78df29a2", + "de158fab-7bc2-49e3-98d1-9db5451d43e3", + "97087307-cab2-40cb-a84c-86d98da0f393" + ] + } + ], + "lvm_volume_groups": null, + "filesystems": null, + "nic_physical_ports": [ + { + "id": "b49c0624-e89a-469f-8c90-7a27ee1f61cb", + "mac_addr": "8C:DC:D4:B7:41:48", + "plane": "DATA", + "network_physical_port_id": "4cfbe3b2-a502-485f-82fa-a0949396e567", + "hardware_id": "8e1e2fe0-60a7-4211-a891-80e808426708", + "attached_ports": [ + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "8808acc2-d930-40fb-b382-ce7074baef83", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.11" + } + ] + }, + { + "network_id": "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + "port_id": "9d3baa16-e0e5-4e50-9677-08dd338e0c14", + "fixed_ips": [ + { + "subnet_id": "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + "ip_address": "192.168.4.3" + } + ] + } + ] + }, + { + "id": "8bea93c4-721b-480c-8713-f2a4b6e5dbad", + "mac_addr": "8C:DC:D4:B7:41:49", + "plane": "STORAGE", + "network_physical_port_id": "ab38075d-128f-4f3d-a16a-c6426375a380", + "hardware_id": "8e1e2fe0-60a7-4211-a891-80e808426708", + "attached_ports": [] + }, + { + "id": "a7fbca5e-ff49-4ddb-8659-02ec462f98ec", + "mac_addr": "8C:DC:D4:B7:45:89", + "plane": "STORAGE", + "network_physical_port_id": "9ef62803-7848-460c-9dae-17fd02606a26", + "hardware_id": "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + "attached_ports": [] + }, + { + "id": "71af4de1-0c9b-4870-8c95-c7b9b4115bb8", + "mac_addr": "8C:DC:D4:B7:45:88", + "plane": "DATA", + "network_physical_port_id": "ad92f33a-eac4-408d-bc27-22d91eccd465", + "hardware_id": "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + "attached_ports": [ + { + "network_id": "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + "port_id": "705bde94-7189-40b1-b8a2-188e6cc3c546", + "fixed_ips": [ + { + "subnet_id": "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + "ip_address": "192.168.4.4" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "7b860eb4-0eb6-4c2a-873e-ccefd6029d97", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.12" + } + ] + } + ] + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true, + "console": true + }, + "media_attachments": [], + "managed_by_service": "dedicated-hypervisor", + "managed_service_resource_id": "194573e4-8f53-4ee4-806f-d9b2db74a380" + } + }, + { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "name": "test", + "imageRef": "dfd25820-b368-4012-997b-29a6d0cf8518", + "description": "test", + "status": "ACTIVE", + "hypervisor_type": "vsphere_esxi", + "baremetal_server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "groupb", + "progress": 100, + "created": "2019-10-10T04:11:41Z", + "flavor": { + "id": "a830b61c-3155-4a61-b7ed-c450862845e6", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/a830b61c-3155-4a61-b7ed-c450862845e6", + "rel": "bookmark" + } + ] + }, + "id": "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "image": { + "id": "112a26a0-ff25-4513-afe1-407e41b0a48b", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/112a26a0-ff25-4513-afe1-407e41b0a48b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "test", + "status": "ACTIVE", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "updated": "2019-10-10T04:14:08Z", + "user_id": "55891ce6a3cb4bb0833514667d67288c", + "raid_arrays": [ + { + "primary_storage": true, + "partitions": null, + "raid_card_hardware_id": "bdfb75d1-194d-426d-b288-f588dfa5ac49", + "disk_hardware_ids": [ + "76649053-863e-4533-86e3-f194a79485a6", + "a25827e3-67da-47be-ba96-849ab4685a1d" + ] + } + ], + "lvm_volume_groups": null, + "filesystems": null, + "nic_physical_ports": [ + { + "id": "a2f63380-6c77-4cd5-8868-e3556ffd35ce", + "mac_addr": "48:DF:37:90:B4:58", + "plane": "DATA", + "network_physical_port_id": "d8e40a51-f1e2-4681-8953-9fe1e9992c42", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "30fc1c27-fb5f-4955-94d0-a56cd28d09e8", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.10" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "aa6c61f4-db8a-44c7-a91c-7e636dac1dc6", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.9" + } + ] + } + ] + }, + { + "id": "b01dfdb0-f247-47d8-8224-c257aa3265e9", + "mac_addr": "48:DF:37:90:B4:50", + "plane": "STORAGE", + "network_physical_port_id": "00dfea92-5c5b-4860-aa05-efef6c2bb2af", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [] + }, + { + "id": "f4355e8e-39fc-48bd-a283-a2dbef8a2e32", + "mac_addr": "48:DF:37:82:B0:A0", + "plane": "STORAGE", + "network_physical_port_id": "cf798cc0-c869-45d5-a5a7-bcc578a300b0", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [] + }, + { + "id": "5ef177fd-888c-4fae-9925-a8920beb07cb", + "mac_addr": "48:DF:37:82:B0:A8", + "plane": "DATA", + "network_physical_port_id": "2bbbb516-c75a-42b2-8a46-9cb5f26c219e", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "4e329a01-2cf4-4028-9259-03b7aa145cb6", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.20" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "a256b4a1-3ae3-4102-a14e-987ae1610f97", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.10" + } + ] + } + ] + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true, + "console": true + }, + "media_attachments": [], + "managed_by_service": "dedicated-hypervisor", + "managed_service_resource_id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + } + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "server": { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "name": "test", + "imageRef": "dfd25820-b368-4012-997b-29a6d0cf8518", + "description": "test", + "status": "ACTIVE", + "hypervisor_type": "vsphere_esxi", + "baremetal_server": { + "OS-EXT-STS:power_state": "RUNNING", + "OS-EXT-STS:task_state": "None", + "OS-EXT-STS:vm_state": "ACTIVE", + "OS-EXT-AZ:availability_zone": "groupb", + "progress": 100, + "created": "2019-10-10T04:11:41Z", + "flavor": { + "id": "a830b61c-3155-4a61-b7ed-c450862845e6", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/a830b61c-3155-4a61-b7ed-c450862845e6", + "rel": "bookmark" + } + ] + }, + "id": "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "image": { + "id": "112a26a0-ff25-4513-afe1-407e41b0a48b", + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/112a26a0-ff25-4513-afe1-407e41b0a48b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "self" + }, + { + "href": "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "test", + "status": "ACTIVE", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "updated": "2019-10-10T04:14:08Z", + "user_id": "55891ce6a3cb4bb0833514667d67288c", + "raid_arrays": [ + { + "primary_storage": true, + "partitions": null, + "raid_card_hardware_id": "bdfb75d1-194d-426d-b288-f588dfa5ac49", + "disk_hardware_ids": [ + "76649053-863e-4533-86e3-f194a79485a6", + "a25827e3-67da-47be-ba96-849ab4685a1d" + ] + } + ], + "lvm_volume_groups": null, + "filesystems": null, + "nic_physical_ports": [ + { + "id": "a2f63380-6c77-4cd5-8868-e3556ffd35ce", + "mac_addr": "48:DF:37:90:B4:58", + "plane": "DATA", + "network_physical_port_id": "d8e40a51-f1e2-4681-8953-9fe1e9992c42", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "30fc1c27-fb5f-4955-94d0-a56cd28d09e8", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.10" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "aa6c61f4-db8a-44c7-a91c-7e636dac1dc6", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.9" + } + ] + } + ] + }, + { + "id": "b01dfdb0-f247-47d8-8224-c257aa3265e9", + "mac_addr": "48:DF:37:90:B4:50", + "plane": "STORAGE", + "network_physical_port_id": "00dfea92-5c5b-4860-aa05-efef6c2bb2af", + "hardware_id": "be2d30d6-f891-4200-b827-95f229fb8c6b", + "attached_ports": [] + }, + { + "id": "f4355e8e-39fc-48bd-a283-a2dbef8a2e32", + "mac_addr": "48:DF:37:82:B0:A0", + "plane": "STORAGE", + "network_physical_port_id": "cf798cc0-c869-45d5-a5a7-bcc578a300b0", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [] + }, + { + "id": "5ef177fd-888c-4fae-9925-a8920beb07cb", + "mac_addr": "48:DF:37:82:B0:A8", + "plane": "DATA", + "network_physical_port_id": "2bbbb516-c75a-42b2-8a46-9cb5f26c219e", + "hardware_id": "84c74a86-7045-4284-80f9-0e7aff5d27ad", + "attached_ports": [ + { + "network_id": "94055904-6b2c-4839-a14a-c61c93a8bc48", + "port_id": "4e329a01-2cf4-4028-9259-03b7aa145cb6", + "fixed_ips": [ + { + "subnet_id": "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + "ip_address": "2.1.1.20" + } + ] + }, + { + "network_id": "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + "port_id": "a256b4a1-3ae3-4102-a14e-987ae1610f97", + "fixed_ips": [ + { + "subnet_id": "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + "ip_address": "169.254.0.10" + } + ] + } + ] + } + ], + "chassis-status": { + "chassis-power": true, + "power-supply": true, + "cpu": true, + "memory": true, + "fan": true, + "disk": 0, + "nic": true, + "system-board": true, + "etc": true, + "console": true + }, + "media_attachments": [], + "managed_by_service": "dedicated-hypervisor", + "managed_service_resource_id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5" + } + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "server": { + "imageRef": "dfd25820-b368-4012-997b-29a6d0cf8518", + "name": "test", + "networks": [ + { + "segmentation_id": 6, + "plane": "data", + "uuid": "94055904-6b2c-4839-a14a-c61c93a8bc48" + }, + { + "segmentation_id": 6, + "plane": "data", + "uuid": "94055904-6b2c-4839-a14a-c61c93a8bc48" + } + ], + "flavorRef": "a830b61c-3155-4a61-b7ed-c450862845e6" + } +} +` + +const CreateResponse = ` +{ + "server": { + "id": "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "links": [ + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "self" + }, + { + "href": "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + "rel": "bookmark" + } + ], + "adminPass": "aabbccddeeff" + } +} +` + +const AddLicenseRequest = ` +{ + "add-license-to-vm": { + "vm_name": "Alice", + "license_types": [ + "Windows Server", + "SQL Server Standard 2014" + ] + } +} +` + +const AddLicenseResponse = ` +{ + "job_id": "b4f888dc2b9d4c41bb769cbd" +} +` + +const GetAddLicenseResultRequest = ` +{ + "get-result-for-add-license-to-vm": { + "job_id": "b4f888dc2b9d4c41bb769cbd" + } +} +` + +const GetAddLicenseResultResponse = ` +{ + "job_id": "b4f888dc2b9d4c41bb769cbd", + "status": "COMPLETED", + "requested_param": { + "vm_name": "Alice", + "license_types": ["Windows Server", "SQL Server Standard 2014"] + } +} +` + +// FirstServer is the first resource in the List request. +var FirstServer = servers.Server{ + ID: "194573e4-8f53-4ee4-806f-d9b2db74a380", + Name: "GP2v1", + Links: []servers.Link{ + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + Rel: "self", + }, + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/194573e4-8f53-4ee4-806f-d9b2db74a380", + Rel: "bookmark", + }, + }, + BaremetalServer: servers.BaremetalServer{ + ID: "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Name: "GP2v1", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "bookmark", + }, + }, + }, +} + +// SecondServer is the second resource in the List request. +var SecondServer = servers.Server{ + ID: "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Name: "test", + Links: []servers.Link{ + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//v2/1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Rel: "self", + }, + { + Href: "https://dedicated-hypervisor-jp1-ecl.api.ntt.com/v1.0//1bc271e7a8af4d988ff91612f5b122f8/servers/f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Rel: "bookmark", + }, + }, + BaremetalServer: servers.BaremetalServer{ + ID: "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Name: "test", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "bookmark", + }, + }, + }, +} + +// ExpectedServersSlice is the slice of resources expected to be returned from ListResult. +var ExpectedServersSlice = []servers.Server{FirstServer, SecondServer} + +// FirstServerDetail is the first resource in the List details request. +var FirstServerDetail = servers.Server{ + ID: "194573e4-8f53-4ee4-806f-d9b2db74a380", + Name: "GP2v1", + ImageRef: "293063f6-8986-4b79-becd-7a6d28794bb8", + Description: nil, + Status: "ACTIVE", + HypervisorType: "vsphere_esxi", + BaremetalServer: servers.BaremetalServer{ + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "groupb", + Created: time.Date(2019, 10, 18, 7, 42, 35, 0, time.UTC), + Flavor: servers.Flavor{ + ID: "303b4993-cf29-4301-abd0-99512b5413a5", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/303b4993-cf29-4301-abd0-99512b5413a5", + Rel: "bookmark", + }, + }, + }, + ID: "621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Image: servers.Image{ + ID: "02441adc-0d9a-4e9d-b359-ce23413e7ea7", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/02441adc-0d9a-4e9d-b359-ce23413e7ea7", + Rel: "bookmark", + }, + }, + }, + Metadata: map[string]string{}, + Name: "GP2v1", + Progress: 100, + Status: "ACTIVE", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + Updated: time.Date(2019, 10, 18, 7, 44, 18, 0, time.UTC), + UserID: "55891ce6a3cb4bb0833514667d67288c", + NicPhysicalPorts: []servers.NicPhysicalPort{ + { + ID: "b49c0624-e89a-469f-8c90-7a27ee1f61cb", + MACAddr: "8C:DC:D4:B7:41:48", + Plane: "DATA", + NetworkPhysicalPortID: "4cfbe3b2-a502-485f-82fa-a0949396e567", + HardwareID: "8e1e2fe0-60a7-4211-a891-80e808426708", + AttachedPorts: []servers.Port{ + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "8808acc2-d930-40fb-b382-ce7074baef83", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.11", + }, + }, + }, + { + NetworkID: "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + PortID: "9d3baa16-e0e5-4e50-9677-08dd338e0c14", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + IPAddress: "192.168.4.3", + }, + }, + }, + }, + }, + { + ID: "8bea93c4-721b-480c-8713-f2a4b6e5dbad", + MACAddr: "8C:DC:D4:B7:41:49", + Plane: "STORAGE", + NetworkPhysicalPortID: "ab38075d-128f-4f3d-a16a-c6426375a380", + HardwareID: "8e1e2fe0-60a7-4211-a891-80e808426708", + AttachedPorts: []servers.Port{}, + }, + { + ID: "a7fbca5e-ff49-4ddb-8659-02ec462f98ec", + MACAddr: "8C:DC:D4:B7:45:89", + Plane: "STORAGE", + NetworkPhysicalPortID: "9ef62803-7848-460c-9dae-17fd02606a26", + HardwareID: "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + AttachedPorts: []servers.Port{}, + }, + { + ID: "71af4de1-0c9b-4870-8c95-c7b9b4115bb8", + MACAddr: "8C:DC:D4:B7:45:88", + Plane: "DATA", + NetworkPhysicalPortID: "ad92f33a-eac4-408d-bc27-22d91eccd465", + HardwareID: "1aa1c2a4-2608-41e6-b4f5-87679d1aea43", + AttachedPorts: []servers.Port{ + { + NetworkID: "722f9e4f-39f8-406a-b98c-5fbd5689b89a", + PortID: "705bde94-7189-40b1-b8a2-188e6cc3c546", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "dc84c9dc-0b4d-40fc-8605-e518af7cdd30", + IPAddress: "192.168.4.4", + }, + }, + }, + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "7b860eb4-0eb6-4c2a-873e-ccefd6029d97", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.12", + }, + }, + }, + }, + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + Nic: true, + SystemBoard: true, + Etc: true, + Console: true, + }, + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/621b56e4-4aae-4de5-86a0-8ffeeda6a00b", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + Partitions: nil, + RaidCardHardwareID: "24184dcf-dc76-4ea2-a34e-bccc6c11d5be", + DiskHardwareIDs: []string{ + "4de7d3df-8e2f-4193-98dc-145f78df29a2", + "de158fab-7bc2-49e3-98d1-9db5451d43e3", + "97087307-cab2-40cb-a84c-86d98da0f393", + }, + }, + }, + LvmVolumeGroups: nil, + Filesystems: nil, + MediaAttachments: []servers.MediaAttachment{}, + ManagedByService: "dedicated-hypervisor", + ManagedServiceResourceID: "194573e4-8f53-4ee4-806f-d9b2db74a380", + }, +} + +var SecondServerDescription = "test" + +// SecondServerDetail is the second resource in the List detail request. +var SecondServerDetail = servers.Server{ + ID: "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + Name: "test", + ImageRef: "dfd25820-b368-4012-997b-29a6d0cf8518", + Description: &SecondServerDescription, + Status: "ACTIVE", + HypervisorType: "vsphere_esxi", + BaremetalServer: servers.BaremetalServer{ + PowerState: "RUNNING", + TaskState: "None", + VMState: "ACTIVE", + AvailabilityZone: "groupb", + Created: time.Date(2019, 10, 10, 4, 11, 41, 0, time.UTC), + Flavor: servers.Flavor{ + ID: "a830b61c-3155-4a61-b7ed-c450862845e6", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/flavors/a830b61c-3155-4a61-b7ed-c450862845e6", + Rel: "bookmark", + }, + }, + }, + ID: "24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Image: servers.Image{ + ID: "112a26a0-ff25-4513-afe1-407e41b0a48b", + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/images/112a26a0-ff25-4513-afe1-407e41b0a48b", + Rel: "bookmark", + }, + }, + }, + Metadata: map[string]string{}, + Name: "test", + Progress: 100, + Status: "ACTIVE", + TenantID: "1bc271e7a8af4d988ff91612f5b122f8", + Updated: time.Date(2019, 10, 10, 4, 14, 8, 0, time.UTC), + UserID: "55891ce6a3cb4bb0833514667d67288c", + NicPhysicalPorts: []servers.NicPhysicalPort{ + { + ID: "a2f63380-6c77-4cd5-8868-e3556ffd35ce", + MACAddr: "48:DF:37:90:B4:58", + Plane: "DATA", + NetworkPhysicalPortID: "d8e40a51-f1e2-4681-8953-9fe1e9992c42", + HardwareID: "be2d30d6-f891-4200-b827-95f229fb8c6b", + AttachedPorts: []servers.Port{ + { + NetworkID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + PortID: "30fc1c27-fb5f-4955-94d0-a56cd28d09e8", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + IPAddress: "2.1.1.10", + }, + }, + }, + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "aa6c61f4-db8a-44c7-a91c-7e636dac1dc6", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.9", + }, + }, + }, + }, + }, + { + ID: "b01dfdb0-f247-47d8-8224-c257aa3265e9", + MACAddr: "48:DF:37:90:B4:50", + Plane: "STORAGE", + NetworkPhysicalPortID: "00dfea92-5c5b-4860-aa05-efef6c2bb2af", + HardwareID: "be2d30d6-f891-4200-b827-95f229fb8c6b", + AttachedPorts: []servers.Port{}, + }, + { + ID: "f4355e8e-39fc-48bd-a283-a2dbef8a2e32", + MACAddr: "48:DF:37:82:B0:A0", + Plane: "STORAGE", + NetworkPhysicalPortID: "cf798cc0-c869-45d5-a5a7-bcc578a300b0", + HardwareID: "84c74a86-7045-4284-80f9-0e7aff5d27ad", + AttachedPorts: []servers.Port{}, + }, + { + ID: "5ef177fd-888c-4fae-9925-a8920beb07cb", + MACAddr: "48:DF:37:82:B0:A8", + Plane: "DATA", + NetworkPhysicalPortID: "2bbbb516-c75a-42b2-8a46-9cb5f26c219e", + HardwareID: "84c74a86-7045-4284-80f9-0e7aff5d27ad", + AttachedPorts: []servers.Port{ + { + NetworkID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + PortID: "4e329a01-2cf4-4028-9259-03b7aa145cb6", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "acd41997-5ebb-4ff2-8cd2-22cae6cf2883", + IPAddress: "2.1.1.20", + }, + }, + }, + { + NetworkID: "4a59f728-3920-4b71-ae54-d0d5c14ba04b", + PortID: "a256b4a1-3ae3-4102-a14e-987ae1610f97", + FixedIPs: []servers.FixedIP{ + { + SubnetID: "b87d9c85-af5c-403d-a49a-55a6ab0a36d2", + IPAddress: "169.254.0.10", + }, + }, + }, + }, + }, + }, + ChassisStatus: servers.ChassisStatus{ + ChassisPower: true, + PowerSupply: true, + CPU: true, + Memory: true, + Fan: true, + Disk: 0, + Nic: true, + SystemBoard: true, + Etc: true, + Console: true, + }, + Links: []servers.Link{ + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/v2/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "self", + }, + { + Href: "https://baremetal-server-jp1-ecl.api.ntt.com/1bc271e7a8af4d988ff91612f5b122f8/servers/24ebe7b8-ecfb-4d9f-a66b-c0120534fc90", + Rel: "bookmark", + }, + }, + RaidArrays: []servers.RaidArray{ + { + PrimaryStorage: true, + Partitions: nil, + RaidCardHardwareID: "bdfb75d1-194d-426d-b288-f588dfa5ac49", + DiskHardwareIDs: []string{ + "76649053-863e-4533-86e3-f194a79485a6", + "a25827e3-67da-47be-ba96-849ab4685a1d", + }, + }, + }, + LvmVolumeGroups: nil, + Filesystems: nil, + MediaAttachments: []servers.MediaAttachment{}, + ManagedByService: "dedicated-hypervisor", + ManagedServiceResourceID: "f42dbc37-4642-4628-8b47-50bf95d8fdd5", + }, +} + +// ExpectedServersDetailsSlice is the slice of resources expected to be returned from ListDetailsResult. +var ExpectedServersDetailsSlice = []servers.Server{FirstServerDetail, SecondServerDetail} + +var AddLicenseJob = servers.Job{ + JobID: "b4f888dc2b9d4c41bb769cbd", + Status: "COMPLETED", + RequestedParam: servers.RequestedParam{ + VmName: "Alice", + LicenseTypes: []string{ + "Windows Server", + "SQL Server Standard 2014", + }, + }, +} + +// HandleListServersSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a list of two servers. +func HandleListServersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers", 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, ListResult) + }) +} + +// HandleListServersDetailsSuccessfully creates an HTTP handler at `/servers/detail` on the +// test handler mux that responds with a list of two servers. +func HandleListServersDetailsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", 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, ListDetailsResult) + }) +} + +// HandleGetServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that responds with a single server. +func HandleGetServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", SecondServer.ID), 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, GetResult) + }) +} + +// HandleCreateServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server creation. +func HandleCreateServerSuccessfully(t *testing.T) { + 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, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, CreateResponse) + }) +} + +// HandleDeleteServerSuccessfully creates an HTTP handler at `/servers` on the +// test handler mux that tests server deletion. +func HandleDeleteServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s", FirstServer.ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +func HandleAddLicenseSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", SecondServer.ID), 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, AddLicenseRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, AddLicenseResponse) + }) +} + +func HandleGetAddLicenseResultSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/servers/%s/action", SecondServer.ID), 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, GetAddLicenseResultRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetAddLicenseResultResponse) + }) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/servers/testing/requests_test.go b/v4/ecl/dedicated_hypervisor/v1/servers/testing/requests_test.go new file mode 100644 index 0000000..86b9956 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/servers/testing/requests_test.go @@ -0,0 +1,155 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/servers" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersSuccessfully(t) + + count := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedServersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListServersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersSuccessfully(t) + + allPages, err := servers.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedServersSlice, actual) +} + +func TestListServersDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + count := 0 + err := servers.ListDetails(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedServersDetailsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListServersDetailsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListServersDetailsSuccessfully(t) + + allPages, err := servers.ListDetails(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedServersDetailsSlice, actual) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetServerSuccessfully(t) + + actual, err := servers.Get(client.ServiceClient(), SecondServer.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondServerDetail, *actual) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerSuccessfully(t) + + createOpts := servers.CreateOpts{ + Name: "test", + Networks: []servers.Network{ + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + { + UUID: "94055904-6b2c-4839-a14a-c61c93a8bc48", + Plane: "data", + SegmentationID: 6, + }, + }, + ImageRef: "dfd25820-b368-4012-997b-29a6d0cf8518", + FlavorRef: "a830b61c-3155-4a61-b7ed-c450862845e6", + } + + actual, err := servers.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, SecondServer.ID, actual.ID) + th.AssertDeepEquals(t, SecondServer.Links, actual.Links) + th.AssertEquals(t, "aabbccddeeff", actual.AdminPass) +} + +func TestDeleteResource(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteServerSuccessfully(t) + + res := servers.Delete(client.ServiceClient(), FirstServer.ID) + th.AssertNoErr(t, res.Err) +} + +func TestAddLicense(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAddLicenseSuccessfully(t) + + addLicenseOpts := servers.AddLicenseOpts{ + VmName: "Alice", + LicenseTypes: []string{ + "Windows Server", + "SQL Server Standard 2014", + }, + } + + actual, err := servers.AddLicense(client.ServiceClient(), SecondServer.ID, addLicenseOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, AddLicenseJob.JobID, actual.JobID) +} + +func TestGetAddLicenseResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetAddLicenseResultSuccessfully(t) + + getAddLicenseResultOpts := servers.GetAddLicenseResultOpts{ + JobID: AddLicenseJob.JobID, + } + + actual, err := servers.GetAddLicenseResult(client.ServiceClient(), SecondServer.ID, getAddLicenseResultOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, AddLicenseJob, *actual) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/servers/urls.go b/v4/ecl/dedicated_hypervisor/v1/servers/urls.go new file mode 100644 index 0000000..215148d --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/servers/urls.go @@ -0,0 +1,27 @@ +package servers + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listDetailsURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func actionURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} diff --git a/v4/ecl/dedicated_hypervisor/v1/usages/doc.go b/v4/ecl/dedicated_hypervisor/v1/usages/doc.go new file mode 100644 index 0000000..ce71466 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/usages/doc.go @@ -0,0 +1,39 @@ +/* +Package usages manages and retrieves usage in the Enterprise Cloud Dedicated Hypervisor Service. + +Example to List Usages + + listOpts := usages.ListOpts{ + From: "2019-10-10T00:00:00Z", + To: "2019-10-15T00:00:00Z", + LicenseType: "dedicated-hypervisor.guest-image.vcenter-server-6-0-standard", + } + + allPages, err := usages.List(dhClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsages, err := usages.ExtractUsages(allPages) + if err != nil { + panic(err) + } + + for _, usage := range allUsages { + fmt.Printf("%+v\n", usage) + } + +Example to Get Usage Histories + + getHistoriesOpts := usages.GetHistoriesOpts{ + From: "2019-10-10T00:00:00Z", + To: "2019-10-15T00:00:00Z", + } + + usageID := "9ada4c06-a2a4-46d5-b969-72ac12433a79" + histories, err := usages.GetHistories(client, usageID, getHistoriesOpts).ExtractHistories() + if err != nil { + panic(err) + } +*/ +package usages diff --git a/v4/ecl/dedicated_hypervisor/v1/usages/requests.go b/v4/ecl/dedicated_hypervisor/v1/usages/requests.go new file mode 100644 index 0000000..7003e60 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/usages/requests.go @@ -0,0 +1,73 @@ +package usages + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToResourceListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + From string `q:"from"` + To string `q:"to"` + LicenseType string `q:"license_type"` +} + +// ToResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Usages. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UsagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetHistoriesOpts provides options to filter the GetHistories results. +type GetHistoriesOpts struct { + From string `q:"from"` + To string `q:"to"` +} + +// ToResourceListQuery formats a GetHistoriesOpts into a query string. +func (opts GetHistoriesOpts) ToResourceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// GetHistories retrieves details of usage histories. +func GetHistories(client *eclcloud.ServiceClient, usageID string, opts ListOptsBuilder) (r GetHistoriesResult) { + url := getHistoriesURL(client, usageID) + if opts != nil { + query, err := opts.ToResourceListQuery() + if err != nil { + r.Err = err + return + } + url += query + } + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/v4/ecl/dedicated_hypervisor/v1/usages/results.go b/v4/ecl/dedicated_hypervisor/v1/usages/results.go new file mode 100644 index 0000000..30ba7f1 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/usages/results.go @@ -0,0 +1,89 @@ +package usages + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Usage represents guest image usage information. +type Usage struct { + ID string `json:"id"` + Type string `json:"type"` + Value string `json:"value"` + Unit string `json:"unit"` + Name string `json:"name"` + Description string `json:"description"` + HasLicenseKey bool `json:"has_license_key"` + ResourceID string `json:"resource_id"` +} + +type UsageHistories struct { + Unit string `json:"unit"` + ResourceID string `json:"resource_id"` + LicenseType string `json:"license_type"` + Histories []History `json:"histories"` +} + +type History struct { + Time time.Time `json:"-"` + Value string `json:"value"` +} + +func (h *History) UnmarshalJSON(b []byte) error { + type tmp History + var s struct { + tmp + Time eclcloud.JSONRFC3339ZNoTNoZ `json:"time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *h = History(s.tmp) + + h.Time = time.Time(s.Time) + + return err +} + +type commonResult struct { + eclcloud.Result +} + +// GetHistoriesResult is the response from a Get operation. Call its Extract method +// to interpret it as usage histories. +type GetHistoriesResult struct { + commonResult +} + +// UsagePage is a single page of Usage results. +type UsagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Usages contains any results. +func (r UsagePage) IsEmpty() (bool, error) { + usages, err := ExtractUsages(r) + return len(usages) == 0, err +} + +// ExtractUsages returns a slice of Usages contained in a single page of +// results. +func ExtractUsages(r pagination.Page) ([]Usage, error) { + var s struct { + Usages []Usage `json:"usages"` + } + err := (r.(UsagePage)).ExtractInto(&s) + return s.Usages, err +} + +// ExtractHistories interprets any commonResult as usage histories. +func (r commonResult) ExtractHistories() (*UsageHistories, error) { + var s UsageHistories + err := r.ExtractInto(&s) + return &s, err +} diff --git a/v4/ecl/dedicated_hypervisor/v1/usages/testing/fixtures.go b/v4/ecl/dedicated_hypervisor/v1/usages/testing/fixtures.go new file mode 100644 index 0000000..77592d8 --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/usages/testing/fixtures.go @@ -0,0 +1,140 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/usages" + + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// ListResult provides a single page of Usage results. +const ListResult = ` +{ + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "usages": [ + { + "description": "vCenter Server 6.x Standard", + "has_license_key": true, + "id": "9ada4c06-a2a4-46d5-b969-72ac12433a79", + "name": "vCenter Server 6.x Standard", + "resource_id": "1bc271e7a8af4d988ff91612f5b122f8", + "type": "dedicated-hypervisor.guest-image.vcenter-server-6-0-standard", + "unit": "License", + "value": "2" + }, + { + "description": "SQL Server 2014 Standard Edition", + "has_license_key": false, + "id": "9da9116d-cc44-4ad8-aca5-7db398fcb478", + "name": "SQL Server 2014 Standard Edition", + "resource_id": "d-cc44-4ad8-aca5-7db398fcb477bbbbbb", + "type": "dedicated-hypervisor.guest-image.sql-server-2014-standard", + "unit": "vCPU", + "value": "6" + } + ] +} +` + +// FirstUsage is the first Usage in the List request. +var FirstUsage = usages.Usage{ + ID: "9ada4c06-a2a4-46d5-b969-72ac12433a79", + Type: "dedicated-hypervisor.guest-image.vcenter-server-6-0-standard", + Value: "2", + Unit: "License", + Name: "vCenter Server 6.x Standard", + Description: "vCenter Server 6.x Standard", + HasLicenseKey: true, + ResourceID: "1bc271e7a8af4d988ff91612f5b122f8", +} + +// SecondUsage is the second Usage in the List request. +var SecondUsage = usages.Usage{ + ID: "9da9116d-cc44-4ad8-aca5-7db398fcb478", + Type: "dedicated-hypervisor.guest-image.sql-server-2014-standard", + Value: "6", + Unit: "vCPU", + Name: "SQL Server 2014 Standard Edition", + Description: "SQL Server 2014 Standard Edition", + HasLicenseKey: false, + ResourceID: "d-cc44-4ad8-aca5-7db398fcb477bbbbbb", +} + +// ExpectedUsagesSlice is the slice of LicenseTypes expected to be returned from ListResult. +var ExpectedUsagesSlice = []usages.Usage{FirstUsage, SecondUsage} + +// HandleListUsagesSuccessfully creates an HTTP handler at `/usages` on the +// test handler mux that responds with a list of two Usages. +func HandleListUsagesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/usages", 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, ListResult) + }) +} + +const usageID = "9ada4c06-a2a4-46d5-b969-72ac12433a79" + +// GetHistoriesResult provides a single page of Usage results. +const GetHistoriesResult = ` +{ + "description": "vCenter Server 6.x Standard", + "histories": [ + { + "time": "2019-10-10 00:00:00", + "value": "1" + }, + { + "time": "2019-10-10 01:00:00", + "value": "1" + } + ], + "license_type": "vCenter Server 6.x Standard", + "resource_id": "1bc271e7a8af4d988ff91612f5b122f8", + "tenant_id": "1bc271e7a8af4d988ff91612f5b122f8", + "unit": "License" +} +` + +// FirstHistory is the first History in the Get histories request. +var FirstHistory = usages.History{ + Time: time.Date(2019, 10, 10, 0, 0, 0, 0, time.UTC), + Value: "1", +} + +// SecondHistory is the second History in the Get histories request. +var SecondHistory = usages.History{ + Time: time.Date(2019, 10, 10, 1, 0, 0, 0, time.UTC), + Value: "1", +} + +// ExpectedHistories is the UsageHistories expected to be returned from GetHistoriesResult. +var ExpectedHistories = &usages.UsageHistories{ + Unit: "License", + ResourceID: "1bc271e7a8af4d988ff91612f5b122f8", + LicenseType: "vCenter Server 6.x Standard", + Histories: []usages.History{FirstHistory, SecondHistory}, +} + +// HandleGetHistoriesSuccessfully creates an HTTP handler at `/usages//histories` on the +// test handler mux that responds with usage histories. +func HandleGetHistoriesSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/usages/%s/histories", usageID), 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, GetHistoriesResult) + }) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/usages/testing/requests_test.go b/v4/ecl/dedicated_hypervisor/v1/usages/testing/requests_test.go new file mode 100644 index 0000000..139f41e --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/usages/testing/requests_test.go @@ -0,0 +1,55 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/dedicated_hypervisor/v1/usages" + + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListUsages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsagesSuccessfully(t) + + count := 0 + err := usages.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := usages.ExtractUsages(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedUsagesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListUsagesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsagesSuccessfully(t) + + allPages, err := usages.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := usages.ExtractUsages(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedUsagesSlice, actual) +} + +func TestGetUsageHistories(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetHistoriesSuccessfully(t) + + result := usages.GetHistories(client.ServiceClient(), usageID, nil) + th.AssertNoErr(t, result.Err) + actual, err := result.ExtractHistories() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedHistories, actual) +} diff --git a/v4/ecl/dedicated_hypervisor/v1/usages/urls.go b/v4/ecl/dedicated_hypervisor/v1/usages/urls.go new file mode 100644 index 0000000..e97f96e --- /dev/null +++ b/v4/ecl/dedicated_hypervisor/v1/usages/urls.go @@ -0,0 +1,11 @@ +package usages + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("usages") +} + +func getHistoriesURL(client *eclcloud.ServiceClient, usageID string) string { + return client.ServiceURL("usages", usageID, "histories") +} diff --git a/v4/ecl/dns/v2/recordsets/doc.go b/v4/ecl/dns/v2/recordsets/doc.go new file mode 100644 index 0000000..cc65bed --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/doc.go @@ -0,0 +1,54 @@ +/* +Package recordsets provides information and interaction with the zone API +resource for the Enterprise Cloud DNS service. + +Example to List RecordSets by Zone + + listOpts := recordsets.ListOpts{ + Type: "A", + } + + zoneID := "fff121f5-c506-410a-a69e-2d73ef9cbdbd" + + allPages, err := recordsets.ListByZone(dnsClient, zoneID, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRRs, err := recordsets.ExtractRecordSets(allPages() + if err != nil { + panic(err) + } + + for _, rr := range allRRs { + fmt.Printf("%+v\n", rr) + } + +Example to Create a RecordSet + + createOpts := recordsets.CreateOpts{ + Name: "example.com.", + Type: "A", + TTL: 3600, + Description: "This is a recordset.", + Records: []string{"10.1.0.2"}, + } + + zoneID := "fff121f5-c506-410a-a69e-2d73ef9cbdbd" + + rr, err := recordsets.Create(dnsClient, zoneID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a RecordSet + + zoneID := "fff121f5-c506-410a-a69e-2d73ef9cbdbd" + recordsetID := "d96ed01a-b439-4eb8-9b90-7a9f71017f7b" + + err := recordsets.Delete(dnsClient, zoneID, recordsetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package recordsets diff --git a/v4/ecl/dns/v2/recordsets/requests.go b/v4/ecl/dns/v2/recordsets/requests.go new file mode 100644 index 0000000..4261c87 --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/requests.go @@ -0,0 +1,162 @@ +package recordsets + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToRecordSetListQuery() (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 { + ZoneID string `q:"zone_id"` + + // Domain name of zone for partial-match search. + DomainName string `q:"data"` + + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + + // UUID of the recordset 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"` +} + +// ToRecordSetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRecordSetListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListByZone implements the recordset list request. +func ListByZone(client *eclcloud.ServiceClient, zoneID string, opts ListOptsBuilder) pagination.Pager { + url := baseURL(client, zoneID) + if opts != nil { + query, err := opts.ToRecordSetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RecordSetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get implements the recordset Get request. +func Get(client *eclcloud.ServiceClient, zoneID string, rrsetID string) (r GetResult) { + _, r.Err = client.Get(rrsetURL(client, zoneID, rrsetID), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional attributes to the +// Create request. +type CreateOptsBuilder interface { + ToRecordSetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies the base attributes that may be used to create a +// RecordSet. +type CreateOpts struct { + // Name is the name of the RecordSet. + Name string `json:"name" required:"true"` + + // Description is a description of the RecordSet. + Description string `json:"description,omitempty"` + + // Records are the DNS records of the RecordSet. + Records []string `json:"records,omitempty"` + + // TTL is the time to live of the RecordSet. + TTL int `json:"ttl,omitempty"` + + // Type is the record type of the RecordSet. + Type string `json:"type" required:"true"` +} + +// ToRecordSetCreateMap formats an CreateOpts structure into a request body. +func (opts CreateOpts) ToRecordSetCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create creates a recordset in a given zone. +func Create(client *eclcloud.ServiceClient, zoneID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRecordSetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client, zoneID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201, 202}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToRecordSetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// RecordSet. +type UpdateOpts struct { + // Name is the name of the RecordSet. + Name *string `json:"name,omitempty"` + + // Description is a description of the RecordSet. + Description *string `json:"description,omitempty"` + + // TTL is the time to live of the RecordSet. + TTL *int `json:"ttl,omitempty"` + + // Records are the DNS records of the RecordSet. + Records *[]string `json:"records,omitempty"` +} + +// ToRecordSetUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToRecordSetUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update updates a recordset in a given zone +func Update(client *eclcloud.ServiceClient, zoneID string, rrsetID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRecordSetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(rrsetURL(client, zoneID, rrsetID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete removes an existing RecordSet. +func Delete(client *eclcloud.ServiceClient, zoneID string, rrsetID string) (r DeleteResult) { + _, r.Err = client.Delete( + rrsetURL(client, zoneID, rrsetID), + &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} diff --git a/v4/ecl/dns/v2/recordsets/results.go b/v4/ecl/dns/v2/recordsets/results.go new file mode 100644 index 0000000..e11c5fb --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/results.go @@ -0,0 +1,163 @@ +package recordsets + +import ( + "encoding/json" + "fmt" + // "log" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a RecordSet. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*RecordSet, error) { + var s *RecordSet + err := r.ExtractInto(&s) + return s, err +} + +func (r commonResult) ExtractCreatedRecordSet() (*RecordSet, error) { + var sl []*RecordSet + err := r.ExtractIntoSlicePtr(&sl, "recordsets") + if err != nil { + return nil, fmt.Errorf("[Error] Error in parsing result of recordset create %s", err) + } + return sl[0], nil +} + +// CreateResult is the result of a Create operation. Call its Extract method to +// interpret the result as a RecordSet. +type CreateResult struct { + commonResult +} + +// GetResult is the result of a Get operation. Call its Extract method to +// interpret the result as a RecordSet. +type GetResult struct { + commonResult +} + +// RecordSetPage is a single page of RecordSet results. +type RecordSetPage struct { + pagination.LinkedPageBase +} + +// UpdateResult is result of an Update operation. Call its Extract method to +// interpret the result as a RecordSet. +type UpdateResult struct { + commonResult +} + +// DeleteResult is result of a Delete operation. Call its ExtractErr method to +// determine if the operation succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// IsEmpty returns true if the page contains no results. +func (r RecordSetPage) IsEmpty() (bool, error) { + s, err := ExtractRecordSets(r) + return len(s) == 0, err +} + +// ExtractRecordSets extracts a slice of RecordSets from a List result. +func ExtractRecordSets(r pagination.Page) ([]RecordSet, error) { + var s struct { + RecordSets []RecordSet `json:"recordsets"` + } + err := (r.(RecordSetPage)).ExtractInto(&s) + return s.RecordSets, err +} + +// RecordSet represents a DNS Record Set. +type RecordSet struct { + // ID is the unique ID of the recordset + ID string `json:"id"` + + // ZoneID is the ID of the zone the recordset belongs to. + ZoneID string `json:"zone_id"` + + // ProjectID is the ID of the project that owns the recordset. + // ProjectID string `json:"project_id"` + + // Name is the name of the recordset. + Name string `json:"name"` + + // Type is the RRTYPE of the recordset. + Type string `json:"type"` + + // Records are the DNS records of the recordset. + // This is original code. + // Records []string `json:"records"` + // + // But in ECL2.0, record set will be returned as simple string + // e.g. + // Usual response(like creation) reccordset: "[10.0.0.1]" + // Update response(like creation) reccordset: "10.0.0.1]" + Records interface{} `json:"records"` + + // TTL is the time to live of the recordset. + TTL int `json:"ttl"` + + // Description is the description of the recordset. + Description string `json:"description"` + + // Version is the revision of the recordset. + Version int `json:"version"` + + // CreatedAt is the date when the recordset was created. + CreatedAt time.Time `json:"-"` + + // UpdatedAt is the date when the recordset was updated. + UpdatedAt time.Time `json:"-"` + + // Status is the current status of recordset. + Status string `json:"status"` + + // Current action in progress on the resource. + // This parameter is not currently supported. it always return an empty. + Action string `json:"action"` + + // Links includes HTTP references to the itself, + // useful for passing along to other APIs that might want a recordset + // reference. + Links []eclcloud.Link `json:"-"` +} + +func (r *RecordSet) UnmarshalJSON(b []byte) error { + type tmp RecordSet + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + Links map[string]interface{} `json:"links"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = RecordSet(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + if s.Links != nil { + for rel, href := range s.Links { + if v, ok := href.(string); ok { + link := eclcloud.Link{ + Rel: rel, + Href: v, + } + r.Links = append(r.Links, link) + } + } + } + + return err +} diff --git a/v4/ecl/dns/v2/recordsets/testing/doc.go b/v4/ecl/dns/v2/recordsets/testing/doc.go new file mode 100644 index 0000000..f4d91dc --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/testing/doc.go @@ -0,0 +1,2 @@ +// recordsets unit tests +package testing diff --git a/v4/ecl/dns/v2/recordsets/testing/fixtures.go b/v4/ecl/dns/v2/recordsets/testing/fixtures.go new file mode 100644 index 0000000..5ae2eeb --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/testing/fixtures.go @@ -0,0 +1,320 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/dns/v2/recordsets" +) + +const idZone = "4eb5c333-7031-48ff-a247-0eeccc57472e" + +const idRecordSet1 = "f1dcc528-6b94-47aa-9b13-b62a356ed44f" +const idRecordSet2 = "a0b9df1e-fa38-47a5-855d-f15927fea579" + +const nameRecordSet1 = "rs1.zone1.com." +const nameRecordSet1Update = "rs1update.zone1.com." + +const descriptionRecordSet1 = "a record set 1" +const descriptionRecordSet1Update = "a record set 1-updated" + +const ipRecordSet1 = "10.1.0.0" +const ipRecordSet1Update = "10.1.0.1" + +const TTLRecordSet1 = 3000 +const TTLRecordSet1Update = 3600 + +const recordSetCreatedAt = "2019-02-05T23:41:57" +const recordSetUpdatedAt = "2019-02-05T23:42:13" + +// ListResponse is a sample response to a TestListDNSRecordSet call. +var ListResponse = fmt.Sprintf(`{ + "recordsets": [{ + "id": "%s", + "action": "", + "name": "%s", + "ttl": %d, + "description": "%s", + "records": ["%s"], + "type": "A", + "version": 1, + "status": "ACTIVE", + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "links": { + "self": "dummylink" + } + }, { + "id": "%s", + "action": "", + "name": "rs2.zone1.com.", + "ttl": 3000, + "description": "a record set 2", + "records": ["20.1.0.0"], + "type": "A", + "version": 1, + "status": "ACTIVE", + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "links": { + "self": "dummylink" + } + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 2 + } +}`, + // For recordSet1 + idRecordSet1, + nameRecordSet1, + TTLRecordSet1, + descriptionRecordSet1, + ipRecordSet1, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, + // For recordSet2 + idRecordSet2, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, +) + +// ListResponseLimited is a sample response with limit query option. +var ListResponseLimited = fmt.Sprintf(`{ + "recordsets": [{ + "id": "%s", + "action": "", + "name": "rs2.zone1.com.", + "ttl": 3000, + "description": "a record set 2", + "records": ["20.1.0.0"], + "type": "A", + "version": 1, + "status": "ACTIVE", + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "links": { + "self": "dummylink" + } + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 1 + } +}`, + idRecordSet2, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, +) + +// RecordSetCreatedAt is mocked created time of each records. +var RecordSetCreatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, recordSetCreatedAt) + +// RecordSetUpdatedAt is mocked updated time of each records. +var RecordSetUpdatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, recordSetUpdatedAt) + +// FirstRecordSet is initialized struct as actual response +var FirstRecordSet = recordsets.RecordSet{ + ID: idRecordSet1, + Description: descriptionRecordSet1, + Records: []string{ipRecordSet1}, + TTL: TTLRecordSet1, + Name: nameRecordSet1, + ZoneID: idZone, + CreatedAt: RecordSetCreatedAt, + UpdatedAt: RecordSetUpdatedAt, + Version: 1, + Type: "A", + Status: "ACTIVE", + Action: "", + Links: []eclcloud.Link{ + { + Rel: "self", + Href: "dummylink", + }, + }, +} + +// SecondRecordSet is initialized struct as actual response +var SecondRecordSet = recordsets.RecordSet{ + ID: idRecordSet2, + Description: "a record set 2", + Records: []string{"20.1.0.0"}, + TTL: 3000, + Name: "rs2.zone1.com.", + ZoneID: idZone, + CreatedAt: RecordSetCreatedAt, + UpdatedAt: RecordSetUpdatedAt, + Version: 1, + Type: "A", + Status: "ACTIVE", + Action: "", + Links: []eclcloud.Link{ + { + Rel: "self", + Href: "dummylink", + }, + }, +} + +// ExpectedRecordSetSlice is the slice of results that should be parsed +// from ListByZoneOutput, in the expected order. +var ExpectedRecordSetSlice = []recordsets.RecordSet{FirstRecordSet, SecondRecordSet} + +// ExpectedRecordSetSliceLimited is the slice of limited results that should be parsed +// from ListByZoneOutput. +var ExpectedRecordSetSliceLimited = []recordsets.RecordSet{SecondRecordSet} + +// GetResponse is a sample response to a Get call. +var GetResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "ttl": %d, + "description": "%s", + "records": ["%s"], + "type": "A", + "version": 1, + "created_at": "%s", + "updated_at": "%s", + "zone_id": "%s", + "status": "ACTIVE", + "links": { + "self": "dummylink" + } +}`, + idRecordSet1, + nameRecordSet1, + TTLRecordSet1, + descriptionRecordSet1, + ipRecordSet1, + recordSetCreatedAt, + recordSetUpdatedAt, + idZone, +) + +const selfURL = "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1" +const nextURL = "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1&marker=f7b10e9b-0cae-4a91-b162-562bc6096648" + +// NextPageRequest is a sample request to test pagination. +var NextPageRequest = fmt.Sprintf(` +{ + "links": { + "self": "%s", + "next": "%s" + } +}`, selfURL, nextURL) + +// CreateRequest is a sample request to create a resource record. +var CreateRequest = fmt.Sprintf(`{ + "name" : "%s", + "description" : "%s", + "type" : "A", + "ttl" : %d, + "records" : ["%s"] +}`, + nameRecordSet1, + descriptionRecordSet1, + TTLRecordSet1, + ipRecordSet1, +) + +// CreateResponse is a sample response to a create request. +var CreateResponse = fmt.Sprintf(`{ + "recordsets": [{ + "id": "%s", + "zone_id": "%s", + "records": ["%s"], + "ttl": %d, + "name": "%s", + "description": "%s", + "type": "A", + "version": 1, + "created_at": "", + "updated_at": null, + "links": { + "self": "dummylink" + } + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 1 + } +}`, idRecordSet1, + idZone, + ipRecordSet1, + TTLRecordSet1, + nameRecordSet1, + descriptionRecordSet1, +) + +// UpdateRequest is a sample request to update a record set. +var UpdateRequest = fmt.Sprintf(`{ + "name": "%s", + "description" : "%s", + "ttl" : %d, + "records" : ["%s"] +}`, + nameRecordSet1Update, + descriptionRecordSet1Update, + TTLRecordSet1Update, + ipRecordSet1Update, +) + +// UpdateResponse is a sample response to an update request. +var UpdateResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "ttl": %d, + "description": "%s", + "records": "%s", + "type": "A", + "version": 1, + "created_at": null, + "updated_at": null, + "zone_id": "%s", + "links": { + "self": "dummylink" + } +}`, + idRecordSet1, + nameRecordSet1Update, + TTLRecordSet1Update, + descriptionRecordSet1Update, + ipRecordSet1Update, + idZone, +) + +// UpdatedRecordSet is initialized struct as actual response of update +var UpdatedRecordSet = recordsets.RecordSet{ + ID: idRecordSet1, + Name: nameRecordSet1Update, + TTL: TTLRecordSet1Update, + Description: descriptionRecordSet1Update, + Records: ipRecordSet1Update, + Type: "A", + Version: 1, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + ZoneID: idZone, + // Status: "", + // Action: "", + Links: []eclcloud.Link{ + { + Rel: "self", + Href: "dummylink", + }, + }, +} diff --git a/v4/ecl/dns/v2/recordsets/testing/requests_test.go b/v4/ecl/dns/v2/recordsets/testing/requests_test.go new file mode 100644 index 0000000..e811318 --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/testing/requests_test.go @@ -0,0 +1,204 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/dns/v2/recordsets" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + prepareMuxForListResponse(t) + + count := 0 + err := recordsets.ListByZone(fakeclient.ServiceClient(), idZone, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := recordsets.ExtractRecordSets(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedRecordSetSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDNSRecordSetLimited(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + prepareMuxForListResponse(t) + + count := 0 + listOpts := recordsets.ListOpts{ + Limit: 1, + Marker: idRecordSet1, + } + err := recordsets.ListByZone(fakeclient.ServiceClient(), idZone, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := recordsets.ExtractRecordSets(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedRecordSetSliceLimited, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDNSRecordSetAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + prepareMuxForListResponse(t) + + allPages, err := recordsets.ListByZone(fakeclient.ServiceClient(), idZone, nil).AllPages() + th.AssertNoErr(t, err) + allRecordSets, err := recordsets.ExtractRecordSets(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allRecordSets)) +} + +func prepareMuxForListResponse(t *testing.T) { + url := fmt.Sprintf("/zones/%s/recordsets", idZone) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + + marker := r.Form.Get("marker") + switch marker { + case idRecordSet1: + fmt.Fprintf(w, ListResponseLimited) + case "": + fmt.Fprintf(w, ListResponse) + } + }) +} + +func TestGetDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets/%s", idZone, idRecordSet1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetResponse) + }) + + actual, err := recordsets.Get(fakeclient.ServiceClient(), idZone, idRecordSet1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstRecordSet, actual) +} + +func TestNextPageURL(t *testing.T) { + var page recordsets.RecordSetPage + var body map[string]interface{} + err := json.Unmarshal([]byte(NextPageRequest), &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + expected := nextURL + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} + +func TestCreateDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets", idZone) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := recordsets.CreateOpts{ + Name: nameRecordSet1, + Type: "A", + TTL: TTLRecordSet1, + Description: descriptionRecordSet1, + Records: []string{ipRecordSet1}, + } + + // Clone FirstRecord into CreatedRecordSet(Created result struct) + CreatedRecordSet := FirstRecordSet + CreatedRecordSet.CreatedAt = time.Time{} + CreatedRecordSet.UpdatedAt = time.Time{} + CreatedRecordSet.Status = "" + + actual, err := recordsets.Create(fakeclient.ServiceClient(), idZone, createOpts).ExtractCreatedRecordSet() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedRecordSet, actual) +} + +func TestUpdateDNSRecordSet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets/%s", idZone, idRecordSet1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, UpdateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameRecordSet1Update + ttl := TTLRecordSet1Update + description := descriptionRecordSet1Update + records := []string{ipRecordSet1Update} + + updateOpts := recordsets.UpdateOpts{ + Name: &name, + TTL: &ttl, + Description: &description, + Records: &records, + } + + actual, err := recordsets.Update( + fakeclient.ServiceClient(), idZone, idRecordSet1, updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UpdatedRecordSet, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s/recordsets/%s", idZone, idRecordSet1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + rs := recordsets.Delete(fakeclient.ServiceClient(), idZone, idRecordSet1) + th.AssertNoErr(t, rs.Err) +} diff --git a/v4/ecl/dns/v2/recordsets/urls.go b/v4/ecl/dns/v2/recordsets/urls.go new file mode 100644 index 0000000..6d4048b --- /dev/null +++ b/v4/ecl/dns/v2/recordsets/urls.go @@ -0,0 +1,11 @@ +package recordsets + +import "github.com/nttcom/eclcloud/v4" + +func baseURL(c *eclcloud.ServiceClient, zoneID string) string { + return c.ServiceURL("zones", zoneID, "recordsets") +} + +func rrsetURL(c *eclcloud.ServiceClient, zoneID string, rrsetID string) string { + return c.ServiceURL("zones", zoneID, "recordsets", rrsetID) +} diff --git a/v4/ecl/dns/v2/zones/doc.go b/v4/ecl/dns/v2/zones/doc.go new file mode 100644 index 0000000..9a56cd5 --- /dev/null +++ b/v4/ecl/dns/v2/zones/doc.go @@ -0,0 +1,48 @@ +/* +Package zones provides information and interaction with the zone API +resource for the Enterprise Cloud DNS service. + +Example to List Zones + + listOpts := zones.ListOpts{ + Email: "jdoe@example.com", + } + + allPages, err := zones.List(dnsClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allZones, err := zones.ExtractZones(allPages) + if err != nil { + panic(err) + } + + for _, zone := range allZones { + fmt.Printf("%+v\n", zone) + } + +Example to Create a Zone + + createOpts := zones.CreateOpts{ + Name: "example.com.", + Email: "jdoe@example.com", + Type: "PRIMARY", + TTL: 7200, + Description: "This is a zone.", + } + + zone, err := zones.Create(dnsClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Zone + + zoneID := "99d10f68-5623-4491-91a0-6daafa32b60e" + err := zones.Delete(dnsClient, zoneID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package zones diff --git a/v4/ecl/dns/v2/zones/requests.go b/v4/ecl/dns/v2/zones/requests.go new file mode 100644 index 0000000..650193c --- /dev/null +++ b/v4/ecl/dns/v2/zones/requests.go @@ -0,0 +1,173 @@ +package zones + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add parameters to the List request. +type ListOptsBuilder interface { + ToZoneListQuery() (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 { + // Domain name of zone for partial-match search. + DomainName string `q:"domain_name"` + // Sorts the response by the attribute value. A valid value is only domain_name. + SortKey string `q:"sort_key"` + // Sorts the response by the requested sort direction. + // A valid value is asc (ascending) or desc (descending). Default is asc. + SortDir string `q:"sort_dir"` + // UUID of the zone 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"` + + // Following are original designate parameters. + // But can not be used in ECL2.0 + // TODO: Remove them at last of development. + // + // Description string `q:"description"` + // Email string `q:"email"` + // Name string `q:"name"` + // Status string `q:"status"` + // TTL int `q:"ttl"` + // Type string `q:"type"` +} + +// ToZoneListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToZoneListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List implements a zone List request. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := baseURL(client) + if opts != nil { + query, err := opts.ToZoneListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ZonePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns information about a zone, given its ID. +func Get(client *eclcloud.ServiceClient, zoneID string) (r GetResult) { + _, r.Err = client.Get(zoneURL(client, zoneID), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional attributes to the +// Create request. +type CreateOptsBuilder interface { + ToZoneCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies the attributes used to create a zone. +type CreateOpts struct { + // Description of the zone. + Description string `json:"description,omitempty"` + + // Email contact of the zone. + Email string `json:"email,omitempty"` + + // Name of the zone. + Name string `json:"name" required:"true"` + + // Masters specifies zone masters if this is a secondary zone. + Masters []string `json:"masters,omitempty"` + + // TTL is the time to live of the zone. + TTL int `json:"-"` + + // Type specifies if this is a primary or secondary zone. + Type string `json:"type,omitempty"` +} + +// ToZoneCreateMap formats an CreateOpts structure into a request body. +func (opts CreateOpts) ToZoneCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.TTL > 0 { + b["ttl"] = opts.TTL + } + + return b, nil +} + +// Create implements a zone create request. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToZoneCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201, 202}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToZoneUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the attributes to update a zone. +type UpdateOpts struct { + // Description of the zone. + Description *string `json:"description,omitempty"` + + // TTL is the time to live of the zone. + TTL *int `json:"ttl,omitempty"` + + // Masters specifies zone masters if this is a secondary zone. + Masters *[]string `json:"masters,omitempty"` + + // Email contact of the zone. + Email *string `json:"email,omitempty"` +} + +// ToZoneUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToZoneUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update implements a zone update request. +func Update(client *eclcloud.ServiceClient, zoneID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToZoneUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(zoneURL(client, zoneID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete implements a zone delete request. +func Delete(client *eclcloud.ServiceClient, zoneID string) (r DeleteResult) { + _, r.Err = client.Delete(zoneURL(client, zoneID), &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} diff --git a/v4/ecl/dns/v2/zones/results.go b/v4/ecl/dns/v2/zones/results.go new file mode 100644 index 0000000..1fc86fb --- /dev/null +++ b/v4/ecl/dns/v2/zones/results.go @@ -0,0 +1,166 @@ +package zones + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a Zone. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Zone, error) { + var s *Zone + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult is the result of a Create request. Call its Extract method +// to interpret the result as a Zone. +type CreateResult struct { + commonResult +} + +// GetResult is the result of a Get request. Call its Extract method +// to interpret the result as a Zone. +type GetResult struct { + commonResult +} + +// UpdateResult is the result of an Update request. Call its Extract method +// to interpret the result as a Zone. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method +// to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// ZonePage is a single page of Zone results. +type ZonePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (r ZonePage) IsEmpty() (bool, error) { + s, err := ExtractZones(r) + return len(s) == 0, err +} + +// ExtractZones extracts a slice of Zones from a List result. +func ExtractZones(r pagination.Page) ([]Zone, error) { + var s struct { + Zones []Zone `json:"zones"` + } + err := (r.(ZonePage)).ExtractInto(&s) + return s.Zones, err +} + +// Zone represents a DNS zone. +type Zone struct { + // ID uniquely identifies this zone amongst all other zones, including those + // not accessible to the current tenant. + ID string `json:"id"` + + // PoolID is the ID for the pool hosting this zone. + PoolID string `json:"pool_id"` + + // ProjectID identifies the project/tenant owning this resource. + ProjectID string `json:"project_id"` + + // Name is the DNS Name for the zone. + Name string `json:"name"` + + // Email for the zone. Used in SOA records for the zone. + Email string `json:"email"` + + // TTL is the Time to Live for the zone. + TTL int `json:"ttl"` + + // Serial is the current serial number for the zone. + Serial int `json:"-"` + + // Status is the status of the resource. + Status string `json:"status"` + + // Description for this zone. + Description string `json:"description"` + + // Masters is the servers for slave servers to get DNS information from. + Masters []string `json:"masters"` + + // Type of zone. Primary is controlled by Designate. + // Secondary zones are slaved from another DNS Server. + // Defaults to Primary. + Type string `json:"type"` + + // TransferredAt is the last time an update was retrieved from the + // master servers. + TransferredAt time.Time `json:"-"` + + // Version of the resource. + Version int `json:"version"` + + // CreatedAt is the date when the zone was created. + CreatedAt time.Time `json:"-"` + + // UpdatedAt is the date when the last change was made to the zone. + UpdatedAt time.Time `json:"-"` + + // Action for the zone. + Action string `json:"action"` + + // Attributes for the zone. + Attributes []string `json:"attributes"` + + // Links includes HTTP references to the itself, useful for passing along + // to other APIs that might want a server reference. + Links map[string]interface{} `json:"links"` +} + +func (r *Zone) UnmarshalJSON(b []byte) error { + type tmp Zone + var s struct { + tmp + CreatedAt eclcloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt eclcloud.JSONRFC3339MilliNoZ `json:"updated_at"` + TransferredAt eclcloud.JSONRFC3339MilliNoZ `json:"transferred_at"` + Serial interface{} `json:"serial"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Zone(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.TransferredAt = time.Time(s.TransferredAt) + + switch t := s.Serial.(type) { + case float64: + r.Serial = int(t) + case string: + switch t { + case "": + r.Serial = 0 + default: + serial, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Serial = int(serial) + } + } + + return err +} diff --git a/v4/ecl/dns/v2/zones/testing/doc.go b/v4/ecl/dns/v2/zones/testing/doc.go new file mode 100644 index 0000000..b9b6286 --- /dev/null +++ b/v4/ecl/dns/v2/zones/testing/doc.go @@ -0,0 +1,2 @@ +// zones unit tests +package testing diff --git a/v4/ecl/dns/v2/zones/testing/fixtures.go b/v4/ecl/dns/v2/zones/testing/fixtures.go new file mode 100644 index 0000000..ffa7755 --- /dev/null +++ b/v4/ecl/dns/v2/zones/testing/fixtures.go @@ -0,0 +1,324 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/dns/v2/zones" +) + +const idZone1 = "dcbd3d17-26ce-461d-b77c-a8774cafee75" +const idZone2 = "db511e50-3b1d-4805-98c6-a00adeb6ded0" + +const nameZone1 = "myzone.com." + +const descriptionZone1 = "this is my zone" +const descriptionZone1Update = "this is my zone-update" + +const tenantID = "9b8f16df551e42f3b905859f28a33d55" + +const zoneCreatedAt = "2019-02-05T06:09:41" +const zoneUpdatedAt = "2019-02-05T06:09:45" + +// ListResponse is a sample response to a List call. +var ListResponse = fmt.Sprintf(` +{ + "api_result": "success", + "zones": [{ + "id": "%s", + "description": "%s", + "project_id": "%s", + "created_at": "%s", + "updated_at": "%s", + "name": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "links": { + "self": "dummylink" + }, + "action": "", + "attributes": [] + }, { + "id": "%s", + "description": "This is my zone 2", + "project_id": "%s", + "created_at": "%s", + "updated_at": "%s", + "name": "myzone2.com.", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "links": { + "self": "dummylink" + }, + "action": "", + "attributes": [] + }], + "links": { + "self": "dummylink" + }, + "metadata": { + "total_count": 2 + } +}`, + // for Zone1 + idZone1, + descriptionZone1, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, + nameZone1, + // for Zone2 + idZone2, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, +) + +// ExpectedZonesSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedZonesSlice = []zones.Zone{FirstZone, SecondZone} + +// ZoneCreatedAt is parsed zone creation time +var ZoneCreatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, zoneCreatedAt) + +// ZoneUpdatedAt is parsed zone update time +var ZoneUpdatedAt, _ = time.Parse(eclcloud.RFC3339MilliNoZ, zoneUpdatedAt) + +// FirstZone is the mock object of expected zone-1 +var FirstZone = zones.Zone{ + ID: idZone1, + PoolID: "", + ProjectID: tenantID, + Name: nameZone1, + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Description: descriptionZone1, + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + Action: "", + Attributes: []string{}, + Links: map[string]interface{}{ + "self": "dummylink", + }, +} + +// SecondZone is the mock object of expected zone-2 +var SecondZone = zones.Zone{ + ID: idZone2, + PoolID: "", + ProjectID: tenantID, + Name: "myzone2.com.", + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Description: "This is my zone 2", + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + Action: "", + Attributes: []string{}, + Links: map[string]interface{}{ + "self": "dummylink", + }} + +// GetResponse is a sample response to a Get call. +// This get result does not have action, attributes in ECL2.0 +var GetResponse = fmt.Sprintf(` +{ + "id": "%s", + "name": "%s", + "description": "%s", + "project_id": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "created_at": "%s", + "updated_at": "%s", + "links": { + "self": "dummylink" + } +}`, idZone1, + nameZone1, + descriptionZone1, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, +) + +// GetResponseStruct mocked actual +var GetResponseStruct = zones.Zone{ + ID: idZone1, + PoolID: "", + ProjectID: tenantID, + Name: nameZone1, + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Description: descriptionZone1, + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + Action: "", + Links: map[string]interface{}{ + "self": "dummylink", + }, +} + +// CreateZoneRequest is a sample request to create a zone. +var CreateZoneRequest = fmt.Sprintf(`{ + "description": "%s", + "email": "joe@example.org", + "name": "%s", + "ttl": 7200, + "type": "PRIMARY" +}`, + descriptionZone1, + nameZone1, +) + +// CreateZoneResponse is a sample response to a create request. +var CreateZoneResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "description": "%s", + "project_id": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "CREATING", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "created_at": "%s", + "updated_at": null, + "links": { + "self": "dummylink" + } +}`, idZone1, + nameZone1, + descriptionZone1, + tenantID, + zoneCreatedAt, +) + +// CreatedZone is the expected created zone +var CreatedZone = zones.Zone{ + ID: idZone1, + Name: nameZone1, + Description: descriptionZone1, + ProjectID: tenantID, + PoolID: "", + Email: "", + TTL: 0, + Serial: 0, + Status: "CREATING", + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: time.Time{}, + // Action: "", + Links: map[string]interface{}{ + "self": "dummylink", + }, +} + +// UpdateZoneRequest is a sample request to update a zone. +var UpdateZoneRequest = fmt.Sprintf(` +{ + "ttl": 600, + "description": "%s", + "masters": [], + "email": "" +}`, + descriptionZone1Update, +) + +// UpdateZoneResponse is a sample response to update a zone. +var UpdateZoneResponse = fmt.Sprintf(`{ + "id": "%s", + "name": "%s", + "description": "%s", + "project_id": "%s", + "pool_id": "", + "email": "", + "ttl": 0, + "serial": 0, + "status": "ACTIVE", + "masters": [], + "type": "", + "transferred_at": null, + "version": 1, + "created_at": "%s", + "updated_at": "%s", + "links": { + "self": "dummylink" + } +}`, idZone1, + nameZone1, + descriptionZone1Update, + tenantID, + zoneCreatedAt, + zoneUpdatedAt, +) + +// UpdatedZone is the expected updated zone +var UpdatedZone = zones.Zone{ + ID: idZone1, + Name: nameZone1, + Description: descriptionZone1Update, + ProjectID: tenantID, + PoolID: "", + Email: "", + TTL: 0, + Serial: 0, + Status: "ACTIVE", + Masters: []string{}, + Type: "", + TransferredAt: time.Time{}, + Version: 1, + CreatedAt: ZoneCreatedAt, + UpdatedAt: ZoneUpdatedAt, + // Action: "", + Links: map[string]interface{}{ + "self": "dummylink", + }, +} diff --git a/v4/ecl/dns/v2/zones/testing/requests_test.go b/v4/ecl/dns/v2/zones/testing/requests_test.go new file mode 100644 index 0000000..0d4cd94 --- /dev/null +++ b/v4/ecl/dns/v2/zones/testing/requests_test.go @@ -0,0 +1,151 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/dns/v2/zones" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, ListResponse) + }) + + count := 0 + err := zones.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := zones.ExtractZones(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedZonesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDNSZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, ListResponse) + }) + + allPages, err := zones.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := zones.ExtractZones(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s", idZone1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, GetResponse) + }) + + actual, err := zones.Get(fakeclient.ServiceClient(), idZone1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GetResponseStruct, actual) +} + +func TestCreateDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, CreateZoneRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, CreateZoneResponse) + }) + + createOpts := zones.CreateOpts{ + Description: descriptionZone1, + Email: "joe@example.org", + Name: nameZone1, + TTL: 7200, + Type: "PRIMARY", + } + + actual, err := zones.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedZone, actual) +} + +func TestUpdateDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s", idZone1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, UpdateZoneRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, UpdateZoneResponse) + }) + + description := descriptionZone1Update + ttl := 600 + masters := make([]string, 0) + email := "" + + updateOpts := zones.UpdateOpts{ + TTL: &ttl, + Description: &description, + Masters: &masters, + Email: &email, + } + + actual, err := zones.Update(fakeclient.ServiceClient(), idZone1, updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UpdatedZone, actual) +} + +func TestDeleteDNSZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/zones/%s", idZone1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + }) + + res := zones.Delete(fakeclient.ServiceClient(), idZone1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/dns/v2/zones/urls.go b/v4/ecl/dns/v2/zones/urls.go new file mode 100644 index 0000000..2f06af5 --- /dev/null +++ b/v4/ecl/dns/v2/zones/urls.go @@ -0,0 +1,11 @@ +package zones + +import "github.com/nttcom/eclcloud/v4" + +func baseURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("zones") +} + +func zoneURL(c *eclcloud.ServiceClient, zoneID string) string { + return c.ServiceURL("zones", zoneID) +} diff --git a/v4/ecl/doc.go b/v4/ecl/doc.go new file mode 100644 index 0000000..c7024cf --- /dev/null +++ b/v4/ecl/doc.go @@ -0,0 +1,14 @@ +/* +Package ecl contains resources for the individual Enterprise Cloud projects +supported in eclcloud. It also includes functions to authenticate to an +Enterprise cloud and for provisioning various service-level clients. + +Example of Creating a Service Client + + ao, err := ecl.AuthOptionsFromEnv() + provider, err := ecl.AuthenticatedClient(ao) + client, err := ecl.NewNetworkV2(client, eclcloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +package ecl diff --git a/v4/ecl/endpoint_location.go b/v4/ecl/endpoint_location.go new file mode 100644 index 0000000..6ae498b --- /dev/null +++ b/v4/ecl/endpoint_location.go @@ -0,0 +1,52 @@ +package ecl + +import ( + "github.com/nttcom/eclcloud/v4" + tokens3 "github.com/nttcom/eclcloud/v4/ecl/identity/v3/tokens" +) + +/* +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 Enterprise Cloud deployment. +*/ +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts eclcloud.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 != eclcloud.AvailabilityPublic { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if (opts.Availability == eclcloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints} + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return eclcloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &eclcloud.ErrEndpointNotFound{} + return "", err +} diff --git a/v4/ecl/errors.go b/v4/ecl/errors.go new file mode 100644 index 0000000..3fe806d --- /dev/null +++ b/v4/ecl/errors.go @@ -0,0 +1,58 @@ +package ecl + +import ( + "fmt" + "github.com/nttcom/eclcloud/v4" + tokens3 "github.com/nttcom/eclcloud/v4/ecl/identity/v3/tokens" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ eclcloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ eclcloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint +// for the given options is found in the v3 catalog +type ErrMultipleMatchingEndpointsV3 struct { + eclcloud.BaseError + Endpoints []tokens3.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV3) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ eclcloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ eclcloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ eclcloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/v4/ecl/identity/v3/endpoints/doc.go b/v4/ecl/identity/v3/endpoints/doc.go new file mode 100644 index 0000000..c0ea6b2 --- /dev/null +++ b/v4/ecl/identity/v3/endpoints/doc.go @@ -0,0 +1,66 @@ +/* +Package endpoints provides information and interaction with the service +endpoints API resource in the Enterprise Cloud Identity service. + +Example to List Endpoints + + serviceID := "e629d6e599d9489fb3ae5d9cc12eaea3" + + listOpts := endpoints.ListOpts{ + ServiceID: serviceID, + } + + allPages, err := endpoints.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allEndpoints, err := endpoints.ExtractEndpoints(allPages) + if err != nil { + panic(err) + } + + for _, endpoint := range allEndpoints { + fmt.Printf("%+v\n", endpoint) + } + +Example to Create an Endpoint + + serviceID := "e629d6e599d9489fb3ae5d9cc12eaea3" + + createOpts := endpoints.CreateOpts{ + Availability: eclcloud.AvailabilityPublic, + Name: "neutron", + Region: "RegionOne", + URL: "https://localhost:9696", + ServiceID: serviceID, + } + + endpoint, err := endpoints.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + + +Example to Update an Endpoint + + endpointID := "ad59deeec5154d1fa0dcff518596f499" + + updateOpts := endpoints.UpdateOpts{ + Region: "RegionTwo", + } + + endpoint, err := endpoints.Update(identityClient, endpointID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Endpoint + + endpointID := "ad59deeec5154d1fa0dcff518596f499" + err := endpoints.Delete(identityClient, endpointID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package endpoints diff --git a/v4/ecl/identity/v3/endpoints/requests.go b/v4/ecl/identity/v3/endpoints/requests.go new file mode 100644 index 0000000..03009d7 --- /dev/null +++ b/v4/ecl/identity/v3/endpoints/requests.go @@ -0,0 +1,136 @@ +package endpoints + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type CreateOptsBuilder interface { + ToEndpointCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains the subset of Endpoint attributes that should be used +// to create an Endpoint. +type CreateOpts struct { + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `json:"interface" required:"true"` + + // Name is the name of the Endpoint. + Name string `json:"name" required:"true"` + + // Region is the region the Endpoint is located in. + // This field can be omitted or left as a blank string. + Region string `json:"region,omitempty"` + + // URL is the url of the Endpoint. + URL string `json:"url" required:"true"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `json:"service_id" required:"true"` +} + +// ToEndpointCreateMap builds a request body from the Endpoint Create options. +func (opts CreateOpts) ToEndpointCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "endpoint") +} + +// Create inserts a new Endpoint into the service catalog. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToEndpointCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), &b, &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add parameters to the List request. +type ListOptsBuilder interface { + ToEndpointListParams() (string, error) +} + +// ListOpts allows finer control over the endpoints returned by a List call. +// All fields are optional. +type ListOpts struct { + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `q:"interface"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `q:"service_id"` + + // RegionID is the ID of the region the Endpoint refers to. + RegionID int `q:"region_id"` +} + +// ToEndpointListParams builds a list request from the List options. +func (opts ListOpts) ToEndpointListParams() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates endpoints in a paginated collection, optionally filtered +// by ListOpts criteria. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + u := listURL(client) + if opts != nil { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + } + return pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page { + return EndpointPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add parameters to the Update request. +type UpdateOptsBuilder interface { + ToEndpointUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the subset of Endpoint attributes that should be used to +// update an Endpoint. +type UpdateOpts struct { + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `json:"interface,omitempty"` + + // Name is the name of the Endpoint. + Name string `json:"name,omitempty"` + + // Region is the region the Endpoint is located in. + // This field can be omitted or left as a blank string. + Region string `json:"region,omitempty"` + + // URL is the url of the Endpoint. + URL string `json:"url,omitempty"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `json:"service_id,omitempty"` +} + +// ToEndpointUpdateMap builds an update request body from the Update options. +func (opts UpdateOpts) ToEndpointUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "endpoint") +} + +// Update changes an existing endpoint with new data. +func Update(client *eclcloud.ServiceClient, endpointID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToEndpointUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(endpointURL(client, endpointID), &b, &r.Body, nil) + return +} + +// Delete removes an endpoint from the service catalog. +func Delete(client *eclcloud.ServiceClient, endpointID string) (r DeleteResult) { + _, r.Err = client.Delete(endpointURL(client, endpointID), nil) + return +} diff --git a/v4/ecl/identity/v3/endpoints/results.go b/v4/ecl/identity/v3/endpoints/results.go new file mode 100644 index 0000000..33b3937 --- /dev/null +++ b/v4/ecl/identity/v3/endpoints/results.go @@ -0,0 +1,80 @@ +package endpoints + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.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) { + var s struct { + Endpoint *Endpoint `json:"endpoint"` + } + err := r.ExtractInto(&s) + return s.Endpoint, err +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as an Endpoint. +type CreateResult struct { + commonResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as an Endpoint. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Endpoint describes the entry point for another service's API. +type Endpoint struct { + // ID is the unique ID of the endpoint. + ID string `json:"id"` + + // Availability is the interface type of the Endpoint (admin, internal, + // or public), referenced by the eclcloud.Availability type. + Availability eclcloud.Availability `json:"interface"` + + // Name is the name of the Endpoint. + Name string `json:"name"` + + // Region is the region the Endpoint is located in. + Region string `json:"region"` + + // ServiceID is the ID of the service the Endpoint refers to. + ServiceID string `json:"service_id"` + + // URL is the url of the Endpoint. + URL string `json:"url"` +} + +// EndpointPage is a single page of Endpoint results. +type EndpointPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if no Endpoints were returned. +func (r EndpointPage) IsEmpty() (bool, error) { + es, err := ExtractEndpoints(r) + return len(es) == 0, err +} + +// ExtractEndpoints extracts an Endpoint slice from a Page. +func ExtractEndpoints(r pagination.Page) ([]Endpoint, error) { + var s struct { + Endpoints []Endpoint `json:"endpoints"` + } + err := (r.(EndpointPage)).ExtractInto(&s) + return s.Endpoints, err +} diff --git a/v4/ecl/identity/v3/endpoints/urls.go b/v4/ecl/identity/v3/endpoints/urls.go new file mode 100644 index 0000000..124bcd4 --- /dev/null +++ b/v4/ecl/identity/v3/endpoints/urls.go @@ -0,0 +1,11 @@ +package endpoints + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("endpoints") +} + +func endpointURL(client *eclcloud.ServiceClient, endpointID string) string { + return client.ServiceURL("endpoints", endpointID) +} diff --git a/v4/ecl/identity/v3/groups/doc.go b/v4/ecl/identity/v3/groups/doc.go new file mode 100644 index 0000000..40afa1d --- /dev/null +++ b/v4/ecl/identity/v3/groups/doc.go @@ -0,0 +1,60 @@ +/* +Package groups manages and retrieves Groups in the Enterprise Cloud Identity Service. + +Example to List Groups + + listOpts := groups.ListOpts{ + DomainID: "default", + } + + allPages, err := groups.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + panic(err) + } + + for _, group := range allGroups { + fmt.Printf("%+v\n", group) + } + +Example to Create a Group + + createOpts := groups.CreateOpts{ + Name: "groupname", + DomainID: "default", + Extra: map[string]interface{}{ + "email": "groupname@example.com", + } + } + + group, err := groups.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Group + + groupID := "0fe36e73809d46aeae6705c39077b1b3" + + updateOpts := groups.UpdateOpts{ + Description: "Updated Description for group", + } + + group, err := groups.Update(identityClient, groupID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Group + + groupID := "0fe36e73809d46aeae6705c39077b1b3" + err := groups.Delete(identityClient, groupID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package groups diff --git a/v4/ecl/identity/v3/groups/errors.go b/v4/ecl/identity/v3/groups/errors.go new file mode 100644 index 0000000..98e6fe4 --- /dev/null +++ b/v4/ecl/identity/v3/groups/errors.go @@ -0,0 +1,17 @@ +package groups + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v4/ecl/identity/v3/groups/requests.go b/v4/ecl/identity/v3/groups/requests.go new file mode 100644 index 0000000..95e8436 --- /dev/null +++ b/v4/ecl/identity/v3/groups/requests.go @@ -0,0 +1,179 @@ +package groups + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" + "net/url" + "strings" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToGroupListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Name filters the response by group name. + Name string `q:"name"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToGroupListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the Groups to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return GroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single group, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a group. +type CreateOpts struct { + // Name is the name of the new group. + Name string `json:"name" required:"true"` + + // Description is a description of the group. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the group belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the group. + Extra map[string]interface{} `json:"-"` +} + +// ToGroupCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToGroupCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "group") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["group"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new Group. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToGroupUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a group. +type UpdateOpts struct { + // Name is the name of the new group. + Name string `json:"name,omitempty"` + + // Description is a description of the group. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the group belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the group. + Extra map[string]interface{} `json:"-"` +} + +// ToGroupUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToGroupUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "group") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["group"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing Group. +func Update(client *eclcloud.ServiceClient, groupID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToGroupUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, groupID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a group. +func Delete(client *eclcloud.ServiceClient, groupID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, groupID), nil) + return +} diff --git a/v4/ecl/identity/v3/groups/results.go b/v4/ecl/identity/v3/groups/results.go new file mode 100644 index 0000000..2536ce1 --- /dev/null +++ b/v4/ecl/identity/v3/groups/results.go @@ -0,0 +1,131 @@ +package groups + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/internal" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Group helps manage related users. +type Group struct { + // Description describes the group purpose. + Description string `json:"description"` + + // DomainID is the domain ID the group belongs to. + DomainID string `json:"domain_id"` + + // ID is the unique ID of the group. + ID string `json:"id"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // Links contains referencing links to the group. + Links map[string]interface{} `json:"links"` + + // Name is the name of the group. + Name string `json:"name"` +} + +func (r *Group) UnmarshalJSON(b []byte) error { + type tmp Group + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Group(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Group{}, resultMap) + } + } + + return err +} + +type groupResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Group. +type GetResult struct { + groupResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Group. +type CreateResult struct { + groupResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Group. +type UpdateResult struct { + groupResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// GroupPage is a single page of Group results. +type GroupPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Groups contains any results. +func (r GroupPage) IsEmpty() (bool, error) { + groups, err := ExtractGroups(r) + return len(groups) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r GroupPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractGroups returns a slice of Groups contained in a single page of results. +func ExtractGroups(r pagination.Page) ([]Group, error) { + var s struct { + Groups []Group `json:"groups"` + } + err := (r.(GroupPage)).ExtractInto(&s) + return s.Groups, err +} + +// Extract interprets any group results as a Group. +func (r groupResult) Extract() (*Group, error) { + var s struct { + Group *Group `json:"group"` + } + err := r.ExtractInto(&s) + return s.Group, err +} diff --git a/v4/ecl/identity/v3/groups/urls.go b/v4/ecl/identity/v3/groups/urls.go new file mode 100644 index 0000000..4568d9f --- /dev/null +++ b/v4/ecl/identity/v3/groups/urls.go @@ -0,0 +1,23 @@ +package groups + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("groups") +} + +func getURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("groups") +} + +func updateURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID) +} + +func deleteURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID) +} diff --git a/v4/ecl/identity/v3/projects/doc.go b/v4/ecl/identity/v3/projects/doc.go new file mode 100644 index 0000000..f2e73a8 --- /dev/null +++ b/v4/ecl/identity/v3/projects/doc.go @@ -0,0 +1,58 @@ +/* +Package projects manages and retrieves Projects in the Enterprise Cloud Identity +Service. + +Example to List Projects + + listOpts := projects.ListOpts{ + Enabled: eclcloud.Enabled, + } + + allPages, err := projects.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, project := range allProjects { + fmt.Printf("%+v\n", project) + } + +Example to Create a Project + + createOpts := projects.CreateOpts{ + Name: "project_name", + Description: "Project Description" + } + + project, err := projects.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + + updateOpts := projects.UpdateOpts{ + Enabled: eclcloud.Disabled, + } + + project, err := projects.Update(identityClient, projectID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + err := projects.Delete(identityClient, projectID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package projects diff --git a/v4/ecl/identity/v3/projects/errors.go b/v4/ecl/identity/v3/projects/errors.go new file mode 100644 index 0000000..7be97d8 --- /dev/null +++ b/v4/ecl/identity/v3/projects/errors.go @@ -0,0 +1,17 @@ +package projects + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v4/ecl/identity/v3/projects/requests.go b/v4/ecl/identity/v3/projects/requests.go new file mode 100644 index 0000000..0ed1889 --- /dev/null +++ b/v4/ecl/identity/v3/projects/requests.go @@ -0,0 +1,173 @@ +package projects + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" + "net/url" + "strings" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToProjectListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Enabled filters the response by enabled projects. + Enabled *bool `q:"enabled"` + + // IsDomain filters the response by projects that are domains. + // Setting this to true is effectively listing domains. + IsDomain *bool `q:"is_domain"` + + // Name filters the response by project name. + Name string `q:"name"` + + // ParentID filters the response by projects of a given parent project. + ParentID string `q:"parent_id"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToProjectListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToProjectListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the Projects to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToProjectListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single project, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToProjectCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a project. +type CreateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the project status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // IsDomain indicates if this project is a domain. + IsDomain *bool `json:"is_domain,omitempty"` + + // Name is the name of the project. + Name string `json:"name" required:"true"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToProjectCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "project") +} + +// Create creates a new Project. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToProjectCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a project. +func Delete(client *eclcloud.ServiceClient, projectID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, projectID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToProjectUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a project. +type UpdateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the project status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // IsDomain indicates if this project is a domain. + IsDomain *bool `json:"is_domain,omitempty"` + + // Name is the name of the project. + Name string `json:"name,omitempty"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "project") +} + +// Update modifies the attributes of a project. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToProjectUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/identity/v3/projects/results.go b/v4/ecl/identity/v3/projects/results.go new file mode 100644 index 0000000..87422ba --- /dev/null +++ b/v4/ecl/identity/v3/projects/results.go @@ -0,0 +1,103 @@ +package projects + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type projectResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a Project. +type GetResult struct { + projectResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Project. +type CreateResult struct { + projectResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Project. +type UpdateResult struct { + projectResult +} + +// Project represents an Enterprise Cloud Identity Project. +type Project struct { + // IsDomain indicates whether the project is a domain. + IsDomain bool `json:"is_domain"` + + // Description is the description of the project. + Description string `json:"description"` + + // DomainID is the domain ID the project belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the project is enabled. + Enabled bool `json:"enabled"` + + // ID is the unique ID of the project. + ID string `json:"id"` + + // Name is the name of the project. + Name string `json:"name"` + + // ParentID is the parent_id of the project. + ParentID string `json:"parent_id"` +} + +// ProjectPage is a single page of Project results. +type ProjectPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Projects contains any results. +func (r ProjectPage) IsEmpty() (bool, error) { + projects, err := ExtractProjects(r) + return len(projects) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r ProjectPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractProjects returns a slice of Projects contained in a single page of +// results. +func ExtractProjects(r pagination.Page) ([]Project, error) { + var s struct { + Projects []Project `json:"projects"` + } + err := (r.(ProjectPage)).ExtractInto(&s) + return s.Projects, err +} + +// Extract interprets any projectResults as a Project. +func (r projectResult) Extract() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} diff --git a/v4/ecl/identity/v3/projects/urls.go b/v4/ecl/identity/v3/projects/urls.go new file mode 100644 index 0000000..433b03d --- /dev/null +++ b/v4/ecl/identity/v3/projects/urls.go @@ -0,0 +1,23 @@ +package projects + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func getURL(client *eclcloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func deleteURL(client *eclcloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func updateURL(client *eclcloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} diff --git a/v4/ecl/identity/v3/roles/doc.go b/v4/ecl/identity/v3/roles/doc.go new file mode 100644 index 0000000..2c0dfeb --- /dev/null +++ b/v4/ecl/identity/v3/roles/doc.go @@ -0,0 +1,135 @@ +/* +Package roles provides information and interaction with the roles API +resource for the Enterprise Cloud Identity service. + +Example to List Roles + + listOpts := roles.ListOpts{ + DomainID: "default", + } + + allPages, err := roles.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRoles, err := roles.ExtractRoles(allPages) + if err != nil { + panic(err) + } + + for _, role := range allRoles { + fmt.Printf("%+v\n", role) + } + +Example to Create a Role + + createOpts := roles.CreateOpts{ + Name: "read-only-admin", + DomainID: "default", + Extra: map[string]interface{}{ + "description": "this role grants read-only privilege cross tenant", + } + } + + role, err := roles.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Role + + roleID := "0fe36e73809d46aeae6705c39077b1b3" + + updateOpts := roles.UpdateOpts{ + Name: "read only admin", + } + + role, err := roles.Update(identityClient, roleID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Role + + roleID := "0fe36e73809d46aeae6705c39077b1b3" + err := roles.Delete(identityClient, roleID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Role Assignments + + listOpts := roles.ListAssignmentsOpts{ + UserID: "97061de2ed0647b28a393c36ab584f39", + ScopeProjectID: "9df1a02f5eb2416a9781e8b0c022d3ae", + } + + allPages, err := roles.ListAssignments(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRoles, err := roles.ExtractRoleAssignments(allPages) + if err != nil { + panic(err) + } + + for _, role := range allRoles { + fmt.Printf("%+v\n", role) + } + +Example to List Role Assignments for a User on a Project + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + userID := "9df1a02f5eb2416a9781e8b0c022d3ae" + listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{ + UserID: userID, + ProjectID: projectID, + } + + allPages, err := roles.ListAssignmentsOnResource(identityClient, listAssignmentsOnResourceOpts).AllPages() + if err != nil { + panic(err) + } + + allRoles, err := roles.ExtractRoles(allPages) + if err != nil { + panic(err) + } + + for _, role := range allRoles { + fmt.Printf("%+v\n", role) + } + +Example to Assign a Role to a User in a Project + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + userID := "9df1a02f5eb2416a9781e8b0c022d3ae" + roleID := "9fe2ff9ee4384b1894a90878d3e92bab" + + err := roles.Assign(identityClient, roleID, roles.AssignOpts{ + UserID: userID, + ProjectID: projectID, + }).ExtractErr() + + if err != nil { + panic(err) + } + +Example to Unassign a Role From a User in a Project + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + userID := "9df1a02f5eb2416a9781e8b0c022d3ae" + roleID := "9fe2ff9ee4384b1894a90878d3e92bab" + + err := roles.Unassign(identityClient, roleID, roles.UnassignOpts{ + UserID: userID, + ProjectID: projectID, + }).ExtractErr() + + if err != nil { + panic(err) + } +*/ +package roles diff --git a/v4/ecl/identity/v3/roles/errors.go b/v4/ecl/identity/v3/roles/errors.go new file mode 100644 index 0000000..b60d7d1 --- /dev/null +++ b/v4/ecl/identity/v3/roles/errors.go @@ -0,0 +1,17 @@ +package roles + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v4/ecl/identity/v3/roles/requests.go b/v4/ecl/identity/v3/roles/requests.go new file mode 100644 index 0000000..8e7bf23 --- /dev/null +++ b/v4/ecl/identity/v3/roles/requests.go @@ -0,0 +1,391 @@ +package roles + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" + "net/url" + "strings" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToRoleListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Name filters the response by role name. + Name string `q:"name"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToRoleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRoleListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the roles to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToRoleListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single role, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToRoleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a role. +type CreateOpts struct { + // Name is the name of the new role. + Name string `json:"name" required:"true"` + + // DomainID is the ID of the domain the role belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the role. + Extra map[string]interface{} `json:"-"` +} + +// ToRoleCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToRoleCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "role") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["role"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new Role. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRoleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToRoleUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a role. +type UpdateOpts struct { + // Name is the name of the new role. + Name string `json:"name,omitempty"` + + // Extra is free-form extra key/value pairs to describe the role. + Extra map[string]interface{} `json:"-"` +} + +// ToRoleUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToRoleUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "role") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["role"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing Role. +func Update(client *eclcloud.ServiceClient, roleID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRoleUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, roleID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a role. +func Delete(client *eclcloud.ServiceClient, roleID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, roleID), nil) + return +} + +// ListAssignmentsOptsBuilder allows extensions to add additional parameters to +// the ListAssignments request. +type ListAssignmentsOptsBuilder interface { + ToRolesListAssignmentsQuery() (string, error) +} + +// ListAssignmentsOpts allows you to query the ListAssignments method. +// Specify one of or a combination of GroupId, RoleId, ScopeDomainId, +// ScopeProjectId, and/or UserId to search for roles assigned to corresponding +// entities. +type ListAssignmentsOpts struct { + // GroupID is the group ID to query. + GroupID string `q:"group.id"` + + // RoleID is the specific role to query assignments to. + RoleID string `q:"role.id"` + + // ScopeDomainID filters the results by the given domain ID. + ScopeDomainID string `q:"scope.domain.id"` + + // ScopeProjectID filters the results by the given Project ID. + ScopeProjectID string `q:"scope.project.id"` + + // UserID filterst he results by the given User ID. + UserID string `q:"user.id"` + + // Effective lists effective assignments at the user, project, and domain + // level, allowing for the effects of group membership. + Effective *bool `q:"effective"` +} + +// ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string. +func (opts ListAssignmentsOpts) ToRolesListAssignmentsQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// ListAssignments enumerates the roles assigned to a specified resource. +func ListAssignments(client *eclcloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager { + url := listAssignmentsURL(client) + if opts != nil { + query, err := opts.ToRolesListAssignmentsQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RoleAssignmentPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAssignmentsOnResourceOpts provides options to list role assignments +// for a user/group on a project/domain +type ListAssignmentsOnResourceOpts struct { + // UserID is the ID of a user to assign a role + // Note: exactly one of UserID or GroupID must be provided + UserID string `xor:"GroupID"` + + // GroupID is the ID of a group to assign a role + // Note: exactly one of UserID or GroupID must be provided + GroupID string `xor:"UserID"` + + // ProjectID is the ID of a project to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + ProjectID string `xor:"DomainID"` + + // DomainID is the ID of a domain to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + DomainID string `xor:"ProjectID"` +} + +// AssignOpts provides options to assign a role +type AssignOpts struct { + // UserID is the ID of a user to assign a role + // Note: exactly one of UserID or GroupID must be provided + UserID string `xor:"GroupID"` + + // GroupID is the ID of a group to assign a role + // Note: exactly one of UserID or GroupID must be provided + GroupID string `xor:"UserID"` + + // ProjectID is the ID of a project to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + ProjectID string `xor:"DomainID"` + + // DomainID is the ID of a domain to assign a role on + // Note: exactly one of ProjectID or DomainID must be provided + DomainID string `xor:"ProjectID"` +} + +// UnassignOpts provides options to unassign a role +type UnassignOpts struct { + // UserID is the ID of a user to unassign a role + // Note: exactly one of UserID or GroupID must be provided + UserID string `xor:"GroupID"` + + // GroupID is the ID of a group to unassign a role + // Note: exactly one of UserID or GroupID must be provided + GroupID string `xor:"UserID"` + + // ProjectID is the ID of a project to unassign a role on + // Note: exactly one of ProjectID or DomainID must be provided + ProjectID string `xor:"DomainID"` + + // DomainID is the ID of a domain to unassign a role on + // Note: exactly one of ProjectID or DomainID must be provided + DomainID string `xor:"ProjectID"` +} + +// ListAssignmentsOnResource is the operation responsible for listing role +// assignments for a user/group on a project/domain. +func ListAssignmentsOnResource(client *eclcloud.ServiceClient, opts ListAssignmentsOnResourceOpts) pagination.Pager { + // Check xor conditions + _, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return pagination.Pager{Err: err} + } + + // Get corresponding URL + var targetID string + var targetType string + if opts.ProjectID != "" { + targetID = opts.ProjectID + targetType = "projects" + } else { + targetID = opts.DomainID + targetType = "domains" + } + + var actorID string + var actorType string + if opts.UserID != "" { + actorID = opts.UserID + actorType = "users" + } else { + actorID = opts.GroupID + actorType = "groups" + } + + url := listAssignmentsOnResourceURL(client, targetType, targetID, actorType, actorID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Assign is the operation responsible for assigning a role +// to a user/group on a project/domain. +func Assign(client *eclcloud.ServiceClient, roleID string, opts AssignOpts) (r AssignmentResult) { + // Check xor conditions + _, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + r.Err = err + return + } + + // Get corresponding URL + var targetID string + var targetType string + if opts.ProjectID != "" { + targetID = opts.ProjectID + targetType = "projects" + } else { + targetID = opts.DomainID + targetType = "domains" + } + + var actorID string + var actorType string + if opts.UserID != "" { + actorID = opts.UserID + actorType = "users" + } else { + actorID = opts.GroupID + actorType = "groups" + } + + _, r.Err = client.Put(assignURL(client, targetType, targetID, actorType, actorID, roleID), nil, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// Unassign is the operation responsible for unassigning a role +// from a user/group on a project/domain. +func Unassign(client *eclcloud.ServiceClient, roleID string, opts UnassignOpts) (r UnassignmentResult) { + // Check xor conditions + _, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + r.Err = err + return + } + + // Get corresponding URL + var targetID string + var targetType string + if opts.ProjectID != "" { + targetID = opts.ProjectID + targetType = "projects" + } else { + targetID = opts.DomainID + targetType = "domains" + } + + var actorID string + var actorType string + if opts.UserID != "" { + actorID = opts.UserID + actorType = "users" + } else { + actorID = opts.GroupID + actorType = "groups" + } + + _, r.Err = client.Delete(assignURL(client, targetType, targetID, actorType, actorID, roleID), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} diff --git a/v4/ecl/identity/v3/roles/results.go b/v4/ecl/identity/v3/roles/results.go new file mode 100644 index 0000000..db10800 --- /dev/null +++ b/v4/ecl/identity/v3/roles/results.go @@ -0,0 +1,213 @@ +package roles + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/internal" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Role grants permissions to a user. +type Role struct { + // DomainID is the domain ID the role belongs to. + DomainID string `json:"domain_id"` + + // ID is the unique ID of the role. + ID string `json:"id"` + + // Links contains referencing links to the role. + Links map[string]interface{} `json:"links"` + + // Name is the role name + Name string `json:"name"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` +} + +func (r *Role) UnmarshalJSON(b []byte) error { + type tmp Role + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Role(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Role{}, resultMap) + } + } + + return err +} + +type roleResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Role. +type GetResult struct { + roleResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Role +type CreateResult struct { + roleResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Role. +type UpdateResult struct { + roleResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// RolePage is a single page of Role results. +type RolePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Roles contains any results. +func (r RolePage) IsEmpty() (bool, error) { + roles, err := ExtractRoles(r) + return len(roles) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r RolePage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractProjects returns a slice of Roles contained in a single page of +// results. +func ExtractRoles(r pagination.Page) ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := (r.(RolePage)).ExtractInto(&s) + return s.Roles, err +} + +// Extract interprets any roleResults as a Role. +func (r roleResult) Extract() (*Role, error) { + var s struct { + Role *Role `json:"role"` + } + err := r.ExtractInto(&s) + return s.Role, err +} + +// RoleAssignment is the result of a role assignments query. +type RoleAssignment struct { + Role AssignedRole `json:"role,omitempty"` + Scope Scope `json:"scope,omitempty"` + User User `json:"user,omitempty"` + Group Group `json:"group,omitempty"` +} + +// AssignedRole represents a Role in an assignment. +type AssignedRole struct { + ID string `json:"id,omitempty"` +} + +// Scope represents a scope in a Role assignment. +type Scope struct { + Domain Domain `json:"domain,omitempty"` + Project Project `json:"project,omitempty"` +} + +// Domain represents a domain in a role assignment scope. +type Domain struct { + ID string `json:"id,omitempty"` +} + +// Project represents a project in a role assignment scope. +type Project struct { + ID string `json:"id,omitempty"` +} + +// User represents a user in a role assignment scope. +type User struct { + ID string `json:"id,omitempty"` +} + +// Group represents a group in a role assignment scope. +type Group struct { + ID string `json:"id,omitempty"` +} + +// RoleAssignmentPage is a single page of RoleAssignments results. +type RoleAssignmentPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the RoleAssignmentPage contains no results. +func (r RoleAssignmentPage) IsEmpty() (bool, error) { + roleAssignments, err := ExtractRoleAssignments(r) + return len(roleAssignments) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r RoleAssignmentPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + } `json:"links"` + } + err := r.ExtractInto(&s) + return s.Links.Next, err +} + +// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection +// acquired from List. +func ExtractRoleAssignments(r pagination.Page) ([]RoleAssignment, error) { + var s struct { + RoleAssignments []RoleAssignment `json:"role_assignments"` + } + err := (r.(RoleAssignmentPage)).ExtractInto(&s) + return s.RoleAssignments, err +} + +// AssignmentResult represents the result of an assign operation. +// Call ExtractErr method to determine if the request succeeded or failed. +type AssignmentResult struct { + eclcloud.ErrResult +} + +// UnassignmentResult represents the result of an unassign operation. +// Call ExtractErr method to determine if the request succeeded or failed. +type UnassignmentResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/identity/v3/roles/urls.go b/v4/ecl/identity/v3/roles/urls.go new file mode 100644 index 0000000..0a7a7d8 --- /dev/null +++ b/v4/ecl/identity/v3/roles/urls.go @@ -0,0 +1,39 @@ +package roles + +import "github.com/nttcom/eclcloud/v4" + +const ( + rolePath = "roles" +) + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL(rolePath) +} + +func getURL(client *eclcloud.ServiceClient, roleID string) string { + return client.ServiceURL(rolePath, roleID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL(rolePath) +} + +func updateURL(client *eclcloud.ServiceClient, roleID string) string { + return client.ServiceURL(rolePath, roleID) +} + +func deleteURL(client *eclcloud.ServiceClient, roleID string) string { + return client.ServiceURL(rolePath, roleID) +} + +func listAssignmentsURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("role_assignments") +} + +func listAssignmentsOnResourceURL(client *eclcloud.ServiceClient, targetType, targetID, actorType, actorID string) string { + return client.ServiceURL(targetType, targetID, actorType, actorID, rolePath) +} + +func assignURL(client *eclcloud.ServiceClient, targetType, targetID, actorType, actorID, roleID string) string { + return client.ServiceURL(targetType, targetID, actorType, actorID, rolePath, roleID) +} diff --git a/v4/ecl/identity/v3/services/doc.go b/v4/ecl/identity/v3/services/doc.go new file mode 100644 index 0000000..07cd7b7 --- /dev/null +++ b/v4/ecl/identity/v3/services/doc.go @@ -0,0 +1,66 @@ +/* +Package services provides information and interaction with the services API +resource for the Enterprise Cloud Identity service. + +Example to List Services + + listOpts := services.ListOpts{ + ServiceType: "compute", + } + + allPages, err := services.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServices, err := services.ExtractServices(allPages) + if err != nil { + panic(err) + } + + for _, service := range allServices { + fmt.Printf("%+v\n", service) + } + +Example to Create a Service + + createOpts := services.CreateOpts{ + Type: "compute", + Extra: map[string]interface{}{ + "name": "compute-service", + "description": "Compute Service", + }, + } + + service, err := services.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Service + + serviceID := "3c7bbe9a6ecb453ca1789586291380ed" + + var iFalse bool = false + updateOpts := services.UpdateOpts{ + Enabled: &iFalse, + Extra: map[string]interface{}{ + "description": "Disabled Compute Service" + }, + } + + service, err := services.Update(identityClient, serviceID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Service + + serviceID := "3c7bbe9a6ecb453ca1789586291380ed" + err := services.Delete(identityClient, serviceID).ExtractErr() + if err != nil { + panic(err) + } + +*/ +package services diff --git a/v4/ecl/identity/v3/services/requests.go b/v4/ecl/identity/v3/services/requests.go new file mode 100644 index 0000000..0ef3959 --- /dev/null +++ b/v4/ecl/identity/v3/services/requests.go @@ -0,0 +1,154 @@ +package services + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToServiceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a service. +type CreateOpts struct { + // Type is the type of the service. + Type string `json:"type"` + + // Enabled is whether or not the service is enabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the service. + Extra map[string]interface{} `json:"-"` +} + +// ToServiceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToServiceCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "service") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["service"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create adds a new service of the requested type to the catalog. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServiceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// ListOptsBuilder enables extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToServiceListMap() (string, error) +} + +// ListOpts provides options for filtering the List results. +type ListOpts struct { + // ServiceType filter the response by a type of service. + ServiceType string `q:"type"` + + // Name filters the response by a service name. + Name string `q:"name"` +} + +// ToServiceListMap builds a list query from the list options. +func (opts ListOpts) ToServiceListMap() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the services available to a specific user. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServiceListMap() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns additional information about a service, given its ID. +func Get(client *eclcloud.ServiceClient, serviceID string) (r GetResult) { + _, r.Err = client.Get(serviceURL(client, serviceID), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToServiceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a service. +type UpdateOpts struct { + // Type is the type of the service. + Type string `json:"type"` + + // Enabled is whether or not the service is enabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the service. + Extra map[string]interface{} `json:"-"` +} + +// ToServiceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToServiceUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "service") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["service"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing Service. +func Update(client *eclcloud.ServiceClient, serviceID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServiceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, serviceID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete removes an existing service. +// It either deletes all associated endpoints, or fails until all endpoints +// are deleted. +func Delete(client *eclcloud.ServiceClient, serviceID string) (r DeleteResult) { + _, r.Err = client.Delete(serviceURL(client, serviceID), nil) + return +} diff --git a/v4/ecl/identity/v3/services/results.go b/v4/ecl/identity/v3/services/results.go new file mode 100644 index 0000000..06ac6e4 --- /dev/null +++ b/v4/ecl/identity/v3/services/results.go @@ -0,0 +1,130 @@ +package services + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/internal" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type serviceResult struct { + eclcloud.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 serviceResult) Extract() (*Service, error) { + var s struct { + Service *Service `json:"service"` + } + err := r.ExtractInto(&s) + return s.Service, err +} + +// CreateResult is the response from a Create request. Call its Extract method +// to interpret it as a Service. +type CreateResult struct { + serviceResult +} + +// GetResult is the response from a Get request. Call its Extract method +// to interpret it as a Service. +type GetResult struct { + serviceResult +} + +// UpdateResult is the response from an Update request. Call its Extract method +// to interpret it as a Service. +type UpdateResult struct { + serviceResult +} + +// DeleteResult is the response from a Delete request. Call its ExtractErr +// method to interpret it as a Service. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Service represents an Enterprise Cloud Service. +type Service struct { + // ID is the unique ID of the service. + ID string `json:"id"` + + // Type is the type of the service. + Type string `json:"type"` + + // Enabled is whether or not the service is enabled. + Enabled bool `json:"enabled"` + + // Links contains referencing links to the service. + Links map[string]interface{} `json:"links"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` +} + +func (r *Service) UnmarshalJSON(b []byte) error { + type tmp Service + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Service(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Service{}, resultMap) + } + } + + return err +} + +// ServicePage is a single page of Service results. +type ServicePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the ServicePage contains no results. +func (r ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(r) + return len(services) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r ServicePage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractServices extracts a slice of Services from a Collection acquired +// from List. +func ExtractServices(r pagination.Page) ([]Service, error) { + var s struct { + Services []Service `json:"services"` + } + err := (r.(ServicePage)).ExtractInto(&s) + return s.Services, err +} diff --git a/v4/ecl/identity/v3/services/urls.go b/v4/ecl/identity/v3/services/urls.go new file mode 100644 index 0000000..0a25440 --- /dev/null +++ b/v4/ecl/identity/v3/services/urls.go @@ -0,0 +1,19 @@ +package services + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func serviceURL(client *eclcloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} + +func updateURL(client *eclcloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} diff --git a/v4/ecl/identity/v3/tokens/doc.go b/v4/ecl/identity/v3/tokens/doc.go new file mode 100644 index 0000000..6c9f6b3 --- /dev/null +++ b/v4/ecl/identity/v3/tokens/doc.go @@ -0,0 +1,105 @@ +/* +Package tokens provides information and interaction with the token API +resource for the Enterprise Cloud Identity service. + +Example to Create a Token From a Username and Password + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Username, Password, and Domain + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainID: "default", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + + authOptions = tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainName: "default", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Token + + authOptions := tokens.AuthOptions{ + TokenID: "token_id", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project ID Scope + + scope := tokens.Scope{ + ProjectID: "0fe36e73809d46aeae6705c39077b1b3", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Domain ID Scope + + scope := tokens.Scope{ + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project Name Scope + + scope := tokens.Scope{ + ProjectName: "project_name", + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +*/ +package tokens diff --git a/v4/ecl/identity/v3/tokens/requests.go b/v4/ecl/identity/v3/tokens/requests.go new file mode 100644 index 0000000..8d53585 --- /dev/null +++ b/v4/ecl/identity/v3/tokens/requests.go @@ -0,0 +1,162 @@ +package tokens + +import "github.com/nttcom/eclcloud/v4" + +// 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 +} + +// AuthOptionsBuilder provides the ability for extensions to add additional +// parameters to AuthOptions. Extensions must satisfy all required methods. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool +} + +// AuthOptions represents options for authenticating a user. +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 `json:"-"` + + // 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 string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Eclcloud + // to cache your credentials in memory, and to allow Eclcloud 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 `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` + + Scope Scope `json:"-"` +} + +// ToTokenV3CreateMap builds a request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + eclcloudAuthOpts := eclcloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + ApplicationCredentialID: opts.ApplicationCredentialID, + ApplicationCredentialName: opts.ApplicationCredentialName, + ApplicationCredentialSecret: opts.ApplicationCredentialSecret, + } + + return eclcloudAuthOpts.ToTokenV3CreateMap(scope) +} + +// ToTokenV3CreateMap builds a scope request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + scope := eclcloud.AuthScope(opts.Scope) + + eclcloudAuthOpts := eclcloud.AuthOptions{ + Scope: &scope, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + } + + return eclcloudAuthOpts.ToTokenV3ScopeMap() +} + +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +func subjectTokenHeaders(c *eclcloud.ServiceClient, subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope +// of an existing token. +func Create(c *eclcloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(tokenURL(c), b, &r.Body, &eclcloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + }) + r.Err = err + if resp != nil { + r.Header = resp.Header + } + return +} + +// Get validates and retrieves information about another token. +func Get(c *eclcloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &eclcloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 203}, + }) + if resp != nil { + r.Err = err + r.Header = resp.Header + } + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *eclcloud.ServiceClient, token string) (bool, error) { + resp, err := c.Head(tokenURL(c), &eclcloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 200 || resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *eclcloud.ServiceClient, token string) (r RevokeResult) { + _, r.Err = c.Delete(tokenURL(c), &eclcloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + }) + return +} diff --git a/v4/ecl/identity/v3/tokens/results.go b/v4/ecl/identity/v3/tokens/results.go new file mode 100644 index 0000000..95e28a8 --- /dev/null +++ b/v4/ecl/identity/v3/tokens/results.go @@ -0,0 +1,170 @@ +package tokens + +import ( + "github.com/nttcom/eclcloud/v4" + "time" +) + +// 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 `json:"id"` + Region string `json:"region"` + RegionID string `json:"region_id"` + Interface string `json:"interface"` + URL string `json:"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 `json:"id"` + + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if Enterprise Cloud defines a type for the + // service. Otherwise, for provider-specific services, the provider may + // assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// Domain provides information about the domain to which this token grants +// access. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// User represents a user resource that exists in the Identity Service. +type User struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// Role provides information about roles to which User is authorized. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Project provides information about project to which User is authorized. +type Project struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// commonResult is the response from a request. A commonResult has various +// methods which can be used to extract different details about the result. +type commonResult struct { + eclcloud.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) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractUser returns the User that is the owner of the Token. +func (r commonResult) ExtractUser() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ExtractRoles returns Roles to which User is authorized. +func (r commonResult) ExtractRoles() ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := r.ExtractInto(&s) + return s.Roles, err +} + +// ExtractProject returns Project to which User is authorized. +func (r commonResult) ExtractProject() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} + +// CreateResult is the response from a Create request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the response from a Get request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type GetResult struct { + commonResult +} + +// RevokeResult is response from a Revoke request. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services +// in an Enterprise Cloud provider. Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/v4/ecl/identity/v3/tokens/urls.go b/v4/ecl/identity/v3/tokens/urls.go new file mode 100644 index 0000000..9435310 --- /dev/null +++ b/v4/ecl/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/nttcom/eclcloud/v4" + +func tokenURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/v4/ecl/identity/v3/users/doc.go b/v4/ecl/identity/v3/users/doc.go new file mode 100644 index 0000000..f72ed45 --- /dev/null +++ b/v4/ecl/identity/v3/users/doc.go @@ -0,0 +1,172 @@ +/* +Package users manages and retrieves Users in the Enterprise Cloud Identity Service. + +Example to List Users + + listOpts := users.ListOpts{ + DomainID: "default", + } + + allPages, err := users.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +Example to Create a User + + projectID := "a99e9b4e620e4db09a2dfb6e42a01e66" + + createOpts := users.CreateOpts{ + Name: "username", + DomainID: "default", + DefaultProjectID: projectID, + Enabled: eclcloud.Enabled, + Password: "supersecret", + Extra: map[string]interface{}{ + "email": "username@example.com", + } + } + + user, err := users.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a User + + userID := "0fe36e73809d46aeae6705c39077b1b3" + + updateOpts := users.UpdateOpts{ + Enabled: eclcloud.Disabled, + } + + user, err := users.Update(identityClient, userID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Change Password of a User + + userID := "0fe36e73809d46aeae6705c39077b1b3" + originalPassword := "secretsecret" + password := "new_secretsecret" + + changePasswordOpts := users.ChangePasswordOpts{ + OriginalPassword: originalPassword, + Password: password, + } + + err := users.ChangePassword(identityClient, userID, changePasswordOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a User + + userID := "0fe36e73809d46aeae6705c39077b1b3" + err := users.Delete(identityClient, userID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Groups a User Belongs To + + userID := "0fe36e73809d46aeae6705c39077b1b3" + + allPages, err := users.ListGroups(identityClient, userID).AllPages() + if err != nil { + panic(err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + panic(err) + } + + for _, group := range allGroups { + fmt.Printf("%+v\n", group) + } + +Example to Add a User to a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + userID := "0fe36e73809d46aeae6705c39077b1b3" + err := users.AddToGroup(identityClient, groupID, userID).ExtractErr() + + if err != nil { + panic(err) + } + +Example to Check Whether a User Belongs to a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + userID := "0fe36e73809d46aeae6705c39077b1b3" + ok, err := users.IsMemberOfGroup(identityClient, groupID, userID).Extract() + if err != nil { + panic(err) + } + + if ok { + fmt.Printf("user %s is a member of group %s\n", userID, groupID) + } + +Example to Remove a User from a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + userID := "0fe36e73809d46aeae6705c39077b1b3" + err := users.RemoveFromGroup(identityClient, groupID, userID).ExtractErr() + + if err != nil { + panic(err) + } + +Example to List Projects a User Belongs To + + userID := "0fe36e73809d46aeae6705c39077b1b3" + + allPages, err := users.ListProjects(identityClient, userID).AllPages() + if err != nil { + panic(err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, project := range allProjects { + fmt.Printf("%+v\n", project) + } + +Example to List Users in a Group + + groupID := "bede500ee1124ae9b0006ff859758b3a" + listOpts := users.ListOpts{ + DomainID: "default", + } + + allPages, err := users.ListInGroup(identityClient, groupID, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +*/ +package users diff --git a/v4/ecl/identity/v3/users/errors.go b/v4/ecl/identity/v3/users/errors.go new file mode 100644 index 0000000..0f0b798 --- /dev/null +++ b/v4/ecl/identity/v3/users/errors.go @@ -0,0 +1,17 @@ +package users + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v4/ecl/identity/v3/users/requests.go b/v4/ecl/identity/v3/users/requests.go new file mode 100644 index 0000000..cd8a339 --- /dev/null +++ b/v4/ecl/identity/v3/users/requests.go @@ -0,0 +1,337 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/identity/v3/groups" + "github.com/nttcom/eclcloud/v4/ecl/identity/v3/projects" + "github.com/nttcom/eclcloud/v4/pagination" + "net/http" + "net/url" + "strings" +) + +// Option is a specific option defined at the API to enable features +// on a user account. +type Option string + +const ( + IgnoreChangePasswordUponFirstUse Option = "ignore_change_password_upon_first_use" + IgnorePasswordExpiry Option = "ignore_password_expiry" + IgnoreLockoutFailureAttempts Option = "ignore_lockout_failure_attempts" + MultiFactorAuthRules Option = "multi_factor_auth_rules" + MultiFactorAuthEnabled Option = "multi_factor_auth_enabled" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToUserListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Enabled filters the response by enabled users. + Enabled *bool `q:"enabled"` + + // IdpID filters the response by an Identity Provider ID. + IdPID string `q:"idp_id"` + + // Name filters the response by username. + Name string `q:"name"` + + // PasswordExpiresAt filters the response based on expiring passwords. + PasswordExpiresAt string `q:"password_expires_at"` + + // ProtocolID filters the response by protocol ID. + ProtocolID string `q:"protocol_id"` + + // UniqueID filters the response by unique ID. + UniqueID string `q:"unique_id"` + + // Filters filters the response by custom filters such as + // 'name__contains=foo' + Filters map[string]string `q:"-"` +} + +// ToUserListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToUserListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + for k, v := range opts.Filters { + i := strings.Index(k, "__") + if i > 0 && i < len(k)-2 { + params.Add(k, v) + } else { + return "", InvalidListFilter{FilterName: k} + } + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List enumerates the Users to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single user, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a user. +type CreateOpts struct { + // Name is the name of the new user. + Name string `json:"name" required:"true"` + + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id,omitempty"` + + // Description is a description of the user. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the user belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the user status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the user. + Extra map[string]interface{} `json:"-"` + + // Options are defined options in the API to enable certain features. + Options map[Option]interface{} `json:"options,omitempty"` + + // Password is the password of the new user. + Password string `json:"password,omitempty"` +} + +// ToUserCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["user"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new User. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a user account. +type UpdateOpts struct { + // Name is the name of the new user. + Name string `json:"name,omitempty"` + + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id,omitempty"` + + // Description is a description of the user. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the user belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the user status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the user. + Extra map[string]interface{} `json:"-"` + + // Options are defined options in the API to enable certain features. + Options map[Option]interface{} `json:"options,omitempty"` + + // Password is the password of the new user. + Password string `json:"password,omitempty"` +} + +// ToUserUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToUserUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["user"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing User. +func Update(client *eclcloud.ServiceClient, userID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToUserUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, userID), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ChangePasswordOptsBuilder allows extensions to add additional parameters to +// the ChangePassword request. +type ChangePasswordOptsBuilder interface { + ToUserChangePasswordMap() (map[string]interface{}, error) +} + +// ChangePasswordOpts provides options for changing password for a user. +type ChangePasswordOpts struct { + // OriginalPassword is the original password of the user. + OriginalPassword string `json:"original_password"` + + // Password is the new password of the user. + Password string `json:"password"` +} + +// ToUserChangePasswordMap formats a ChangePasswordOpts into a ChangePassword request. +func (opts ChangePasswordOpts) ToUserChangePasswordMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + return b, nil +} + +// ChangePassword changes password for a user. +func ChangePassword(client *eclcloud.ServiceClient, userID string, opts ChangePasswordOptsBuilder) (r ChangePasswordResult) { + b, err := opts.ToUserChangePasswordMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(changePasswordURL(client, userID), &b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// Delete deletes a user. +func Delete(client *eclcloud.ServiceClient, userID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, userID), nil) + return +} + +// ListGroups enumerates groups user belongs to. +func ListGroups(client *eclcloud.ServiceClient, userID string) pagination.Pager { + url := listGroupsURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return groups.GroupPage{LinkedPageBase: pagination.LinkedPageBase{PageResult: r}} + }) +} + +// AddToGroup adds a user to a group. +func AddToGroup(client *eclcloud.ServiceClient, groupID, userID string) (r AddToGroupResult) { + url := addToGroupURL(client, groupID, userID) + _, r.Err = client.Put(url, nil, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// IsMemberOfGroup checks whether a user belongs to a group. +func IsMemberOfGroup(client *eclcloud.ServiceClient, groupID, userID string) (r IsMemberOfGroupResult) { + url := isMemberOfGroupURL(client, groupID, userID) + var response *http.Response + response, r.Err = client.Head(url, &eclcloud.RequestOpts{ + OkCodes: []int{204, 404}, + }) + if r.Err == nil && response != nil { + if (*response).StatusCode == 204 { + r.isMember = true + } + } + + return +} + +// RemoveFromGroup removes a user from a group. +func RemoveFromGroup(client *eclcloud.ServiceClient, groupID, userID string) (r RemoveFromGroupResult) { + url := removeFromGroupURL(client, groupID, userID) + _, r.Err = client.Delete(url, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// ListProjects enumerates groups user belongs to. +func ListProjects(client *eclcloud.ServiceClient, userID string) pagination.Pager { + url := listProjectsURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return projects.ProjectPage{LinkedPageBase: pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListInGroup enumerates users that belong to a group. +func ListInGroup(client *eclcloud.ServiceClient, groupID string, opts ListOptsBuilder) pagination.Pager { + url := listInGroupURL(client, groupID) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v4/ecl/identity/v3/users/results.go b/v4/ecl/identity/v3/users/results.go new file mode 100644 index 0000000..c8320e1 --- /dev/null +++ b/v4/ecl/identity/v3/users/results.go @@ -0,0 +1,178 @@ +package users + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/internal" + "github.com/nttcom/eclcloud/v4/pagination" + "time" +) + +// User represents a User in the Enterprise Cloud Identity Service. +type User struct { + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id"` + + // Description is the description of the user. + Description string `json:"description"` + + // DomainID is the domain ID the user belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the user is enabled. + Enabled bool `json:"enabled"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // ID is the unique ID of the user. + ID string `json:"id"` + + // Links contains referencing links to the user. + Links map[string]interface{} `json:"links"` + + // Name is the name of the user. + Name string `json:"name"` + + // Options are a set of defined options of the user. + Options map[string]interface{} `json:"options"` + + // PasswordExpiresAt is the timestamp when the user's password expires. + PasswordExpiresAt time.Time `json:"-"` +} + +func (r *User) UnmarshalJSON(b []byte) error { + type tmp User + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + PasswordExpiresAt eclcloud.JSONRFC3339MilliNoZ `json:"password_expires_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = User(s.tmp) + + r.PasswordExpiresAt = time.Time(s.PasswordExpiresAt) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "password_expires_at") + r.Extra = internal.RemainingKeys(User{}, resultMap) + } + } + + return err +} + +type userResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a User. +type GetResult struct { + userResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a User. +type CreateResult struct { + userResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a User. +type UpdateResult struct { + userResult +} + +// ChangePasswordResult is the response from a ChangePassword operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type ChangePasswordResult struct { + eclcloud.ErrResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// AddToGroupResult is the response from a AddToGroup operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type AddToGroupResult struct { + eclcloud.ErrResult +} + +// IsMemberOfGroupResult is the response from a IsMemberOfGroup operation. Call its +// Extract method to determine if the request succeeded or failed. +type IsMemberOfGroupResult struct { + isMember bool + eclcloud.Result +} + +// RemoveFromGroupResult is the response from a RemoveFromGroup operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RemoveFromGroupResult struct { + eclcloud.ErrResult +} + +// UserPage is a single page of User results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a UserPage contains any results. +func (r UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(r) + return len(users) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r UserPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractUsers returns a slice of Users contained in a single page of results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} + +// Extract interprets any user results as a User. +func (r userResult) Extract() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// Extract extracts IsMemberOfGroupResult as bool and error values +func (r IsMemberOfGroupResult) Extract() (bool, error) { + return r.isMember, r.Err +} diff --git a/v4/ecl/identity/v3/users/urls.go b/v4/ecl/identity/v3/users/urls.go new file mode 100644 index 0000000..55e18ac --- /dev/null +++ b/v4/ecl/identity/v3/users/urls.go @@ -0,0 +1,51 @@ +package users + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func updateURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func changePasswordURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "password") +} + +func deleteURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func listGroupsURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "groups") +} + +func addToGroupURL(client *eclcloud.ServiceClient, groupID, userID string) string { + return client.ServiceURL("groups", groupID, "users", userID) +} + +func isMemberOfGroupURL(client *eclcloud.ServiceClient, groupID, userID string) string { + return client.ServiceURL("groups", groupID, "users", userID) +} + +func removeFromGroupURL(client *eclcloud.ServiceClient, groupID, userID string) string { + return client.ServiceURL("groups", groupID, "users", userID) +} + +func listProjectsURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "projects") +} + +func listInGroupURL(client *eclcloud.ServiceClient, groupID string) string { + return client.ServiceURL("groups", groupID, "users") +} diff --git a/v4/ecl/imagestorage/v2/imagedata/doc.go b/v4/ecl/imagestorage/v2/imagedata/doc.go new file mode 100644 index 0000000..0c12bf2 --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/doc.go @@ -0,0 +1,48 @@ +/* +Package imagedata enables management of image data. + +Example to Upload Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + imageData, err := os.Open("/path/to/image/file") + if err != nil { + panic(err) + } + defer imageData.Close() + + err = imagedata.Upload(imageClient, imageID, imageData).ExtractErr() + if err != nil { + panic(err) + } + +Example to Stage Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + imageData, err := os.Open("/path/to/image/file") + if err != nil { + panic(err) + } + defer imageData.Close() + + err = imagedata.Stage(imageClient, imageID, imageData).ExtractErr() + if err != nil { + panic(err) + } + +Example to Download Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + image, err := imagedata.Download(imageClient, imageID).Extract() + if err != nil { + panic(err) + } + + imageData, err := ioutil.ReadAll(image) + if err != nil { + panic(err) + } +*/ +package imagedata diff --git a/v4/ecl/imagestorage/v2/imagedata/requests.go b/v4/ecl/imagestorage/v2/imagedata/requests.go new file mode 100644 index 0000000..6d70e21 --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/requests.go @@ -0,0 +1,28 @@ +package imagedata + +import ( + "io" + "net/http" + + "github.com/nttcom/eclcloud/v4" +) + +// Upload uploads an image file. +func Upload(client *eclcloud.ServiceClient, id string, data io.Reader) (r UploadResult) { + _, r.Err = client.Put(uploadURL(client, id), data, nil, &eclcloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"}, + OkCodes: []int{204}, + }) + return +} + +// Download retrieves an image. +func Download(client *eclcloud.ServiceClient, id string) (r DownloadResult) { + var resp *http.Response + resp, r.Err = client.Get(downloadURL(client, id), nil, nil) + if resp != nil { + r.Body = resp.Body + r.Header = resp.Header + } + return +} diff --git a/v4/ecl/imagestorage/v2/imagedata/results.go b/v4/ecl/imagestorage/v2/imagedata/results.go new file mode 100644 index 0000000..5723556 --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/results.go @@ -0,0 +1,34 @@ +package imagedata + +import ( + "fmt" + "io" + + "github.com/nttcom/eclcloud/v4" +) + +// UploadResult is the result of an upload image operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type UploadResult struct { + eclcloud.ErrResult +} + +// StageResult is the result of a stage image operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StageResult struct { + eclcloud.ErrResult +} + +// DownloadResult is the result of a download image operation. Call its Extract +// method to gain access to the image data. +type DownloadResult struct { + eclcloud.Result +} + +// Extract builds images model from io.Reader +func (r DownloadResult) Extract() (io.Reader, error) { + if r, ok := r.Body.(io.Reader); ok { + return r, nil + } + return nil, fmt.Errorf("expected io.Reader but got: %T(%#v)", r.Body, r.Body) +} diff --git a/v4/ecl/imagestorage/v2/imagedata/testing/doc.go b/v4/ecl/imagestorage/v2/imagedata/testing/doc.go new file mode 100644 index 0000000..5a9db1b --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/testing/doc.go @@ -0,0 +1,2 @@ +// imagedata unit tests +package testing diff --git a/v4/ecl/imagestorage/v2/imagedata/testing/fixtures.go b/v4/ecl/imagestorage/v2/imagedata/testing/fixtures.go new file mode 100644 index 0000000..66691de --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/testing/fixtures.go @@ -0,0 +1,40 @@ +package testing + +import ( + "io/ioutil" + "net/http" + "testing" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// HandlePutImageDataSuccessfully setup +func HandlePutImageDataSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/0cb9328d-dd8c-41bb-b378-404b854b93b9/file", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read request body: %v", err) + } + + th.AssertByteArrayEquals(t, []byte{5, 3, 7, 24}, b) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleGetImageDataSuccessfully setup +func HandleGetImageDataSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/100f4d2d-dcb5-472e-b93f-b4e13d888604/file", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + + _, err := w.Write([]byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}) + th.AssertNoErr(t, err) + }) +} diff --git a/v4/ecl/imagestorage/v2/imagedata/testing/requests_test.go b/v4/ecl/imagestorage/v2/imagedata/testing/requests_test.go new file mode 100644 index 0000000..6f5f0c5 --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/testing/requests_test.go @@ -0,0 +1,103 @@ +package testing + +import ( + "fmt" + "io" + "io/ioutil" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/imagestorage/v2/imagedata" + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestUpload(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandlePutImageDataSuccessfully(t) + + err := imagedata.Upload( + fakeclient.ServiceClient(), + "0cb9328d-dd8c-41bb-b378-404b854b93b9", + readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr() + + th.AssertNoErr(t, err) +} + +/* +func TestStage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleStageImageDataSuccessfully(t) + + err := imagedata.Stage( + fakeclient.ServiceClient(), + "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr() + + th.AssertNoErr(t, err) +} +*/ + +func readSeekerOfBytes(bs []byte) io.ReadSeeker { + return &RS{bs: bs} +} + +// implements io.ReadSeeker +type RS struct { + bs []byte + offset int +} + +func (rs *RS) Read(p []byte) (int, error) { + leftToRead := len(rs.bs) - rs.offset + + if 0 < leftToRead { + bytesToWrite := min(leftToRead, len(p)) + for i := 0; i < bytesToWrite; i++ { + p[i] = rs.bs[rs.offset] + rs.offset++ + } + return bytesToWrite, nil + } + return 0, io.EOF +} + +func min(a int, b int) int { + if a < b { + return a + } + return b +} + +func (rs *RS) Seek(offset int64, whence int) (int64, error) { + var offsetInt = int(offset) + if whence == 0 { + rs.offset = offsetInt + } else if whence == 1 { + rs.offset = rs.offset + offsetInt + } else if whence == 2 { + rs.offset = len(rs.bs) - offsetInt + } else { + return 0, fmt.Errorf("for parameter `whence`, expected value in {0,1,2} but got: %#v", whence) + } + + return int64(rs.offset), nil +} + +func TestDownload(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetImageDataSuccessfully(t) + + rdr, err := imagedata.Download(fakeclient.ServiceClient(), "100f4d2d-dcb5-472e-b93f-b4e13d888604").Extract() + th.AssertNoErr(t, err) + + bs, err := ioutil.ReadAll(rdr) + th.AssertNoErr(t, err) + + th.AssertByteArrayEquals(t, []byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}, bs) +} diff --git a/v4/ecl/imagestorage/v2/imagedata/urls.go b/v4/ecl/imagestorage/v2/imagedata/urls.go new file mode 100644 index 0000000..163f003 --- /dev/null +++ b/v4/ecl/imagestorage/v2/imagedata/urls.go @@ -0,0 +1,23 @@ +package imagedata + +import "github.com/nttcom/eclcloud/v4" + +const ( + rootPath = "images" + uploadPath = "file" + stagePath = "stage" +) + +// `imageDataURL(c,i)` is the URL for the binary image data for the +// image identified by ID `i` in the service `c`. +func uploadURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, uploadPath) +} + +func stageURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, stagePath) +} + +func downloadURL(c *eclcloud.ServiceClient, imageID string) string { + return uploadURL(c, imageID) +} diff --git a/v4/ecl/imagestorage/v2/images/doc.go b/v4/ecl/imagestorage/v2/images/doc.go new file mode 100644 index 0000000..cd3f5e2 --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/doc.go @@ -0,0 +1,60 @@ +/* +Package images enables management and retrieval of images from the Enterprise Cloud +Image Service. + +Example to List Images + + images.ListOpts{ + Owner: "a7509e1ae65945fda83f3e52c6296017", + } + + allPages, err := images.List(imagesClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } + +Example to Create an Image + + createOpts := images.CreateOpts{ + Name: "image_name", + Visibility: images.ImageVisibilityPrivate, + } + + image, err := images.Create(imageClient, createOpts) + if err != nil { + panic(err) + } + +Example to Update an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + + updateOpts := images.UpdateOpts{ + images.ReplaceImageName{ + NewName: "new_name", + }, + } + + image, err := images.Update(imageClient, imageID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + err := images.Delete(imageClient, imageID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package images diff --git a/v4/ecl/imagestorage/v2/images/requests.go b/v4/ecl/imagestorage/v2/images/requests.go new file mode 100644 index 0000000..e4b4db7 --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/requests.go @@ -0,0 +1,349 @@ +package images + +import ( + "fmt" + "net/url" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (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 { + // ID is the ID of the image. + // Multiple IDs can be specified by constructing a string + // such as "in:uuid1,uuid2,uuid3". + ID string `q:"id"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Name filters on the name of the image. + // Multiple names can be specified by constructing a string + // such as "in:name1,name2,name3". + Name string `q:"name"` + + // Visibility filters on the visibility of the image. + Visibility ImageVisibility `q:"visibility"` + + // MemberStatus filters on the member status of the image. + MemberStatus ImageMemberStatus `q:"member_status"` + + // Owner filters on the project ID of the image. + Owner string `q:"owner"` + + // Status filters on the status of the image. + // Multiple statuses can be specified by constructing a string + // such as "in:saving,queued". + Status ImageStatus `q:"status"` + + // SizeMin filters on the size_min image property. + SizeMin int64 `q:"size_min"` + + // SizeMax filters on the size_max image property. + SizeMax int64 `q:"size_max"` + + // Sort sorts the results using the new style of sorting. See the Enterprise Cloud + // Image API reference for the exact syntax. + // + // Sort cannot be used with the classic sort options (sort_key and sort_dir). + Sort string `q:"sort"` + + // SortKey will sort the results based on a specified image property. + SortKey string `q:"sort_key"` + + // SortDir will sort the list results either ascending or decending. + SortDir string `q:"sort_dir"` + + // Tags filters on specific image tags. + Tags []string `q:"tag"` + + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery + + // ContainerFormat filters images based on the container_format. + // Multiple container formats can be specified by constructing a + // string such as "in:bare,ami". + ContainerFormat string `q:"container_format"` + + // DiskFormat filters images based on the disk_format. + // Multiple disk formats can be specified by constructing a string + // such as "in:qcow2,iso". + DiskFormat string `q:"disk_format"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + + return q.String(), err +} + +// List implements image list request. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + imagePage := ImagePage{ + serviceURL: c.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create an image. +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name,omitempty"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{OkCodes: []int{201}}) + return +} + +// Delete implements image delete request. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get implements image get request. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Update implements image updated request. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches + // the patch schema. + ToImageUpdateMap() ([]interface{}, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap assembles a request body based on the contents of +// UpdateOpts. +func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) { + m := make([]interface{}, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates +// to an image can be submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]interface{} +} + +// UpdateVisibility represents an updated visibility property request. +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap assembles a request body based on UpdateVisibility. +func (r UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": r.Visibility, + } +} + +// ReplaceImageName represents an updated image_name property request. +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageName. +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum represents an updated checksum property request. +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum. +func (r ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": r.Checksum, + } +} + +// ReplaceImageTags represents an updated tags property request. +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} + +// UpdateOp represents a valid update operation. +type UpdateOp string + +const ( + AddOp UpdateOp = "add" + ReplaceOp UpdateOp = "replace" + RemoveOp UpdateOp = "remove" +) + +// UpdateImageProperty represents an update property request. +type UpdateImageProperty struct { + Op UpdateOp + Name string + Value string +} + +// ToImagePatchMap assembles a request body based on UpdateImageProperty. +func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} { + updateMap := map[string]interface{}{ + "op": r.Op, + "path": fmt.Sprintf("/%s", r.Name), + } + + if r.Value != "" { + updateMap["value"] = r.Value + } + + return updateMap +} diff --git a/v4/ecl/imagestorage/v2/images/results.go b/v4/ecl/imagestorage/v2/images/results.go new file mode 100644 index 0000000..bd6eb18 --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/results.go @@ -0,0 +1,201 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/internal" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Image represents an image found in the Enterprise Cloud Image service. +type Image struct { + // ID is the image UUID. + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See imageservice/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to + // boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to + // boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant ID the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Checksum is the checksum of the data that's associated with the image. + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"-"` + + // Metadata is a set of metadata associated with the image. + // Image metadata allow for meaningfully define the image properties + // and tags. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with + // the image. + Properties map[string]interface{} + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or + // it's properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the + // location of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image + // entity. + Schema string `json:"schema"` + + // VirtualSize is the virtual size of the image + VirtualSize int64 `json:"virtual_size"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes interface{} `json:"size"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + r.SizeBytes = 0 + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + // Bundle all other fields into Properties + var result interface{} + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "self") + delete(resultMap, "size") + r.Properties = internal.RemainingKeys(Image{}, resultMap) + } + + return err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as an Image. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as an Image. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret it as an Image. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to interpret it as an Image. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.serviceURL, s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, +// producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/v4/ecl/imagestorage/v2/images/testing/doc.go b/v4/ecl/imagestorage/v2/images/testing/doc.go new file mode 100644 index 0000000..d4bd4f3 --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains images unit tests +package testing diff --git a/v4/ecl/imagestorage/v2/images/testing/fixtures.go b/v4/ecl/imagestorage/v2/images/testing/fixtures.go new file mode 100644 index 0000000..858e1f8 --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/testing/fixtures.go @@ -0,0 +1,447 @@ +package testing + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "testing" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +type imageEntry struct { + ID string + JSON string +} + +// HandleImageListSuccessfully test setup +func HandleImageListSuccessfully(t *testing.T) { + + images := make([]imageEntry, 3) + + images[0] = imageEntry{"cirros-0.3.4-x86_64-uec", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec", + "tags": [], + "kernel_id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "container_format": "ami", + "created_at": "2015-07-15T11:43:35Z", + "ramdisk_id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "disk_format": "ami", + "updated_at": "2015-07-15T11:43:35Z", + "visibility": "public", + "self": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431", + "min_disk": 0, + "protected": false, + "id": "07aa21a9-fa1a-430e-9a33-185be5982431", + "size": 25165824, + "file": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431/file", + "checksum": "eb9139e4942121f22bbc2afc0400b2a4", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + images[1] = imageEntry{"cirros-0.3.4-x86_64-uec-ramdisk", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec-ramdisk", + "tags": [], + "container_format": "ari", + "created_at": "2015-07-15T11:43:32Z", + "size": 3740163, + "disk_format": "ari", + "updated_at": "2015-07-15T11:43:32Z", + "visibility": "public", + "self": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "min_disk": 0, + "protected": false, + "id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "file": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b/file", + "checksum": "be575a2b939972276ef675752936977f", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + images[2] = imageEntry{"cirros-0.3.4-x86_64-uec-kernel", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec-kernel", + "tags": [], + "container_format": "aki", + "created_at": "2015-07-15T11:43:29Z", + "size": 4979632, + "disk_format": "aki", + "updated_at": "2015-07-15T11:43:30Z", + "visibility": "public", + "self": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "min_disk": 0, + "protected": false, + "id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "file": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4/file", + "checksum": "8a40c862b5735975d82605c1dd395796", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + limit := 10 + var err error + if r.FormValue("limit") != "" { + limit, err = strconv.Atoi(r.FormValue("limit")) + if err != nil { + t.Errorf("Error value for 'limit' parameter %v (error: %v)", r.FormValue("limit"), err) + } + + } + + marker := "" + newMarker := "" + + if r.Form["marker"] != nil { + marker = r.Form["marker"][0] + } + + t.Logf("limit = %v marker = %v", limit, marker) + + selected := 0 + addNext := false + var imageJSON []string + + fmt.Fprintf(w, `{"images": [`) + + for _, i := range images { + if marker == "" || addNext { + t.Logf("Adding image %v to page", i.ID) + imageJSON = append(imageJSON, i.JSON) + newMarker = i.ID + selected++ + } else { + if strings.Contains(i.JSON, marker) { + addNext = true + } + } + + if selected == limit { + break + } + } + t.Logf("Writing out %v image(s)", len(imageJSON)) + fmt.Fprintf(w, strings.Join(imageJSON, ",")) + + fmt.Fprintf(w, `], + "next": "/images?marker=%s&limit=%v", + "schema": "/schemas/images", + "first": "/images?limit=%v"}`, newMarker, limit, limit) + + }) +} + +// HandleImageCreationSuccessfully test setup +func HandleImageCreationSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, `{ + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "name": "Ubuntu 12.10", + "architecture": "x86_64", + "tags": [ + "ubuntu", + "quantal" + ] + }`) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "queued", + "name": "Ubuntu 12.10", + "protected": false, + "tags": ["ubuntu","quantal"], + "container_format": "bare", + "created_at": "2014-11-11T20:47:55Z", + "disk_format": "qcow2", + "updated_at": "2014-11-11T20:47:55Z", + "visibility": "private", + "self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "min_disk": 0, + "protected": false, + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file", + "owner": "b4eedccc6fb74fa8a7ad6b08382b852b", + "min_ram": 0, + "schema": "/v2/schemas/image", + "size": 0, + "checksum": "", + "virtual_size": 0, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageCreationSuccessfullyNulls test setup +// JSON null values could be also returned according to behaviour https://bugs.launchpad.net/glance/+bug/1481512 +func HandleImageCreationSuccessfullyNulls(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, `{ + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "architecture": "x86_64", + "name": "Ubuntu 12.10", + "tags": [ + "ubuntu", + "quantal" + ] + }`) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "architecture": "x86_64", + "status": "queued", + "name": "Ubuntu 12.10", + "protected": false, + "tags": ["ubuntu","quantal"], + "container_format": "bare", + "created_at": "2014-11-11T20:47:55Z", + "disk_format": "qcow2", + "updated_at": "2014-11-11T20:47:55Z", + "visibility": "private", + "self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "min_disk": 0, + "protected": false, + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file", + "owner": "b4eedccc6fb74fa8a7ad6b08382b852b", + "min_ram": 0, + "schema": "/v2/schemas/image", + "size": null, + "checksum": null, + "virtual_size": null + }`) + }) +} + +// HandleImageGetSuccessfully test setup +func HandleImageGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "active", + "name": "cirros-0.3.2-x86_64-disk", + "tags": [], + "container_format": "bare", + "created_at": "2014-05-05T17:15:10Z", + "disk_format": "qcow2", + "updated_at": "2014-05-05T17:15:11Z", + "visibility": "public", + "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "min_disk": 0, + "protected": false, + "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", + "checksum": "64d7c1cd2b6f60c92c14662941cb7913", + "owner": "5ef70662f8b34079a6eddb8da9d75fe8", + "size": 13167616, + "min_ram": 0, + "schema": "/v2/schemas/image", + "virtual_size": null, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageDeleteSuccessfully test setup +func HandleImageDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleImageUpdateSuccessfully setup +func HandleImageUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `[ + { + "op": "replace", + "path": "/name", + "value": "Fedora 17" + }, + { + "op": "replace", + "path": "/tags", + "value": [ + "fedora", + "beefy" + ] + } + ]`) + + th.AssertEquals(t, "application/openstack-images-v2.1-json-patch", r.Header.Get("Content-Type")) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "name": "Fedora 17", + "status": "active", + "visibility": "public", + "size": 2254249, + "checksum": "2cec138d7dae2aa59038ef8c9aec2390", + "tags": [ + "fedora", + "beefy" + ], + "created_at": "2012-08-10T19:23:50Z", + "updated_at": "2012-08-12T11:11:33Z", + "self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", + "schema": "/v2/schemas/image", + "owner": "", + "min_ram": 0, + "min_disk": 0, + "disk_format": "", + "virtual_size": 0, + "container_format": "", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageListByTagsSuccessfully tests a list operation with tags. +func HandleImageListByTagsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "images": [ + { + "status": "active", + "name": "cirros-0.3.2-x86_64-disk", + "tags": ["foo", "bar"], + "container_format": "bare", + "created_at": "2014-05-05T17:15:10Z", + "disk_format": "qcow2", + "updated_at": "2014-05-05T17:15:11Z", + "visibility": "public", + "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "min_disk": 0, + "protected": false, + "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", + "checksum": "64d7c1cd2b6f60c92c14662941cb7913", + "owner": "5ef70662f8b34079a6eddb8da9d75fe8", + "size": 13167616, + "min_ram": 0, + "schema": "/v2/schemas/image", + "virtual_size": null, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + } + ] + }`) + }) +} + +// HandleImageUpdatePropertiesSuccessfully setup +func HandleImageUpdatePropertiesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `[ + { + "op": "add", + "path": "/hw_disk_bus", + "value": "scsi" + }, + { + "op": "add", + "path": "/hw_disk_bus_model", + "value": "virtio-scsi" + }, + { + "op": "add", + "path": "/hw_scsi_model", + "value": "virtio-scsi" + } + ]`) + + th.AssertEquals(t, "application/openstack-images-v2.1-json-patch", r.Header.Get("Content-Type")) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "name": "Fedora 17", + "status": "active", + "visibility": "public", + "size": 2254249, + "checksum": "2cec138d7dae2aa59038ef8c9aec2390", + "tags": [ + "fedora", + "beefy" + ], + "created_at": "2012-08-10T19:23:50Z", + "updated_at": "2012-08-12T11:11:33Z", + "self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", + "schema": "/v2/schemas/image", + "owner": "", + "min_ram": 0, + "min_disk": 0, + "disk_format": "", + "virtual_size": 0, + "container_format": "", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} diff --git a/v4/ecl/imagestorage/v2/images/testing/requests_test.go b/v4/ecl/imagestorage/v2/images/testing/requests_test.go new file mode 100644 index 0000000..4a07a2b --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/testing/requests_test.go @@ -0,0 +1,458 @@ +package testing + +import ( + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/imagestorage/v2/images" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListSuccessfully(t) + + t.Logf("Test setup %+v\n", th.Server) + + t.Logf("Id\tName\tOwner\tChecksum\tSizeBytes") + + pager := images.List(fakeclient.ServiceClient(), images.ListOpts{Limit: 1}) + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + images, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range images { + t.Logf("%s\t%s\t%s\t%s\t%v\t\n", i.ID, i.Name, i.Owner, i.Checksum, i.SizeBytes) + count++ + } + + return true, nil + }) + th.AssertNoErr(t, err) + + t.Logf("--------\n%d images listed on %d pages.\n", count, pages) + th.AssertEquals(t, 3, pages) + th.AssertEquals(t, 3, count) +} + +func TestAllPagesImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListSuccessfully(t) + + pages, err := images.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + images, err := images.ExtractImages(pages) + th.AssertNoErr(t, err) + th.AssertEquals(t, 3, len(images)) +} + +func TestCreateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageCreationSuccessfully(t) + + id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd" + name := "Ubuntu 12.10" + + actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{ + ID: id, + Name: name, + Properties: map[string]string{ + "architecture": "x86_64", + }, + Tags: []string{"ubuntu", "quantal"}, + }).Extract() + + th.AssertNoErr(t, err) + + containerFormat := "bare" + diskFormat := "qcow2" + owner := "b4eedccc6fb74fa8a7ad6b08382b852b" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + Name: "Ubuntu 12.10", + Tags: []string{"ubuntu", "quantal"}, + + Status: images.ImageStatusQueued, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Visibility: images.ImageVisibilityPrivate, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestCreateImageNulls(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageCreationSuccessfullyNulls(t) + + id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd" + name := "Ubuntu 12.10" + + actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{ + ID: id, + Name: name, + Tags: []string{"ubuntu", "quantal"}, + Properties: map[string]string{ + "architecture": "x86_64", + }, + }).Extract() + + th.AssertNoErr(t, err) + + containerFormat := "bare" + diskFormat := "qcow2" + owner := "b4eedccc6fb74fa8a7ad6b08382b852b" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + properties := map[string]interface{}{ + "architecture": "x86_64", + } + sizeBytes := int64(0) + + expectedImage := images.Image{ + ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + Name: "Ubuntu 12.10", + Tags: []string{"ubuntu", "quantal"}, + + Status: images.ImageStatusQueued, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Visibility: images.ImageVisibilityPrivate, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + Properties: properties, + SizeBytes: sizeBytes, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageGetSuccessfully(t) + + actualImage, err := images.Get(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27").Extract() + + th.AssertNoErr(t, err) + + checksum := "64d7c1cd2b6f60c92c14662941cb7913" + sizeBytes := int64(13167616) + containerFormat := "bare" + diskFormat := "qcow2" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + owner := "5ef70662f8b34079a6eddb8da9d75fe8" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + Name: "cirros-0.3.2-x86_64-disk", + Tags: []string{}, + + Status: images.ImageStatusActive, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Protected: false, + Visibility: images.ImageVisibilityPublic, + + Checksum: checksum, + SizeBytes: sizeBytes, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestDeleteImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageDeleteSuccessfully(t) + + result := images.Delete(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27") + th.AssertNoErr(t, result.Err) +} + +func TestUpdateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageUpdateSuccessfully(t) + + actualImage, err := images.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", images.UpdateOpts{ + images.ReplaceImageName{NewName: "Fedora 17"}, + images.ReplaceImageTags{NewTags: []string{"fedora", "beefy"}}, + }).Extract() + + th.AssertNoErr(t, err) + + sizebytes := int64(2254249) + checksum := "2cec138d7dae2aa59038ef8c9aec2390" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + Name: "Fedora 17", + Status: images.ImageStatusActive, + Visibility: images.ImageVisibilityPublic, + + SizeBytes: sizebytes, + Checksum: checksum, + + Tags: []string{ + "fedora", + "beefy", + }, + + Owner: "", + MinRAMMegabytes: 0, + MinDiskGigabytes: 0, + + DiskFormat: "", + ContainerFormat: "", + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestImageDateQuery(t *testing.T) { + date := time.Date(2014, 1, 1, 1, 1, 1, 0, time.UTC) + + listOpts := images.ListOpts{ + CreatedAtQuery: &images.ImageDateQuery{ + Date: date, + Filter: images.FilterGTE, + }, + UpdatedAtQuery: &images.ImageDateQuery{ + Date: date, + }, + } + + expectedQueryString := "?created_at=gte%3A2014-01-01T01%3A01%3A01Z&updated_at=2014-01-01T01%3A01%3A01Z" + actualQueryString, err := listOpts.ToImageListQuery() + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedQueryString, actualQueryString) +} + +func TestImageListByTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListByTagsSuccessfully(t) + + listOpts := images.ListOpts{ + Tags: []string{"foo", "bar"}, + } + + expectedQueryString := "?tag=foo&tag=bar" + actualQueryString, err := listOpts.ToImageListQuery() + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedQueryString, actualQueryString) + + pages, err := images.List(fakeclient.ServiceClient(), listOpts).AllPages() + th.AssertNoErr(t, err) + allImages, err := images.ExtractImages(pages) + th.AssertNoErr(t, err) + + checksum := "64d7c1cd2b6f60c92c14662941cb7913" + sizeBytes := int64(13167616) + containerFormat := "bare" + diskFormat := "qcow2" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + owner := "5ef70662f8b34079a6eddb8da9d75fe8" + file := allImages[0].File + createdDate := allImages[0].CreatedAt + lastUpdate := allImages[0].UpdatedAt + schema := "/v2/schemas/image" + tags := []string{"foo", "bar"} + + expectedImage := images.Image{ + ID: "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + Name: "cirros-0.3.2-x86_64-disk", + Tags: tags, + + Status: images.ImageStatusActive, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Protected: false, + Visibility: images.ImageVisibilityPublic, + + Checksum: checksum, + SizeBytes: sizeBytes, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, expectedImage, allImages[0]) +} + +func TestUpdateImageProperties(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageUpdatePropertiesSuccessfully(t) + + actualImage, err := images.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", images.UpdateOpts{ + images.UpdateImageProperty{ + Op: images.AddOp, + Name: "hw_disk_bus", + Value: "scsi", + }, + images.UpdateImageProperty{ + Op: images.AddOp, + Name: "hw_disk_bus_model", + Value: "virtio-scsi", + }, + images.UpdateImageProperty{ + Op: images.AddOp, + Name: "hw_scsi_model", + Value: "virtio-scsi", + }, + }).Extract() + + th.AssertNoErr(t, err) + + sizebytes := int64(2254249) + checksum := "2cec138d7dae2aa59038ef8c9aec2390" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + Name: "Fedora 17", + Status: images.ImageStatusActive, + Visibility: images.ImageVisibilityPublic, + + SizeBytes: sizebytes, + Checksum: checksum, + + Tags: []string{ + "fedora", + "beefy", + }, + + Owner: "", + MinRAMMegabytes: 0, + MinDiskGigabytes: 0, + + DiskFormat: "", + ContainerFormat: "", + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} diff --git a/v4/ecl/imagestorage/v2/images/types.go b/v4/ecl/imagestorage/v2/images/types.go new file mode 100644 index 0000000..5005b34 --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/types.go @@ -0,0 +1,101 @@ +package images + +import ( + "time" +) + +// ImageStatus image statuses +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image’s raw data is currently being + // uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet + // deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to + // any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size is explicitly +// set to zero on creation. +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with + // member_status == 'accepted' have this image in their default image-list. + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image +// member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} diff --git a/v4/ecl/imagestorage/v2/images/urls.go b/v4/ecl/imagestorage/v2/images/urls.go new file mode 100644 index 0000000..d4e806e --- /dev/null +++ b/v4/ecl/imagestorage/v2/images/urls.go @@ -0,0 +1,64 @@ +package images + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/utils" + "net/url" + "strings" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *eclcloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *eclcloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *eclcloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = eclcloud.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + + return nextURL.String(), nil +} diff --git a/v4/ecl/imagestorage/v2/members/doc.go b/v4/ecl/imagestorage/v2/members/doc.go new file mode 100644 index 0000000..acb4272 --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/doc.go @@ -0,0 +1 @@ +package members diff --git a/v4/ecl/imagestorage/v2/members/requests.go b/v4/ecl/imagestorage/v2/members/requests.go new file mode 100644 index 0000000..a75deac --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/requests.go @@ -0,0 +1,80 @@ +package members + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* + Create member for specific image + + Preconditions + + * The specified images must exist. + * You can only add a new member to an image which 'visibility' attribute is + private. + * You must be the owner of the specified image. + + Synchronous Postconditions + + With correct permissions, you can see the member status of the image as + pending through API calls. + +*/ + +func Create(client *eclcloud.ServiceClient, id string, member string) (r CreateResult) { + b := map[string]interface{}{"member": member} + _, r.Err = client.Post(createMemberURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// List members returns list of members for specifed image id. +func List(client *eclcloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listMembersURL(client, id), func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.SinglePageBase(r)} + }) +} + +// Get image member details. +func Get(client *eclcloud.ServiceClient, imageID string, memberID string) (r DetailsResult) { + _, r.Err = client.Get(getMemberURL(client, imageID, memberID), &r.Body, &eclcloud.RequestOpts{OkCodes: []int{200}}) + return +} + +// Delete membership for given image. Callee should be image owner. +func Delete(client *eclcloud.ServiceClient, imageID string, memberID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteMemberURL(client, imageID, memberID), &eclcloud.RequestOpts{OkCodes: []int{204}}) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToImageMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options to an Update request. +type UpdateOpts struct { + Status string `json:"status,omitempty" required:"true1"` +} + +// ToMemberUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToImageMemberUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "status": opts.Status, + }, nil +} + +// Update function updates member. +func Update(client *eclcloud.ServiceClient, imageID string, memberID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageMemberUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body, + &eclcloud.RequestOpts{OkCodes: []int{200}}) + return +} diff --git a/v4/ecl/imagestorage/v2/members/results.go b/v4/ecl/imagestorage/v2/members/results.go new file mode 100644 index 0000000..f23162d --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/results.go @@ -0,0 +1,74 @@ +package members + +import ( + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Member represents a member of an Image. +type Member struct { + CreatedAt time.Time `json:"created_at"` + ImageID string `json:"image_id"` + MemberID string `json:"member_id"` + Schema string `json:"schema"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Extract Member model from a request. +func (r commonResult) Extract() (*Member, error) { + var s *Member + err := r.ExtractInto(&s) + return s, err +} + +// MemberPage is a single page of Members results. +type MemberPage struct { + pagination.SinglePageBase +} + +// ExtractMembers returns a slice of Members contained in a single page +// of results. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := r.(MemberPage).ExtractInto(&s) + return s.Members, err +} + +// IsEmpty determines whether or not a MemberPage contains any results. +func (r MemberPage) IsEmpty() (bool, error) { + members, err := ExtractMembers(r) + return len(members) == 0, err +} + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as a Member. +type CreateResult struct { + commonResult +} + +// DetailsResult represents the result of a Get operation. Call its Extract +// method to interpret it as a Member. +type DetailsResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as a Member. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/imagestorage/v2/members/testing/doc.go b/v4/ecl/imagestorage/v2/members/testing/doc.go new file mode 100644 index 0000000..1afbc43 --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/testing/doc.go @@ -0,0 +1,2 @@ +// members unit tests +package testing diff --git a/v4/ecl/imagestorage/v2/members/testing/fixtures.go b/v4/ecl/imagestorage/v2/members/testing/fixtures.go new file mode 100644 index 0000000..f3be434 --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/testing/fixtures.go @@ -0,0 +1,138 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// HandleCreateImageMemberSuccessfully setup +func HandleCreateImageMemberSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `{"member": "f6a818c3d4aa458798ed86892e7150c0"}`) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "created_at": "2013-09-20T19:22:19Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member", + "status": "pending", + "updated_at": "2013-09-20T19:25:31Z" + }`) + + }) +} + +// HandleImageMemberList happy path setup +func HandleImageMemberList(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "members": [ + { + "created_at": "2013-10-07T17:58:03Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member", + "status": "pending", + "updated_at": "2013-10-07T17:58:03Z" + }, + { + "created_at": "2013-10-07T17:58:55Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "1efb79fe4437490aab966b57da5b9f05", + "schema": "/v2/schemas/member", + "status": "accepted", + "updated_at": "2013-10-08T12:08:55Z" + } + ], + "schema": "/v2/schemas/members" + }`) + }) +} + +// HandleImageMemberEmptyList happy path setup +func HandleImageMemberEmptyList(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "members": [], + "schema": "/v2/schemas/members" + }`) + }) +} + +// HandleImageMemberDetails setup +func HandleImageMemberDetails(t *testing.T) { + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members/f6a818c3d4aa458798ed86892e7150c0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "status": "pending", + "created_at": "2013-11-26T07:21:21Z", + "updated_at": "2013-11-26T07:21:21Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member" + }`) + }) +} + +// HandleImageMemberDeleteSuccessfully setup +func HandleImageMemberDeleteSuccessfully(t *testing.T) *CallsCounter { + var counter CallsCounter + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members/f6a818c3d4aa458798ed86892e7150c0", func(w http.ResponseWriter, r *http.Request) { + counter.Counter = counter.Counter + 1 + + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + return &counter +} + +// HandleImageMemberUpdate setup +func HandleImageMemberUpdate(t *testing.T) *CallsCounter { + var counter CallsCounter + th.Mux.HandleFunc("/images/54d63e39-4ee1-4a62-8704-0ae5025a0deb/members/f6a818c3d4aa458798ed86892e7150c0", func(w http.ResponseWriter, r *http.Request) { + counter.Counter = counter.Counter + 1 + + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `{"status": "accepted"}`) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "status": "accepted", + "created_at": "2013-11-26T07:21:21Z", + "updated_at": "2013-11-26T07:21:21Z", + "image_id": "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "member_id": "f6a818c3d4aa458798ed86892e7150c0", + "schema": "/v2/schemas/member" + }`) + }) + return &counter +} + +// CallsCounter for checking if request handler was called at all +type CallsCounter struct { + Counter int +} diff --git a/v4/ecl/imagestorage/v2/members/testing/requests_test.go b/v4/ecl/imagestorage/v2/members/testing/requests_test.go new file mode 100644 index 0000000..8ad67a1 --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/testing/requests_test.go @@ -0,0 +1,172 @@ +package testing + +import ( + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/imagestorage/v2/members" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +const createdAtString = "2013-09-20T19:22:19Z" +const updatedAtString = "2013-09-20T19:25:31Z" + +func TestCreateMemberSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateImageMemberSuccessfully(t) + im, err := members.Create(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0").Extract() + th.AssertNoErr(t, err) + + createdAt, err := time.Parse(time.RFC3339, createdAtString) + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, updatedAtString) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + MemberID: "f6a818c3d4aa458798ed86892e7150c0", + Schema: "/v2/schemas/member", + Status: "pending", + UpdatedAt: updatedAt, + }, *im) + +} + +func TestMemberListSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberList(t) + + pager := members.List(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb") + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + members, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, i := range members { + t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema) + count++ + } + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) + th.AssertEquals(t, 2, count) +} + +func TestMemberListEmpty(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberEmptyList(t) + + pager := members.List(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb") + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + members, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, i := range members { + t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema) + count++ + } + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 0, pages) + th.AssertEquals(t, 0, count) +} + +func TestShowMemberDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberDetails(t) + md, err := members.Get(fakeclient.ServiceClient(), + "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0").Extract() + + th.AssertNoErr(t, err) + if md == nil { + t.Errorf("Expected non-nil value for md") + } + + createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + MemberID: "f6a818c3d4aa458798ed86892e7150c0", + Schema: "/v2/schemas/member", + Status: "pending", + UpdatedAt: updatedAt, + }, *md) +} + +func TestDeleteMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + counter := HandleImageMemberDeleteSuccessfully(t) + + result := members.Delete(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0") + th.AssertEquals(t, 1, counter.Counter) + th.AssertNoErr(t, result.Err) +} + +func TestMemberUpdateSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + counter := HandleImageMemberUpdate(t) + im, err := members.Update(fakeclient.ServiceClient(), "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + "f6a818c3d4aa458798ed86892e7150c0", + members.UpdateOpts{ + Status: "accepted", + }).Extract() + th.AssertEquals(t, 1, counter.Counter) + th.AssertNoErr(t, err) + + createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "54d63e39-4ee1-4a62-8704-0ae5025a0deb", + MemberID: "f6a818c3d4aa458798ed86892e7150c0", + Schema: "/v2/schemas/member", + Status: "accepted", + UpdatedAt: updatedAt, + }, *im) + +} diff --git a/v4/ecl/imagestorage/v2/members/urls.go b/v4/ecl/imagestorage/v2/members/urls.go new file mode 100644 index 0000000..9e6442d --- /dev/null +++ b/v4/ecl/imagestorage/v2/members/urls.go @@ -0,0 +1,31 @@ +package members + +import "github.com/nttcom/eclcloud/v4" + +func imageMembersURL(c *eclcloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID, "members") +} + +func listMembersURL(c *eclcloud.ServiceClient, imageID string) string { + return imageMembersURL(c, imageID) +} + +func createMemberURL(c *eclcloud.ServiceClient, imageID string) string { + return imageMembersURL(c, imageID) +} + +func imageMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return c.ServiceURL("images", imageID, "members", memberID) +} + +func getMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} + +func updateMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} + +func deleteMemberURL(c *eclcloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} diff --git a/v4/ecl/managed_load_balancer/v1/certificates/doc.go b/v4/ecl/managed_load_balancer/v1/certificates/doc.go new file mode 100644 index 0000000..02633bc --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/doc.go @@ -0,0 +1,104 @@ +/* +Package certificates contains functionality for working with ECL Managed Load Balancer resources. + +Example to list certificates + + listOpts := certificates.ListOpts{} + + allPages, err := certificates.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allCertificates, err := certificates.ExtractCertificates(allPages) + if err != nil { + panic(err) + } + + for _, certificate := range allCertificates { + fmt.Printf("%+v\n", certificate) + } + +Example to create a certificate + + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := certificates.CreateOpts{ + Name: "certificate", + Description: "description", + Tags: tags, + } + + certificate, err := certificates.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", certificate) + +Example to show a certificate + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificate, err := certificates.Show(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", certificate) + +Example to update a certificate + + name := "certificate" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := certificates.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificate, err := certificates.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", certificate) + +Example to delete a certificate + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := certificates.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to upload a certificate file + + uploadFileOpts := certificates.UploadFileOpts{ + Type: "ca-cert", + Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEKMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMwo0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1CjY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1NjcKODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OQpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCCkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0QKRUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRgpHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdICklKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUoKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTApNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OCk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1AKUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUgpTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUClVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVYKV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWApZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaCmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWIKY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZAplZmdoaWprbG1ub3BxcnN0dXZ3eHl6VjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlCmZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificateFile, err := certificates.UploadFile(managedLoadBalancerClient, id, uploadFileOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", certificateFile) +*/ +package certificates diff --git a/v4/ecl/managed_load_balancer/v1/certificates/requests.go b/v4/ecl/managed_load_balancer/v1/certificates/requests.go new file mode 100644 index 0000000..67605cf --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/requests.go @@ -0,0 +1,236 @@ +package certificates + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Certificates +*/ + +// 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 certificate attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` + + // - CA certificate file upload status of the certificate + CACertStatus string `q:"ca_cert_status"` + + // - SSL certificate file upload status of the certificate + SSLCertStatus string `q:"ssl_cert_status"` + + // - SSL key file upload status of the certificate + SSLKeyStatus string `q:"ssl_key_status"` +} + +// ToCertificateListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCertificateListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToCertificateListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of certificates. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToCertificateListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CertificatePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Create Certificate +*/ + +// CreateOpts represents options used to create a new certificate. +type CreateOpts struct { + + // - Name of the certificate + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the certificate + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the certificate + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` +} + +// ToCertificateCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToCertificateCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "certificate") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToCertificateCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new certificate using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToCertificateCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Certificate +*/ + +// Show retrieves a specific certificate based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string) (r ShowResult) { + _, r.Err = c.Get(showURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Certificate +*/ + +// UpdateOpts represents options used to update a existing certificate. +type UpdateOpts struct { + + // - Name of the certificate + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the certificate + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the certificate + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToCertificateUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToCertificateUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "certificate") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToCertificateUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing certificate using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToCertificateUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Certificate +*/ + +// Delete accepts a unique ID and deletes the certificate associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Upload Certificate File +*/ + +// UploadFileOpts represents options used to upload a file to a existing certificate. +type UploadFileOpts struct { + + // - Type of the certificate file to be uploaded + // - Can be uploaded only once for each type + Type string `json:"type"` + + // - Content of the certificate file to be uploaded + // - Content must be Base64 encoded + // - The file size before encoding must be less than or equal to 16KB + // - The file format before encoding must be PEM + // - DER can be converted to PEM by using OpenSSL command + Content string `json:"content"` +} + +// ToCertificateUploadFileMap builds a request body from UploadFileOpts. +func (opts UploadFileOpts) ToCertificateUploadFileMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// UploadFileOptsBuilder allows extensions to add additional parameters to the UploadFile request. +type UploadFileOptsBuilder interface { + ToCertificateUploadFileMap() (map[string]interface{}, error) +} + +// UploadFile accepts a UploadFileOpts struct and uploads a file to a existing certificate using the values provided. +func UploadFile(c *eclcloud.ServiceClient, id string, opts UploadFileOptsBuilder) (r UploadFileResult) { + b, err := opts.ToCertificateUploadFileMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(uploadFileURL(c, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/certificates/results.go b/v4/ecl/managed_load_balancer/v1/certificates/results.go new file mode 100644 index 0000000..b3a3c1f --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/results.go @@ -0,0 +1,116 @@ +package certificates + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a Certificate. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Certificate. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a Certificate. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UploadFileResult represents the result of a UploadFile operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type UploadFileResult struct { + eclcloud.ErrResult +} + +// FileInResponse represents a file in a certificate. +type FileInResponse struct { + + // - File upload status of the certificate + Status string `json:"status"` +} + +// Certificate represents a certificate. +type Certificate struct { + + // - ID of the certificate + ID string `json:"id"` + + // - Name of the certificate + Name string `json:"name"` + + // - Description of the certificate + Description string `json:"description"` + + // - Tags of the certificate (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - ID of the owner tenant of the certificate + TenantID string `json:"tenant_id"` + + // - CA certificate file of the certificate + CACert FileInResponse `json:"ca_cert"` + + // - SSL certificate file of the certificate + SSLCert FileInResponse `json:"ssl_cert"` + + // - SSL key file of the certificate + SSLKey FileInResponse `json:"ssl_key"` +} + +// ExtractInto interprets any commonResult as a certificate, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "certificate") +} + +// Extract is a function that accepts a result and extracts a Certificate resource. +func (r commonResult) Extract() (*Certificate, error) { + var certificate Certificate + + err := r.ExtractInto(&certificate) + + return &certificate, err +} + +// CertificatePage is the page returned by a pager when traversing over a collection of certificate. +type CertificatePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a CertificatePage struct is empty. +func (r CertificatePage) IsEmpty() (bool, error) { + is, err := ExtractCertificates(r) + + return len(is) == 0, err +} + +// ExtractCertificatesInto interprets the results of a single page from a List() call, producing a slice of certificate entities. +func ExtractCertificatesInto(r pagination.Page, v interface{}) error { + return r.(CertificatePage).Result.ExtractIntoSlicePtr(v, "certificates") +} + +// ExtractCertificates accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Certificate structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractCertificates(r pagination.Page) ([]Certificate, error) { + var s []Certificate + + err := ExtractCertificatesInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/certificates/testing/doc.go b/v4/ecl/managed_load_balancer/v1/certificates/testing/doc.go new file mode 100644 index 0000000..5f40172 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains certificate unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/certificates/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/certificates/testing/fixtures.go new file mode 100644 index 0000000..952c582 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/testing/fixtures.go @@ -0,0 +1,256 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/certificates" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "certificates": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "certificate", + "description": "description", + "tags": { + "key": "value" + }, + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ca_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_key": { + "status": "NOT_UPLOADED" + } + } + ] +}`) + +func listResult() []certificates.Certificate { + var certificate1 certificates.Certificate + + sslKey1 := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + sslCert1 := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + caCert1 := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + certificate1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificate1.Name = "certificate" + certificate1.Description = "description" + certificate1.Tags = tags1 + certificate1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + certificate1.CACert = caCert1 + certificate1.SSLCert = sslCert1 + certificate1.SSLKey = sslKey1 + + return []certificates.Certificate{certificate1} +} + +var createRequest = fmt.Sprintf(` +{ + "certificate": { + "name": "certificate", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "certificate": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "certificate", + "description": "description", + "tags": { + "key": "value" + }, + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ca_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_key": { + "status": "NOT_UPLOADED" + } + } +}`) + +func createResult() *certificates.Certificate { + var certificate certificates.Certificate + + sslKey := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + sslCert := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + caCert := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + certificate.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificate.Name = "certificate" + certificate.Description = "description" + certificate.Tags = tags + certificate.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + certificate.CACert = caCert + certificate.SSLCert = sslCert + certificate.SSLKey = sslKey + + return &certificate +} + +var showResponse = fmt.Sprintf(` +{ + "certificate": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "certificate", + "description": "description", + "tags": { + "key": "value" + }, + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ca_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_key": { + "status": "NOT_UPLOADED" + } + } +}`) + +func showResult() *certificates.Certificate { + var certificate certificates.Certificate + + sslKey := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + sslCert := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + caCert := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + certificate.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificate.Name = "certificate" + certificate.Description = "description" + certificate.Tags = tags + certificate.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + certificate.CACert = caCert + certificate.SSLCert = sslCert + certificate.SSLKey = sslKey + + return &certificate +} + +var updateRequest = fmt.Sprintf(` +{ + "certificate": { + "name": "certificate", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "certificate": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "certificate", + "description": "description", + "tags": { + "key": "value" + }, + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ca_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_cert": { + "status": "NOT_UPLOADED" + }, + "ssl_key": { + "status": "NOT_UPLOADED" + } + } +}`) + +func updateResult() *certificates.Certificate { + var certificate certificates.Certificate + + sslKey := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + sslCert := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + caCert := certificates.FileInResponse{ + Status: "NOT_UPLOADED", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + certificate.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + certificate.Name = "certificate" + certificate.Description = "description" + certificate.Tags = tags + certificate.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + certificate.CACert = caCert + certificate.SSLCert = sslCert + certificate.SSLKey = sslKey + + return &certificate +} + +var uploadFileRequest = fmt.Sprintf(` +{ + "type": "ca-cert", + "content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEKMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMwo0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1CjY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1NjcKODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OQpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCCkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0QKRUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRgpHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdICklKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUoKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTApNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OCk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1AKUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUgpTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUClVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVYKV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWApZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaCmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWIKY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZAplZmdoaWprbG1ub3BxcnN0dXZ3eHl6VjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlCmZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" +}`) diff --git a/v4/ecl/managed_load_balancer/v1/certificates/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/certificates/testing/requests_test.go new file mode 100644 index 0000000..f1c6716 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/testing/requests_test.go @@ -0,0 +1,215 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/certificates" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListCertificates(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/certificates", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := certificates.ListOpts{} + + err := certificates.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := certificates.ExtractCertificates(page) + if err != nil { + t.Errorf("Failed to extract certificates: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateCertificate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/certificates", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := certificates.CreateOpts{ + Name: "certificate", + Description: "description", + Tags: tags, + } + + actual, err := certificates.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowCertificate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/certificates/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + + actual, err := certificates.Show(cli, id).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateCertificate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/certificates/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "certificate" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := certificates.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := certificates.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteCertificate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/certificates/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := certificates.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestUploadFileCertificate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/certificates/%s/files", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, uploadFileRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + uploadFileOpts := certificates.UploadFileOpts{ + Type: "ca-cert", + Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEKMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMwo0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1CjY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1NjcKODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OQpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCCkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0QKRUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRgpHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdICklKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUoKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTApNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OCk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1AKUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUgpTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUClVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVYKV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWApZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaCmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWIKY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZAplZmdoaWprbG1ub3BxcnN0dXZ3eHl6VjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlCmZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + } + + err := certificates.UploadFile(cli, id, uploadFileOpts).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/certificates/urls.go b/v4/ecl/managed_load_balancer/v1/certificates/urls.go new file mode 100644 index 0000000..03d4fc2 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/certificates/urls.go @@ -0,0 +1,41 @@ +package certificates + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("certificates") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("certificates", id) +} + +func filesURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("certificates", id, "files") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func uploadFileURL(c *eclcloud.ServiceClient, id string) string { + return filesURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/doc.go b/v4/ecl/managed_load_balancer/v1/health_monitors/doc.go new file mode 100644 index 0000000..8fb714c --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/doc.go @@ -0,0 +1,164 @@ +/* +Package health_monitors contains functionality for working with ECL Managed Load Balancer resources. + +Example to list health monitors + + listOpts := health_monitors.ListOpts{} + + allPages, err := health_monitors.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allHealthMonitors, err := health_monitors.ExtractHealthMonitors(allPages) + if err != nil { + panic(err) + } + + for _, healthMonitor := range allHealthMonitors { + fmt.Printf("%+v\n", healthMonitor) + } + +Example to create a health monitor + + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := health_monitors.CreateOpts{ + Name: "health_monitor", + Description: "description", + Tags: tags, + Port: 80, + Protocol: "http", + Interval: 5, + Retry: 3, + Timeout: 5, + Path: "/health", + HttpStatusCode: "200-299", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + healthMonitor, err := health_monitors.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", healthMonitor) + +Example to show a health monitor + + showOpts := health_monitors.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitor, err := health_monitors.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", healthMonitor) + +Example to update a health monitor + + name := "health_monitor" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := health_monitors.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitor, err := health_monitors.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", healthMonitor) + +Example to delete a health monitor + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := health_monitors.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged health monitor configurations + + createStagedOpts := health_monitors.CreateStagedOpts{ + Port: 80, + Protocol: "http", + Interval: 5, + Retry: 3, + Timeout: 5, + Path: "/health", + HttpStatusCode: "200-299", + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitorConfigurations, err := health_monitors.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", healthMonitorConfigurations) + +Example to show staged health monitor configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitorConfigurations, err := health_monitors.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", healthMonitorConfigurations) + +Example to update staged health monitor configurations + + port := 80 + protocol := "http" + interval := 5 + retry := 3 + timeout := 5 + path := "/health" + httpStatusCode := "200-299" + updateStagedOpts := health_monitors.UpdateStagedOpts{ + Port: &port, + Protocol: &protocol, + Interval: &interval, + Retry: &retry, + Timeout: &timeout, + Path: &path, + HttpStatusCode: &httpStatusCode, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitorConfigurations, err := health_monitors.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", healthMonitorConfigurations) + +Example to cancel staged health monitor configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := health_monitors.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package health_monitors diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/requests.go b/v4/ecl/managed_load_balancer/v1/health_monitors/requests.go new file mode 100644 index 0000000..a970672 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/requests.go @@ -0,0 +1,430 @@ +package health_monitors + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Health Monitors +*/ + +// 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 health monitor attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - Port number of the resource for healthchecking or listening + Port int `q:"port"` + + // - Protocol of the resource for healthchecking or listening + Protocol string `q:"protocol"` + + // - Interval of healthchecking (in seconds) + Interval int `q:"interval"` + + // - Retry count of healthchecking + Retry int `q:"retry"` + + // - Timeout of healthchecking (in seconds) + Timeout int `q:"timeout"` + + // - URL path of healthchecking + // - Must be started with `"/"` + Path string `q:"path"` + + // - HTTP status codes expected in healthchecking + // - Format: `"xxx"` or `"xxx-xxx"` ( `xxx` between [100, 599]) + HttpStatusCode string `q:"http_status_code"` + + // - ID of the load balancer which the resource belongs to + LoadBalancerID string `q:"load_balancer_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToHealthMonitorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToHealthMonitorListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToHealthMonitorListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of health monitors. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToHealthMonitorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return HealthMonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Create Health Monitor +*/ + +// CreateOpts represents options used to create a new health monitor. +type CreateOpts struct { + + // - Name of the health monitor + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the health monitor + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the health monitor + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - Port number of the health monitor for healthchecking + // - If 'protocol' is 'icmp', value must be set `0` + Port int `json:"port"` + + // - Protocol of the health monitor for healthchecking + Protocol string `json:"protocol"` + + // - Interval of healthchecking (in seconds) + Interval int `json:"interval,omitempty"` + + // - Retry count of healthchecking + // - Initial monitoring is not included + // - Retry is executed at the interval set in `interval` + Retry int `json:"retry,omitempty"` + + // - Timeout of healthchecking (in seconds) + // - Value must be less than or equal to `interval` + Timeout int `json:"timeout,omitempty"` + + // - URL path of healthchecking + // - If `protocol` is `"http"` or `"https"`, URL path can be set + // - If `protocol` is neither `"http"` nor `"https"`, URL path must not be set + // - Must be started with / + Path string `json:"path,omitempty"` + + // - HTTP status codes expected in healthchecking + // - If `protocol` is `"http"` or `"https"`, HTTP status code (or range) can be set + // - If `protocol` is neither `"http"` nor `"https"`, HTTP status code (or range) must not be set + // - Format: `"xxx"` or `"xxx-xxx"` ( `xxx` between [100, 599]) + HttpStatusCode string `json:"http_status_code,omitempty"` + + // - ID of the load balancer which the health monitor belongs to + LoadBalancerID string `json:"load_balancer_id"` +} + +// ToHealthMonitorCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToHealthMonitorCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "health_monitor") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToHealthMonitorCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new health monitor using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToHealthMonitorCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Health Monitor +*/ + +// ShowOpts represents options used to show a health monitor. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToHealthMonitorShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToHealthMonitorShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToHealthMonitorShowQuery() (string, error) +} + +// Show retrieves a specific health monitor based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToHealthMonitorShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Health Monitor Attributes +*/ + +// UpdateOpts represents options used to update a existing health monitor. +type UpdateOpts struct { + + // - Name of the health monitor + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the health monitor + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the health monitor + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToHealthMonitorUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToHealthMonitorUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "health_monitor") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToHealthMonitorUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing health monitor using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToHealthMonitorUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Health Monitor +*/ + +// Delete accepts a unique ID and deletes the health monitor associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Health Monitor Configurations +*/ + +// CreateStagedOpts represents options used to create new health monitor configurations. +type CreateStagedOpts struct { + + // - Port number of the health monitor for healthchecking + // - If 'protocol' is 'icmp', value must be set `0` + Port int `json:"port,omitempty"` + + // - Protocol of the health monitor for healthchecking + Protocol string `json:"protocol,omitempty"` + + // - Interval of healthchecking (in seconds) + Interval int `json:"interval,omitempty"` + + // - Retry count of healthchecking + // - Initial monitoring is not included + // - Retry is executed at the interval set in `interval` + Retry int `json:"retry,omitempty"` + + // - Timeout of healthchecking (in seconds) + // - Value must be less than or equal to `interval` + Timeout int `json:"timeout,omitempty"` + + // - URL path of healthchecking + // - If `protocol` is `"http"` or `"https"`, URL path can be set + // - If `protocol` is neither `"http"` nor `"https"`, URL path must not be set + // - Must be started with / + Path string `json:"path,omitempty"` + + // - HTTP status codes expected in healthchecking + // - If `protocol` is `"http"` or `"https"`, HTTP status code (or range) can be set + // - If `protocol` is neither `"http"` nor `"https"`, HTTP status code (or range) must not be set + // - Format: `"xxx"` or `"xxx-xxx"` ( `xxx` between [100, 599]) + HttpStatusCode string `json:"http_status_code,omitempty"` +} + +// ToHealthMonitorCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToHealthMonitorCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "health_monitor") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToHealthMonitorCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new health monitor configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToHealthMonitorCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Health Monitor Configurations +*/ + +// ShowStaged retrieves specific health monitor configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Health Monitor Configurations +*/ + +// UpdateStagedOpts represents options used to update existing Health Monitor configurations. +type UpdateStagedOpts struct { + + // - Port number of the health monitor for healthchecking + // - If 'protocol' is 'icmp', value must be set `0` + Port *int `json:"port,omitempty"` + + // - Protocol of the health monitor for healthchecking + Protocol *string `json:"protocol,omitempty"` + + // - Interval of healthchecking (in seconds) + Interval *int `json:"interval,omitempty"` + + // - Retry count of healthchecking + // - Initial monitoring is not included + // - Retry is executed at the interval set in `interval` + Retry *int `json:"retry,omitempty"` + + // - Timeout of healthchecking (in seconds) + // - Value must be less than or equal to `interval` + Timeout *int `json:"timeout,omitempty"` + + // - URL path of healthchecking + // - If `protocol` is `"http"` or `"https"`, URL path can be set + // - If `protocol` is neither `"http"` nor `"https"`, URL path must not be set + // - Must be started with / + Path *string `json:"path,omitempty"` + + // - HTTP status codes expected in healthchecking + // - If `protocol` is `"http"` or `"https"`, HTTP status code (or range) can be set + // - If `protocol` is neither `"http"` nor `"https"`, HTTP status code (or range) must not be set + // - Format: `"xxx"` or `"xxx-xxx"` ( `xxx` between [100, 599]) + HttpStatusCode *string `json:"http_status_code,omitempty"` +} + +// ToHealthMonitorUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToHealthMonitorUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "health_monitor") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToHealthMonitorUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Health Monitor configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToHealthMonitorUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Health Monitor Configurations +*/ + +// CancelStaged accepts a unique ID and deletes health monitor configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/results.go b/v4/ecl/managed_load_balancer/v1/health_monitors/results.go new file mode 100644 index 0000000..24cccd2 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/results.go @@ -0,0 +1,220 @@ +package health_monitors + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a HealthMonitor. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a HealthMonitor. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a HealthMonitor. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a HealthMonitor. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a HealthMonitor. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a HealthMonitor. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ConfigurationInResponse represents a configuration in a health monitor. +type ConfigurationInResponse struct { + + // - Port number of the health monitor for healthchecking + // - If `protocol` is `"icmp"`, returns `0` + Port int `json:"port,omitempty"` + + // - Protocol of the health monitor for healthchecking + Protocol string `json:"protocol,omitempty"` + + // - Interval of healthchecking (in seconds) + Interval int `json:"interval,omitempty"` + + // - Retry count of healthchecking + // - Initial monitoring is not included + // - Retry is executed at the interval set in `interval` + Retry int `json:"retry,omitempty"` + + // - Timeout of healthchecking (in seconds) + Timeout int `json:"timeout,omitempty"` + + // - URL path of healthchecking + // - If `protocol` is `"http"` or `"https"`, uses this parameter + Path string `json:"path,omitempty"` + + // - HTTP status codes expected in healthchecking + // - If `protocol` is `"http"` or `"https"`, uses this parameter + // - Format: `"xxx"` or `"xxx-xxx"` ( `xxx` between [100, 599]) + HttpStatusCode string `json:"http_status_code,omitempty"` +} + +// HealthMonitor represents a health monitor. +type HealthMonitor struct { + + // - ID of the health monitor + ID string `json:"id"` + + // - Name of the health monitor + Name string `json:"name"` + + // - Description of the health monitor + Description string `json:"description"` + + // - Tags of the health monitor (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the health monitor + // - `"ACTIVE"` + // - There are no configurations of the health monitor that waiting to be applied + // - `"CREATE_STAGED"` + // - The health monitor has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the health monitor exists that waiting to be applied + // - `"DELETE_STAGED"` + // - The health monitor has been removed and waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Operation status of the load balancer which the health monitor belongs to + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - ID of the load balancer which the health monitor belongs to + LoadBalancerID string `json:"load_balancer_id"` + + // - ID of the owner tenant of the health monitor + TenantID string `json:"tenant_id"` + + // - Port number of the health monitor for healthchecking + // - If `protocol` is `"icmp"`, returns `0` + Port int `json:"port,omitempty"` + + // - Protocol of the health monitor for healthchecking + Protocol string `json:"protocol,omitempty"` + + // - Interval of healthchecking (in seconds) + Interval int `json:"interval,omitempty"` + + // - Retry count of healthchecking + // - Initial monitoring is not included + // - Retry is executed at the interval set in `interval` + Retry int `json:"retry,omitempty"` + + // - Timeout of healthchecking (in seconds) + Timeout int `json:"timeout,omitempty"` + + // - URL path of healthchecking + // - If `protocol` is `"http"` or `"https"`, uses this parameter + Path string `json:"path,omitempty"` + + // - HTTP status codes expected in healthchecking + // - If `protocol` is `"http"` or `"https"`, uses this parameter + // - Format: `"xxx"` or `"xxx-xxx"` ( `xxx` between [100, 599]) + HttpStatusCode string `json:"http_status_code,omitempty"` + + // - Running configurations of the health monitor + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the health monitor that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a health monitor, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "health_monitor") +} + +// Extract is a function that accepts a result and extracts a HealthMonitor resource. +func (r commonResult) Extract() (*HealthMonitor, error) { + var healthMonitor HealthMonitor + + err := r.ExtractInto(&healthMonitor) + + return &healthMonitor, err +} + +// HealthMonitorPage is the page returned by a pager when traversing over a collection of health monitor. +type HealthMonitorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a HealthMonitorPage struct is empty. +func (r HealthMonitorPage) IsEmpty() (bool, error) { + is, err := ExtractHealthMonitors(r) + + return len(is) == 0, err +} + +// ExtractHealthMonitorsInto interprets the results of a single page from a List() call, producing a slice of health monitor entities. +func ExtractHealthMonitorsInto(r pagination.Page, v interface{}) error { + return r.(HealthMonitorPage).Result.ExtractIntoSlicePtr(v, "health_monitors") +} + +// ExtractHealthMonitors accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of HealthMonitor structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractHealthMonitors(r pagination.Page) ([]HealthMonitor, error) { + var s []HealthMonitor + + err := ExtractHealthMonitorsInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/testing/doc.go b/v4/ecl/managed_load_balancer/v1/health_monitors/testing/doc.go new file mode 100644 index 0000000..a6f96d0 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains health monitor unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/health_monitors/testing/fixtures.go new file mode 100644 index 0000000..12b421f --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/testing/fixtures.go @@ -0,0 +1,380 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/health_monitors" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "health_monitors": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "health_monitor", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + } + ] +}`) + +func listResult() []health_monitors.HealthMonitor { + var healthMonitor1 health_monitors.HealthMonitor + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + healthMonitor1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitor1.Name = "health_monitor" + healthMonitor1.Description = "description" + healthMonitor1.Tags = tags1 + healthMonitor1.ConfigurationStatus = "ACTIVE" + healthMonitor1.OperationStatus = "COMPLETE" + healthMonitor1.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + healthMonitor1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + healthMonitor1.Port = 80 + healthMonitor1.Protocol = "http" + healthMonitor1.Interval = 5 + healthMonitor1.Retry = 3 + healthMonitor1.Timeout = 5 + healthMonitor1.Path = "/health" + healthMonitor1.HttpStatusCode = "200-299" + + return []health_monitors.HealthMonitor{healthMonitor1} +} + +var createRequest = fmt.Sprintf(` +{ + "health_monitor": { + "name": "health_monitor", + "description": "description", + "tags": { + "key": "value" + }, + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040" + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "health_monitor": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "health_monitor", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "port": null, + "protocol": null, + "interval": null, + "retry": null, + "timeout": null, + "path": null, + "http_status_code": null + } +}`) + +func createResult() *health_monitors.HealthMonitor { + var healthMonitor health_monitors.HealthMonitor + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + healthMonitor.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitor.Name = "health_monitor" + healthMonitor.Description = "description" + healthMonitor.Tags = tags + healthMonitor.ConfigurationStatus = "CREATE_STAGED" + healthMonitor.OperationStatus = "NONE" + healthMonitor.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + healthMonitor.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + healthMonitor.Port = 0 + healthMonitor.Protocol = "" + healthMonitor.Interval = 0 + healthMonitor.Retry = 0 + healthMonitor.Timeout = 0 + healthMonitor.Path = "" + healthMonitor.HttpStatusCode = "" + + return &healthMonitor +} + +var showResponse = fmt.Sprintf(` +{ + "health_monitor": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "health_monitor", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299", + "current": { + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + }, + "staged": null + } +}`) + +func showResult() *health_monitors.HealthMonitor { + var healthMonitor health_monitors.HealthMonitor + + var staged health_monitors.ConfigurationInResponse + current := health_monitors.ConfigurationInResponse{ + Port: 80, + Protocol: "http", + Interval: 5, + Retry: 3, + Timeout: 5, + Path: "/health", + HttpStatusCode: "200-299", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + healthMonitor.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitor.Name = "health_monitor" + healthMonitor.Description = "description" + healthMonitor.Tags = tags + healthMonitor.ConfigurationStatus = "ACTIVE" + healthMonitor.OperationStatus = "COMPLETE" + healthMonitor.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + healthMonitor.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + healthMonitor.Port = 80 + healthMonitor.Protocol = "http" + healthMonitor.Interval = 5 + healthMonitor.Retry = 3 + healthMonitor.Timeout = 5 + healthMonitor.Path = "/health" + healthMonitor.HttpStatusCode = "200-299" + healthMonitor.Current = current + healthMonitor.Staged = staged + + return &healthMonitor +} + +var updateRequest = fmt.Sprintf(` +{ + "health_monitor": { + "name": "health_monitor", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "health_monitor": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "health_monitor", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "port": null, + "protocol": null, + "interval": null, + "retry": null, + "timeout": null, + "path": null, + "http_status_code": null + } +}`) + +func updateResult() *health_monitors.HealthMonitor { + var healthMonitor health_monitors.HealthMonitor + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + healthMonitor.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + healthMonitor.Name = "health_monitor" + healthMonitor.Description = "description" + healthMonitor.Tags = tags + healthMonitor.ConfigurationStatus = "CREATE_STAGED" + healthMonitor.OperationStatus = "NONE" + healthMonitor.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + healthMonitor.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + healthMonitor.Port = 0 + healthMonitor.Protocol = "" + healthMonitor.Interval = 0 + healthMonitor.Retry = 0 + healthMonitor.Timeout = 0 + healthMonitor.Path = "" + healthMonitor.HttpStatusCode = "" + + return &healthMonitor +} + +var createStagedRequest = fmt.Sprintf(` +{ + "health_monitor": { + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "health_monitor": { + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + } +}`) + +func createStagedResult() *health_monitors.HealthMonitor { + var healthMonitor health_monitors.HealthMonitor + + healthMonitor.Port = 80 + healthMonitor.Protocol = "http" + healthMonitor.Interval = 5 + healthMonitor.Retry = 3 + healthMonitor.Timeout = 5 + healthMonitor.Path = "/health" + healthMonitor.HttpStatusCode = "200-299" + + return &healthMonitor +} + +var showStagedResponse = fmt.Sprintf(` +{ + "health_monitor": { + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + } +}`) + +func showStagedResult() *health_monitors.HealthMonitor { + var healthMonitor health_monitors.HealthMonitor + + healthMonitor.Port = 80 + healthMonitor.Protocol = "http" + healthMonitor.Interval = 5 + healthMonitor.Retry = 3 + healthMonitor.Timeout = 5 + healthMonitor.Path = "/health" + healthMonitor.HttpStatusCode = "200-299" + + return &healthMonitor +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "health_monitor": { + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "health_monitor": { + "port": 80, + "protocol": "http", + "interval": 5, + "retry": 3, + "timeout": 5, + "path": "/health", + "http_status_code": "200-299" + } +}`) + +func updateStagedResult() *health_monitors.HealthMonitor { + var healthMonitor health_monitors.HealthMonitor + + healthMonitor.Port = 80 + healthMonitor.Protocol = "http" + healthMonitor.Interval = 5 + healthMonitor.Retry = 3 + healthMonitor.Timeout = 5 + healthMonitor.Path = "/health" + healthMonitor.HttpStatusCode = "200-299" + + return &healthMonitor +} diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/health_monitors/testing/requests_test.go new file mode 100644 index 0000000..fad9ab7 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/testing/requests_test.go @@ -0,0 +1,319 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/health_monitors" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListHealthMonitors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/health_monitors", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := health_monitors.ListOpts{} + + err := health_monitors.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := health_monitors.ExtractHealthMonitors(page) + if err != nil { + t.Errorf("Failed to extract health monitors: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/health_monitors", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := health_monitors.CreateOpts{ + Name: "health_monitor", + Description: "description", + Tags: tags, + Port: 80, + Protocol: "http", + Interval: 5, + Retry: 3, + Timeout: 5, + Path: "/health", + HttpStatusCode: "200-299", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + actual, err := health_monitors.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := health_monitors.ShowOpts{} + + actual, err := health_monitors.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "health_monitor" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := health_monitors.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := health_monitors.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := health_monitors.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + createStagedOpts := health_monitors.CreateStagedOpts{ + Port: 80, + Protocol: "http", + Interval: 5, + Retry: 3, + Timeout: 5, + Path: "/health", + HttpStatusCode: "200-299", + } + + actual, err := health_monitors.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := health_monitors.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + port := 80 + protocol := "http" + interval := 5 + retry := 3 + timeout := 5 + path := "/health" + httpStatusCode := "200-299" + updateStagedOpts := health_monitors.UpdateStagedOpts{ + Port: &port, + Protocol: &protocol, + Interval: &interval, + Retry: &retry, + Timeout: &timeout, + Path: &path, + HttpStatusCode: &httpStatusCode, + } + + actual, err := health_monitors.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/health_monitors/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := health_monitors.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/health_monitors/urls.go b/v4/ecl/managed_load_balancer/v1/health_monitors/urls.go new file mode 100644 index 0000000..4811006 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/health_monitors/urls.go @@ -0,0 +1,53 @@ +package health_monitors + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("health_monitors") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("health_monitors", id) +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("health_monitors", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/listeners/doc.go b/v4/ecl/managed_load_balancer/v1/listeners/doc.go new file mode 100644 index 0000000..7682c62 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/doc.go @@ -0,0 +1,148 @@ +/* +Package listeners contains functionality for working with ECL Managed Load Balancer resources. + +Example to list listeners + + listOpts := listeners.ListOpts{} + + allPages, err := listeners.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allListeners, err := listeners.ExtractListeners(allPages) + if err != nil { + panic(err) + } + + for _, listener := range allListeners { + fmt.Printf("%+v\n", listener) + } + +Example to create a listener + + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := listeners.CreateOpts{ + Name: "listener", + Description: "description", + Tags: tags, + IPAddress: "10.0.0.1", + Port: 443, + Protocol: "https", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + listener, err := listeners.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", listener) + +Example to show a listener + + showOpts := listeners.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listener, err := listeners.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", listener) + +Example to update a listener + + name := "listener" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := listeners.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listener, err := listeners.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", listener) + +Example to delete a listener + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := listeners.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged listener configurations + + createStagedOpts := listeners.CreateStagedOpts{ + IPAddress: "10.0.0.1", + Port: 443, + Protocol: "https", + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listenerConfigurations, err := listeners.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", listenerConfigurations) + +Example to show staged listener configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listenerConfigurations, err := listeners.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", listenerConfigurations) + +Example to update staged listener configurations + + ipAddress := "10.0.0.1" + port := 443 + protocol := "https" + updateStagedOpts := listeners.UpdateStagedOpts{ + IPAddress: &ipAddress, + Port: &port, + Protocol: &protocol, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listenerConfigurations, err := listeners.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", listenerConfigurations) + +Example to cancel staged listener configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := listeners.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package listeners diff --git a/v4/ecl/managed_load_balancer/v1/listeners/requests.go b/v4/ecl/managed_load_balancer/v1/listeners/requests.go new file mode 100644 index 0000000..3bd5c44 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/requests.go @@ -0,0 +1,362 @@ +package listeners + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Listeners +*/ + +// 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 listener attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - IP address of the resource for listening + IPAddress string `q:"ip_address"` + + // - Port number of the resource for healthchecking or listening + Port int `q:"port"` + + // - Protocol of the resource for healthchecking or listening + Protocol string `q:"protocol"` + + // - ID of the load balancer which the resource belongs to + LoadBalancerID string `q:"load_balancer_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToListenerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToListenerListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToListenerListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of listeners. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToListenerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ListenerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Create Listener +*/ + +// CreateOpts represents options used to create a new listener. +type CreateOpts struct { + + // - Name of the listener + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the listener + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the listener + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - IP address of the listener for listening + // - Set an unique combination of IP address and port in all listeners which belong to the same load balancer + // - Must not set a IP address which is included in `virtual_ip_address` and `reserved_fixed_ips` of load balancer interfaces that the listener belongs to + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress string `json:"ip_address"` + + // - Port number of the listener for listening + // - Combination of IP address and port must be unique for all listeners which belong to the same load balancer + Port int `json:"port"` + + // - Protocol of the listener for listening + Protocol string `json:"protocol"` + + // - ID of the load balancer which the listener belongs to + LoadBalancerID string `json:"load_balancer_id"` +} + +// ToListenerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "listener") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToListenerCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new listener using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToListenerCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Listener +*/ + +// ShowOpts represents options used to show a listener. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToListenerShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToListenerShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToListenerShowQuery() (string, error) +} + +// Show retrieves a specific listener based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToListenerShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Listener Attribute +*/ + +// UpdateOpts represents options used to update a existing listener. +type UpdateOpts struct { + + // - Name of the listener + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the listener + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the listener + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToListenerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "listener") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToListenerUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing listener using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToListenerUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Listener +*/ + +// Delete accepts a unique ID and deletes the listener associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Listener Configurations +*/ + +// CreateStagedOpts represents options used to create new listener configurations. +type CreateStagedOpts struct { + + // - IP address of the listener for listening + // - Set an unique combination of IP address and port in all listeners which belong to the same load balancer + // - Must not set a IP address which is included in `virtual_ip_address` and `reserved_fixed_ips` of load balancer interfaces that the listener belongs to + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress string `json:"ip_address,omitempty"` + + // - Port number of the listener for listening + // - Combination of IP address and port must be unique for all listeners which belong to the same load balancer + Port int `json:"port,omitempty"` + + // - Protocol of the listener for listening + Protocol string `json:"protocol,omitempty"` +} + +// ToListenerCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToListenerCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "listener") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToListenerCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new listener configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToListenerCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Listener Configurations +*/ + +// ShowStaged retrieves specific listener configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Listener Configurations +*/ + +// UpdateStagedOpts represents options used to update existing Listener configurations. +type UpdateStagedOpts struct { + + // - IP address of the listener for listening + // - Set an unique combination of IP address and port in all listeners which belong to the same load balancer + // - Must not set a IP address which is included in `virtual_ip_address` and `reserved_fixed_ips` of load balancer interfaces that the listener belongs to + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress *string `json:"ip_address,omitempty"` + + // - Port number of the listener for listening + // - Combination of IP address and port must be unique for all listeners which belong to the same load balancer + Port *int `json:"port,omitempty"` + + // - Protocol of the listener for listening + Protocol *string `json:"protocol,omitempty"` +} + +// ToListenerUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToListenerUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "listener") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToListenerUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Listener configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToListenerUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Listener Configurations +*/ + +// CancelStaged accepts a unique ID and deletes listener configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/listeners/results.go b/v4/ecl/managed_load_balancer/v1/listeners/results.go new file mode 100644 index 0000000..364971e --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/results.go @@ -0,0 +1,184 @@ +package listeners + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a Listener. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Listener. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a Listener. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a Listener. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a Listener. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a Listener. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ConfigurationInResponse represents a configuration in a listener. +type ConfigurationInResponse struct { + + // - IP address of the listener for listening + IPAddress string `json:"ip_address,omitempty"` + + // - Port number of the listener for listening + Port int `json:"port,omitempty"` + + // - Protocol of the listener for listening + Protocol string `json:"protocol,omitempty"` +} + +// Listener represents a listener. +type Listener struct { + + // - ID of the listener + ID string `json:"id"` + + // - Name of the listener + Name string `json:"name"` + + // - Description of the listener + Description string `json:"description"` + + // - Tags of the listener (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the listener + // - `"ACTIVE"` + // - There are no configurations of the listener that waiting to be applied + // - `"CREATE_STAGED"` + // - The listener has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the listener exists that waiting to be applied + // - `"DELETE_STAGED"` + // - The listener has been removed and waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Operation status of the load balancer which the listener belongs to + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - ID of the load balancer which the listener belongs to + LoadBalancerID string `json:"load_balancer_id"` + + // - ID of the owner tenant of the listener + TenantID string `json:"tenant_id"` + + // - IP address of the listener for listening + IPAddress string `json:"ip_address,omitempty"` + + // - Port number of the listener for listening + Port int `json:"port,omitempty"` + + // - Protocol of the listener for listening + Protocol string `json:"protocol,omitempty"` + + // - Running configurations of the listener + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the listener that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a listener, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "listener") +} + +// Extract is a function that accepts a result and extracts a Listener resource. +func (r commonResult) Extract() (*Listener, error) { + var listener Listener + + err := r.ExtractInto(&listener) + + return &listener, err +} + +// ListenerPage is the page returned by a pager when traversing over a collection of listener. +type ListenerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a ListenerPage struct is empty. +func (r ListenerPage) IsEmpty() (bool, error) { + is, err := ExtractListeners(r) + + return len(is) == 0, err +} + +// ExtractListenersInto interprets the results of a single page from a List() call, producing a slice of listener entities. +func ExtractListenersInto(r pagination.Page, v interface{}) error { + return r.(ListenerPage).Result.ExtractIntoSlicePtr(v, "listeners") +} + +// ExtractListeners accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Listener structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractListeners(r pagination.Page) ([]Listener, error) { + var s []Listener + + err := ExtractListenersInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/listeners/testing/doc.go b/v4/ecl/managed_load_balancer/v1/listeners/testing/doc.go new file mode 100644 index 0000000..58b8f72 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains listener unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/listeners/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/listeners/testing/fixtures.go new file mode 100644 index 0000000..b035d5e --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/testing/fixtures.go @@ -0,0 +1,304 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/listeners" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "listeners": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "listener", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + } + ] +}`) + +func listResult() []listeners.Listener { + var listener1 listeners.Listener + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + listener1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listener1.Name = "listener" + listener1.Description = "description" + listener1.Tags = tags1 + listener1.ConfigurationStatus = "ACTIVE" + listener1.OperationStatus = "COMPLETE" + listener1.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + listener1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + listener1.IPAddress = "10.0.0.1" + listener1.Port = 443 + listener1.Protocol = "https" + + return []listeners.Listener{listener1} +} + +var createRequest = fmt.Sprintf(` +{ + "listener": { + "name": "listener", + "description": "description", + "tags": { + "key": "value" + }, + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040" + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "listener": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "listener", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ip_address": null, + "port": null, + "protocol": null + } +}`) + +func createResult() *listeners.Listener { + var listener listeners.Listener + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + listener.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listener.Name = "listener" + listener.Description = "description" + listener.Tags = tags + listener.ConfigurationStatus = "CREATE_STAGED" + listener.OperationStatus = "NONE" + listener.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + listener.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + listener.IPAddress = "" + listener.Port = 0 + listener.Protocol = "" + + return &listener +} + +var showResponse = fmt.Sprintf(` +{ + "listener": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "listener", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https", + "current": { + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + }, + "staged": null + } +}`) + +func showResult() *listeners.Listener { + var listener listeners.Listener + + var staged listeners.ConfigurationInResponse + current := listeners.ConfigurationInResponse{ + IPAddress: "10.0.0.1", + Port: 443, + Protocol: "https", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + listener.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listener.Name = "listener" + listener.Description = "description" + listener.Tags = tags + listener.ConfigurationStatus = "ACTIVE" + listener.OperationStatus = "COMPLETE" + listener.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + listener.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + listener.IPAddress = "10.0.0.1" + listener.Port = 443 + listener.Protocol = "https" + listener.Current = current + listener.Staged = staged + + return &listener +} + +var updateRequest = fmt.Sprintf(` +{ + "listener": { + "name": "listener", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "listener": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "listener", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "ip_address": null, + "port": null, + "protocol": null + } +}`) + +func updateResult() *listeners.Listener { + var listener listeners.Listener + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + listener.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + listener.Name = "listener" + listener.Description = "description" + listener.Tags = tags + listener.ConfigurationStatus = "CREATE_STAGED" + listener.OperationStatus = "NONE" + listener.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + listener.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + listener.IPAddress = "" + listener.Port = 0 + listener.Protocol = "" + + return &listener +} + +var createStagedRequest = fmt.Sprintf(` +{ + "listener": { + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "listener": { + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + } +}`) + +func createStagedResult() *listeners.Listener { + var listener listeners.Listener + + listener.IPAddress = "10.0.0.1" + listener.Port = 443 + listener.Protocol = "https" + + return &listener +} + +var showStagedResponse = fmt.Sprintf(` +{ + "listener": { + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + } +}`) + +func showStagedResult() *listeners.Listener { + var listener listeners.Listener + + listener.IPAddress = "10.0.0.1" + listener.Port = 443 + listener.Protocol = "https" + + return &listener +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "listener": { + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "listener": { + "ip_address": "10.0.0.1", + "port": 443, + "protocol": "https" + } +}`) + +func updateStagedResult() *listeners.Listener { + var listener listeners.Listener + + listener.IPAddress = "10.0.0.1" + listener.Port = 443 + listener.Protocol = "https" + + return &listener +} diff --git a/v4/ecl/managed_load_balancer/v1/listeners/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/listeners/testing/requests_test.go new file mode 100644 index 0000000..bf64de3 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/testing/requests_test.go @@ -0,0 +1,303 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/listeners" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListListeners(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/listeners", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := listeners.ListOpts{} + + err := listeners.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := listeners.ExtractListeners(page) + if err != nil { + t.Errorf("Failed to extract listeners: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/listeners", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := listeners.CreateOpts{ + Name: "listener", + Description: "description", + Tags: tags, + IPAddress: "10.0.0.1", + Port: 443, + Protocol: "https", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + actual, err := listeners.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := listeners.ShowOpts{} + + actual, err := listeners.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "listener" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := listeners.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := listeners.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := listeners.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + createStagedOpts := listeners.CreateStagedOpts{ + IPAddress: "10.0.0.1", + Port: 443, + Protocol: "https", + } + + actual, err := listeners.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := listeners.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + ipAddress := "10.0.0.1" + port := 443 + protocol := "https" + updateStagedOpts := listeners.UpdateStagedOpts{ + IPAddress: &ipAddress, + Port: &port, + Protocol: &protocol, + } + + actual, err := listeners.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/listeners/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := listeners.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/listeners/urls.go b/v4/ecl/managed_load_balancer/v1/listeners/urls.go new file mode 100644 index 0000000..1c2e0a8 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/listeners/urls.go @@ -0,0 +1,53 @@ +package listeners + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("listeners") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("listeners", id) +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("listeners", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/doc.go b/v4/ecl/managed_load_balancer/v1/load_balancers/doc.go new file mode 100644 index 0000000..2d40e69 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/doc.go @@ -0,0 +1,274 @@ +/* +Package load_balancers contains functionality for working with ECL Managed Load Balancer resources. + +Example to list load balancers + + listOpts := load_balancers.ListOpts{} + + allPages, err := load_balancers.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancers, err := load_balancers.ExtractLoadBalancers(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancer := range allLoadBalancers { + fmt.Printf("%+v\n", loadBalancer) + } + +Example to create a load balancer + + reservedFixedIP1 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.CreateOptsInterface{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: &[]load_balancers.CreateOptsReservedFixedIP{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.CreateOptsSyslogServer{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := load_balancers.CreateOpts{ + Name: "load_balancer", + Description: "description", + Tags: tags, + PlanID: "00713021-9aea-41da-9a88-87760c08fa72", + SyslogServers: &[]load_balancers.CreateOptsSyslogServer{syslogServer1}, + Interfaces: &[]load_balancers.CreateOptsInterface{interface1}, + } + + loadBalancer, err := load_balancers.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancer) + +Example to show a load balancer + + showOpts := load_balancers.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancer, err := load_balancers.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancer) + +Example to update a load balancer + + name := "load_balancer" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := load_balancers.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancer, err := load_balancers.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancer) + +Example to delete a load balancer + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := load_balancers.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to perform apply-configurations action on a load balancer + + actionOpts := load_balancers.ActionOpts{ + ApplyConfigurations: true, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := load_balancers.Action(cli, id, actionOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to perform system-update action on a load balancer + + systemUpdate := load_balancers.ActionOptsSystemUpdate{ + SystemUpdateID: "31746df7-92f9-4b5e-ad05-59f6684a54eb", + } + actionOpts := load_balancers.ActionOpts{ + SystemUpdate: &systemUpdate, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := load_balancers.Action(cli, id, actionOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to perform apply-configurations and system-update action on a load balancer + + systemUpdate := load_balancers.ActionOptsSystemUpdate{ + SystemUpdateID: "31746df7-92f9-4b5e-ad05-59f6684a54eb", + } + actionOpts := load_balancers.ActionOpts{ + ApplyConfigurations: true, + SystemUpdate: &systemUpdate, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := load_balancers.Action(cli, id, actionOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to perform cancel-configurations action on a load balancer + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := load_balancers.CancelConfigurations(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged load balancer configurations + + reservedFixedIP1 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.CreateStagedOptsInterface{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: &[]load_balancers.CreateStagedOptsReservedFixedIP{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.CreateStagedOptsSyslogServer{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + createStagedOpts := load_balancers.CreateStagedOpts{ + SyslogServers: &[]load_balancers.CreateStagedOptsSyslogServer{syslogServer1}, + Interfaces: &[]load_balancers.CreateStagedOptsInterface{interface1}, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancerConfigurations, err := load_balancers.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerConfigurations) + +Example to show staged load balancer configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancerConfigurations, err := load_balancers.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerConfigurations) + +Example to update staged load balancer configurations + + reservedFixedIP1IPAddress := "192.168.0.2" + reservedFixedIP1 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP1IPAddress, + } + + reservedFixedIP2IPAddress := "192.168.0.3" + reservedFixedIP2 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP2IPAddress, + } + + reservedFixedIP3IPAddress := "192.168.0.4" + reservedFixedIP3 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP3IPAddress, + } + + reservedFixedIP4IPAddress := "192.168.0.5" + reservedFixedIP4 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP4IPAddress, + } + + interface1NetworkID := "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3" + interface1VirtualIPAddress := "192.168.0.1" + interface1 := load_balancers.UpdateStagedOptsInterface{ + NetworkID: &interface1NetworkID, + VirtualIPAddress: &interface1VirtualIPAddress, + ReservedFixedIPs: &[]load_balancers.UpdateStagedOptsReservedFixedIP{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + + syslogServer1IPAddress := "192.168.0.6" + syslogServer1Port := 514 + syslogServer1Protocol := "udp" + syslogServer1 := load_balancers.UpdateStagedOptsSyslogServer{ + IPAddress: &syslogServer1IPAddress, + Port: &syslogServer1Port, + Protocol: &syslogServer1Protocol, + } + + updateStagedOpts := load_balancers.UpdateStagedOpts{ + SyslogServers: &[]load_balancers.UpdateStagedOptsSyslogServer{syslogServer1}, + Interfaces: &[]load_balancers.UpdateStagedOptsInterface{interface1}, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancerConfigurations, err := load_balancers.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerConfigurations) + +Example to cancel staged load balancer configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := load_balancers.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package load_balancers diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/requests.go b/v4/ecl/managed_load_balancer/v1/load_balancers/requests.go new file mode 100644 index 0000000..1ba6593 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/requests.go @@ -0,0 +1,580 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Load Balancers +*/ + +// 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 load balancer attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Monitoring status of the load balancer + MonitoringStatus string `q:"monitoring_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - The zone / group where the primary virtual server of load balancer is deployed + PrimaryAvailabilityZone string `q:"primary_availability_zone"` + + // - The zone / group where the secondary virtual server of load balancer is deployed + SecondaryAvailabilityZone string `q:"secondary_availability_zone"` + + // - Primary or secondary availability zone where the load balancer is currently running + ActiveAvailabilityZone string `q:"active_availability_zone"` + + // - Revision of the load balancer + Revision int `q:"revision"` + + // - ID of the plan + PlanID string `q:"plan_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToLoadBalancerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToLoadBalancerListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of load balancers. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToLoadBalancerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Create Load Balancer +*/ + +// CreateOptsReservedFixedIP represents reserved_fixed_ip information in the load balancer creation. +type CreateOptsReservedFixedIP struct { + + // - The IP address assign to this interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + // - Set an unique IP address in `virtual_ip_address` and `reserved_fixed_ips` + // - Must not set a network IP address and broadcast IP address + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress string `json:"ip_address"` +} + +// CreateOptsInterface represents interface information in the load balancer creation. +type CreateOptsInterface struct { + + // - ID of the network that this interface belongs to + // - Set a unique network ID in `interfaces` + // - Set a network of which plane is data + // - Must not set ID of a network that uses ISP shared address (RFC 6598) + NetworkID string `json:"network_id"` + + // - Virtual IP address of the interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + // - Set an unique IP address in `virtual_ip_address` and `reserved_fixed_ips` + // - Set a network IP address and broadcast IP address + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + VirtualIPAddress string `json:"virtual_ip_address"` + + // - IP addresses that are pre-reserved for applying configurations of load balancer to be performed without losing redundancy + ReservedFixedIPs *[]CreateOptsReservedFixedIP `json:"reserved_fixed_ips"` +} + +// CreateOptsSyslogServer represents syslog_server information in the load balancer creation. +type CreateOptsSyslogServer struct { + + // - IP address of the syslog server + // - The load balancer sends ICMP to this IP address for health check purpose + IPAddress string `json:"ip_address"` + + // - Port number of the syslog server + Port int `json:"port,omitempty"` + + // - Protocol of the syslog server + // - Set same protocol in all syslog servers which belong to the same load balancer + Protocol string `json:"protocol,omitempty"` +} + +// CreateOpts represents options used to create a new load balancer. +type CreateOpts struct { + + // - Name of the load balancer + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the load balancer + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the load balancer + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - ID of the plan + PlanID string `json:"plan_id,omitempty"` + + // - Syslog servers to which access logs are transferred + // - The facility code of syslog is 0 (kern), and the severity level is 6 (info) + // - Only access logs to listeners which `protocol` is either `"http"` or `"https"` are transferred + // - If `protocol` of `syslog_servers` is `"tcp"` + // - Access logs are transferred to all healthy syslog servers set in `syslog_servers` + // - If `protocol` of `syslog_servers` is `"udp"` + // - Access logs are transferred to the syslog server set first in `syslog_servers` as long as it is healthy + // - Access logs are transferred to the syslog server set second (last) in `syslog_servers` if the first syslog server is not healthy + SyslogServers *[]CreateOptsSyslogServer `json:"syslog_servers,omitempty"` + + // - Interfaces that attached to the load balancer + // - `virtual_ip_address` and `reserved_fixed_ips` can not be changed once attached + // - To change `virtual_ip_address` and `reserved_fixed_ips` , recreating the interface is needed + Interfaces *[]CreateOptsInterface `json:"interfaces,omitempty"` +} + +// ToLoadBalancerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "load_balancer") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToLoadBalancerCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new load balancer using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Load Balancer +*/ + +// ShowOpts represents options used to show a load balancer. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToLoadBalancerShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToLoadBalancerShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToLoadBalancerShowQuery() (string, error) +} + +// Show retrieves a specific load balancer based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToLoadBalancerShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Load Balancer Attributes +*/ + +// UpdateOpts represents options used to update a existing load balancer. +type UpdateOpts struct { + + // - Name of the load balancer + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the load balancer + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the load balancer + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "load_balancer") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToLoadBalancerUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing load balancer using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Load Balancer +*/ + +// Delete accepts a unique ID and deletes the load balancer associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Action Load Balancer +*/ + +// ActionOptsSystemUpdate represents system-update information in the load balancer action. +type ActionOptsSystemUpdate struct { + + // - ID of the system update that will be applied to the load balancer + SystemUpdateID string `json:"system_update_id"` +} + +// ActionOpts represents options used to perform action on a existing load balancer. +type ActionOpts struct { + + // - Added or changed configurations of the load balancer and related resources will be applied + ApplyConfigurations bool `json:"apply-configurations,omitempty"` + + // - Apply the system update to the load balancer + SystemUpdate *ActionOptsSystemUpdate `json:"system-update,omitempty"` +} + +// ToLoadBalancerActionMap builds a request body from ActionOpts. +func (opts ActionOpts) ToLoadBalancerActionMap() map[string]interface{} { + optsMap := make(map[string]interface{}) + + if opts.ApplyConfigurations { + optsMap["apply-configurations"] = nil + } + + if opts.SystemUpdate != nil { + optsMap["system-update"] = map[string]interface{}{ + "system_update_id": opts.SystemUpdate.SystemUpdateID, + } + } + + return optsMap +} + +// ActionOptsBuilder allows extensions to add additional parameters to the Action request. +type ActionOptsBuilder interface { + ToLoadBalancerActionMap() map[string]interface{} +} + +// Action accepts a ActionOpts struct and performs action on a existing load balancer using the values provided. +func Action(c *eclcloud.ServiceClient, id string, opts ActionOptsBuilder) (r ActionResult) { + b := opts.ToLoadBalancerActionMap() + + _, r.Err = c.Post(actionURL(c, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +// CancelConfigurations performs action on a existing load balancer. +func CancelConfigurations(c *eclcloud.ServiceClient, id string) (r ActionResult) { + b := map[string]interface{}{"cancel-configurations": nil} + _, r.Err = c.Post(actionURL(c, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Load Balancer Configurations +*/ + +// CreateStagedOptsReservedFixedIP represents reserved_fixed_ip information in the load balancer configurations creation. +type CreateStagedOptsReservedFixedIP struct { + + // - The IP address assign to this interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + // - Set an unique IP address in `virtual_ip_address` and `reserved_fixed_ips` + // - Must not set a network IP address and broadcast IP address + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress string `json:"ip_address"` +} + +// CreateStagedOptsInterface represents interface information in the load balancer configurations creation. +type CreateStagedOptsInterface struct { + + // - ID of the network that this interface belongs to + // - Set a unique network ID in `interfaces` + // - Set a network of which plane is data + // - Must not set ID of a network that uses ISP shared address (RFC 6598) + NetworkID string `json:"network_id"` + + // - Virtual IP address of the interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + // - Set an unique IP address in `virtual_ip_address` and `reserved_fixed_ips` + // - Must not set a network IP address and broadcast IP address + // - If there are no changes to the `network_id` within the `interfaces[]` , set the current `virtual_ip_address` value + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + VirtualIPAddress string `json:"virtual_ip_address"` + + // - IP addresses that are pre-reserved for applying configurations of load balancer to be performed without losing redundancy + // - If there are no changes to the `network_id` within the `interfaces[]` , set the current `reserved_fixed_ips` value + ReservedFixedIPs *[]CreateStagedOptsReservedFixedIP `json:"reserved_fixed_ips"` +} + +// CreateStagedOptsSyslogServer represents syslog_server information in the load balancer configurations creation. +type CreateStagedOptsSyslogServer struct { + + // - IP address of the syslog server + // - The load balancer sends ICMP to this IP address for health check purpose + IPAddress string `json:"ip_address"` + + // - Port number of the syslog server + Port int `json:"port,omitempty"` + + // - Protocol of the syslog server + // - Set same protocol in all syslog servers which belong to the same load balancer + Protocol string `json:"protocol,omitempty"` +} + +// CreateStagedOpts represents options used to create new load balancer configurations. +type CreateStagedOpts struct { + + // - Syslog servers to which access logs are transferred + // - The facility code of syslog is 0 (kern), and the severity level is 6 (info) + // - Only access logs to listeners which `protocol` is either `"http"` or `"https"` are transferred + // - If `protocol` of `syslog_servers` is `"tcp"` + // - Access logs are transferred to all healthy syslog servers set in `syslog_servers` + // - If `protocol` of `syslog_servers` is `"udp"` + // - Access logs are transferred to the syslog server set first in `syslog_servers` as long as it is healthy + // - Access logs are transferred to the syslog server set second (last) in `syslog_servers` if the first syslog server is not healthy + SyslogServers *[]CreateStagedOptsSyslogServer `json:"syslog_servers,omitempty"` + + // - Interfaces that attached to the load balancer + // - `virtual_ip_address` and `reserved_fixed_ips` can not be changed once attached + // - To change `virtual_ip_address` and `reserved_fixed_ips` , recreating the interface is needed + Interfaces *[]CreateStagedOptsInterface `json:"interfaces,omitempty"` +} + +// ToLoadBalancerCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToLoadBalancerCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "load_balancer") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToLoadBalancerCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new load balancer configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToLoadBalancerCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Load Balancer Configurations +*/ + +// ShowStaged retrieves specific load balancer configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Load Balancer Configurations +*/ + +// UpdateStagedOptsReservedFixedIP represents reserved_fixed_ip information in load balancer configurations updation. +type UpdateStagedOptsReservedFixedIP struct { + + // - The IP address assign to this interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + // - Set an unique IP address in `virtual_ip_address` and `reserved_fixed_ips` + // - Must not set a network IP address and broadcast IP address + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress *string `json:"ip_address"` +} + +// UpdateStagedOptsInterface represents interface information in load balancer configurations updation. +type UpdateStagedOptsInterface struct { + + // - ID of the network that this interface belongs to + // - Set a unique network ID in `interfaces` + // - Set a network of which plane is data + // - Must not set ID of a network that uses ISP shared address (RFC 6598) + NetworkID *string `json:"network_id"` + + // - Virtual IP address of the interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + // - Set an unique IP address in `virtual_ip_address` and `reserved_fixed_ips` + // - Must not set a network IP address and broadcast IP address + // - If there are no changes to the `network_id` within the `interfaces[]` , set the current `virtual_ip_address` value + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + VirtualIPAddress *string `json:"virtual_ip_address"` + + // - IP addresses that are pre-reserved for applying configurations of load balancer to be performed without losing redundancy + // - If there are no changes to the `network_id` within the `interfaces[]` , set the current `reserved_fixed_ips` value + ReservedFixedIPs *[]UpdateStagedOptsReservedFixedIP `json:"reserved_fixed_ips"` +} + +// UpdateStagedOptsSyslogServer represents syslog_server information in load balancer configurations updation. +type UpdateStagedOptsSyslogServer struct { + + // - IP address of the syslog server + // - The load balancer sends ICMP to this IP address for health check purpose + IPAddress *string `json:"ip_address"` + + // - Port number of the syslog server + Port *int `json:"port,omitempty"` + + // - Protocol of the syslog server + // - Set same protocol in all syslog servers which belong to the same load balancer + Protocol *string `json:"protocol,omitempty"` +} + +// UpdateStagedOpts represents options used to update existing Load Balancer configurations. +type UpdateStagedOpts struct { + + // - Syslog servers to which access logs are transferred + // - The facility code of syslog is 0 (kern), and the severity level is 6 (info) + // - Only access logs to listeners which `protocol` is either `"http"` or `"https"` are transferred + // - If `protocol` of `syslog_servers` is `"tcp"` + // - Access logs are transferred to all healthy syslog servers set in `syslog_servers` + // - If `protocol` of `syslog_servers` is `"udp"` + // - Access logs are transferred to the syslog server set first in `syslog_servers` as long as it is healthy + // - Access logs are transferred to the syslog server set second (last) in `syslog_servers` if the first syslog server is not healthy + SyslogServers *[]UpdateStagedOptsSyslogServer `json:"syslog_servers,omitempty"` + + // - Interfaces that attached to the load balancer + // - `virtual_ip_address` and `reserved_fixed_ips` can not be changed once attached + // - To change `virtual_ip_address` and `reserved_fixed_ips` , recreating the interface is needed + Interfaces *[]UpdateStagedOptsInterface `json:"interfaces,omitempty"` +} + +// ToLoadBalancerUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToLoadBalancerUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "load_balancer") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToLoadBalancerUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Load Balancer configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToLoadBalancerUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Load Balancer Configurations +*/ + +// CancelStaged accepts a unique ID and deletes load balancer configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/results.go b/v4/ecl/managed_load_balancer/v1/load_balancers/results.go new file mode 100644 index 0000000..a51a9e5 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/results.go @@ -0,0 +1,257 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a LoadBalancer. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a LoadBalancer. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a LoadBalancer. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// ActionResult represents the result of a Action operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type ActionResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a LoadBalancer. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a LoadBalancer. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a LoadBalancer. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ReservedFixedIPInResponse represents a reserved fixed ip in a load balancer. +type ReservedFixedIPInResponse struct { + + // - The IP address assign to this interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + IPAddress string `json:"ip_address"` +} + +// ConfigurationInResponse represents a configuration in a load balancer. +type ConfigurationInResponse struct { + + // - Syslog servers to which access logs are transferred + // - The facility code of syslog is 0 (kern), and the severity level is 6 (info) + // - Only access logs to listeners which `protocol` is either `"http"` or `"https"` are transferred + // - If `protocol` of `syslog_servers` is `"tcp"` + // - Access logs are transferred to all healthy syslog servers set in `syslog_servers` + // - If `protocol` of `syslog_servers` is `"udp"` + // - Access logs are transferred to the syslog server set first in `syslog_servers` as long as it is healthy + // - Access logs are transferred to the syslog server set second (last) in `syslog_servers` if the first syslog server is not healthy + SyslogServers []SyslogServerInResponse `json:"syslog_servers,omitempty"` + + // - Interfaces that attached to the load balancer + Interfaces []InterfaceInResponse `json:"interfaces,omitempty"` +} + +// InterfaceInResponse represents a interface in a load balancer. +type InterfaceInResponse struct { + + // - ID of the network that this interface belongs to + NetworkID string `json:"network_id"` + + // - Virtual IP address of the interface within subnet + // - Do not use this IP address at the interface of other devices, allowed address pairs, etc + VirtualIPAddress string `json:"virtual_ip_address"` + + // - IP addresses that are pre-reserved for applying configurations of load balancer to be performed without losing redundancy + ReservedFixedIPs []ReservedFixedIPInResponse `json:"reserved_fixed_ips"` +} + +// SyslogServerInResponse represents a syslog server in a load balancer. +type SyslogServerInResponse struct { + + // - IP address of the syslog server + // - The load balancer sends ICMP to this IP address for health check purpose + IPAddress string `json:"ip_address"` + + // - Port number of the syslog server + Port int `json:"port"` + + // - Protocol of the syslog server + Protocol string `json:"protocol"` +} + +// LoadBalancer represents a load balancer. +type LoadBalancer struct { + + // - ID of the load balancer + ID string `json:"id"` + + // - Name of the load balancer + Name string `json:"name"` + + // - Description of the load balancer + Description string `json:"description"` + + // - Tags of the load balancer (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the load balancer + // - `"ACTIVE"` + // - There are no configurations of the load balancer that waiting to be applied + // - `"CREATE_STAGED"` + // - The load balancer has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the load balancer exists that waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Monitoring status of the load balancer + // - `"ACTIVE"` + // - The load balancer is operating normally + // - `"INITIAL"` + // - The load balancer is not deployed and does not monitored + // - `"UNAVAILABLE"` + // - The load balancer is not operating normally + MonitoringStatus string `json:"monitoring_status"` + + // - Operation status of the load balancer + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - The zone / group where the primary virtual server of load balancer is deployed + PrimaryAvailabilityZone string `json:"primary_availability_zone,omitempty"` + + // - The zone / group where the secondary virtual server of load balancer is deployed + SecondaryAvailabilityZone string `json:"secondary_availability_zone,omitempty"` + + // - Primary or secondary availability zone where the load balancer is currently running + // - If can not define active availability zone, returns `"UNDEFINED"` + ActiveAvailabilityZone string `json:"active_availability_zone"` + + // - Revision of the load balancer + Revision int `json:"revision"` + + // - ID of the plan + PlanID string `json:"plan_id"` + + // - Name of the plan + PlanName string `json:"plan_name"` + + // - ID of the owner tenant of the load balancer + TenantID string `json:"tenant_id"` + + // - Syslog servers to which access logs are transferred + // - The facility code of syslog is 0 (kern), and the severity level is 6 (info) + // - Only access logs to listeners which `protocol` is either `"http"` or `"https"` are transferred + // - If `protocol` of `syslog_servers` is `"tcp"` + // - Access logs are transferred to all healthy syslog servers set in `syslog_servers` + // - If `protocol` of `syslog_servers` is `"udp"` + // - Access logs are transferred to the syslog server set first in `syslog_servers` as long as it is healthy + // - Access logs are transferred to the syslog server set second (last) in `syslog_servers` if the first syslog server is not healthy + SyslogServers []SyslogServerInResponse `json:"syslog_servers,omitempty"` + + // - Interfaces that attached to the load balancer + Interfaces []InterfaceInResponse `json:"interfaces,omitempty"` + + // - Running configurations of the load balancer + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the load balancer that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a load balancer, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "load_balancer") +} + +// Extract is a function that accepts a result and extracts a LoadBalancer resource. +func (r commonResult) Extract() (*LoadBalancer, error) { + var loadBalancer LoadBalancer + + err := r.ExtractInto(&loadBalancer) + + return &loadBalancer, err +} + +// LoadBalancerPage is the page returned by a pager when traversing over a collection of load balancer. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (r LoadBalancerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancers(r) + + return len(is) == 0, err +} + +// ExtractLoadBalancersInto interprets the results of a single page from a List() call, producing a slice of load balancer entities. +func ExtractLoadBalancersInto(r pagination.Page, v interface{}) error { + return r.(LoadBalancerPage).Result.ExtractIntoSlicePtr(v, "load_balancers") +} + +// ExtractLoadBalancers accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of LoadBalancer structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s []LoadBalancer + + err := ExtractLoadBalancersInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/testing/doc.go b/v4/ecl/managed_load_balancer/v1/load_balancers/testing/doc.go new file mode 100644 index 0000000..6963c4a --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains load balancer unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/load_balancers/testing/fixtures.go new file mode 100644 index 0000000..1c57968 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/testing/fixtures.go @@ -0,0 +1,697 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/load_balancers" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "load_balancers": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "load_balancer", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "monitoring_status": "ACTIVE", + "operation_status": "COMPLETE", + "primary_availability_zone": "zone1_groupa", + "secondary_availability_zone": "zone1_groupb", + "active_availability_zone": "zone1_groupa", + "revision": 1, + "plan_id": "00713021-9aea-41da-9a88-87760c08fa72", + "plan_name": "50M_HA_4IF", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } + ] +}`) + +func listResult() []load_balancers.LoadBalancer { + var loadBalancer1 load_balancers.LoadBalancer + + reservedFixedIP11 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.2", + } + reservedFixedIP12 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.3", + } + reservedFixedIP13 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.4", + } + reservedFixedIP14 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.5", + } + interface11 := load_balancers.InterfaceInResponse{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: []load_balancers.ReservedFixedIPInResponse{reservedFixedIP11, reservedFixedIP12, reservedFixedIP13, reservedFixedIP14}, + } + syslogServer11 := load_balancers.SyslogServerInResponse{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + loadBalancer1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancer1.Name = "load_balancer" + loadBalancer1.Description = "description" + loadBalancer1.Tags = tags1 + loadBalancer1.ConfigurationStatus = "ACTIVE" + loadBalancer1.MonitoringStatus = "ACTIVE" + loadBalancer1.OperationStatus = "COMPLETE" + loadBalancer1.PrimaryAvailabilityZone = "zone1_groupa" + loadBalancer1.SecondaryAvailabilityZone = "zone1_groupb" + loadBalancer1.ActiveAvailabilityZone = "zone1_groupa" + loadBalancer1.Revision = 1 + loadBalancer1.PlanID = "00713021-9aea-41da-9a88-87760c08fa72" + loadBalancer1.PlanName = "50M_HA_4IF" + loadBalancer1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + loadBalancer1.SyslogServers = []load_balancers.SyslogServerInResponse{syslogServer11} + loadBalancer1.Interfaces = []load_balancers.InterfaceInResponse{interface11} + + return []load_balancers.LoadBalancer{loadBalancer1} +} + +var createRequest = fmt.Sprintf(` +{ + "load_balancer": { + "name": "load_balancer", + "description": "description", + "tags": { + "key": "value" + }, + "plan_id": "00713021-9aea-41da-9a88-87760c08fa72", + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "load_balancer": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "load_balancer", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "monitoring_status": "INITIAL", + "operation_status": "NONE", + "primary_availability_zone": null, + "secondary_availability_zone": null, + "active_availability_zone": "UNDEFINED", + "revision": 1, + "plan_id": "00713021-9aea-41da-9a88-87760c08fa72", + "plan_name": "50M_HA_4IF", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "syslog_servers": null, + "interfaces": null + } +}`) + +func createResult() *load_balancers.LoadBalancer { + var loadBalancer load_balancers.LoadBalancer + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + loadBalancer.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancer.Name = "load_balancer" + loadBalancer.Description = "description" + loadBalancer.Tags = tags + loadBalancer.ConfigurationStatus = "CREATE_STAGED" + loadBalancer.MonitoringStatus = "INITIAL" + loadBalancer.OperationStatus = "NONE" + loadBalancer.PrimaryAvailabilityZone = "" + loadBalancer.SecondaryAvailabilityZone = "" + loadBalancer.ActiveAvailabilityZone = "UNDEFINED" + loadBalancer.Revision = 1 + loadBalancer.PlanID = "00713021-9aea-41da-9a88-87760c08fa72" + loadBalancer.PlanName = "50M_HA_4IF" + loadBalancer.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + loadBalancer.SyslogServers = nil + loadBalancer.Interfaces = nil + + return &loadBalancer +} + +var showResponse = fmt.Sprintf(` +{ + "load_balancer": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "load_balancer", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "monitoring_status": "ACTIVE", + "operation_status": "COMPLETE", + "primary_availability_zone": "zone1_groupa", + "secondary_availability_zone": "zone1_groupb", + "active_availability_zone": "zone1_groupa", + "revision": 1, + "plan_id": "00713021-9aea-41da-9a88-87760c08fa72", + "plan_name": "50M_HA_4IF", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ], + "current": { + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + }, + "staged": null + } +}`) + +func showResult() *load_balancers.LoadBalancer { + var loadBalancer load_balancers.LoadBalancer + + reservedFixedIP1 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.InterfaceInResponse{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: []load_balancers.ReservedFixedIPInResponse{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.SyslogServerInResponse{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + var staged load_balancers.ConfigurationInResponse + current := load_balancers.ConfigurationInResponse{ + SyslogServers: []load_balancers.SyslogServerInResponse{syslogServer1}, + Interfaces: []load_balancers.InterfaceInResponse{interface1}, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + loadBalancer.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancer.Name = "load_balancer" + loadBalancer.Description = "description" + loadBalancer.Tags = tags + loadBalancer.ConfigurationStatus = "ACTIVE" + loadBalancer.MonitoringStatus = "ACTIVE" + loadBalancer.OperationStatus = "COMPLETE" + loadBalancer.PrimaryAvailabilityZone = "zone1_groupa" + loadBalancer.SecondaryAvailabilityZone = "zone1_groupb" + loadBalancer.ActiveAvailabilityZone = "zone1_groupa" + loadBalancer.Revision = 1 + loadBalancer.PlanID = "00713021-9aea-41da-9a88-87760c08fa72" + loadBalancer.PlanName = "50M_HA_4IF" + loadBalancer.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + loadBalancer.SyslogServers = []load_balancers.SyslogServerInResponse{syslogServer1} + loadBalancer.Interfaces = []load_balancers.InterfaceInResponse{interface1} + loadBalancer.Current = current + loadBalancer.Staged = staged + + return &loadBalancer +} + +var updateRequest = fmt.Sprintf(` +{ + "load_balancer": { + "name": "load_balancer", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "load_balancer": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "load_balancer", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "monitoring_status": "INITIAL", + "operation_status": "NONE", + "primary_availability_zone": null, + "secondary_availability_zone": null, + "active_availability_zone": "UNDEFINED", + "revision": 1, + "plan_id": "00713021-9aea-41da-9a88-87760c08fa72", + "plan_name": "50M_HA_4IF", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "syslog_servers": null, + "interfaces": null + } +}`) + +func updateResult() *load_balancers.LoadBalancer { + var loadBalancer load_balancers.LoadBalancer + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + loadBalancer.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + loadBalancer.Name = "load_balancer" + loadBalancer.Description = "description" + loadBalancer.Tags = tags + loadBalancer.ConfigurationStatus = "CREATE_STAGED" + loadBalancer.MonitoringStatus = "INITIAL" + loadBalancer.OperationStatus = "NONE" + loadBalancer.PrimaryAvailabilityZone = "" + loadBalancer.SecondaryAvailabilityZone = "" + loadBalancer.ActiveAvailabilityZone = "UNDEFINED" + loadBalancer.Revision = 1 + loadBalancer.PlanID = "00713021-9aea-41da-9a88-87760c08fa72" + loadBalancer.PlanName = "50M_HA_4IF" + loadBalancer.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + loadBalancer.SyslogServers = nil + loadBalancer.Interfaces = nil + + return &loadBalancer +} + +var applyConfigurationsRequest = fmt.Sprintf(` +{ + "apply-configurations": null +}`) + +var systemUpdateRequest = fmt.Sprintf(` +{ + "system-update": { + "system_update_id": "31746df7-92f9-4b5e-ad05-59f6684a54eb" + } +}`) + +var applyConfigurationsAndSystemUpdateRequest = fmt.Sprintf(` +{ + "apply-configurations": null, + "system-update": { + "system_update_id": "31746df7-92f9-4b5e-ad05-59f6684a54eb" + } +}`) + +var cancelConfigurationsRequest = fmt.Sprintf(` +{ + "cancel-configurations": null +}`) + +var createStagedRequest = fmt.Sprintf(` +{ + "load_balancer": { + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "load_balancer": { + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } +}`) + +func createStagedResult() *load_balancers.LoadBalancer { + var loadBalancer load_balancers.LoadBalancer + + reservedFixedIP1 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.InterfaceInResponse{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: []load_balancers.ReservedFixedIPInResponse{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.SyslogServerInResponse{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + + loadBalancer.SyslogServers = []load_balancers.SyslogServerInResponse{syslogServer1} + loadBalancer.Interfaces = []load_balancers.InterfaceInResponse{interface1} + + return &loadBalancer +} + +var showStagedResponse = fmt.Sprintf(` +{ + "load_balancer": { + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } +}`) + +func showStagedResult() *load_balancers.LoadBalancer { + var loadBalancer load_balancers.LoadBalancer + + reservedFixedIP1 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.InterfaceInResponse{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: []load_balancers.ReservedFixedIPInResponse{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.SyslogServerInResponse{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + + loadBalancer.SyslogServers = []load_balancers.SyslogServerInResponse{syslogServer1} + loadBalancer.Interfaces = []load_balancers.InterfaceInResponse{interface1} + + return &loadBalancer +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "load_balancer": { + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "load_balancer": { + "syslog_servers": [ + { + "ip_address": "192.168.0.6", + "port": 514, + "protocol": "udp" + } + ], + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } +}`) + +func updateStagedResult() *load_balancers.LoadBalancer { + var loadBalancer load_balancers.LoadBalancer + + reservedFixedIP1 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.ReservedFixedIPInResponse{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.InterfaceInResponse{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: []load_balancers.ReservedFixedIPInResponse{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.SyslogServerInResponse{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + + loadBalancer.SyslogServers = []load_balancers.SyslogServerInResponse{syslogServer1} + loadBalancer.Interfaces = []load_balancers.InterfaceInResponse{interface1} + + return &loadBalancer +} diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/load_balancers/testing/requests_test.go new file mode 100644 index 0000000..8382d4e --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/testing/requests_test.go @@ -0,0 +1,488 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/load_balancers" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListLoadBalancers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/load_balancers", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := load_balancers.ListOpts{} + + err := load_balancers.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancers.ExtractLoadBalancers(page) + if err != nil { + t.Errorf("Failed to extract load balancers: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/load_balancers", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + reservedFixedIP1 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.CreateOptsReservedFixedIP{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.CreateOptsInterface{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: &[]load_balancers.CreateOptsReservedFixedIP{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.CreateOptsSyslogServer{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := load_balancers.CreateOpts{ + Name: "load_balancer", + Description: "description", + Tags: tags, + PlanID: "00713021-9aea-41da-9a88-87760c08fa72", + SyslogServers: &[]load_balancers.CreateOptsSyslogServer{syslogServer1}, + Interfaces: &[]load_balancers.CreateOptsInterface{interface1}, + } + + actual, err := load_balancers.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := load_balancers.ShowOpts{} + + actual, err := load_balancers.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "load_balancer" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := load_balancers.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := load_balancers.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := load_balancers.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestApplyConfigurationsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/action", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, applyConfigurationsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + actionOpts := load_balancers.ActionOpts{ + ApplyConfigurations: true, + } + err := load_balancers.Action(cli, id, actionOpts).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestSystemUpdateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/action", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, systemUpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + systemUpdate := load_balancers.ActionOptsSystemUpdate{ + SystemUpdateID: "31746df7-92f9-4b5e-ad05-59f6684a54eb", + } + actionOpts := load_balancers.ActionOpts{ + SystemUpdate: &systemUpdate, + } + + err := load_balancers.Action(cli, id, actionOpts).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestApplyConfigurationsAndSystemUpdateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/action", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, applyConfigurationsAndSystemUpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + systemUpdate := load_balancers.ActionOptsSystemUpdate{ + SystemUpdateID: "31746df7-92f9-4b5e-ad05-59f6684a54eb", + } + actionOpts := load_balancers.ActionOpts{ + ApplyConfigurations: true, + SystemUpdate: &systemUpdate, + } + + err := load_balancers.Action(cli, id, actionOpts).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCancelConfigurationsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/action", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, cancelConfigurationsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + err := load_balancers.CancelConfigurations(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + reservedFixedIP1 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.2", + } + reservedFixedIP2 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.3", + } + reservedFixedIP3 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.4", + } + reservedFixedIP4 := load_balancers.CreateStagedOptsReservedFixedIP{ + IPAddress: "192.168.0.5", + } + interface1 := load_balancers.CreateStagedOptsInterface{ + NetworkID: "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + VirtualIPAddress: "192.168.0.1", + ReservedFixedIPs: &[]load_balancers.CreateStagedOptsReservedFixedIP{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + syslogServer1 := load_balancers.CreateStagedOptsSyslogServer{ + IPAddress: "192.168.0.6", + Port: 514, + Protocol: "udp", + } + createStagedOpts := load_balancers.CreateStagedOpts{ + SyslogServers: &[]load_balancers.CreateStagedOptsSyslogServer{syslogServer1}, + Interfaces: &[]load_balancers.CreateStagedOptsInterface{interface1}, + } + + actual, err := load_balancers.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := load_balancers.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + reservedFixedIP1IPAddress := "192.168.0.2" + reservedFixedIP1 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP1IPAddress, + } + + reservedFixedIP2IPAddress := "192.168.0.3" + reservedFixedIP2 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP2IPAddress, + } + + reservedFixedIP3IPAddress := "192.168.0.4" + reservedFixedIP3 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP3IPAddress, + } + + reservedFixedIP4IPAddress := "192.168.0.5" + reservedFixedIP4 := load_balancers.UpdateStagedOptsReservedFixedIP{ + IPAddress: &reservedFixedIP4IPAddress, + } + + interface1NetworkID := "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3" + interface1VirtualIPAddress := "192.168.0.1" + interface1 := load_balancers.UpdateStagedOptsInterface{ + NetworkID: &interface1NetworkID, + VirtualIPAddress: &interface1VirtualIPAddress, + ReservedFixedIPs: &[]load_balancers.UpdateStagedOptsReservedFixedIP{reservedFixedIP1, reservedFixedIP2, reservedFixedIP3, reservedFixedIP4}, + } + + syslogServer1IPAddress := "192.168.0.6" + syslogServer1Port := 514 + syslogServer1Protocol := "udp" + syslogServer1 := load_balancers.UpdateStagedOptsSyslogServer{ + IPAddress: &syslogServer1IPAddress, + Port: &syslogServer1Port, + Protocol: &syslogServer1Protocol, + } + + updateStagedOpts := load_balancers.UpdateStagedOpts{ + SyslogServers: &[]load_balancers.UpdateStagedOptsSyslogServer{syslogServer1}, + Interfaces: &[]load_balancers.UpdateStagedOptsInterface{interface1}, + } + + actual, err := load_balancers.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/load_balancers/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := load_balancers.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/load_balancers/urls.go b/v4/ecl/managed_load_balancer/v1/load_balancers/urls.go new file mode 100644 index 0000000..2b15079 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/load_balancers/urls.go @@ -0,0 +1,57 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancers") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id) +} + +func actionURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id, "action") +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/operations/doc.go b/v4/ecl/managed_load_balancer/v1/operations/doc.go new file mode 100644 index 0000000..6eb28a6 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/doc.go @@ -0,0 +1,32 @@ +/* +Package operations contains functionality for working with ECL Managed Load Balancer resources. + +Example to list operations + + listOpts := operations.ListOpts{} + + allPages, err := operations.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allOperations, err := operations.ExtractOperations(allPages) + if err != nil { + panic(err) + } + + for _, operation := range allOperations { + fmt.Printf("%+v\n", operation) + } + +Example to show a operation + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + operation, err := operations.Show(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", operation) +*/ +package operations diff --git a/v4/ecl/managed_load_balancer/v1/operations/requests.go b/v4/ecl/managed_load_balancer/v1/operations/requests.go new file mode 100644 index 0000000..c3607e8 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/requests.go @@ -0,0 +1,87 @@ +package operations + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Operations +*/ + +// 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 operation attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - ID of the resource + ResourceID string `q:"resource_id"` + + // - Type of the resource + ResourceType string `q:"resource_type"` + + // - The unique hyphenated UUID to identify the request + // - The UUID which has been set by `X-MVNA-Request-Id` in request headers + RequestID string `q:"request_id"` + + // - Type of the request + RequestType string `q:"request_type"` + + // - Operation status of the resource + Status string `q:"status"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` + + // - If `true` is set, operations of deleted resource is not displayed + NoDeleted bool `q:"no_deleted"` + + // - If `true` is set, only the latest operation of each resource is displayed + Latest bool `q:"latest"` +} + +// ToOperationListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToOperationListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToOperationListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of operations. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToOperationListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return OperationPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Show Operation +*/ + +// Show retrieves a specific operation based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string) (r ShowResult) { + _, r.Err = c.Get(showURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/operations/results.go b/v4/ecl/managed_load_balancer/v1/operations/results.go new file mode 100644 index 0000000..6b6786b --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/results.go @@ -0,0 +1,100 @@ +package operations + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Operation. +type ShowResult struct { + commonResult +} + +// Operation represents a operation. +type Operation struct { + + // - ID of the operation + ID string `json:"id"` + + // - ID of the resource + ResourceID string `json:"resource_id"` + + // - Type of the resource + ResourceType string `json:"resource_type"` + + // - The unique hyphenated UUID to identify the request + // - The UUID which has been set by X-MVNA-Request-Id in request headers + RequestID string `json:"request_id"` + + // - Types of the request + RequestTypes []string `json:"request_types"` + + // - Body of the request + RequestBody map[string]interface{} `json:"request_body,omitempty"` + + // - Operation status of the resource + Status string `json:"status"` + + // - The time when operation has been started by API execution + // - Format: `"%Y-%m-%d %H:%M:%S"` (UTC) + ReceptionDatetime string `json:"reception_datetime"` + + // - The time when operation has been finished + // - Format: `"%Y-%m-%d %H:%M:%S"` (UTC) + CommitDatetime string `json:"commit_datetime"` + + // - The warning message of operation that has been stopped or failed + Warning string `json:"warning"` + + // - The error message of operation that has been stopped or failed + Error string `json:"error"` + + // - ID of the owner tenant of the resource + TenantID string `json:"tenant_id"` +} + +// ExtractInto interprets any commonResult as a operation, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "operation") +} + +// Extract is a function that accepts a result and extracts a Operation resource. +func (r commonResult) Extract() (*Operation, error) { + var operation Operation + + err := r.ExtractInto(&operation) + + return &operation, err +} + +// OperationPage is the page returned by a pager when traversing over a collection of operation. +type OperationPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a OperationPage struct is empty. +func (r OperationPage) IsEmpty() (bool, error) { + is, err := ExtractOperations(r) + + return len(is) == 0, err +} + +// ExtractOperationsInto interprets the results of a single page from a List() call, producing a slice of operation entities. +func ExtractOperationsInto(r pagination.Page, v interface{}) error { + return r.(OperationPage).Result.ExtractIntoSlicePtr(v, "operations") +} + +// ExtractOperations accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Operation structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractOperations(r pagination.Page) ([]Operation, error) { + var s []Operation + + err := ExtractOperationsInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/operations/testing/doc.go b/v4/ecl/managed_load_balancer/v1/operations/testing/doc.go new file mode 100644 index 0000000..028cc81 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains operation unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/operations/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/operations/testing/fixtures.go new file mode 100644 index 0000000..c04cbc9 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/testing/fixtures.go @@ -0,0 +1,205 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/operations" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "operations": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "resource_type": "ECL::ManagedLoadBalancer::LoadBalancer", + "request_id": "", + "request_types": [ + "Action::apply-configurations" + ], + "status": "COMPLETE", + "reception_datetime": "2019-08-24 14:15:22", + "commit_datetime": "2019-08-24 14:30:44", + "warning": "", + "error": "", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0" + } + ] +}`) + +func listResult() []operations.Operation { + var operation1 operations.Operation + + operation1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + operation1.ResourceID = "4d5215ed-38bb-48ed-879a-fdb9ca58522f" + operation1.ResourceType = "ECL::ManagedLoadBalancer::LoadBalancer" + operation1.RequestID = "" + operation1.RequestTypes = []string{"Action::apply-configurations"} + operation1.Status = "COMPLETE" + operation1.ReceptionDatetime = "2019-08-24 14:15:22" + operation1.CommitDatetime = "2019-08-24 14:30:44" + operation1.Warning = "" + operation1.Error = "" + operation1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + + return []operations.Operation{operation1} +} + +var showResponse = fmt.Sprintf(` +{ + "operation": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "resource_type": "ECL::ManagedLoadBalancer::LoadBalancer", + "request_id": "", + "request_types": [ + "Action::apply-configurations" + ], + "request_body": { + "apply-configurations": { + "load_balancer": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "configuration_status": "CREATE_STAGED", + "current": null, + "staged": { + "interfaces": [ + { + "network_id": "d6797cf4-42b9-4cad-8591-9dd91c3f0fc3", + "virtual_ip_address": "192.168.0.1", + "reserved_fixed_ips": [ + { + "ip_address": "192.168.0.2" + }, + { + "ip_address": "192.168.0.3" + }, + { + "ip_address": "192.168.0.4" + }, + { + "ip_address": "192.168.0.5" + } + ] + } + ] + } + }, + "health_monitors": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "configuration_status": "CREATE_STAGED", + "current": null, + "staged": { + "port": 0, + "protocol": "icmp", + "interval": 5, + "retry": 3, + "timeout": 5 + } + } + ], + "listeners": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "configuration_status": "CREATE_STAGED", + "current": null, + "staged": { + "ip_address": "10.0.0.1", + "port": 80, + "protocol": "tcp" + } + } + ], + "policies": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "configuration_status": "CREATE_STAGED", + "current": null, + "staged": { + "algorithm": "round-robin", + "persistence": "none", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + } + } + ], + "routes": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "configuration_status": "CREATE_STAGED", + "current": null, + "staged": { + "next_hop_ip_address": "192.168.0.254" + } + } + ], + "target_groups": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "configuration_status": "CREATE_STAGED", + "current": null, + "staged": { + "members": [ + { + "ip_address": "192.168.0.6", + "port": 80, + "weight": 1 + } + ] + } + } + ] + } + }, + "status": "COMPLETE", + "reception_datetime": "2019-08-24 14:15:22", + "commit_datetime": "2019-08-24 14:30:44", + "warning": "", + "error": "", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0" + } +}`) + +func showResult() *operations.Operation { + var operation operations.Operation + + var requestBody map[string]interface{} + requestBodyJson := `{"apply-configurations":{"load_balancer":{"id":"497f6eca-6276-4993-bfeb-53cbbbba6f08","configuration` + + `_status":"CREATE_STAGED","current":null,"staged":{"interfaces":[{"network_id":"d6797cf4-42b9-4cad-85` + + `91-9dd91c3f0fc3","virtual_ip_address":"192.168.0.1","reserved_fixed_ips":[{"ip_address":"192.168.0.2` + + `"},{"ip_address":"192.168.0.3"},{"ip_address":"192.168.0.4"},{"ip_address":"192.168.0.5"}]}]}},"heal` + + `th_monitors":[{"id":"497f6eca-6276-4993-bfeb-53cbbbba6f08","configuration_status":"CREATE_STAGED","c` + + `urrent":null,"staged":{"port":0,"protocol":"icmp","interval":5,"retry":3,"timeout":5}}],"listeners":` + + `[{"id":"497f6eca-6276-4993-bfeb-53cbbbba6f08","configuration_status":"CREATE_STAGED","current":null,` + + `"staged":{"ip_address":"10.0.0.1","port":80,"protocol":"tcp"}}],"policies":[{"id":"497f6eca-6276-499` + + `3-bfeb-53cbbbba6f08","configuration_status":"CREATE_STAGED","current":null,"staged":{"algorithm":"ro` + + `und-robin","persistence":"none","health_monitor_id":"dd7a96d6-4e66-4666-baca-a8555f0c472c","listener` + + `_id":"68633f4f-f52a-402f-8572-b8173418904f","default_target_group_id":"a44c4072-ed90-4b50-a33a-6b38f` + + `b10c7db"}}],"routes":[{"id":"497f6eca-6276-4993-bfeb-53cbbbba6f08","configuration_status":"CREATE_ST` + + `AGED","current":null,"staged":{"next_hop_ip_address":"192.168.0.254"}}],"target_groups":[{"id":"497f` + + `6eca-6276-4993-bfeb-53cbbbba6f08","configuration_status":"CREATE_STAGED","current":null,"staged":{"m` + + `embers":[{"ip_address":"192.168.0.6","port":80,"weight":1}]}}]}}` + err := json.Unmarshal([]byte(requestBodyJson), &requestBody) + if err != nil { + panic(err) + } + + operation.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + operation.ResourceID = "4d5215ed-38bb-48ed-879a-fdb9ca58522f" + operation.ResourceType = "ECL::ManagedLoadBalancer::LoadBalancer" + operation.RequestID = "" + operation.RequestTypes = []string{"Action::apply-configurations"} + operation.RequestBody = requestBody + operation.Status = "COMPLETE" + operation.ReceptionDatetime = "2019-08-24 14:15:22" + operation.CommitDatetime = "2019-08-24 14:30:44" + operation.Warning = "" + operation.Error = "" + operation.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + + return &operation +} diff --git a/v4/ecl/managed_load_balancer/v1/operations/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/operations/testing/requests_test.go new file mode 100644 index 0000000..e44630b --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/testing/requests_test.go @@ -0,0 +1,85 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/operations" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListOperations(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/operations", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := operations.ListOpts{} + + err := operations.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := operations.ExtractOperations(page) + if err != nil { + t.Errorf("Failed to extract operations: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestShowOperation(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/operations/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + + actual, err := operations.Show(cli, id).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/operations/urls.go b/v4/ecl/managed_load_balancer/v1/operations/urls.go new file mode 100644 index 0000000..473fd2c --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/operations/urls.go @@ -0,0 +1,21 @@ +package operations + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("operations") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("operations", id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/plans/doc.go b/v4/ecl/managed_load_balancer/v1/plans/doc.go new file mode 100644 index 0000000..c5710fb --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/doc.go @@ -0,0 +1,32 @@ +/* +Package plans contains functionality for working with ECL Managed Load Balancer resources. + +Example to list plans + + listOpts := plans.ListOpts{} + + allPages, err := plans.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPlans, err := plans.ExtractPlans(allPages) + if err != nil { + panic(err) + } + + for _, plan := range allPlans { + fmt.Printf("%+v\n", plan) + } + +Example to show a plan + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + plan, err := plans.Show(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", plan) +*/ +package plans diff --git a/v4/ecl/managed_load_balancer/v1/plans/requests.go b/v4/ecl/managed_load_balancer/v1/plans/requests.go new file mode 100644 index 0000000..91912d7 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/requests.go @@ -0,0 +1,106 @@ +package plans + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Plans +*/ + +// 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 plan attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Bandwidth of the plan + Bandwidth string `q:"bandwidth"` + + // - Redundancy of the plan + Redundancy string `q:"redundancy"` + + // - Maximum number of interfaces for the plan + MaxNumberOfInterfaces int `q:"max_number_of_interfaces"` + + // - Maximum number of health monitors for the plan + MaxNumberOfHealthMonitors int `q:"max_number_of_health_monitors"` + + // - Maximum number of listeners for the plan + MaxNumberOfListeners int `q:"max_number_of_listeners"` + + // - Maximum number of policies for the plan + MaxNumberOfPolicies int `q:"max_number_of_policies"` + + // - Maximum number of routes for the plan + MaxNumberOfRoutes int `q:"max_number_of_routes"` + + // - Maximum number of target groups for the plan + MaxNumberOfTargetGroups int `q:"max_number_of_target_groups"` + + // - Maximum number of members for the target group of plan + MaxNumberOfMembers int `q:"max_number_of_members"` + + // - Maximum number of rules for the policy of plan + MaxNumberOfRules int `q:"max_number_of_rules"` + + // - Maximum number of conditions in the rule of the plan + MaxNumberOfConditions int `q:"max_number_of_conditions"` + + // - Whether a new load balancer can be created with this plan + Enabled bool `q:"enabled"` +} + +// ToPlanListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPlanListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToPlanListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of plans. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToPlanListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PlanPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Show Plan +*/ + +// Show retrieves a specific plan based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string) (r ShowResult) { + _, r.Err = c.Get(showURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/plans/results.go b/v4/ecl/managed_load_balancer/v1/plans/results.go new file mode 100644 index 0000000..6bf13c3 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/results.go @@ -0,0 +1,106 @@ +package plans + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Plan. +type ShowResult struct { + commonResult +} + +// Plan represents a plan. +type Plan struct { + + // - ID of the plan + ID string `json:"id"` + + // - Name of the plan + Name string `json:"name"` + + // - Description of the plan + Description string `json:"description"` + + // - Bandwidth of the load balancer created with this plan + Bandwidth string `json:"bandwidth"` + + // - Redundancy of the load balancer created with this plan + Redundancy string `json:"redundancy"` + + // - Maximum number of interfaces for the load balancer created with this plan + MaxNumberOfInterfaces int `json:"max_number_of_interfaces"` + + // - Maximum number of health monitors for the load balancer created with this plan + MaxNumberOfHealthMonitors int `json:"max_number_of_health_monitors"` + + // - Maximum number of listeners for the load balancer created with this plan + MaxNumberOfListeners int `json:"max_number_of_listeners"` + + // - Maximum number of routes for the load balancer created with this plan + MaxNumberOfPolicies int `json:"max_number_of_policies"` + + // - Maximum number of routes for the load balancer created with this plan + MaxNumberOfRoutes int `json:"max_number_of_routes"` + + // - Maximum number of target groups for the load balancer created with this plan + MaxNumberOfTargetGroups int `json:"max_number_of_target_groups"` + + // - Maximum number of members for the target group of load balancer created with this plan + MaxNumberOfMembers int `json:"max_number_of_members"` + + // - Maximum number of rules for the policy of load balancer created with this plan + MaxNumberOfRules int `json:"max_number_of_rules"` + + // - Maximum number of conditions in the rule of load balancer created with this plan + MaxNumberOfConditions int `json:"max_number_of_conditions"` + + // - Whether a new load balancer can be created with this plan + Enabled bool `json:"enabled"` +} + +// ExtractInto interprets any commonResult as a plan, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "plan") +} + +// Extract is a function that accepts a result and extracts a Plan resource. +func (r commonResult) Extract() (*Plan, error) { + var plan Plan + + err := r.ExtractInto(&plan) + + return &plan, err +} + +// PlanPage is the page returned by a pager when traversing over a collection of plan. +type PlanPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a PlanPage struct is empty. +func (r PlanPage) IsEmpty() (bool, error) { + is, err := ExtractPlans(r) + + return len(is) == 0, err +} + +// ExtractPlansInto interprets the results of a single page from a List() call, producing a slice of plan entities. +func ExtractPlansInto(r pagination.Page, v interface{}) error { + return r.(PlanPage).Result.ExtractIntoSlicePtr(v, "plans") +} + +// ExtractPlans accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Plan structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractPlans(r pagination.Page) ([]Plan, error) { + var s []Plan + + err := ExtractPlansInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/plans/testing/doc.go b/v4/ecl/managed_load_balancer/v1/plans/testing/doc.go new file mode 100644 index 0000000..ec2e1f0 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains plan unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/plans/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/plans/testing/fixtures.go new file mode 100644 index 0000000..1d1cf68 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/testing/fixtures.go @@ -0,0 +1,97 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/plans" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "plans": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "50M_HA_4IF", + "description": "description", + "bandwidth": "50M", + "redundancy": "HA", + "max_number_of_interfaces": 4, + "max_number_of_health_monitors": 50, + "max_number_of_listeners": 50, + "max_number_of_policies": 50, + "max_number_of_routes": 25, + "max_number_of_target_groups": 50, + "max_number_of_members": 100, + "max_number_of_rules": 50, + "max_number_of_conditions": 5, + "enabled": true + } + ] +}`) + +func listResult() []plans.Plan { + var plan1 plans.Plan + + plan1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + plan1.Name = "50M_HA_4IF" + plan1.Description = "description" + plan1.Bandwidth = "50M" + plan1.Redundancy = "HA" + plan1.MaxNumberOfInterfaces = 4 + plan1.MaxNumberOfHealthMonitors = 50 + plan1.MaxNumberOfListeners = 50 + plan1.MaxNumberOfPolicies = 50 + plan1.MaxNumberOfRoutes = 25 + plan1.MaxNumberOfTargetGroups = 50 + plan1.MaxNumberOfMembers = 100 + plan1.MaxNumberOfRules = 50 + plan1.MaxNumberOfConditions = 5 + plan1.Enabled = true + + return []plans.Plan{plan1} +} + +var showResponse = fmt.Sprintf(` +{ + "plan": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "50M_HA_4IF", + "description": "description", + "bandwidth": "50M", + "redundancy": "HA", + "max_number_of_interfaces": 4, + "max_number_of_health_monitors": 50, + "max_number_of_listeners": 50, + "max_number_of_policies": 50, + "max_number_of_routes": 25, + "max_number_of_target_groups": 50, + "max_number_of_members": 100, + "max_number_of_rules": 50, + "max_number_of_conditions": 5, + "enabled": true + } +}`) + +func showResult() *plans.Plan { + var plan plans.Plan + + plan.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + plan.Name = "50M_HA_4IF" + plan.Description = "description" + plan.Bandwidth = "50M" + plan.Redundancy = "HA" + plan.MaxNumberOfInterfaces = 4 + plan.MaxNumberOfHealthMonitors = 50 + plan.MaxNumberOfListeners = 50 + plan.MaxNumberOfPolicies = 50 + plan.MaxNumberOfRoutes = 25 + plan.MaxNumberOfTargetGroups = 50 + plan.MaxNumberOfMembers = 100 + plan.MaxNumberOfRules = 50 + plan.MaxNumberOfConditions = 5 + plan.Enabled = true + + return &plan +} diff --git a/v4/ecl/managed_load_balancer/v1/plans/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/plans/testing/requests_test.go new file mode 100644 index 0000000..211ab69 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/testing/requests_test.go @@ -0,0 +1,85 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/plans" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListPlans(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/plans", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := plans.ListOpts{} + + err := plans.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := plans.ExtractPlans(page) + if err != nil { + t.Errorf("Failed to extract plans: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestShowPlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/plans/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + + actual, err := plans.Show(cli, id).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/plans/urls.go b/v4/ecl/managed_load_balancer/v1/plans/urls.go new file mode 100644 index 0000000..276f934 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/plans/urls.go @@ -0,0 +1,21 @@ +package plans + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("plans") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("plans", id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/policies/doc.go b/v4/ecl/managed_load_balancer/v1/policies/doc.go new file mode 100644 index 0000000..a804c5d --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/doc.go @@ -0,0 +1,176 @@ +/* +Package policies contains functionality for working with ECL Managed Load Balancer resources. + +Example to list policies + + listOpts := policies.ListOpts{} + + allPages, err := policies.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPolicies, err := policies.ExtractPolicies(allPages) + if err != nil { + panic(err) + } + + for _, policy := range allPolicies { + fmt.Printf("%+v\n", policy) + } + +Example to create a policy + + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := policies.CreateOpts{ + Name: "policy", + Description: "description", + Tags: tags, + Algorithm: "round-robin", + Persistence: "cookie", + IdleTimeout: 600, + SorryPageUrl: "https://example.com/sorry", + SourceNat: "enable", + CertificateID: "f57a98fe-d63e-4048-93a0-51fe163f30d7", + HealthMonitorID: "dd7a96d6-4e66-4666-baca-a8555f0c472c", + ListenerID: "68633f4f-f52a-402f-8572-b8173418904f", + DefaultTargetGroupID: "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + TLSPolicyID: "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + policy, err := policies.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", policy) + +Example to show a policy + + showOpts := policies.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policy, err := policies.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", policy) + +Example to update a policy + + name := "policy" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := policies.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policy, err := policies.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", policy) + +Example to delete a policy + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := policies.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged policy configurations + + createStagedOpts := policies.CreateStagedOpts{ + Algorithm: "round-robin", + Persistence: "cookie", + IdleTimeout: 600, + SorryPageUrl: "https://example.com/sorry", + SourceNat: "enable", + CertificateID: "f57a98fe-d63e-4048-93a0-51fe163f30d7", + HealthMonitorID: "dd7a96d6-4e66-4666-baca-a8555f0c472c", + ListenerID: "68633f4f-f52a-402f-8572-b8173418904f", + DefaultTargetGroupID: "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + TLSPolicyID: "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policyConfigurations, err := policies.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", policyConfigurations) + +Example to show staged policy configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policyConfigurations, err := policies.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", policyConfigurations) + +Example to update staged policy configurations + + algorithm := "round-robin" + persistence := "cookie" + idleTimeout := 600 + sorryPageUrl := "https://example.com/sorry" + sourceNat := "enable" + certificateID := "f57a98fe-d63e-4048-93a0-51fe163f30d7" + healthMonitorID := "dd7a96d6-4e66-4666-baca-a8555f0c472c" + listenerID := "68633f4f-f52a-402f-8572-b8173418904f" + defaultTargetGroupID := "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + tlsPolicyID := "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + updateStagedOpts := policies.UpdateStagedOpts{ + Algorithm: &algorithm, + Persistence: &persistence, + IdleTimeout: &idleTimeout, + SorryPageUrl: &sorryPageUrl, + SourceNat: &sourceNat, + CertificateID: &certificateID, + HealthMonitorID: &healthMonitorID, + ListenerID: &listenerID, + DefaultTargetGroupID: &defaultTargetGroupID, + TLSPolicyID: &tlsPolicyID, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policyConfigurations, err := policies.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", policyConfigurations) + +Example to cancel staged policy configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := policies.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package policies diff --git a/v4/ecl/managed_load_balancer/v1/policies/requests.go b/v4/ecl/managed_load_balancer/v1/policies/requests.go new file mode 100644 index 0000000..64e88e9 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/requests.go @@ -0,0 +1,506 @@ +package policies + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Policies +*/ + +// 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 policy attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - Load balancing algorithm (method) of the policy + Algorithm string `q:"algorithm"` + + // - Persistence setting of the policy + Persistence string `q:"persistence"` + + // - The duration (in seconds) during which a session is allowed to remain inactive + IdleTimeout int `q:"idle_timeout"` + + // - URL of the sorry page to which accesses are redirected if all members in the target group are down + SorryPageUrl string `q:"sorry_page_url"` + + // - Source NAT setting of the policy + SourceNat string `q:"source_nat"` + + // - ID of the certificate that assigned to the policy + CertificateID string `q:"certificate_id"` + + // - ID of the health monitor that assigned to the policy + HealthMonitorID string `q:"health_monitor_id"` + + // - ID of the listener that assigned to the policy + ListenerID string `q:"listener_id"` + + // - ID of the default target group that assigned to the policy + DefaultTargetGroupID string `q:"default_target_group_id"` + + // - ID of the TLS policy that assigned to the policy + TLSPolicyID string `q:"tls_policy_id"` + + // - ID of the load balancer which the resource belongs to + LoadBalancerID string `q:"load_balancer_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToPolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPolicyListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToPolicyListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of policies. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(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}} + }) +} + +/* +Create Policy +*/ + +// CreateOpts represents options used to create a new policy. +type CreateOpts struct { + + // - Name of the policy + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the policy + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the policy + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - Load balancing algorithm (method) of the policy + Algorithm string `json:"algorithm,omitempty"` + + // - Persistence setting of the policy + // - If `listener.protocol` is `"http"` or `"https"`, `"cookie"` is available + Persistence string `json:"persistence,omitempty"` + + // - The duration (in seconds) during which a session is allowed to remain inactive + // - There may be a time difference up to 60 seconds, between the set value and the actual timeout + // - If `listener.protocol` is `"tcp"` or `"udp"` + // - Default value is 120 + // - If `listener.protocol` is `"http"` or `"https"` + // - Default value is 600 + // - On session timeout, the load balancer sends TCP RST packets to both the client and the real server + IdleTimeout int `json:"idle_timeout,omitempty"` + + // - URL of the sorry page to which accesses are redirected if all members in the target group are down + // - If `listener.protocol` is `"http"` or `"https"`, this parameter can be set + // - If `listener.protocol` is neither `"http"` nor `"https"`, must not set this parameter or set `""` + SorryPageUrl string `json:"sorry_page_url,omitempty"` + + // - Source NAT setting of the policy + // - If `source_nat` is `"enable"` and `listener.protocol` is `"http"` or `"https"` + // - The source IP address of the request is replaced with `virtual_ip_address` which is assigned to the interface from which the request was sent + // - `X-Forwarded-For` header with the IP address of the client is added + SourceNat string `json:"source_nat,omitempty"` + + // - ID of the certificate that assigned to the policy + // - You can set a ID of the certificate in which `ca_cert.status`, `ssl_cert.status` and `ssl_key.status` are all `"UPLOADED"` + // - If `listener.protocol` is `"https"`, set `certificate.id` + // - If `listener.protocol` is not `"https"`, must not set this parameter or set `""` + CertificateID string `json:"certificate_id,omitempty"` + + // - ID of the health monitor that assigned to the policy + // - Must not set ID of the health monitor that `configuration_status` is `"DELETE_STAGED"` + HealthMonitorID string `json:"health_monitor_id"` + + // - ID of the listener that assigned to the policy + // - Must not set ID of the listener that `configuration_status` is `"DELETE_STAGED"` + // - Must not set ID of the listener that already assigned to the other policy + ListenerID string `json:"listener_id"` + + // - ID of the default target group that assigned to the policy + // - Must not set ID of the target group that `configuration_status` is `"DELETE_STAGED"` + DefaultTargetGroupID string `json:"default_target_group_id"` + + // - ID of the TLS policy that assigned to the policy + // - If `listener.protocol` is `"https"`, you can set this parameter explicitly + // - If not set this parameter, the ID of the `tls_policy` with `default: true` will be automatically set + // - If `listener.protocol` is not `"https"`, must not set this parameter or set `""` + TLSPolicyID string `json:"tls_policy_id,omitempty"` + + // - ID of the load balancer which the policy belongs to + LoadBalancerID string `json:"load_balancer_id"` +} + +// ToPolicyCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "policy") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToPolicyCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new policy using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPolicyCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Policy +*/ + +// ShowOpts represents options used to show a policy. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToPolicyShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToPolicyShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToPolicyShowQuery() (string, error) +} + +// Show retrieves a specific policy based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToPolicyShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Policy Attributes +*/ + +// UpdateOpts represents options used to update a existing policy. +type UpdateOpts struct { + + // - Name of the policy + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the policy + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the policy + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToPolicyUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "policy") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToPolicyUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing policy using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPolicyUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Policy +*/ + +// Delete accepts a unique ID and deletes the policy associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Policy Configurations +*/ + +// CreateStagedOpts represents options used to create new policy configurations. +type CreateStagedOpts struct { + + // - Load balancing algorithm (method) of the policy + Algorithm string `json:"algorithm,omitempty"` + + // - Persistence setting of the policy + // - If `listener.protocol` is `"http"` or `"https"`, `"cookie"` is available + Persistence string `json:"persistence,omitempty"` + + // - The duration (in seconds) during which a session is allowed to remain inactive + // - There may be a time difference up to 60 seconds, between the set value and the actual timeout + // - If `listener.protocol` is `"tcp"` or `"udp"` + // - Default value is 120 + // - If `listener.protocol` is `"http"` or `"https"` + // - Default value is 600 + // - On session timeout, the load balancer sends TCP RST packets to both the client and the real server + IdleTimeout int `json:"idle_timeout,omitempty"` + + // - URL of the sorry page to which accesses are redirected if all members in the target group are down + // - If `listener.protocol` is `"http"` or `"https"`, this parameter can be set + // - If `listener.protocol` is neither `"http"` nor `"https"`, must not set this parameter or set `""` + // - If you change `listener.protocol` from `"http"` or `"https"` to others, set `""` + SorryPageUrl string `json:"sorry_page_url,omitempty"` + + // - Source NAT setting of the policy + // - If `source_nat` is `"enable"` and `listener.protocol` is `"http"` or `"https"` + // - The source IP address of the request is replaced with `virtual_ip_address` which is assigned to the interface from which the request was sent + // - `X-Forwarded-For` header with the IP address of the client is added + SourceNat string `json:"source_nat,omitempty"` + + // - ID of the certificate that assigned to the policy + // - You can set a ID of the certificate in which `ca_cert.status`, `ssl_cert.status` and `ssl_key.status` are all `"UPLOADED"` + // - If `listener.protocol` is `"https"`, set `certificate.id` + // - If `listener.protocol` is not `"https"`, must not set this parameter or set `""` + // - If you change `listener.protocol` from `"https"` to others, set `""` + CertificateID string `json:"certificate_id,omitempty"` + + // - ID of the health monitor that assigned to the policy + // - Must not set ID of the health monitor that `configuration_status` is `"DELETE_STAGED"` + HealthMonitorID string `json:"health_monitor_id,omitempty"` + + // - ID of the listener that assigned to the policy + // - Must not set ID of the listener that `configuration_status` is `"DELETE_STAGED"` + // - Must not set ID of the listener that already assigned to the other policy + ListenerID string `json:"listener_id,omitempty"` + + // - ID of the default target group that assigned to the policy + // - Must not set ID of the target group that `configuration_status` is `"DELETE_STAGED"` + DefaultTargetGroupID string `json:"default_target_group_id,omitempty"` + + // - ID of the TLS policy that assigned to the policy + // - If `listener.protocol` is `"https"`, you can set this parameter explicitly + // - If not set this parameter, the ID of the `tls_policy` with `default: true` will be automatically set + // - If `listener.protocol` is not `"https"`, must not set this parameter or set `""` + // - If you change `listener.protocol` from `"https"` to others, set `""` + TLSPolicyID string `json:"tls_policy_id,omitempty"` +} + +// ToPolicyCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToPolicyCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "policy") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToPolicyCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new policy configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToPolicyCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Policy Configurations +*/ + +// ShowStaged retrieves specific policy configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Policy Configurations +*/ + +// UpdateStagedOpts represents options used to update existing Policy configurations. +type UpdateStagedOpts struct { + + // - Load balancing algorithm (method) of the policy + Algorithm *string `json:"algorithm,omitempty"` + + // - Persistence setting of the policy + // - If `listener.protocol` is `"http"` or `"https"`, `"cookie"` is available + Persistence *string `json:"persistence,omitempty"` + + // - The duration (in seconds) during which a session is allowed to remain inactive + // - There may be a time difference up to 60 seconds, between the set value and the actual timeout + // - If `listener.protocol` is `"tcp"` or `"udp"` + // - Default value is 120 + // - If `listener.protocol` is `"http"` or `"https"` + // - Default value is 600 + // - On session timeout, the load balancer sends TCP RST packets to both the client and the real server + IdleTimeout *int `json:"idle_timeout,omitempty"` + + // - URL of the sorry page to which accesses are redirected if all members in the target group are down + // - If `listener.protocol` is `"http"` or `"https"`, this parameter can be set + // - If `listener.protocol` is neither `"http"` nor `"https"`, must not set this parameter or set `""` + // - If you change `listener.protocol` from `"http"` or `"https"` to others, set `""` + SorryPageUrl *string `json:"sorry_page_url,omitempty"` + + // - Source NAT setting of the policy + // - If `source_nat` is `"enable"` and `listener.protocol` is `"http"` or `"https"` + // - The source IP address of the request is replaced with `virtual_ip_address` which is assigned to the interface from which the request was sent + // - `X-Forwarded-For` header with the IP address of the client is added + SourceNat *string `json:"source_nat,omitempty"` + + // - ID of the certificate that assigned to the policy + // - You can set a ID of the certificate in which `ca_cert.status`, `ssl_cert.status` and `ssl_key.status` are all `"UPLOADED"` + // - If `listener.protocol` is `"https"`, set `certificate.id` + // - If `listener.protocol` is not `"https"`, must not set this parameter or set `""` + // - If you change `listener.protocol` from `"https"` to others, set `""` + CertificateID *string `json:"certificate_id,omitempty"` + + // - ID of the health monitor that assigned to the policy + // - Must not set ID of the health monitor that `configuration_status` is `"DELETE_STAGED"` + HealthMonitorID *string `json:"health_monitor_id,omitempty"` + + // - ID of the listener that assigned to the policy + // - Must not set ID of the listener that `configuration_status` is `"DELETE_STAGED"` + // - Must not set ID of the listener that already assigned to the other policy + ListenerID *string `json:"listener_id,omitempty"` + + // - ID of the default target group that assigned to the policy + // - Must not set ID of the target group that `configuration_status` is `"DELETE_STAGED"` + DefaultTargetGroupID *string `json:"default_target_group_id,omitempty"` + + // - ID of the TLS policy that assigned to the policy + // - If `listener.protocol` is `"https"`, you can set this parameter explicitly + // - If not set this parameter, the ID of the `tls_policy` with `default: true` will be automatically set + // - If `listener.protocol` is not `"https"`, must not set this parameter or set `""` + // - If you change `listener.protocol` from `"https"` to others, set `""` + TLSPolicyID *string `json:"tls_policy_id,omitempty"` +} + +// ToPolicyUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToPolicyUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "policy") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToPolicyUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Policy configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToPolicyUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Policy Configurations +*/ + +// CancelStaged accepts a unique ID and deletes policy configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/policies/results.go b/v4/ecl/managed_load_balancer/v1/policies/results.go new file mode 100644 index 0000000..a88a015 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/results.go @@ -0,0 +1,252 @@ +package policies + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a Policy. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Policy. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a Policy. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a Policy. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a Policy. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a Policy. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ConfigurationInResponse represents a configuration in a policy. +type ConfigurationInResponse struct { + + // - Load balancing algorithm (method) of the policy + Algorithm string `json:"algorithm,omitempty"` + + // - Persistence setting of the policy + // - If `listener.protocol` is `"http"` or `"https"`, `"cookie"` is available + Persistence string `json:"persistence,omitempty"` + + // - The duration (in seconds) during which a session is allowed to remain inactive + // - There may be a time difference up to 60 seconds, between the set value and the actual timeout + // - If `listener.protocol` is `"tcp"` or `"udp"` + // - Default value is 120 + // - If `listener.protocol` is `"http"` or `"https"` + // - Default value is 600 + // - On session timeout, the load balancer sends TCP RST packets to both the client and the real server + IdleTimeout int `json:"idle_timeout,omitempty"` + + // - URL of the sorry page to which accesses are redirected if all members in the target group are down + // - If protocol is not `"http"` or `"https"`, returns `""` + SorryPageUrl string `json:"sorry_page_url,omitempty"` + + // - Source NAT setting of the policy + // - If `source_nat` is `"enable"` and `listener.protocol` is `"http"` or `"https"` , + // - The source IP address of the request is replaced with `virtual_ip_address` which is assigned to the interface from which the request was sent + // - `X-Forwarded-For` header with the IP address of the client is added + SourceNat string `json:"source_nat,omitempty"` + + // - ID of the certificate that assigned to the policy + // - If protocol is not `"https"`, returns `""` + CertificateID string `json:"certificate_id,omitempty"` + + // - ID of the health monitor that assigned to the policy + HealthMonitorID string `json:"health_monitor_id,omitempty"` + + // - ID of the listener that assigned to the policy + ListenerID string `json:"listener_id,omitempty"` + + // - ID of the default target group that assigned to the policy + DefaultTargetGroupID string `json:"default_target_group_id,omitempty"` + + // - ID of the TLS policy that assigned to the policy + // - If protocol is not `"https"`, returns `""` + TLSPolicyID string `json:"tls_policy_id,omitempty"` +} + +// Policy represents a policy. +type Policy struct { + + // - ID of the policy + ID string `json:"id"` + + // - Name of the policy + Name string `json:"name"` + + // - Description of the policy + Description string `json:"description"` + + // - Tags of the policy (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the policy + // - `"ACTIVE"` + // - There are no configurations of the policy that waiting to be applied + // - `"CREATE_STAGED"` + // - The policy has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the policy exists that waiting to be applied + // - `"DELETE_STAGED"` + // - The policy has been removed and waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Operation status of the load balancer which the policy belongs to + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - ID of the load balancer which the policy belongs to + LoadBalancerID string `json:"load_balancer_id"` + + // - ID of the owner tenant of the policy + TenantID string `json:"tenant_id"` + + // - Load balancing algorithm (method) of the policy + Algorithm string `json:"algorithm,omitempty"` + + // - Persistence setting of the policy + // - If `listener.protocol` is `"http"` or `"https"`, `"cookie"` is available + Persistence string `json:"persistence,omitempty"` + + // - The duration (in seconds) during which a session is allowed to remain inactive + // - There may be a time difference up to 60 seconds, between the set value and the actual timeout + // - If `listener.protocol` is `"tcp"` or `"udp"` + // - Default value is 120 + // - If `listener.protocol` is `"http"` or `"https"` + // - Default value is 600 + // - On session timeout, the load balancer sends TCP RST packets to both the client and the real server + IdleTimeout int `json:"idle_timeout,omitempty"` + + // - URL of the sorry page to which accesses are redirected if all members in the target group are down + // - If protocol is not `"http"` or `"https"`, returns `""` + SorryPageUrl string `json:"sorry_page_url,omitempty"` + + // - Source NAT setting of the policy + // - If `source_nat` is `"enable"` and `listener.protocol` is `"http"` or `"https"` , + // - The source IP address of the request is replaced with `virtual_ip_address` which is assigned to the interface from which the request was sent + // - `X-Forwarded-For` header with the IP address of the client is added + SourceNat string `json:"source_nat,omitempty"` + + // - ID of the certificate that assigned to the policy + // - If protocol is not `"https"`, returns `""` + CertificateID string `json:"certificate_id,omitempty"` + + // - ID of the health monitor that assigned to the policy + HealthMonitorID string `json:"health_monitor_id,omitempty"` + + // - ID of the listener that assigned to the policy + ListenerID string `json:"listener_id,omitempty"` + + // - ID of the default target group that assigned to the policy + DefaultTargetGroupID string `json:"default_target_group_id,omitempty"` + + // - ID of the TLS policy that assigned to the policy + // - If protocol is not `"https"`, returns `""` + TLSPolicyID string `json:"tls_policy_id,omitempty"` + + // - Running configurations of the policy + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the policy that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a policy, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "policy") +} + +// Extract is a function that accepts a result and extracts a Policy resource. +func (r commonResult) Extract() (*Policy, error) { + var policy Policy + + err := r.ExtractInto(&policy) + + return &policy, err +} + +// PolicyPage is the page returned by a pager when traversing over a collection of policy. +type PolicyPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a PolicyPage struct is empty. +func (r PolicyPage) IsEmpty() (bool, error) { + is, err := ExtractPolicies(r) + + return len(is) == 0, err +} + +// ExtractPoliciesInto interprets the results of a single page from a List() call, producing a slice of policy entities. +func ExtractPoliciesInto(r pagination.Page, v interface{}) error { + return r.(PolicyPage).Result.ExtractIntoSlicePtr(v, "policies") +} + +// ExtractPolicies accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Policy structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractPolicies(r pagination.Page) ([]Policy, error) { + var s []Policy + + err := ExtractPoliciesInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/policies/testing/doc.go b/v4/ecl/managed_load_balancer/v1/policies/testing/doc.go new file mode 100644 index 0000000..1869ef3 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains policy unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/policies/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/policies/testing/fixtures.go new file mode 100644 index 0000000..0e0792e --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/testing/fixtures.go @@ -0,0 +1,437 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/policies" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "policies": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "policy", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + } + ] +}`) + +func listResult() []policies.Policy { + var policy1 policies.Policy + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + policy1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policy1.Name = "policy" + policy1.Description = "description" + policy1.Tags = tags1 + policy1.ConfigurationStatus = "ACTIVE" + policy1.OperationStatus = "COMPLETE" + policy1.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + policy1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + policy1.Algorithm = "round-robin" + policy1.Persistence = "cookie" + policy1.IdleTimeout = 600 + policy1.SorryPageUrl = "https://example.com/sorry" + policy1.SourceNat = "enable" + policy1.CertificateID = "f57a98fe-d63e-4048-93a0-51fe163f30d7" + policy1.HealthMonitorID = "dd7a96d6-4e66-4666-baca-a8555f0c472c" + policy1.ListenerID = "68633f4f-f52a-402f-8572-b8173418904f" + policy1.DefaultTargetGroupID = "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + policy1.TLSPolicyID = "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + + return []policies.Policy{policy1} +} + +var createRequest = fmt.Sprintf(` +{ + "policy": { + "name": "policy", + "description": "description", + "tags": { + "key": "value" + }, + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040" + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "policy": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "policy", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "algorithm": null, + "persistence": null, + "idle_timeout": null, + "sorry_page_url": null, + "source_nat": null, + "certificate_id": null, + "health_monitor_id": null, + "listener_id": null, + "default_target_group_id": null, + "tls_policy_id": null + } +}`) + +func createResult() *policies.Policy { + var policy policies.Policy + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + policy.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policy.Name = "policy" + policy.Description = "description" + policy.Tags = tags + policy.ConfigurationStatus = "CREATE_STAGED" + policy.OperationStatus = "NONE" + policy.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + policy.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + policy.Algorithm = "" + policy.Persistence = "" + policy.IdleTimeout = 0 + policy.SorryPageUrl = "" + policy.SourceNat = "" + policy.CertificateID = "" + policy.HealthMonitorID = "" + policy.ListenerID = "" + policy.DefaultTargetGroupID = "" + policy.TLSPolicyID = "" + + return &policy +} + +var showResponse = fmt.Sprintf(` +{ + "policy": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "policy", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + "current": { + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + }, + "staged": null + } +}`) + +func showResult() *policies.Policy { + var policy policies.Policy + + var staged policies.ConfigurationInResponse + current := policies.ConfigurationInResponse{ + Algorithm: "round-robin", + Persistence: "cookie", + IdleTimeout: 600, + SorryPageUrl: "https://example.com/sorry", + SourceNat: "enable", + CertificateID: "f57a98fe-d63e-4048-93a0-51fe163f30d7", + HealthMonitorID: "dd7a96d6-4e66-4666-baca-a8555f0c472c", + ListenerID: "68633f4f-f52a-402f-8572-b8173418904f", + DefaultTargetGroupID: "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + TLSPolicyID: "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + policy.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policy.Name = "policy" + policy.Description = "description" + policy.Tags = tags + policy.ConfigurationStatus = "ACTIVE" + policy.OperationStatus = "COMPLETE" + policy.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + policy.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + policy.Algorithm = "round-robin" + policy.Persistence = "cookie" + policy.IdleTimeout = 600 + policy.SorryPageUrl = "https://example.com/sorry" + policy.SourceNat = "enable" + policy.CertificateID = "f57a98fe-d63e-4048-93a0-51fe163f30d7" + policy.HealthMonitorID = "dd7a96d6-4e66-4666-baca-a8555f0c472c" + policy.ListenerID = "68633f4f-f52a-402f-8572-b8173418904f" + policy.DefaultTargetGroupID = "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + policy.TLSPolicyID = "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + policy.Current = current + policy.Staged = staged + + return &policy +} + +var updateRequest = fmt.Sprintf(` +{ + "policy": { + "name": "policy", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "policy": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "policy", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "algorithm": null, + "persistence": null, + "idle_timeout": null, + "sorry_page_url": null, + "source_nat": null, + "certificate_id": null, + "health_monitor_id": null, + "listener_id": null, + "default_target_group_id": null, + "tls_policy_id": null + } +}`) + +func updateResult() *policies.Policy { + var policy policies.Policy + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + policy.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + policy.Name = "policy" + policy.Description = "description" + policy.Tags = tags + policy.ConfigurationStatus = "CREATE_STAGED" + policy.OperationStatus = "NONE" + policy.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + policy.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + policy.Algorithm = "" + policy.Persistence = "" + policy.IdleTimeout = 0 + policy.SorryPageUrl = "" + policy.SourceNat = "" + policy.CertificateID = "" + policy.HealthMonitorID = "" + policy.ListenerID = "" + policy.DefaultTargetGroupID = "" + policy.TLSPolicyID = "" + + return &policy +} + +var createStagedRequest = fmt.Sprintf(` +{ + "policy": { + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "policy": { + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + } +}`) + +func createStagedResult() *policies.Policy { + var policy policies.Policy + + policy.Algorithm = "round-robin" + policy.Persistence = "cookie" + policy.IdleTimeout = 600 + policy.SorryPageUrl = "https://example.com/sorry" + policy.SourceNat = "enable" + policy.CertificateID = "f57a98fe-d63e-4048-93a0-51fe163f30d7" + policy.HealthMonitorID = "dd7a96d6-4e66-4666-baca-a8555f0c472c" + policy.ListenerID = "68633f4f-f52a-402f-8572-b8173418904f" + policy.DefaultTargetGroupID = "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + policy.TLSPolicyID = "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + + return &policy +} + +var showStagedResponse = fmt.Sprintf(` +{ + "policy": { + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + } +}`) + +func showStagedResult() *policies.Policy { + var policy policies.Policy + + policy.Algorithm = "round-robin" + policy.Persistence = "cookie" + policy.IdleTimeout = 600 + policy.SorryPageUrl = "https://example.com/sorry" + policy.SourceNat = "enable" + policy.CertificateID = "f57a98fe-d63e-4048-93a0-51fe163f30d7" + policy.HealthMonitorID = "dd7a96d6-4e66-4666-baca-a8555f0c472c" + policy.ListenerID = "68633f4f-f52a-402f-8572-b8173418904f" + policy.DefaultTargetGroupID = "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + policy.TLSPolicyID = "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + + return &policy +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "policy": { + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "policy": { + "algorithm": "round-robin", + "persistence": "cookie", + "idle_timeout": 600, + "sorry_page_url": "https://example.com/sorry", + "source_nat": "enable", + "certificate_id": "f57a98fe-d63e-4048-93a0-51fe163f30d7", + "health_monitor_id": "dd7a96d6-4e66-4666-baca-a8555f0c472c", + "listener_id": "68633f4f-f52a-402f-8572-b8173418904f", + "default_target_group_id": "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + "tls_policy_id": "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + } +}`) + +func updateStagedResult() *policies.Policy { + var policy policies.Policy + + policy.Algorithm = "round-robin" + policy.Persistence = "cookie" + policy.IdleTimeout = 600 + policy.SorryPageUrl = "https://example.com/sorry" + policy.SourceNat = "enable" + policy.CertificateID = "f57a98fe-d63e-4048-93a0-51fe163f30d7" + policy.HealthMonitorID = "dd7a96d6-4e66-4666-baca-a8555f0c472c" + policy.ListenerID = "68633f4f-f52a-402f-8572-b8173418904f" + policy.DefaultTargetGroupID = "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + policy.TLSPolicyID = "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + + return &policy +} diff --git a/v4/ecl/managed_load_balancer/v1/policies/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/policies/testing/requests_test.go new file mode 100644 index 0000000..15b7900 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/testing/requests_test.go @@ -0,0 +1,331 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/policies" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListPolicies(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/policies", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := policies.ListOpts{} + + err := policies.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := policies.ExtractPolicies(page) + if err != nil { + t.Errorf("Failed to extract policies: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreatePolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/policies", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := policies.CreateOpts{ + Name: "policy", + Description: "description", + Tags: tags, + Algorithm: "round-robin", + Persistence: "cookie", + IdleTimeout: 600, + SorryPageUrl: "https://example.com/sorry", + SourceNat: "enable", + CertificateID: "f57a98fe-d63e-4048-93a0-51fe163f30d7", + HealthMonitorID: "dd7a96d6-4e66-4666-baca-a8555f0c472c", + ListenerID: "68633f4f-f52a-402f-8572-b8173418904f", + DefaultTargetGroupID: "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + TLSPolicyID: "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + actual, err := policies.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowPolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := policies.ShowOpts{} + + actual, err := policies.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdatePolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "policy" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := policies.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := policies.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeletePolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := policies.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedPolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + createStagedOpts := policies.CreateStagedOpts{ + Algorithm: "round-robin", + Persistence: "cookie", + IdleTimeout: 600, + SorryPageUrl: "https://example.com/sorry", + SourceNat: "enable", + CertificateID: "f57a98fe-d63e-4048-93a0-51fe163f30d7", + HealthMonitorID: "dd7a96d6-4e66-4666-baca-a8555f0c472c", + ListenerID: "68633f4f-f52a-402f-8572-b8173418904f", + DefaultTargetGroupID: "a44c4072-ed90-4b50-a33a-6b38fb10c7db", + TLSPolicyID: "4ba79662-f2a1-41a4-a3d9-595799bbcd86", + } + + actual, err := policies.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedPolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := policies.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedPolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + algorithm := "round-robin" + persistence := "cookie" + idleTimeout := 600 + sorryPageUrl := "https://example.com/sorry" + sourceNat := "enable" + certificateID := "f57a98fe-d63e-4048-93a0-51fe163f30d7" + healthMonitorID := "dd7a96d6-4e66-4666-baca-a8555f0c472c" + listenerID := "68633f4f-f52a-402f-8572-b8173418904f" + defaultTargetGroupID := "a44c4072-ed90-4b50-a33a-6b38fb10c7db" + tlsPolicyID := "4ba79662-f2a1-41a4-a3d9-595799bbcd86" + updateStagedOpts := policies.UpdateStagedOpts{ + Algorithm: &algorithm, + Persistence: &persistence, + IdleTimeout: &idleTimeout, + SorryPageUrl: &sorryPageUrl, + SourceNat: &sourceNat, + CertificateID: &certificateID, + HealthMonitorID: &healthMonitorID, + ListenerID: &listenerID, + DefaultTargetGroupID: &defaultTargetGroupID, + TLSPolicyID: &tlsPolicyID, + } + + actual, err := policies.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedPolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/policies/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := policies.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/policies/urls.go b/v4/ecl/managed_load_balancer/v1/policies/urls.go new file mode 100644 index 0000000..f5b3832 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/policies/urls.go @@ -0,0 +1,53 @@ +package policies + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("policies") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("policies", id) +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("policies", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/routes/doc.go b/v4/ecl/managed_load_balancer/v1/routes/doc.go new file mode 100644 index 0000000..e6a5a70 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/doc.go @@ -0,0 +1,141 @@ +/* +Package routes contains functionality for working with ECL Managed Load Balancer resources. + +Example to list routes + + listOpts := routes.ListOpts{} + + allPages, err := routes.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRoutes, err := routes.ExtractRoutes(allPages) + if err != nil { + panic(err) + } + + for _, route := range allRoutes { + fmt.Printf("%+v\n", route) + } + +Example to create a route + + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := routes.CreateOpts{ + Name: "route", + Description: "description", + Tags: tags, + DestinationCidr: "172.16.0.0/24", + NextHopIPAddress: "192.168.0.254", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + route, err := routes.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", route) + +Example to show a route + + showOpts := routes.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + route, err := routes.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", route) + +Example to update a route + + name := "route" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := routes.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + route, err := routes.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", route) + +Example to delete a route + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := routes.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged route configurations + + createStagedOpts := routes.CreateStagedOpts{ + NextHopIPAddress: "192.168.0.254", + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + routeConfigurations, err := routes.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", routeConfigurations) + +Example to show staged route configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + routeConfigurations, err := routes.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", routeConfigurations) + +Example to update staged route configurations + + nextHopIPAddress := "192.168.0.254" + updateStagedOpts := routes.UpdateStagedOpts{ + NextHopIPAddress: &nextHopIPAddress, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + routeConfigurations, err := routes.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", routeConfigurations) + +Example to cancel staged route configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := routes.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package routes diff --git a/v4/ecl/managed_load_balancer/v1/routes/requests.go b/v4/ecl/managed_load_balancer/v1/routes/requests.go new file mode 100644 index 0000000..c92fabc --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/requests.go @@ -0,0 +1,344 @@ +package routes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Routes +*/ + +// 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 route attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - CIDR of destination for the (static) route + DestinationCidr string `q:"destination_cidr"` + + // - IP address of next hop for the (static) route + NextHopIPAddress string `q:"next_hop_ip_address"` + + // - ID of the load balancer which the resource belongs to + LoadBalancerID string `q:"load_balancer_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToRouteListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRouteListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToRouteListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of routes. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToRouteListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RoutePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Create Route +*/ + +// CreateOpts represents options used to create a new route. +type CreateOpts struct { + + // - Name of the (static) route + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the (static) route + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the (static) route + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - CIDR of destination for the (static) route + // - If you configure `destination_cidr` as default gateway, set `0.0.0.0/0` + // - `destination_cidr` can not be changed once configured + // - If you want to change `destination_cidr`, recreate the (static) route again + // - Set a unique CIDR for all (static) routes which belong to the same load balancer + // - Set a CIDR which is not included in subnet of load balancer interfaces that the (static) route belongs to + // - Must not set a link-local CIDR (RFC 3927) which includes Common Function Gateway + DestinationCidr string `json:"destination_cidr"` + + // - ID of the load balancer which the (static) route belongs to + // - Set a CIDR which is not included in subnet of load balancer interfaces that the (static) route belongs to + // - Must not set a network IP address and broadcast IP address + NextHopIPAddress string `json:"next_hop_ip_address"` + + // - ID of the load balancer which the (static) route belongs to + LoadBalancerID string `json:"load_balancer_id"` +} + +// ToRouteCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToRouteCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "route") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToRouteCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new route using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRouteCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Route +*/ + +// ShowOpts represents options used to show a route. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToRouteShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToRouteShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToRouteShowQuery() (string, error) +} + +// Show retrieves a specific route based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToRouteShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Route Attributes +*/ + +// UpdateOpts represents options used to update a existing route. +type UpdateOpts struct { + + // - Name of the (static) route + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the (static) route + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the (static) route + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToRouteUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToRouteUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "route") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToRouteUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing route using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRouteUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Route +*/ + +// Delete accepts a unique ID and deletes the route associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Route Configurations +*/ + +// CreateStagedOpts represents options used to create new route configurations. +type CreateStagedOpts struct { + + // - ID of the load balancer which the (static) route belongs to + // - Set a CIDR which is included in subnet of load balancer interfaces that the (static) route belongs to + // - Must not set a network IP address and broadcast IP address + NextHopIPAddress string `json:"next_hop_ip_address,omitempty"` +} + +// ToRouteCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToRouteCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "route") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToRouteCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new route configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToRouteCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Route Configurations +*/ + +// ShowStaged retrieves specific route configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Route Configurations +*/ + +// UpdateStagedOpts represents options used to update existing Route configurations. +type UpdateStagedOpts struct { + + // - ID of the load balancer which the (static) route belongs to + // - Set a CIDR which is included in subnet of load balancer interfaces that the (static) route belongs to + // - Must not set a network IP address and broadcast IP address + NextHopIPAddress *string `json:"next_hop_ip_address,omitempty"` +} + +// ToRouteUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToRouteUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "route") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToRouteUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Route configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToRouteUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Route Configurations +*/ + +// CancelStaged accepts a unique ID and deletes route configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/routes/results.go b/v4/ecl/managed_load_balancer/v1/routes/results.go new file mode 100644 index 0000000..5bc90d7 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/results.go @@ -0,0 +1,175 @@ +package routes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a Route. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Route. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a Route. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a Route. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a Route. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a Route. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ConfigurationInResponse represents a configuration in a route. +type ConfigurationInResponse struct { + + // - IP address of next hop for the (static) route + NextHopIPAddress string `json:"next_hop_ip_address,omitempty"` +} + +// Route represents a route. +type Route struct { + + // - ID of the (static) route + ID string `json:"id"` + + // - Name of the (static) route + Name string `json:"name"` + + // - Description of the (static) route + Description string `json:"description"` + + // - Tags of the (static) route (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the (static) route + // - `"ACTIVE"` + // - There are no configurations of the (static) route that waiting to be applied + // - `"CREATE_STAGED"` + // - The (static) route has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the (static) route exists that waiting to be applied + // - `"DELETE_STAGED"` + // - The (static) route has been removed and waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Operation status of the load balancer which the (static) route belongs to + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - CIDR of destination for the (static) route + DestinationCidr string `json:"destination_cidr,omitempty"` + + // - ID of the load balancer which the (static) route belongs to + LoadBalancerID string `json:"load_balancer_id"` + + // - ID of the owner tenant of the (static) route + TenantID string `json:"tenant_id"` + + // - IP address of next hop for the (static) route + NextHopIPAddress string `json:"next_hop_ip_address,omitempty"` + + // - Running configurations of the (static) route + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the (static) route that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a route, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "route") +} + +// Extract is a function that accepts a result and extracts a Route resource. +func (r commonResult) Extract() (*Route, error) { + var route Route + + err := r.ExtractInto(&route) + + return &route, err +} + +// RoutePage is the page returned by a pager when traversing over a collection of route. +type RoutePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a RoutePage struct is empty. +func (r RoutePage) IsEmpty() (bool, error) { + is, err := ExtractRoutes(r) + + return len(is) == 0, err +} + +// ExtractRoutesInto interprets the results of a single page from a List() call, producing a slice of route entities. +func ExtractRoutesInto(r pagination.Page, v interface{}) error { + return r.(RoutePage).Result.ExtractIntoSlicePtr(v, "routes") +} + +// ExtractRoutes accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Route structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractRoutes(r pagination.Page) ([]Route, error) { + var s []Route + + err := ExtractRoutesInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/routes/testing/doc.go b/v4/ecl/managed_load_balancer/v1/routes/testing/doc.go new file mode 100644 index 0000000..3bc14e7 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains route unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/routes/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/routes/testing/fixtures.go new file mode 100644 index 0000000..98f7d63 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/testing/fixtures.go @@ -0,0 +1,275 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/routes" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "routes": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "route", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "destination_cidr": "172.16.0.0/24", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "next_hop_ip_address": "192.168.0.254" + } + ] +}`) + +func listResult() []routes.Route { + var route1 routes.Route + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + route1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + route1.Name = "route" + route1.Description = "description" + route1.Tags = tags1 + route1.ConfigurationStatus = "ACTIVE" + route1.OperationStatus = "COMPLETE" + route1.DestinationCidr = "172.16.0.0/24" + route1.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + route1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + route1.NextHopIPAddress = "192.168.0.254" + + return []routes.Route{route1} +} + +var createRequest = fmt.Sprintf(` +{ + "route": { + "name": "route", + "description": "description", + "tags": { + "key": "value" + }, + "destination_cidr": "172.16.0.0/24", + "next_hop_ip_address": "192.168.0.254", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040" + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "route": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "route", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "destination_cidr": "172.16.0.0/24", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "next_hop_ip_address": null + } +}`) + +func createResult() *routes.Route { + var route routes.Route + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + route.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + route.Name = "route" + route.Description = "description" + route.Tags = tags + route.ConfigurationStatus = "CREATE_STAGED" + route.OperationStatus = "NONE" + route.DestinationCidr = "172.16.0.0/24" + route.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + route.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + route.NextHopIPAddress = "" + + return &route +} + +var showResponse = fmt.Sprintf(` +{ + "route": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "route", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "destination_cidr": "172.16.0.0/24", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "next_hop_ip_address": "192.168.0.254", + "current": { + "next_hop_ip_address": "192.168.0.254" + }, + "staged": null + } +}`) + +func showResult() *routes.Route { + var route routes.Route + + var staged routes.ConfigurationInResponse + current := routes.ConfigurationInResponse{ + NextHopIPAddress: "192.168.0.254", + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + route.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + route.Name = "route" + route.Description = "description" + route.Tags = tags + route.ConfigurationStatus = "ACTIVE" + route.OperationStatus = "COMPLETE" + route.DestinationCidr = "172.16.0.0/24" + route.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + route.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + route.NextHopIPAddress = "192.168.0.254" + route.Current = current + route.Staged = staged + + return &route +} + +var updateRequest = fmt.Sprintf(` +{ + "route": { + "name": "route", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "route": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "route", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "destination_cidr": "172.16.0.0/24", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "next_hop_ip_address": null + } +}`) + +func updateResult() *routes.Route { + var route routes.Route + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + route.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + route.Name = "route" + route.Description = "description" + route.Tags = tags + route.ConfigurationStatus = "CREATE_STAGED" + route.OperationStatus = "NONE" + route.DestinationCidr = "172.16.0.0/24" + route.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + route.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + route.NextHopIPAddress = "" + + return &route +} + +var createStagedRequest = fmt.Sprintf(` +{ + "route": { + "next_hop_ip_address": "192.168.0.254" + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "route": { + "next_hop_ip_address": "192.168.0.254" + } +}`) + +func createStagedResult() *routes.Route { + var route routes.Route + + route.NextHopIPAddress = "192.168.0.254" + + return &route +} + +var showStagedResponse = fmt.Sprintf(` +{ + "route": { + "next_hop_ip_address": "192.168.0.254" + } +}`) + +func showStagedResult() *routes.Route { + var route routes.Route + + route.NextHopIPAddress = "192.168.0.254" + + return &route +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "route": { + "next_hop_ip_address": "192.168.0.254" + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "route": { + "next_hop_ip_address": "192.168.0.254" + } +}`) + +func updateStagedResult() *routes.Route { + var route routes.Route + + route.NextHopIPAddress = "192.168.0.254" + + return &route +} diff --git a/v4/ecl/managed_load_balancer/v1/routes/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/routes/testing/requests_test.go new file mode 100644 index 0000000..48f8252 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/testing/requests_test.go @@ -0,0 +1,296 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/routes" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListRoutes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/routes", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := routes.ListOpts{} + + err := routes.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := routes.ExtractRoutes(page) + if err != nil { + t.Errorf("Failed to extract routes: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/routes", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := routes.CreateOpts{ + Name: "route", + Description: "description", + Tags: tags, + DestinationCidr: "172.16.0.0/24", + NextHopIPAddress: "192.168.0.254", + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + } + + actual, err := routes.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := routes.ShowOpts{} + + actual, err := routes.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "route" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := routes.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := routes.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := routes.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + createStagedOpts := routes.CreateStagedOpts{ + NextHopIPAddress: "192.168.0.254", + } + + actual, err := routes.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := routes.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + nextHopIPAddress := "192.168.0.254" + updateStagedOpts := routes.UpdateStagedOpts{ + NextHopIPAddress: &nextHopIPAddress, + } + + actual, err := routes.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/routes/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := routes.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/routes/urls.go b/v4/ecl/managed_load_balancer/v1/routes/urls.go new file mode 100644 index 0000000..8db2c3a --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/routes/urls.go @@ -0,0 +1,53 @@ +package routes + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("routes") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("routes", id) +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("routes", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/rules/doc.go b/v4/ecl/managed_load_balancer/v1/rules/doc.go new file mode 100644 index 0000000..1356c59 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/doc.go @@ -0,0 +1,157 @@ +/* +Package rules contains functionality for working with ECL Managed Load Balancer resources. + +Example to list rules + + listOpts := rules.ListOpts{} + + allPages, err := rules.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRules, err := rules.ExtractRules(allPages) + if err != nil { + panic(err) + } + + for _, rule := range allRules { + fmt.Printf("%+v\n", rule) + } + +Example to create a rule + + condition := rules.CreateOptsCondition{ + PathPatterns: []string{"^/statics/"}, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := rules.CreateOpts{ + Name: "rule", + Description: "description", + Tags: tags, + Priority: 1, + TargetGroupID: "29527a3c-9e5d-48b7-868f-6442c7d21a95", + PolicyID: "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + Conditions: &condition, + } + + rule, err := rules.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", rule) + +Example to show a rule + + showOpts := rules.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + rule, err := rules.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", rule) + +Example to update a rule + + name := "rule" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := rules.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + rule, err := rules.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", rule) + +Example to delete a rule + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := rules.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged rule configurations + + condition := rules.CreateStagedOptsCondition{ + PathPatterns: []string{"^/statics/"}, + } + createStagedOpts := rules.CreateStagedOpts{ + Priority: 1, + TargetGroupID: "29527a3c-9e5d-48b7-868f-6442c7d21a95", + Conditions: &condition, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ruleConfigurations, err := rules.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", ruleConfigurations) + +Example to show staged rule configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ruleConfigurations, err := rules.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", ruleConfigurations) + +Example to update staged rule configurations + + condition := rules.UpdateStagedOptsCondition{ + PathPatterns: &[]string{"^/statics/"}, + } + + priority := 1 + targetGroupID := "29527a3c-9e5d-48b7-868f-6442c7d21a95" + updateStagedOpts := rules.UpdateStagedOpts{ + Priority: &priority, + TargetGroupID: &targetGroupID, + Conditions: &condition, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ruleConfigurations, err := rules.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", ruleConfigurations) + +Example to cancel staged rule configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := rules.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package rules diff --git a/v4/ecl/managed_load_balancer/v1/rules/requests.go b/v4/ecl/managed_load_balancer/v1/rules/requests.go new file mode 100644 index 0000000..783ccdc --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/requests.go @@ -0,0 +1,390 @@ +package rules + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Rules +*/ + +// 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 rule attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - Priority of the rule + Priority int `q:"priority"` + + // - ID of the target group that assigned to the rule + TargetGroupID string `q:"target_group_id"` + + // - ID of the policy which the rule belongs to + PolicyID string `q:"policy_id"` + + // - ID of the load balancer which the resource belongs to + LoadBalancerID string `q:"load_balancer_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToRuleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRuleListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToRuleListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of rules. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(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}} + }) +} + +/* +Create Rule +*/ + +// CreateOptsCondition represents condition information in the rule creation. +type CreateOptsCondition struct { + + // - URL path patterns (regular expressions) of the condition + // - Set a path pattern as unique string in all path patterns which belong to the same policy + // - Set a path pattern in PCRE (Perl Compatible Regular Expressions) format + // - Capturing groups and backreferences are not supported + PathPatterns []string `json:"path_patterns,omitempty"` +} + +// CreateOpts represents options used to create a new rule. +type CreateOpts struct { + + // - Name of the rule + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the rule + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the rule + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - Priority of the rule + // - Set an unique number in all rules which belong to the same policy + Priority int `json:"priority,omitempty"` + + // - ID of the target group that assigned to the rule + // - Set a different target group from `"default_target_group_id"` of the policy + TargetGroupID string `json:"target_group_id,omitempty"` + + // - ID of the policy which the rule belongs to + // - Set ID of the policy which has a listener in which protocol is either `"http"` or `"https"` + PolicyID string `json:"policy_id,omitempty"` + + // - Conditions of the rules to distribute accesses to the target groups + // - Set one or more condition + Conditions *CreateOptsCondition `json:"conditions,omitempty"` +} + +// ToRuleCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "rule") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new rule using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Rule +*/ + +// ShowOpts represents options used to show a rule. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToRuleShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToRuleShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToRuleShowQuery() (string, error) +} + +// Show retrieves a specific rule based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToRuleShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Rule Attributes +*/ + +// UpdateOpts represents options used to update a existing rule. +type UpdateOpts struct { + + // - Name of the rule + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the rule + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the rule + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToRuleUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToRuleUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "rule") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToRuleUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing rule using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRuleUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Rule +*/ + +// Delete accepts a unique ID and deletes the rule associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Rule Configurations +*/ + +// CreateStagedOptsCondition represents condition information in the rule configurations creation. +type CreateStagedOptsCondition struct { + + // - URL path patterns (regular expressions) of the condition + // - Set a path pattern as unique string in all path patterns which belong to the same policy + // - Set a path pattern in PCRE (Perl Compatible Regular Expressions) format + // - Capturing groups and backreferences are not supported + PathPatterns []string `json:"path_patterns,omitempty"` +} + +// CreateStagedOpts represents options used to create new rule configurations. +type CreateStagedOpts struct { + + // - Priority of the rule + // - Set an unique number in all rules which belong to the same policy + Priority int `json:"priority,omitempty"` + + // - ID of the target group that assigned to the rule + // - Set a different target group from `"default_target_group_id"` of the policy + TargetGroupID string `json:"target_group_id,omitempty"` + + // - Conditions of the rules to distribute accesses to the target groups + // - Set one or more condition + Conditions *CreateStagedOptsCondition `json:"conditions,omitempty"` +} + +// ToRuleCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToRuleCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "rule") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToRuleCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new rule configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToRuleCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Rule Configurations +*/ + +// ShowStaged retrieves specific rule configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Rule Configurations +*/ + +// UpdateStagedOptsCondition represents condition information in rule configurations updation. +type UpdateStagedOptsCondition struct { + + // - URL path patterns (regular expressions) of the condition + // - Set a path pattern as unique string in all path patterns which belong to the same policy + // - Set a path pattern in PCRE (Perl Compatible Regular Expressions) format + // - Capturing groups and backreferences are not supported + PathPatterns *[]string `json:"path_patterns,omitempty"` +} + +// UpdateStagedOpts represents options used to update existing Rule configurations. +type UpdateStagedOpts struct { + + // - Priority of the rule + // - Set an unique number in all rules which belong to the same policy + Priority *int `json:"priority,omitempty"` + + // - ID of the target group that assigned to the rule + // - Set a different target group from `"default_target_group_id"` of the policy + TargetGroupID *string `json:"target_group_id,omitempty"` + + // - Conditions of the rules to distribute accesses to the target groups + // - Set one or more condition + Conditions *UpdateStagedOptsCondition `json:"conditions,omitempty"` +} + +// ToRuleUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToRuleUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "rule") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToRuleUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Rule configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToRuleUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Rule Configurations +*/ + +// CancelStaged accepts a unique ID and deletes rule configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/rules/results.go b/v4/ecl/managed_load_balancer/v1/rules/results.go new file mode 100644 index 0000000..9fed080 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/results.go @@ -0,0 +1,194 @@ +package rules + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a Rule. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a Rule. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a Rule. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a Rule. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a Rule. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a Rule. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ConfigurationInResponse represents a configuration in a rule. +type ConfigurationInResponse struct { + + // - Priority of the rule + Priority int `json:"priority,omitempty"` + + // - ID of the target group that assigned to the rule + TargetGroupID string `json:"target_group_id,omitempty"` + + // - Conditions of the rules to distribute accesses to the target groups + Conditions ConditionInResponse `json:"conditions,omitempty"` +} + +// ConditionInResponse represents a condition in a rule. +type ConditionInResponse struct { + + // - URL path patterns (regular expressions) of the condition + PathPatterns []string `json:"path_patterns"` +} + +// Rule represents a rule. +type Rule struct { + + // - ID of the rule + ID string `json:"id"` + + // - Name of the rule + Name string `json:"name"` + + // - Description of the rule + Description string `json:"description"` + + // - Tags of the rule (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the rule + // - `"ACTIVE"` + // - There are no configurations of the rule that waiting to be applied + // - `"CREATE_STAGED"` + // - The rule has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the rule exists that waiting to be applied + // - `"DELETE_STAGED"` + // - The rule has been removed and waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Operation status of the load balancer which the rule belongs to + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - ID of the policy which the rule belongs to + PolicyID string `json:"policy_id"` + + // - ID of the load balancer which the rule belongs to + LoadBalancerID string `json:"load_balancer_id"` + + // - ID of the owner tenant of the rule + TenantID string `json:"tenant_id"` + + // - Priority of the rule + Priority int `json:"priority,omitempty"` + + // - ID of the target group that assigned to the rule + TargetGroupID string `json:"target_group_id,omitempty"` + + // - Conditions of the rules to distribute accesses to the target groups + Conditions ConditionInResponse `json:"conditions,omitempty"` + + // - Running configurations of the rule + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the rule that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a rule, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "rule") +} + +// Extract is a function that accepts a result and extracts a Rule resource. +func (r commonResult) Extract() (*Rule, error) { + var rule Rule + + err := r.ExtractInto(&rule) + + return &rule, err +} + +// RulePage is the page returned by a pager when traversing over a collection of rule. +type RulePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a RulePage struct is empty. +func (r RulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(r) + + return len(is) == 0, err +} + +// ExtractRulesInto interprets the results of a single page from a List() call, producing a slice of rule entities. +func ExtractRulesInto(r pagination.Page, v interface{}) error { + return r.(RulePage).Result.ExtractIntoSlicePtr(v, "rules") +} + +// ExtractRules accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of Rule structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]Rule, error) { + var s []Rule + + err := ExtractRulesInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/rules/testing/doc.go b/v4/ecl/managed_load_balancer/v1/rules/testing/doc.go new file mode 100644 index 0000000..a4aa0e4 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains rule unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/rules/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/rules/testing/fixtures.go new file mode 100644 index 0000000..89aa42d --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/testing/fixtures.go @@ -0,0 +1,371 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/rules" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "rules": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "rule", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "policy_id": "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } + ] +}`) + +func listResult() []rules.Rule { + var rule1 rules.Rule + + condition1 := rules.ConditionInResponse{ + PathPatterns: []string{"^/statics/"}, + } + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + rule1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + rule1.Name = "rule" + rule1.Description = "description" + rule1.Tags = tags1 + rule1.ConfigurationStatus = "ACTIVE" + rule1.OperationStatus = "COMPLETE" + rule1.PolicyID = "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4" + rule1.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + rule1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + rule1.Priority = 1 + rule1.TargetGroupID = "29527a3c-9e5d-48b7-868f-6442c7d21a95" + rule1.Conditions = condition1 + + return []rules.Rule{rule1} +} + +var createRequest = fmt.Sprintf(` +{ + "rule": { + "name": "rule", + "description": "description", + "tags": { + "key": "value" + }, + "policy_id": "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "rule": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "rule", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "policy_id": "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "priority": null, + "target_group_id": null, + "conditions": null + } +}`) + +func createResult() *rules.Rule { + var rule rules.Rule + + var condition rules.ConditionInResponse + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + rule.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + rule.Name = "rule" + rule.Description = "description" + rule.Tags = tags + rule.ConfigurationStatus = "CREATE_STAGED" + rule.OperationStatus = "NONE" + rule.PolicyID = "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4" + rule.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + rule.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + rule.Priority = 0 + rule.TargetGroupID = "" + rule.Conditions = condition + + return &rule +} + +var showResponse = fmt.Sprintf(` +{ + "rule": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "rule", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "policy_id": "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + }, + "current": { + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + }, + "staged": null + } +}`) + +func showResult() *rules.Rule { + var rule rules.Rule + + condition := rules.ConditionInResponse{ + PathPatterns: []string{"^/statics/"}, + } + var staged rules.ConfigurationInResponse + current := rules.ConfigurationInResponse{ + Priority: 1, + TargetGroupID: "29527a3c-9e5d-48b7-868f-6442c7d21a95", + Conditions: condition, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + rule.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + rule.Name = "rule" + rule.Description = "description" + rule.Tags = tags + rule.ConfigurationStatus = "ACTIVE" + rule.OperationStatus = "COMPLETE" + rule.PolicyID = "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4" + rule.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + rule.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + rule.Priority = 1 + rule.TargetGroupID = "29527a3c-9e5d-48b7-868f-6442c7d21a95" + rule.Conditions = condition + rule.Current = current + rule.Staged = staged + + return &rule +} + +var updateRequest = fmt.Sprintf(` +{ + "rule": { + "name": "rule", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "rule": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "rule", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "policy_id": "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "priority": null, + "target_group_id": null, + "conditions": null + } +}`) + +func updateResult() *rules.Rule { + var rule rules.Rule + + var condition rules.ConditionInResponse + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + rule.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + rule.Name = "rule" + rule.Description = "description" + rule.Tags = tags + rule.ConfigurationStatus = "CREATE_STAGED" + rule.OperationStatus = "NONE" + rule.PolicyID = "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4" + rule.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + rule.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + rule.Priority = 0 + rule.TargetGroupID = "" + rule.Conditions = condition + + return &rule +} + +var createStagedRequest = fmt.Sprintf(` +{ + "rule": { + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "rule": { + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } +}`) + +func createStagedResult() *rules.Rule { + var rule rules.Rule + + condition := rules.ConditionInResponse{ + PathPatterns: []string{"^/statics/"}, + } + + rule.Priority = 1 + rule.TargetGroupID = "29527a3c-9e5d-48b7-868f-6442c7d21a95" + rule.Conditions = condition + + return &rule +} + +var showStagedResponse = fmt.Sprintf(` +{ + "rule": { + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } +}`) + +func showStagedResult() *rules.Rule { + var rule rules.Rule + + condition := rules.ConditionInResponse{ + PathPatterns: []string{"^/statics/"}, + } + + rule.Priority = 1 + rule.TargetGroupID = "29527a3c-9e5d-48b7-868f-6442c7d21a95" + rule.Conditions = condition + + return &rule +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "rule": { + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "rule": { + "priority": 1, + "target_group_id": "29527a3c-9e5d-48b7-868f-6442c7d21a95", + "conditions": { + "path_patterns": [ + "^/statics/" + ] + } + } +}`) + +func updateStagedResult() *rules.Rule { + var rule rules.Rule + + condition := rules.ConditionInResponse{ + PathPatterns: []string{"^/statics/"}, + } + + rule.Priority = 1 + rule.TargetGroupID = "29527a3c-9e5d-48b7-868f-6442c7d21a95" + rule.Conditions = condition + + return &rule +} diff --git a/v4/ecl/managed_load_balancer/v1/rules/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/rules/testing/requests_test.go new file mode 100644 index 0000000..e68c03b --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/testing/requests_test.go @@ -0,0 +1,312 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/rules" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListRules(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/rules", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := rules.ListOpts{} + + err := rules.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := rules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract rules: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/rules", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + condition := rules.CreateOptsCondition{ + PathPatterns: []string{"^/statics/"}, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := rules.CreateOpts{ + Name: "rule", + Description: "description", + Tags: tags, + Priority: 1, + TargetGroupID: "29527a3c-9e5d-48b7-868f-6442c7d21a95", + PolicyID: "fcb520e5-858d-4f9f-bc6c-7bd225fe7cf4", + Conditions: &condition, + } + + actual, err := rules.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := rules.ShowOpts{} + + actual, err := rules.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "rule" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := rules.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := rules.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := rules.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + condition := rules.CreateStagedOptsCondition{ + PathPatterns: []string{"^/statics/"}, + } + createStagedOpts := rules.CreateStagedOpts{ + Priority: 1, + TargetGroupID: "29527a3c-9e5d-48b7-868f-6442c7d21a95", + Conditions: &condition, + } + + actual, err := rules.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := rules.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + condition := rules.UpdateStagedOptsCondition{ + PathPatterns: &[]string{"^/statics/"}, + } + + priority := 1 + targetGroupID := "29527a3c-9e5d-48b7-868f-6442c7d21a95" + updateStagedOpts := rules.UpdateStagedOpts{ + Priority: &priority, + TargetGroupID: &targetGroupID, + Conditions: &condition, + } + + actual, err := rules.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/rules/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := rules.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/rules/urls.go b/v4/ecl/managed_load_balancer/v1/rules/urls.go new file mode 100644 index 0000000..3614e8c --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/rules/urls.go @@ -0,0 +1,53 @@ +package rules + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("rules") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("rules", id) +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("rules", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/doc.go b/v4/ecl/managed_load_balancer/v1/system_updates/doc.go new file mode 100644 index 0000000..19bf8c2 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/doc.go @@ -0,0 +1,32 @@ +/* +Package system_updates contains functionality for working with ECL Managed Load Balancer resources. + +Example to list system updates + + listOpts := system_updates.ListOpts{} + + allPages, err := system_updates.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSystemUpdates, err := system_updates.ExtractSystemUpdates(allPages) + if err != nil { + panic(err) + } + + for _, systemUpdate := range allSystemUpdates { + fmt.Printf("%+v\n", systemUpdate) + } + +Example to show a system update + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + systemUpdate, err := system_updates.Show(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", systemUpdate) +*/ +package system_updates diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/requests.go b/v4/ecl/managed_load_balancer/v1/system_updates/requests.go new file mode 100644 index 0000000..2d0e297 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/requests.go @@ -0,0 +1,85 @@ +package system_updates + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List System Updates +*/ + +// 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 system update attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - URL of announcement for the system update (for example, Knowledge Center news) + Href string `q:"href"` + + // - Current revision for the system update + CurrentRevision int `q:"current_revision"` + + // - Next revision for the system update + NextRevision int `q:"next_revision"` + + // - Whether the system update can be applied to the load balancer + Applicable bool `q:"applicable"` + + // - If `true` is set, only the latest resource is displayed + Latest bool `q:"latest"` +} + +// ToSystemUpdateListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSystemUpdateListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToSystemUpdateListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of system updates. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToSystemUpdateListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SystemUpdatePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Show System Update +*/ + +// Show retrieves a specific system update based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string) (r ShowResult) { + _, r.Err = c.Get(showURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/results.go b/v4/ecl/managed_load_balancer/v1/system_updates/results.go new file mode 100644 index 0000000..f6751d1 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/results.go @@ -0,0 +1,93 @@ +package system_updates + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a SystemUpdate. +type ShowResult struct { + commonResult +} + +// SystemUpdate represents a system update. +type SystemUpdate struct { + + // - ID of the system update + ID string `json:"id"` + + // - Name of the system update + Name string `json:"name"` + + // - Description of the system update + Description string `json:"description"` + + // - URL of announcement for the system update (for example, Knowledge Center news) + Href string `json:"href"` + + // - The time when the system update has been announced + // - Format: `"%Y-%m-%d %H:%M:%S"` (UTC) + PublishDatetime string `json:"publish_datetime"` + + // - The deadline for applying the system update to the load balancer at any time + // - **For load balancers that have not been applied the system update even after the deadline, the provider will automatically apply it in the maintenance window of each region** + // - Format: `"%Y-%m-%d %H:%M:%S"` (UTC) + LimitDatetime string `json:"limit_datetime"` + + // - Current revision for the system update + // - The system update can be applied to the load balancers that is this revision + CurrentRevision int `json:"current_revision"` + + // - Next revision for the system update + // - The load balancer to which the system update is applied will be this revision + NextRevision int `json:"next_revision"` + + // - Whether the system update can be applied to the load balancer + Applicable bool `json:"applicable"` +} + +// ExtractInto interprets any commonResult as a system update, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "system_update") +} + +// Extract is a function that accepts a result and extracts a SystemUpdate resource. +func (r commonResult) Extract() (*SystemUpdate, error) { + var systemUpdate SystemUpdate + + err := r.ExtractInto(&systemUpdate) + + return &systemUpdate, err +} + +// SystemUpdatePage is the page returned by a pager when traversing over a collection of system update. +type SystemUpdatePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a SystemUpdatePage struct is empty. +func (r SystemUpdatePage) IsEmpty() (bool, error) { + is, err := ExtractSystemUpdates(r) + + return len(is) == 0, err +} + +// ExtractSystemUpdatesInto interprets the results of a single page from a List() call, producing a slice of system update entities. +func ExtractSystemUpdatesInto(r pagination.Page, v interface{}) error { + return r.(SystemUpdatePage).Result.ExtractIntoSlicePtr(v, "system_updates") +} + +// ExtractSystemUpdates accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of SystemUpdate structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractSystemUpdates(r pagination.Page) ([]SystemUpdate, error) { + var s []SystemUpdate + + err := ExtractSystemUpdatesInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/testing/doc.go b/v4/ecl/managed_load_balancer/v1/system_updates/testing/doc.go new file mode 100644 index 0000000..94b14f9 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains system update unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/system_updates/testing/fixtures.go new file mode 100644 index 0000000..efc5b5f --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/testing/fixtures.go @@ -0,0 +1,73 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/system_updates" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "system_updates": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "security_update_202210", + "description": "description", + "href": "https://sdpf.ntt.com/news/2022100301/", + "publish_datetime": "2022-10-03 00:00:00", + "limit_datetime": "2022-10-11 12:59:59", + "current_revision": 1, + "next_revision": 2, + "applicable": true + } + ] +}`) + +func listResult() []system_updates.SystemUpdate { + var systemUpdate1 system_updates.SystemUpdate + + systemUpdate1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + systemUpdate1.Name = "security_update_202210" + systemUpdate1.Description = "description" + systemUpdate1.Href = "https://sdpf.ntt.com/news/2022100301/" + systemUpdate1.PublishDatetime = "2022-10-03 00:00:00" + systemUpdate1.LimitDatetime = "2022-10-11 12:59:59" + systemUpdate1.CurrentRevision = 1 + systemUpdate1.NextRevision = 2 + systemUpdate1.Applicable = true + + return []system_updates.SystemUpdate{systemUpdate1} +} + +var showResponse = fmt.Sprintf(` +{ + "system_update": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "security_update_202210", + "description": "description", + "href": "https://sdpf.ntt.com/news/2022100301/", + "publish_datetime": "2022-10-03 00:00:00", + "limit_datetime": "2022-10-11 12:59:59", + "current_revision": 1, + "next_revision": 2, + "applicable": true + } +}`) + +func showResult() *system_updates.SystemUpdate { + var systemUpdate system_updates.SystemUpdate + + systemUpdate.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + systemUpdate.Name = "security_update_202210" + systemUpdate.Description = "description" + systemUpdate.Href = "https://sdpf.ntt.com/news/2022100301/" + systemUpdate.PublishDatetime = "2022-10-03 00:00:00" + systemUpdate.LimitDatetime = "2022-10-11 12:59:59" + systemUpdate.CurrentRevision = 1 + systemUpdate.NextRevision = 2 + systemUpdate.Applicable = true + + return &systemUpdate +} diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/system_updates/testing/requests_test.go new file mode 100644 index 0000000..1689739 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/testing/requests_test.go @@ -0,0 +1,85 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/system_updates" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListSystemUpdates(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/system_updates", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := system_updates.ListOpts{} + + err := system_updates.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := system_updates.ExtractSystemUpdates(page) + if err != nil { + t.Errorf("Failed to extract system updates: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestShowSystemUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/system_updates/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + + actual, err := system_updates.Show(cli, id).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/system_updates/urls.go b/v4/ecl/managed_load_balancer/v1/system_updates/urls.go new file mode 100644 index 0000000..c6422c8 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/system_updates/urls.go @@ -0,0 +1,21 @@ +package system_updates + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("system_updates") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("system_updates", id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/doc.go b/v4/ecl/managed_load_balancer/v1/target_groups/doc.go new file mode 100644 index 0000000..bc9f526 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/doc.go @@ -0,0 +1,158 @@ +/* +Package target_groups contains functionality for working with ECL Managed Load Balancer resources. + +Example to list target groups + + listOpts := target_groups.ListOpts{} + + allPages, err := target_groups.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allTargetGroups, err := target_groups.ExtractTargetGroups(allPages) + if err != nil { + panic(err) + } + + for _, targetGroup := range allTargetGroups { + fmt.Printf("%+v\n", targetGroup) + } + +Example to create a target group + + member1 := target_groups.CreateOptsMember{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + createOpts := target_groups.CreateOpts{ + Name: "target_group", + Description: "description", + Tags: tags, + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + Members: &[]target_groups.CreateOptsMember{member1}, + } + + targetGroup, err := target_groups.Create(managedLoadBalancerClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", targetGroup) + +Example to show a target group + + showOpts := target_groups.ShowOpts{} + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroup, err := target_groups.Show(managedLoadBalancerClient, id, showOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", targetGroup) + +Example to update a target group + + name := "target_group" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + updateOpts := target_groups.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroup, err := target_groups.Update(managedLoadBalancerClient, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", targetGroup) + +Example to delete a target group + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := target_groups.Delete(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } + +Example to create staged target group configurations + + member1 := target_groups.CreateStagedOptsMember{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + createStagedOpts := target_groups.CreateStagedOpts{ + Members: &[]target_groups.CreateStagedOptsMember{member1}, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroupConfigurations, err := target_groups.CreateStaged(managedLoadBalancerClient, id, createStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", targetGroupConfigurations) + +Example to show staged target group configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroupConfigurations, err := target_groups.ShowStaged(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", targetGroupConfigurations) + +Example to update staged target group configurations + + member1IPAddress := "192.168.0.7" + member1Port := 80 + member1Weight := 1 + member1 := target_groups.UpdateStagedOptsMember{ + IPAddress: &member1IPAddress, + Port: &member1Port, + Weight: &member1Weight, + } + + updateStagedOpts := target_groups.UpdateStagedOpts{ + Members: &[]target_groups.UpdateStagedOptsMember{member1}, + } + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroupConfigurations, err := target_groups.UpdateStaged(managedLoadBalancerClient, updateStagedOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", targetGroupConfigurations) + +Example to cancel staged target group configurations + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + err := target_groups.CancelStaged(managedLoadBalancerClient, id).ExtractErr() + if err != nil { + panic(err) + } +*/ +package target_groups diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/requests.go b/v4/ecl/managed_load_balancer/v1/target_groups/requests.go new file mode 100644 index 0000000..cd34a09 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/requests.go @@ -0,0 +1,383 @@ +package target_groups + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List Target Groups +*/ + +// 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 target group attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Configuration status of the resource + ConfigurationStatus string `q:"configuration_status"` + + // - Operation status of the resource + OperationStatus string `q:"operation_status"` + + // - ID of the load balancer which the resource belongs to + LoadBalancerID string `q:"load_balancer_id"` + + // - ID of the owner tenant of the resource + TenantID string `q:"tenant_id"` +} + +// ToTargetGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTargetGroupListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToTargetGroupListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of target groups. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToTargetGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return TargetGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Create Target Group +*/ + +// CreateOptsMember represents member information in the target group creation. +type CreateOptsMember struct { + + // - IP address of the member (real server) + // - Set an unique combination of IP address and port in all members which belong to the same target group + // - Must not set a IP address which is included in `virtual_ip_address` and `reserved_fixed_ips` of load balancer interfaces that the target group belongs to + // - Must not set a IP address of listeners which belong to the same load balancer as the target group + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress string `json:"ip_address"` + + // - Port number of the member (real server) + // - Set an unique combination of IP address and port in all members which belong to the same target group + Port int `json:"port"` + + // - Weight for the member (real server) + // - If `policy.algorithm` is `"weighted-round-robin"` or `"weighted-least-connection"`, use this parameter + // - Set same weight for the combination of IP address and port in all members which belong to the same load balancer + Weight int `json:"weight,omitempty"` +} + +// CreateOpts represents options used to create a new target group. +type CreateOpts struct { + + // - Name of the target group + // - This field accepts single-byte characters only + Name string `json:"name,omitempty"` + + // - Description of the target group + // - This field accepts single-byte characters only + Description string `json:"description,omitempty"` + + // - Tags of the target group + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags map[string]interface{} `json:"tags,omitempty"` + + // - ID of the load balancer which the target group belongs to + LoadBalancerID string `json:"load_balancer_id,omitempty"` + + // - Members (real servers) of the target group + Members *[]CreateOptsMember `json:"members,omitempty"` +} + +// ToTargetGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToTargetGroupCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "target_group") +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToTargetGroupCreateMap() (map[string]interface{}, error) +} + +// Create accepts a CreateOpts struct and creates a new target group using the values provided. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTargetGroupCreateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Target Group +*/ + +// ShowOpts represents options used to show a target group. +type ShowOpts struct { + + // - If `true` is set, `current` and `staged` are returned in response body + Changes bool `q:"changes"` +} + +// ToTargetGroupShowQuery formats a ShowOpts into a query string. +func (opts ShowOpts) ToTargetGroupShowQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ShowOptsBuilder allows extensions to add additional parameters to the Show request. +type ShowOptsBuilder interface { + ToTargetGroupShowQuery() (string, error) +} + +// Show retrieves a specific target group based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string, opts ShowOptsBuilder) (r ShowResult) { + url := showURL(c, id) + + if opts != nil { + query, _ := opts.ToTargetGroupShowQuery() + url += query + } + + _, r.Err = c.Get(url, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Target Group Attributes +*/ + +// UpdateOpts represents options used to update a existing target group. +type UpdateOpts struct { + + // - Name of the target group + // - This field accepts single-byte characters only + Name *string `json:"name,omitempty"` + + // - Description of the target group + // - This field accepts single-byte characters only + Description *string `json:"description,omitempty"` + + // - Tags of the target group + // - Set JSON object up to 32,768 characters + // - Nested structure is permitted + // - This field accepts single-byte characters only + Tags *map[string]interface{} `json:"tags,omitempty"` +} + +// ToTargetGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToTargetGroupUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "target_group") +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToTargetGroupUpdateMap() (map[string]interface{}, error) +} + +// Update accepts a UpdateOpts struct and updates a existing target group using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTargetGroupUpdateMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Delete Target Group +*/ + +// Delete accepts a unique ID and deletes the target group associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} + +/* +Create Staged Target Group Configurations +*/ + +// CreateStagedOptsMember represents member information in the target group configurations creation. +type CreateStagedOptsMember struct { + + // - IP address of the member (real server) + // - Set an unique combination of IP address and port in all members which belong to the same target group + // - Must not set a IP address which is included in `virtual_ip_address` and `reserved_fixed_ips` of load balancer interfaces that the target group belongs to + // - Must not set a IP address of listeners which belong to the same load balancer as the target group + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress string `json:"ip_address"` + + // - Port number of the member (real server) + // - Set an unique combination of IP address and port in all members which belong to the same target group + Port int `json:"port"` + + // - Weight for the member (real server) + // - If `policy.algorithm` is `"weighted-round-robin"` or `"weighted-least-connection"`, use this parameter + // - Set same weight for the combination of IP address and port in all members which belong to the same load balancer + Weight int `json:"weight,omitempty"` +} + +// CreateStagedOpts represents options used to create new target group configurations. +type CreateStagedOpts struct { + + // - Members (real servers) of the target group + Members *[]CreateStagedOptsMember `json:"members,omitempty"` +} + +// ToTargetGroupCreateStagedMap builds a request body from CreateStagedOpts. +func (opts CreateStagedOpts) ToTargetGroupCreateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "target_group") +} + +// CreateStagedOptsBuilder allows extensions to add additional parameters to the CreateStaged request. +type CreateStagedOptsBuilder interface { + ToTargetGroupCreateStagedMap() (map[string]interface{}, error) +} + +// CreateStaged accepts a CreateStagedOpts struct and creates new target group configurations using the values provided. +func CreateStaged(c *eclcloud.ServiceClient, id string, opts CreateStagedOptsBuilder) (r CreateStagedResult) { + b, err := opts.ToTargetGroupCreateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Post(createStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Show Staged Target Group Configurations +*/ + +// ShowStaged retrieves specific target group configurations based on its unique ID. +func ShowStaged(c *eclcloud.ServiceClient, id string) (r ShowStagedResult) { + _, r.Err = c.Get(showStagedURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Update Staged Target Group Configurations +*/ + +// UpdateStagedOptsMember represents member information in target group configurations updation. +type UpdateStagedOptsMember struct { + + // - IP address of the member (real server) + // - Set an unique combination of IP address and port in all members which belong to the same target group + // - Must not set a IP address which is included in `virtual_ip_address` and `reserved_fixed_ips` of load balancer interfaces that the target group belongs to + // - Must not set a IP address of listeners which belong to the same load balancer as the target group + // - Must not set a link-local IP address (RFC 3927) which includes Common Function Gateway + IPAddress *string `json:"ip_address"` + + // - Port number of the member (real server) + // - Set an unique combination of IP address and port in all members which belong to the same target group + Port *int `json:"port"` + + // - Weight for the member (real server) + // - If `policy.algorithm` is `"weighted-round-robin"` or `"weighted-least-connection"`, use this parameter + // - Set same weight for the combination of IP address and port in all members which belong to the same load balancer + Weight *int `json:"weight,omitempty"` +} + +// UpdateStagedOpts represents options used to update existing Target Group configurations. +type UpdateStagedOpts struct { + + // - Members (real servers) of the target group + Members *[]UpdateStagedOptsMember `json:"members,omitempty"` +} + +// ToTargetGroupUpdateStagedMap builds a request body from UpdateStagedOpts. +func (opts UpdateStagedOpts) ToTargetGroupUpdateStagedMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "target_group") +} + +// UpdateStagedOptsBuilder allows extensions to add additional parameters to the UpdateStaged request. +type UpdateStagedOptsBuilder interface { + ToTargetGroupUpdateStagedMap() (map[string]interface{}, error) +} + +// UpdateStaged accepts a UpdateStagedOpts struct and updates existing Target Group configurations using the values provided. +func UpdateStaged(c *eclcloud.ServiceClient, id string, opts UpdateStagedOptsBuilder) (r UpdateStagedResult) { + b, err := opts.ToTargetGroupUpdateStagedMap() + if err != nil { + r.Err = err + + return + } + + _, r.Err = c.Patch(updateStagedURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} + +/* +Cancel Staged Target Group Configurations +*/ + +// CancelStaged accepts a unique ID and deletes target group configurations associated with it. +func CancelStaged(c *eclcloud.ServiceClient, id string) (r CancelStagedResult) { + _, r.Err = c.Delete(cancelStagedURL(c, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/results.go b/v4/ecl/managed_load_balancer/v1/target_groups/results.go new file mode 100644 index 0000000..ba663e8 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/results.go @@ -0,0 +1,186 @@ +package target_groups + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// CreateResult represents the result of a Create operation. +// Call its Extract method to interpret it as a TargetGroup. +type CreateResult struct { + commonResult +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a TargetGroup. +type ShowResult struct { + commonResult +} + +// UpdateResult represents the result of a Update operation. +// Call its Extract method to interpret it as a TargetGroup. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CreateStagedResult represents the result of a CreateStaged operation. +// Call its Extract method to interpret it as a TargetGroup. +type CreateStagedResult struct { + commonResult +} + +// ShowStagedResult represents the result of a ShowStaged operation. +// Call its Extract method to interpret it as a TargetGroup. +type ShowStagedResult struct { + commonResult +} + +// UpdateStagedResult represents the result of a UpdateStaged operation. +// Call its Extract method to interpret it as a TargetGroup. +type UpdateStagedResult struct { + commonResult +} + +// CancelStagedResult represents the result of a CancelStaged operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type CancelStagedResult struct { + eclcloud.ErrResult +} + +// ConfigurationInResponse represents a configuration in a target group. +type ConfigurationInResponse struct { + + // - Members (real servers) of the target group + Members []MemberInResponse `json:"members,omitempty"` +} + +// MemberInResponse represents a member in a target group. +type MemberInResponse struct { + + // - IP address of the member (real server) + IPAddress string `json:"ip_address"` + + // - Port number of the member (real server) + Port int `json:"port"` + + // - Weight for the member (real server) + // - If `policy.algorithm` is `"weighted-round-robin"` or `"weighted-least-connection"`, uses this parameter + Weight int `json:"weight"` +} + +// TargetGroup represents a target group. +type TargetGroup struct { + + // - ID of the target group + ID string `json:"id"` + + // - Name of the target group + Name string `json:"name"` + + // - Description of the target group + Description string `json:"description"` + + // - Tags of the target group (JSON object format) + Tags map[string]interface{} `json:"tags"` + + // - Configuration status of the target group + // - `"ACTIVE"` + // - There are no configurations of the target group that waiting to be applied + // - `"CREATE_STAGED"` + // - The target group has been added and waiting to be applied + // - `"UPDATE_STAGED"` + // - Changed configurations of the target group exists that waiting to be applied + // - `"DELETE_STAGED"` + // - The target group has been removed and waiting to be applied + ConfigurationStatus string `json:"configuration_status"` + + // - Operation status of the load balancer which the target group belongs to + // - `"NONE"` : + // - There are no operations of the load balancer + // - The load balancer and related resources can be operated + // - `"PROCESSING"` + // - The latest operation of the load balancer is processing + // - The load balancer and related resources cannot be operated + // - `"COMPLETE"` + // - The latest operation of the load balancer has been succeeded + // - The load balancer and related resources can be operated + // - `"STUCK"` + // - The latest operation of the load balancer has been stopped + // - Operators of NTT Communications will investigate the operation + // - The load balancer and related resources cannot be operated + // - `"ERROR"` + // - The latest operation of the load balancer has been failed + // - The operation was roll backed normally + // - The load balancer and related resources can be operated + OperationStatus string `json:"operation_status"` + + // - ID of the load balancer which the target group belongs to + LoadBalancerID string `json:"load_balancer_id"` + + // - ID of the owner tenant of the target group + TenantID string `json:"tenant_id"` + + // - Members (real servers) of the target group + Members []MemberInResponse `json:"members,omitempty"` + + // - Running configurations of the target group + // - If `changes` is `true`, return object + // - If current configuration does not exist, return `null` + Current ConfigurationInResponse `json:"current,omitempty"` + + // - Added or changed configurations of the target group that waiting to be applied + // - If `changes` is `true`, return object + // - If staged configuration does not exist, return `null` + Staged ConfigurationInResponse `json:"staged,omitempty"` +} + +// ExtractInto interprets any commonResult as a target group, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "target_group") +} + +// Extract is a function that accepts a result and extracts a TargetGroup resource. +func (r commonResult) Extract() (*TargetGroup, error) { + var targetGroup TargetGroup + + err := r.ExtractInto(&targetGroup) + + return &targetGroup, err +} + +// TargetGroupPage is the page returned by a pager when traversing over a collection of target group. +type TargetGroupPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a TargetGroupPage struct is empty. +func (r TargetGroupPage) IsEmpty() (bool, error) { + is, err := ExtractTargetGroups(r) + + return len(is) == 0, err +} + +// ExtractTargetGroupsInto interprets the results of a single page from a List() call, producing a slice of target group entities. +func ExtractTargetGroupsInto(r pagination.Page, v interface{}) error { + return r.(TargetGroupPage).Result.ExtractIntoSlicePtr(v, "target_groups") +} + +// ExtractTargetGroups accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of TargetGroup structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractTargetGroups(r pagination.Page) ([]TargetGroup, error) { + var s []TargetGroup + + err := ExtractTargetGroupsInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/testing/doc.go b/v4/ecl/managed_load_balancer/v1/target_groups/testing/doc.go new file mode 100644 index 0000000..8f80820 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains target group unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/target_groups/testing/fixtures.go new file mode 100644 index 0000000..7338e2b --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/testing/fixtures.go @@ -0,0 +1,349 @@ +package testing + +import ( + "encoding/json" + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/target_groups" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "target_groups": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "target_group", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } + ] +}`) + +func listResult() []target_groups.TargetGroup { + var targetGroup1 target_groups.TargetGroup + + member11 := target_groups.MemberInResponse{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + + var tags1 map[string]interface{} + tags1Json := `{"key":"value"}` + err := json.Unmarshal([]byte(tags1Json), &tags1) + if err != nil { + panic(err) + } + + targetGroup1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroup1.Name = "target_group" + targetGroup1.Description = "description" + targetGroup1.Tags = tags1 + targetGroup1.ConfigurationStatus = "ACTIVE" + targetGroup1.OperationStatus = "COMPLETE" + targetGroup1.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + targetGroup1.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + targetGroup1.Members = []target_groups.MemberInResponse{member11} + + return []target_groups.TargetGroup{targetGroup1} +} + +var createRequest = fmt.Sprintf(` +{ + "target_group": { + "name": "target_group", + "description": "description", + "tags": { + "key": "value" + }, + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } +}`) + +var createResponse = fmt.Sprintf(` +{ + "target_group": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "target_group", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "members": null + } +}`) + +func createResult() *target_groups.TargetGroup { + var targetGroup target_groups.TargetGroup + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + targetGroup.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroup.Name = "target_group" + targetGroup.Description = "description" + targetGroup.Tags = tags + targetGroup.ConfigurationStatus = "CREATE_STAGED" + targetGroup.OperationStatus = "NONE" + targetGroup.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + targetGroup.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + targetGroup.Members = nil + + return &targetGroup +} + +var showResponse = fmt.Sprintf(` +{ + "target_group": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "target_group", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "ACTIVE", + "operation_status": "COMPLETE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ], + "current": { + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + }, + "staged": null + } +}`) + +func showResult() *target_groups.TargetGroup { + var targetGroup target_groups.TargetGroup + + member1 := target_groups.MemberInResponse{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + var staged target_groups.ConfigurationInResponse + current := target_groups.ConfigurationInResponse{ + Members: []target_groups.MemberInResponse{member1}, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + targetGroup.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroup.Name = "target_group" + targetGroup.Description = "description" + targetGroup.Tags = tags + targetGroup.ConfigurationStatus = "ACTIVE" + targetGroup.OperationStatus = "COMPLETE" + targetGroup.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + targetGroup.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + targetGroup.Members = []target_groups.MemberInResponse{member1} + targetGroup.Current = current + targetGroup.Staged = staged + + return &targetGroup +} + +var updateRequest = fmt.Sprintf(` +{ + "target_group": { + "name": "target_group", + "description": "description", + "tags": { + "key": "value" + } + } +}`) + +var updateResponse = fmt.Sprintf(` +{ + "target_group": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "target_group", + "description": "description", + "tags": { + "key": "value" + }, + "configuration_status": "CREATE_STAGED", + "operation_status": "NONE", + "load_balancer_id": "67fea379-cff0-4191-9175-de7d6941a040", + "tenant_id": "34f5c98ef430457ba81292637d0c6fd0", + "members": null + } +}`) + +func updateResult() *target_groups.TargetGroup { + var targetGroup target_groups.TargetGroup + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + if err != nil { + panic(err) + } + + targetGroup.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + targetGroup.Name = "target_group" + targetGroup.Description = "description" + targetGroup.Tags = tags + targetGroup.ConfigurationStatus = "CREATE_STAGED" + targetGroup.OperationStatus = "NONE" + targetGroup.LoadBalancerID = "67fea379-cff0-4191-9175-de7d6941a040" + targetGroup.TenantID = "34f5c98ef430457ba81292637d0c6fd0" + targetGroup.Members = nil + + return &targetGroup +} + +var createStagedRequest = fmt.Sprintf(` +{ + "target_group": { + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } +}`) + +var createStagedResponse = fmt.Sprintf(` +{ + "target_group": { + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } +}`) + +func createStagedResult() *target_groups.TargetGroup { + var targetGroup target_groups.TargetGroup + + member1 := target_groups.MemberInResponse{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + + targetGroup.Members = []target_groups.MemberInResponse{member1} + + return &targetGroup +} + +var showStagedResponse = fmt.Sprintf(` +{ + "target_group": { + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } +}`) + +func showStagedResult() *target_groups.TargetGroup { + var targetGroup target_groups.TargetGroup + + member1 := target_groups.MemberInResponse{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + + targetGroup.Members = []target_groups.MemberInResponse{member1} + + return &targetGroup +} + +var updateStagedRequest = fmt.Sprintf(` +{ + "target_group": { + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } +}`) + +var updateStagedResponse = fmt.Sprintf(` +{ + "target_group": { + "members": [ + { + "ip_address": "192.168.0.7", + "port": 80, + "weight": 1 + } + ] + } +}`) + +func updateStagedResult() *target_groups.TargetGroup { + var targetGroup target_groups.TargetGroup + + member1 := target_groups.MemberInResponse{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + + targetGroup.Members = []target_groups.MemberInResponse{member1} + + return &targetGroup +} diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/target_groups/testing/requests_test.go new file mode 100644 index 0000000..c6da108 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/testing/requests_test.go @@ -0,0 +1,313 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/target_groups" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListTargetGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/target_groups", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := target_groups.ListOpts{} + + err := target_groups.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := target_groups.ExtractTargetGroups(page) + if err != nil { + t.Errorf("Failed to extract target groups: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestCreateTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/target_groups", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createResponse) + }) + + cli := ServiceClient() + member1 := target_groups.CreateOptsMember{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + createOpts := target_groups.CreateOpts{ + Name: "target_group", + Description: "description", + Tags: tags, + LoadBalancerID: "67fea379-cff0-4191-9175-de7d6941a040", + Members: &[]target_groups.CreateOptsMember{member1}, + } + + actual, err := target_groups.Create(cli, createOpts).Extract() + + th.CheckDeepEquals(t, createResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + showOpts := target_groups.ShowOpts{} + + actual, err := target_groups.Show(cli, id, showOpts).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateResponse) + }) + + cli := ServiceClient() + + name := "target_group" + description := "description" + + var tags map[string]interface{} + tagsJson := `{"key":"value"}` + err := json.Unmarshal([]byte(tagsJson), &tags) + + th.AssertNoErr(t, err) + + updateOpts := target_groups.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := target_groups.Update(cli, id, updateOpts).Extract() + + th.CheckDeepEquals(t, updateResult(), actual) + th.AssertNoErr(t, err) +} + +func TestDeleteTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := target_groups.Delete(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestCreateStagedTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, createStagedResponse) + }) + + cli := ServiceClient() + member1 := target_groups.CreateStagedOptsMember{ + IPAddress: "192.168.0.7", + Port: 80, + Weight: 1, + } + createStagedOpts := target_groups.CreateStagedOpts{ + Members: &[]target_groups.CreateStagedOptsMember{member1}, + } + + actual, err := target_groups.CreateStaged(cli, id, createStagedOpts).Extract() + + th.CheckDeepEquals(t, createStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestShowStagedTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showStagedResponse) + }) + + cli := ServiceClient() + actual, err := target_groups.ShowStaged(cli, id).Extract() + + th.CheckDeepEquals(t, showStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestUpdateStagedTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateStagedRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateStagedResponse) + }) + + cli := ServiceClient() + + member1IPAddress := "192.168.0.7" + member1Port := 80 + member1Weight := 1 + member1 := target_groups.UpdateStagedOptsMember{ + IPAddress: &member1IPAddress, + Port: &member1Port, + Weight: &member1Weight, + } + + updateStagedOpts := target_groups.UpdateStagedOpts{ + Members: &[]target_groups.UpdateStagedOptsMember{member1}, + } + + actual, err := target_groups.UpdateStaged(cli, id, updateStagedOpts).Extract() + + th.CheckDeepEquals(t, updateStagedResult(), actual) + th.AssertNoErr(t, err) +} + +func TestCancelStagedTargetGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/target_groups/%s/staged", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + cli := ServiceClient() + + err := target_groups.CancelStaged(cli, id).ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/target_groups/urls.go b/v4/ecl/managed_load_balancer/v1/target_groups/urls.go new file mode 100644 index 0000000..49a574d --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/target_groups/urls.go @@ -0,0 +1,53 @@ +package target_groups + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("target_groups") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("target_groups", id) +} + +func stagedURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("target_groups", id, "staged") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func showStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func updateStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} + +func cancelStagedURL(c *eclcloud.ServiceClient, id string) string { + return stagedURL(c, id) +} diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/doc.go b/v4/ecl/managed_load_balancer/v1/tls_policies/doc.go new file mode 100644 index 0000000..f861a87 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/doc.go @@ -0,0 +1,32 @@ +/* +Package tls_policies contains functionality for working with ECL Managed Load Balancer resources. + +Example to list tls policies + + listOpts := tls_policies.ListOpts{} + + allPages, err := tls_policies.List(managedLoadBalancerClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allTLSPolicies, err := tls_policies.ExtractTLSPolicies(allPages) + if err != nil { + panic(err) + } + + for _, tLSPolicy := range allTLSPolicies { + fmt.Printf("%+v\n", tLSPolicy) + } + +Example to show a tls policy + + id := "497f6eca-6276-4993-bfeb-53cbbbba6f08" + tLSPolicy, err := tls_policies.Show(managedLoadBalancerClient, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", tLSPolicy) +*/ +package tls_policies diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/requests.go b/v4/ecl/managed_load_balancer/v1/tls_policies/requests.go new file mode 100644 index 0000000..c19a868 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/requests.go @@ -0,0 +1,73 @@ +package tls_policies + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +/* +List TLS Policies +*/ + +// 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 tls policy attributes you want to see returned. +type ListOpts struct { + + // - ID of the resource + ID string `q:"id"` + + // - Name of the resource + // - This field accepts single-byte characters only + Name string `q:"name"` + + // - Description of the resource + // - This field accepts single-byte characters only + Description string `q:"description"` + + // - Whether the TLS policy will be set `policy.tls_policy_id` when that is not specified + Default bool `q:"default"` +} + +// ToTLSPolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTLSPolicyListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + + return q.String(), err +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToTLSPolicyListQuery() (string, error) +} + +// List returns a Pager which allows you to iterate over a collection of tls policies. +// It accepts a ListOpts struct, which allows you to filter and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + + if opts != nil { + query, err := opts.ToTLSPolicyListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return TLSPolicyPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +/* +Show TLS Policy +*/ + +// Show retrieves a specific tls policy based on its unique ID. +func Show(c *eclcloud.ServiceClient, id string) (r ShowResult) { + _, r.Err = c.Get(showURL(c, id), &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return +} diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/results.go b/v4/ecl/managed_load_balancer/v1/tls_policies/results.go new file mode 100644 index 0000000..3a07792 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/results.go @@ -0,0 +1,79 @@ +package tls_policies + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// ShowResult represents the result of a Show operation. +// Call its Extract method to interpret it as a TLSPolicy. +type ShowResult struct { + commonResult +} + +// TLSPolicy represents a tls policy. +type TLSPolicy struct { + + // - ID of the TLS policy + ID string `json:"id"` + + // - Name of the TLS policy + Name string `json:"name"` + + // - Description of the TLS policy + Description string `json:"description"` + + // - Whether the TLS policy will be set `policy.tls_policy_id` when that is not specified + Default bool `json:"default"` + + // - The list of acceptable TLS protocols in the policy that specifed this TLS policty + TLSProtocols []string `json:"tls_protocols"` + + // - The list of acceptable TLS ciphers in the policy that specifed this TLS policty + TLSCiphers []string `json:"tls_ciphers"` +} + +// ExtractInto interprets any commonResult as a tls policy, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "tls_policy") +} + +// Extract is a function that accepts a result and extracts a TLSPolicy resource. +func (r commonResult) Extract() (*TLSPolicy, error) { + var tLSPolicy TLSPolicy + + err := r.ExtractInto(&tLSPolicy) + + return &tLSPolicy, err +} + +// TLSPolicyPage is the page returned by a pager when traversing over a collection of tls policy. +type TLSPolicyPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a TLSPolicyPage struct is empty. +func (r TLSPolicyPage) IsEmpty() (bool, error) { + is, err := ExtractTLSPolicies(r) + + return len(is) == 0, err +} + +// ExtractTLSPoliciesInto interprets the results of a single page from a List() call, producing a slice of tls policy entities. +func ExtractTLSPoliciesInto(r pagination.Page, v interface{}) error { + return r.(TLSPolicyPage).Result.ExtractIntoSlicePtr(v, "tls_policies") +} + +// ExtractTLSPolicies accepts a Page struct, specifically a NetworkPage struct, and extracts the elements into a slice of TLSPolicy structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractTLSPolicies(r pagination.Page) ([]TLSPolicy, error) { + var s []TLSPolicy + + err := ExtractTLSPoliciesInto(r, &s) + + return s, err +} diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/testing/doc.go b/v4/ecl/managed_load_balancer/v1/tls_policies/testing/doc.go new file mode 100644 index 0000000..f7de45c --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/testing/doc.go @@ -0,0 +1,4 @@ +/* +Package testing contains tls policy unit tests +*/ +package testing diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/testing/fixtures.go b/v4/ecl/managed_load_balancer/v1/tls_policies/testing/fixtures.go new file mode 100644 index 0000000..56d3d08 --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/testing/fixtures.go @@ -0,0 +1,87 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/tls_policies" +) + +const id = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + +var listResponse = fmt.Sprintf(` +{ + "tls_policies": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "TLSv1.2_202210_01", + "description": "description", + "default": true, + "tls_protocols": [ + "TLSv1.2", + "TLSv1.3" + ], + "tls_ciphers": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_GCM_SHA256" + ] + } + ] +}`) + +func listResult() []tls_policies.TLSPolicy { + var tLSPolicy1 tls_policies.TLSPolicy + + tLSPolicy1.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + tLSPolicy1.Name = "TLSv1.2_202210_01" + tLSPolicy1.Description = "description" + tLSPolicy1.Default = true + tLSPolicy1.TLSProtocols = []string{"TLSv1.2", "TLSv1.3"} + tLSPolicy1.TLSCiphers = []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_GCM_SHA256"} + + return []tls_policies.TLSPolicy{tLSPolicy1} +} + +var showResponse = fmt.Sprintf(` +{ + "tls_policy": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "TLSv1.2_202210_01", + "description": "description", + "default": true, + "tls_protocols": [ + "TLSv1.2", + "TLSv1.3" + ], + "tls_ciphers": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_GCM_SHA256" + ] + } +}`) + +func showResult() *tls_policies.TLSPolicy { + var tLSPolicy tls_policies.TLSPolicy + + tLSPolicy.ID = "497f6eca-6276-4993-bfeb-53cbbbba6f08" + tLSPolicy.Name = "TLSv1.2_202210_01" + tLSPolicy.Description = "description" + tLSPolicy.Default = true + tLSPolicy.TLSProtocols = []string{"TLSv1.2", "TLSv1.3"} + tLSPolicy.TLSCiphers = []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_GCM_SHA256"} + + return &tLSPolicy +} diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/testing/requests_test.go b/v4/ecl/managed_load_balancer/v1/tls_policies/testing/requests_test.go new file mode 100644 index 0000000..2fbb37b --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/testing/requests_test.go @@ -0,0 +1,85 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/managed_load_balancer/v1/tls_policies" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + + return sc +} + +func TestListTLSPolicies(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/tls_policies", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + listOpts := tls_policies.ListOpts{} + + err := tls_policies.List(cli, listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := tls_policies.ExtractTLSPolicies(page) + if err != nil { + t.Errorf("Failed to extract tls policies: %v", err) + + return false, err + } + + th.CheckDeepEquals(t, listResult(), actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestShowTLSPolicy(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + fmt.Sprintf("/v1.0/tls_policies/%s", id), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, showResponse) + }) + + cli := ServiceClient() + + actual, err := tls_policies.Show(cli, id).Extract() + + th.CheckDeepEquals(t, showResult(), actual) + th.AssertNoErr(t, err) +} diff --git a/v4/ecl/managed_load_balancer/v1/tls_policies/urls.go b/v4/ecl/managed_load_balancer/v1/tls_policies/urls.go new file mode 100644 index 0000000..644ee6a --- /dev/null +++ b/v4/ecl/managed_load_balancer/v1/tls_policies/urls.go @@ -0,0 +1,21 @@ +package tls_policies + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("tls_policies") +} + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("tls_policies", id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func showURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/common/common_tests.go b/v4/ecl/network/v2/common/common_tests.go new file mode 100644 index 0000000..82b961d --- /dev/null +++ b/v4/ecl/network/v2/common/common_tests.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc +} diff --git a/v4/ecl/network/v2/common_function_gateways/doc.go b/v4/ecl/network/v2/common_function_gateways/doc.go new file mode 100644 index 0000000..766c6e0 --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/doc.go @@ -0,0 +1,57 @@ +/* +Package common_function_gateways contains functionality for working with +ECL Commnon Function Gateway resources. + +Example to List CommonFunctionGateways + + listOpts := common_function_gateways.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := common_function_gateways.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allCommonFunctionGateways, err := common_function_gateways.ExtractCommonFunctionGateways(allPages) + if err != nil { + panic(err) + } + + for _, common_function_gateways := range allCommonFunctionGateways { + fmt.Printf("%+v", common_function_gateways) + } + +Example to Create a common_function_gateways + + createOpts := common_function_gateways.CreateOpts{ + Name: "network_1", + } + + common_function_gateways, err := common_function_gateways.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a common_function_gateways + + commonFunctionGatewayID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := common_function_gateways.UpdateOpts{ + Name: "new_name", + } + + common_function_gateways, err := common_function_gateways.Update(networkClient, commonFunctionGatewayID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a common_function_gateways + + commonFunctionGatewayID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := common_function_gateways.Delete(networkClient, commonFunctionGatewayID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package common_function_gateways diff --git a/v4/ecl/network/v2/common_function_gateways/requests.go b/v4/ecl/network/v2/common_function_gateways/requests.go new file mode 100644 index 0000000..e769529 --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/requests.go @@ -0,0 +1,169 @@ +package common_function_gateways + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToCommonFunctionGatewayListQuery() (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 common function gateway attributes you want to see returned. +type ListOpts struct { + CommonFunctionPoolID string `q:"common_function_pool_id"` + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + Status string `q:"status"` + SubnetID string `q:"subnet_id"` + TenantID string `q:"tenant_id"` +} + +// ToCommonFunctionGatewayListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCommonFunctionGatewayListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// common function gateways. +// It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToCommonFunctionGatewayListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CommonFunctionGatewayPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific common function gateway based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToCommonFunctionGatewayCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a common function gateway. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CommonFunctionPoolID string `json:"common_function_pool_id,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToCommonFunctionGatewayCreateMap builds a request body from CreateOpts. +// func (opts CreateOpts) ToCommonFunctionGatewayCreateMap() (map[string]interface{}, error) { +func (opts CreateOpts) ToCommonFunctionGatewayCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "common_function_gateway") +} + +// Create accepts a CreateOpts struct and creates a new common function gateway +// 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 +// common function gateway. +// An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToCommonFunctionGatewayCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToCommonFunctionGatewayUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a common function gateway. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` +} + +// ToCommonFunctionGatewayUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToCommonFunctionGatewayUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "common_function_gateway") +} + +// Update accepts a UpdateOpts struct and updates an existing common function gateway +// using the values provided. For more information, see the Create function. +func Update(c *eclcloud.ServiceClient, commonFunctionGatewayID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToCommonFunctionGatewayUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, commonFunctionGatewayID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the common function gateway associated with it. +func Delete(c *eclcloud.ServiceClient, commonFunctionGatewayID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, commonFunctionGatewayID), nil) + return +} + +// IDFromName is a convenience function that returns a common function gateway's +// ID, given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractCommonFunctionGateways(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "common_function_gateway"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "common_function_gateway"} + } +} diff --git a/v4/ecl/network/v2/common_function_gateways/results.go b/v4/ecl/network/v2/common_function_gateways/results.go new file mode 100644 index 0000000..d998dc5 --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/results.go @@ -0,0 +1,108 @@ +package common_function_gateways + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a common function gateway resource. +func (r commonResult) Extract() (*CommonFunctionGateway, error) { + var cfgw CommonFunctionGateway + err := r.ExtractInto(&cfgw) + return &cfgw, err +} + +// Extract interprets any commonResult as a Common Function Gateway, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "common_function_gateway") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Common Function Gateway. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Common Function Gateway. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Common Function Gateway. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// CommonFunctionGateway represents, well, a common function gateway. +type CommonFunctionGateway struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + Description string `json:"description"` + + CommonFunctionPoolID string `json:"common_function_pool_id"` + + NetworkID string `json:"network_id"` + + SubnetID string `json:"subnet_id"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` +} + +// CommonFunctionGatewayPage is the page returned by a pager +// when traversing over a collection of common function gateway. +type CommonFunctionGatewayPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of common function gateway +// 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 (r CommonFunctionGatewayPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"common_function_gateways_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a CommonFunctionGatewayPage struct is empty. +func (r CommonFunctionGatewayPage) IsEmpty() (bool, error) { + is, err := ExtractCommonFunctionGateways(r) + return len(is) == 0, err +} + +// ExtractCommonFunctionGateways accepts a Page struct, +// specifically a NetworkPage struct, and extracts the elements +// into a slice of Common Function Gateway structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractCommonFunctionGateways(r pagination.Page) ([]CommonFunctionGateway, error) { + var s []CommonFunctionGateway + err := ExtractCommonFunctionGatewaysInto(r, &s) + return s, err +} + +// ExtractCommonFunctionGatewaysInto interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractCommonFunctionGatewaysInto(r pagination.Page, v interface{}) error { + return r.(CommonFunctionGatewayPage).Result.ExtractIntoSlicePtr(v, "common_function_gateways") +} diff --git a/v4/ecl/network/v2/common_function_gateways/testing/doc.go b/v4/ecl/network/v2/common_function_gateways/testing/doc.go new file mode 100644 index 0000000..c8a2e0c --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains common function gateways unit tests +package testing diff --git a/v4/ecl/network/v2/common_function_gateways/testing/fixtures.go b/v4/ecl/network/v2/common_function_gateways/testing/fixtures.go new file mode 100644 index 0000000..4791ae8 --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/testing/fixtures.go @@ -0,0 +1,178 @@ +package testing + +import ( + "fmt" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/common_function_gateways" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idCommonFunctionGatway1 = "fb3efc23-ca8c-4eb5-b7f6-6fc66ff24f9c" +const idCommonFunctionGatway2 = "3535de20-192d-4f5a-a74a-cd1a9c1bf747" +const idCommonFunctionPool = "4f4971a5-899d-42b4-8442-24f17eac9683" + +const nameCommonFunctionGateway1 = "common_function_gateway_name_1" +const descriptionCommonFunctionGateway1 = "common_function_gateway_description_1" + +const nameCommonFunctionGateway1Update = "common_function_gateway_name_1-update" +const descriptionCommonFunctionGateway1Update = "common_function_gateway_description_1-update" + +const tenantID = "2d5b878c-147a-4d7c-87fd-90a8be9d255f" + +const networkID = "511f266e-a8bf-4547-ab2a-fc4d2bda9f81" +const subnetID = "9f3fd369-e4d4-4c3a-84f1-9c5ba7686297" + +// ListResponse is mocked response of common_function_gateways.List +var ListResponse = fmt.Sprintf(` +{ + "common_function_gateways": [ + { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + }, + { + "id": "%s", + "common_function_pool_id": "%s", + "tenant_id": "%s", + "name": "common_function_gateway_name_2", + "description": "common_function_gateway_description_2", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + } + ] +}`, + // for common function gateway1 + idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + tenantID, + networkID, + subnetID, + // for common function gateway2 + idCommonFunctionGatway2, + idCommonFunctionPool, + tenantID, + networkID, + subnetID) + +// GetResponse is mocked format of common_function_gateways.Get +var GetResponse = fmt.Sprintf(` +{ + "common_function_gateway": { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + } +}`, idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + tenantID, + networkID, + subnetID) + +// CreateRequest is mocked request for common_function_gateways.Create +var CreateRequest = fmt.Sprintf(` +{ + "common_function_gateway": { + "name": "%s", + "description": "%s", + "common_function_pool_id": "%s", + "tenant_id": "%s" + } +}`, nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + idCommonFunctionPool, + tenantID) + +// CreateResponse is mocked response of common_function_gateways.Create +var CreateResponse = fmt.Sprintf(` +{ + "common_function_gateway": { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s", + "status": "ACTIVE" + } +}`, idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1, + descriptionCommonFunctionGateway1, + tenantID, + networkID, + subnetID) + +// UpdateRequest is mocked request of common_function_gateways.Update +var UpdateRequest = fmt.Sprintf(` +{ + "common_function_gateway": { + "name": "%s", + "description": "%s" + } +}`, nameCommonFunctionGateway1Update, + descriptionCommonFunctionGateway1Update) + +// UpdateResponse is mocked response of common_function_gateways.Update +var UpdateResponse = fmt.Sprintf(` +{ + "common_function_gateway": { + "id": "%s", + "common_function_pool_id": "%s", + "name": "%s", + "description": "%s", + "tenant_id": "%s", + "network_id": "%s", + "subnet_id": "%s" + } +}`, idCommonFunctionGatway1, + idCommonFunctionPool, + nameCommonFunctionGateway1Update, + descriptionCommonFunctionGateway1Update, + tenantID, + networkID, + subnetID) + +var commonFunctionGateway1 = common_function_gateways.CommonFunctionGateway{ + ID: idCommonFunctionGatway1, + CommonFunctionPoolID: idCommonFunctionPool, + TenantID: tenantID, + Name: nameCommonFunctionGateway1, + Description: descriptionCommonFunctionGateway1, + Status: "ACTIVE", + NetworkID: networkID, + SubnetID: subnetID, +} + +var commonFunctionGateway2 = common_function_gateways.CommonFunctionGateway{ + ID: idCommonFunctionGatway2, + CommonFunctionPoolID: idCommonFunctionPool, + TenantID: tenantID, + Name: "common_function_gateway_name_2", + Description: "common_function_gateway_description_2", + Status: "ACTIVE", + NetworkID: networkID, + SubnetID: subnetID, +} + +// ExpectedCommonFunctionGatewaysSlice is expected assertion target +var ExpectedCommonFunctionGatewaysSlice = []common_function_gateways.CommonFunctionGateway{ + commonFunctionGateway1, + commonFunctionGateway2, +} diff --git a/v4/ecl/network/v2/common_function_gateways/testing/requests_test.go b/v4/ecl/network/v2/common_function_gateways/testing/requests_test.go new file mode 100644 index 0000000..b20b0da --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/testing/requests_test.go @@ -0,0 +1,147 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/common_function_gateways" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v2.0/common_function_gateways", + 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + common_function_gateways.List(client, common_function_gateways.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := common_function_gateways.ExtractCommonFunctionGateways(page) + if err != nil { + t.Errorf("Failed to extract common function gateways: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedCommonFunctionGatewaysSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v2.0/common_function_gateways/%s", idCommonFunctionGatway1) + 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, GetResponse) + }) + + cfGw, err := common_function_gateways.Get(fake.ServiceClient(), idCommonFunctionGatway1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &commonFunctionGateway1, cfGw) +} + +func TestCreateCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_gateways", + 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := common_function_gateways.CreateOpts{ + Name: nameCommonFunctionGateway1, + Description: descriptionCommonFunctionGateway1, + CommonFunctionPoolID: idCommonFunctionPool, + TenantID: tenantID, + } + cfGw, err := common_function_gateways.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, cfGw.Status, "ACTIVE") + th.AssertDeepEquals(t, &commonFunctionGateway1, cfGw) +} + +func TestUpdateCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v2.0/common_function_gateways/%s", idCommonFunctionGatway1) + 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.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameCommonFunctionGateway1Update + description := descriptionCommonFunctionGateway1Update + updateOpts := common_function_gateways.UpdateOpts{ + Name: &name, + Description: &description, + } + cfGw, err := common_function_gateways.Update( + fake.ServiceClient(), idCommonFunctionGatway1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, cfGw.Name, nameCommonFunctionGateway1Update) + th.AssertEquals(t, cfGw.Description, descriptionCommonFunctionGateway1Update) + th.AssertEquals(t, cfGw.ID, idCommonFunctionGatway1) +} + +func TestDeleteCommonFunctionGatway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v2.0/common_function_gateways/%s", idCommonFunctionGatway1) + 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.StatusNoContent) + }) + + res := common_function_gateways.Delete(fake.ServiceClient(), idCommonFunctionGatway1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/common_function_gateways/urls.go b/v4/ecl/network/v2/common_function_gateways/urls.go new file mode 100644 index 0000000..ebc5755 --- /dev/null +++ b/v4/ecl/network/v2/common_function_gateways/urls.go @@ -0,0 +1,33 @@ +package common_function_gateways + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("common_function_gateways", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("common_function_gateways") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/common_function_pool/doc.go b/v4/ecl/network/v2/common_function_pool/doc.go new file mode 100644 index 0000000..1873e2a --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/doc.go @@ -0,0 +1,47 @@ +/* +Package common_function_pool contains functionality for working with +ECL Common Function Pool resources. + +Example to List Common Function Pools + + listOpts := common_function_pool.ListOpts{ + Description: "general", + } + + allPages, err := common_function_pool.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allCommonFunctionPools, err := common_function_pool.ExtractCommonFunctionPools(allPages) + if err != nil { + panic(err) + } + + for _, commonFunctionPool := range allCommonFunctionPools { + fmt.Printf("%+v\n", commonFunctionPool) + } + +Example to Show Common Function Pool + + commonFunctionPoolID := "c57066cc-9553-43a6-90de-asfdfesfffff" + + commonFunctionPool, err := common_function_pool.Get(networkClient, commonFunctionPoolID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", commonFunctionPool) + +Example to look for Common Function Pool's ID by its name + + commonFunctionPoolName := "CF_Pool1" + + commonFunctionPoolID, err := common_function_pool.IDFromName(networkClient, commonFunctionPoolName) + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", commonFunctionPoolID) +*/ +package common_function_pool diff --git a/v4/ecl/network/v2/common_function_pool/requests.go b/v4/ecl/network/v2/common_function_pool/requests.go new file mode 100644 index 0000000..c4b1ecd --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/requests.go @@ -0,0 +1,93 @@ +package common_function_pool + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToCommonFunctionPoolListQuery() (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 Common Function Pool attributes you want to see returned. SortKey allows you to sort +// by a particular Common Function Pool attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` +} + +// ToCommonFunctionPoolsListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCommonFunctionPoolListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Common Function 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 Common Function Pools that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToCommonFunctionPoolListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CommonFunctionPoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Common Function Pool based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// IDFromName is a convenience function that returns a Common Function Pool's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractCommonFunctionPools(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "common_function_pool"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "common_function_pool"} + } +} diff --git a/v4/ecl/network/v2/common_function_pool/results.go b/v4/ecl/network/v2/common_function_pool/results.go new file mode 100644 index 0000000..2e8bc4c --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/results.go @@ -0,0 +1,62 @@ +package common_function_pool + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// CommonFunctionPool represents a Common Function Pool. See package documentation for a top-level +// description of what this is. +type CommonFunctionPool struct { + + // Description is description + Description string `json:"description"` + + // UUID representing the Common Function Pool. + ID string `json:"id"` + + // Name of Common Function Pool + Name string `json:"name"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Common Function Pool. +type GetResult struct { + commonResult +} + +// CommonFunctionPoolPage is the page returned by a pager when traversing over a collection +// of common function pools. +type CommonFunctionPoolPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a CommonFunctionPoolPage struct is empty. +func (r CommonFunctionPoolPage) IsEmpty() (bool, error) { + is, err := ExtractCommonFunctionPools(r) + return len(is) == 0, err +} + +// ExtractCommonFunctionPools accepts a Page struct, specifically a CommonFunctionPoolPage struct, +// and extracts the elements into a slice of Common Function Pool structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractCommonFunctionPools(r pagination.Page) ([]CommonFunctionPool, error) { + var s struct { + CommonFunctionPools []CommonFunctionPool `json:"common_function_pools"` + } + err := (r.(CommonFunctionPoolPage)).ExtractInto(&s) + return s.CommonFunctionPools, err +} + +// Extract is a function that accepts a result and extracts a Common Function Pool resource. +func (r commonResult) Extract() (*CommonFunctionPool, error) { + var s struct { + CommonFunctionPool *CommonFunctionPool `json:"common_function_pool"` + } + err := r.ExtractInto(&s) + return s.CommonFunctionPool, err +} diff --git a/v4/ecl/network/v2/common_function_pool/testing/doc.go b/v4/ecl/network/v2/common_function_pool/testing/doc.go new file mode 100644 index 0000000..16d9a64 --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/testing/doc.go @@ -0,0 +1,2 @@ +// Common Function Pool unit tests +package testing diff --git a/v4/ecl/network/v2/common_function_pool/testing/fixtures.go b/v4/ecl/network/v2/common_function_pool/testing/fixtures.go new file mode 100644 index 0000000..e634f1b --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/testing/fixtures.go @@ -0,0 +1,69 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/common_function_pool" +) + +const ListResponse = ` +{ + "common_function_pools": [ + { + "description": "Common Function Pool 1", + "id": "c57066cc-9553-43a6-90de-asfdfesfffff", + "name": "CF_Pool1" + }, + { + "description": "Common Function Pool 2", + "id": "fesg66cc-9553-43a6-90de-c8472fdsafedf", + "name": "CF_Pool2" + } + ] +} +` + +const GetResponse = ` +{ + "common_function_pool": { + "description": "Common Function Pool Description", + "id": "c57066cc-9553-43a6-90de-c847231bc70b", + "name": "CF_Pool1" + } +} +` + +var CommonFunctionPool1 = common_function_pool.CommonFunctionPool{ + Description: "Common Function Pool 1", + ID: "c57066cc-9553-43a6-90de-asfdfesfffff", + Name: "CF_Pool1", +} + +var CommonFunctionPool2 = common_function_pool.CommonFunctionPool{ + Description: "Common Function Pool 2", + ID: "fesg66cc-9553-43a6-90de-c8472fdsafedf", + Name: "CF_Pool2", +} + +var CommonFunctionDetail = common_function_pool.CommonFunctionPool{ + Description: "Common Function Pool Description", + ID: "c57066cc-9553-43a6-90de-c847231bc70b", + Name: "CF_Pool1", +} + +var ExpectedCommonFunctionPoolSlice = []common_function_pool.CommonFunctionPool{CommonFunctionPool1, CommonFunctionPool2} + +const ListResponseDuplicatedNames = ` +{ + "common_function_pools": [ + { + "description": "Common Function Pool Description 1", + "id": "c57066cc-9553-43a6-90de-asfdfesfffff", + "name": "CF_Pool1" + }, + { + "description": "Common Function Pool Description 2", + "id": "fesg66cc-9553-43a6-90de-c8472fdsafedf", + "name": "CF_Pool1" + } + ] +} +` diff --git a/v4/ecl/network/v2/common_function_pool/testing/requests_test.go b/v4/ecl/network/v2/common_function_pool/testing/requests_test.go new file mode 100644 index 0000000..2f0d73e --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/testing/requests_test.go @@ -0,0 +1,135 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/common_function_pool" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListCommonFunctionPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + common_function_pool.List(client, common_function_pool.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := common_function_pool.ExtractCommonFunctionPools(page) + if err != nil { + t.Errorf("Failed to extract Common Function Pools: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedCommonFunctionPoolSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetCommonFunctionPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_pools/c57066cc-9553-43a6-90de-c847231bc70b", 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, GetResponse) + }) + + s, err := common_function_pool.Get(fake.ServiceClient(), "c57066cc-9553-43a6-90de-c847231bc70b").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CommonFunctionDetail, s) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "c57066cc-9553-43a6-90de-asfdfesfffff" + actualID, err := common_function_pool.IDFromName(client, "CF_Pool1") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := common_function_pool.IDFromName(client, "CF_PoolX") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/common_function_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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := common_function_pool.IDFromName(client, "CF_Pool1") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/network/v2/common_function_pool/urls.go b/v4/ecl/network/v2/common_function_pool/urls.go new file mode 100644 index 0000000..d96365a --- /dev/null +++ b/v4/ecl/network/v2/common_function_pool/urls.go @@ -0,0 +1,21 @@ +package common_function_pool + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("common_function_pools", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("common_function_pools") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} diff --git a/v4/ecl/network/v2/fic_gateways/doc.go b/v4/ecl/network/v2/fic_gateways/doc.go new file mode 100644 index 0000000..15f0fb6 --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/doc.go @@ -0,0 +1,35 @@ +/* +Package fic_gateways provides information of several service +in the Enterprise Cloud Compute service + +Example to List FIC Gateways + + listOpts := fic_gateways.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := fic_gateways.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFICGateways, err := fic_gateways.ExtractFICGateways(allPages) + if err != nil { + panic(err) + } + + for _, ficGateway := range allFICGateways { + fmt.Printf("%+v", ficGateway) + } + +Example to Show FIC Gateway + + id := "02dc9a22-129c-4b12-9936-4080f6a7ae44" + ficGateway, err := fic_gateways.Get(client, id).Extract() + if err != nil { + panic(err) + } + fmt.Print(ficGateway) + +*/ +package fic_gateways diff --git a/v4/ecl/network/v2/fic_gateways/requests.go b/v4/ecl/network/v2/fic_gateways/requests.go new file mode 100644 index 0000000..b9dc49c --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/requests.go @@ -0,0 +1,66 @@ +package fic_gateways + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFICGatewaysListQuery() (string, error) +} + +// ListOpts allows the filtering of paginated collections through the API. +// Filtering is achieved by passing in struct field values that map to +// the FIC Gateway attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // Description of the FIC Gateway resource. + Description string `q:"description"` + + // FIC Service instantiated by this Gateway. + FICServiceID string `q:"fic_service_id"` + + //Unique ID of the FIC Gateway resource. + ID string `q:"id"` + + //Name of the FIC Gateway resource. + Name string `q:"name"` + + // Quality of Service options selected for this Gateway. + QoSOptionID string `q:"qos_option_id"` + + // The FIC Gateway status. + Status string `q:"status"` + + // Tenant ID of the owner (UUID). + TenantID string `q:"tenant_id"` +} + +// ToFICGatewaysListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFICGatewaysListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list FIC Gateways accessible to you. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFICGatewaysListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FICGatewayPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific FIC Gateway based on its unique ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/v4/ecl/network/v2/fic_gateways/results.go b/v4/ecl/network/v2/fic_gateways/results.go new file mode 100644 index 0000000..b7b3c69 --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/results.go @@ -0,0 +1,53 @@ +package fic_gateways + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type FICGatewayPage struct { + pagination.LinkedPageBase +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a FICGateway. +type GetResult struct { + commonResult +} + +// FICGateway represents a FIC Gateway. +type FICGateway struct { + Description string `json:"description"` + FICServiceID string `json:"fic_service_id"` + ID string `json:"id"` + Name string `json:"name"` + QoSOptionID string `json:"qos_option_id"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` +} + +// IsEmpty checks whether a FICGatewayPage struct is empty. +func (r FICGatewayPage) IsEmpty() (bool, error) { + is, err := ExtractFICGateways(r) + return len(is) == 0, err +} + +// ExtractFICGateways accepts a Page struct, specifically a FICGatewayPage struct, +// and extracts the elements into a slice of ListOpts structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFICGateways(r pagination.Page) ([]FICGateway, error) { + var s []FICGateway + err := r.(FICGatewayPage).Result.ExtractIntoSlicePtr(&s, "fic_gateways") + return s, err +} + +// Extract is a function that accepts a result and extracts a FICGateway. +func (r GetResult) Extract() (*FICGateway, error) { + var l FICGateway + err := r.Result.ExtractIntoStructPtr(&l, "fic_gateway") + return &l, err +} diff --git a/v4/ecl/network/v2/fic_gateways/testing/doc.go b/v4/ecl/network/v2/fic_gateways/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v4/ecl/network/v2/fic_gateways/testing/fixtures.go b/v4/ecl/network/v2/fic_gateways/testing/fixtures.go new file mode 100644 index 0000000..3b99f66 --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/testing/fixtures.go @@ -0,0 +1,64 @@ +package testing + +import "github.com/nttcom/eclcloud/v4/ecl/network/v2/fic_gateways" + +const ListResponse = ` +{ + "fic_gateways": [ + { + "description": "fic_gateway_inet_test, 10M-BE, member role", + "fic_service_id": "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "id": "07f97269-e616-4dff-a73f-ca80bc5682dc", + "name": "lab3-test-member-user-fic-gateway", + "qos_option_id": "e41f6a2f-e197-41c8-9f71-ef19cfd2a85a", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + }, + { + "description": "", + "fic_service_id": "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "id": "4c842674-60e4-48eb-b5a3-b902f832d0af", + "name": "N000001996_V15000001", + "qos_option_id": "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } + ] +} +` + +const GetResponse = ` +{ + "fic_gateway": { + "description": "fic_gateway_inet_test, 10M-BE, member role", + "fic_service_id": "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "id": "07f97269-e616-4dff-a73f-ca80bc5682dc", + "name": "lab3-test-member-user-fic-gateway", + "qos_option_id": "e41f6a2f-e197-41c8-9f71-ef19cfd2a85a", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` + +var ficgw1 = fic_gateways.FICGateway{ + Description: "fic_gateway_inet_test, 10M-BE, member role", + FICServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + ID: "07f97269-e616-4dff-a73f-ca80bc5682dc", + Name: "lab3-test-member-user-fic-gateway", + QoSOptionID: "e41f6a2f-e197-41c8-9f71-ef19cfd2a85a", + Status: "ACTIVE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var ficgw2 = fic_gateways.FICGateway{ + Description: "", + FICServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + ID: "4c842674-60e4-48eb-b5a3-b902f832d0af", + Name: "N000001996_V15000001", + QoSOptionID: "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + Status: "ACTIVE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var ExpectedFICGatewaySlice = []fic_gateways.FICGateway{ficgw1, ficgw2} diff --git a/v4/ecl/network/v2/fic_gateways/testing/request_test.go b/v4/ecl/network/v2/fic_gateways/testing/request_test.go new file mode 100644 index 0000000..ef9b5fd --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/testing/request_test.go @@ -0,0 +1,69 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/network/v2/fic_gateways" + "github.com/nttcom/eclcloud/v4/pagination" + + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListFICGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fic_gateways", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + fic_gateways.List(client, fic_gateways.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := fic_gateways.ExtractFICGateways(page) + if err != nil { + t.Errorf("Failed to extract FIC Gateways: %v", err) + return false, nil + } + th.CheckDeepEquals(t, ExpectedFICGatewaySlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetFICGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + id := "07f97269-e616-4dff-a73f-ca80bc5682dc" + th.Mux.HandleFunc(fmt.Sprintf("/v2.0/fic_gateways/%s", 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, GetResponse) + }) + + n, err := fic_gateways.Get(fake.ServiceClient(), id).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ficgw1, n) +} diff --git a/v4/ecl/network/v2/fic_gateways/urls.go b/v4/ecl/network/v2/fic_gateways/urls.go new file mode 100644 index 0000000..27fc799 --- /dev/null +++ b/v4/ecl/network/v2/fic_gateways/urls.go @@ -0,0 +1,11 @@ +package fic_gateways + +import "github.com/nttcom/eclcloud/v4" + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("fic_gateways", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("fic_gateways") +} diff --git a/v4/ecl/network/v2/gateway_interfaces/doc.go b/v4/ecl/network/v2/gateway_interfaces/doc.go new file mode 100644 index 0000000..2029b1c --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/doc.go @@ -0,0 +1 @@ +package gateway_interfaces diff --git a/v4/ecl/network/v2/gateway_interfaces/requests.go b/v4/ecl/network/v2/gateway_interfaces/requests.go new file mode 100644 index 0000000..289423a --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/requests.go @@ -0,0 +1,162 @@ +package gateway_interfaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type ListOptsBuilder interface { + ToGatewayInterfaceListQuery() (string, error) +} + +type ListOpts struct { + AwsGwID string `q:"aws_gw_id"` + AzureGwID string `q:"azure_gw_id"` + Description string `q:"description"` + FICGatewayID string `q:"fic_gw_id"` + GcpGwID string `q:"gcp_gw_id"` + GwVipv4 string `q:"gw_vipv4"` + GwVipv6 string `q:"gw_vipv6"` + ID string `q:"id"` + InterdcGwID string `q:"interdc_gw_id"` + InternetGwID string `q:"internet_gw_id"` + Name string `q:"name"` + Netmask int `q:"netmask"` + NetworkID string `q:"network_id"` + PrimaryIpv4 string `q:"primary_ipv4"` + PrimaryIpv6 string `q:"primary_ipv6"` + SecondaryIpv4 string `q:"secondary_ipv4"` + SecondaryIpv6 string `q:"secondary_ipv6"` + ServiceType string `q:"service_type"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + VpnGwID string `q:"vpn_gw_id"` + VRID int `q:"vrid"` +} + +func (opts ListOpts) ToGatewayInterfaceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToGatewayInterfaceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return GatewayInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, gatewayInterfaceID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, gatewayInterfaceID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToGatewayInterfaceCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + AwsGwID string `json:"aws_gw_id,omitempty"` + AzureGwID string `json:"azure_gw_id,omitempty"` + Description string `json:"description"` + FICGatewayID string `json:"fic_gw_id,omitempty"` + GcpGwID string `json:"gcp_gw_id,omitempty"` + GwVipv4 string `json:"gw_vipv4" required:"true"` + InterdcGwID string `json:"interdc_gw_id,omitempty"` + InternetGwID string `json:"internet_gw_id,omitempty"` + Name string `json:"name"` + Netmask int `json:"netmask" required:"true"` + NetworkID string `json:"network_id" required:"true"` + PrimaryIpv4 string `json:"primary_ipv4" required:"true"` + SecondaryIpv4 string `json:"secondary_ipv4" required:"true"` + ServiceType string `json:"service_type" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + VpnGwID string `json:"vpn_gw_id,omitempty"` + VRID int `json:"vrid" required:"true"` +} + +func (opts CreateOpts) ToGatewayInterfaceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "gw_interface") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToGatewayInterfaceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToGatewayInterfaceUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` +} + +func (opts UpdateOpts) ToGatewayInterfaceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "gw_interface") +} + +func Update(c *eclcloud.ServiceClient, gatewayInterfaceID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToGatewayInterfaceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, gatewayInterfaceID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, gatewayInterfaceID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, gatewayInterfaceID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractGatewayInterfaces(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "gw_interface"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "gw_interface"} + } +} diff --git a/v4/ecl/network/v2/gateway_interfaces/results.go b/v4/ecl/network/v2/gateway_interfaces/results.go new file mode 100644 index 0000000..25062a9 --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/results.go @@ -0,0 +1,91 @@ +package gateway_interfaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*GatewayInterface, error) { + var s GatewayInterface + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "gw_interface") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type GatewayInterface struct { + AwsGwID string `json:"aws_gw_id"` + AzureGwID string `json:"azure_gw_id"` + Description string `json:"description"` + FICGatewayID string `json:"fic_gw_id"` + GcpGwID string `json:"gcp_gw_id"` + GwVipv4 string `json:"gw_vipv4"` + GwVipv6 string `json:"gw_vipv6"` + ID string `json:"id"` + InterdcGwID string `json:"interdc_gw_id"` + InternetGwID string `json:"internet_gw_id"` + Name string `json:"name"` + Netmask int `json:"netmask"` + NetworkID string `json:"network_id"` + PrimaryIpv4 string `json:"primary_ipv4"` + PrimaryIpv6 string `json:"primary_ipv6"` + SecondaryIpv4 string `json:"secondary_ipv4"` + SecondaryIpv6 string `json:"secondary_ipv6"` + ServiceType string `json:"service_type"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + VpnGwID string `json:"vpn_gw_id"` + VRID int `json:"vrid"` +} + +type GatewayInterfacePage struct { + pagination.LinkedPageBase +} + +func (r GatewayInterfacePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"gw_interfaces_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r GatewayInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractGatewayInterfaces(r) + return len(is) == 0, err +} + +func ExtractGatewayInterfaces(r pagination.Page) ([]GatewayInterface, error) { + var s []GatewayInterface + err := ExtractGatewayInterfacesInto(r, &s) + return s, err +} + +func ExtractGatewayInterfacesInto(r pagination.Page, v interface{}) error { + return r.(GatewayInterfacePage).Result.ExtractIntoSlicePtr(v, "gw_interfaces") +} diff --git a/v4/ecl/network/v2/gateway_interfaces/testing/docs.go b/v4/ecl/network/v2/gateway_interfaces/testing/docs.go new file mode 100644 index 0000000..37028c4 --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/testing/docs.go @@ -0,0 +1,2 @@ +// gateway_interfaces unit tests +package testing diff --git a/v4/ecl/network/v2/gateway_interfaces/testing/fixtures.go b/v4/ecl/network/v2/gateway_interfaces/testing/fixtures.go new file mode 100644 index 0000000..9284dc1 --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/testing/fixtures.go @@ -0,0 +1,202 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/gateway_interfaces" +) + +const ListResponse = ` +{ + "gw_interfaces": [ + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_CREATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + }, + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "lab3-test-user-fic-gateway-interface, role : member", + "fic_gw_id": "dd04adc4-459f-4fc4-83a5-47436c6aece5", + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.1", + "gw_vipv6": null, + "id": "165ed64c-b9d4-46b1-afc1-cbbdc356ddcb", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "lab3-hara-cfg-20151204", + "netmask": 29, + "network_id": "cce5c9a1-1ec3-40b1-bfc7-634bb914646b", + "primary_ipv4": "100.127.254.3", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.4", + "secondary_ipv6": null, + "service_type": "fic", + "status": "ACTIVE", + "tenant_id": "fe1f6fb95b0e48ba8c59be2121a58adc", + "vpn_gw_id": null, + "vrid": 10 + } + ] +}` + +const GetResponse = ` +{ + "gw_interface": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_CREATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + } +}` + +const CreateRequest = ` +{ + "gw_interface": { + "description": "", + "gw_vipv4": "100.127.254.49", + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "secondary_ipv4": "100.127.254.54", + "service_type": "internet", + "vrid": 1 + } +} +` + +const CreateResponse = ` +{ + "gw_interface": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "5_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_CREATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + } +}` + +const UpdateRequest = ` +{ + "gw_interface": { + "description": "Updated", + "name": "6_Gateway" + } +}` + +const UpdateResponse = ` +{ + "gw_interface": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "Updated", + "fic_gw_id": null, + "gcp_gw_id": null, + "gw_vipv4": "100.127.254.49", + "gw_vipv6": null, + "id": "09771fbb-6496-4ae1-9b53-226b6edcc1be", + "interdc_gw_id": null, + "internet_gw_id": "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + "name": "6_Gateway", + "netmask": 29, + "network_id": "0200a550-82cf-4d6d-b564-a87eb63e2b75", + "primary_ipv4": "100.127.254.53", + "primary_ipv6": null, + "secondary_ipv4": "100.127.254.54", + "secondary_ipv6": null, + "service_type": "internet", + "status": "PENDING_UPDATE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9", + "vpn_gw_id": null, + "vrid": 1 + } +}` + +var GatewayInterface1 = gateway_interfaces.GatewayInterface{ + Description: "", + GwVipv4: "100.127.254.49", + ID: "09771fbb-6496-4ae1-9b53-226b6edcc1be", + InternetGwID: "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + Name: "5_Gateway", + Netmask: 29, + NetworkID: "0200a550-82cf-4d6d-b564-a87eb63e2b75", + PrimaryIpv4: "100.127.254.53", + SecondaryIpv4: "100.127.254.54", + ServiceType: "internet", + Status: "PENDING_CREATE", + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", + VRID: 1, +} + +var GatewayInterface2 = gateway_interfaces.GatewayInterface{ + Description: "lab3-test-user-fic-gateway-interface, role : member", + FICGatewayID: "dd04adc4-459f-4fc4-83a5-47436c6aece5", + GwVipv4: "100.127.254.1", + ID: "165ed64c-b9d4-46b1-afc1-cbbdc356ddcb", + Name: "lab3-hara-cfg-20151204", + Netmask: 29, + NetworkID: "cce5c9a1-1ec3-40b1-bfc7-634bb914646b", + PrimaryIpv4: "100.127.254.3", + SecondaryIpv4: "100.127.254.4", + ServiceType: "fic", + Status: "ACTIVE", + TenantID: "fe1f6fb95b0e48ba8c59be2121a58adc", + VRID: 10, +} + +var ExpectedGatewayInterfaceSlice = []gateway_interfaces.GatewayInterface{GatewayInterface1, GatewayInterface2} diff --git a/v4/ecl/network/v2/gateway_interfaces/testing/request_test.go b/v4/ecl/network/v2/gateway_interfaces/testing/request_test.go new file mode 100644 index 0000000..0000bfc --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/testing/request_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/gateway_interfaces" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListGatewayInterfaces(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := gateway_interfaces.List(client, gateway_interfaces.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := gateway_interfaces.ExtractGatewayInterfaces(page) + if err != nil { + t.Errorf("Failed to extract gateway interfaces: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedGatewayInterfaceSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces/09771fbb-6496-4ae1-9b53-226b6edcc1be", 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, GetResponse) + }) + + i, err := gateway_interfaces.Get(fake.ServiceClient(), "09771fbb-6496-4ae1-9b53-226b6edcc1be").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GatewayInterface1, i) +} + +func TestCreateGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := gateway_interfaces.CreateOpts{ + Description: "", + GwVipv4: "100.127.254.49", + InternetGwID: "e72ef35a-c96f-45f8-aeee-e7547c5b94b3", + Name: "5_Gateway", + Netmask: 29, + NetworkID: "0200a550-82cf-4d6d-b564-a87eb63e2b75", + PrimaryIpv4: "100.127.254.53", + SecondaryIpv4: "100.127.254.54", + ServiceType: "internet", + VRID: 1, + } + i, err := gateway_interfaces.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &GatewayInterface1, i) +} + +func TestUpdateGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces/09771fbb-6496-4ae1-9b53-226b6edcc1be", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "Updated" + name := "6_Gateway" + options := gateway_interfaces.UpdateOpts{ + Description: &description, + Name: &name, + } + i, err := gateway_interfaces.Update(fake.ServiceClient(), "09771fbb-6496-4ae1-9b53-226b6edcc1be", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "6_Gateway") + th.AssertEquals(t, i.Description, "Updated") + th.AssertEquals(t, i.ID, "09771fbb-6496-4ae1-9b53-226b6edcc1be") +} + +func TestDeleteGatewayInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/gw_interfaces/09771fbb-6496-4ae1-9b53-226b6edcc1be", 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 := gateway_interfaces.Delete(fake.ServiceClient(), "09771fbb-6496-4ae1-9b53-226b6edcc1be") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/gateway_interfaces/urls.go b/v4/ecl/network/v2/gateway_interfaces/urls.go new file mode 100644 index 0000000..f6e4e27 --- /dev/null +++ b/v4/ecl/network/v2/gateway_interfaces/urls.go @@ -0,0 +1,31 @@ +package gateway_interfaces + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("gw_interfaces", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("gw_interfaces") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/internet_gateways/doc.go b/v4/ecl/network/v2/internet_gateways/doc.go new file mode 100644 index 0000000..9be4fab --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/doc.go @@ -0,0 +1 @@ +package internet_gateways diff --git a/v4/ecl/network/v2/internet_gateways/requests.go b/v4/ecl/network/v2/internet_gateways/requests.go new file mode 100644 index 0000000..d16815e --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/requests.go @@ -0,0 +1,136 @@ +package internet_gateways + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type ListOptsBuilder interface { + ToInternetGatewayListQuery() (string, error) +} + +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + InternetServiceID string `q:"internet_service_id"` + Name string `q:"name"` + QoSOptionID string `q:"qos_option_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +func (opts ListOpts) ToInternetGatewayListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToInternetGatewayListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return InternetGatewayPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, internetGatewayID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, internetGatewayID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToInternetGatewayCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + Description string `json:"description,omitempty"` + InternetServiceID string `json:"internet_service_id" required:"true"` + Name string `json:"name,omitempty"` + QoSOptionID string `json:"qos_option_id" required:"true"` + TenantID string `json:"tenant_id,omitempty"` +} + +func (opts CreateOpts) ToInternetGatewayCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "internet_gateway") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToInternetGatewayCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToInternetGatewayUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + QoSOptionID *string `json:"qos_option_id,omitempty"` +} + +func (opts UpdateOpts) ToInternetGatewayUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "internet_gateway") +} + +func Update(c *eclcloud.ServiceClient, internetGatewayID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToInternetGatewayUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, internetGatewayID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, internetGatewayID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, internetGatewayID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractInternetGateways(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "internet_gateway"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "internet_gateway"} + } +} diff --git a/v4/ecl/network/v2/internet_gateways/results.go b/v4/ecl/network/v2/internet_gateways/results.go new file mode 100644 index 0000000..2ed9b2f --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/results.go @@ -0,0 +1,76 @@ +package internet_gateways + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*InternetGateway, error) { + var s InternetGateway + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "internet_gateway") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type InternetGateway struct { + ID string `json:"id"` + Description string `json:"description"` + InternetServiceID string `json:"internet_service_id"` + Name string `json:"name"` + QoSOptionID string `json:"qos_option_id"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` +} + +type InternetGatewayPage struct { + pagination.LinkedPageBase +} + +func (r InternetGatewayPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"internet_gateways_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r InternetGatewayPage) IsEmpty() (bool, error) { + is, err := ExtractInternetGateways(r) + return len(is) == 0, err +} + +func ExtractInternetGateways(r pagination.Page) ([]InternetGateway, error) { + var s []InternetGateway + err := ExtractInternetGatewaysInto(r, &s) + return s, err +} + +func ExtractInternetGatewaysInto(r pagination.Page, v interface{}) error { + return r.(InternetGatewayPage).Result.ExtractIntoSlicePtr(v, "internet_gateways") +} diff --git a/v4/ecl/network/v2/internet_gateways/testing/doc.go b/v4/ecl/network/v2/internet_gateways/testing/doc.go new file mode 100644 index 0000000..79517b8 --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/testing/doc.go @@ -0,0 +1,2 @@ +// internet_gateways unit tests +package testing diff --git a/v4/ecl/network/v2/internet_gateways/testing/fixtures.go b/v4/ecl/network/v2/internet_gateways/testing/fixtures.go new file mode 100644 index 0000000..52e2dcf --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/testing/fixtures.go @@ -0,0 +1,110 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/internet_gateways" +) + +const ListResponse = ` +{ + "internet_gateways": [ + { + "description": "test", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_CREATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + }, + { + "description": "", + "id": "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "6_performance", + "qos_option_id": "be985a60-e918-4cca-98f1-8886333f6f5e", + "status": "ACTIVE", + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } + ] +}` + +const GetResponse = `{ + "internet_gateway": { + "description": "test", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_CREATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } +}` + +const CreateRequest = ` +{ + "internet_gateway": { + "description": "test", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } +} +` + +const CreateResponse = ` +{ + "internet_gateway": { + "description": "test", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_CREATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } + }` + +const UpdateRequest = ` + { + "internet_gateway": { + "description": "test2", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40" + } +}` + +const UpdateResponse = ` +{ + "internet_gateway": { + "description": "test2", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "internet_service_id": "5536154d-9a00-4b11-81fb-b185c9111d90", + "name": "Lab3-Internet-Service-Provider-01", + "qos_option_id": "e497bbc3-1127-4490-a51d-93582c40ab40", + "status": "PENDING_UPDATE", + "tenant_id": "6c0bdafab1914ab2b2b6c415477defc7" + } +}` + +var InternetGateway1 = internet_gateways.InternetGateway{ + Description: "test", + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + InternetServiceID: "5536154d-9a00-4b11-81fb-b185c9111d90", + Name: "Lab3-Internet-Service-Provider-01", + QoSOptionID: "e497bbc3-1127-4490-a51d-93582c40ab40", + Status: "PENDING_CREATE", + TenantID: "6c0bdafab1914ab2b2b6c415477defc7", +} + +var InternetGateway2 = internet_gateways.InternetGateway{ + Description: "", + ID: "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + InternetServiceID: "5536154d-9a00-4b11-81fb-b185c9111d90", + Name: "6_performance", + QoSOptionID: "be985a60-e918-4cca-98f1-8886333f6f5e", + Status: "ACTIVE", + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", +} + +var ExpectedInternetGatewaySlice = []internet_gateways.InternetGateway{InternetGateway1, InternetGateway2} diff --git a/v4/ecl/network/v2/internet_gateways/testing/request_test.go b/v4/ecl/network/v2/internet_gateways/testing/request_test.go new file mode 100644 index 0000000..d873cd4 --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/testing/request_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/internet_gateways" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways", 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.Fprint(w, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := internet_gateways.List(client, internet_gateways.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := internet_gateways.ExtractInternetGateways(page) + if err != nil { + t.Errorf("Failed to extract internet gateways: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedInternetGatewaySlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + 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/internet_gateways/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.Fprint(w, GetResponse) + }) + + i, err := internet_gateways.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &InternetGateway1, i) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprint(w, CreateResponse) + }) + + options := internet_gateways.CreateOpts{ + Name: "Lab3-Internet-Service-Provider-01", + TenantID: "6c0bdafab1914ab2b2b6c415477defc7", + Description: "test", + InternetServiceID: "5536154d-9a00-4b11-81fb-b185c9111d90", + QoSOptionID: "e497bbc3-1127-4490-a51d-93582c40ab40", + } + i, err := internet_gateways.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Status, "PENDING_CREATE") + th.AssertDeepEquals(t, &InternetGateway1, i) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways/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) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, UpdateResponse) + }) + + name := "Lab3-Internet-Service-Provider-01" + description := "test2" + qosOptionId := "e497bbc3-1127-4490-a51d-93582c40ab40" + options := internet_gateways.UpdateOpts{ + Name: &name, + Description: &description, + QoSOptionID: &qosOptionId, + } + i, err := internet_gateways.Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "Lab3-Internet-Service-Provider-01") + th.AssertEquals(t, i.Description, "test2") + th.AssertEquals(t, i.QoSOptionID, "e497bbc3-1127-4490-a51d-93582c40ab40") + th.AssertEquals(t, i.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_gateways/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) + }) + + res := internet_gateways.Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/internet_gateways/urls.go b/v4/ecl/network/v2/internet_gateways/urls.go new file mode 100644 index 0000000..ec2fe84 --- /dev/null +++ b/v4/ecl/network/v2/internet_gateways/urls.go @@ -0,0 +1,31 @@ +package internet_gateways + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("internet_gateways", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("internet_gateways") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/internet_services/doc.go b/v4/ecl/network/v2/internet_services/doc.go new file mode 100644 index 0000000..1d5dd38 --- /dev/null +++ b/v4/ecl/network/v2/internet_services/doc.go @@ -0,0 +1 @@ +package internet_services diff --git a/v4/ecl/network/v2/internet_services/requests.go b/v4/ecl/network/v2/internet_services/requests.go new file mode 100644 index 0000000..dbf6741 --- /dev/null +++ b/v4/ecl/network/v2/internet_services/requests.go @@ -0,0 +1,42 @@ +package internet_services + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type ListOptsBuilder interface { + ToInternetServiceListQuery() (string, error) +} + +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + MinimalSubmaskLength int `q:"minimal_submask_length"` + Name string `q:"name"` + Zone string `q:"zone"` +} + +func (opts ListOpts) ToInternetServiceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToInternetServiceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return InternetServicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, internetServiceID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, internetServiceID), &r.Body, nil) + return +} diff --git a/v4/ecl/network/v2/internet_services/results.go b/v4/ecl/network/v2/internet_services/results.go new file mode 100644 index 0000000..3464e80 --- /dev/null +++ b/v4/ecl/network/v2/internet_services/results.go @@ -0,0 +1,62 @@ +package internet_services + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*InternetService, error) { + var s InternetService + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "internet_service") +} + +type GetResult struct { + commonResult +} + +type InternetService struct { + Description string `json:"description"` + ID string `json:"id"` + MinimalSubmaskLength int `json:"minimal_submask_length"` + Name string `json:"name"` + Zone string `json:"zone"` +} + +type InternetServicePage struct { + pagination.LinkedPageBase +} + +func (r InternetServicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"internet_services_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r InternetServicePage) IsEmpty() (bool, error) { + is, err := ExtractInternetServices(r) + return len(is) == 0, err +} + +func ExtractInternetServices(r pagination.Page) ([]InternetService, error) { + var s []InternetService + err := ExtractInternetServicesInto(r, &s) + return s, err +} + +func ExtractInternetServicesInto(r pagination.Page, v interface{}) error { + return r.(InternetServicePage).Result.ExtractIntoSlicePtr(v, "internet_services") +} diff --git a/v4/ecl/network/v2/internet_services/testing/doc.go b/v4/ecl/network/v2/internet_services/testing/doc.go new file mode 100644 index 0000000..7603f83 --- /dev/null +++ b/v4/ecl/network/v2/internet_services/testing/doc.go @@ -0,0 +1 @@ +package testing diff --git a/v4/ecl/network/v2/internet_services/testing/fixtures.go b/v4/ecl/network/v2/internet_services/testing/fixtures.go new file mode 100644 index 0000000..ea277bc --- /dev/null +++ b/v4/ecl/network/v2/internet_services/testing/fixtures.go @@ -0,0 +1,53 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/internet_services" +) + +const ListResponse = ` +{ + "internet_services": [ + { + "description": "Example internet_service 1 description", + "id": "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", + "minimal_submask_length": 26, + "name": "Internet-Service-01", + "zone": "jp1-zone1" + }, + { + "description": "Example internet_service 2 description.", + "id": "5d6eaf32-8c42-4187-973b-dcee142dcb9d", + "minimal_submask_length": 26, + "name": "Internet-Service-01", + "zone": "jp2-zone1" + } + ] +}` + +const GetResponse = `{ + "internet_service": { + "description": "Example internet_service 1 description", + "id": "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", + "minimal_submask_length": 26, + "name": "Internet-Service-01", + "zone": "jp1-zone1" + } +}` + +var InternetService1 = internet_services.InternetService{ + Description: "Example internet_service 1 description", + ID: "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", + MinimalSubmaskLength: 26, + Name: "Internet-Service-01", + Zone: "jp1-zone1", +} + +var InternetService2 = internet_services.InternetService{ + Description: "Example internet_service 2 description.", + ID: "5d6eaf32-8c42-4187-973b-dcee142dcb9d", + MinimalSubmaskLength: 26, + Name: "Internet-Service-01", + Zone: "jp2-zone1", +} + +var ExpectedInternetServiceSlice = []internet_services.InternetService{InternetService1, InternetService2} diff --git a/v4/ecl/network/v2/internet_services/testing/request_test.go b/v4/ecl/network/v2/internet_services/testing/request_test.go new file mode 100644 index 0000000..ee96dae --- /dev/null +++ b/v4/ecl/network/v2/internet_services/testing/request_test.go @@ -0,0 +1,71 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/internet_services" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/internet_services", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := internet_services.List(client, internet_services.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := internet_services.ExtractInternetServices(page) + if err != nil { + t.Errorf("Failed to extract internet services: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedInternetServiceSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + 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/internet_services/a7791c79-19b0-4eb6-9a8f-ea739b44e8d5", 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, GetResponse) + }) + + i, err := internet_services.Get(fake.ServiceClient(), "a7791c79-19b0-4eb6-9a8f-ea739b44e8d5").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &InternetService1, i) +} diff --git a/v4/ecl/network/v2/internet_services/urls.go b/v4/ecl/network/v2/internet_services/urls.go new file mode 100644 index 0000000..d0ee222 --- /dev/null +++ b/v4/ecl/network/v2/internet_services/urls.go @@ -0,0 +1,19 @@ +package internet_services + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("internet_services", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("internet_services") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} diff --git a/v4/ecl/network/v2/load_balancer_actions/doc.go b/v4/ecl/network/v2/load_balancer_actions/doc.go new file mode 100644 index 0000000..26b188d --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/doc.go @@ -0,0 +1,34 @@ +/* +Package load_balancer_actions contains functionality for working with +ECL Load Balancer/Actions resources. + +Example to reboot a Load Balancer + + loadBalancerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + + rebootOpts := load_balancer_actions.RebootOpts{ + Type: "HARD", + } + + err := load_balancer_actions.Reboot(networkClient, loadBalancerID, rebootOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to reset password of Load Balancer + + loadBalancerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + + resetPasswordOpts := load_balancer_actions.ResetPasswordOpts{ + Username: "user-read", + } + + resetPasswordResult, err := load_balancer_actions.ResetPassword(networkClient, loadBalancerID, resetPasswordOpts).ExtractResetPassword() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", resetPasswordResult) + +*/ +package load_balancer_actions diff --git a/v4/ecl/network/v2/load_balancer_actions/requests.go b/v4/ecl/network/v2/load_balancer_actions/requests.go new file mode 100644 index 0000000..d278a77 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/requests.go @@ -0,0 +1,67 @@ +package load_balancer_actions + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// RebootOpts represents the attributes used when rebooting a Load Balancer. +type RebootOpts struct { + + // should syslog record acl info + Type string `json:"type" required:"true"` +} + +// ToLoadBalancerActionRebootMap builds a request body from RebootOpts. +func (opts RebootOpts) ToLoadBalancerActionRebootMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Reboot accepts a RebootOpts struct and reboots an existing Load Balancer using the +// values provided. +func Reboot(c *eclcloud.ServiceClient, id string, opts RebootOpts) (r RebootResult) { + b, err := opts.ToLoadBalancerActionRebootMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rebootURL(c, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResetPasswordOpts represents the attributes used when resetting password of load_balancer instance. +type ResetPasswordOpts struct { + + // should syslog record acl info + Username string `json:"username" required:"true"` +} + +// ToLoadBalancerActionResetPasswordMap builds a request body from ResetPasswordOpts. +func (opts ResetPasswordOpts) ToLoadBalancerActionResetPasswordMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// ResetPassword accepts a ResetPasswordOpts struct and resets an existing Load Balancer password using the +// values provided. +func ResetPassword(c *eclcloud.ServiceClient, id string, opts ResetPasswordOpts) (r ResetPasswordResult) { + b, err := opts.ToLoadBalancerActionResetPasswordMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(resetPasswordURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/network/v2/load_balancer_actions/results.go b/v4/ecl/network/v2/load_balancer_actions/results.go new file mode 100644 index 0000000..e028259 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/results.go @@ -0,0 +1,38 @@ +package load_balancer_actions + +import ( + "github.com/nttcom/eclcloud/v4" +) + +type commonResult struct { + eclcloud.Result +} + +// ExtractResetPassword is a function that accepts a result and extracts a result of reset_password. +func (r commonResult) ExtractResetPassword() (*Password, error) { + var s Password + err := r.ExtractInto(&s) + return &s, err +} + +// RebootResult represents the result of a reboot operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RebootResult struct { + eclcloud.ErrResult +} + +// ResetPasswordResult represents the result of a Reset Password operation. Call its ExtractResetPassword +// method to interpret it as an action's result. +type ResetPasswordResult struct { + commonResult +} + +// Password represents a detail of a Reset Password operation. +type Password struct { + + // new password + NewPassword string `json:"new_password"` + + // username + Username string `json:"username"` +} diff --git a/v4/ecl/network/v2/load_balancer_actions/testing/doc.go b/v4/ecl/network/v2/load_balancer_actions/testing/doc.go new file mode 100644 index 0000000..b0c1fa6 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer/Actions unit tests +package testing diff --git a/v4/ecl/network/v2/load_balancer_actions/testing/fixtures.go b/v4/ecl/network/v2/load_balancer_actions/testing/fixtures.go new file mode 100644 index 0000000..27d3de2 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/testing/fixtures.go @@ -0,0 +1,27 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_actions" +) + +const RebootRequest = ` +{ + "type": "HARD" +} +` +const ResetPasswordResponse = ` +{ + "new_password": "ABCDabcd4321", + "username": "user-read" +} +` +const ResetPasswordRequest = ` +{ + "username": "user-read" +} +` + +var ResetPasswordDetail = load_balancer_actions.Password{ + NewPassword: "ABCDabcd4321", + Username: "user-read", +} diff --git a/v4/ecl/network/v2/load_balancer_actions/testing/request_test.go b/v4/ecl/network/v2/load_balancer_actions/testing/request_test.go new file mode 100644 index 0000000..a84f5cb --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/testing/request_test.go @@ -0,0 +1,78 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_actions" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestRebootLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/6e9c7745-61f2-491f-9689-add8c5fc4b9a/reboot", 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, RebootRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + }) + + options := load_balancer_actions.RebootOpts{ + Type: "HARD", + } + res := load_balancer_actions.Reboot(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", options) + th.AssertNoErr(t, res.Err) +} + +func TestRequiredRebootOptsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancer_actions.Reboot(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", load_balancer_actions.RebootOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestResetPasswordLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/6e9c7745-61f2-491f-9689-add8c5fc4b9a/reset_password", 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, ResetPasswordRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ResetPasswordResponse) + }) + + options := load_balancer_actions.ResetPasswordOpts{ + Username: "user-read", + } + s, err := load_balancer_actions.ResetPassword(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", options).ExtractResetPassword() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &ResetPasswordDetail, s) +} + +func TestRequiredResetPasswordOptsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancer_actions.ResetPassword(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", load_balancer_actions.ResetPasswordOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/network/v2/load_balancer_actions/urls.go b/v4/ecl/network/v2/load_balancer_actions/urls.go new file mode 100644 index 0000000..070712a --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_actions/urls.go @@ -0,0 +1,11 @@ +package load_balancer_actions + +import "github.com/nttcom/eclcloud/v4" + +func rebootURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id, "reboot") +} + +func resetPasswordURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id, "reset_password") +} diff --git a/v4/ecl/network/v2/load_balancer_interfaces/doc.go b/v4/ecl/network/v2/load_balancer_interfaces/doc.go new file mode 100644 index 0000000..973f0d8 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/doc.go @@ -0,0 +1,51 @@ +/* +Package load_balancer_interfaces contains functionality for working with +ECL Load Balancer Interface resources. + +Example to List Load Balancer Interfaces + + listOpts := load_balancer_interfaces.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := load_balancer_interfaces.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancerInterfaces, err := load_balancer_interfaces.ExtractLoadBalancerInterfaces(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancerInterface := range allLoadBalancerInterfaces { + fmt.Printf("%+v\n", loadBalancerInterface) + } + + +Example to Show Load Balancer Interface + + loadBalancerInterfaceID := "f44e063c-5fea-45b8-9124-956995eafe2a" + + loadBalancerInterface, err := load_balancer_interfaces.Get(networkClient, loadBalancerInterfaceID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerInterface) + + +Example to Update Load Balancer Interface + + loadBalancerInterfaceID := "f44e063c-5fea-45b8-9124-956995eafe2a" + + updateOpts := load_balancer_interfaces.UpdateOpts{ + Name: "new_name", + } + + loadBalancerInterface, err := load_balancer_interfaces.Update(networkClient, loadBalancerInterfaceID, updateOpts).Extract() + if err != nil { + panic(err) + } +*/ +package load_balancer_interfaces diff --git a/v4/ecl/network/v2/load_balancer_interfaces/requests.go b/v4/ecl/network/v2/load_balancer_interfaces/requests.go new file mode 100644 index 0000000..61a625f --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/requests.go @@ -0,0 +1,138 @@ +package load_balancer_interfaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/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 Load Balancer Interface attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer Interface attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + IPAddress string `q:"ip_address"` + LoadBalancerID string `q:"load_balancer_id"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + SlotNumber int `q:"slot_number"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + VirtualIPAddress string `q:"virtual_ip_address"` +} + +// ToLoadBalancerInterfacesListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerInterfacesListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancer Interfaces. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancer Interfaces that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancerInterfacesListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer Interface based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// UpdateOpts represents the attributes used when updating an existing Load Balancer Interface. +type UpdateOpts struct { + + // Description is description + Description *string `json:"description,omitempty"` + + // IP Address + IPAddress string `json:"ip_address,omitempty"` + + // Name of the Load Balancer Interface + Name *string `json:"name,omitempty"` + + // UUID of the parent network. + NetworkID *interface{} `json:"network_id,omitempty"` + + // Virtual IP Address + VirtualIPAddress *interface{} `json:"virtual_ip_address,omitempty"` + + // Properties used for virtual IP address + VirtualIPProperties *VirtualIPProperties `json:"virtual_ip_properties,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerInterfaceUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer_interface") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing Load Balancer Interface using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerInterfaceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convenience function that returns a Load Balancer Interface's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancerInterfaces(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer_interface"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer_interface"} + } +} diff --git a/v4/ecl/network/v2/load_balancer_interfaces/results.go b/v4/ecl/network/v2/load_balancer_interfaces/results.go new file mode 100644 index 0000000..9c15962 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/results.go @@ -0,0 +1,101 @@ +package load_balancer_interfaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Load Balancer Interface. +type UpdateResult struct { + commonResult +} + +// Extract is a function that accepts a result and extracts a Load Balancer Interface resource. +func (r commonResult) Extract() (*LoadBalancerInterface, error) { + var s struct { + LoadBalancerInterface *LoadBalancerInterface `json:"load_balancer_interface"` + } + err := r.ExtractInto(&s) + return s.LoadBalancerInterface, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer Interface. +type GetResult struct { + commonResult +} + +// Properties used for virtual IP address +type VirtualIPProperties struct { + Protocol string `json:"protocol"` + Vrid int `json:"vrid"` +} + +// LoadBalancerInterface represents a Load Balancer Interface. See package documentation for a top-level +// description of what this is. +type LoadBalancerInterface struct { + + // Description is description + Description string `json:"description"` + + // UUID representing the Load Balancer Interface. + ID string `json:"id"` + + // IP Address + IPAddress *string `json:"ip_address"` + + // The ID of load_balancer this load_balancer_interface belongs to. + LoadBalancerID string `json:"load_balancer_id"` + + // Name of the Load Balancer Interface + Name string `json:"name"` + + // UUID of the parent network. + NetworkID *string `json:"network_id"` + + // Slot Number + SlotNumber int `json:"slot_number"` + + // Load Balancer Interface status + Status string `json:"status"` + + // Tenant ID of the owner (UUID) + TenantID string `json:"tenant_id"` + + // Load Balancer Interface type + Type string `json:"type"` + + // Virtual IP Address + VirtualIPAddress *string `json:"virtual_ip_address"` + + // Properties used for virtual IP address + VirtualIPProperties *VirtualIPProperties `json:"virtual_ip_properties"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a collection +// of load balancers. +type LoadBalancerInterfacePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerInterfacePage struct is empty. +func (r LoadBalancerInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancerInterfaces(r) + return len(is) == 0, err +} + +// ExtractLoadBalancerInterfaces accepts a Page struct, specifically a LoadBalancerPage struct, +// and extracts the elements into a slice of Load Balancer Interface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancerInterfaces(r pagination.Page) ([]LoadBalancerInterface, error) { + var s struct { + LoadBalancerInterfaces []LoadBalancerInterface `json:"load_balancer_interfaces"` + } + err := (r.(LoadBalancerInterfacePage)).ExtractInto(&s) + return s.LoadBalancerInterfaces, err +} diff --git a/v4/ecl/network/v2/load_balancer_interfaces/testing/doc.go b/v4/ecl/network/v2/load_balancer_interfaces/testing/doc.go new file mode 100644 index 0000000..d644bd6 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer Interfaces unit tests +package testing diff --git a/v4/ecl/network/v2/load_balancer_interfaces/testing/fixtures.go b/v4/ecl/network/v2/load_balancer_interfaces/testing/fixtures.go new file mode 100644 index 0000000..2f4c67a --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/testing/fixtures.go @@ -0,0 +1,183 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_interfaces" +) + +const ListResponse = ` +{ + "load_balancer_interfaces": [ + { + "description": "test1", + "id": "b409f68e-9307-4649-9073-bb3cb776bda5", + "ip_address": "100.64.64.34", + "load_balancer_id": "5a109f4a-ebd8-4998-8410-98629e2bd5cd", + "name": "Interface 1/2", + "network_id": "30b665e3-db2b-473b-a09a-8940148b6491", + "slot_number": 2, + "status": "ACTIVE", + "tenant_id": "8fe1cc29-ff7d4773bced6cb02fc8002f", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "description": "test2", + "id": "0aaef2e9-b4a0-4c31-bd98-496e0a8fed4f", + "ip_address": null, + "load_balancer_id": "12efe0b1-02b6-4e97-ad93-9dc1f7b5c0fc", + "name": "Interface 1/1", + "network_id": null, + "slot_number": 1, + "status": "DOWN", + "tenant_id": "44777b33f0ee474ab1466ebee9fa369f", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ] +} +` +const GetResponse = ` +{ + "load_balancer_interface": { + "description": "test3", + "id": "da3f99e8-a949-40e7-a0e4-4609b705a7c7", + "ip_address": "100.64.64.34", + "load_balancer_id": "79378a5d-bc2f-4a74-ab4b-ceae8693dca5", + "name": "Interface 1/2", + "network_id": "30b665e3-db2b-473b-a09a-8940148b6491", + "slot_number": 2, + "status": "ACTIVE", + "tenant_id": "401c9473a52b4ee486d17ea76f466f66", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + } +} + ` + +const UpdateRequest = ` +{ + "load_balancer_interface": { + "description": "test", + "ip_address": "100.64.64.34", + "name": "Interface 1/2", + "network_id": "e6106a35-d79b-44a3-bda0-6009b2f8775a", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + } +} +` +const UpdateResponse = ` +{ + "load_balancer_interface": { + "description": "test", + "id": "2897f333-3554-4099-a638-64d7022bf9ae", + "ip_address": "100.64.64.34", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "name": "Interface 1/2", + "network_id": "e6106a35-d79b-44a3-bda0-6009b2f8775a", + "slot_number": 2, + "status": "PENDING_UPDATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + } +} +` + +var LoadBalancerInterface1 = load_balancer_interfaces.LoadBalancerInterface{ + Description: "test1", + ID: "b409f68e-9307-4649-9073-bb3cb776bda5", + IPAddress: &DetailIPAddress, + LoadBalancerID: "5a109f4a-ebd8-4998-8410-98629e2bd5cd", + Name: "Interface 1/2", + NetworkID: &DetailNetworkID, + SlotNumber: 2, + Status: "ACTIVE", + TenantID: "8fe1cc29-ff7d4773bced6cb02fc8002f", + VirtualIPAddress: &DetailVirtualIPAddress, + VirtualIPProperties: &load_balancer_interfaces.VirtualIPProperties{ + Protocol: "vrrp", + Vrid: 10, + }, +} + +var DetailIPAddress = "100.64.64.34" +var DetailNetworkID = "30b665e3-db2b-473b-a09a-8940148b6491" +var DetailVirtualIPAddress = "100.64.64.101" + +var LoadBalancerInterface2 = load_balancer_interfaces.LoadBalancerInterface{ + Description: "test2", + ID: "0aaef2e9-b4a0-4c31-bd98-496e0a8fed4f", + LoadBalancerID: "12efe0b1-02b6-4e97-ad93-9dc1f7b5c0fc", + Name: "Interface 1/1", + SlotNumber: 1, + Status: "DOWN", + TenantID: "44777b33f0ee474ab1466ebee9fa369f", +} + +var LoadBalancerInterfaceDetail = load_balancer_interfaces.LoadBalancerInterface{ + Description: "test3", + ID: "da3f99e8-a949-40e7-a0e4-4609b705a7c7", + IPAddress: &DetailIPAddress, + LoadBalancerID: "79378a5d-bc2f-4a74-ab4b-ceae8693dca5", + Name: "Interface 1/2", + NetworkID: &DetailNetworkID, + SlotNumber: 2, + Status: "ACTIVE", + TenantID: "401c9473a52b4ee486d17ea76f466f66", + VirtualIPAddress: &DetailVirtualIPAddress, + VirtualIPProperties: &load_balancer_interfaces.VirtualIPProperties{ + Protocol: "vrrp", + Vrid: 10, + }, +} + +var ExpectedLoadBalancerInterfaceSlice = []load_balancer_interfaces.LoadBalancerInterface{LoadBalancerInterface1, LoadBalancerInterface2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancer_interfaces": [ + { + "description": "test1", + "id": "b409f68e-9307-4649-9073-bb3cb776bda5", + "ip_address": "100.64.64.34", + "load_balancer_id": "5a109f4a-ebd8-4998-8410-98629e2bd5cd", + "name": "Interface 1/2", + "network_id": "30b665e3-db2b-473b-a09a-8940148b6491", + "slot_number": 2, + "status": "ACTIVE", + "tenant_id": "8fe1cc29-ff7d4773bced6cb02fc8002f", + "virtual_ip_address": "100.64.64.101", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "description": "test2", + "id": "0aaef2e9-b4a0-4c31-bd98-496e0a8fed4f", + "ip_address": null, + "load_balancer_id": "12efe0b1-02b6-4e97-ad93-9dc1f7b5c0fc", + "name": "Interface 1/2", + "network_id": null, + "slot_number": 1, + "status": "DOWN", + "tenant_id": "44777b33f0ee474ab1466ebee9fa369f", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ] +} +` diff --git a/v4/ecl/network/v2/load_balancer_interfaces/testing/request_test.go b/v4/ecl/network/v2/load_balancer_interfaces/testing/request_test.go new file mode 100644 index 0000000..1ca3cf0 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/testing/request_test.go @@ -0,0 +1,197 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListLoadBalancerInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancer_interfaces.List(client, load_balancer_interfaces.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancer_interfaces.ExtractLoadBalancerInterfaces(page) + if err != nil { + t.Errorf("Failed to extract Load Balancer Interfaces: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerInterfaceSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancerInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces/5f3cae7c-58a5-4124-b622-9ca3cfbf2525", 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, GetResponse) + }) + + s, err := load_balancer_interfaces.Get(fake.ServiceClient(), "5f3cae7c-58a5-4124-b622-9ca3cfbf2525").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerInterfaceDetail, s) +} + +func TestUpdateLoadBalancerInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "test" + ipAddress := "100.64.64.34" + name := "Interface 1/2" + networkID := interface{}("e6106a35-d79b-44a3-bda0-6009b2f8775a") + virtualIPAddress := interface{}("100.64.64.101") + virtualIPProperties := load_balancer_interfaces.VirtualIPProperties{ + Protocol: "vrrp", + Vrid: 10, + } + + id := "2897f333-3554-4099-a638-64d7022bf9ae" + slotNumber := 2 + + status := "PENDING_UPDATE" + + tenantID := "6a156ddf2ecd497ca786ff2da6df5aa8" + + loadBalancerID := "9f872504-36ab-46af-83ce-a4991c669edd" + + options := load_balancer_interfaces.UpdateOpts{ + Description: &description, + IPAddress: ipAddress, + Name: &name, + NetworkID: &networkID, + VirtualIPAddress: &virtualIPAddress, + VirtualIPProperties: &virtualIPProperties, + } + + s, err := load_balancer_interfaces.Update(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, description, s.Description) + th.CheckEquals(t, id, s.ID) + th.CheckEquals(t, ipAddress, *s.IPAddress) + th.CheckEquals(t, loadBalancerID, s.LoadBalancerID) + th.CheckEquals(t, name, s.Name) + th.CheckEquals(t, networkID, *s.NetworkID) + th.CheckEquals(t, slotNumber, s.SlotNumber) + th.CheckEquals(t, status, s.Status) + th.CheckEquals(t, tenantID, s.TenantID) + th.CheckEquals(t, virtualIPAddress, *s.VirtualIPAddress) + th.CheckDeepEquals(t, virtualIPProperties, *s.VirtualIPProperties) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "b409f68e-9307-4649-9073-bb3cb776bda5" + actualID, err := load_balancer_interfaces.IDFromName(client, "Interface 1/2") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_interfaces.IDFromName(client, "Interface X") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_interfaces", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_interfaces.IDFromName(client, "Interface 1/2") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/network/v2/load_balancer_interfaces/urls.go b/v4/ecl/network/v2/load_balancer_interfaces/urls.go new file mode 100644 index 0000000..5c65db9 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_interfaces/urls.go @@ -0,0 +1,23 @@ +package load_balancer_interfaces + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancer_interfaces", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancer_interfaces") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/load_balancer_plans/doc.go b/v4/ecl/network/v2/load_balancer_plans/doc.go new file mode 100644 index 0000000..f5f38bc --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/doc.go @@ -0,0 +1,37 @@ +/* +Package load_balancer_plans contains functionality for working with +ECL Load Balancer Plan resources. + +Example to List Load Balancer Plans + + listOpts := load_balancer_plans.ListOpts{ + Description: "general", + } + + allPages, err := load_balancer_plans.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancerPlans, err := load_balancer_plans.ExtractLoadBalancerPlans(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancerPlan := range allLoadBalancerPlans { + fmt.Printf("%+v\n", loadBalancerPlan) + } + +Example to Show Load Balancer Plan + + loadBalancerPlanID := "a46eeb5a-bc0a-40fa-b455-e5dc13b1220a" + + loadBalancerPlan, err := load_balancer_plans.Get(networkClient, loadBalancerPlanID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerPlan) + +*/ +package load_balancer_plans diff --git a/v4/ecl/network/v2/load_balancer_plans/requests.go b/v4/ecl/network/v2/load_balancer_plans/requests.go new file mode 100644 index 0000000..5faa6d5 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/requests.go @@ -0,0 +1,88 @@ +package load_balancer_plans + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/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 Load Balancer Plan attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer Plan attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + MaximumSyslogServers int `q:"maximum_syslog_servers"` + Name string `q:"name"` + Vendor string `q:"vendor"` + Version string `q:"version"` +} + +// ToLoadBalancerPlansListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerPlansListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancer Plans. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancer Plans that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancerPlansListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPlanPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer Plan based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// IDFromName is a convenience function that returns a Load Balancer Plan's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancerPlans(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer_plan"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer_plan"} + } +} diff --git a/v4/ecl/network/v2/load_balancer_plans/results.go b/v4/ecl/network/v2/load_balancer_plans/results.go new file mode 100644 index 0000000..c9afa71 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/results.go @@ -0,0 +1,83 @@ +package load_balancer_plans + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Load Balancer Plan resource. +func (r commonResult) Extract() (*LoadBalancerPlan, error) { + var s struct { + LoadBalancerPlan *LoadBalancerPlan `json:"load_balancer_plan"` + } + err := r.ExtractInto(&s) + return s.LoadBalancerPlan, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer Plan. +type GetResult struct { + commonResult +} + +// Model of Load Balancer. +type Model struct { + Edition string `json:"edition"` + Size string `json:"size"` +} + +// LoadBalancerPlan represents a Load Balancer Plan. See package documentation for a top-level +// description of what this is. +type LoadBalancerPlan struct { + + // Description is description + Description string `json:"description"` + + // Is user allowed to create new load balancers with this plan. + Enabled bool `json:"enabled"` + + // UUID representing the Load Balancer Plan. + ID string `json:"id"` + + // Maximum number of syslog servers + MaximumSyslogServers int `json:"maximum_syslog_servers"` + + // Model of load balancer + Model Model `json:"model"` + + // Name of the Load Balancer Plan + Name string `json:"name"` + + // Load Balancer Type + Vendor string `json:"vendor"` + + // Version name + Version string `json:"version"` +} + +// LoadBalancerPlanPage is the page returned by a pager when traversing over a collection +// of load balancer plans. +type LoadBalancerPlanPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerPlanPage struct is empty. +func (r LoadBalancerPlanPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancerPlans(r) + return len(is) == 0, err +} + +// ExtractLoadBalancerPlans accepts a Page struct, specifically a LoadBalancerPage struct, +// and extracts the elements into a slice of Load Balancer Plan structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancerPlans(r pagination.Page) ([]LoadBalancerPlan, error) { + var s struct { + LoadBalancerPlans []LoadBalancerPlan `json:"load_balancer_plans"` + } + err := (r.(LoadBalancerPlanPage)).ExtractInto(&s) + return s.LoadBalancerPlans, err +} diff --git a/v4/ecl/network/v2/load_balancer_plans/testing/doc.go b/v4/ecl/network/v2/load_balancer_plans/testing/doc.go new file mode 100644 index 0000000..6a790dc --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer Plans unit tests +package testing diff --git a/v4/ecl/network/v2/load_balancer_plans/testing/fixtures.go b/v4/ecl/network/v2/load_balancer_plans/testing/fixtures.go new file mode 100644 index 0000000..540bec6 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/testing/fixtures.go @@ -0,0 +1,132 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_plans" +) + +const ListResponse = ` +{ + "load_balancer_plans": [ + { + "description": "Load Balancer Description 1", + "enabled": true, + "id": "58ab4df4-10f2-4fa0-b374-74b06dd648ee", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "50" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + }, + { + "description": "Load Balancer Description 2", + "enabled": false, + "id": "8b0cc5cc-b612-4810-ae45-7d6c5e806b3a", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "1000" + }, + "name": "LB_Plan2", + "vendor": "citrix", + "version": "10.5-57.7" + } + ] +} +` +const GetResponse = ` +{ + "load_balancer_plan": { + "description": "Load Balance Plan Description", + "enabled": true, + "id": "6e5faf0c-9361-4b98-bfc4-670497c9bde3", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "50" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + } +} + ` + +var LoadBalancerPlan1 = load_balancer_plans.LoadBalancerPlan{ + Description: "Load Balancer Description 1", + Enabled: true, + ID: "58ab4df4-10f2-4fa0-b374-74b06dd648ee", + MaximumSyslogServers: 10, + Model: load_balancer_plans.Model{ + Edition: "Standard", + Size: "50", + }, + Name: "LB_Plan1", + Vendor: "citrix", + Version: "10.5-57.7", +} + +var LoadBalancerPlan2 = load_balancer_plans.LoadBalancerPlan{ + Description: "Load Balancer Description 2", + Enabled: false, + ID: "8b0cc5cc-b612-4810-ae45-7d6c5e806b3a", + MaximumSyslogServers: 10, + Model: load_balancer_plans.Model{ + Edition: "Standard", + Size: "1000", + }, + Name: "LB_Plan2", + Vendor: "citrix", + Version: "10.5-57.7", +} + +var LoadBalancerDetail = load_balancer_plans.LoadBalancerPlan{ + Description: "Load Balance Plan Description", + Enabled: true, + ID: "6e5faf0c-9361-4b98-bfc4-670497c9bde3", + MaximumSyslogServers: 10, + Model: load_balancer_plans.Model{ + Edition: "Standard", + Size: "50", + }, + Name: "LB_Plan1", + Vendor: "citrix", + Version: "10.5-57.7", +} + +var ExpectedLoadBalancerPlanSlice = []load_balancer_plans.LoadBalancerPlan{LoadBalancerPlan1, LoadBalancerPlan2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancer_plans": [ + { + "description": "Load Balancer Description 1", + "enabled": true, + "id": "58ab4df4-10f2-4fa0-b374-74b06dd648ee", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "50" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + }, + { + "description": "Load Balancer Description 2", + "enabled": false, + "id": "8b0cc5cc-b612-4810-ae45-7d6c5e806b3a", + "maximum_syslog_servers": 10, + "model": { + "edition": "Standard", + "size": "1000" + }, + "name": "LB_Plan1", + "vendor": "citrix", + "version": "10.5-57.7" + } + ] +} +` diff --git a/v4/ecl/network/v2/load_balancer_plans/testing/request_test.go b/v4/ecl/network/v2/load_balancer_plans/testing/request_test.go new file mode 100644 index 0000000..aa08818 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/testing/request_test.go @@ -0,0 +1,136 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_plans" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListLoadBalancerPlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancer_plans.List(client, load_balancer_plans.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancer_plans.ExtractLoadBalancerPlans(page) + if err != nil { + t.Errorf("Failed to extract Load Balancer Plans: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerPlanSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancerPlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans/5f3cae7c-58a5-4124-b622-9ca3cfbf2525", 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, GetResponse) + }) + + s, err := load_balancer_plans.Get(fake.ServiceClient(), "5f3cae7c-58a5-4124-b622-9ca3cfbf2525").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerDetail, s) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "58ab4df4-10f2-4fa0-b374-74b06dd648ee" + actualID, err := load_balancer_plans.IDFromName(client, "LB_Plan1") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_plans.IDFromName(client, "LB_PlanX") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_plans", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_plans.IDFromName(client, "LB_Plan1") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/network/v2/load_balancer_plans/urls.go b/v4/ecl/network/v2/load_balancer_plans/urls.go new file mode 100644 index 0000000..20f0434 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_plans/urls.go @@ -0,0 +1,19 @@ +package load_balancer_plans + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancer_plans", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancer_plans") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/doc.go b/v4/ecl/network/v2/load_balancer_syslog_servers/doc.go new file mode 100644 index 0000000..623ddd3 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/doc.go @@ -0,0 +1,88 @@ +/* +Package load_balancer_syslog_servers contains functionality for working with +ECL Load Balancer Syslog Server resources. + +Example to List Load Balancer Syslog Servers + + listOpts := load_balancer_syslog_servers.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := load_balancer_syslog_servers.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancerSyslogServers, err := load_balancer_syslog_servers.ExtractLoadBalancerSyslogServers(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancerSyslogServer := range allLoadBalancerSyslogServers { + fmt.Printf("%+v\n", loadBalancerSyslogServer) + } + + +Example to Show Load Balancer Syslog Server + + loadBalancerSyslogServerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + + loadBalancerSyslogServer, err := load_balancer_syslog_servers.Get(networkClient, loadBalancerSyslogServerID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancerSyslogServer) + + +Example to Create a Load Balancer Syslog Server + + priority := 20 + + createOpts := load_balancer_syslog_servers.CreateOpts{ + AclLogging: "DISABLED", + AppflowLogging: "DISABLED", + DateFormat: "MMDDYYYY", + Description: "test", + IPAddress: "120.120.120.30", + LoadBalancerID: "4f6ebc24-f768-485b-99ef-f308063d0209", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Priority: &priority, + TcpLogging: "ALL", + TenantID: "b58531f716614e82a9bf001571c8bb15", + TimeZone: "LOCAL_TIME", + TransportType: "UDP", + UserConfigurableLogMessages: "NO", + } + + loadBalancerSyslogServer, err := load_balancer_syslog_servers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer Syslog Server + + loadBalancerSyslogServerID := "9ab7ab3c-38a6-417c-926b-93772c4eb2f9" + description := "new_description" + + updateOpts := load_balancer_syslog_servers.UpdateOpts{ + Description: &description, + } + + loadBalancerSyslogServer, err := load_balancer_syslog_servers.Update(networkClient, loadBalancerSyslogServerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancer Syslog Server + + loadBalancerSyslogServerID := "13762eaf-9564-4c94-a106-98ece9fa189e" + err := load_balancer_syslog_servers.Delete(networkClient, loadBalancerSyslogServerID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package load_balancer_syslog_servers diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/requests.go b/v4/ecl/network/v2/load_balancer_syslog_servers/requests.go new file mode 100644 index 0000000..98195a1 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/requests.go @@ -0,0 +1,233 @@ +package load_balancer_syslog_servers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/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 Load Balancer Syslog Server attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer Syslog Server attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + IPAddress string `q:"ip_address"` + LoadBalancerID string `q:"load_balancer_id"` + LogFacility string `q:"log_facility"` + LogLevel string `q:"log_level"` + Name string `q:"name"` + PortNumber int `q:"port_number"` + Status string `q:"status"` + TransportType string `q:"transport_type"` +} + +// ToLoadBalancerSyslogServersListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerSyslogServersListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancer Syslog Servers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancer Syslog Servers that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancerSyslogServersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerSyslogServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer Syslog Server based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOpts represents the attributes used when creating a new Load Balancer Syslog Server. +type CreateOpts struct { + + // should syslog record acl info + AclLogging string `json:"acl_logging,omitempty"` + + // should syslog record appflow info + AppflowLogging string `json:"appflow_logging,omitempty"` + + // date format utilized by syslog + DateFormat string `json:"date_format,omitempty"` + + // Description is description + Description string `json:"description,omitempty"` + + // Ip address of syslog server + IPAddress string `json:"ip_address" required:"true"` + + // The ID of load_balancer this load_balancer_syslog_server belongs to. + LoadBalancerID string `json:"load_balancer_id" required:"true"` + + // Log facility for syslog + LogFacility string `json:"log_facility,omitempty"` + + // Log level for syslog + LogLevel string `json:"log_level,omitempty"` + + // Name is a human-readable name of the Load Balancer Syslog Server. + Name string `json:"name" required:"true"` + + // Port number of syslog server + PortNumber int `json:"port_number,omitempty"` + + // priority (0-255) + Priority *int `json:"priority,omitempty"` + + // should syslog record tcp protocol info + TcpLogging string `json:"tcp_logging,omitempty"` + + // The UUID of the project who owns the Load Balancer Syslog Server. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // time zone utilized by syslog + TimeZone string `json:"time_zone,omitempty"` + + // protocol for syslog transport + TransportType string `json:"transport_type,omitempty"` + + // can user configure log messages + UserConfigurableLogMessages string `json:"user_configurable_log_messages,omitempty"` +} + +// ToLoadBalancerSyslogServerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerSyslogServerCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer_syslog_server") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new Load Balancer Syslog Server using the values +// provided. You must remember to provide a valid LoadBalancerPlanID. +func Create(c *eclcloud.ServiceClient, opts CreateOpts) (r CreateResult) { + b, err := opts.ToLoadBalancerSyslogServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOpts represents the attributes used when updating an existing Load Balancer Syslog Server. +type UpdateOpts struct { + + // should syslog record acl info + AclLogging string `json:"acl_logging,omitempty"` + + // should syslog record appflow info + AppflowLogging string `json:"appflow_logging,omitempty"` + + // date format utilized by syslog + DateFormat string `json:"date_format,omitempty"` + + // Description is description + Description *string `json:"description,omitempty"` + + // Log facility for syslog + LogFacility string `json:"log_facility,omitempty"` + + // Log level for syslog + LogLevel string `json:"log_level,omitempty"` + + // priority (0-255) + Priority *int `json:"priority,omitempty"` + + // should syslog record tcp protocol info + TcpLogging string `json:"tcp_logging,omitempty"` + + // time zone utilized by syslog + TimeZone string `json:"time_zone,omitempty"` + + // can user configure log messages + UserConfigurableLogMessages string `json:"user_configurable_log_messages,omitempty"` +} + +// ToLoadBalancerSyslogServerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerSyslogServerUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer_syslog_server") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing Load Balancer Syslog Server using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerSyslogServerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the Load Balancer Syslog Server associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a Load Balancer Syslog Server's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancerSyslogServers(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer_syslog_server"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer_syslog_server"} + } +} diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/results.go b/v4/ecl/network/v2/load_balancer_syslog_servers/results.go new file mode 100644 index 0000000..498ae39 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/results.go @@ -0,0 +1,125 @@ +package load_balancer_syslog_servers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Load Balancer Syslog Server resource. +func (r commonResult) Extract() (*LoadBalancerSyslogServer, error) { + var s struct { + LoadBalancerSyslogServer *LoadBalancerSyslogServer `json:"load_balancer_syslog_server"` + } + err := r.ExtractInto(&s) + return s.LoadBalancerSyslogServer, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Load Balancer Syslog Server. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer Syslog Server. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Load Balancer Syslog Server. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// LoadBalancerSyslogServer represents a Load Balancer Syslog Server. See package documentation for a top-level +// description of what this is. +type LoadBalancerSyslogServer struct { + + // should syslog record acl info + AclLogging string `json:"acl_logging"` + + // should syslog record appflow info + AppflowLogging string `json:"appflow_logging"` + + // date format utilized by syslog + DateFormat string `json:"date_format"` + + // Description is description + Description string `json:"description"` + + // UUID representing the Load Balancer Syslog Server. + ID string `json:"id"` + + // Ip address of syslog server + IPAddress string `json:"ip_address"` + + // The ID of load_balancer this load_balancer_syslog_server belongs to. + LoadBalancerID string `json:"load_balancer_id"` + + // Log facility for syslog + LogFacility string `json:"log_facility"` + + // Log level for syslog + LogLevel string `json:"log_level"` + + // Name of the syslog resource + Name string `json:"name"` + + // Port number of syslog server + PortNumber int `json:"port_number"` + + // priority (0-255) + Priority int `json:"priority"` + + // Load balancer syslog server status + Status string `json:"status"` + + // should syslog record tcp protocol info + TcpLogging string `json:"tcp_logging"` + + // TenantID is the project owner of the Load Balancer Syslog Server. + TenantID string `json:"tenant_id"` + + // time zone utilized by syslog + TimeZone string `json:"time_zone"` + + // protocol for syslog transport + TransportType string `json:"transport_type"` + + // can user configure log messages + UserConfigurableLogMessages string `json:"user_configurable_log_messages"` +} + +// LoadBalancerSyslogServerPage is the page returned by a pager when traversing over a collection +// of load balancer Syslog Servers. +type LoadBalancerSyslogServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerSyslogServerPage struct is empty. +func (r LoadBalancerSyslogServerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancerSyslogServers(r) + return len(is) == 0, err +} + +// ExtractLoadBalancerSyslogServers accepts a Page struct, specifically a LoadBalancerSyslogServerPage struct, +// and extracts the elements into a slice of Load Balancer Syslog Server structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancerSyslogServers(r pagination.Page) ([]LoadBalancerSyslogServer, error) { + var s struct { + LoadBalancerSyslogServers []LoadBalancerSyslogServer `json:"load_balancer_syslog_servers"` + } + err := (r.(LoadBalancerSyslogServerPage)).ExtractInto(&s) + return s.LoadBalancerSyslogServers, err +} diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/testing/doc.go b/v4/ecl/network/v2/load_balancer_syslog_servers/testing/doc.go new file mode 100644 index 0000000..1593aa9 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancer Syslog Servers unit tests +package testing diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/testing/fixtures.go b/v4/ecl/network/v2/load_balancer_syslog_servers/testing/fixtures.go new file mode 100644 index 0000000..6c638e3 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/testing/fixtures.go @@ -0,0 +1,226 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_syslog_servers" +) + +const ListResponse = ` +{ + "load_balancer_syslog_servers": [ + { + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + }, + { + "description": "My second backup server", + "id": "c7de2dee-73a0-4a9b-acdf-8a348c242a30", + "ip_address": "120.120.122.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL2", + "log_level": "ERROR", + "name": "second_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + } + ] +} +` +const GetResponse = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "status": "ACTIVE", + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const CreateResponse = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "status": "ACTIVE", + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const CreateRequest = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const UpdateResponse = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test2", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "priority": 20, + "status": "PENDING_UPDATE", + "tcp_logging": "ALL", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "time_zone": "LOCAL_TIME", + "transport_type": "UDP", + "user_configurable_log_messages": "NO" + } +} +` +const UpdateRequest = ` +{ + "load_balancer_syslog_server": { + "acl_logging": "DISABLED", + "appflow_logging": "DISABLED", + "date_format": "MMDDYYYY", + "description": "test2", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "priority": 20, + "tcp_logging": "ALL", + "time_zone": "LOCAL_TIME", + "user_configurable_log_messages": "NO" + } +} +` + +var LoadBalancerSyslogServer1 = load_balancer_syslog_servers.LoadBalancerSyslogServer{ + Description: "test", + ID: "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + IPAddress: "120.120.120.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Status: "ACTIVE", + TransportType: "UDP", +} + +var LoadBalancerSyslogServer2 = load_balancer_syslog_servers.LoadBalancerSyslogServer{ + Description: "My second backup server", + ID: "c7de2dee-73a0-4a9b-acdf-8a348c242a30", + IPAddress: "120.120.122.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL2", + LogLevel: "ERROR", + Name: "second_syslog_server", + PortNumber: 514, + Status: "ACTIVE", + TransportType: "UDP", +} + +var LoadBalancerSyslogServerDetail = load_balancer_syslog_servers.LoadBalancerSyslogServer{ + AclLogging: "DISABLED", + AppflowLogging: "DISABLED", + DateFormat: "MMDDYYYY", + Description: "test", + ID: "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + IPAddress: "120.120.120.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Priority: 20, + Status: "ACTIVE", + TcpLogging: "ALL", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + TimeZone: "LOCAL_TIME", + TransportType: "UDP", + UserConfigurableLogMessages: "NO", +} + +var ExpectedLoadBalancerSlice = []load_balancer_syslog_servers.LoadBalancerSyslogServer{LoadBalancerSyslogServer1, LoadBalancerSyslogServer2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancer_syslog_servers": [ + { + "description": "test", + "id": "6e9c7745-61f2-491f-9689-add8c5fc4b9a", + "ip_address": "120.120.120.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL3", + "log_level": "DEBUG", + "name": "first_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + }, + { + "description": "My second backup server", + "id": "c7de2dee-73a0-4a9b-acdf-8a348c242a30", + "ip_address": "120.120.122.30", + "load_balancer_id": "9f872504-36ab-46af-83ce-a4991c669edd", + "log_facility": "LOCAL2", + "log_level": "ERROR", + "name": "first_syslog_server", + "port_number": 514, + "status": "ACTIVE", + "transport_type": "UDP" + } + ] +} +` diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/testing/request_test.go b/v4/ecl/network/v2/load_balancer_syslog_servers/testing/request_test.go new file mode 100644 index 0000000..f2f81e0 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/testing/request_test.go @@ -0,0 +1,276 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancer_syslog_servers.List(client, load_balancer_syslog_servers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancer_syslog_servers.ExtractLoadBalancerSyslogServers(page) + if err != nil { + t.Errorf("Failed to extract Load Balancer Syslog Servers: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers/6e9c7745-61f2-491f-9689-add8c5fc4b9a", 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, GetResponse) + }) + + s, err := load_balancer_syslog_servers.Get(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerSyslogServerDetail, s) +} + +func TestCreateLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + priority := 20 + + options := load_balancer_syslog_servers.CreateOpts{ + AclLogging: "DISABLED", + AppflowLogging: "DISABLED", + DateFormat: "MMDDYYYY", + Description: "test", + IPAddress: "120.120.120.30", + LoadBalancerID: "9f872504-36ab-46af-83ce-a4991c669edd", + LogFacility: "LOCAL3", + LogLevel: "DEBUG", + Name: "first_syslog_server", + PortNumber: 514, + Priority: &priority, + TcpLogging: "ALL", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + TimeZone: "LOCAL_TIME", + TransportType: "UDP", + UserConfigurableLogMessages: "NO", + } + s, err := load_balancer_syslog_servers.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &LoadBalancerSyslogServerDetail, s) +} + +func TestRequiredCreateOptsLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancer_syslog_servers.Create(fake.ServiceClient(), load_balancer_syslog_servers.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers/6e9c7745-61f2-491f-9689-add8c5fc4b9a", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + aclLogging := "DISABLED" + appflowLogging := "DISABLED" + dateFormat := "MMDDYYYY" + description := "test2" + logFacility := "LOCAL3" + logLevel := "DEBUG" + priority := 20 + tcpLogging := "ALL" + timeZone := "LOCAL_TIME" + userConfigurableLogMessages := "NO" + + id := "6e9c7745-61f2-491f-9689-add8c5fc4b9a" + + ipAddress := "120.120.120.30" + loadBalancerID := "9f872504-36ab-46af-83ce-a4991c669edd" + name := "first_syslog_server" + portNumber := 514 + status := "PENDING_UPDATE" + tenantID := "6a156ddf2ecd497ca786ff2da6df5aa8" + transportType := "UDP" + + options := load_balancer_syslog_servers.UpdateOpts{ + AclLogging: aclLogging, + AppflowLogging: appflowLogging, + DateFormat: dateFormat, + Description: &description, + LogFacility: logFacility, + LogLevel: logLevel, + Priority: &priority, + TcpLogging: tcpLogging, + TimeZone: timeZone, + UserConfigurableLogMessages: userConfigurableLogMessages, + } + + s, err := load_balancer_syslog_servers.Update(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, aclLogging, s.AclLogging) + th.CheckEquals(t, appflowLogging, s.AppflowLogging) + th.CheckEquals(t, dateFormat, s.DateFormat) + th.CheckEquals(t, description, s.Description) + th.CheckEquals(t, id, s.ID) + th.CheckEquals(t, logFacility, s.LogFacility) + th.CheckEquals(t, logLevel, s.LogLevel) + th.CheckEquals(t, priority, s.Priority) + th.CheckEquals(t, tcpLogging, s.TcpLogging) + th.CheckEquals(t, timeZone, s.TimeZone) + th.CheckEquals(t, userConfigurableLogMessages, s.UserConfigurableLogMessages) + th.CheckEquals(t, ipAddress, s.IPAddress) + th.CheckEquals(t, loadBalancerID, s.LoadBalancerID) + th.CheckEquals(t, name, s.Name) + th.CheckEquals(t, portNumber, s.PortNumber) + th.CheckEquals(t, status, s.Status) + th.CheckEquals(t, tenantID, s.TenantID) + th.CheckEquals(t, transportType, s.TransportType) + +} + +func TestDeleteLoadBalancerSyslogServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers/6e9c7745-61f2-491f-9689-add8c5fc4b9a", 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 := load_balancer_syslog_servers.Delete(fake.ServiceClient(), "6e9c7745-61f2-491f-9689-add8c5fc4b9a") + th.AssertNoErr(t, res.Err) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "6e9c7745-61f2-491f-9689-add8c5fc4b9a" + actualID, err := load_balancer_syslog_servers.IDFromName(client, "first_syslog_server") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_syslog_servers.IDFromName(client, "syslog_server X") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancer_syslog_servers", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancer_syslog_servers.IDFromName(client, "first_syslog_server") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/network/v2/load_balancer_syslog_servers/urls.go b/v4/ecl/network/v2/load_balancer_syslog_servers/urls.go new file mode 100644 index 0000000..9b97771 --- /dev/null +++ b/v4/ecl/network/v2/load_balancer_syslog_servers/urls.go @@ -0,0 +1,31 @@ +package load_balancer_syslog_servers + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancer_syslog_servers", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancer_syslog_servers") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/load_balancers/doc.go b/v4/ecl/network/v2/load_balancers/doc.go new file mode 100644 index 0000000..e4b5a81 --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/doc.go @@ -0,0 +1,75 @@ +/* +Package load_balancers contains functionality for working with +ECL Load Balancer resources. + +Example to List Load Balancers + + listOpts := load_balancers.ListOpts{ + Status: "ACTIVE", + } + + allPages, err := load_balancers.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadBalancers, err := load_balancers.ExtractLoadBalancers(allPages) + if err != nil { + panic(err) + } + + for _, loadBalancer := range allLoadBalancers { + fmt.Printf("%+v\n", loadBalancer) + } + + +Example to Show Load Balancer + + loadBalancerID := "f44e063c-5fea-45b8-9124-956995eafe2a" + + loadBalancer, err := load_balancers.Get(networkClient, loadBalancerID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", loadBalancer) + + +Example to Create a Load Balancer + + createOpts := load_balancers.CreateOpts{ + AvailabilityZone: "zone1-groupa", + Description: "Load Balancer 1", + LoadBalancerPlanID: "69bf1e91-73f6-41d5-84c4-91de21a9af05", + Name: "abcdefghijklmnopqrstuvwxyz", + TenantID: "5cc454d62d8c4a0595134b2632bf2263", + } + + loadBalancer, err := load_balancers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer + + loadBalancerID := "f44e063c-5fea-45b8-9124-956995eafe2a" + name := "new_name" + + updateOpts := load_balancers.UpdateOpts{ + Name: &name, + } + + loadBalancer, err := load_balancers.Update(networkClient, loadBalancerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancer + + loadBalancerID := "165fb257-2365-4c05-b368-a7bed21bb927" + err := load_balancers.Delete(networkClient, loadBalancerID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package load_balancers diff --git a/v4/ecl/network/v2/load_balancers/requests.go b/v4/ecl/network/v2/load_balancers/requests.go new file mode 100644 index 0000000..946e49a --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/requests.go @@ -0,0 +1,182 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/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 Load Balancer attributes you want to see returned. SortKey allows you to sort +// by a particular Load Balancer attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + AdminUsername string `q:"admin_username"` + AvailabilityZone string `q:"availability_zone"` + DefaultGateway string `q:"default_gateway"` + Description string `q:"description"` + ID string `q:"id"` + LoadBalancerPlanID string `q:"load_balancer_plan_id"` + Name string `q:"name"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + UserUsername string `q:"user_username"` +} + +// ToLoadBalancersListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancersListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Load Balancers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those Load Balancers that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToLoadBalancersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific Load Balancer based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOpts represents the attributes used when creating a new Load Balancer. +type CreateOpts struct { + + // AvailabilityZone is one of the Virtual Server (Nova)’s availability zone. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Description is description + Description string `json:"description,omitempty"` + + // LoadBalancerPlanID is the UUID of Load Balancer Plan. + LoadBalancerPlanID string `json:"load_balancer_plan_id" required:"true"` + + // Name is a human-readable name of the Load Balancer. + Name string `json:"name,omitempty"` + + // The UUID of the project who owns the Load Balancer. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` +} + +// ToLoadBalancerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new Load Balancer using the values +// provided. You must remember to provide a valid LoadBalancerPlanID. +func Create(c *eclcloud.ServiceClient, opts CreateOpts) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOpts represents the attributes used when updating an existing Load Balancer. +type UpdateOpts struct { + + // Description is description + DefaultGateway *interface{} `json:"default_gateway,omitempty"` + + // Description is description + Description *string `json:"description,omitempty"` + + // LoadBalancerPlanID is the UUID of Load Balancer Plan. + LoadBalancerPlanID string `json:"load_balancer_plan_id,omitempty"` + + // Name is a human-readable name of the Load Balancer. + Name *string `json:"name,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "load_balancer") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing Load Balancer using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the Load Balancer associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a Load Balancer's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractLoadBalancers(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "load_balancer"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "load_balancer"} + } +} diff --git a/v4/ecl/network/v2/load_balancers/results.go b/v4/ecl/network/v2/load_balancers/results.go new file mode 100644 index 0000000..4c409d2 --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/results.go @@ -0,0 +1,115 @@ +package load_balancers + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Load Balancer resource. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"load_balancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Load Balancer. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Load Balancer. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Load Balancer. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// LoadBalancer represents a Load Balancer. See package documentation for a top-level +// description of what this is. +type LoadBalancer struct { + + // AdminPassword is admin's password + AdminPassword string `json:"admin_password"` + + // AdminUsername is admin's username + AdminUsername string `json:"admin_username"` + + // AvailabilityZone is one of the Virtual Server (Nova)’s availability zone. + AvailabilityZone string `json:"availability_zone"` + + // Description is description + DefaultGateway *string `json:"default_gateway"` + + // Description is description + Description string `json:"description"` + + // UUID representing the Load Balancer. + ID string `json:"id"` + + // Attached interfaces by Load Balancer. + Interfaces []load_balancer_interfaces.LoadBalancerInterface `json:"interfaces"` + + // LoadBalancerPlanID is the UUID of Load Balancer Plan. + LoadBalancerPlanID string `json:"load_balancer_plan_id"` + + // Name of the Load Balancer. + Name string `json:"name"` + + // The Load Balancer status. + Status string `json:"status"` + + // Connected syslog servers. + SyslogServers []load_balancer_syslog_servers.LoadBalancerSyslogServer `json:"syslog_servers"` + + // TenantID is the project owner of the Load Balancer. + TenantID string `json:"tenant_id"` + + // User's password placeholder. + UserPassword string `json:"user_password"` + + // User's username placeholder. + UserUsername string `json:"user_username"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a collection +// of load balancers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (r LoadBalancerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancers(r) + return len(is) == 0, err +} + +// ExtractLoadBalancers accepts a Page struct, specifically a LoadBalancerPage struct, +// and extracts the elements into a slice of Load Balancer structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s struct { + LoadBalancers []LoadBalancer `json:"load_balancers"` + } + err := (r.(LoadBalancerPage)).ExtractInto(&s) + return s.LoadBalancers, err +} diff --git a/v4/ecl/network/v2/load_balancers/testing/doc.go b/v4/ecl/network/v2/load_balancers/testing/doc.go new file mode 100644 index 0000000..22e7d91 --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/testing/doc.go @@ -0,0 +1,2 @@ +// Load Balancers unit tests +package testing diff --git a/v4/ecl/network/v2/load_balancers/testing/fixtures.go b/v4/ecl/network/v2/load_balancers/testing/fixtures.go new file mode 100644 index 0000000..d04ea42 --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/testing/fixtures.go @@ -0,0 +1,370 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancers" +) + +const ListResponse = ` +{ + "load_balancers": [ + { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + }, + { + "admin_username": "user-admin", + "availability_zone": "zone1_groupa", + "default_gateway": null, + "description": "abcdefghijklmnopqrstuvwxyz", + "id": "601665cf-c161-4e80-87f0-a3c0925d07a0", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 2", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } + ] +} +` +const GetResponse = ` +{ + "load_balancer": { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "interfaces": [ + { + "id": "ee335c69-b50f-4a32-9d0f-f44cef84a456", + "ip_address": "100.127.253.173", + "name": "Interface 1/1", + "network_id": "c7f88fab-573e-47aa-b0b4-257db28dae23", + "slot_number": 1, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": "100.127.253.174", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "id": "b39b61e4-00b1-4698-aed0-1928beb90abe", + "ip_address": "192.168.110.1", + "name": "Interface 1/2", + "network_id": "1839d290-721c-49ba-99f1-3d7aa37811b0", + "slot_number": 2, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ], + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "syslog_servers": [ + { + "id": "11001101-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.215", + "log_facility": "LOCAL0", + "log_level": "ALERT|INFO|ERROR", + "name": "syslog_server_main", + "port_number": 514, + "status": "ACTIVE" + }, + { + "id": "22002202-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.211", + "log_facility": "LOCAL1", + "log_level": "ERROR", + "name": "syslog_server_backup_fst", + "port_number": 514, + "status": "ACTIVE" + } + ], + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } +} + ` +const CreateResponse = ` +{ + "load_balancer": { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "interfaces": [ + { + "id": "ee335c69-b50f-4a32-9d0f-f44cef84a456", + "ip_address": "100.127.253.173", + "name": "Interface 1/1", + "network_id": "c7f88fab-573e-47aa-b0b4-257db28dae23", + "slot_number": 1, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": "100.127.253.174", + "virtual_ip_properties": { + "protocol": "vrrp", + "vrid": 10 + } + }, + { + "id": "b39b61e4-00b1-4698-aed0-1928beb90abe", + "ip_address": "192.168.110.1", + "name": "Interface 1/2", + "network_id": "1839d290-721c-49ba-99f1-3d7aa37811b0", + "slot_number": 2, + "status": "ACTIVE", + "type": "user", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ], + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "syslog_servers": [ + { + "id": "11001101-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.215", + "log_facility": "LOCAL0", + "log_level": "ALERT|INFO|ERROR", + "name": "syslog_server_main", + "port_number": 514, + "status": "ACTIVE" + }, + { + "id": "22002202-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.211", + "log_facility": "LOCAL1", + "log_level": "ERROR", + "name": "syslog_server_backup_fst", + "port_number": 514, + "status": "ACTIVE" + } + ], + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } +} + ` +const CreateRequest = ` +{ + "load_balancer": { + "availability_zone": "zone1-groupa", + "description": "abcdefghijklmnopqrstuvwxyz", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "abcdefghijklmnopqrstuvwxyz", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` +const UpdateResponse = ` +{ + "load_balancer": { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "UPDATED", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "interfaces": [ + { + "id": "ee335c69-b50f-4a32-9d0f-f44cef84a456", + "ip_address": "100.127.253.173", + "name": "Interface 1/1", + "network_id": "c7f88fab-573e-47aa-b0b4-257db28dae23", + "slot_number": 1, + "status": "ACTIVE", + "virtual_ip_address": null, + "virtual_ip_properties": null + }, + { + "id": "b39b61e4-00b1-4698-aed0-1928beb90abe", + "ip_address": "192.168.110.1", + "name": "Interface 1/2", + "network_id": "1839d290-721c-49ba-99f1-3d7aa37811b0", + "slot_number": 2, + "status": "ACTIVE", + "virtual_ip_address": null, + "virtual_ip_properties": null + } + ], + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "abcdefghijklmnopqrstuvwxyz", + "status": "PENDING_UPDATE", + "syslog_servers": [ + { + "id": "11001101-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.215", + "log_facility": "LOCAL0", + "log_level": "ALERT|INFO|ERROR", + "name": "syslog_server_main", + "port_number": 514, + "status": "ACTIVE" + }, + { + "id": "22002202-2edf-1844-1ff7-12ba5b7e566a", + "ip_address": "177.77.07.211", + "log_facility": "LOCAL1", + "log_level": "ERROR", + "name": "syslog_server_backup_fst", + "port_number": 514, + "status": "ACTIVE" + } + ], + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } +} +` +const UpdateRequest = ` +{ + "load_balancer": { + "default_gateway": "100.127.253.1", + "description": "UPDATED", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "abcdefghijklmnopqrstuvwxyz" + } +} +` + +var LoadBalancer1 = load_balancers.LoadBalancer{ + ID: "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + AdminUsername: "user-admin", + AvailabilityZone: "zone1-groupa", + DefaultGateway: &DetailDefaultGateway, + Description: "Load Balancer 1 Description", + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "Load Balancer 1", + Status: "ACTIVE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + UserUsername: "user-read", +} + +var LoadBalancer2 = load_balancers.LoadBalancer{ + ID: "601665cf-c161-4e80-87f0-a3c0925d07a0", + AdminUsername: "user-admin", + AvailabilityZone: "zone1_groupa", + Description: "abcdefghijklmnopqrstuvwxyz", + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "Load Balancer 2", + Status: "PENDING_CREATE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + UserUsername: "user-read", +} + +var DetailDefaultGateway = "100.127.253.1" +var DetailIPAddress1 = "100.127.253.173" +var DetailNetworkID1 = "c7f88fab-573e-47aa-b0b4-257db28dae23" +var DetailVirtualIPAddress1 = "100.127.253.174" + +var DetailIPAddress2 = "192.168.110.1" +var DetailNetworkID2 = "1839d290-721c-49ba-99f1-3d7aa37811b0" + +var VirtualIPPropertiesProtocol = "vrrp" +var VirtualIPPropertiesVrid = 10 + +var LoadBalancerDetail = load_balancers.LoadBalancer{ + ID: "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + AdminUsername: "user-admin", + AvailabilityZone: "zone1-groupa", + DefaultGateway: &DetailDefaultGateway, + Description: "Load Balancer 1 Description", + Interfaces: []load_balancer_interfaces.LoadBalancerInterface{ + { + ID: "ee335c69-b50f-4a32-9d0f-f44cef84a456", + IPAddress: &DetailIPAddress1, + Name: "Interface 1/1", + NetworkID: &DetailNetworkID1, + SlotNumber: 1, + Status: "ACTIVE", + Type: "user", + VirtualIPAddress: &DetailVirtualIPAddress1, + VirtualIPProperties: &load_balancer_interfaces.VirtualIPProperties{ + Protocol: VirtualIPPropertiesProtocol, + Vrid: VirtualIPPropertiesVrid, + }, + }, + { + ID: "b39b61e4-00b1-4698-aed0-1928beb90abe", + IPAddress: &DetailIPAddress2, + Name: "Interface 1/2", + NetworkID: &DetailNetworkID2, + SlotNumber: 2, + Status: "ACTIVE", + Type: "user", + }, + }, + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "Load Balancer 1", + Status: "ACTIVE", + SyslogServers: []load_balancer_syslog_servers.LoadBalancerSyslogServer{ + { + ID: "11001101-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.215", + LogFacility: "LOCAL0", + LogLevel: "ALERT|INFO|ERROR", + Name: "syslog_server_main", + PortNumber: 514, + Status: "ACTIVE", + }, + { + ID: "22002202-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.211", + LogFacility: "LOCAL1", + LogLevel: "ERROR", + Name: "syslog_server_backup_fst", + PortNumber: 514, + Status: "ACTIVE", + }, + }, + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + UserUsername: "user-read", +} + +var ExpectedLoadBalancerSlice = []load_balancers.LoadBalancer{LoadBalancer1, LoadBalancer2} + +const ListResponseDuplicatedNames = ` +{ + "load_balancers": [ + { + "admin_username": "user-admin", + "availability_zone": "zone1-groupa", + "default_gateway": "100.127.253.1", + "description": "Load Balancer 1 Description", + "id": "5f3cae7c-58a5-4124-b622-9ca3cfbf2525", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "ACTIVE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + }, + { + "admin_username": "user-admin", + "availability_zone": "zone1_groupa", + "default_gateway": null, + "description": "abcdefghijklmnopqrstuvwxyz", + "id": "601665cf-c161-4e80-87f0-a3c0925d07a0", + "load_balancer_plan_id": "bd12784a-c66e-4f13-9f72-5143d64762b6", + "name": "Load Balancer 1", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "user_username": "user-read" + } + ] +} +` diff --git a/v4/ecl/network/v2/load_balancers/testing/request_test.go b/v4/ecl/network/v2/load_balancers/testing/request_test.go new file mode 100644 index 0000000..0773240 --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/testing/request_test.go @@ -0,0 +1,289 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_interfaces" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancer_syslog_servers" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/load_balancers" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + load_balancers.List(client, load_balancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := load_balancers.ExtractLoadBalancers(page) + if err != nil { + t.Errorf("Failed to extract Load Balancers: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedLoadBalancerSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/5f3cae7c-58a5-4124-b622-9ca3cfbf2525", 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, GetResponse) + }) + + s, err := load_balancers.Get(fake.ServiceClient(), "5f3cae7c-58a5-4124-b622-9ca3cfbf2525").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LoadBalancerDetail, s) +} + +func TestCreateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := load_balancers.CreateOpts{ + AvailabilityZone: "zone1-groupa", + Description: "abcdefghijklmnopqrstuvwxyz", + LoadBalancerPlanID: "bd12784a-c66e-4f13-9f72-5143d64762b6", + Name: "abcdefghijklmnopqrstuvwxyz", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + } + s, err := load_balancers.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &LoadBalancerDetail, s) +} + +func TestRequiredCreateOptsLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := load_balancers.Create(fake.ServiceClient(), load_balancers.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + adminUsername := "user-admin" + availabilityZone := "zone1-groupa" + defaultGateway := interface{}("100.127.253.1") + description := "UPDATED" + id := "5f3cae7c-58a5-4124-b622-9ca3cfbf2525" + + ipAddress1 := "100.127.253.173" + networkID1 := "c7f88fab-573e-47aa-b0b4-257db28dae23" + ipAddress2 := "192.168.110.1" + networkID2 := "1839d290-721c-49ba-99f1-3d7aa37811b0" + + interfaces := []load_balancer_interfaces.LoadBalancerInterface{ + { + ID: "ee335c69-b50f-4a32-9d0f-f44cef84a456", + IPAddress: &ipAddress1, + Name: "Interface 1/1", + NetworkID: &networkID1, + SlotNumber: 1, + Status: "ACTIVE", + }, + { + ID: "b39b61e4-00b1-4698-aed0-1928beb90abe", + IPAddress: &ipAddress2, + Name: "Interface 1/2", + NetworkID: &networkID2, + SlotNumber: 2, + Status: "ACTIVE", + }, + } + + loadBalancerPlanID := "bd12784a-c66e-4f13-9f72-5143d64762b6" + name := "abcdefghijklmnopqrstuvwxyz" + status := "PENDING_UPDATE" + + syslogServers := []load_balancer_syslog_servers.LoadBalancerSyslogServer{ + { + ID: "11001101-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.215", + LogFacility: "LOCAL0", + LogLevel: "ALERT|INFO|ERROR", + Name: "syslog_server_main", + PortNumber: 514, + Status: "ACTIVE", + }, + { + ID: "22002202-2edf-1844-1ff7-12ba5b7e566a", + IPAddress: "177.77.07.211", + LogFacility: "LOCAL1", + LogLevel: "ERROR", + Name: "syslog_server_backup_fst", + PortNumber: 514, + Status: "ACTIVE", + }, + } + + tenantID := "6a156ddf2ecd497ca786ff2da6df5aa8" + userUsername := "user-read" + + options := load_balancers.UpdateOpts{ + DefaultGateway: &defaultGateway, + Description: &description, + LoadBalancerPlanID: loadBalancerPlanID, + Name: &name, + } + + s, err := load_balancers.Update(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, adminUsername, s.AdminUsername) + th.CheckEquals(t, availabilityZone, s.AvailabilityZone) + th.CheckEquals(t, defaultGateway, *s.DefaultGateway) + th.CheckEquals(t, description, s.Description) + th.CheckEquals(t, id, s.ID) + th.CheckDeepEquals(t, interfaces, s.Interfaces) + th.CheckEquals(t, loadBalancerPlanID, s.LoadBalancerPlanID) + th.CheckEquals(t, name, s.Name) + th.CheckEquals(t, status, s.Status) + th.CheckDeepEquals(t, syslogServers, s.SyslogServers) + th.CheckEquals(t, tenantID, s.TenantID) + th.CheckEquals(t, userUsername, s.UserUsername) +} + +func TestDeleteLoadBalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers/ab49eb24-667f-4a4e-9421-b4d915bff416", 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 := load_balancers.Delete(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416") + th.AssertNoErr(t, res.Err) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + expectedID := "5f3cae7c-58a5-4124-b622-9ca3cfbf2525" + actualID, err := load_balancers.IDFromName(client, "Load Balancer 1") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponse) + }) + + client := fake.ServiceClient() + + _, err := load_balancers.IDFromName(client, "Load Balancer X") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/load_balancers", 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, ListResponseDuplicatedNames) + }) + + client := fake.ServiceClient() + + _, err := load_balancers.IDFromName(client, "Load Balancer 1") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/network/v2/load_balancers/urls.go b/v4/ecl/network/v2/load_balancers/urls.go new file mode 100644 index 0000000..4f37278 --- /dev/null +++ b/v4/ecl/network/v2/load_balancers/urls.go @@ -0,0 +1,31 @@ +package load_balancers + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("load_balancers", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("load_balancers") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/networks/doc.go b/v4/ecl/network/v2/networks/doc.go new file mode 100644 index 0000000..e768b71 --- /dev/null +++ b/v4/ecl/network/v2/networks/doc.go @@ -0,0 +1,65 @@ +/* +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. + +Example to List Networks + + listOpts := networks.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := networks.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + panic(err) + } + + for _, network := range allNetworks { + fmt.Printf("%+v", network) + } + +Example to Create a Network + + iTrue := true + createOpts := networks.CreateOpts{ + Name: "network_1", + AdminStateUp: &iTrue, + } + + network, err := networks.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := networks.UpdateOpts{ + Name: "new_name", + } + + network, err := networks.Update(networkClient, networkID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := networks.Delete(networkClient, networkID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package networks diff --git a/v4/ecl/network/v2/networks/requests.go b/v4/ecl/network/v2/networks/requests.go new file mode 100644 index 0000000..1f98bcd --- /dev/null +++ b/v4/ecl/network/v2/networks/requests.go @@ -0,0 +1,170 @@ +package networks + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// 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 { + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + Plane string `q:"plane"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 *eclcloud.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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a network. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + Plane string `json:"plane,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToNetworkCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "network") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a network. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToNetworkUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "network") +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *eclcloud.ServiceClient, networkID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, networkID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *eclcloud.ServiceClient, networkID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, networkID), nil) + return +} + +// IDFromName is a convenience function that returns a network's ID, given +// its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractNetworks(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "network"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "network"} + } +} diff --git a/v4/ecl/network/v2/networks/results.go b/v4/ecl/network/v2/networks/results.go new file mode 100644 index 0000000..ac11e5c --- /dev/null +++ b/v4/ecl/network/v2/networks/results.go @@ -0,0 +1,120 @@ +package networks + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + var s Network + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "network") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Network. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Network. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Network. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // The administrative state of network. If false (down), the network does not + // forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Description is the description of the network. + Description string `json:"description"` + + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // Plane it the ype of the traffic for which network will be used. + Plane string `json:"plane"` + + // Specifies whether the network resource can be accessed by any tenant. + Shared bool `json:"shared"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // Tags optionally set via extensions/attributestags + Tags map[string]string `json:"tags"` + + // TenantID is the project owner of the network. + TenantID string `json:"tenant_id"` +} + +// 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 (r NetworkPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"networks_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r NetworkPage) IsEmpty() (bool, error) { + is, err := ExtractNetworks(r) + return len(is) == 0, err +} + +// 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(r pagination.Page) ([]Network, error) { + var s []Network + err := ExtractNetworksInto(r, &s) + return s, err +} + +func ExtractNetworksInto(r pagination.Page, v interface{}) error { + return r.(NetworkPage).Result.ExtractIntoSlicePtr(v, "networks") +} diff --git a/v4/ecl/network/v2/networks/testing/doc.go b/v4/ecl/network/v2/networks/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v4/ecl/network/v2/networks/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v4/ecl/network/v2/networks/testing/fixtures.go b/v4/ecl/network/v2/networks/testing/fixtures.go new file mode 100644 index 0000000..b5fff24 --- /dev/null +++ b/v4/ecl/network/v2/networks/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/networks" +) + +const ListResponse = ` +{ + "networks": [ + { + "admin_state_up": true, + "description": "", + "id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "name": "", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [ + "ab49eb24-667f-4a4e-9421-b4d915bff416", + "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678" + ], + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + }, + { + "admin_state_up": true, + "description": "Example Network 2", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "Example Network 2", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [], + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + ] + }` +const GetResponse = `{ + "network": { + "admin_state_up": true, + "description": "Example Network 2", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "Example Network 2", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [], + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const CreateResponse = ` +{ + "network": { + "admin_state_up": true, + "description": "Example Network 2", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "Example Network 2", + "plane": "data", + "shared": false, + "status": "ACTIVE", + "subnets": [], + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const CreateRequest = ` +{ + "network": { + "admin_state_up": true, + "description": "Example Network 2", + "name": "Example Network 2", + "plane": "data", + "tags": { + "keyword1": "value1", + "keyword2": "value2" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` +const UpdateResponse = ` +{ + "network": { + "admin_state_up": false, + "description": "UPDATED", + "id": "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + "name": "UPDATED", + "plane": "data", + "shared": false, + "status": "PENDING_UPDATE", + "subnets": [], + "tags": { + "keyword1": "UPDATED", + "keyword3": "CREATED" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const UpdateRequest = ` +{ + "network": { + "admin_state_up": false, + "description": "UPDATED", + "name": "UPDATED", + "tags": { + "keyword1": "UPDATED", + "keyword3": "CREATED" + } + } + }` + +var Network1 = networks.Network{ + AdminStateUp: true, + Description: "", + ID: "8f36b88a-443f-4d97-9751-34d34af9e782", + Name: "", + Plane: "data", + Shared: false, + Status: "ACTIVE", + Subnets: []string{ + "ab49eb24-667f-4a4e-9421-b4d915bff416", + "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + }, + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var Network2 = networks.Network{ + AdminStateUp: true, + Description: "Example Network 2", + ID: "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", + Name: "Example Network 2", + Plane: "data", + Shared: false, + Status: "ACTIVE", + Subnets: []string{}, + Tags: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var ExpectedNetworkSlice = []networks.Network{Network1, Network2} diff --git a/v4/ecl/network/v2/networks/testing/request_test.go b/v4/ecl/network/v2/networks/testing/request_test.go new file mode 100644 index 0000000..006348a --- /dev/null +++ b/v4/ecl/network/v2/networks/testing/request_test.go @@ -0,0 +1,164 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/networks" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListNetwork(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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + networks.List(client, networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := networks.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extrace ports: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", 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, GetResponse) + }) + + n, err := networks.Get(fake.ServiceClient(), "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Network2, n) +} + +func TestCreateNetwork(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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + asu := true + + options := &networks.CreateOpts{ + AdminStateUp: &asu, + Description: "Example Network 2", + Name: "Example Network 2", + Plane: "data", + Tags: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", + } + n, err := networks.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &Network2, n) +} + +func TestRequiredCreateOptsNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := networks.Create(fake.ServiceClient(), networks.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + asu := false + description := "UPDATED" + name := "UPDATED" + tags := map[string]string{ + "keyword1": "UPDATED", + "keyword3": "CREATED", + } + + options := &networks.UpdateOpts{ + AdminStateUp: &asu, + Description: &description, + Name: &name, + Tags: &tags, + } + n, err := networks.Update(fake.ServiceClient(), "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, asu, n.AdminStateUp) + th.CheckEquals(t, description, n.Description) + th.CheckEquals(t, name, n.Name) + th.CheckDeepEquals(t, tags, n.Tags) +} + +func TestDeleteNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2", 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 := networks.Delete(fake.ServiceClient(), "a033d04b-b1fe-4ff4-a7c7-5f4b6da981d2") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/networks/urls.go b/v4/ecl/network/v2/networks/urls.go new file mode 100644 index 0000000..55ca5d5 --- /dev/null +++ b/v4/ecl/network/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/ports/doc.go b/v4/ecl/network/v2/ports/doc.go new file mode 100644 index 0000000..cfb1774 --- /dev/null +++ b/v4/ecl/network/v2/ports/doc.go @@ -0,0 +1,73 @@ +/* +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. + +Example to List Ports + + listOpts := ports.ListOpts{ + DeviceID: "b0b89efe-82f8-461d-958b-adbf80f50c7d", + } + + allPages, err := ports.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPorts, err := ports.ExtractPorts(allPages) + if err != nil { + panic(err) + } + + for _, port := range allPorts { + fmt.Printf("%+v\n", port) + } + +Example to Create a Port + + createOtps := ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: &[]string{"foo"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + port, err := ports.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + + updateOpts := ports.UpdateOpts{ + Name: "new_name", + SecurityGroups: &[]string{}, + } + + port, err := ports.Update(networkClient, portID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + err := ports.Delete(networkClient, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package ports diff --git a/v4/ecl/network/v2/ports/requests.go b/v4/ecl/network/v2/ports/requests.go new file mode 100644 index 0000000..1029b4d --- /dev/null +++ b/v4/ecl/network/v2/ports/requests.go @@ -0,0 +1,186 @@ +package ports + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// 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 { + Description string `q:"description"` + DeviceID string `q:"device_id"` + DeviceOwner string `q:"device_owner"` + ID string `q:"id"` + MACAddress string `q:"mac_address"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + SegmentationID int `q:"segmentation_id"` + SegmentationType string `q:"segmentation_type"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 *eclcloud.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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + Description string `json:"description,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + MACAddress string `json:"mac_address,omitempty"` + Name string `json:"name,omitempty"` + NetworkID string `json:"network_id"` + SegmentationID int `json:"segmentation_id,omitempty"` + SegmentationType string `json:"segmentation_type,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToPortCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "port") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + AllowedAddressPairs *[]AddressPair `json:"allowed_address_pairs,omitempty"` + Description *string `json:"description,omitempty"` + DeviceID *string `json:"device_id,omitempty"` + DeviceOwner *string `json:"device_owner,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + Name *string `json:"name,omitempty"` + SegmentationID *int `json:"segmentation_id,omitempty"` + SegmentationType *string `json:"segmentation_type,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToPortUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "port") +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a port's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractPorts(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "port"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "port"} + } +} diff --git a/v4/ecl/network/v2/ports/results.go b/v4/ecl/network/v2/ports/results.go new file mode 100644 index 0000000..b262894 --- /dev/null +++ b/v4/ecl/network/v2/ports/results.go @@ -0,0 +1,152 @@ +package ports + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s Port + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "port") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Port. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Port. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // Administrative state of port. If false (down), port does not forward + // packets. + AdminStateUp bool `json:"admin_state_up"` + + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` + + // Description is description + Description string `json:"description"` + + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `json:"device_id"` + + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `json:"device_owner"` + + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `json:"fixed_ips"` + + // UUID for the port. + ID string `json:"id"` + + // Mac address to use on this port. + MACAddress string `json:"mac_address"` + + // ManagedByService is set to true if only admin can modify it. Normal user has only read access. + ManagedByService bool `json:"managed_by_service"` + + // Human-readable name for the port. Might not be unique. + Name string `json:"name"` + + // Network that this port is associated with. + NetworkID string `json:"network_id"` + + // SegmentationID is the segmenation ID used for this port (i.e. for vlan type it is vlan tag) + SegmentationID int `json:"segmentation_id"` + + // SegmenationType is the segmentation type used for this port (i.e. vlan) + SegmentationType string `json:"segmentation_type"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Tags optionally set via extensions/attributestags + Tags map[string]string `json:"tags"` + + // TenantID is the project owner of the port. + TenantID string `json:"tenant_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 (r PortPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"ports_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (r PortPage) IsEmpty() (bool, error) { + is, err := ExtractPorts(r) + return len(is) == 0, err +} + +// 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(r pagination.Page) ([]Port, error) { + var s []Port + err := ExtractPortsInto(r, &s) + return s, err +} + +func ExtractPortsInto(r pagination.Page, v interface{}) error { + return r.(PortPage).Result.ExtractIntoSlicePtr(v, "ports") +} diff --git a/v4/ecl/network/v2/ports/testing/doc.go b/v4/ecl/network/v2/ports/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v4/ecl/network/v2/ports/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v4/ecl/network/v2/ports/testing/fixtures.go b/v4/ecl/network/v2/ports/testing/fixtures.go new file mode 100644 index 0000000..2a8c166 --- /dev/null +++ b/v4/ecl/network/v2/ports/testing/fixtures.go @@ -0,0 +1,290 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/ports" +) + +const ListResponse = ` +{ + "ports": [ + { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "DHCP Server Port", + "device_id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "device_owner": "network:dhcp", + "fixed_ips": [ + { + "ip_address": "192.168.2.2", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "8db1ba30-be40-4943-a7be-ed5b98f053b3", + "mac_address": "00:00:5e:00:01:00", + "managed_by_service": false, + "name": "dhcp-server-port", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": null, + "segmentation_type": null, + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + }, + { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "managed_by_service": false, + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + ] + }` +const GetResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "managed_by_service": false, + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` + +const CreateResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` +const CreateRequest = ` +{ + "port": + { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + }], + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "name": "port_12", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1", + "segmentation_type": "flat" + } +}` +const UpdateResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + },{ + "ip_address": "192.168.2.255", + "mac_address": "26:8d:42:f6:c2:c4" + }], + "description": "UPDATED", + "device_id": "b269b8c0-1a42-4464-9314-4396e51e5107", + "device_owner": "UPDATED", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + }, { + "ip_address": "192.168.2.31", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff417" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "name": "UPDATED", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 2, + "segmentation_type": "vlan", + "status": "PENDING_CREATE", + "tags": { + "some-key":"UPDATED" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + }` + +const UpdateRequest = ` +{ + "port": { + "allowed_address_pairs": [{ + "ip_address": "192.168.2.100", + "mac_address": "00:00:5e:00:01:01" + },{ + "ip_address": "192.168.2.255", + "mac_address": "26:8d:42:f6:c2:c4" + }], + "description": "UPDATED", + "device_id": "b269b8c0-1a42-4464-9314-4396e51e5107", + "device_owner": "UPDATED", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + }, { + "ip_address": "192.168.2.31", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff417" + } + ], + "name": "UPDATED", + "segmentation_id": 2, + "segmentation_type": "vlan", + "tags": { + "some-key":"UPDATED" + } + } + }` + +const RemoveAllowedAddressPairsRequest = ` +{ + "port": { + "allowed_address_pairs": [], + "name": "new_port_name" + } + } +` + +const RemoveAllowedAddressPairsResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.30", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + "mac_address": "fa:16:3e:b0:ca:f1", + "name": "new_port_name", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "segmentation_id": 0, + "segmentation_type": "flat", + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` + +var Port1 = ports.Port{ + AdminStateUp: true, + AllowedAddressPairs: []ports.AddressPair{}, + Description: "DHCP Server Port", + DeviceID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + DeviceOwner: "network:dhcp", + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.2", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + ID: "8db1ba30-be40-4943-a7be-ed5b98f053b3", + MACAddress: "00:00:5e:00:01:00", + ManagedByService: false, + Name: "dhcp-server-port", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var Port2 = ports.Port{ + AdminStateUp: true, + AllowedAddressPairs: []ports.AddressPair{{ + IPAddress: "192.168.2.100", + MACAddress: "00:00:5e:00:01:01", + }}, + Description: "", + DeviceID: "", + DeviceOwner: "", + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.30", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + ID: "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", + MACAddress: "fa:16:3e:b0:ca:f1", + ManagedByService: false, + Name: "port_12", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + SegmentationID: 0, + SegmentationType: "flat", + Status: "PENDING_CREATE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var ExpectedPortSlice = []ports.Port{Port1, Port2} diff --git a/v4/ecl/network/v2/ports/testing/request_test.go b/v4/ecl/network/v2/ports/testing/request_test.go new file mode 100644 index 0000000..ee9cbdb --- /dev/null +++ b/v4/ecl/network/v2/ports/testing/request_test.go @@ -0,0 +1,221 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/ports" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListPort(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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + ports.List(client, ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ports.ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extrace ports: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedPortSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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, GetResponse) + }) + + p, err := ports.Get(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Port2, p) +} + +func TestCreatePort(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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + asu := true + + options := &ports.CreateOpts{ + AdminStateUp: &asu, + AllowedAddressPairs: []ports.AddressPair{{ + IPAddress: "192.168.2.100", + MACAddress: "00:00:5e:00:01:01", + }}, + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.30", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + Name: "port_12", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", + SegmentationType: "flat", + } + p, err := ports.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &Port2, p) +} + +func TestRequiredCreateOptsPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := ports.Create(fake.ServiceClient(), ports.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} +func TestUpdatePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + aap := []ports.AddressPair{{ + IPAddress: "192.168.2.100", + MACAddress: "00:00:5e:00:01:01", + }, { + IPAddress: "192.168.2.255", + MACAddress: "26:8d:42:f6:c2:c4", + }} + description := "UPDATED" + deviceID := "b269b8c0-1a42-4464-9314-4396e51e5107" + deviceOwner := "UPDATED" + fip := []ports.IP{{ + IPAddress: "192.168.2.30", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }, { + IPAddress: "192.168.2.31", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff417", + }} + name := "UPDATED" + segmentationID := 2 + segmentationType := "vlan" + tags := map[string]string{"some-key": "UPDATED"} + + options := ports.UpdateOpts{ + AllowedAddressPairs: &aap, + Description: &description, + DeviceID: &deviceID, + DeviceOwner: &deviceOwner, + FixedIPs: fip, + Name: &name, + SegmentationID: &segmentationID, + SegmentationType: &segmentationType, + Tags: &tags, + } + p, err := ports.Update(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", options).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, aap, p.AllowedAddressPairs) + th.CheckEquals(t, description, p.Description) + th.CheckEquals(t, deviceID, p.DeviceID) + th.CheckEquals(t, deviceOwner, p.DeviceOwner) + th.CheckDeepEquals(t, fip, p.FixedIPs) + th.CheckEquals(t, name, p.Name) + th.CheckEquals(t, segmentationID, p.SegmentationID) + th.CheckEquals(t, segmentationType, p.SegmentationType) + th.CheckDeepEquals(t, tags, p.Tags) +} + +func TestRemoveAllowedAddressPairs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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, RemoveAllowedAddressPairsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, RemoveAllowedAddressPairsResponse) + }) + + name := "new_port_name" + options := ports.UpdateOpts{ + Name: &name, + AllowedAddressPairs: &[]ports.AddressPair{}, + } + + s, err := ports.Update(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{}) +} + +func TestDeletePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730", 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 := ports.Delete(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/ports/urls.go b/v4/ecl/network/v2/ports/urls.go new file mode 100644 index 0000000..bb6b08d --- /dev/null +++ b/v4/ecl/network/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/public_ips/doc.go b/v4/ecl/network/v2/public_ips/doc.go new file mode 100644 index 0000000..eff1dea --- /dev/null +++ b/v4/ecl/network/v2/public_ips/doc.go @@ -0,0 +1 @@ +package public_ips diff --git a/v4/ecl/network/v2/public_ips/requests.go b/v4/ecl/network/v2/public_ips/requests.go new file mode 100644 index 0000000..772409f --- /dev/null +++ b/v4/ecl/network/v2/public_ips/requests.go @@ -0,0 +1,136 @@ +package public_ips + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type ListOptsBuilder interface { + ToPublicIPListQuery() (string, error) +} + +type ListOpts struct { + Cidr string `q:"cidr"` + Description string `q:"description"` + ID string `q:"id"` + InternetGwID string `q:"internet_gw_id"` + Name string `q:"name"` + Status string `q:"status"` + SubmaskLength int `q:"submask_length"` + TenantID string `q:"tenant_id"` +} + +func (opts ListOpts) ToPublicIPListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPublicIPListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PublicIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, publicIPID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, publicIPID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToPublicIPCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + Description string `json:"description,omitempty"` + InternetGwID string `json:"internet_gw_id" required:"true"` + Name string `json:"name,omitempty"` + SubmaskLength int `json:"submask_length" required:"true"` + TenantID string `json:"tenant_id,omitempty"` +} + +func (opts CreateOpts) ToPublicIPCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "public_ip") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPublicIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToPublicIPUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` +} + +func (opts UpdateOpts) ToPublicIPUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "public_ip") +} + +func Update(c *eclcloud.ServiceClient, publicIPID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPublicIPUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, publicIPID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, publicIPID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, publicIPID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractPublicIPs(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "public_ip"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "public_ip"} + } +} diff --git a/v4/ecl/network/v2/public_ips/results.go b/v4/ecl/network/v2/public_ips/results.go new file mode 100644 index 0000000..3ffd6a8 --- /dev/null +++ b/v4/ecl/network/v2/public_ips/results.go @@ -0,0 +1,77 @@ +package public_ips + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*PublicIP, error) { + var s PublicIP + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "public_ip") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type PublicIP struct { + Cidr string `json:"cidr"` + Description string `json:"description"` + ID string `json:"id"` + InternetGwID string `json:"internet_gw_id"` + Name string `json:"name"` + Status string `json:"status"` + SubmaskLength int `json:"submask_length"` + TenantID string `json:"tenant_id"` +} + +type PublicIPPage struct { + pagination.LinkedPageBase +} + +func (r PublicIPPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"public_ips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r PublicIPPage) IsEmpty() (bool, error) { + is, err := ExtractPublicIPs(r) + return len(is) == 0, err +} + +func ExtractPublicIPs(r pagination.Page) ([]PublicIP, error) { + var s []PublicIP + err := ExtractPublicIPsInto(r, &s) + return s, err +} + +func ExtractPublicIPsInto(r pagination.Page, v interface{}) error { + return r.(PublicIPPage).Result.ExtractIntoSlicePtr(v, "public_ips") +} diff --git a/v4/ecl/network/v2/public_ips/testing/doc.go b/v4/ecl/network/v2/public_ips/testing/doc.go new file mode 100644 index 0000000..ab98250 --- /dev/null +++ b/v4/ecl/network/v2/public_ips/testing/doc.go @@ -0,0 +1,2 @@ +// public_ips unit tests +package testing diff --git a/v4/ecl/network/v2/public_ips/testing/fixtures.go b/v4/ecl/network/v2/public_ips/testing/fixtures.go new file mode 100644 index 0000000..361f149 --- /dev/null +++ b/v4/ecl/network/v2/public_ips/testing/fixtures.go @@ -0,0 +1,121 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/public_ips" +) + +const ListResponse = ` +{ + "public_ips": [ + { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_CREATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + }, + { + "cidr": "100.127.254.56", + "description": "", + "id": "110846c3-3a20-42ff-ad3d-25ba7b0272bb", + "internet_gw_id": "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + "name": "6_Public", + "status": "ACTIVE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } + ] +} +` + +const GetResponse = ` +{ + "public_ip": { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_CREATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +const CreateRequest = ` +{ + "public_ip": { + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +const CreateResponse = ` +{ + "public_ip": { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_CREATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +const UpdateRequest = ` +{ + "public_ip": { + "name": "seinou-test-public", + "description": "" + } +} + ` + +const UpdateResponse = ` +{ + "public_ip": { + "cidr": "100.127.255.80", + "description": "", + "id": "0718a31b-67be-4349-946b-61a0fc38e4cd", + "internet_gw_id": "2a75cfa6-89af-425b-bce5-2a85197ef04f", + "name": "seinou-test-public", + "status": "PENDING_UPDATE", + "submask_length": 29, + "tenant_id": "19ab165c7a664abe9c217334cd0e9cc9" + } +} +` + +var PublicIP1 = public_ips.PublicIP{ + Cidr: "100.127.255.80", + Description: "", + ID: "0718a31b-67be-4349-946b-61a0fc38e4cd", + InternetGwID: "2a75cfa6-89af-425b-bce5-2a85197ef04f", + Name: "seinou-test-public", + Status: "PENDING_CREATE", + SubmaskLength: 29, + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", +} + +var PublicIP2 = public_ips.PublicIP{ + Cidr: "100.127.254.56", + Description: "", + ID: "110846c3-3a20-42ff-ad3d-25ba7b0272bb", + InternetGwID: "05db9b0e-65ed-4478-a6b3-d3fc259c8d07", + Name: "6_Public", + Status: "ACTIVE", + SubmaskLength: 29, + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", +} + +var ExpectedPublicIPSlice = []public_ips.PublicIP{PublicIP1, PublicIP2} diff --git a/v4/ecl/network/v2/public_ips/testing/request_test.go b/v4/ecl/network/v2/public_ips/testing/request_test.go new file mode 100644 index 0000000..9acb7a7 --- /dev/null +++ b/v4/ecl/network/v2/public_ips/testing/request_test.go @@ -0,0 +1,143 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/public_ips" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/public_ips", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := public_ips.List(client, public_ips.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := public_ips.ExtractPublicIPs(page) + if err != nil { + t.Errorf("Failed to extract public ips: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedPublicIPSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + 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/public_ips/0718a31b-67be-4349-946b-61a0fc38e4cd", 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, GetResponse) + }) + + i, err := public_ips.Get(fake.ServiceClient(), "0718a31b-67be-4349-946b-61a0fc38e4cd").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &PublicIP1, i) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := public_ips.CreateOpts{ + Name: "seinou-test-public", + Description: "", + InternetGwID: "2a75cfa6-89af-425b-bce5-2a85197ef04f", + SubmaskLength: 29, + TenantID: "19ab165c7a664abe9c217334cd0e9cc9", + } + i, err := public_ips.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Status, "PENDING_CREATE") + th.AssertDeepEquals(t, &PublicIP1, i) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/public_ips/0718a31b-67be-4349-946b-61a0fc38e4cd", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := "seinou-test-public" + description := "" + options := public_ips.UpdateOpts{Name: &name, Description: &description} + i, err := public_ips.Update(fake.ServiceClient(), "0718a31b-67be-4349-946b-61a0fc38e4cd", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "seinou-test-public") + th.AssertEquals(t, i.Description, "") + th.AssertEquals(t, i.ID, "0718a31b-67be-4349-946b-61a0fc38e4cd") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/public_ips/0718a31b-67be-4349-946b-61a0fc38e4cd", 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 := public_ips.Delete(fake.ServiceClient(), "0718a31b-67be-4349-946b-61a0fc38e4cd") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/public_ips/urls.go b/v4/ecl/network/v2/public_ips/urls.go new file mode 100644 index 0000000..b62fc12 --- /dev/null +++ b/v4/ecl/network/v2/public_ips/urls.go @@ -0,0 +1,31 @@ +package public_ips + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("public_ips", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("public_ips") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/qos_options/doc.go b/v4/ecl/network/v2/qos_options/doc.go new file mode 100644 index 0000000..cd8de8c --- /dev/null +++ b/v4/ecl/network/v2/qos_options/doc.go @@ -0,0 +1,35 @@ +/* +Package qos_options provides information of several service +in the Enterprise Cloud Compute service + +Example to List QoS Options + + listOpts := qos_options.ListOpts{ + QoSType: "guarantee", + } + + allPages, err := qos_options.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allQoSOptions, err := qos_options.ExtractQoSOptions(allPages) + if err != nil { + panic(err) + } + + for _, qosOption := range allQoSOptions { + fmt.Printf("%+v", qosOption) + } + +Example to Show QoS Option + + id := "02dc9a22-129c-4b12-9936-4080f6a7ae44" + qosOption, err := qos_options.Get(client, id).Extract() + if err != nil { + panic(err) + } + fmt.Print(qosOption) + +*/ +package qos_options diff --git a/v4/ecl/network/v2/qos_options/requests.go b/v4/ecl/network/v2/qos_options/requests.go new file mode 100644 index 0000000..c90287d --- /dev/null +++ b/v4/ecl/network/v2/qos_options/requests.go @@ -0,0 +1,87 @@ +package qos_options + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToQosOptionsListQuery() (string, error) +} + +// ListOpts allows the filtering of paginated collections through the API. +// Filtering is achieved by passing in struct field values that map to +// the QoS option attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // Unique ID for the AWSService. + AWSServiceID string `q:"aws_service_id"` + + // Unique ID for the AzureService. + AzureServiceID string `q:"azure_service_id"` + + // Bandwidth assigned with this QoS Option + Bandwidth string `q:"bandwidth"` + + // Description is the description of the QoS Policy. + Description string `q:"description"` + + // Unique ID for the FICService. + FICServiceID string `q:"fic_service_id"` + + // Unique ID for the GCPService. + GCPServiceID string `q:"gcp_service_id"` + + // Unique ID for the QoS option. + ID string `q:"id"` + + // Unique ID for the InterDCService. + InterDCServiceID string `q:"interdc_service_id"` + + // Unique ID for the InternetService. + InternetServiceID string `q:"internet_service_id"` + + // Name is the name of the QoS option. + Name string `q:"name"` + + // Type of the QoS option.(guarantee or besteffort) + QoSType string `q:"qos_type"` + + // Service type of the QoS option.(aws, azure, fic, gcp, vpn, internet, interdc) + ServiceType string `q:"service_type"` + + // Indicates whether QoS option is currently operational. + Status string `q:"status"` + + // Unique ID for the VPNService. + VPNServiceID string `q:"vpn_service_id"` +} + +// ToQosOptionsListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToQosOptionsListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list QoS options accessible to you. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToQosOptionsListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return QosOptionPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific QoS option based on its unique ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/v4/ecl/network/v2/qos_options/results.go b/v4/ecl/network/v2/qos_options/results.go new file mode 100644 index 0000000..06f345a --- /dev/null +++ b/v4/ecl/network/v2/qos_options/results.go @@ -0,0 +1,60 @@ +package qos_options + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type QosOptionPage struct { + pagination.LinkedPageBase +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the result of Get operations. Call its Extract method to +// interpret it as a QoSOption. +type GetResult struct { + commonResult +} + +// QoSOption represents a QoS option. +type QoSOption struct { + AWSServiceID string `json:"aws_service_id"` + AzureServiceID string `json:"azure_service_id"` + Bandwidth string `json:"bandwidth"` + Description string `json:"description"` + FICServiceID string `json:"fic_service_id"` + GCPServiceID string `json:"gcp_service_id"` + ID string `json:"id"` + InterDCServiceID string `json:"interdc_service_id"` + InternetServiceID string `json:"internet_service_id"` + Name string `json:"name"` + QoSType string `json:"qos_type"` + ServiceType string `json:"service_type"` + Status string `json:"status"` + VPNServiceID string `json:"vpn_service_id"` +} + +// IsEmpty checks whether a QosOptionPage struct is empty. +func (r QosOptionPage) IsEmpty() (bool, error) { + is, err := ExtractQoSOptions(r) + return len(is) == 0, err +} + +// ExtractQoSOptions accepts a Page struct, specifically a QoSOptionPage struct, +// and extracts the elements into a slice of ListOpts structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractQoSOptions(r pagination.Page) ([]QoSOption, error) { + var s []QoSOption + err := r.(QosOptionPage).Result.ExtractIntoSlicePtr(&s, "qos_options") + return s, err +} + +// Extract is a function that accepts a result and extracts a QoSOption. +func (r GetResult) Extract() (*QoSOption, error) { + var l QoSOption + err := r.Result.ExtractIntoStructPtr(&l, "qos_option") + return &l, err +} diff --git a/v4/ecl/network/v2/qos_options/testing/doc.go b/v4/ecl/network/v2/qos_options/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v4/ecl/network/v2/qos_options/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v4/ecl/network/v2/qos_options/testing/fixtures.go b/v4/ecl/network/v2/qos_options/testing/fixtures.go new file mode 100644 index 0000000..911c85d --- /dev/null +++ b/v4/ecl/network/v2/qos_options/testing/fixtures.go @@ -0,0 +1,99 @@ +package testing + +import "github.com/nttcom/eclcloud/v4/ecl/network/v2/qos_options" + +const ListResponse = ` +{ + "qos_options": [ + { + "aws_service_id" : null, + "azure_service_id" : "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "bandwidth" : "20", + "description" : "20M-guarantee-menu-for-azure", + "fic_service_id" : null, + "gcp_service_id" : null, + "id" : "a6b91294-8870-4f2c-b9e9-a899acada723", + "interdc_service_id" : null, + "internet_service_id" : null, + "name" : "20M-GA-AZURE", + "qos_type" : "guarantee", + "service_type" : "azure", + "status" : "ACTIVE", + "vpn_service_id" : null + }, + { + "aws_service_id" : null, + "azure_service_id" : "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "bandwidth" : "500", + "description" : "500M-guarantee-menu-for-azure", + "fic_service_id" : null, + "gcp_service_id" : null, + "id" : "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + "interdc_service_id" : null, + "internet_service_id" : null, + "name" : "500M-GA-AZURE", + "qos_type" : "guarantee", + "service_type" : "azure", + "status" : "ACTIVE", + "vpn_service_id" : null + } + ] +} +` + +const GetResponse = ` +{ + "qos_option": { + "aws_service_id" : null, + "azure_service_id" : "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + "bandwidth" : "20", + "description" : "20M-guarantee-menu-for-azure", + "fic_service_id" : null, + "gcp_service_id" : null, + "id" : "a6b91294-8870-4f2c-b9e9-a899acada723", + "interdc_service_id" : null, + "internet_service_id" : null, + "name" : "20M-GA-AZURE", + "qos_type" : "guarantee", + "service_type" : "azure", + "status" : "ACTIVE", + "vpn_service_id" : null + } +} +` + +var Qos1 = qos_options.QoSOption{ + AWSServiceID: "", + AzureServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + Bandwidth: "20", + Description: "20M-guarantee-menu-for-azure", + FICServiceID: "", + GCPServiceID: "", + ID: "a6b91294-8870-4f2c-b9e9-a899acada723", + InterDCServiceID: "", + InternetServiceID: "", + Name: "20M-GA-AZURE", + QoSType: "guarantee", + ServiceType: "azure", + Status: "ACTIVE", + VPNServiceID: "", +} + +var Qos2 = qos_options.QoSOption{ + AWSServiceID: "", + AzureServiceID: "d4006e79-9f60-4b72-9f86-5f6ef8b4e9e9", + Bandwidth: "500", + Description: "500M-guarantee-menu-for-azure", + FICServiceID: "", + GCPServiceID: "", + ID: "aa776ce4-08a8-4cc1-9a2c-bb95e547916b", + InterDCServiceID: "", + InternetServiceID: "", + Name: "500M-GA-AZURE", + QoSType: "guarantee", + ServiceType: "azure", + Status: "ACTIVE", + VPNServiceID: "", +} + +var ExpectedQosSlice = []qos_options.QoSOption{Qos1, Qos2} diff --git a/v4/ecl/network/v2/qos_options/testing/request_test.go b/v4/ecl/network/v2/qos_options/testing/request_test.go new file mode 100644 index 0000000..0dedab5 --- /dev/null +++ b/v4/ecl/network/v2/qos_options/testing/request_test.go @@ -0,0 +1,68 @@ +package testing + +import ( + "fmt" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/qos_options" + "github.com/nttcom/eclcloud/v4/pagination" + + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListQoS(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/qos_options", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + qos_options.List(client, qos_options.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := qos_options.ExtractQoSOptions(page) + if err != nil { + t.Errorf("Failed to extract QoS options: %v", err) + return false, nil + } + th.CheckDeepEquals(t, ExpectedQosSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetQoS(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + id := "2c649b8e-f007-4d90-b208-9b8710937a94" + th.Mux.HandleFunc(fmt.Sprintf("/v2.0/qos_options/%s", 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, GetResponse) + }) + + n, err := qos_options.Get(fake.ServiceClient(), id).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Qos1, n) +} diff --git a/v4/ecl/network/v2/qos_options/urls.go b/v4/ecl/network/v2/qos_options/urls.go new file mode 100644 index 0000000..b9375d8 --- /dev/null +++ b/v4/ecl/network/v2/qos_options/urls.go @@ -0,0 +1,11 @@ +package qos_options + +import "github.com/nttcom/eclcloud/v4" + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("qos_options", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("qos_options") +} diff --git a/v4/ecl/network/v2/static_routes/doc.go b/v4/ecl/network/v2/static_routes/doc.go new file mode 100644 index 0000000..00ca023 --- /dev/null +++ b/v4/ecl/network/v2/static_routes/doc.go @@ -0,0 +1 @@ +package static_routes diff --git a/v4/ecl/network/v2/static_routes/requests.go b/v4/ecl/network/v2/static_routes/requests.go new file mode 100644 index 0000000..adfc6e8 --- /dev/null +++ b/v4/ecl/network/v2/static_routes/requests.go @@ -0,0 +1,151 @@ +package static_routes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type ListOptsBuilder interface { + ToStaticRouteListQuery() (string, error) +} + +type ListOpts struct { + AwsGwID string `q:"aws_gw_id"` + AzureGwID string `q:"azure_gw_id"` + Description string `q:"description"` + Destination string `q:"destination"` + FICGatewayID string `q:"fic_gw_id"` + GcpGwID string `q:"gcp_gw_id"` + ID string `q:"id"` + InterdcGwID string `q:"inter_dc_id"` + InternetGwID string `q:"internet_gw_id"` + Name string `q:"name"` + Nexthop string `q:"nexthop"` + ServiceType string `q:"service_type"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + VpnGwID string `q:"vpn_gw_id"` +} + +func (opts ListOpts) ToStaticRouteListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToStaticRouteListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return StaticRoutePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func Get(c *eclcloud.ServiceClient, publicIPID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, publicIPID), &r.Body, nil) + return +} + +type CreateOptsBuilder interface { + ToStaticRouteCreateMap() (map[string]interface{}, error) +} + +type CreateOpts struct { + AwsGwID string `json:"aws_gw_id,omitempty"` + AzureGwID string `json:"azure_gw_id,omitempty"` + Description string `json:"description"` + Destination string `json:"destination" required:"true"` + FICGatewayID string `json:"fic_gw_id,omitempty"` + GcpGwID string `json:"gcp_gw_id,omitempty"` + InterdcGwID string `json:"inter_dc_id,omitempty"` + InternetGwID string `json:"internet_gw_id,omitempty"` + Name string `json:"name"` + Nexthop string `json:"nexthop" required:"true"` + ServiceType string `json:"service_type" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + VpnGwID string `json:"vpn_gw_id,omitempty"` +} + +func (opts CreateOpts) ToStaticRouteCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "static_route") +} + +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToStaticRouteCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToStaticRouteUpdateMap() (map[string]interface{}, error) +} + +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` +} + +func (opts UpdateOpts) ToStaticRouteUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "static_route") +} + +func Update(c *eclcloud.ServiceClient, publicIPID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToStaticRouteUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, publicIPID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +func Delete(c *eclcloud.ServiceClient, publicIPID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, publicIPID), nil) + return +} + +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractStaticRoutes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "static_route"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "static_route"} + } +} diff --git a/v4/ecl/network/v2/static_routes/results.go b/v4/ecl/network/v2/static_routes/results.go new file mode 100644 index 0000000..edd1c85 --- /dev/null +++ b/v4/ecl/network/v2/static_routes/results.go @@ -0,0 +1,84 @@ +package static_routes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*StaticRoute, error) { + var s StaticRoute + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "static_route") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +type StaticRoute struct { + AwsGwID string `json:"aws_gw_id"` + AzureGwID string `json:"azure_gw_id"` + Description string `json:"description"` + Destination string `json:"destination"` + FICGatewayID string `json:"fic_gw_id"` + GcpGwID string `json:"gcp_gw_id"` + ID string `json:"id"` + InterdcGwID string `json:"interdc_gw_id"` + InternetGwID string `json:"internet_gw_id"` + Name string `json:"name"` + Nexthop string `json:"nexthop"` + ServiceType string `json:"service_type"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + VpnGwID string `json:"vpn_gw_id"` +} + +type StaticRoutePage struct { + pagination.LinkedPageBase +} + +func (r StaticRoutePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"static_routes_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r StaticRoutePage) IsEmpty() (bool, error) { + is, err := ExtractStaticRoutes(r) + return len(is) == 0, err +} + +func ExtractStaticRoutes(r pagination.Page) ([]StaticRoute, error) { + var s []StaticRoute + err := ExtractStaticRoutesInto(r, &s) + return s, err +} + +func ExtractStaticRoutesInto(r pagination.Page, v interface{}) error { + return r.(StaticRoutePage).Result.ExtractIntoSlicePtr(v, "static_routes") +} diff --git a/v4/ecl/network/v2/static_routes/testing/doc.go b/v4/ecl/network/v2/static_routes/testing/doc.go new file mode 100644 index 0000000..ab98250 --- /dev/null +++ b/v4/ecl/network/v2/static_routes/testing/doc.go @@ -0,0 +1,2 @@ +// public_ips unit tests +package testing diff --git a/v4/ecl/network/v2/static_routes/testing/fixtures.go b/v4/ecl/network/v2/static_routes/testing/fixtures.go new file mode 100644 index 0000000..b502e6e --- /dev/null +++ b/v4/ecl/network/v2/static_routes/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/static_routes" +) + +const ListResponse = ` +{ + "static_routes": [ + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "gcp_gw_id": null, + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + }, + { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "StaticRoute for Scenario-test.", + "destination": "100.127.255.184/29", + "fic_gw_id": "1331e6a7-2876-4d34-b12f-5aac9517b034", + "gcp_gw_id": null, + "id": "e58162ca-9fef-4f27-898f-af0d495b780c", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "StaticRoute_INGW_02_01", + "nexthop": "100.127.255.189", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + } + ] +} +` + +const GetResponse = ` +{ + "static_route": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "gcp_gw_id": null, + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + } +} +` + +const CreateRequest = ` +{ + "static_route": { + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` + +const CreateResponse = ` +{ + "static_route": { + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_CREATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8" + } +} +` + +const UpdateRequest = ` +{ + "static_route": { + "description": "SRT2", + "name": "SRT2" + } +} + ` + +const UpdateResponse = ` +{ + "static_route": { + "aws_gw_id": null, + "azure_gw_id": null, + "description": "SRT2", + "destination": "100.127.254.116/30", + "fic_gw_id": "5af4f343-91a7-4956-aabb-9ac678d215e5", + "gcp_gw_id": null, + "id": "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + "interdc_gw_id": null, + "internet_gw_id": null, + "name": "SRT2", + "nexthop": "100.127.254.117", + "service_type": "fic", + "status": "PENDING_UPDATE", + "tenant_id": "6a156ddf2ecd497ca786ff2da6df5aa8", + "vpn_gw_id": null + } +} +` + +var StaticRoute1 = static_routes.StaticRoute{ + Description: "SRT2", + Destination: "100.127.254.116/30", + FICGatewayID: "5af4f343-91a7-4956-aabb-9ac678d215e5", + ID: "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", + Name: "SRT2", + Nexthop: "100.127.254.117", + ServiceType: "fic", + Status: "PENDING_CREATE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var StaticRoute2 = static_routes.StaticRoute{ + Description: "StaticRoute for Scenario-test.", + Destination: "100.127.255.184/29", + FICGatewayID: "1331e6a7-2876-4d34-b12f-5aac9517b034", + ID: "e58162ca-9fef-4f27-898f-af0d495b780c", + Name: "StaticRoute_INGW_02_01", + Nexthop: "100.127.255.189", + ServiceType: "fic", + Status: "PENDING_CREATE", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", +} + +var ExpectedStaticRouteSlice = []static_routes.StaticRoute{StaticRoute1, StaticRoute2} diff --git a/v4/ecl/network/v2/static_routes/testing/request_test.go b/v4/ecl/network/v2/static_routes/testing/request_test.go new file mode 100644 index 0000000..b51ed0f --- /dev/null +++ b/v4/ecl/network/v2/static_routes/testing/request_test.go @@ -0,0 +1,145 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/static_routes" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListStaticRoutes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes", 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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + tmp := static_routes.List(client, static_routes.ListOpts{}) + err := tmp.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := static_routes.ExtractStaticRoutes(page) + if err != nil { + t.Errorf("Failed to extract public ips: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedStaticRouteSlice, actual) + + return true, nil + }) + + if err != nil { + fmt.Printf("%s", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes/cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", 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, GetResponse) + }) + + i, err := static_routes.Get(fake.ServiceClient(), "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a").Extract() + t.Logf("%s", err) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &StaticRoute1, i) +} + +func TestCreateStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes", 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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := static_routes.CreateOpts{ + Name: "SRT2", + Description: "SRT2", + Destination: "100.127.254.116/30", + FICGatewayID: "5af4f343-91a7-4956-aabb-9ac678d215e5", + Nexthop: "100.127.254.117", + ServiceType: "fic", + TenantID: "6a156ddf2ecd497ca786ff2da6df5aa8", + } + i, err := static_routes.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Status, "PENDING_CREATE") + th.AssertDeepEquals(t, &StaticRoute1, i) +} + +func TestUpdateStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes/cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := "SRT2" + description := "SRT2" + options := static_routes.UpdateOpts{Name: &name, Description: &description} + i, err := static_routes.Update(fake.ServiceClient(), "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, i.Name, "SRT2") + th.AssertEquals(t, i.Description, "SRT2") + th.AssertEquals(t, i.ID, "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a") +} + +func TestDeleteStaticRoute(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/static_routes/cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a", 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 := static_routes.Delete(fake.ServiceClient(), "cd1dacf1-0838-4ffc-bbb8-54d3152b9a5a") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/static_routes/urls.go b/v4/ecl/network/v2/static_routes/urls.go new file mode 100644 index 0000000..ace58ae --- /dev/null +++ b/v4/ecl/network/v2/static_routes/urls.go @@ -0,0 +1,31 @@ +package static_routes + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("static_routes", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("static_routes") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/subnets/doc.go b/v4/ecl/network/v2/subnets/doc.go new file mode 100644 index 0000000..d0ed8df --- /dev/null +++ b/v4/ecl/network/v2/subnets/doc.go @@ -0,0 +1,133 @@ +/* +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. + +Example to List Subnets + + listOpts := subnets.ListOpts{ + IPVersion: 4, + } + + allPages, err := subnets.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + panic(err) + } + + for _, subnet := range allSubnets { + fmt.Printf("%+v\n", subnet) + } + +Example to Create a Subnet With Specified Gateway + + var gatewayIP = "192.168.199.1" + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + GatewayIP: &gatewayIP, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With No Gateway + + var noGateway = "" + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With a Default Gateway + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + Name: "new_name", + DNSNameservers: []string{"8.8.8.8}, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove a Gateway From a Subnet + + var noGateway = "" + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + GatewayIP: &noGateway, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + err := subnets.Delete(networkClient, subnetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package subnets diff --git a/v4/ecl/network/v2/subnets/requests.go b/v4/ecl/network/v2/subnets/requests.go new file mode 100644 index 0000000..b3755bc --- /dev/null +++ b/v4/ecl/network/v2/subnets/requests.go @@ -0,0 +1,238 @@ +package subnets + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// 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 { + CIDR string `q:"cidr"` + Description string `q:"description"` + GatewayIP string `q:"gateway_ip"` + ID string `q:"id"` + IPVersion int `q:"ip_version"` + IPv6AddressMode string `q:"ipv6_address_mode"` + IPv6RAMode string `q:"ipv6_ra_mode"` + Name string `q:"name"` + NetworkID string `q:"network_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 *eclcloud.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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + // CIDR is the address CIDR of the subnet. + CIDR string `json:"cidr" required:"true"` + // Description is description + Description string `json:"description,omitempty"` + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes []HostRoute `json:"host_routes,omitempty"` + // IPVersion is the IP version for the subnet. + IPVersion eclcloud.IPVersion `json:"ip_version,omitempty"` + // Name is a human-readable name of the subnet. + Name string `json:"name,omitempty"` + // NetworkID is the UUID of the network the subnet will be associated with. + NetworkID string `json:"network_id" required:"true"` + // NTPServers are List of ntp servers. + NTPServers []string `json:"ntp_servers,omitempty"` + // Tags are tags + Tags map[string]string `json:"tags,omitempty"` + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSubnetCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSubnetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// 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 is a human-readable name of the subnet. + Name *string `json:"name,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // Description is description + Description *string `json:"description,omitempty"` + + // NTPServers are List of ntp servers. + NTPServers *[]string `json:"ntp_servers,omitempty"` + + // Tags are tags + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToSubnetUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + b, err := eclcloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSubnetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a subnet's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractSubnets(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "subnet"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "subnet"} + } +} diff --git a/v4/ecl/network/v2/subnets/results.go b/v4/ecl/network/v2/subnets/results.go new file mode 100644 index 0000000..b0e526f --- /dev/null +++ b/v4/ecl/network/v2/subnets/results.go @@ -0,0 +1,152 @@ +package subnets + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + var s struct { + Subnet *Subnet `json:"subnet"` + } + err := r.ExtractInto(&s) + return s.Subnet, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Subnet. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Subnet. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Subnet. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.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 `json:"id"` + + // UUID of the parent network. + NetworkID string `json:"network_id"` + + // Human-readable name for the subnet. Might not be unique. + Name string `json:"name"` + + // IP version, either `4' or `6'. + IPVersion int `json:"ip_version"` + + // CIDR representing IP range for this subnet, based on IP version. + CIDR string `json:"cidr"` + + // Default gateway used by devices in this subnet. + GatewayIP string `json:"gateway_ip"` + + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `json:"dns_nameservers"` + + // Sub-ranges of CIDR available for dynamic allocation to ports. + // See AllocationPool. + AllocationPools []AllocationPool `json:"allocation_pools"` + + // Routes that should be used by devices with IPs from this subnet + // (not including local subnet route). + HostRoutes []HostRoute `json:"host_routes"` + + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `json:"enable_dhcp"` + + // TenantID is the project owner of the subnet. + TenantID string `json:"tenant_id"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode"` + + // Description is description + Description string `json:"description"` + + // NTPServers are List of ntp servers. + NTPServers []string `json:"ntp_servers"` + + // Status is Status + Status string `json:"status"` + + // Tags optionally set via extensions/attributestags + Tags map[string]string `json:"tags"` +} + +// 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 (r SubnetPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"subnets_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (r SubnetPage) IsEmpty() (bool, error) { + is, err := ExtractSubnets(r) + return len(is) == 0, err +} + +// 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(r pagination.Page) ([]Subnet, error) { + var s struct { + Subnets []Subnet `json:"subnets"` + } + err := (r.(SubnetPage)).ExtractInto(&s) + return s.Subnets, err +} diff --git a/v4/ecl/network/v2/subnets/testing/doc.go b/v4/ecl/network/v2/subnets/testing/doc.go new file mode 100644 index 0000000..bf82f4e --- /dev/null +++ b/v4/ecl/network/v2/subnets/testing/doc.go @@ -0,0 +1,2 @@ +// ports unit tests +package testing diff --git a/v4/ecl/network/v2/subnets/testing/fixtures.go b/v4/ecl/network/v2/subnets/testing/fixtures.go new file mode 100644 index 0000000..e33bb66 --- /dev/null +++ b/v4/ecl/network/v2/subnets/testing/fixtures.go @@ -0,0 +1,246 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/subnets" +) + +const ListResponse = ` +{ + "subnets": [ + { + "allocation_pools": [ + { + "end": "192.168.2.254", + "start": "192.168.2.2" + } + ], + "cidr": "192.168.2.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.2.1", + "host_routes": [], + "id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + }, + { + "allocation_pools": [ + { + "end": "192.168.10.254", + "start": "192.168.10.2" + } + ], + "cidr": "192.168.10.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.10.1", + "host_routes": [], + "id": "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + ] + } +` +const GetResponse = ` +{ + "subnet": { + "allocation_pools": [ + { + "end": "192.168.2.254", + "start": "192.168.2.2" + } + ], + "cidr": "192.168.2.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.2.1", + "host_routes": [], + "id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } + ` +const CreateResponse = ` +{ + "subnet": { + "allocation_pools": [ + { + "end": "192.168.10.254", + "start": "192.168.10.2" + } + ], + "cidr": "192.168.10.0/24", + "description": "", + "dns_nameservers": [ + "0.0.0.0" + ], + "enable_dhcp": true, + "gateway_ip": "192.168.10.1", + "host_routes": [], + "id": "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + "ip_version": 4, + "name": "", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } + ` +const CreateRequest = ` +{ + "subnet": { + "cidr": "192.168.10.0/24", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782" + } +} +` +const UpdateResponse = ` +{ + "subnet": { + "allocation_pools": [ + { + "end": "192.168.2.254", + "start": "192.168.2.2" + } + ], + "cidr": "192.168.2.0/24", + "description": "UPDATED", + "dns_nameservers": [ + "0.0.0.0", + "1.1.1.1" + ], + "enable_dhcp": false, + "gateway_ip": "192.168.10.1", + "host_routes": [ + { + "destination": "10.2.0.0/24", + "nexthop": "10.1.0.10" + } + ], + "id": "ab49eb24-667f-4a4e-9421-b4d915bff416", + "ip_version": 4, + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "name": "UPDATED", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "ntp_servers": [ + "2.2.2.2" + ], + "status": "PENDING_UPDATE", + "tags": { + "updated": "true" + }, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } + } +` +const UpdateRequest = ` +{ + "subnet": { + "description": "UPDATED", + "dns_nameservers": [ + "0.0.0.0", + "1.1.1.1" + ], + "enable_dhcp": false, + "gateway_ip": "192.168.10.1", + "host_routes": [{ + "destination": "10.2.0.0/24", + "nexthop": "10.1.0.10" + }], + "name": "UPDATED", + "ntp_servers": [ + "2.2.2.2" + ], + "tags": { + "updated": "true" + } + } +} +` + +var Subnet1 = subnets.Subnet{ + AllocationPools: []subnets.AllocationPool{ + { + End: "192.168.2.254", + Start: "192.168.2.2", + }, + }, + CIDR: "192.168.2.0/24", + Description: "", + DNSNameservers: []string{ + "0.0.0.0", + }, + EnableDHCP: true, + GatewayIP: "192.168.2.1", + HostRoutes: []subnets.HostRoute{}, + ID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + IPVersion: 4, + Name: "", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + NTPServers: []string{}, + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var Subnet2 = subnets.Subnet{ + AllocationPools: []subnets.AllocationPool{ + { + End: "192.168.10.254", + Start: "192.168.10.2", + }, + }, + CIDR: "192.168.10.0/24", + Description: "", + DNSNameservers: []string{ + "0.0.0.0", + }, + EnableDHCP: true, + GatewayIP: "192.168.10.1", + HostRoutes: []subnets.HostRoute{}, + ID: "f6aa2d33-f3ae-4c4e-82f7-0d4ab4c67678", + IPVersion: 4, + Name: "", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + NTPServers: []string{}, + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", +} + +var ExpectedSubnetSlice = []subnets.Subnet{Subnet1, Subnet2} diff --git a/v4/ecl/network/v2/subnets/testing/request_test.go b/v4/ecl/network/v2/subnets/testing/request_test.go new file mode 100644 index 0000000..1e798a5 --- /dev/null +++ b/v4/ecl/network/v2/subnets/testing/request_test.go @@ -0,0 +1,175 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/subnets" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListSubnet(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, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + subnets.List(client, subnets.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := subnets.ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extrace ports: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedSubnetSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, GetResponse) + }) + + s, err := subnets.Get(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Subnet1, s) +} + +func TestCreateSubnet(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, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := &subnets.CreateOpts{ + CIDR: "192.168.10.0/24", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + } + s, err := subnets.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &Subnet2, s) +} + +func TestRequiredCreateOptsSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + res := subnets.Create(fake.ServiceClient(), subnets.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdateSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/ab49eb24-667f-4a4e-9421-b4d915bff416", 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, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "UPDATED" + dnsNameservers := []string{ + "0.0.0.0", + "1.1.1.1", + } + enableDHCP := false + gatewayIP := "192.168.10.1" + hostRoutes := []subnets.HostRoute{{ + DestinationCIDR: "10.2.0.0/24", + NextHop: "10.1.0.10", + }} + name := "UPDATED" + ntpServers := []string{ + "2.2.2.2", + } + tags := map[string]string{ + "updated": "true", + } + + options := subnets.UpdateOpts{ + Description: &description, + DNSNameservers: dnsNameservers, + EnableDHCP: &enableDHCP, + GatewayIP: &gatewayIP, + HostRoutes: &hostRoutes, + Name: &name, + NTPServers: &ntpServers, + Tags: &tags, + } + + s, err := subnets.Update(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, description, s.Description) + th.CheckDeepEquals(t, dnsNameservers, s.DNSNameservers) + th.CheckEquals(t, enableDHCP, s.EnableDHCP) + th.CheckEquals(t, gatewayIP, s.GatewayIP) + th.CheckDeepEquals(t, hostRoutes, s.HostRoutes) + th.CheckEquals(t, name, s.Name) + th.CheckDeepEquals(t, ntpServers, s.NTPServers) + th.CheckDeepEquals(t, tags, s.Tags) +} + +func TestDeleteSubnet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/ab49eb24-667f-4a4e-9421-b4d915bff416", 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 := subnets.Delete(fake.ServiceClient(), "ab49eb24-667f-4a4e-9421-b4d915bff416") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/subnets/urls.go b/v4/ecl/network/v2/subnets/urls.go new file mode 100644 index 0000000..22db577 --- /dev/null +++ b/v4/ecl/network/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/doc.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/doc.go new file mode 100644 index 0000000..1058a9b --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/doc.go @@ -0,0 +1,79 @@ +/* +Package tenant_connection_requests manages and retrieves Tenant Connection Request in the Enterprise Cloud Provider Connectivity Service. + +Example to List Tenant Connection Request + + allPages, err := tenant_connection_requests.List(tcrClient).AllPages() + if err != nil { + panic(err) + } + + allTenantConnectionRequests, err := tenant_connection_requests.ExtractTenantConnectionRequests(allPages) + if err != nil { + panic(err) + } + + for _, tenantConnectionRequest := range allTenantConnectionRequests { + fmt.Printf("%+v\n", tenantConnectionRequest) + } + +Example to Get a Tenant Connection Request + + tenant_connection_request_id := "85a1dc30-2e48-11ea-9e55-525403060300" + + tenantConnectionRequest, err := tenant_connection_requests.Get(tcrClient, tenant_connection_request_id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", tenantConnectionRequest) + +Example to Create a Tenant Connection Request + + createOpts := tenant_connection_requests.CreateOpts{ + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + Name: "create_test_name", + Description: "create_test_desc", + Tags: map[string]string{"foo", "bar"}, + } + + result := tenant_connection_requests.Create(tcrClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a Tenant Connection Request + + tenant_connection_request_id := "85a1dc30-2e48-11ea-9e55-525403060300" + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: "update_test_name", + Description: "update_test_desc", + Tags: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + NameOther: "update_test_name_other", + DescriptionOther: "update_test_desc_other", + TagsOther: map[string]string{ + "keyword1": "value1", + "keyword2": "value2", + }, + } + + result := tenant_connection_requests.Update(tcrClient, tenant_connection_request_id, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a Tenant Connection Request + + tenant_connection_request_id := "85a1dc30-2e48-11ea-9e55-525403060300" + + result := tenant_connection_requests.Delete(tcrClient, tenant_connection_request_id) + if result.Err != nil { + panic(result.Err) + } + +*/ +package tenant_connection_requests diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/requests.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/requests.go new file mode 100644 index 0000000..567c148 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/requests.go @@ -0,0 +1,129 @@ +package tenant_connection_requests + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToTenantConnectionRequestListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + TenantConnectionRequestID string `q:"tenant_connection_request_id"` + Status string `q:"status"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + NameOther string `q:"name_other"` + TenantIDOther string `q:"tenant_id_other"` + NetworkID string `q:"network_id"` + ApprovalRequestID string `q:"approval_request_id"` +} + +// ToTenantConnectionRequestListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantConnectionRequestListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Tenant Connection Requests. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantConnectionRequestListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantConnectionRequestPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of an Tenant Connection Request. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToTenantConnectionRequestCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a Tenant Connection Request. +type CreateOpts struct { + TenantIDOther string `json:"tenant_id_other" required:"true"` + NetworkID string `json:"network_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +// ToTenantConnectionRequestCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToTenantConnectionRequestCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection_request") +} + +// Create creates a new Tenant Connection Request. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantConnectionRequestCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a Tenant Connection Request. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToTenantConnectionRequestUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a Tenant Connection Request. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` + NameOther *string `json:"name_other,omitempty"` + DescriptionOther *string `json:"description_other,omitempty"` + TagsOther *map[string]string `json:"tags_other,omitempty"` +} + +// ToResourceUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToTenantConnectionRequestUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection_request") +} + +// Update modifies the attributes of a Tenant Connection Request. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantConnectionRequestUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/results.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/results.go new file mode 100644 index 0000000..1e5d3ae --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/results.go @@ -0,0 +1,80 @@ +package tenant_connection_requests + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// TenantConnectionRequest represents Tenant Connection Request. +type TenantConnectionRequest struct { + ID string `json:"id"` + Status string `json:"status"` + Name string `json:"name"` + Description string `json:"description"` + Tags map[string]string `json:"tags"` + TenantID string `json:"tenant_id"` + NameOther string `json:"name_other"` + DescriptionOther string `json:"description_other"` + TagsOther map[string]string `json:"tags_other"` + TenantIDOther string `json:"tenant_id_other"` + NetworkID string `json:"network_id"` + ApprovalRequestID string `json:"approval_request_id"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Tenant Connection Request. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Tenant Connection Request. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Tenant Connection Request. +type UpdateResult struct { + commonResult +} + +// TenantConnectionRequestPage is a single page of Tenant Connection Request results. +type TenantConnectionRequestPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenant Connection Request contains any results. +func (r TenantConnectionRequestPage) IsEmpty() (bool, error) { + resources, err := ExtractTenantConnectionRequests(r) + return len(resources) == 0, err +} + +// ExtractTenantConnectionRequests returns a slice of Tenant Connection Requests contained in a +// single page of results. +func ExtractTenantConnectionRequests(r pagination.Page) ([]TenantConnectionRequest, error) { + var s struct { + TenantConnectionRequest []TenantConnectionRequest `json:"tenant_connection_requests"` + } + err := (r.(TenantConnectionRequestPage)).ExtractInto(&s) + return s.TenantConnectionRequest, err +} + +// Extract interprets any commonResult as a Tenant Connection Request. +func (r commonResult) Extract() (*TenantConnectionRequest, error) { + var s struct { + TenantConnectionRequest *TenantConnectionRequest `json:"tenant_connection_request"` + } + err := r.ExtractInto(&s) + return s.TenantConnectionRequest, err +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/doc.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/doc.go new file mode 100644 index 0000000..7370972 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/doc.go @@ -0,0 +1,2 @@ +// Tenant Connection Request unit tests +package testing diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/fixtures.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/fixtures.go new file mode 100644 index 0000000..166e81a --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/fixtures.go @@ -0,0 +1,457 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/provider_connectivity/v2/tenant_connection_requests" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +// ListResult provides a single page of tenant_connection_request results. +const ListResult = ` +{ + "tenant_connection_requests": [ + { + "id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "name": "test_name1", + "description": "test_desc1", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "", + "description_other": "", + "tags_other": {}, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registering", + "approval_request_id": "req0000010454" + }, + { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "created_name", + "description": "created_desc", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": { + "test_tags_other2": "test2" + }, + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "created_name", + "description": "created_desc", + "tags": { + "test_tags2":"test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": { + "test_tags_other2":"test2" + }, + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "tenant_connection_request": { + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "name": "test_name1", + "description": "test_desc1", + "tags": { + "test_tags1": "test1" + } + } +} +` + +// CreateResponse provides the output from a Create request. +const CreateResponse = ` +{ + "tenant_connection_request": { + "id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "name": "test_name1", + "description": "test_desc1", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "", + "description_other": "", + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "tags_other": {}, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registering", + "approval_request_id": "req0000010454" + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "tenant_connection_request":{ + "name": "updated_name", + "description": "updated_desc", + "tags": { + "k2":"v2" + } + } +} +` + +// UpdateResult provides an update result. +const UpdateResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "updated_name", + "description": "updated_desc", + "tags": { + "k2": "v2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "test_tags_other2": "test2" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// UpdateOtherMetadataRequest provides the input to as Update to other metadata request. +const UpdateOtherMetadataRequest = ` +{ + "tenant_connection_request":{ + "name_other": "updated_name_other", + "description_other": "updated_desc_other", + "tags_other": { + "k3":"v3" + } + } +} +` + +// UpdateOtherMetadataResult provides an update to other metadata result. +const UpdateOtherMetadataResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "created_name", + "description": "created_desc", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "updated_name_other", + "description_other": "updated_desc_other", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "k3": "v3" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// UpdateBlankRequest provides the input to as Update with blank request. +const UpdateBlankRequest = ` +{ + "tenant_connection_request":{ + "name": "", + "description": "", + "tags": {} + } +} +` + +// UpdateBlankResult provides an update with blank result. +const UpdateBlankResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "", + "description": "", + "tags": {}, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "test_tags_other2": "test2" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// UpdateNilRequest provides the input to as Update with nil request. +const UpdateNilRequest = ` +{ + "tenant_connection_request":{ + "name": "nilupdate" + } +} +` + +// UpdateNilResult provides an update with nil result. +const UpdateNilResult = ` +{ + "tenant_connection_request": { + "id": "90381138-b572-11e7-9391-0050569c850d", + "name": "nilupdate", + "description": "created_desc", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tags_other": { + "test_tags_other2": "test2" + }, + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "status": "registered", + "approval_request_id": "req0000010363" + } +} +` + +// FirstTenantConnectionRequest is the first tenant_connection_request in the List request. +var FirstTenantConnectionRequest = tenant_connection_requests.TenantConnectionRequest{ + ID: "5fbcc350-bd33-11e7-afb6-0050569c850d", + Status: "registering", + Name: "test_name1", + Description: "test_desc1", + Tags: map[string]string{"test_tags1": "test1"}, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{}, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010454", +} + +// SecondTenantConnectionRequest is the second tenant_connection_request in the List request. +var SecondTenantConnectionRequest = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "created_name", + Description: "created_desc", + Tags: map[string]string{"test_tags2": "test2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestUpdated is how second tenant_connection_request should look after an Update. +var SecondTenantConnectionRequestUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "updated_name", + Description: "updated_desc", + Tags: map[string]string{"k2": "v2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestOtherMetadataUpdated is how second tenant_connection_request should look after an Update to other metadata. +var SecondTenantConnectionRequestOtherMetadataUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "created_name", + Description: "created_desc", + Tags: map[string]string{"test_tags2": "test2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "updated_name_other", + DescriptionOther: "updated_desc_other", + TagsOther: map[string]string{"k3": "v3"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestBlankUpdated is how second tenant_connection_request should look after an Update with blank. +var SecondTenantConnectionRequestBlankUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "", + Description: "", + Tags: map[string]string{}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// SecondTenantConnectionRequestNilUpdated is how second tenant_connection_request should look after an Update with nil. +var SecondTenantConnectionRequestNilUpdated = tenant_connection_requests.TenantConnectionRequest{ + ID: "90381138-b572-11e7-9391-0050569c850d", + Status: "registered", + Name: "nilupdate", + Description: "created_desc", + Tags: map[string]string{"test_tags2": "test2"}, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + ApprovalRequestID: "req0000010363", +} + +// ExpectedTenantConnectionRequestsSlice is the slice of tenant_connection_request expected to be returned from ListResult. +var ExpectedTenantConnectionRequestsSlice = []tenant_connection_requests.TenantConnectionRequest{FirstTenantConnectionRequest, SecondTenantConnectionRequest} + +// HandleListTenantConnectionRequestsSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that responds with a list of two tenant_connection_requests. +func HandleListTenantConnectionRequestsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connection_requests", 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.Fprint(w, ListResult) + }) +} + +// HandleGetTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that responds with a single tenant_connection_request. +func HandleGetTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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.Fprint(w, GetResult) + }) +} + +// HandleCreateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request creation. +func HandleCreateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connection_requests", 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, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateResponse) + }) +} + +// HandleDeleteTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request deletion. +func HandleDeleteTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", FirstTenantConnectionRequest.ID), 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) + }) +} + +// HandleUpdateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update. +func HandleUpdateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateResult) + }) +} + +// HandleUpdateOtherMetadataTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update to other metadata result. +func HandleUpdateOtherMetadataTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateOtherMetadataRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateOtherMetadataResult) + }) +} + +// HandleBlankUpdateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update with blank. +func HandleBlankUpdateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateBlankRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateBlankResult) + }) +} + +// HandleNilUpdateTenantConnectionRequestSuccessfully creates an HTTP handler at `/tenant_connection_requests` on the +// test handler mux that tests tenant_connection_request update with nil. +func HandleNilUpdateTenantConnectionRequestSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connection_requests/%s", SecondTenantConnectionRequest.ID), 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, UpdateNilRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateNilResult) + }) +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/requests_test.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/requests_test.go new file mode 100644 index 0000000..7ac3023 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/testing/requests_test.go @@ -0,0 +1,155 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/provider_connectivity/v2/tenant_connection_requests" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListTenantConnectionRequests(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionRequestsSuccessfully(t) + + count := 0 + err := tenant_connection_requests.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := tenant_connection_requests.ExtractTenantConnectionRequests(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedTenantConnectionRequestsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListTenantConnectionRequestsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionRequestsSuccessfully(t) + + allPages, err := tenant_connection_requests.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := tenant_connection_requests.ExtractTenantConnectionRequests(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedTenantConnectionRequestsSlice, actual) +} + +func TestGetTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetTenantConnectionRequestSuccessfully(t) + + actual, err := tenant_connection_requests.Get(client.ServiceClient(), SecondTenantConnectionRequest.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequest, *actual) +} + +func TestCreateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionRequestSuccessfully(t) + + createOpts := tenant_connection_requests.CreateOpts{ + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + Name: "test_name1", + Description: "test_desc1", + Tags: map[string]string{"test_tags1": "test1"}, + } + + actual, err := tenant_connection_requests.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &FirstTenantConnectionRequest, actual) +} + +func TestDeleteTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteTenantConnectionRequestSuccessfully(t) + + res := tenant_connection_requests.Delete(client.ServiceClient(), FirstTenantConnectionRequest.ID) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateTenantConnectionRequestSuccessfully(t) + + name := "updated_name" + description := "updated_desc" + tags := map[string]string{"k2": "v2"} + + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestUpdated, *actual) +} + +func TestUpdateOtherMetadataTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateOtherMetadataTenantConnectionRequestSuccessfully(t) + + nameOther := "updated_name_other" + descriptionOther := "updated_desc_other" + tagsOther := map[string]string{"k3": "v3"} + + updateOpts := tenant_connection_requests.UpdateOpts{ + NameOther: &nameOther, + DescriptionOther: &descriptionOther, + TagsOther: &tagsOther, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestOtherMetadataUpdated, *actual) +} + +func TestBlankUpdateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleBlankUpdateTenantConnectionRequestSuccessfully(t) + + name := "" + description := "" + tags := map[string]string{} + + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestBlankUpdated, *actual) +} + +func TestNilUpdateTenantConnectionRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleNilUpdateTenantConnectionRequestSuccessfully(t) + + name := "nilupdate" + + updateOpts := tenant_connection_requests.UpdateOpts{ + Name: &name, + } + + actual, err := tenant_connection_requests.Update(client.ServiceClient(), SecondTenantConnectionRequest.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionRequestNilUpdated, *actual) +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connection_requests/urls.go b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/urls.go new file mode 100644 index 0000000..5f28f7c --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connection_requests/urls.go @@ -0,0 +1,23 @@ +package tenant_connection_requests + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connection_requests") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connection_requests", id) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connection_requests") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connection_requests", id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connection_requests", id) +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/doc.go b/v4/ecl/provider_connectivity/v2/tenant_connections/doc.go new file mode 100644 index 0000000..3d82c48 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/doc.go @@ -0,0 +1,87 @@ +/* +Package tenant_connections manages and retrieves Tenant Connection in the Enterprise Cloud Provider Connectivity Service. + +Example to List Tenant Connection + + allPages, err := tenant_connections.List(tcClient).AllPages() + if err != nil { + panic(err) + } + + allTenantConnections, err := tenant_connections.ExtractTenantConnections(allPages) + if err != nil { + panic(err) + } + + for _, tenantConnection := range allTenantConnections { + fmt.Printf("%+v\n", tenantConnection) + } + +Example to Get a Tenant Connection + + tenant_connection_id := "ea5d975c-bd31-11e7-bcac-0050569c850d" + + tenantConnection, err := tenant_connections.Get(tcClient, tenant_connection_id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", tenantConnection) + +Example to Create a Tenant Connection + + createOpts := tenant_connections.CreateOpts{ + Name: "create_test_name", + Description: "create_test_desc", + Tags: map[string]string{ + "test_tags": "test", + }, + TenantConnectionRequestID: "21b344d8-be11-11e7-bf3c-0050569c850d", + DeviceType: "ECL::VirtualNetworkAppliance::VSRX", + DeviceID: "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + DeviceInterfaceID: "interface_2", + AttachmentOpts: tenant_connections.Vna{ + FixedIPs: []tenant_connections.VnaFixedIPs{ + IPAddress: "192.168.1.3", + }, + }, + } + + result := tenant_connections.Create(tcClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a Tenant Connection + + tenant_connection_id := "ea5d975c-bd31-11e7-bcac-0050569c850d" + + updateOpts := tenant_connections.UpdateOpts{ + Name: "test_name", + Description: "test_desc", + Tags: map[string]string{ + "test_tags": "test", + }, + NameOther: "test_name_other", + DescriptionOther: "test_desc_other", + TagsOther: map[string]string{ + "test_tags_other": "test_other", + }, + } + + result := tenant_connections.Update(tcClient, tenant_connection_id, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a Tenant Connection + + tenant_connection_id := "ea5d975c-bd31-11e7-bcac-0050569c850d" + + result := tenant_connections.Delete(tcClient, tenant_connection_id) + if result.Err != nil { + panic(result.Err) + } + +*/ +package tenant_connections diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/requests.go b/v4/ecl/provider_connectivity/v2/tenant_connections/requests.go new file mode 100644 index 0000000..af76b35 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/requests.go @@ -0,0 +1,171 @@ +package tenant_connections + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToTenantConnectionListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + TenantConnectionRequestID string `q:"tenant_connection_request_id"` + Status string `q:"status"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + NameOther string `q:"name_other"` + TenantIDOther string `q:"tenant_id_other"` + NetworkID string `q:"network_id"` + DeviceType string `q:"device_type"` + DeviceID string `q:"device_id"` + DeviceInterfaceID string `q:"device_interface_id"` + PortID string `q:"port_id"` +} + +// ToTenantConnectionListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantConnectionListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves a list of Tenant Connection. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantConnectionListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantConnectionPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of an Tenant Connection. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToTenantConnectionCreateMap() (map[string]interface{}, error) +} + +// ServerFixedIPs contains the IP Address and the SubnetID. +type ServerFixedIPs struct { + SubnetID string `json:"subnet_id,omitempty"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// VnaFixedIPs represents ip address part of virtual network appliance. +type VnaFixedIPs struct { + IPAddress string `json:"ip_address,omitempty"` +} + +// Vna represents the parameter when device_type is VSRX. +type Vna struct { + FixedIPs []VnaFixedIPs `json:"fixed_ips,omitempty"` +} + +// ComputeServer represents the parameter when device_type is Compute Server. +type ComputeServer struct { + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + FixedIPs []ServerFixedIPs `json:"fixed_ips,omitempty"` +} + +// BaremetalServer represents the parameter when device_type is Baremetal Server. +type BaremetalServer struct { + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + FixedIPs []ServerFixedIPs `json:"fixed_ips,omitempty"` + SegmentationID int `json:"segmentation_id,omitempty"` + SegmentationType string `json:"segmentation_type,omitempty"` +} + +// CreateOpts provides options used to create a Tenant Connection. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantConnectionRequestID string `json:"tenant_connection_request_id" required:"true"` + DeviceType string `json:"device_type" required:"true"` + DeviceID string `json:"device_id" required:"true"` + DeviceInterfaceID string `json:"device_interface_id,omitempty"` + AttachmentOpts interface{} `json:"attachment_opts,omitempty"` +} + +// ToTenantConnectionCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToTenantConnectionCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection") +} + +// Create creates a new Tenant Connection. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantConnectionCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a Tenant Connection. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToTenantConnectionUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a Tenant Connection. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` + NameOther *string `json:"name_other,omitempty"` + DescriptionOther *string `json:"description_other,omitempty"` + TagsOther *map[string]string `json:"tags_other,omitempty"` +} + +// ToTenantConnectionUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToTenantConnectionUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "tenant_connection") +} + +// Update modifies the attributes of a Tenant Connection. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantConnectionUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/results.go b/v4/ecl/provider_connectivity/v2/tenant_connections/results.go new file mode 100644 index 0000000..128b258 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/results.go @@ -0,0 +1,87 @@ +package tenant_connections + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// TenantConnection represents Tenant Connection. +// TagsOther is interface{} because the data type returned by Create API depends on the value of device_type. +// When the device_type of Create Request is ECL::Compute::Server, the data type of tags_other is map[]. +// When the device_type of Create Request is ECL::Baremetal::Server or ECL::VirtualNetworkAppliance::VSRX, the data type of tags_other is string. +type TenantConnection struct { + ID string `json:"id"` + TenantConnectionRequestID string `json:"tenant_connection_request_id"` + Name string `json:"name"` + Description string `json:"description"` + Tags map[string]string `json:"tags"` + TenantID string `json:"tenant_id"` + NameOther string `json:"name_other"` + DescriptionOther string `json:"description_other"` + TagsOther interface{} `json:"tags_other"` + TenantIDOther string `json:"tenant_id_other"` + NetworkID string `json:"network_id"` + DeviceType string `json:"device_type"` + DeviceID string `json:"device_id"` + DeviceInterfaceID string `json:"device_interface_id"` + PortID string `json:"port_id"` + Status string `json:"status"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Tenant Connection. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Tenant Connection. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Tenant Connection. +type UpdateResult struct { + commonResult +} + +// TenantConnectionPage is a single page of Tenant Connection results. +type TenantConnectionPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenant Connection contains any results. +func (r TenantConnectionPage) IsEmpty() (bool, error) { + resources, err := ExtractTenantConnections(r) + return len(resources) == 0, err +} + +// ExtractTenantConnections returns a slice of Tenant Connections contained in a +// single page of results. +func ExtractTenantConnections(r pagination.Page) ([]TenantConnection, error) { + var s struct { + TenantConnection []TenantConnection `json:"tenant_connections"` + } + err := (r.(TenantConnectionPage)).ExtractInto(&s) + return s.TenantConnection, err +} + +// Extract interprets any commonResult as a Tenant Connection. +func (r commonResult) Extract() (*TenantConnection, error) { + var s struct { + TenantConnection *TenantConnection `json:"tenant_connection"` + } + err := r.ExtractInto(&s) + return s.TenantConnection, err +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/testing/doc.go b/v4/ecl/provider_connectivity/v2/tenant_connections/testing/doc.go new file mode 100644 index 0000000..fb04ea5 --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/testing/doc.go @@ -0,0 +1,2 @@ +// Tenant Connection unit tests +package testing diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/testing/fixtures.go b/v4/ecl/provider_connectivity/v2/tenant_connections/testing/fixtures.go new file mode 100644 index 0000000..344feae --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/testing/fixtures.go @@ -0,0 +1,722 @@ +package testing + +import ( + "fmt" + "github.com/nttcom/eclcloud/v4/ecl/provider_connectivity/v2/tenant_connections" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" + "net/http" + "testing" +) + +// ListResult provides a single page of tenant_connection results. +const ListResult = ` +{ + "tenant_connections": [ + { + "id": "2a23e5a6-bd34-11e7-afb6-0050569c850d", + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tenant_connection_request_id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "device_type": "ECL::Compute::Server", + "device_id": "8c235a3b-8dee-41a1-b81a-64e06edc0986", + "device_interface_id": "", + "port_id": "b404ed73-9438-41a1-91ed-49d0e403be64", + "status": "creating", + "name": "test_name_1", + "description": "test_desc_1", + "tags": { + "test_tags1": "test1" + }, + "name_other": "", + "description_other": "", + "tags_other": {} + }, + { + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down", + "name": "test_name_2", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + } + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "tenant_connection": { + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down", + "name": "test_name_2", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + } + } +} +` + +// CreateAttachComputeServerRequest provides the input to a Create request. +const CreateAttachComputeServerRequest = ` +{ + "tenant_connection": { + "name": "test_name_1", + "description": "test_desc_1", + "tags": { + "test_tags1": "test1" + }, + "tenant_connection_request_id": "21b344d8-be11-11e7-bf3c-0050569c850d", + "device_type": "ECL::Compute::Server", + "device_id": "8c235a3b-8dee-41a1-b81a-64e06edc0986", + "attachment_opts": { + "fixed_ips": [ + { + "ip_address": "192.168.1.1", + "subnet_id": "1f424165-2202-4022-ad70-0fa6f9ec99e1" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "192.168.1.2", + "mac_address": "11:22:33:aa:bb:cc" + } + ] + } + } +} +` + +// CreateAttachComputeServerResponse provides the output from a Create request. +const CreateAttachComputeServerResponse = ` +{ + "tenant_connection":{ + "id": "2a23e5a6-bd34-11e7-afb6-0050569c850d", + "tenant_connection_request_id": "5fbcc350-bd33-11e7-afb6-0050569c850d", + "name": "test_name_1", + "description": "test_desc_1", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": {}, + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + "device_type": "ECL::Compute::Server", + "device_id": "8c235a3b-8dee-41a1-b81a-64e06edc0986", + "device_interface_id": "", + "port_id": "b404ed73-9438-41a1-91ed-49d0e403be64", + "status": "creating" + } +} +` + +// CreateAttachBaremetalServerRequest provides the input to a Create request. +const CreateAttachBaremetalServerRequest = ` +{ + "tenant_connection": { + "name": "attach_bare_name", + "description": "attach_bare_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_connection_request_id": "147c4ffa-481e-11ea-8088-525400060300", + "device_type": "ECL::Baremetal::Server", + "device_interface_id": "46eb7624-d462-46c2-8ac7-f988a15d3280", + "device_id": "0acab22f-8993-451c-8a6b-398b0244f578", + "attachment_opts": { + "segmentation_id": 10, + "segmentation_type": "flat", + "fixed_ips": [ + { + "ip_address": "192.168.1.1", + "subnet_id": "1f424165-2202-4022-ad70-0fa6f9ec99e1" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "192.168.1.2", + "mac_address": "11:22:33:aa:bb:cc" + } + ] + } + } +} +` + +// CreateAttachBaremetalServerResponse provides the output from a Create request. +const CreateAttachBaremetalServerResponse = ` +{ + "tenant_connection":{ + "id": "0d956a2e-4958-11ea-8088-525400060300", + "tenant_connection_request_id": "147c4ffa-481e-11ea-8088-525400060300", + "name": "attach_bare_name", + "description": "attach_bare_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": "{}", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "061dbaa9-a3e0-4343-b3fc-0a619db66854", + "device_type": "ECL::Baremetal::Server", + "device_id": "0acab22f-8993-451c-8a6b-398b0244f578", + "device_interface_id": "46eb7624-d462-46c2-8ac7-f988a15d3280", + "port_id": "87449d66-4e99-4cf7-9b93-9f153548ccc7", + "status": "creating" + } +} +` + +// CreateAttachVnaRequest provides the input to a Create request. +const CreateAttachVnaRequest = ` +{ + "tenant_connection": { + "name": "attach_vna_name", + "description": "attach_vna_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_connection_request_id": "67d76b00-3804-11ea-8088-525400060300", + "device_type": "ECL::VirtualNetworkAppliance::VSRX", + "device_interface_id": "interface_2", + "device_id": "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + "attachment_opts": { + "fixed_ips": [ + { + "ip_address": "192.168.1.3" + } + ] + } + } +} +` + +// CreateAttachVnaResponse provides the output from a Create request. +const CreateAttachVnaResponse = ` +{ + "tenant_connection":{ + "id": "f6331886-3804-11ea-95a8-525400060400", + "tenant_connection_request_id": "67d76b00-3804-11ea-8088-525400060300", + "name": "attach_vna_name", + "description": "attach_vna_desc", + "tags": { + "test_tags1": "test1" + }, + "tenant_id": "7e91b19b9baa423793ee74a8e1ff2be1", + "name_other": "", + "description_other": "", + "tags_other": "{}", + "tenant_id_other": "c7f3a68a73e845d4ba6a42fb80fce03f", + "network_id": "061dbaa9-a3e0-4343-b3fc-0a619db66854", + "device_interface_id": "interface_2", + "device_type": "ECL::VirtualNetworkAppliance::VSRX", + "device_id": "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + "port_id": "", + "status": "active" + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "tenant_connection": { + "name": "update_name", + "description": "update_desc", + "tags": { + "update_tags": "update" + } + } +} +` + +// UpdateResult provides an update result. +const UpdateResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "update_name", + "description": "update_desc", + "tags": { + "update_tags": "update" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + }, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// UpdateOtherMetadataRequest provides the input to as Update to other metadata request. +const UpdateOtherMetadataRequest = ` +{ + "tenant_connection": { + "name_other": "update_name_other", + "description_other": "update_desc_other", + "tags_other": { + "test_tags_other": "update" + } + } +} +` + +// UpdateOtherMetadataResult provides an update to other metadata result. +const UpdateOtherMetadataResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "test_name_2", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "update_name_other", + "description_other": "update_desc_other", + "tags_other": { + "test_tags_other": "update" + }, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// UpdateBlankRequest provides the input to as Update with blank request. +const UpdateBlankRequest = ` +{ + "tenant_connection": { + "name": "", + "description": "", + "tags": {} + } +} +` + +// UpdateBlankResult provides an update with blank result. +const UpdateBlankResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "", + "description": "", + "tags": {}, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": {"test_tags_other2": "test2"}, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// UpdateNilRequest provides the input to as Update with nil request. +const UpdateNilRequest = ` +{ + "tenant_connection": { + "name": "nilupdate" + } +} +` + +// UpdateNilResult provides an update with blank with nil result. +const UpdateNilResult = ` +{ + "tenant_connection":{ + "id": "ea5d975c-bd31-11e7-bcac-0050569c850d", + "tenant_connection_request_id": "90381138-b572-11e7-9391-0050569c850d", + "name": "nilupdate", + "description": "test_desc_2", + "tags": { + "test_tags2": "test2" + }, + "tenant_id": "c7f3a68a73e845d4ba6a42fb80fce03f", + "name_other": "test_name_other_2", + "description_other": "test_desc_other_2", + "tags_other": { + "test_tags_other2": "test2" + }, + "tenant_id_other": "7e91b19b9baa423793ee74a8e1ff2be1", + "network_id": "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + "device_type": "ECL::Compute::Server", + "device_id": "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + "device_interface_id": "", + "port_id": "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + "status": "down" + } +} +` + +// FirstTenantConnection is the first tenant_connection in the List request. +var FirstTenantConnection = tenant_connections.TenantConnection{ + ID: "2a23e5a6-bd34-11e7-afb6-0050569c850d", + TenantConnectionRequestID: "5fbcc350-bd33-11e7-afb6-0050569c850d", + Name: "test_name_1", + Description: "test_desc_1", + Tags: map[string]string{ + "test_tags1": "test1", + }, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: map[string]string{}, + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "77cfc6b0-d032-4e5a-b6fb-4cce2537f4d1", + DeviceType: "ECL::Compute::Server", + DeviceID: "8c235a3b-8dee-41a1-b81a-64e06edc0986", + DeviceInterfaceID: "", + PortID: "b404ed73-9438-41a1-91ed-49d0e403be64", + Status: "creating", +} + +// SecondTenantConnection is the second tenant_connection in the List request. +var SecondTenantConnection = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "test_name_2", + Description: "test_desc_2", + Tags: map[string]string{ + "test_tags2": "test2", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{ + "test_tags_other2": "test2", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// CreateTenantConnectionAttachBaremetalServer is the tenant_connection in the Create Attach Baremetal Server request. +var CreateTenantConnectionAttachBaremetalServer = tenant_connections.TenantConnection{ + ID: "0d956a2e-4958-11ea-8088-525400060300", + TenantConnectionRequestID: "147c4ffa-481e-11ea-8088-525400060300", + Name: "attach_bare_name", + Description: "attach_bare_desc", + Tags: map[string]string{ + "test_tags1": "test1", + }, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: "{}", + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "061dbaa9-a3e0-4343-b3fc-0a619db66854", + DeviceType: "ECL::Baremetal::Server", + DeviceID: "0acab22f-8993-451c-8a6b-398b0244f578", + DeviceInterfaceID: "46eb7624-d462-46c2-8ac7-f988a15d3280", + PortID: "87449d66-4e99-4cf7-9b93-9f153548ccc7", + Status: "creating", +} + +// CreateTenantConnectionAttachVna is the tenant_connection in the Create Attach Vna request. +var CreateTenantConnectionAttachVna = tenant_connections.TenantConnection{ + ID: "f6331886-3804-11ea-95a8-525400060400", + TenantConnectionRequestID: "67d76b00-3804-11ea-8088-525400060300", + Name: "attach_vna_name", + Description: "attach_vna_desc", + Tags: map[string]string{ + "test_tags1": "test1", + }, + TenantID: "7e91b19b9baa423793ee74a8e1ff2be1", + NameOther: "", + DescriptionOther: "", + TagsOther: "{}", + TenantIDOther: "c7f3a68a73e845d4ba6a42fb80fce03f", + NetworkID: "061dbaa9-a3e0-4343-b3fc-0a619db66854", + DeviceType: "ECL::VirtualNetworkAppliance::VSRX", + DeviceID: "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + DeviceInterfaceID: "interface_2", + PortID: "", + Status: "active", +} + +// SecondTenantConnectionUpdated is how second tenant_connection should look after an Update. +var SecondTenantConnectionUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "update_name", + Description: "update_desc", + Tags: map[string]string{ + "update_tags": "update", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{ + "test_tags_other2": "test2", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// SecondTenantConnectionOtherMetadataUpdated is how second tenant_connection should look after an Update to other metadata. +var SecondTenantConnectionOtherMetadataUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "test_name_2", + Description: "test_desc_2", + Tags: map[string]string{ + "test_tags2": "test2", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "update_name_other", + DescriptionOther: "update_desc_other", + TagsOther: map[string]string{ + "test_tags_other": "update", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// SecondTenantConnectionBlankUpdated is how second tenant_connection should look after an Update with blank. +var SecondTenantConnectionBlankUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "", + Description: "", + Tags: map[string]string{}, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{"test_tags_other2": "test2"}, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// SecondTenantConnectionNilUpdated is how second tenant_connection should look after an Update with nil. +var SecondTenantConnectionNilUpdated = tenant_connections.TenantConnection{ + ID: "ea5d975c-bd31-11e7-bcac-0050569c850d", + TenantConnectionRequestID: "90381138-b572-11e7-9391-0050569c850d", + Name: "nilupdate", + Description: "test_desc_2", + Tags: map[string]string{ + "test_tags2": "test2", + }, + TenantID: "c7f3a68a73e845d4ba6a42fb80fce03f", + NameOther: "test_name_other_2", + DescriptionOther: "test_desc_other_2", + TagsOther: map[string]string{ + "test_tags_other2": "test2", + }, + TenantIDOther: "7e91b19b9baa423793ee74a8e1ff2be1", + NetworkID: "c4d5fc41-b7e8-4f19-96f4-85299e54373c", + DeviceType: "ECL::Compute::Server", + DeviceID: "7cc34d4b-a345-4e51-b3d9-62540faca7bf", + DeviceInterfaceID: "", + PortID: "c9c3de44-0720-4acd-87c1-9c76f0f77cac", + Status: "down", +} + +// ExpectedTenantConnectionsSlice is the slice of tenant_connection expected to be returned from ListResult. +var ExpectedTenantConnectionsSlice = []tenant_connections.TenantConnection{FirstTenantConnection, SecondTenantConnection} + +// HandleListTenantConnectionsSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that responds with a list of two tenant_connections. +func HandleListTenantConnectionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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.Fprint(w, ListResult) + }) +} + +// HandleGetTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that responds with a single tenant_connection. +func HandleGetTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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.Fprint(w, GetResult) + }) +} + +// HandleCreateTenantConnectionAttachComputeServerSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests creation of tenant_connection with Compute Server attached. +func HandleCreateTenantConnectionAttachComputeServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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, CreateAttachComputeServerRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateAttachComputeServerResponse) + }) +} + +// HandleCreateTenantConnectionAttachBaremetalServerSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests creation of tenant_connection with Baremetal Server attached. +func HandleCreateTenantConnectionAttachBaremetalServerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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, CreateAttachBaremetalServerRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateAttachBaremetalServerResponse) + }) +} + +// HandleCreateTenantConnectionAttachVnaSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that that tests creation of tenant_connection with Vna attached. +func HandleCreateTenantConnectionAttachVnaSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenant_connections", 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, CreateAttachVnaRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, CreateAttachVnaResponse) + }) +} + +// HandleDeleteTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection deletion. +func HandleDeleteTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", FirstTenantConnection.ID), 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) + }) +} + +// HandleUpdateTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update. +func HandleUpdateTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateResult) + }) +} + +// HandleUpdateOtherMetadataTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update to other metadata. +func HandleUpdateOtherMetadataTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateOtherMetadataRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateOtherMetadataResult) + }) +} + +// HandleBlankUpdateTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update with blank. +func HandleBlankUpdateTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateBlankRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateBlankResult) + }) +} + +// HandleNilUpdateTenantConnectionSuccessfully creates an HTTP handler at `/tenant_connections` on the +// test handler mux that tests tenant_connection update with nil. +func HandleNilUpdateTenantConnectionSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/tenant_connections/%s", SecondTenantConnection.ID), 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, UpdateNilRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, UpdateNilResult) + }) +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/testing/requests_test.go b/v4/ecl/provider_connectivity/v2/tenant_connections/testing/requests_test.go new file mode 100644 index 0000000..7d982ce --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/testing/requests_test.go @@ -0,0 +1,234 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/provider_connectivity/v2/tenant_connections" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListTenantConnections(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionsSuccessfully(t) + + count := 0 + err := tenant_connections.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := tenant_connections.ExtractTenantConnections(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedTenantConnectionsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListTenantConnectionsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantConnectionsSuccessfully(t) + + allPages, err := tenant_connections.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := tenant_connections.ExtractTenantConnections(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedTenantConnectionsSlice, actual) +} + +func TestGetTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetTenantConnectionSuccessfully(t) + + actual, err := tenant_connections.Get(client.ServiceClient(), SecondTenantConnection.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnection, *actual) +} + +func TestCreateTenantConnectionAttachComputeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionAttachComputeServerSuccessfully(t) + + createOpts := tenant_connections.CreateOpts{ + Name: "test_name_1", + Description: "test_desc_1", + Tags: map[string]string{"test_tags1": "test1"}, + TenantConnectionRequestID: "21b344d8-be11-11e7-bf3c-0050569c850d", + DeviceType: "ECL::Compute::Server", + DeviceID: "8c235a3b-8dee-41a1-b81a-64e06edc0986", + DeviceInterfaceID: "", + AttachmentOpts: tenant_connections.ComputeServer{ + AllowedAddressPairs: []tenant_connections.AddressPair{ + { + IPAddress: "192.168.1.2", + MACAddress: "11:22:33:aa:bb:cc", + }, + }, + FixedIPs: []tenant_connections.ServerFixedIPs{ + { + SubnetID: "1f424165-2202-4022-ad70-0fa6f9ec99e1", + IPAddress: "192.168.1.1", + }, + }, + }, + } + + actual, err := tenant_connections.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &FirstTenantConnection, actual) +} + +func TestCreateTenantConnectionAttachBaremetalServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionAttachBaremetalServerSuccessfully(t) + + createOpts := tenant_connections.CreateOpts{ + Name: "attach_bare_name", + Description: "attach_bare_desc", + Tags: map[string]string{"test_tags1": "test1"}, + TenantConnectionRequestID: "147c4ffa-481e-11ea-8088-525400060300", + DeviceType: "ECL::Baremetal::Server", + DeviceID: "0acab22f-8993-451c-8a6b-398b0244f578", + DeviceInterfaceID: "46eb7624-d462-46c2-8ac7-f988a15d3280", + AttachmentOpts: tenant_connections.BaremetalServer{ + AllowedAddressPairs: []tenant_connections.AddressPair{ + { + IPAddress: "192.168.1.2", + MACAddress: "11:22:33:aa:bb:cc", + }, + }, + FixedIPs: []tenant_connections.ServerFixedIPs{ + { + SubnetID: "1f424165-2202-4022-ad70-0fa6f9ec99e1", + IPAddress: "192.168.1.1", + }, + }, + SegmentationID: 10, + SegmentationType: "flat", + }, + } + + actual, err := tenant_connections.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &CreateTenantConnectionAttachBaremetalServer, actual) +} + +func TestCreateTenantConnectionAttachVna(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTenantConnectionAttachVnaSuccessfully(t) + + createOpts := tenant_connections.CreateOpts{ + Name: "attach_vna_name", + Description: "attach_vna_desc", + Tags: map[string]string{"test_tags1": "test1"}, + TenantConnectionRequestID: "67d76b00-3804-11ea-8088-525400060300", + DeviceType: "ECL::VirtualNetworkAppliance::VSRX", + DeviceID: "c291f4c4-a680-4db0-8b88-7e579f0aaa37", + DeviceInterfaceID: "interface_2", + AttachmentOpts: tenant_connections.Vna{ + FixedIPs: []tenant_connections.VnaFixedIPs{ + { + IPAddress: "192.168.1.3", + }, + }, + }, + } + + actual, err := tenant_connections.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &CreateTenantConnectionAttachVna, actual) +} + +func TestDeleteTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteTenantConnectionSuccessfully(t) + + res := tenant_connections.Delete(client.ServiceClient(), FirstTenantConnection.ID) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateTenantConnectionSuccessfully(t) + + name := "update_name" + description := "update_desc" + tags := map[string]string{"update_tags": "update"} + + updateOpts := tenant_connections.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionUpdated, *actual) +} + +func TestUpdateOtherMetadataTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateOtherMetadataTenantConnectionSuccessfully(t) + + nameOther := "update_name_other" + descriptionOther := "update_desc_other" + tagsOther := map[string]string{"test_tags_other": "update"} + + updateOpts := tenant_connections.UpdateOpts{ + NameOther: &nameOther, + DescriptionOther: &descriptionOther, + TagsOther: &tagsOther, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionOtherMetadataUpdated, *actual) +} + +func TestBlankUpdateTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleBlankUpdateTenantConnectionSuccessfully(t) + + name := "" + description := "" + tags := map[string]string{} + + updateOpts := tenant_connections.UpdateOpts{ + Name: &name, + Description: &description, + Tags: &tags, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionBlankUpdated, *actual) +} + +func TestNilUpdateTenantConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleNilUpdateTenantConnectionSuccessfully(t) + + name := "nilupdate" + + updateOpts := tenant_connections.UpdateOpts{ + Name: &name, + } + + actual, err := tenant_connections.Update(client.ServiceClient(), SecondTenantConnection.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondTenantConnectionNilUpdated, *actual) +} diff --git a/v4/ecl/provider_connectivity/v2/tenant_connections/urls.go b/v4/ecl/provider_connectivity/v2/tenant_connections/urls.go new file mode 100644 index 0000000..cfda21e --- /dev/null +++ b/v4/ecl/provider_connectivity/v2/tenant_connections/urls.go @@ -0,0 +1,23 @@ +package tenant_connections + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connections") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connections", id) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenant_connections") +} + +func deleteURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connections", id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("tenant_connections", id) +} diff --git a/v4/ecl/rca/v1/users/doc.go b/v4/ecl/rca/v1/users/doc.go new file mode 100644 index 0000000..97ce3dd --- /dev/null +++ b/v4/ecl/rca/v1/users/doc.go @@ -0,0 +1,64 @@ +/* +Package users manages and retrieves users in the Enterprise Cloud Remote Console Access Service. + +Example to List users + + allPages, err := users.List(rcaClient).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +Example to Get a user + + username := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + user, err := users.Get(rcaClient, username).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", user) + +Example to Create a user + + createOpts := users.CreateOpts{ + Password: "dummy_passw@rd", + } + + result := users.Create(rcaClient, createOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Update a user + + username := "02471b45-3de0-4fc8-8469-a7cc52c378df" + updateOpts := users.UpdateOpts{ + Password: "dummy_passw@rd", + } + + result := users.Update(rcaClient, username, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a user + + username := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + result := users.Delete(rcaClient, username) + if result.Err != nil { + panic(result.Err) + } + +*/ +package users diff --git a/v4/ecl/rca/v1/users/requests.go b/v4/ecl/rca/v1/users/requests.go new file mode 100644 index 0000000..4272a3b --- /dev/null +++ b/v4/ecl/rca/v1/users/requests.go @@ -0,0 +1,86 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// List retrieves a list of users. +func List(client *eclcloud.ServiceClient) pagination.Pager { + url := listURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of a user. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToResourceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a user. +type CreateOpts struct { + Password string `json:"password"` +} + +// ToResourceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToResourceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "user") +} + +// Create creates a new user. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToResourceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a user. +func Delete(client *eclcloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToResourceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a user. +type UpdateOpts struct { + Password string `json:"password"` +} + +// ToResourceUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToResourceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "user") +} + +// Update modifies the attributes of a user. +func Update(client *eclcloud.ServiceClient, name string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToResourceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, name), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/rca/v1/users/results.go b/v4/ecl/rca/v1/users/results.go new file mode 100644 index 0000000..80c710c --- /dev/null +++ b/v4/ecl/rca/v1/users/results.go @@ -0,0 +1,77 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// User represents VPN user. +type User struct { + Name string `json:"name"` + Password string `json:"password"` + VPNEndpoints []VPNEndpoint `json:"vpn_endpoints"` +} + +// VPNEndpoint represents VPN Endpoint. +type VPNEndpoint struct { + Endpoint string `json:"endpoint"` + Type string `json:"type"` +} + +type commonResult struct { + eclcloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a user. +type GetResult struct { + commonResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a user. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a user. +type UpdateResult struct { + commonResult +} + +// UserPage is a single page of user results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of users contains any results. +func (r UserPage) IsEmpty() (bool, error) { + resources, err := ExtractUsers(r) + return len(resources) == 0, err +} + +// ExtractUsers returns a slice of users contained in a single page of +// results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} + +// Extract interprets any commonResult as a user. +func (r commonResult) Extract() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} diff --git a/v4/ecl/rca/v1/users/testing/fixtures.go b/v4/ecl/rca/v1/users/testing/fixtures.go new file mode 100644 index 0000000..d03dc65 --- /dev/null +++ b/v4/ecl/rca/v1/users/testing/fixtures.go @@ -0,0 +1,196 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/rca/v1/users" + + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +var ( + password = "dummy_passw@rd" + passwordUpdated = "dummy_passw@rd_updated" +) + +// ListResult provides a single page of user results. +const ListResult = ` +{ + "users": [ + { + "name": "ef5778e553a24d789c15c689e30adf5d", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + }, + { + "name": "8bbe05d4bec747189e0dab81e486969f-1005", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + } + ] +} +` + +// GetResult provides a Get result. +const GetResult = ` +{ + "user": { + "name": "8bbe05d4bec747189e0dab81e486969f-1005", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "user": { + "password": "dummy_passw@rd" + } +} +` + +// CreateResponse provides the output from a Create request. +const CreateResponse = ` +{ + "user": { + "name": "8bbe05d4bec747189e0dab81e486969f-1005", + "password": "dummy_passw@rd", + "vpn_endpoints": [ + { + "endpoint": "https://rca-sslvpn1-jp1.ecl.ntt.com", + "type": "SSL-VPN" + } + ] + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "user": { + "password": "dummy_passw@rd_updated" + } +} +` + +// UpdateResult provides an update result. +const UpdateResult = GetResult + +// FirstUser is the first user in the List request. +var FirstUser = users.User{ + Name: "ef5778e553a24d789c15c689e30adf5d", + VPNEndpoints: []users.VPNEndpoint{ + { + Endpoint: "https://rca-sslvpn1-jp1.ecl.ntt.com", + Type: "SSL-VPN", + }, + }, +} + +// SecondUser is the second user in the List request. +var SecondUser = users.User{ + Name: "8bbe05d4bec747189e0dab81e486969f-1005", + VPNEndpoints: []users.VPNEndpoint{ + { + Endpoint: "https://rca-sslvpn1-jp1.ecl.ntt.com", + Type: "SSL-VPN", + }, + }, +} + +// SecondUserUpdated is how SecondUser should look after an Update. +var SecondUserUpdated = users.User{ + Name: "8bbe05d4bec747189e0dab81e486969f-1005", + VPNEndpoints: []users.VPNEndpoint{ + { + Endpoint: "https://rca-sslvpn1-jp1.ecl.ntt.com", + Type: "SSL-VPN", + }, + }, +} + +// ExpectedUsersSlice is the slice of users expected to be returned from ListResult. +var ExpectedUsersSlice = []users.User{FirstUser, SecondUser} + +// HandleListUsersSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that responds with a list of two users. +func HandleListUsersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users", 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, ListResult) + }) +} + +// HandleGetUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that responds with a single user. +func HandleGetUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/users/%s", SecondUser.Name), 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, GetResult) + }) +} + +// HandleCreateUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user creation. +func HandleCreateUserSuccessfully(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", client.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, CreateResponse) + }) +} + +// HandleDeleteUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user deletion. +func HandleDeleteUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/users/%s", FirstUser.Name), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusOK) + }) +} + +// HandleUpdateUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user update. +func HandleUpdateUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc(fmt.Sprintf("/users/%s", SecondUser.Name), 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, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateResult) + }) +} diff --git a/v4/ecl/rca/v1/users/testing/requests_test.go b/v4/ecl/rca/v1/users/testing/requests_test.go new file mode 100644 index 0000000..2541630 --- /dev/null +++ b/v4/ecl/rca/v1/users/testing/requests_test.go @@ -0,0 +1,91 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/rca/v1/users" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListUsers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsersSuccessfully(t) + + count := 0 + err := users.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ExpectedUsersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, count, 1) +} + +func TestListUsersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsersSuccessfully(t) + + allPages, err := users.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + actual, err := users.ExtractUsers(allPages) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedUsersSlice, actual) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetUserSuccessfully(t) + + actual, err := users.Get(client.ServiceClient(), SecondUser.Name).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondUser, *actual) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateUserSuccessfully(t) + + createOpts := users.CreateOpts{ + Password: password, + } + + actual, err := users.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, SecondUser.Name, actual.Name) + th.AssertEquals(t, password, actual.Password) + th.AssertDeepEquals(t, SecondUser.VPNEndpoints, actual.VPNEndpoints) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteUserSuccessfully(t) + + res := users.Delete(client.ServiceClient(), FirstUser.Name) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateUserSuccessfully(t) + + updateOpts := users.UpdateOpts{ + Password: passwordUpdated, + } + + actual, err := users.Update(client.ServiceClient(), SecondUser.Name, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, SecondUserUpdated, *actual) +} diff --git a/v4/ecl/rca/v1/users/urls.go b/v4/ecl/rca/v1/users/urls.go new file mode 100644 index 0000000..821b4bf --- /dev/null +++ b/v4/ecl/rca/v1/users/urls.go @@ -0,0 +1,23 @@ +package users + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *eclcloud.ServiceClient, name string) string { + return client.ServiceURL("users", name) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func deleteURL(client *eclcloud.ServiceClient, name string) string { + return client.ServiceURL("users", name) +} + +func updateURL(client *eclcloud.ServiceClient, name string) string { + return client.ServiceURL("users", name) +} diff --git a/v4/ecl/security_order/v3/host_based/doc.go b/v4/ecl/security_order/v3/host_based/doc.go new file mode 100644 index 0000000..75eaef7 --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/doc.go @@ -0,0 +1,2 @@ +// Package host_based contains Host Based Security functionality. +package host_based diff --git a/v4/ecl/security_order/v3/host_based/requests.go b/v4/ecl/security_order/v3/host_based/requests.go new file mode 100644 index 0000000..47c18d2 --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/requests.go @@ -0,0 +1,139 @@ +package host_based + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// GetOptsBuilder allows extensions to add additional parameters to +// the order progress API request +type GetOptsBuilder interface { + ToServiceOrderQuery() (string, error) +} + +// GetOpts represents result of host based security API response. +type GetOpts struct { + TenantID string `q:"tenant_id"` +} + +// ToServiceOrderQuery formats a GetOpts into a query string. +func (opts GetOpts) ToServiceOrderQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves details of an order progress, by SoId. +func Get(client *eclcloud.ServiceClient, opts GetOptsBuilder) (r GetResult) { + url := getURL(client) + if opts != nil { + query, _ := opts.ToServiceOrderQuery() + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToHostBasedCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a Host based security. +type CreateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + ServiceOrderService string `json:"service_order_service" required:"true"` + MaxAgentValue int `json:"max_agent_value" required:"true"` + MailAddress string `json:"mailaddress" required:"true"` + DSMLang string `json:"dsm_lang" required:"true"` + TimeZone string `json:"time_zone" required:"true"` +} + +// ToHostBasedCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToHostBasedCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new Host based security. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToHostBasedCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to +// the Delete request. +type DeleteOptsBuilder interface { + ToHostBasedDeleteMap() (map[string]interface{}, error) +} + +// DeleteOpts represents parameters used to cancel Host Based Security. +type DeleteOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + MailAddress string `json:"mailaddress" required:"true"` +} + +// ToHostBasedDeleteMap formats a DeleteOpts into a delete request. +func (opts DeleteOpts) ToHostBasedDeleteMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Delete deletes a device. +func Delete(client *eclcloud.ServiceClient, opts DeleteOptsBuilder) (r DeleteResult) { + b, err := opts.ToHostBasedDeleteMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return + +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToHostBasedUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a Host Based Security. +type UpdateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + MailAddress string `json:"mailaddress" required:"true"` + // Set this in case of Type M1 Change + ServiceOrderService *string `json:"service_order_service,omitempty"` + // Set this in case of Type M2 Change + MaxAgentValue *int `json:"max_agent_value,omitempty"` +} + +// ToHostBasedUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToHostBasedUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a Host Based Security. +func Update(client *eclcloud.ServiceClient, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToHostBasedUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(updateURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/security_order/v3/host_based/results.go b/v4/ecl/security_order/v3/host_based/results.go new file mode 100644 index 0000000..4989f14 --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/results.go @@ -0,0 +1,85 @@ +package host_based + +import ( + "github.com/nttcom/eclcloud/v4" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Host Based Security resource. +func (r commonResult) Extract() (*HostBasedOrder, error) { + var hbo HostBasedOrder + err := r.ExtractInto(&hbo) + return &hbo, err +} + +// Extract interprets any commonResult as a Host Based Security if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Host Based Security. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Host Based Security. +type GetResult struct { + commonResult +} + +// HostBasedSecurity represents a Host Based Security's each order. +type HostBasedSecurity struct { + Code string `json:"code"` + Message string `json:"message"` + Region string `json:"region"` + TenantName string `json:"tenant_name"` + TenantDescription string `json:"tenant_description"` + ContractID string `json:"contract_id"` + ServiceOrderService string `json:"service_order_service"` + MaxAgentValue interface{} `json:"max_agent_value"` + TimeZone string `json:"time_zone"` + CustomerName string `json:"customer_name"` + MailAddress string `json:"mailaddress"` + DSMLang string `json:"dsm_lang"` + TenantFlg bool `json:"tenant_flg"` + Status int `json:"status"` +} + +// Extract is a function that accepts a result +// and extracts a Host Based Security resource. +func (r GetResult) Extract() (*HostBasedSecurity, error) { + var h HostBasedSecurity + err := r.ExtractInto(&h) + return &h, err +} + +// ExtractInto interprets any commonResult as a Host Based Security if possible. +func (r GetResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Host Based Security. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// HostBasedOrder represents a Host Based Security's each order. +type HostBasedOrder struct { + ID string `json:"soId"` + Code string `json:"code"` + Message string `json:"message"` + Status int `json:"status"` +} diff --git a/v4/ecl/security_order/v3/host_based/testing/doc.go b/v4/ecl/security_order/v3/host_based/testing/doc.go new file mode 100644 index 0000000..085b2c7 --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains host based security unittests +package testing diff --git a/v4/ecl/security_order/v3/host_based/testing/fixtures.go b/v4/ecl/security_order/v3/host_based/testing/fixtures.go new file mode 100644 index 0000000..b5eefd6 --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/testing/fixtures.go @@ -0,0 +1,139 @@ +package testing + +import ( + security "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/host_based" +) + +const getResponse = ` +{ + "code": "DEP-01", + "message": "Successful completion", + "region": "jp4", + "tenant_name": "Test Tenant", + "tenant_description": "Test Tenant", + "contract_id": "econ9999999999", + "service_order_service": "Managed Anti-Virus", + "max_agent_value": 1, + "customer_name": "Customer", + "time_zone": "Asia/Tokyo", + "mailaddress": "terraform@example.com", + "dsm_lang": "ja", + "tenant_flg": true, + "status": 1 +} +` + +var expectedResult = security.HostBasedSecurity{ + Code: "DEP-01", + Message: "Successful completion", + Region: "jp4", + TenantName: "Test Tenant", + TenantDescription: "Test Tenant", + ContractID: "econ9999999999", + ServiceOrderService: "Managed Anti-Virus", + MaxAgentValue: float64(1), + TimeZone: "Asia/Tokyo", + CustomerName: "Customer", + MailAddress: "terraform@example.com", + DSMLang: "ja", + TenantFlg: true, + Status: 1, +} + +var createRequest = ` +{ + "sokind": "N", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "service_order_service": "Managed Anti-Virus", + "max_agent_value": 1, + "mailaddress": "terraform@example.com", + "dsm_lang": "ja", + "time_zone": "Asia/Tokyo" +}` + +var createResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var createResult = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequestM1 = ` +{ + "sokind": "M1", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "mailaddress": "terraform@example.com", + "service_order_service": "Managed Anti-Virus" +}` + +var updateResponseM1 = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResultM1 = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequestM2 = ` +{ + "sokind": "M2", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "mailaddress": "terraform@example.com", + "max_agent_value": 10 +}` + +var updateResponseM2 = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResultM2 = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var deleteRequest = ` +{ + "sokind": "C", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "locale": "ja", + "mailaddress": "terraform@example.com" +}` + +var deleteResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var deleteResult = security.HostBasedOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} diff --git a/v4/ecl/security_order/v3/host_based/testing/requests_test.go b/v4/ecl/security_order/v3/host_based/testing/requests_test.go new file mode 100644 index 0000000..f0ac07a --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/testing/requests_test.go @@ -0,0 +1,146 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + security "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/host_based" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestGetHostBasedSecurity(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/ScreenEventHBSOrderInfoGet" + fmt.Println(url) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := security.Get(fakeclient.ServiceClient(), nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} + +func TestCreateHostBasedSecurity(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/SoEntryHBS", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + createOpts := security.CreateOpts{ + SOKind: "N", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + ServiceOrderService: "Managed Anti-Virus", + MaxAgentValue: 1, + MailAddress: "terraform@example.com", + DSMLang: "ja", + TimeZone: "Asia/Tokyo", + } + + actual, err := security.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createResult, actual) +} + +func TestUpdateHostBasedSecurityTypeM1(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryHBS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequestM1) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponseM1) + }) + + updateOpts := security.UpdateOpts{ + SOKind: "M1", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + MailAddress: "terraform@example.com", + } + + serviceOrderService := "Managed Anti-Virus" + updateOpts.ServiceOrderService = &serviceOrderService + actual, err := security.Update(fakeclient.ServiceClient(), updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResultM1, actual) +} + +func TestUpdateHostBasedSecurityTypeM2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryHBS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequestM2) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponseM2) + }) + + updateOpts := security.UpdateOpts{ + SOKind: "M2", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + MailAddress: "terraform@example.com", + } + + maxAgentValue := 10 + updateOpts.MaxAgentValue = &maxAgentValue + actual, err := security.Update(fakeclient.ServiceClient(), updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResultM2, actual) +} + +func TestDeleteDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryHBS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, deleteRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, deleteResponse) + }) + + deleteOpts := security.DeleteOpts{ + SOKind: "C", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Locale: "ja", + MailAddress: "terraform@example.com", + } + + actual, err := security.Delete(fakeclient.ServiceClient(), deleteOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &deleteResult, actual) +} diff --git a/v4/ecl/security_order/v3/host_based/urls.go b/v4/ecl/security_order/v3/host_based/urls.go new file mode 100644 index 0000000..77b156e --- /dev/null +++ b/v4/ecl/security_order/v3/host_based/urls.go @@ -0,0 +1,21 @@ +package host_based + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/ScreenEventHBSOrderInfoGet") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryHBS") +} + +func deleteURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryHBS") +} + +func updateURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryHBS") +} diff --git a/v4/ecl/security_order/v3/network_based_device_ha/doc.go b/v4/ecl/security_order/v3/network_based_device_ha/doc.go new file mode 100644 index 0000000..e5f191e --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/doc.go @@ -0,0 +1,2 @@ +// Package network_based_device_ha contains HA device functionality on security. +package network_based_device_ha diff --git a/v4/ecl/security_order/v3/network_based_device_ha/requests.go b/v4/ecl/security_order/v3/network_based_device_ha/requests.go new file mode 100644 index 0000000..a6586df --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/requests.go @@ -0,0 +1,162 @@ +package network_based_device_ha + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToHADeviceQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Locale string `q:"locale"` +} + +// ToHADeviceQuery formats a ListOpts into a query string. +func (opts ListOpts) ToHADeviceQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Devices to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToHADeviceQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return HADevicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToHADeviceCreateMap() (map[string]interface{}, error) +} + +// GtHostInCreate represents parameters used to create a HA Device. +type GtHostInCreate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + AZGroup string `json:"azgroup" required:"true"` + + HALink1NetworkID string `json:"halink1networkid" required:"true"` + HALink1SubnetID string `json:"halink1subnetid" required:"true"` + HALink1IPAddress string `json:"halink1ipaddress" required:"true"` + + HALink2NetworkID string `json:"halink2networkid" required:"true"` + HALink2SubnetID string `json:"halink2subnetid" required:"true"` + HALink2IPAddress string `json:"halink2ipaddress" required:"true"` +} + +// CreateOpts represents parameters used to create a device. +type CreateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + GtHost [2]GtHostInCreate `json:"gt_host" required:"true"` +} + +// ToHADeviceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToHADeviceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new device. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToHADeviceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to +// the Delete request. +type DeleteOptsBuilder interface { + ToHADeviceDeleteMap() (map[string]interface{}, error) +} + +// GtHostInDelete represents parameters used to delete a HA Device. +type GtHostInDelete struct { + HostName string `json:"hostname" required:"true"` +} + +// DeleteOpts represents parameters used to delete a device. +type DeleteOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [2]GtHostInDelete `json:"gt_host" required:"true"` +} + +// ToHADeviceDeleteMap formats a DeleteOpts into a delete request. +func (opts DeleteOpts) ToHADeviceDeleteMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Delete deletes a device. +func Delete(client *eclcloud.ServiceClient, deviceType string, opts DeleteOptsBuilder) (r DeleteResult) { + b, err := opts.ToHADeviceDeleteMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return + +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToHADeviceUpdateMap() (map[string]interface{}, error) +} + +// GtHostInUpdate represents parameters used to update a HA Device. +type GtHostInUpdate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + HostName string `json:"hostname" required:"true"` +} + +// UpdateOpts represents parameters to update a HA Device. +type UpdateOpts struct { + SOKind string `json:"sokind" required:"true"` + Locale string `json:"locale,omitempty"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [2]GtHostInUpdate `json:"gt_host" required:"true"` +} + +// ToHADeviceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToHADeviceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a device. +func Update(client *eclcloud.ServiceClient, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToHADeviceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(updateURL(client), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/security_order/v3/network_based_device_ha/results.go b/v4/ecl/security_order/v3/network_based_device_ha/results.go new file mode 100644 index 0000000..2defcf7 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/results.go @@ -0,0 +1,104 @@ +package network_based_device_ha + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a HA Device resource. +func (r commonResult) Extract() (*HADeviceOrder, error) { + var sdo HADeviceOrder + err := r.ExtractInto(&sdo) + return &sdo, err +} + +// Extract interprets any commonResult as a HA Device if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a HA Device. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a HA Device. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a HA Device. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// HADevice represents the result of a each element in +// response of HA Device api result. +type HADevice struct { + ID int `json:"id"` + Cell []string `json:"cell"` +} + +// HADeviceOrder represents a HA Device's each order. +type HADeviceOrder struct { + ID string `json:"soId"` + Code string `json:"code"` + Message string `json:"message"` + Status int `json:"status"` +} + +// HADevicePage is the page returned by a pager +// when traversing over a collection of HA Device. +type HADevicePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of HA Device +// 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 (r HADevicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"ha_device_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a HADevicePage struct is empty. +func (r HADevicePage) IsEmpty() (bool, error) { + is, err := ExtractHADevices(r) + return len(is) == 0, err +} + +// ExtractHADevices accepts a Page struct, +// specifically a HADevicePage struct, and extracts the elements +// into a slice of HA Device structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractHADevices(r pagination.Page) ([]HADevice, error) { + var s []HADevice + err := ExtractHADevicesInto(r, &s) + return s, err +} + +// ExtractHADevicesInto interprets the results of a single page from a List() call, +// producing a slice of Device entities. +func ExtractHADevicesInto(r pagination.Page, v interface{}) error { + return r.(HADevicePage).Result.ExtractIntoSlicePtr(v, "rows") +} diff --git a/v4/ecl/security_order/v3/network_based_device_ha/testing/doc.go b/v4/ecl/security_order/v3/network_based_device_ha/testing/doc.go new file mode 100644 index 0000000..499badf --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains network based security HA device unittests +package testing diff --git a/v4/ecl/security_order/v3/network_based_device_ha/testing/fixtures.go b/v4/ecl/security_order/v3/network_based_device_ha/testing/fixtures.go new file mode 100644 index 0000000..e4ae9b3 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/testing/fixtures.go @@ -0,0 +1,149 @@ +package testing + +import ( + security "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/network_based_device_ha" +) + +const listResponse = ` +{ + "status": 1, + "code": "FOV-01", + "message": "Successful completion", + "records": 2, + "rows": [ + { + "cell": ["false", "1", "1902F60E", "CES12085", "FW_HA", "02", "ha", "zone1-groupa", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.3", "dummyNetworkID2", "dummySubnetID2", "192.168.2.3"], + "id": 1 + }, + { + "cell": ["false", "2", "1902F60E", "CES12086", "FW_HA", "02", "ha", "zone1-groupb", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.4", "dummyNetworkID2", "dummySubnetID2", "192.168.2.4"], + "id": 2 + } + ] +} +` + +var expectedDevicesSlice = []security.HADevice{firstDevice, secondDevice} + +var firstDevice = security.HADevice{ + ID: 1, + Cell: []string{ + "false", "1", "1902F60E", "CES12085", "FW_HA", "02", "ha", "zone1-groupa", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.3", "dummyNetworkID2", "dummySubnetID2", "192.168.2.3", + }, +} + +var secondDevice = security.HADevice{ + ID: 2, + Cell: []string{ + "false", "2", "1902F60E", "CES12086", "FW_HA", "02", "ha", "zone1-groupb", "jp4_zone1", "dummyNetworkID1", "dummySubnetID1", "192.168.1.4", "dummyNetworkID2", "dummySubnetID2", "192.168.2.4", + }, +} + +var createRequest = ` +{ + "gt_host": [ + { + "azgroup": "zone1-groupa", + "licensekind": "02", + "operatingmode": "FW_HA", + "halink1ipaddress": "192.168.1.3", + "halink1networkid": "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + "halink1subnetid": "9a2116e2-52be-439c-9587-506a1a5d288d", + "halink2ipaddress": "192.168.2.3", + "halink2networkid": "a8df4d5f-8752-4574-a255-dc749acd458f", + "halink2subnetid": "a2ff5669-8422-421c-bb85-a6d691ecf223" + }, + { + "azgroup": "zone1-groupb", + "licensekind": "02", + "operatingmode": "FW_HA", + "halink1ipaddress": "192.168.1.4", + "halink1networkid": "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + "halink1subnetid": "9a2116e2-52be-439c-9587-506a1a5d288d", + "halink2ipaddress": "192.168.2.4", + "halink2networkid": "a8df4d5f-8752-4574-a255-dc749acd458f", + "halink2subnetid": "a2ff5669-8422-421c-bb85-a6d691ecf223" + } + ], + "locale": "ja", + "sokind": "AH", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var createResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var createResult = security.HADeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811", + "licensekind": "08", + "operatingmode": "UTM_HA" + }, + { + "hostname": "CES11812", + "licensekind": "08", + "operatingmode": "UTM_HA" + } + ], + "locale": "en", + "sokind": "MH", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var updateResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResult = security.HADeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var deleteRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811" + }, + { + "hostname": "CES11812" + } + ], + "sokind": "DH", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var deleteResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var deleteResult = security.HADeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} diff --git a/v4/ecl/security_order/v3/network_based_device_ha/testing/requests_test.go b/v4/ecl/security_order/v3/network_based_device_ha/testing/requests_test.go new file mode 100644 index 0000000..48f8189 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/testing/requests_test.go @@ -0,0 +1,180 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + security "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/network_based_device_ha" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGHADeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := security.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security.ExtractHADevices(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDevicesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGHADeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := security.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allDevices, err := security.ExtractHADevices(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDevices)) +} + +func TestCreateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/SoEntryFGHA", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + gtHost1 := security.GtHostInCreate{ + AZGroup: "zone1-groupa", + LicenseKind: "02", + OperatingMode: "FW_HA", + HALink1IPAddress: "192.168.1.3", + HALink1NetworkID: "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + HALink1SubnetID: "9a2116e2-52be-439c-9587-506a1a5d288d", + HALink2IPAddress: "192.168.2.3", + HALink2NetworkID: "a8df4d5f-8752-4574-a255-dc749acd458f", + HALink2SubnetID: "a2ff5669-8422-421c-bb85-a6d691ecf223", + } + + gtHost2 := security.GtHostInCreate{ + AZGroup: "zone1-groupb", + LicenseKind: "02", + OperatingMode: "FW_HA", + HALink1IPAddress: "192.168.1.4", + HALink1NetworkID: "c5b1b0a8-45a3-4c99-b808-84e7c13e557f", + HALink1SubnetID: "9a2116e2-52be-439c-9587-506a1a5d288d", + HALink2IPAddress: "192.168.2.4", + HALink2NetworkID: "a8df4d5f-8752-4574-a255-dc749acd458f", + HALink2SubnetID: "a2ff5669-8422-421c-bb85-a6d691ecf223", + } + + createOpts := security.CreateOpts{ + SOKind: "AH", + Locale: "ja", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [2]security.GtHostInCreate{gtHost1, gtHost2}, + } + + actual, err := security.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createResult, actual) +} + +func TestUpdateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGHA" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponse) + }) + + gtHost1 := security.GtHostInUpdate{ + OperatingMode: "UTM_HA", + LicenseKind: "08", + HostName: "CES11811", + } + + gtHost2 := security.GtHostInUpdate{ + OperatingMode: "UTM_HA", + LicenseKind: "08", + HostName: "CES11812", + } + + updateOpts := security.UpdateOpts{ + SOKind: "MH", + Locale: "en", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [2]security.GtHostInUpdate{gtHost1, gtHost2}, + } + + actual, err := security.Update(fakeclient.ServiceClient(), updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResult, actual) +} + +func TestDeleteDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGHA" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, deleteRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, deleteResponse) + }) + + gtHost1 := security.GtHostInDelete{ + HostName: "CES11811", + } + + gtHost2 := security.GtHostInDelete{ + HostName: "CES11812", + } + + deleteOpts := security.DeleteOpts{ + SOKind: "DH", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [2]security.GtHostInDelete{gtHost1, gtHost2}, + } + + actual, err := security.Delete(fakeclient.ServiceClient(), "CES11811", deleteOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &deleteResult, actual) +} diff --git a/v4/ecl/security_order/v3/network_based_device_ha/urls.go b/v4/ecl/security_order/v3/network_based_device_ha/urls.go new file mode 100644 index 0000000..abba7f9 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_ha/urls.go @@ -0,0 +1,21 @@ +package network_based_device_ha + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/ScreenEventFGHADeviceGet") +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryFGHA") +} + +func deleteURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryFGHA") +} + +func updateURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("API/SoEntryFGHA") +} diff --git a/v4/ecl/security_order/v3/network_based_device_single/doc.go b/v4/ecl/security_order/v3/network_based_device_single/doc.go new file mode 100644 index 0000000..b752fd5 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/doc.go @@ -0,0 +1,2 @@ +// Package network_based_device_single contains single device functionality on security. +package network_based_device_single diff --git a/v4/ecl/security_order/v3/network_based_device_single/requests.go b/v4/ecl/security_order/v3/network_based_device_single/requests.go new file mode 100644 index 0000000..136811e --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/requests.go @@ -0,0 +1,154 @@ +package network_based_device_single + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToSingleDeviceQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Locale string `q:"locale"` +} + +// ToSingleDeviceQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSingleDeviceQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Devices to which the current token has access. +func List(client *eclcloud.ServiceClient, deviceType string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, deviceType) + if opts != nil { + query, err := opts.ToSingleDeviceQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SingleDevicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToSingleDeviceCreateMap() (map[string]interface{}, error) +} + +// GtHostInCreate represents parameters used to create a Single Device. +type GtHostInCreate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + AZGroup string `json:"azgroup" required:"true"` +} + +// CreateOpts represents parameters used to create a device. +type CreateOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + Locale string `json:"locale,omitempty"` + GtHost [1]GtHostInCreate `json:"gt_host" required:"true"` +} + +// ToSingleDeviceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToSingleDeviceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new device. +func Create(client *eclcloud.ServiceClient, deviceType string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSingleDeviceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, deviceType), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to +// the Delete request. +type DeleteOptsBuilder interface { + ToSingleDeviceDeleteMap() (map[string]interface{}, error) +} + +// GtHostInDelete represents parameters used to delete a Single Device. +type GtHostInDelete struct { + HostName string `json:"hostname" required:"true"` +} + +// DeleteOpts represents parameters used to delete a device. +type DeleteOpts struct { + SOKind string `json:"sokind" required:"true"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [1]GtHostInDelete `json:"gt_host" required:"true"` +} + +// ToSingleDeviceDeleteMap formats a DeleteOpts into a delete request. +func (opts DeleteOpts) ToSingleDeviceDeleteMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Delete deletes a device. +func Delete(client *eclcloud.ServiceClient, deviceType string, opts DeleteOptsBuilder) (r DeleteResult) { + b, err := opts.ToSingleDeviceDeleteMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, deviceType), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return + +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToSingleDeviceUpdateMap() (map[string]interface{}, error) +} + +// GtHostInUpdate represents parameters used to update a Single Device. +type GtHostInUpdate struct { + OperatingMode string `json:"operatingmode" required:"true"` + LicenseKind string `json:"licensekind" required:"true"` + HostName string `json:"hostname" required:"true"` +} + +// UpdateOpts represents parameters to update a Single Device. +type UpdateOpts struct { + SOKind string `json:"sokind" required:"true"` + Locale string `json:"locale,omitempty"` + TenantID string `json:"tenant_id" required:"true"` + GtHost [1]GtHostInUpdate `json:"gt_host" required:"true"` +} + +// ToSingleDeviceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToSingleDeviceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a device. +func Update(client *eclcloud.ServiceClient, deviceType string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSingleDeviceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(updateURL(client, deviceType), &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/security_order/v3/network_based_device_single/results.go b/v4/ecl/security_order/v3/network_based_device_single/results.go new file mode 100644 index 0000000..b687645 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/results.go @@ -0,0 +1,104 @@ +package network_based_device_single + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Single Device resource. +func (r commonResult) Extract() (*SingleDeviceOrder, error) { + var sdo SingleDeviceOrder + err := r.ExtractInto(&sdo) + return &sdo, err +} + +// Extract interprets any commonResult as a Single Device if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Single Device. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Single Device. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Single Device. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + commonResult +} + +// SingleDevice represents the result of a each element in +// response of single device api result. +type SingleDevice struct { + ID int `json:"id"` + Cell []string `json:"cell"` +} + +// SingleDeviceOrder represents a Single Device's each order. +type SingleDeviceOrder struct { + ID string `json:"soId"` + Code string `json:"code"` + Message string `json:"message"` + Status int `json:"status"` +} + +// SingleDevicePage is the page returned by a pager +// when traversing over a collection of Single Device. +type SingleDevicePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of Single Device +// 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 (r SingleDevicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"single_device_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SingleDevicePage struct is empty. +func (r SingleDevicePage) IsEmpty() (bool, error) { + is, err := ExtractSingleDevices(r) + return len(is) == 0, err +} + +// ExtractSingleDevices accepts a Page struct, +// specifically a SingleDevicePage struct, and extracts the elements +// into a slice of Single Device structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractSingleDevices(r pagination.Page) ([]SingleDevice, error) { + var s []SingleDevice + err := ExtractSingleDevicesInto(r, &s) + return s, err +} + +// ExtractSingleDevicesInto interprets the results of a single page from a List() call, +// producing a slice of Device entities. +func ExtractSingleDevicesInto(r pagination.Page, v interface{}) error { + return r.(SingleDevicePage).Result.ExtractIntoSlicePtr(v, "rows") +} diff --git a/v4/ecl/security_order/v3/network_based_device_single/testing/doc.go b/v4/ecl/security_order/v3/network_based_device_single/testing/doc.go new file mode 100644 index 0000000..dd8abff --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains network based security single device unittests +package testing diff --git a/v4/ecl/security_order/v3/network_based_device_single/testing/fixtures.go b/v4/ecl/security_order/v3/network_based_device_single/testing/fixtures.go new file mode 100644 index 0000000..c3ad392 --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/testing/fixtures.go @@ -0,0 +1,124 @@ +package testing + +import ( + security "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/network_based_device_single" +) + +const listResponse = ` +{ + "status": 1, + "code": "FOV-01", + "message": "Successful completion", + "records": 2, + "rows": [ + { + "id": 1, + "cell": ["false", "1", "CES11810", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1"] + }, + { + "id": 2, + "cell": ["false", "1", "CES11811", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1"] + } + ] +} +` + +var expectedDevicesSlice = []security.SingleDevice{firstDevice, secondDevice} + +var firstDevice = security.SingleDevice{ + ID: 1, + Cell: []string{ + "false", "1", "CES11810", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1", + }, +} + +var secondDevice = security.SingleDevice{ + ID: 2, + Cell: []string{ + "false", "1", "CES11811", "FW", "02", "standalone", "zone1-groupb", "jp4_zone1", + }, +} + +var createRequest = ` +{ + "gt_host":[ + { + "azgroup": "zone1-groupb", + "licensekind":"02", + "operatingmode":"FW" + } + ], + "locale": "ja", + "sokind": "A", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var createResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var createResult = security.SingleDeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var updateRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811", + "licensekind": "08", + "operatingmode": "UTM" + } + ], + "locale": "en", + "sokind": "M", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var updateResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var updateResult = security.SingleDeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} + +var deleteRequest = ` +{ + "gt_host": [ + { + "hostname": "CES11811" + } + ], + "sokind": "D", + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5" +}` + +var deleteResponse = ` +{ + "status": 1, + "code": "FOV-02", + "message": "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + "soId": "FGS_3B6A7602ACD04E16B6EBEF215AE8E642" +}` + +var deleteResult = security.SingleDeviceOrder{ + Status: 1, + Code: "FOV-02", + Message: "オーダーを受け付けました。ProgressRateにて状況を確認できます。", + ID: "FGS_3B6A7602ACD04E16B6EBEF215AE8E642", +} diff --git a/v4/ecl/security_order/v3/network_based_device_single/testing/requests_test.go b/v4/ecl/security_order/v3/network_based_device_single/testing/requests_test.go new file mode 100644 index 0000000..7ac2ccf --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/testing/requests_test.go @@ -0,0 +1,149 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + security "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/network_based_device_single" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGSDeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := security.List(fakeclient.ServiceClient(), "UTM", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security.ExtractSingleDevices(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDevicesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/ScreenEventFGSDeviceGet", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := security.List(fakeclient.ServiceClient(), "UTM", nil).AllPages() + th.AssertNoErr(t, err) + allDevices, err := security.ExtractSingleDevices(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDevices)) +} + +func TestCreateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/API/SoEntryFGS", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + gtHost := security.GtHostInCreate{ + AZGroup: "zone1-groupb", + LicenseKind: "02", + OperatingMode: "FW", + } + createOpts := security.CreateOpts{ + SOKind: "A", + Locale: "ja", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [1]security.GtHostInCreate{gtHost}, + } + + actual, err := security.Create(fakeclient.ServiceClient(), "UTM", createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createResult, actual) +} + +func TestUpdateDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, updateResponse) + }) + + gtHost := security.GtHostInUpdate{ + OperatingMode: "UTM", + LicenseKind: "08", + HostName: "CES11811", + } + updateOpts := security.UpdateOpts{ + SOKind: "M", + Locale: "en", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [1]security.GtHostInUpdate{gtHost}, + } + + actual, err := security.Update(fakeclient.ServiceClient(), "CES11811", updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &updateResult, actual) +} + +func TestDeleteDevice(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/SoEntryFGS" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, deleteRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, deleteResponse) + }) + + gtHost := security.GtHostInDelete{ + HostName: "CES11811", + } + deleteOpts := security.DeleteOpts{ + SOKind: "D", + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + GtHost: [1]security.GtHostInDelete{gtHost}, + } + + actual, err := security.Delete(fakeclient.ServiceClient(), "CES11811", deleteOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &deleteResult, actual) +} diff --git a/v4/ecl/security_order/v3/network_based_device_single/urls.go b/v4/ecl/security_order/v3/network_based_device_single/urls.go new file mode 100644 index 0000000..7644e9b --- /dev/null +++ b/v4/ecl/security_order/v3/network_based_device_single/urls.go @@ -0,0 +1,38 @@ +package network_based_device_single + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func getURLPartFromDeviceType(deviceType string) string { + if deviceType == "WAF" { + return "WAF" + } + return "S" +} + +func listURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/ScreenEventFG%sDeviceGet", part) + return client.ServiceURL(url) +} + +func createURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/SoEntryFG%s", part) + return client.ServiceURL(url) +} + +func deleteURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/SoEntryFG%s", part) + return client.ServiceURL(url) +} + +func updateURL(client *eclcloud.ServiceClient, deviceType string) string { + part := getURLPartFromDeviceType(deviceType) + url := fmt.Sprintf("API/SoEntryFG%s", part) + return client.ServiceURL(url) +} diff --git a/v4/ecl/security_order/v3/service_order_status/doc.go b/v4/ecl/security_order/v3/service_order_status/doc.go new file mode 100644 index 0000000..f55fa76 --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/doc.go @@ -0,0 +1,2 @@ +// Package service_order_status contains order management functionality on security +package service_order_status diff --git a/v4/ecl/security_order/v3/service_order_status/requests.go b/v4/ecl/security_order/v3/service_order_status/requests.go new file mode 100644 index 0000000..3ed8f9a --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/requests.go @@ -0,0 +1,36 @@ +package service_order_status + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// GetOptsBuilder allows extensions to add additional parameters to +// the order progress API request +type GetOptsBuilder interface { + ToServiceOrderQuery() (string, error) +} + +// GetOpts represents result of order progress API response. +type GetOpts struct { + TenantID string `q:"tenant_id"` + Locale string `q:"locale"` + SoID string `q:"soid"` +} + +// ToServiceOrderQuery formats a GetOpts into a query string. +func (opts GetOpts) ToServiceOrderQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves details of an order progress, by SoId. +func Get(client *eclcloud.ServiceClient, deviceType string, opts GetOptsBuilder) (r GetResult) { + url := getURL(client, deviceType) + if opts != nil { + query, _ := opts.ToServiceOrderQuery() + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/v4/ecl/security_order/v3/service_order_status/results.go b/v4/ecl/security_order/v3/service_order_status/results.go new file mode 100644 index 0000000..81a959c --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/results.go @@ -0,0 +1,36 @@ +package service_order_status + +import ( + "github.com/nttcom/eclcloud/v4" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts an Order Progress resource. +func (r commonResult) Extract() (*OrderProgress, error) { + var sd OrderProgress + err := r.ExtractInto(&sd) + return &sd, err +} + +// Extract interprets any commonResult as an Order Progress, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as an Order. +type GetResult struct { + commonResult +} + +// OrderProgress represents an Order Progress response. +type OrderProgress struct { + Status int `json:"status"` + Code string `json:"code"` + Message string `json:"message"` + ProgressRate int `json:"progressRate"` +} diff --git a/v4/ecl/security_order/v3/service_order_status/testing/doc.go b/v4/ecl/security_order/v3/service_order_status/testing/doc.go new file mode 100644 index 0000000..6b493c6 --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains service order status unit tests +package testing diff --git a/v4/ecl/security_order/v3/service_order_status/testing/fixtures.go b/v4/ecl/security_order/v3/service_order_status/testing/fixtures.go new file mode 100644 index 0000000..2b6340a --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/testing/fixtures.go @@ -0,0 +1,21 @@ +package testing + +import ( + order "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/service_order_status" +) + +const getResponse = ` +{ + "status": 1, + "code": "FOV-05", + "message": "We accepted the order. Please wait", + "progressRate": 45 +} +` + +var expectedResult = order.OrderProgress{ + Status: 1, + Code: "FOV-05", + Message: "We accepted the order. Please wait", + ProgressRate: 45, +} diff --git a/v4/ecl/security_order/v3/service_order_status/testing/requests_test.go b/v4/ecl/security_order/v3/service_order_status/testing/requests_test.go new file mode 100644 index 0000000..f0640d2 --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/testing/requests_test.go @@ -0,0 +1,31 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + order "github.com/nttcom/eclcloud/v4/ecl/security_order/v3/service_order_status" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestGetOrder(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/API/ScreenEventFGSOrderProgressRate" + fmt.Println(url) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := order.Get(fakeclient.ServiceClient(), "UTM", nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} diff --git a/v4/ecl/security_order/v3/service_order_status/urls.go b/v4/ecl/security_order/v3/service_order_status/urls.go new file mode 100644 index 0000000..00e1320 --- /dev/null +++ b/v4/ecl/security_order/v3/service_order_status/urls.go @@ -0,0 +1,24 @@ +package service_order_status + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient, deviceType string) string { + var part string + switch deviceType { + case "WAF": + part = "FGWAF" + break + case "HostBased": + part = "HBS" + break + default: + part = "FGS" + } + + url := fmt.Sprintf("API/ScreenEvent%sOrderProgressRate", part) + return client.ServiceURL(url) +} diff --git a/v4/ecl/security_portal/v3/device_interfaces/doc.go b/v4/ecl/security_portal/v3/device_interfaces/doc.go new file mode 100644 index 0000000..4ae5098 --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/doc.go @@ -0,0 +1,2 @@ +// Package device_interfaces contains device management functionality in security portal API +package device_interfaces diff --git a/v4/ecl/security_portal/v3/device_interfaces/requests.go b/v4/ecl/security_portal/v3/device_interfaces/requests.go new file mode 100644 index 0000000..22d83da --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/requests.go @@ -0,0 +1,40 @@ +package device_interfaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToDeviceInterfaceQuery() (string, error) +} + +// ListOpts converts tenant id and token as query string +type ListOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToDeviceInterfaceQuery formats a ListOpts into a query string. +func (opts ListOpts) ToDeviceInterfaceQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// device interfaces. +func List(client *eclcloud.ServiceClient, serverUUID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, serverUUID) + if opts != nil { + query, err := opts.ToDeviceInterfaceQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return DeviceInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v4/ecl/security_portal/v3/device_interfaces/results.go b/v4/ecl/security_portal/v3/device_interfaces/results.go new file mode 100644 index 0000000..9ed343b --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/results.go @@ -0,0 +1,65 @@ +package device_interfaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// DeviceInterface represents the result of a each element in +// response of device interface api result. +type DeviceInterface struct { + OSIPAddress string `json:"os_ip_address"` + MSAPortID string `json:"msa_port_id"` + OSPortName string `json:"os_port_name"` + OSPortID string `json:"os_port_id"` + OSNetworkID string `json:"os_network_id"` + OSPortStatus string `json:"os_port_status"` + OSMACAddress string `json:"os_mac_address"` + OSSubnetID string `json:"os_subnet_id"` +} + +// DeviceInterfacePage is the page returned by a pager +// when traversing over a collection of Device Interface. +type DeviceInterfacePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of Single Device Interface +// 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 (r DeviceInterfacePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"device_interfaces"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a DeviceInterfacePage struct is empty. +func (r DeviceInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractDeviceInterfaces(r) + return len(is) == 0, err +} + +// ExtractDeviceInterfaces accepts a Page struct, +// specifically a DeviceInterfacePage struct, and extracts the elements +// into a slice of Device Interface structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractDeviceInterfaces(r pagination.Page) ([]DeviceInterface, error) { + var d []DeviceInterface + err := ExtractDeviceInterfacesInto(r, &d) + return d, err +} + +// ExtractDeviceInterfacesInto interprets the results of a single page from a List() call, +// producing a slice of Device Interface entities. +func ExtractDeviceInterfacesInto(r pagination.Page, v interface{}) error { + return r.(DeviceInterfacePage).Result.ExtractIntoSlicePtr(v, "device_interfaces") +} diff --git a/v4/ecl/security_portal/v3/device_interfaces/testing/doc.go b/v4/ecl/security_portal/v3/device_interfaces/testing/doc.go new file mode 100644 index 0000000..7c9a653 --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains device interfaces unit tests +package testing diff --git a/v4/ecl/security_portal/v3/device_interfaces/testing/fixtures.go b/v4/ecl/security_portal/v3/device_interfaces/testing/fixtures.go new file mode 100644 index 0000000..c957f85 --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/testing/fixtures.go @@ -0,0 +1,60 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/device_interfaces" +) + +const deviceUUID = "cad6e00a-2af9-491c-b732-ca5688d147f5" + +const listResponse = ` +{ + "device_interfaces": [ + { + "os_ip_address": "192.168.1.50", + "msa_port_id": "port4", + "os_port_name": "port4-CES11892", + "os_port_id": "82ebe045-9c9a-4088-8b33-cb0d590079aa", + "os_network_id": "5ef9c597-15fe-431c-84b9-88d00d567202", + "os_port_status": "ACTIVE", + "os_mac_address": "fa:16:3e:05:ff:66", + "os_subnet_id": "48ea24c7-fe48-4a54-9ed0-528aa09cebc7" + }, + { + "os_ip_address": "192.168.2.50", + "msa_port_id": "port7", + "os_port_name": "port7-CES11892", + "os_port_id": "82ebe045-9c9a-4088-8b33-cb0d590079aa", + "os_network_id": "5ef9c597-15fe-431c-84b9-88d00d567203", + "os_port_status": "ACTIVE", + "os_mac_address": "fa:16:3e:05:ff:67", + "os_subnet_id": "48ea24c7-fe48-4a54-9ed0-528aa09cebc8" + } + ] +} +` + +var expectedDeviceInterfacesSlice = []device_interfaces.DeviceInterface{ + firstDeviceInterface, secondDeviceInterface, +} + +var firstDeviceInterface = device_interfaces.DeviceInterface{ + OSIPAddress: "192.168.1.50", + MSAPortID: "port4", + OSPortName: "port4-CES11892", + OSPortID: "82ebe045-9c9a-4088-8b33-cb0d590079aa", + OSNetworkID: "5ef9c597-15fe-431c-84b9-88d00d567202", + OSPortStatus: "ACTIVE", + OSMACAddress: "fa:16:3e:05:ff:66", + OSSubnetID: "48ea24c7-fe48-4a54-9ed0-528aa09cebc7", +} + +var secondDeviceInterface = device_interfaces.DeviceInterface{ + OSIPAddress: "192.168.2.50", + MSAPortID: "port7", + OSPortName: "port7-CES11892", + OSPortID: "82ebe045-9c9a-4088-8b33-cb0d590079aa", + OSNetworkID: "5ef9c597-15fe-431c-84b9-88d00d567203", + OSPortStatus: "ACTIVE", + OSMACAddress: "fa:16:3e:05:ff:67", + OSSubnetID: "48ea24c7-fe48-4a54-9ed0-528aa09cebc8", +} diff --git a/v4/ecl/security_portal/v3/device_interfaces/testing/requests_test.go b/v4/ecl/security_portal/v3/device_interfaces/testing/requests_test.go new file mode 100644 index 0000000..35a4548 --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/testing/requests_test.go @@ -0,0 +1,57 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/device_interfaces" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListDeviceInterfaces(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/ecl-api/devices/%s/interfaces", deviceUUID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := device_interfaces.List(fakeclient.ServiceClient(), deviceUUID, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := device_interfaces.ExtractDeviceInterfaces(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDeviceInterfacesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceInterfaceAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/ecl-api/devices/%s/interfaces", deviceUUID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := device_interfaces.List(fakeclient.ServiceClient(), deviceUUID, nil).AllPages() + th.AssertNoErr(t, err) + allDeviceInterfaces, err := device_interfaces.ExtractDeviceInterfaces(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDeviceInterfaces)) +} diff --git a/v4/ecl/security_portal/v3/device_interfaces/urls.go b/v4/ecl/security_portal/v3/device_interfaces/urls.go new file mode 100644 index 0000000..3d3246d --- /dev/null +++ b/v4/ecl/security_portal/v3/device_interfaces/urls.go @@ -0,0 +1,12 @@ +package device_interfaces + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func listURL(client *eclcloud.ServiceClient, serverUUID string) string { + url := fmt.Sprintf("ecl-api/devices/%s/interfaces", serverUUID) + return client.ServiceURL(url) +} diff --git a/v4/ecl/security_portal/v3/devices/doc.go b/v4/ecl/security_portal/v3/devices/doc.go new file mode 100644 index 0000000..6a88b4d --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/doc.go @@ -0,0 +1,2 @@ +// Package devices contains device management functionality in security portal API +package devices diff --git a/v4/ecl/security_portal/v3/devices/requests.go b/v4/ecl/security_portal/v3/devices/requests.go new file mode 100644 index 0000000..611d267 --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/requests.go @@ -0,0 +1,40 @@ +package devices + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToDevicesQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToDevicesQuery formats a ListOpts into a query string. +func (opts ListOpts) ToDevicesQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over +// a collection of devices. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToDevicesQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return DevicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/v4/ecl/security_portal/v3/devices/results.go b/v4/ecl/security_portal/v3/devices/results.go new file mode 100644 index 0000000..ec11dc9 --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/results.go @@ -0,0 +1,64 @@ +package devices + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Device represents the result of a each element in +// response of device api result. +type Device struct { + MSADeviceID string `json:"msa_device_id"` + OSServerID string `json:"os_server_id"` + OSServerName string `json:"os_server_name"` + OSAvailabilityZone string `json:"os_availability_zone"` + OSAdminUserName string `json:"os_admin_username"` + MSADeviceType string `json:"msa_device_type"` + OSServerStatus string `json:"os_server_status"` +} + +// DevicePage is the page returned by a pager +// when traversing over a collection of Device. +type DevicePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of Device +// 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 (r DevicePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"devices"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a Device struct is empty. +func (r DevicePage) IsEmpty() (bool, error) { + is, err := ExtractDevices(r) + return len(is) == 0, err +} + +// ExtractDevices accepts a Page struct, +// specifically a DevicePage struct, and extracts the elements +// into a slice of Device structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractDevices(r pagination.Page) ([]Device, error) { + var d []Device + err := ExtractDevicesInto(r, &d) + return d, err +} + +// ExtractDevicesInto interprets the results of a single page from a List() call, +// producing a slice of Device entities. +func ExtractDevicesInto(r pagination.Page, v interface{}) error { + return r.(DevicePage).Result.ExtractIntoSlicePtr(v, "devices") +} diff --git a/v4/ecl/security_portal/v3/devices/testing/doc.go b/v4/ecl/security_portal/v3/devices/testing/doc.go new file mode 100644 index 0000000..d541ffa --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains devices unit tests +package testing diff --git a/v4/ecl/security_portal/v3/devices/testing/fixtures.go b/v4/ecl/security_portal/v3/devices/testing/fixtures.go new file mode 100644 index 0000000..2150b43 --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/testing/fixtures.go @@ -0,0 +1,51 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/devices" +) + +const listResponse = ` +{ + "devices": [ + { + "msa_device_id": "CES11810", + "os_server_id": "392a90bf-2c1b-45fd-8221-096894fff39d", + "os_server_name": "UTM-CES11878", + "os_availability_zone": "zone1-groupb", + "os_admin_username": "jp4_sdp_mss_utm_admin", + "msa_device_type": "FW", + "os_server_status": "ACTIVE" + }, + { + "msa_device_id": "CES11811", + "os_server_id": "12768064-e7c9-44d1-b01d-e66f138a278e", + "os_server_name": "WAF-CES11816", + "os_availability_zone": "zone1-groupb", + "os_admin_username": "jp4_sdp_mss_utm_admin", + "msa_device_type": "WAF", + "os_server_status": "ACTIVE" + } + ] +}` + +var expectedDevicesSlice = []devices.Device{firstDevice, secondDevice} + +var firstDevice = devices.Device{ + MSADeviceID: "CES11810", + OSServerID: "392a90bf-2c1b-45fd-8221-096894fff39d", + OSServerName: "UTM-CES11878", + OSAvailabilityZone: "zone1-groupb", + OSAdminUserName: "jp4_sdp_mss_utm_admin", + MSADeviceType: "FW", + OSServerStatus: "ACTIVE", +} + +var secondDevice = devices.Device{ + MSADeviceID: "CES11811", + OSServerID: "12768064-e7c9-44d1-b01d-e66f138a278e", + OSServerName: "WAF-CES11816", + OSAvailabilityZone: "zone1-groupb", + OSAdminUserName: "jp4_sdp_mss_utm_admin", + MSADeviceType: "WAF", + OSServerStatus: "ACTIVE", +} diff --git a/v4/ecl/security_portal/v3/devices/testing/requests_test.go b/v4/ecl/security_portal/v3/devices/testing/requests_test.go new file mode 100644 index 0000000..14d10d3 --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/testing/requests_test.go @@ -0,0 +1,55 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/devices" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListDevices(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ecl-api/devices", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := devices.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := devices.ExtractDevices(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedDevicesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListDeviceAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ecl-api/devices", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := devices.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allDevices, err := devices.ExtractDevices(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allDevices)) +} diff --git a/v4/ecl/security_portal/v3/devices/urls.go b/v4/ecl/security_portal/v3/devices/urls.go new file mode 100644 index 0000000..a51c3af --- /dev/null +++ b/v4/ecl/security_portal/v3/devices/urls.go @@ -0,0 +1,9 @@ +package devices + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("ecl-api/devices") +} diff --git a/v4/ecl/security_portal/v3/ha_ports/doc.go b/v4/ecl/security_portal/v3/ha_ports/doc.go new file mode 100644 index 0000000..6412766 --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/doc.go @@ -0,0 +1,2 @@ +// Package ha_ports contains port management functionality in security portal API +package ha_ports diff --git a/v4/ecl/security_portal/v3/ha_ports/requests.go b/v4/ecl/security_portal/v3/ha_ports/requests.go new file mode 100644 index 0000000..1c1ebe9 --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/requests.go @@ -0,0 +1,78 @@ +package ha_ports + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// SinglePort represents parameters to update a Single Port. +type SinglePort struct { + EnablePort string `json:"enable_port" required:"true"` + IPAddress []string `json:"ip_address,omitempty"` + NetworkID string `json:"network_id,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` + MTU string `json:"mtu,omitempty"` + Comment string `json:"comment,omitempty"` + + EnablePing string `json:"enable_ping,omitempty"` + VRRPGroupID string `json:"vrrp_grp_id,omitempty"` + VRRPID string `json:"vrrp_id,omitempty"` + VRRPIPAddress string `json:"vrrp_ip,omitempty"` + Preempt string `json:"preempt,omitempty"` +} + +// UpdateOpts represents options used to update a port. +type UpdateOpts struct { + Port []SinglePort `json:"port" required:"true"` +} + +// ToPortUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// UpdateQueryOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateQueryOptsBuilder interface { + ToUpdateQuery() (string, error) +} + +// UpdateQueryOpts represents query strings for updating port. +type UpdateQueryOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToUpdateQuery formats a ListOpts into a query string. +func (opts UpdateQueryOpts) ToUpdateQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Update modifies the attributes of a port. +func Update(client *eclcloud.ServiceClient, + hostName string, + opts UpdateOptsBuilder, + qOpts UpdateQueryOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + + url := updateURL(client, hostName) + if qOpts != nil { + query, _ := qOpts.ToUpdateQuery() + url += query + } + + _, r.Err = client.Put(url, &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/security_portal/v3/ha_ports/results.go b/v4/ecl/security_portal/v3/ha_ports/results.go new file mode 100644 index 0000000..50bad45 --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/results.go @@ -0,0 +1,69 @@ +package ha_ports + +import ( + "encoding/json" + "strconv" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Port resource. +func (r commonResult) Extract() (*UpdateProcess, error) { + var p UpdateProcess + err := r.ExtractInto(&p) + return &p, err +} + +// Extract interprets any commonResult as a Port if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// UpdateProcess represents the result of a each element in +// response of port api result. +type UpdateProcess struct { + Message string `json:"message"` + ProcessID int `json:"processId"` + ID string `json:"-"` +} + +// ProcessPage is the page returned by a pager +// when traversing over a collection of Single Port. +type ProcessPage struct { + pagination.LinkedPageBase +} + +// UnmarshalJSON function overrides original functionality, +// to parse processId as unique identifier of process. +// Note: +// ID parameter in each struct must be string, +// but in api result of process polling API, +// processId is returned as integer value. +// This function solves this problem. +func (r *UpdateProcess) UnmarshalJSON(b []byte) error { + type tmp UpdateProcess + var s struct { + tmp + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = UpdateProcess(s.tmp) + + r.ID = strconv.Itoa(r.ProcessID) + + return err +} diff --git a/v4/ecl/security_portal/v3/ha_ports/testing/doc.go b/v4/ecl/security_portal/v3/ha_ports/testing/doc.go new file mode 100644 index 0000000..134142d --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains ports unit tests +package testing diff --git a/v4/ecl/security_portal/v3/ha_ports/testing/fixtures.go b/v4/ecl/security_portal/v3/ha_ports/testing/fixtures.go new file mode 100644 index 0000000..7019ccf --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/testing/fixtures.go @@ -0,0 +1,29 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/ports" +) + +const updateRequest = `{ + "port": [ + { + "comment": "port 0 comment", + "enable_port":"true", + "ip_address": "192.168.1.50/24", + "mtu":"1500", + "network_id": "32314bd2-3583-4fb9-b622-9b121e04e007", + "subnet_id": "7fd77711-abae-4828-93f1-f3d682a8771f" + } + ] +}` + +const updateResponse = `{ + "message": "The process launch request has been accepted", + "processId": 85385 +}` + +var expectedResult = ports.UpdateProcess{ + Message: "The process launch request has been accepted", + ProcessID: 85385, + ID: "85385", +} diff --git a/v4/ecl/security_portal/v3/ha_ports/testing/requests_test.go b/v4/ecl/security_portal/v3/ha_ports/testing/requests_test.go new file mode 100644 index 0000000..132548a --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/testing/requests_test.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/ports" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestUpdatePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/ecl-api/ports/utm/CES11995" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, updateResponse) + }) + + updateOpts := ports.UpdateOpts{ + Port: []ports.SinglePort{ + { + Comment: "port 0 comment", + EnablePort: "true", + IPAddress: "192.168.1.50/24", + MTU: "1500", + NetworkID: "32314bd2-3583-4fb9-b622-9b121e04e007", + SubnetID: "7fd77711-abae-4828-93f1-f3d682a8771f", + }, + }, + } + + actual, err := ports.Update( + fakeclient.ServiceClient(), + "utm", + "CES11995", + updateOpts, + nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} diff --git a/v4/ecl/security_portal/v3/ha_ports/urls.go b/v4/ecl/security_portal/v3/ha_ports/urls.go new file mode 100644 index 0000000..efee52a --- /dev/null +++ b/v4/ecl/security_portal/v3/ha_ports/urls.go @@ -0,0 +1,12 @@ +package ha_ports + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func updateURL(client *eclcloud.ServiceClient, hostName string) string { + url := fmt.Sprintf("ecl-api/ports/utm/ha/%s", hostName) + return client.ServiceURL(url) +} diff --git a/v4/ecl/security_portal/v3/ports/doc.go b/v4/ecl/security_portal/v3/ports/doc.go new file mode 100644 index 0000000..6c06631 --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/doc.go @@ -0,0 +1,2 @@ +// Package ports contains port management functionality in security portal API +package ports diff --git a/v4/ecl/security_portal/v3/ports/requests.go b/v4/ecl/security_portal/v3/ports/requests.go new file mode 100644 index 0000000..2e76a6f --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/requests.go @@ -0,0 +1,73 @@ +package ports + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// SinglePort represents parameters to update a Single Port. +type SinglePort struct { + EnablePort string `json:"enable_port" required:"true"` + IPAddress string `json:"ip_address,omitempty"` + NetworkID string `json:"network_id,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` + MTU string `json:"mtu,omitempty"` + Comment string `json:"comment,omitempty"` +} + +// UpdateOpts represents options used to update a port. +type UpdateOpts struct { + Port []SinglePort `json:"port" required:"true"` +} + +// ToPortUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// UpdateQueryOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateQueryOptsBuilder interface { + ToUpdateQuery() (string, error) +} + +// UpdateQueryOpts represents query strings for updating port. +type UpdateQueryOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToUpdateQuery formats a ListOpts into a query string. +func (opts UpdateQueryOpts) ToUpdateQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Update modifies the attributes of a port. +func Update(client *eclcloud.ServiceClient, + serviceType string, + hostName string, + opts UpdateOptsBuilder, + qOpts UpdateQueryOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + + url := updateURL(client, serviceType, hostName) + if qOpts != nil { + query, _ := qOpts.ToUpdateQuery() + url += query + } + + _, r.Err = client.Put(url, &b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/v4/ecl/security_portal/v3/ports/results.go b/v4/ecl/security_portal/v3/ports/results.go new file mode 100644 index 0000000..ee6b1a8 --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/results.go @@ -0,0 +1,69 @@ +package ports + +import ( + "encoding/json" + "strconv" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Port resource. +func (r commonResult) Extract() (*UpdateProcess, error) { + var p UpdateProcess + err := r.ExtractInto(&p) + return &p, err +} + +// Extract interprets any commonResult as a Port if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// UpdateProcess represents the result of a each element in +// response of port api result. +type UpdateProcess struct { + Message string `json:"message"` + ProcessID int `json:"processId"` + ID string `json:"-"` +} + +// ProcessPage is the page returned by a pager +// when traversing over a collection of Single Port. +type ProcessPage struct { + pagination.LinkedPageBase +} + +// UnmarshalJSON function overrides original functionality, +// to parse processId as unique identifier of process. +// Note: +// ID parameter in each struct must be string, +// but in api result of process polling API, +// processId is returned as integer value. +// This function solves this problem. +func (r *UpdateProcess) UnmarshalJSON(b []byte) error { + type tmp UpdateProcess + var s struct { + tmp + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = UpdateProcess(s.tmp) + + r.ID = strconv.Itoa(r.ProcessID) + + return err +} diff --git a/v4/ecl/security_portal/v3/ports/testing/doc.go b/v4/ecl/security_portal/v3/ports/testing/doc.go new file mode 100644 index 0000000..134142d --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains ports unit tests +package testing diff --git a/v4/ecl/security_portal/v3/ports/testing/fixtures.go b/v4/ecl/security_portal/v3/ports/testing/fixtures.go new file mode 100644 index 0000000..7019ccf --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/testing/fixtures.go @@ -0,0 +1,29 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/ports" +) + +const updateRequest = `{ + "port": [ + { + "comment": "port 0 comment", + "enable_port":"true", + "ip_address": "192.168.1.50/24", + "mtu":"1500", + "network_id": "32314bd2-3583-4fb9-b622-9b121e04e007", + "subnet_id": "7fd77711-abae-4828-93f1-f3d682a8771f" + } + ] +}` + +const updateResponse = `{ + "message": "The process launch request has been accepted", + "processId": 85385 +}` + +var expectedResult = ports.UpdateProcess{ + Message: "The process launch request has been accepted", + ProcessID: 85385, + ID: "85385", +} diff --git a/v4/ecl/security_portal/v3/ports/testing/requests_test.go b/v4/ecl/security_portal/v3/ports/testing/requests_test.go new file mode 100644 index 0000000..132548a --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/testing/requests_test.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/ports" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestUpdatePort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := "/ecl-api/ports/utm/CES11995" + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, updateResponse) + }) + + updateOpts := ports.UpdateOpts{ + Port: []ports.SinglePort{ + { + Comment: "port 0 comment", + EnablePort: "true", + IPAddress: "192.168.1.50/24", + MTU: "1500", + NetworkID: "32314bd2-3583-4fb9-b622-9b121e04e007", + SubnetID: "7fd77711-abae-4828-93f1-f3d682a8771f", + }, + }, + } + + actual, err := ports.Update( + fakeclient.ServiceClient(), + "utm", + "CES11995", + updateOpts, + nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedResult, actual) +} diff --git a/v4/ecl/security_portal/v3/ports/urls.go b/v4/ecl/security_portal/v3/ports/urls.go new file mode 100644 index 0000000..b02a979 --- /dev/null +++ b/v4/ecl/security_portal/v3/ports/urls.go @@ -0,0 +1,12 @@ +package ports + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func updateURL(client *eclcloud.ServiceClient, deviceType string, hostName string) string { + url := fmt.Sprintf("ecl-api/ports/%s/%s", deviceType, hostName) + return client.ServiceURL(url) +} diff --git a/v4/ecl/security_portal/v3/processes/doc.go b/v4/ecl/security_portal/v3/processes/doc.go new file mode 100644 index 0000000..160ef42 --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/doc.go @@ -0,0 +1,2 @@ +// Package process contains port management functionality on security +package processes diff --git a/v4/ecl/security_portal/v3/processes/requests.go b/v4/ecl/security_portal/v3/processes/requests.go new file mode 100644 index 0000000..d1dfe39 --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/requests.go @@ -0,0 +1,35 @@ +package processes + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// GetOptsBuilder allows extensions to add additional parameters to +// the order API request +type GetOptsBuilder interface { + ToProcessQuery() (string, error) +} + +// GetOpts represents result of order API response. +type GetOpts struct { + TenantID string `q:"tenantid"` + UserToken string `q:"usertoken"` +} + +// ToProcessQuery formats a GetOpts into a query string. +func (opts GetOpts) ToProcessQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves details on a single order, by ID. +func Get(client *eclcloud.ServiceClient, processID string, opts GetOptsBuilder) (r GetResult) { + url := getURL(client, processID) + if opts != nil { + query, _ := opts.ToProcessQuery() + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/v4/ecl/security_portal/v3/processes/results.go b/v4/ecl/security_portal/v3/processes/results.go new file mode 100644 index 0000000..bb23909 --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/results.go @@ -0,0 +1,36 @@ +package processes + +import ( + "github.com/nttcom/eclcloud/v4" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a Process. +func (r commonResult) Extract() (*ProcessInstance, error) { + var pr ProcessInstance + err := r.ExtractInto(&pr) + return &pr, err +} + +// Extract interprets any commonResult as a Process, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "processInstance") +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Process. +type GetResult struct { + commonResult +} + +type ProcessStatus struct { + Status string `json:"status"` +} + +type ProcessInstance struct { + Status ProcessStatus `json:"status"` +} diff --git a/v4/ecl/security_portal/v3/processes/testing/doc.go b/v4/ecl/security_portal/v3/processes/testing/doc.go new file mode 100644 index 0000000..d64fb4a --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains processes unit tests +package testing diff --git a/v4/ecl/security_portal/v3/processes/testing/fixtures.go b/v4/ecl/security_portal/v3/processes/testing/fixtures.go new file mode 100644 index 0000000..987ce16 --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/testing/fixtures.go @@ -0,0 +1,186 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/processes" +) + +const processID = "85385" + +const getResponse = ` +{ + "processInstance": { + "processId": { + "id": 85385, + "lastExecNumber": 1, + "name": "ntt/FortiVA_Port_Management/Process_Manage_UTM_Interfaces/Process_Manage_UTM_Interfaces", + "submissionType": "RUN" + }, + "serviceId": { + "id": 19382, + "name": "FortiVA_Port_Management", + "serviceReference": "PORT_MNGT_CES11892", + "state": null + }, + "status": { + "comment": "Ping Monitoring started for the device 11892.", + "duration": 0, + "endingDate": "2019-07-26 04:34:56.0", + "execNumber": 1, + "processInstanceId": 85385, + "processName": "ntt/FortiVA_Port_Management/Process_Manage_UTM_Interfaces/Process_Manage_UTM_Interfaces", + "startingDate": "2019-07-26 04:24:45.0", + "status": "RUNNING", + "taskStatusList": [ + { + "comment": "IP Address inputs verified successfully.", + "endingDate": "2019-07-26 04:24:48.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:24:45.0", + "status": "ENDED", + "taskId": 1, + "taskName": "Verify IP Address, MTU Inputs" + }, + { + "comment": "Ping Monitoring stopped for the device 11892.", + "endingDate": "2019-07-26 04:26:49.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:24:48.0", + "status": "ENDED", + "taskId": 2, + "taskName": "Stop Ping Monitoring" + }, + { + "comment": "Openstack Server 158eb01a-8d45-45c8-a9ff-1fba8f1ab7e3 stopped successfully.\nServer Status : SHUTOFF\nTask State : -\nPower State : Shutdown\n", + "endingDate": "2019-07-26 04:27:03.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:26:49.0", + "status": "ENDED", + "taskId": 3, + "taskName": "Stop the UTM" + }, + { + "comment": "IP Address 100.76.96.230 is now unreachable from MSA.\nPING Status : Destination Host Unreachable\n", + "endingDate": "2019-07-26 04:27:13.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:27:03.0", + "status": "ENDED", + "taskId": 4, + "taskName": "Wait for UTM Ping unreachability from MSA" + }, + { + "comment": "Ports deleted successfully.", + "endingDate": "2019-07-26 04:28:29.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:27:13.0", + "status": "ENDED", + "taskId": 5, + "taskName": "Delete Ports" + }, + { + "comment": "Ports created successfully.\nPort Id : 34c7389d-1428-4f98-a37c-9c2e32aab255\nPort Id : 3d09053b-fad8-45c4-bf71-501c0fc2b58a\nPort Id : 0262d90c-6056-4308-8b76-8e851f0132f5\nPort Id : 5fcabdf2-8a20-4337-bd10-02f5c5000ca1\nPort Id : 53211b09-f82b-40d5-bf5b-7289a298cbdf\nPort Id : 9ce2d3b7-7ae0-400d-8e41-16dc9b94f95e\nPort Id : a36493fe-43d2-4dc1-a39e-c96898e9c0be\n", + "endingDate": "2019-07-26 04:29:50.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:28:29.0", + "status": "ENDED", + "taskId": 6, + "taskName": "Create Ports" + }, + { + "comment": "Ports attached successfully to the Server 158eb01a-8d45-45c8-a9ff-1fba8f1ab7e3.", + "endingDate": "2019-07-26 04:31:33.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:29:50.0", + "status": "ENDED", + "taskId": 7, + "taskName": "Attach Ports" + }, + { + "comment": "Openstack Server 158eb01a-8d45-45c8-a9ff-1fba8f1ab7e3 started successfully.\nServer Status : ACTIVE\nTask State : -\nPower State : Running\n", + "endingDate": "2019-07-26 04:31:47.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:31:33.0", + "status": "ENDED", + "taskId": 8, + "taskName": "Start the UTM" + }, + { + "comment": "IP Address 100.76.96.230 is now reachable from MSA.\nPING Status : OK\n", + "endingDate": "2019-07-26 04:32:30.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:31:47.0", + "status": "ENDED", + "taskId": 9, + "taskName": "Wait for UTM Ping reachability from MSA" + }, + { + "comment": "OK LICENSE IS VALID", + "endingDate": "2019-07-26 04:32:56.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:32:30.0", + "status": "ENDED", + "taskId": 10, + "taskName": "Verify License Validity" + }, + { + "comment": "Ports updated successfully on Fortigate Device 11892.\n", + "endingDate": "2019-07-26 04:33:17.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:32:56.0", + "status": "ENDED", + "taskId": 11, + "taskName": "Update UTM" + }, + { + "comment": "Device 11892 Backup completed successfully.\nBackup Status : ENDED\nBackup Message : BACKUP processed\n\nBackup Revision Id : 209408\n", + "endingDate": "2019-07-26 04:33:28.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:33:17.0", + "status": "ENDED", + "taskId": 12, + "taskName": "Device Backup" + }, + { + "comment": "Ping Monitoring started for the device 11892.", + "endingDate": "2019-07-26 04:34:56.0", + "execNumber": 1, + "newParameters": {}, + "processInstanceId": 85385, + "startingDate": "2019-07-26 04:33:28.0", + "status": "ENDED", + "taskId": 13, + "taskName": "Start Ping Monitoring" + } + ] + } + } +}` + +var expectedProcess = processes.ProcessInstance{ + Status: processes.ProcessStatus{ + Status: "RUNNING", + }, +} diff --git a/v4/ecl/security_portal/v3/processes/testing/requests_test.go b/v4/ecl/security_portal/v3/processes/testing/requests_test.go new file mode 100644 index 0000000..a227124 --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/testing/requests_test.go @@ -0,0 +1,30 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/security_portal/v3/processes" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestGetProcess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/ecl-api/process/%s/status", processID) + fmt.Println(url) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := processes.Get(fakeclient.ServiceClient(), processID, nil).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expectedProcess, actual) +} diff --git a/v4/ecl/security_portal/v3/processes/urls.go b/v4/ecl/security_portal/v3/processes/urls.go new file mode 100644 index 0000000..64b2517 --- /dev/null +++ b/v4/ecl/security_portal/v3/processes/urls.go @@ -0,0 +1,12 @@ +package processes + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient, processID string) string { + url := fmt.Sprintf("ecl-api/process/%s/status", processID) + return client.ServiceURL(url) +} diff --git a/v4/ecl/sss/v2/approval_requests/doc.go b/v4/ecl/sss/v2/approval_requests/doc.go new file mode 100644 index 0000000..49196e5 --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/doc.go @@ -0,0 +1,44 @@ +/* +Package approval_requests manages and retrieves approval requests in the Enterprise Cloud. + +Example to List approval requests + + allPages, err := approval_requests.List(client).AllPages() + if err != nil { + panic(err) + } + + allApprovalRequests, err := approval_requests.ExtractApprovalRequests(allPages) + if err != nil { + panic(err) + } + + for _, approvalRequest := range allApprovalRequests { + fmt.Printf("%+v\n", approvalRequest) + } + +Example to Get an approval requests + + requestID := "02471b45-3de0-4fc8-8469-a7cc52c378df" + + approvalRequest, err := approval_requests.Get(client, requestID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", approvalRequest) + +Example to Update an approval request + + requestID := "02471b45-3de0-4fc8-8469-a7cc52c378df" + updateOpts := approval_requests.UpdateOpts{ + Status: "approved", + } + + result := approval_requests.Update(client, requestID, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +*/ +package approval_requests diff --git a/v4/ecl/sss/v2/approval_requests/requests.go b/v4/ecl/sss/v2/approval_requests/requests.go new file mode 100644 index 0000000..04df786 --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/requests.go @@ -0,0 +1,77 @@ +package approval_requests + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToApprovalRequestListQuery() (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 approval request attributes you want to see returned. +type ListOpts struct { + Status string `q:"status"` + Service string `q:"service"` +} + +// ToApprovalRequestListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToApprovalRequestListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List retrieves a list of approval requests. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToApprovalRequestListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ApprovalRequestPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of an approval request. +func Get(client *eclcloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToResourceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update an approval request. +type UpdateOpts struct { + Status string `json:"status" required:"true"` +} + +// ToResourceUpdateMap formats a UpdateOpts to update approval request. +func (opts UpdateOpts) ToResourceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of an approval request. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToResourceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, nil, &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }) + return +} diff --git a/v4/ecl/sss/v2/approval_requests/results.go b/v4/ecl/sss/v2/approval_requests/results.go new file mode 100644 index 0000000..3f03c1f --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/results.go @@ -0,0 +1,95 @@ +package approval_requests + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type Action struct { + Service string `json:"service"` + Region string `json:"region"` + APIPath string `json:"api_path"` + Method string `json:"method"` + // Basically JSON is passed to Action.Body, + // but depending on the value of the service, it may be a String, so it is set to interface{}. + // If service is "provider-connectivity", body's type is JSON. + // If service is "network", body's type is String. + Body interface{} `json:"body"` +} + +type Description struct { + Lang string `json:"lang"` + Text string `json:"text"` +} + +// ApprovalRequest represents an ECL SSS Approval Request. +type ApprovalRequest struct { + RequestID string `json:"request_id"` + ExternalRequestID string `json:"external_request_id"` + ApproverType string `json:"approver_type"` + ApproverID string `json:"approver_id"` + RequestUserID string `json:"request_user_id"` + Service string `json:"service"` + Actions []Action `json:"actions"` + Descriptions []Description `json:"descriptions"` + RequestUser interface{} `json:"request_user"` + Approver bool `json:"approver"` + ApprovalDeadLine interface{} `json:"approval_deadline"` + ApprovalExpire interface{} `json:"approval_expire"` + RegisteredTime interface{} `json:"registered_time"` + UpdatedTime interface{} `json:"updated_time"` + Status string `json:"status"` +} + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*ApprovalRequest, error) { + var ar ApprovalRequest + err := r.ExtractInto(&ar) + return &ar, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as an approval request. +type GetResult struct { + commonResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as an approval request. +type UpdateResult struct { + commonResult +} + +// ApprovalRequestPage is a single page of approval request results. +type ApprovalRequestPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of approval requests contains any results. +func (r ApprovalRequestPage) IsEmpty() (bool, error) { + resources, err := ExtractApprovalRequests(r) + return len(resources) == 0, err +} + +// ExtractApprovalRequests returns a slice of approval requests +// contained in a single page of results. +func ExtractApprovalRequests(r pagination.Page) ([]ApprovalRequest, error) { + var s struct { + ApprovalRequests []ApprovalRequest `json:"approval_requests"` + } + err := (r.(ApprovalRequestPage)).ExtractInto(&s) + return s.ApprovalRequests, err +} + +// ExtractApprovalRequestsInto interprets the results of a single page from a List() call, +// producing a slice of Approval Request entities. +func ExtractApprovalRequestsInto(r pagination.Page, v interface{}) error { + return r.(ApprovalRequestPage).Result.ExtractIntoSlicePtr(v, "") +} diff --git a/v4/ecl/sss/v2/approval_requests/testing/doc.go b/v4/ecl/sss/v2/approval_requests/testing/doc.go new file mode 100644 index 0000000..c513bc0 --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/testing/doc.go @@ -0,0 +1,2 @@ +// sss approval request unit tests +package testing diff --git a/v4/ecl/sss/v2/approval_requests/testing/fixtures.go b/v4/ecl/sss/v2/approval_requests/testing/fixtures.go new file mode 100644 index 0000000..cb23f9f --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/testing/fixtures.go @@ -0,0 +1,203 @@ +package testing + +import ( + "fmt" + + ar "github.com/nttcom/eclcloud/v4/ecl/sss/v2/approval_requests" +) + +const idApprovalRequest1 = "9a76dca6-d8cd-4391-aac6f-2ea052f10f4" +const idApprovalRequest2 = "fc578e8b-dea2-4f8c-aa7e-9026fa173632" + +var listResponse = fmt.Sprintf(` +{ + "approval_requests": [ + { + "request_id": "%s", + "external_request_id": "test007", + "approver_type":"tenant_owner", + "approver_id":"11a98bf9cb144af5a204c9da566d2bd0", + "request_user_id":"ecid9999888881", + "service":"provider-connectivity", + "actions" : [ + { + "service": "provider-connectivity", + "region": "jp1", + "api_path": "/v2.0/tenant_connections_requests", + "method": "POST", + "body": { + "tenant_connection_request": { + "tenant_id_other": "d2f19c353e6d4c519e530c6a78438b33", + "network_id": "ea7eea8c-0d91-4553-9ecb-01f81e2c3989" + } + } + } + ], + "descriptions": [ + { + "lang": "en", + "text": "approval request test" + } + ], + "request_user": false, + "approver": true, + "approval_deadline": "2017-02-05 09:45:22", + "approval_expire": null, + "registered_time": "2017-01-31 07:43:13", + "updated_time": null, + "status": "registered" + }, + { + "request_id": "%s", + "external_request_id": "test006", + "approver_type":"tenant_owner", + "approver_id":"66a98bf9cb1238192a204c9da566dbd0", + "request_user_id":"ecid9999888882", + "service":"network", + "actions" : [ + { + "service": "network", + "region": "jp1", + "api_path": "/network/v1/firewall", + "method": "POST", + "body": "{\n\t\"firewall\": {\n\t\t\"availability_zone\": \"zone1-groupa\",\n\t\t\"default_gateway\": \"\",\n\t\t\"description\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"firewall_plan_id\": \"bd12784a-c66e-4f13-9f72-5143d64762b6\",\n\t\t\"name\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"tenant_id\": \"6a156ddf2ecd497ca786ff2da6df5aa8\"\n\t}\n}" + } + ], + "descriptions": [ + { + "lang": "en", + "text": "approval request test" + } + ], + "request_user": false, + "approver": true, + "approval_deadline": "2016-12-25 09:45:22", + "approval_expire": null, + "registered_time": "2016-12-13 02:20:21", + "updated_time": null, + "status": "expired" + } + ] +} +`, + idApprovalRequest1, + idApprovalRequest2, +) + +var expectedApprovalRequestsSlice = []ar.ApprovalRequest{ + firstApprovalRequest, + secondApprovalRequest, +} + +var firstApprovalRequest = ar.ApprovalRequest{ + RequestID: idApprovalRequest1, + ExternalRequestID: "test007", + ApproverType: "tenant_owner", + ApproverID: "11a98bf9cb144af5a204c9da566d2bd0", + RequestUserID: "ecid9999888881", + Service: "provider-connectivity", + Actions: []ar.Action{ + { + Service: "provider-connectivity", + Region: "jp1", + APIPath: "/v2.0/tenant_connections_requests", + Method: "POST", + Body: map[string]interface{}{ + "tenant_connection_request": map[string]string{ + "tenant_id_other": "d2f19c353e6d4c519e530c6a78438b33", + "network_id": "ea7eea8c-0d91-4553-9ecb-01f81e2c3989", + }, + }, + }, + }, + Descriptions: []ar.Description{ + { + Lang: "en", + Text: "approval request test", + }, + }, + RequestUser: false, + Approver: true, + ApprovalDeadLine: interface{}("2017-02-05 09:45:22"), + ApprovalExpire: interface{}(nil), + RegisteredTime: interface{}("2017-01-31 07:43:13"), + UpdatedTime: interface{}(nil), + Status: "registered", +} + +var secondApprovalRequest = ar.ApprovalRequest{ + RequestID: idApprovalRequest2, + ExternalRequestID: "test006", + ApproverType: "tenant_owner", + ApproverID: "66a98bf9cb1238192a204c9da566dbd0", + RequestUserID: "ecid9999888882", + Service: "network", + Actions: []ar.Action{ + { + Service: "network", + Region: "jp1", + APIPath: "/network/v1/firewall", + Method: "POST", + Body: "{\n\t\"firewall\": {\n\t\t\"availability_zone\": \"zone1-groupa\",\n\t\t\"default_gateway\": \"\",\n\t\t\"description\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"firewall_plan_id\": \"bd12784a-c66e-4f13-9f72-5143d64762b6\",\n\t\t\"name\": \"abcdefghijklmnopqrstuvwxyz\",\n\t\t\"tenant_id\": \"6a156ddf2ecd497ca786ff2da6df5aa8\"\n\t}\n}", + }, + }, + Descriptions: []ar.Description{ + { + Lang: "en", + Text: "approval request test", + }, + }, + RequestUser: false, + Approver: true, + ApprovalDeadLine: interface{}("2016-12-25 09:45:22"), + ApprovalExpire: interface{}(nil), + RegisteredTime: interface{}("2016-12-13 02:20:21"), + UpdatedTime: interface{}(nil), + Status: "expired", +} + +var getResponse = fmt.Sprintf(` + { + "request_id": "%s", + "external_request_id": "test007", + "approver_type":"tenant_owner", + "approver_id":"11a98bf9cb144af5a204c9da566d2bd0", + "request_user_id":"ecid9999888881", + "service":"provider-connectivity", + "actions" : [ + { + "service": "provider-connectivity", + "region": "jp1", + "api_path": "/v2.0/tenant_connections_requests", + "method": "POST", + "body": { + "tenant_connection_request": { + "tenant_id_other": "d2f19c353e6d4c519e530c6a78438b33", + "network_id": "ea7eea8c-0d91-4553-9ecb-01f81e2c3989" + } + } + } + ], + "descriptions": [ + { + "lang": "en", + "text": "approval request test" + } + ], + "request_user": false, + "approver": true, + "approval_deadline": "2017-02-05 09:45:22", + "approval_expire": null, + "registered_time": "2017-01-31 07:43:13", + "updated_time": null, + "status": "registered" + } +`, + idApprovalRequest1, +) + +const updateRequest = ` +{ + "status": "approved" +} +` diff --git a/v4/ecl/sss/v2/approval_requests/testing/requests_test.go b/v4/ecl/sss/v2/approval_requests/testing/requests_test.go new file mode 100644 index 0000000..dddbd50 --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/testing/requests_test.go @@ -0,0 +1,96 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + ar "github.com/nttcom/eclcloud/v4/ecl/sss/v2/approval_requests" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListApprovalRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/approval-requests", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := ar.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ar.ExtractApprovalRequests(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedApprovalRequestsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListApprovalRequestAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/approval-requests", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := ar.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allRequests, err := ar.ExtractApprovalRequests(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allRequests)) +} + +func TestGetApprovalRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/approval-requests/%s", idApprovalRequest1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := ar.Get(fakeclient.ServiceClient(), idApprovalRequest1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &firstApprovalRequest, actual) +} + +func TestUpdateApprovalRequest(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/approval-requests/%s", idApprovalRequest1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusNoContent) + }) + + updateOpts := ar.UpdateOpts{ + Status: "approved", + } + + res := ar.Update(fakeclient.ServiceClient(), idApprovalRequest1, updateOpts) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/sss/v2/approval_requests/urls.go b/v4/ecl/sss/v2/approval_requests/urls.go new file mode 100644 index 0000000..62f90fa --- /dev/null +++ b/v4/ecl/sss/v2/approval_requests/urls.go @@ -0,0 +1,15 @@ +package approval_requests + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("approval-requests") +} + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("approval-requests", id) +} + +func updateURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("approval-requests", id) +} diff --git a/v4/ecl/sss/v2/tenants/doc.go b/v4/ecl/sss/v2/tenants/doc.go new file mode 100644 index 0000000..e11b7ff --- /dev/null +++ b/v4/ecl/sss/v2/tenants/doc.go @@ -0,0 +1,34 @@ +/* +Package tenants manages and retrieves Projects in the ECL SSS Service. + +Example to List Tenants + + listOpts := tenants.ListOpts{} + + allPages, err := tenants.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allTenants, err := tenants.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, tenant := range allTenants { + fmt.Printf("%+v\n", tenant) + } + +Example to Create a Tenant + + createOpts := projects.CreateOpts{ + Name: "tenant_name", + Description: "Tenant Description" + } + + tenant, err := tenants.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package tenants diff --git a/v4/ecl/sss/v2/tenants/errors.go b/v4/ecl/sss/v2/tenants/errors.go new file mode 100644 index 0000000..08cfa34 --- /dev/null +++ b/v4/ecl/sss/v2/tenants/errors.go @@ -0,0 +1,17 @@ +package tenants + +import "fmt" + +// InvalidListFilter is returned by the ToUserListQuery method when validation of +// a filter does not pass +type InvalidListFilter struct { + FilterName string +} + +func (e InvalidListFilter) Error() string { + s := fmt.Sprintf( + "Invalid filter name [%s]: it must be in format of NAME__COMPARATOR", + e.FilterName, + ) + return s +} diff --git a/v4/ecl/sss/v2/tenants/requests.go b/v4/ecl/sss/v2/tenants/requests.go new file mode 100644 index 0000000..68c0cae --- /dev/null +++ b/v4/ecl/sss/v2/tenants/requests.go @@ -0,0 +1,74 @@ +package tenants + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToTenantListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +// Currently SSS Tenant API does not support any of query parameters. +type ListOpts struct { +} + +// ToTenantListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Projects to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single tenant, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToTenantCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a tenant. +type CreateOpts struct { + // Workspace ID. + WorkspaceID string `json:"workspace_id" required:"true"` + // TenantRegion of the tenant. + TenantRegion string `json:"region" required:"true"` +} + +// ToTenantCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToTenantCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new Project. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} diff --git a/v4/ecl/sss/v2/tenants/results.go b/v4/ecl/sss/v2/tenants/results.go new file mode 100644 index 0000000..9a0e806 --- /dev/null +++ b/v4/ecl/sss/v2/tenants/results.go @@ -0,0 +1,130 @@ +package tenants + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type tenantResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a Tenant. +type GetResult struct { + tenantResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Tenant. +type CreateResult struct { + tenantResult +} + +// Tenant represents an ECL SSS Tenant. +type Tenant struct { + // ID of contract which owns these tenants. + ContractID string `json:"contract_id"` + // ID is the unique ID of the tenant. + TenantID string `json:"tenant_id"` + // Name of the tenant. + TenantName string `json:"tenant_name"` + // Description of the tenant. + Description string `json:"description"` + // TenantRegion the tenant belongs. + TenantRegion string `json:"region"` + // Time that the tenant is created. + StartTime time.Time `json:"-"` + // SSS API endpoint for the region. + RegionApiEndpoint string `json:"region_api_endpoint"` + // Users information who have access to this tenant. + User []User `json:"users"` + // Brand ID which this tenant belongs. (ex. ecl2) + BrandID string `json:"brand_id"` + // Workspace ID of the tenant. + WorkspaceID string `json:"workspace_id"` +} + +type User struct { + // ID of the users who have access to this tenant. + UserID string `json:"user_id"` + // Contract which owns the tenant. + ContractID string `json:"contract_id"` + // This user is contract owner / or not. + ContractOwner bool `json:"contract_owner"` +} + +// UnmarshalJSON creates JSON format of tenant +func (r *Tenant) UnmarshalJSON(b []byte) error { + type tmp Tenant + var s struct { + tmp + StartTime eclcloud.JSONRFC3339ZNoTNoZ `json:"start_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Tenant(s.tmp) + + r.StartTime = time.Time(s.StartTime) + + return err +} + +// 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 (r TenantPage) IsEmpty() (bool, error) { + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractTenants returns a slice of Tenants contained in a single page of +// results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + ContractID string `json:"contract_id"` + Tenants []Tenant `json:"tenants"` + } + + // In list response case, each json element does not have contract_id. + // It is set at out layer of each element. + // So following logic set contract_id into inside of tenants slice forcibly. + // In "show(get with ID of tenant)" case, this does not occur. + err := (r.(TenantPage)).ExtractInto(&s) + contractID := s.ContractID + + for i := 0; i < len(s.Tenants); i++ { + s.Tenants[i].ContractID = contractID + } + return s.Tenants, err +} + +// Extract interprets any projectResults as a Tenant. +func (r tenantResult) Extract() (*Tenant, error) { + var s *Tenant + err := r.ExtractInto(&s) + return s, err +} diff --git a/v4/ecl/sss/v2/tenants/testing/doc.go b/v4/ecl/sss/v2/tenants/testing/doc.go new file mode 100644 index 0000000..020c8c3 --- /dev/null +++ b/v4/ecl/sss/v2/tenants/testing/doc.go @@ -0,0 +1,2 @@ +// sss tenant unit tests +package testing diff --git a/v4/ecl/sss/v2/tenants/testing/fixtures.go b/v4/ecl/sss/v2/tenants/testing/fixtures.go new file mode 100644 index 0000000..e56b755 --- /dev/null +++ b/v4/ecl/sss/v2/tenants/testing/fixtures.go @@ -0,0 +1,160 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/tenants" +) + +const contractID = "econ8000008888" + +const workspaceID1 = "ws0000000001" +const workspaceID2 = "ws0000000002" + +const idTenant1 = "9a76dca6d8cd4391aac6f2ea052f10f4" +const idTenant2 = "27a58d42769141ff8e94920a99aeb44b" + +const nameTenant1 = "jp1_tenant01" +const nameTenant2 = "jp1_tenant02" + +const descriptionTenant1 = "jp1 tenant01" +const descriptionTenant2 = "jp1 tenant02" + +const startTime = "2018-07-26 08:40:01" + +// ListResponse is a sample response to a List call. +var ListResponse = fmt.Sprintf(` +{ + "contract_id": "%s", + "tenants": [{ + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp1", + "start_time": "%s", + "workspace_id": "%s" + }, { + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp2", + "start_time": "%s", + "workspace_id": "%s" + }] +} +`, + contractID, + // fot tenant 1 + idTenant1, + nameTenant1, + descriptionTenant1, + startTime, + workspaceID1, + // for tenant 2 + idTenant2, + nameTenant2, + descriptionTenant2, + startTime, + workspaceID2, +) + +// ExpectedTenantsSlice is the slice of results that should be parsed +// from ListResponse in the expected order. +var ExpectedTenantsSlice = []tenants.Tenant{FirstTenant, SecondTenant} + +// TenantStartTime is parsed tenant start time +var TenantStartTime, _ = time.Parse(eclcloud.RFC3339ZNoTNoZ, startTime) + +// FirstTenant is the mock object of expected tenant-1 +var FirstTenant = tenants.Tenant{ + ContractID: contractID, + TenantID: idTenant1, + TenantName: nameTenant1, + Description: descriptionTenant1, + TenantRegion: "jp1", + StartTime: TenantStartTime, + WorkspaceID: workspaceID1, +} + +// SecondTenant is the mock object of expected tenant-2 +var SecondTenant = tenants.Tenant{ + ContractID: contractID, + TenantID: idTenant2, + TenantName: nameTenant2, + Description: descriptionTenant2, + TenantRegion: "jp2", + StartTime: TenantStartTime, + WorkspaceID: workspaceID2, +} + +// GetResponse is a sample response to a Get call. +// This get result does not have action, attributes in ECL2.0 +var GetResponse = fmt.Sprintf(` +{ + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp1", + "contract_id": "%s", + "region_api_endpoint": "https://example.com:443/api", + "start_time": "%s", + "users": [{ + "user_id": "ecid8000008888", + "contract_id": "%s", + "contract_owner": true + }], + "brand_id": "ecl2", + "workspace_id": "%s" +}`, idTenant1, + nameTenant1, + descriptionTenant1, + contractID, + startTime, + contractID, + workspaceID1, +) + +// GetResponseStruct mocked actual tenant +var GetResponseStruct = tenants.Tenant{ + ContractID: contractID, + TenantID: idTenant1, + TenantName: nameTenant1, + Description: descriptionTenant1, + TenantRegion: "jp1", + StartTime: TenantStartTime, + RegionApiEndpoint: "https://example.com:443/api", + User: []tenants.User{ + { + UserID: "ecid8000008888", + ContractID: contractID, + ContractOwner: true, + }, + }, + BrandID: "ecl2", + WorkspaceID: workspaceID1, +} + +// CreateRequest is a sample request to create a tenant. +var CreateRequest = fmt.Sprintf(`{ + "workspace_id": "%s", + "region": "jp1" +}`, + workspaceID1, +) + +// CreateResponse is a sample response to a create request. +var CreateResponse = fmt.Sprintf(`{ + "workspace_id": "%s", + "tenant_id": "%s", + "tenant_name": "%s", + "description": "%s", + "region": "jp1", + "contract_id": "%s" +}`, workspaceID1, + idTenant1, + nameTenant1, + descriptionTenant1, + contractID, +) diff --git a/v4/ecl/sss/v2/tenants/testing/requests_test.go b/v4/ecl/sss/v2/tenants/testing/requests_test.go new file mode 100644 index 0000000..c6a9991 --- /dev/null +++ b/v4/ecl/sss/v2/tenants/testing/requests_test.go @@ -0,0 +1,105 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/tenants" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListResponse) + }) + + count := 0 + err := tenants.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := tenants.ExtractTenants(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedTenantsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListTenantAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListResponse) + }) + + allPages, err := tenants.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := tenants.ExtractTenants(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/tenants/%s", idTenant1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetResponse) + }) + + actual, err := tenants.Get(fakeclient.ServiceClient(), idTenant1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GetResponseStruct, actual) +} + +func TestCreateTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := tenants.CreateOpts{ + WorkspaceID: workspaceID1, + TenantRegion: "jp1", + } + + // clone FirstTenant into createdTenant(Used as assertion target) + // and initialize StartTime + createdTenant := FirstTenant + createdTenant.StartTime = time.Time{} + + actual, err := tenants.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdTenant, actual) +} diff --git a/v4/ecl/sss/v2/tenants/urls.go b/v4/ecl/sss/v2/tenants/urls.go new file mode 100644 index 0000000..2adffb2 --- /dev/null +++ b/v4/ecl/sss/v2/tenants/urls.go @@ -0,0 +1,15 @@ +package tenants + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func getURL(client *eclcloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("tenants") +} diff --git a/v4/ecl/sss/v2/users/doc.go b/v4/ecl/sss/v2/users/doc.go new file mode 100644 index 0000000..b38d0fe --- /dev/null +++ b/v4/ecl/sss/v2/users/doc.go @@ -0,0 +1,73 @@ +/* +Package users contains user management functionality on SSS. + +Example to List users + + listOpts := users.ListOpts{} + + allPages, err := users.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + panic(err) + } + + for _, user := range allUsers { + fmt.Printf("%+v\n", user) + } + +Example to Get a user + + id := "ecid0000000001" + user, err := users.Get(client, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", user) + +Example to Create a user + + createOpts := users.CreateOpts{ + LoginID: "sample", + MailAddress: "example@example.com", + Password: "Passw0rd", + NotifyPassword: "true", + } + + user, err := users.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a user + + userID := "ecid0000000001" + loginID := "login-id-update" + mailAddress := "update@example.com" + newPassword := "NewPassw0rd" + + updateOpts := users.UpdateOpts{ + LoginID: &loginID, + MailAddress: &mailAddress, + NewPassword: &newPassword, + } + + result := users.Update(client, userID, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a user + + userID := "ecid0000000001" + res := users.Delete(client, userID) + if res.Err != nil { + panic(res.Err) + } + +*/ +package users diff --git a/v4/ecl/sss/v2/users/requests.go b/v4/ecl/sss/v2/users/requests.go new file mode 100644 index 0000000..fe500fb --- /dev/null +++ b/v4/ecl/sss/v2/users/requests.go @@ -0,0 +1,132 @@ +package users + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToUserListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +// Currently SSS User API does not support any of query parameters. +type ListOpts struct { +} + +// ToUserListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToUserListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Users to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single user, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a user. +type CreateOpts struct { + // Login id of new user. + LoginID string `json:"login_id" required:"true"` + + // Mail address of new user. + MailAddress string `json:"mail_address" required:"true"` + + // Initial password of new user. + // If this parameter is not designated, + // random initial password is generated and applied to new user. + Password string `json:"password,omitempty"` + + // If this flag is set 'true', notification e-mail will be sent to new user's email. + NotifyPassword string `json:"notify_password" required:"true"` +} + +// ToUserCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new user. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a user. +func Delete(client *eclcloud.ServiceClient, userID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, userID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a user. +type UpdateOpts struct { + // New login id of the user. + LoginID *string `json:"login_id" required:"true"` + + // New email address of the user + MailAddress *string `json:"mail_address" required:"true"` + + // New password of the user + NewPassword *string `json:"new_password" required:"true"` +} + +// ToUserUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToUserUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a user. +// SSS User PUT API does not have response body, +// so set JSONResponse option as nil. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToUserUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put( + updateURL(client, id), + b, + nil, + &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }, + ) + return +} diff --git a/v4/ecl/sss/v2/users/results.go b/v4/ecl/sss/v2/users/results.go new file mode 100644 index 0000000..a5b23fd --- /dev/null +++ b/v4/ecl/sss/v2/users/results.go @@ -0,0 +1,128 @@ +package users + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type userResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a User. +type GetResult struct { + userResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a User. +type CreateResult struct { + userResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a User. +type UpdateResult struct { + userResult +} + +// User represents an ECL SSS User. +type User struct { + LoginID string `json:"login_id"` + MailAddress string `json:"mail_address"` + UserID string `json:"user_id"` + ContractOwner bool `json:"contract_owner"` + Superuser bool `json:"super_user"` + ApiAvailability bool `json:"api_availability"` + KeystoneName string `json:"keystone_name"` + KeystoneEndpoint string `json:"keystone_endpoint"` + SSSEndpoint string `json:"sss_endpoint"` + ContractID string `json:"contract_id"` + LoginIntegration string `json:"login_integration"` + ExternalReferenceID string `json:"external_reference_id"` + BrandID string `json:"brand_id"` + OtpActivation bool `json:"otp_activation"` + StartTime time.Time `json:"-"` +} + +// UnmarshalJSON creates JSON format of user +func (r *User) UnmarshalJSON(b []byte) error { + type tmp User + var s struct { + tmp + StartTime eclcloud.JSONRFC3339ZNoTNoZ `json:"start_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = User(s.tmp) + + r.StartTime = time.Time(s.StartTime) + + return err +} + +// UserPage is a single page of User results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of User contains any results. +func (r UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(r) + return len(users) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r UserPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractUsers returns a slice of Users contained in a single page of +// results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + ContractID string `json:"contract_id"` + Users []User `json:"users"` + } + + // In list response case, each json element does not have contract_id. + // It is set at out layer of each element. + // So following logic set contract_id into inside of users slice forcibly. + // In "show(get with ID of tenant)" case, this does not occur. + err := (r.(UserPage)).ExtractInto(&s) + contractID := s.ContractID + + for i := 0; i < len(s.Users); i++ { + s.Users[i].ContractID = contractID + } + return s.Users, err +} + +// Extract interprets any projectResults as a User. +func (r userResult) Extract() (*User, error) { + var u *User + err := r.ExtractInto(&u) + return u, err +} diff --git a/v4/ecl/sss/v2/users/testing/doc.go b/v4/ecl/sss/v2/users/testing/doc.go new file mode 100644 index 0000000..5bf4d42 --- /dev/null +++ b/v4/ecl/sss/v2/users/testing/doc.go @@ -0,0 +1,2 @@ +// sss user unit tests +package testing diff --git a/v4/ecl/sss/v2/users/testing/fixtures.go b/v4/ecl/sss/v2/users/testing/fixtures.go new file mode 100644 index 0000000..25a530f --- /dev/null +++ b/v4/ecl/sss/v2/users/testing/fixtures.go @@ -0,0 +1,144 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/users" +) + +const contractID = "econ8000008888" + +const idUser1 = "ecid1000000001" +const idUser2 = "ecid1000000002" + +const startTime = "2018-07-26 08:40:01" + +var listResponse = fmt.Sprintf(` +{ + "contract_id": "%s", + "users": [{ + "user_id": "%s", + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "start_time": "%s", + "api_availability": true, + "contract_owner": true, + "super_user": true + }, { + "user_id": "%s", + "login_id": "login_id_2", + "mail_address": "user2@example.com", + "start_time": "%s", + "api_availability": true, + "contract_owner": true, + "super_user": true + }] +}`, contractID, + idUser1, startTime, + idUser2, startTime) + +var expectedUsersSlice = []users.User{firstUser, secondUser} + +var userStartTime, _ = time.Parse(eclcloud.RFC3339ZNoTNoZ, startTime) + +var firstUser = users.User{ + UserID: idUser1, + LoginID: "login_id_1", + MailAddress: "user1@example.com", + ContractID: contractID, + StartTime: userStartTime, + ApiAvailability: true, + ContractOwner: true, + Superuser: true, +} + +var secondUser = users.User{ + UserID: idUser2, + LoginID: "login_id_2", + MailAddress: "user2@example.com", + ContractID: contractID, + StartTime: userStartTime, + ApiAvailability: true, + ContractOwner: true, + Superuser: true, +} + +var getResponse = fmt.Sprintf(` +{ + "user_id": "%s", + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "contract_owner": false, + "super_user": false, + "api_availability": true, + "sss_endpoint": "http://sss.com", + "keystone_endpoint": "http://keystone.com", + "keystone_name": "keystonename1", + "keystone_password": "keystonepassword1", + "start_time": "%s", + "contract_id": "%s", + "login_integration": "", + "external_reference_id": "econ0000009999", + "brand_id": "ecl2", + "auto_role_assignment_flag": false, + "external_user_type": "iop", + "otp_activation": false +}`, idUser1, + startTime, + contractID, +) + +var getResponseStruct = users.User{ + UserID: idUser1, + LoginID: "login_id_1", + MailAddress: "user1@example.com", + ContractOwner: false, + Superuser: false, + ApiAvailability: true, + SSSEndpoint: "http://sss.com", + KeystoneEndpoint: "http://keystone.com", + KeystoneName: "keystonename1", + StartTime: userStartTime, + ContractID: contractID, + LoginIntegration: "", + ExternalReferenceID: "econ0000009999", + BrandID: "ecl2", + OtpActivation: false, +} + +var createRequest = `{ + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "notify_password": "false", + "password": "Passw0rd" +}` + +var createResponse = fmt.Sprintf(`{ + "login_id": "login_id_1", + "mail_address": "user1@example.com", + "user_id": "%s", + "contract_id": "%s", + "keystone_endpoint": "http://keystone.com", + "sss_endpoint": "http://sss.com", + "password": "Passw0rd" +} +`, idUser1, + contractID, +) + +var createdUser = users.User{ + LoginID: "login_id_1", + UserID: idUser1, + ContractID: contractID, + MailAddress: "user1@example.com", + KeystoneEndpoint: "http://keystone.com", + SSSEndpoint: "http://sss.com", +} + +var updateRequest = `{ + "login_id": "login_id_1_update", + "mail_address": "user1_update@example.com", + "new_password": "NewPassw0rd" +}` diff --git a/v4/ecl/sss/v2/users/testing/requests_test.go b/v4/ecl/sss/v2/users/testing/requests_test.go new file mode 100644 index 0000000..36c28da --- /dev/null +++ b/v4/ecl/sss/v2/users/testing/requests_test.go @@ -0,0 +1,152 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/users" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := users.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedUsersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListUserAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := users.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := users.ExtractUsers(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/users/%s", idUser1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := users.Get(fakeclient.ServiceClient(), idUser1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &getResponseStruct, actual) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + createOpts := users.CreateOpts{ + LoginID: "login_id_1", + MailAddress: "user1@example.com", + NotifyPassword: "false", + Password: "Passw0rd", + } + + // clone FirstTenant into createdUser(Used as assertion target) + // and initialize StartTime + // createdUser := firstUser + // createdUser.StartTime = time.Time{} + + actual, err := users.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdUser, actual) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/users/%s", idUser1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusNoContent) + }) + + loginID := "login_id_1_update" + mailAddress := "user1_update@example.com" + newPassword := "NewPassw0rd" + + updateOpts := users.UpdateOpts{ + LoginID: &loginID, + MailAddress: &mailAddress, + NewPassword: &newPassword, + } + + // In ECL2.0 user update API returns + // - StatusNoContent + // - No response body as PUT response + res := users.Update(fakeclient.ServiceClient(), idUser1, updateOpts) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/users/%s", idUser1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := users.Delete(fakeclient.ServiceClient(), idUser1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/sss/v2/users/urls.go b/v4/ecl/sss/v2/users/urls.go new file mode 100644 index 0000000..dcc7aca --- /dev/null +++ b/v4/ecl/sss/v2/users/urls.go @@ -0,0 +1,23 @@ +package users + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func deleteURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func updateURL(client *eclcloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} diff --git a/v4/ecl/sss/v2/workspace_roles/doc.go b/v4/ecl/sss/v2/workspace_roles/doc.go new file mode 100644 index 0000000..f04b60c --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/doc.go @@ -0,0 +1,31 @@ +/* +Package workspace_roles contains workspace-role management functionality on SSS. + +Example to Create a workspace-role + + workspaceID := "ws00000000001" + userID := "ecid0000000001" + + createOpts := workspace_roles.CreateOpts{ + UserID: userID, + WorkspaceID: workspaceID, + } + + role, err := workspace_roles.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", role) + +Example to Delete a workspace-role + + workspaceID := "ws00000000001" + userID := "ecid00000000001" + result := workspace_roles.Delete(client, workspaceID, userID) + if result.Err != nil { + panic(result.Err) + } + +*/ +package workspace_roles diff --git a/v4/ecl/sss/v2/workspace_roles/requests.go b/v4/ecl/sss/v2/workspace_roles/requests.go new file mode 100644 index 0000000..f9287a5 --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/requests.go @@ -0,0 +1,36 @@ +package workspace_roles + +import "github.com/nttcom/eclcloud/v4" + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToWorkspaceRoleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a workspace-role. +type CreateOpts struct { + UserID string `json:"user_id" required:"true"` + WorkspaceID string `json:"workspace_id" required:"true"` +} + +// ToWorkspaceRoleCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToWorkspaceRoleCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new workspace-role. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToWorkspaceRoleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a workspace-role. +func Delete(client *eclcloud.ServiceClient, workspaceID string, userID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, workspaceID, userID), nil) + return +} diff --git a/v4/ecl/sss/v2/workspace_roles/results.go b/v4/ecl/sss/v2/workspace_roles/results.go new file mode 100644 index 0000000..3a11168 --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/results.go @@ -0,0 +1,34 @@ +package workspace_roles + +import ( + "github.com/nttcom/eclcloud/v4" +) + +type workspaceRoleResult struct { + eclcloud.Result +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Workspace-Role. +type CreateResult struct { + workspaceRoleResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +type WorkspaceRole struct { + UserID string `json:"user_id"` + WorkspaceID string `json:"workspace_id"` + WorkspaceName string `json:"workspace_name"` +} + +// Extract interprets any projectResults as a Workspace-Role. +func (r workspaceRoleResult) Extract() (*WorkspaceRole, error) { + var s WorkspaceRole + err := r.ExtractInto(&s) + return &s, err +} diff --git a/v4/ecl/sss/v2/workspace_roles/testing/doc.go b/v4/ecl/sss/v2/workspace_roles/testing/doc.go new file mode 100644 index 0000000..f11fe00 --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/testing/doc.go @@ -0,0 +1,2 @@ +// sss workspace-role unit tests +package testing diff --git a/v4/ecl/sss/v2/workspace_roles/testing/fixtures.go b/v4/ecl/sss/v2/workspace_roles/testing/fixtures.go new file mode 100644 index 0000000..661fb70 --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/testing/fixtures.go @@ -0,0 +1,28 @@ +package testing + +import "github.com/nttcom/eclcloud/v4/ecl/sss/v2/workspace_roles" + +const workspaceID = "ws0000000001" + +const userID = "ecid1234567891" + +var createRequest = ` +{ + "user_id": "ecid1234567891", + "workspace_id": "ws0000000001" +} +` + +var createResponse = ` +{ + "user_id": "ecid1234567891", + "workspace_id": "ws0000000001", + "workspace_name": "testWorkspace001" +} +` + +var createdWorkspaceRole = workspace_roles.WorkspaceRole{ + UserID: userID, + WorkspaceID: workspaceID, + WorkspaceName: "testWorkspace001", +} diff --git a/v4/ecl/sss/v2/workspace_roles/testing/requests_test.go b/v4/ecl/sss/v2/workspace_roles/testing/requests_test.go new file mode 100644 index 0000000..678af33 --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/testing/requests_test.go @@ -0,0 +1,52 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/workspace_roles" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestCreateWorkspaceRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/workspace-roles", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + createOpts := workspace_roles.CreateOpts{ + UserID: "ecid1234567891", + WorkspaceID: "ws0000000001", + } + + actual, err := workspace_roles.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdWorkspaceRole, actual) +} + +func TestDeleteWorkspace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/workspace-roles/workspaces/%s/users/%s", workspaceID, userID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := workspace_roles.Delete(fakeclient.ServiceClient(), workspaceID, userID) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/sss/v2/workspace_roles/urls.go b/v4/ecl/sss/v2/workspace_roles/urls.go new file mode 100644 index 0000000..b04543b --- /dev/null +++ b/v4/ecl/sss/v2/workspace_roles/urls.go @@ -0,0 +1,16 @@ +package workspace_roles + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4" +) + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("workspace-roles") +} + +func deleteURL(client *eclcloud.ServiceClient, workspaceID string, userID string) string { + url := fmt.Sprintf("workspace-roles/workspaces/%s/users/%s", workspaceID, userID) + return client.ServiceURL(url) +} diff --git a/v4/ecl/sss/v2/workspaces/doc.go b/v4/ecl/sss/v2/workspaces/doc.go new file mode 100644 index 0000000..c9278c0 --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/doc.go @@ -0,0 +1,67 @@ +/* +Package workspaces contains workspace management functionality on SSS. + +Example to List workspaces + + listOpts := workspaces.ListOpts{ContractID: "econ0000000001"} + + allPages, err := workspaces.List(client, listOpts).AllPages() + if err != nil { + panic(err) + } + + allWorkspaces, err := workspaces.ExtractWorkspaces(allPages) + if err != nil { + panic(err) + } + + for _, workspace := range allWorkspaces { + fmt.Printf("%+v\n", workspace) + } + +Example to Get a workspace + + id := "ws0000000001" + workspace, err := workspaces.Get(client, id).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", workspace) + +Example to Create a workspace + + createOpts := workspaces.CreateOpts{ + WorkspaceName: "Example-Workspace", + Description: "Example Workspace", + ContractID: "econ0000000001", + } + + workspace, err := workspaces.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", workspace) + +Example to Update a workspace + + workspaceID := "ws0000000001" + description := "update description" + updateOpts := workspaces.UpdateOpts{Description: &description} + + result := workspaces.Update(client, workspaceID, updateOpts) + if result.Err != nil { + panic(result.Err) + } + +Example to Delete a workspace + + workspaceID := "ws0000000001" + res := workspaces.Delete(client, workspaceID) + if res.Err != nil { + panic(res.Err) + } + +*/ +package workspaces diff --git a/v4/ecl/sss/v2/workspaces/requests.go b/v4/ecl/sss/v2/workspaces/requests.go new file mode 100644 index 0000000..eba46d4 --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/requests.go @@ -0,0 +1,117 @@ +package workspaces + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToWorkspaceListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +// Currently SSS Workspace API does not support any of query parameters. +type ListOpts struct { + ContractID string `q:"contract_id"` +} + +// ToWorkspaceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToWorkspaceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Workspaces to which the current token has access. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToWorkspaceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return WorkspacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single workspace, by ID. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToWorkspaceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a workspace. +type CreateOpts struct { + // Workspace Name. + WorkspaceName string `json:"workspace_name" required:"true"` + // Workspace description. + Description string `json:"description,omitempty"` + // ContractID to be associated with the workspace. + ContractID string `json:"contract_id,omitempty"` +} + +// ToWorkspaceCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToWorkspaceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Create creates a new workspace. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToWorkspaceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a workspace. +func Delete(client *eclcloud.ServiceClient, workspaceID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, workspaceID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the Update request. +type UpdateOptsBuilder interface { + ToWorkspaceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a workspace. +type UpdateOpts struct { + // Workspace description. + Description *string `json:"description" required:"true"` +} + +// ToWorkspaceUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToWorkspaceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "") +} + +// Update modifies the attributes of a workspace. +// SSS Workspace PUT API does not have response body, so set JSONResponse option as nil. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToWorkspaceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put( + updateURL(client, id), + b, + nil, + &eclcloud.RequestOpts{ + OkCodes: []int{204}, + }, + ) + return +} diff --git a/v4/ecl/sss/v2/workspaces/results.go b/v4/ecl/sss/v2/workspaces/results.go new file mode 100644 index 0000000..8ffbc36 --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/results.go @@ -0,0 +1,129 @@ +package workspaces + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type workspaceResult struct { + eclcloud.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a Workspace. +type GetResult struct { + workspaceResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Workspace. +type CreateResult struct { + workspaceResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Workspace. +type UpdateResult struct { + workspaceResult +} + +type Workspace struct { + ContractID string `json:"contract_id"` + WorkspaceID string `json:"workspace_id"` + WorkspaceName string `json:"workspace_name"` + Description string `json:"description"` + StartTime time.Time `json:"-"` + Regions []Region `json:"regions"` + Users []User `json:"users"` +} + +type Region struct { + RegionName string `json:"region_name"` + TenantID string `json:"tenant_id"` +} + +type User struct { + UserID string `json:"user_id"` + ContractID string `json:"contract_id"` + ContractOwner bool `json:"contract_owner"` +} + +// UnmarshalJSON creates JSON format of workspace +func (r *Workspace) UnmarshalJSON(b []byte) error { + type tmp Workspace + var s struct { + tmp + StartTime eclcloud.JSONRFC3339ZNoTNoZ `json:"start_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Workspace(s.tmp) + + r.StartTime = time.Time(s.StartTime) + + return err +} + +// WorkspacePage is a single page of Workspace results. +type WorkspacePage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Workspace contains any results. +func (r WorkspacePage) IsEmpty() (bool, error) { + workspaces, err := ExtractWorkspaces(r) + return len(workspaces) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r WorkspacePage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractWorkspaces returns a slice of Workspace contained in a single page of results. +func ExtractWorkspaces(r pagination.Page) ([]Workspace, error) { + var s struct { + ContractID string `json:"contract_id"` + Workspaces []Workspace `json:"workspaces"` + } + + // In list response case, each json element does not have contract_id. + // It is set at out layer of each element. + // So following logic set contract_id into inside of workspaces slice forcibly. + // In "show(get with ID of workspace)" case, this does not occur. + err := (r.(WorkspacePage)).ExtractInto(&s) + contractID := s.ContractID + + for i := 0; i < len(s.Workspaces); i++ { + s.Workspaces[i].ContractID = contractID + } + return s.Workspaces, err +} + +// Extract interprets any projectResults as a Workspace. +func (r workspaceResult) Extract() (*Workspace, error) { + var s *Workspace + err := r.ExtractInto(&s) + return s, err +} diff --git a/v4/ecl/sss/v2/workspaces/testing/doc.go b/v4/ecl/sss/v2/workspaces/testing/doc.go new file mode 100644 index 0000000..e362fa0 --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/testing/doc.go @@ -0,0 +1,2 @@ +// sss workspace unit tests +package testing diff --git a/v4/ecl/sss/v2/workspaces/testing/fixtures.go b/v4/ecl/sss/v2/workspaces/testing/fixtures.go new file mode 100644 index 0000000..5eee994 --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/testing/fixtures.go @@ -0,0 +1,158 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/workspaces" +) + +const contractID = "econ0000000001" + +const workspaceID1 = "ws0000000001" +const workspaceID2 = "ws0000000002" + +const nameWorkspace1 = "jp1_workspace01" +const nameWorkspace2 = "jp1_workspace02" + +const descriptionWorkspace1 = "jp1 workspace01" +const descriptionWorkspace2 = "jp1 workspace02" + +const startTime = "2020-01-01 00:00:00" + +var workspaceStartTime, _ = time.Parse(eclcloud.RFC3339ZNoTNoZ, startTime) + +var listResponse = fmt.Sprintf(` +{ + "contract_id": "%s", + "workspaces": [ + { + "workspace_id": "%s", + "workspace_name": "%s", + "description": "%s", + "start_time": "%s" + }, + { + "workspace_id": "%s", + "workspace_name": "%s", + "description": "%s", + "start_time": "%s" + } + ] +} +`, + contractID, + workspaceID1, nameWorkspace1, descriptionWorkspace1, startTime, + workspaceID2, nameWorkspace2, descriptionWorkspace2, startTime, +) + +var firstWorkspace = workspaces.Workspace{ + ContractID: contractID, + WorkspaceID: workspaceID1, + WorkspaceName: nameWorkspace1, + Description: descriptionWorkspace1, + StartTime: workspaceStartTime, +} + +var secondWorkspace = workspaces.Workspace{ + ContractID: contractID, + WorkspaceID: workspaceID2, + WorkspaceName: nameWorkspace2, + Description: descriptionWorkspace2, + StartTime: workspaceStartTime, +} + +var expectedWorkspacesSlice = []workspaces.Workspace{firstWorkspace, secondWorkspace} + +var getResponse = fmt.Sprintf(` +{ + "contract_id": "%s", + "workspace_id": "%s", + "workspace_name": "%s", + "description": "%s", + "start_time": "%s", + "regions": [ + { + "region_name": "jp1", + "tenant_id": "9a76dca6d8cd4391aac6f2ea052f10f4" + }, + { + "region_name": "jp2", + "tenant_id": "27a58d42769141ff8e94920a99aeb44b" + } + ], + "users": [ + { + "user_id": "ecid000000001", + "contract_id": "econ0000000001", + "contract_owner": true + }, + { + "user_id": "ecid000000002", + "contract_id": "econ0000000002", + "contract_owner": false + } + ] +}`, + contractID, workspaceID1, nameWorkspace1, descriptionWorkspace1, startTime) + +var getResponseStruct = workspaces.Workspace{ + ContractID: contractID, + WorkspaceID: workspaceID1, + WorkspaceName: nameWorkspace1, + Description: descriptionWorkspace1, + StartTime: workspaceStartTime, + Regions: []workspaces.Region{ + { + RegionName: "jp1", + TenantID: "9a76dca6d8cd4391aac6f2ea052f10f4", + }, + { + RegionName: "jp2", + TenantID: "27a58d42769141ff8e94920a99aeb44b", + }, + }, + Users: []workspaces.User{ + { + UserID: "ecid000000001", + ContractID: "econ0000000001", + ContractOwner: true, + }, + { + UserID: "ecid000000002", + ContractID: "econ0000000002", + ContractOwner: false, + }, + }, +} + +var createRequest = ` +{ + "workspace_name": "sample_workspace", + "description": "sample workspace", + "contract_id": "econ0000000001" +} +` + +var createResponse = fmt.Sprintf(` +{ + "workspace_id": "%s", + "workspace_name": "sample_workspace", + "description": "sample workspace", + "contract_id": "%s" +} +`, workspaceID1, contractID) + +var createdWorkspace = workspaces.Workspace{ + ContractID: contractID, + WorkspaceID: workspaceID1, + WorkspaceName: "sample_workspace", + Description: "sample workspace", +} + +var updateRequest = ` +{ + "description": "updated workspace" +} +` diff --git a/v4/ecl/sss/v2/workspaces/testing/requests_test.go b/v4/ecl/sss/v2/workspaces/testing/requests_test.go new file mode 100644 index 0000000..594959e --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/testing/requests_test.go @@ -0,0 +1,139 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/sss/v2/workspaces" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListWorkspace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/workspaces", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + count := 0 + err := workspaces.List(fakeclient.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := workspaces.ExtractWorkspaces(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expectedWorkspacesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListWorkspaceAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/workspaces", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listResponse) + }) + + allPages, err := workspaces.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := workspaces.ExtractWorkspaces(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGetWorkspace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/workspaces/%s", workspaceID1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, getResponse) + }) + + actual, err := workspaces.Get(fakeclient.ServiceClient(), workspaceID1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &getResponseStruct, actual) +} + +func TestCreateWorkspace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/workspaces", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, createRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, createResponse) + }) + + createOpts := workspaces.CreateOpts{ + WorkspaceName: "sample_workspace", + Description: "sample workspace", + ContractID: "econ0000000001", + } + + actual, err := workspaces.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createdWorkspace, actual) +} + +func TestUpdateWorkspace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/workspaces/%s", workspaceID1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, updateRequest) + + w.WriteHeader(http.StatusNoContent) + }) + + description := "updated workspace" + + updateOpts := workspaces.UpdateOpts{ + Description: &description, + } + + res := workspaces.Update(fakeclient.ServiceClient(), workspaceID1, updateOpts) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteWorkspace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/workspaces/%s", workspaceID1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := workspaces.Delete(fakeclient.ServiceClient(), workspaceID1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/sss/v2/workspaces/urls.go b/v4/ecl/sss/v2/workspaces/urls.go new file mode 100644 index 0000000..42857f5 --- /dev/null +++ b/v4/ecl/sss/v2/workspaces/urls.go @@ -0,0 +1,23 @@ +package workspaces + +import "github.com/nttcom/eclcloud/v4" + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("workspaces") +} + +func getURL(client *eclcloud.ServiceClient, workspaceID string) string { + return client.ServiceURL("workspaces", workspaceID) +} + +func createURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("workspaces") +} + +func deleteURL(client *eclcloud.ServiceClient, workspaceID string) string { + return client.ServiceURL("workspaces", workspaceID) +} + +func updateURL(client *eclcloud.ServiceClient, workspaceID string) string { + return client.ServiceURL("workspaces", workspaceID) +} diff --git a/v4/ecl/storage/v1/virtualstorages/doc.go b/v4/ecl/storage/v1/virtualstorages/doc.go new file mode 100644 index 0000000..0f6dd42 --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/doc.go @@ -0,0 +1,5 @@ +// Package virtualstorages provides information and interaction with virtualstorage in the +// Enterprise Cloud 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 virtualstorages diff --git a/v4/ecl/storage/v1/virtualstorages/requests.go b/v4/ecl/storage/v1/virtualstorages/requests.go new file mode 100644 index 0000000..e83f80a --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/requests.go @@ -0,0 +1,179 @@ +package virtualstorages + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVirtualStorageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a VirtualStorage. This object is passed to +// the virtualstorages.Create function. For more information about these parameters, +// see the VirtualStorage object. +type CreateOpts struct { + // The virtual storage name + Name string `json:"name" required:"true"` + // The virtual storage description + Description string `json:"description,omitempty"` + // The network_id to connect virtual storage + NetworkID string `json:"network_id" required:"true"` + // The subnet_id to connect virtual storage + SubnetID string `json:"subnet_id" required:"true"` + // The virtual storage volume_type_id + VolumeTypeID string `json:"volume_type_id" required:"true"` + // The ip address pool of virtual storage + IPAddrPool IPAddressPool `json:"ip_addr_pool" required:"true"` + // The virtual storage host_routes + HostRoutes []HostRoute `json:"host_routes,omitempty"` +} + +// ToVirtualStorageCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVirtualStorageCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_storage") +} + +// Create will create a new VirtualStorage based on the values in CreateOpts. +// To extract the VirtualStorage object from the response, call the Extract method on the +// CreateResult. +func Create(client *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVirtualStorageCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing VirtualStorage with the provided ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete( + deleteURL(client, id), + &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get retrieves the VirtualStorage with the provided ID. +// To extract the VirtualStorage object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVirtualStorageListQuery() (string, error) +} + +// ListOpts holds options for listing VirtualStorages. +// It is passed to the virtualstorages.List function. +type ListOpts struct { + // Now there are no definiton as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToVirtualStorageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVirtualStorageListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns VirtualStorage optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVirtualStorageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VirtualStoragePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVirtualStorageUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing VirtualStorage. +// This object is passed to the virtual_storage.Update function. +// For more information about the parameters, see the VirtualStorage object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + IPAddrPool *IPAddressPool `json:"ip_addr_pool,omitempty"` + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` +} + +// ToVirtualStorageUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVirtualStorageUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_storage") +} + +// Update will update the VirtualStorage with provided information. +// To extract the updated VirtualStorage from the response, +// call the Extract method on the UpdateResult. +func Update(client *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVirtualStorageUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// IDFromName is a convenience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + // Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVirtualStorages(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "virtual_storage"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_storage"} + } +} diff --git a/v4/ecl/storage/v1/virtualstorages/results.go b/v4/ecl/storage/v1/virtualstorages/results.go new file mode 100644 index 0000000..7e432b1 --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/results.go @@ -0,0 +1,130 @@ +package virtualstorages + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// IPAddressPool is struct which corresponds to ip_addr_pool object. +type IPAddressPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute is struct which corresponds to host_routes object. +type HostRoute struct { + Destination string `json:"destination"` + Nexthop string `json:"nexthop"` +} + +// VirtualStorage contains all the information associated with a Virtual Storage. +type VirtualStorage struct { + // API error in virtual storage creation. + APIErrorMessage string `json:"api_error_message"` + // Unique identifier for the virtual storage. + ID string `json:"id"` + // network_id which this virtual storage is connected. + NetworkID string `json:"network_id"` + // subnet_id which this virtual storage is connected. + SubnetID string `json:"subnet_id"` + // ip_address_pool object for virtual storage. + IPAddrPool IPAddressPool `json:"ip_addr_pool"` + // List of host routes of virtual storage. + HostRoutes []HostRoute `json:"host_routes"` + // volume_type_id of virtual storage + VolumeTypeID string `json:"volume_type_id"` + // Human-readable display name for the virtual storage. + Name string `json:"name"` + // Human-readable description for the virtual storage. + Description string `json:"description"` + // Current status of the virtual storage. + Status string `json:"status"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Error in virtual storage creation. + ErrorMessage string `json:"error_message"` +} + +// UnmarshalJSON creates JSON format of virtual storage +func (r *VirtualStorage) UnmarshalJSON(b []byte) error { + type tmp VirtualStorage + var s struct { + tmp + CreatedAt eclcloud.JSONISO8601 `json:"created_at"` + UpdatedAt eclcloud.JSONISO8601 `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VirtualStorage(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VirtualStoragePage is a pagination.pager that is returned from a call to the List function. +type VirtualStoragePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no VirtualStorages. +func (r VirtualStoragePage) IsEmpty() (bool, error) { + vss, err := ExtractVirtualStorages(r) + return len(vss) == 0, err +} + +// ExtractVirtualStorages extracts and returns VirtualStorages. +// It is used while iterating over a virtualstorages.List call. +func ExtractVirtualStorages(r pagination.Page) ([]VirtualStorage, error) { + var s []VirtualStorage + err := ExtractVirtualStoragesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the VirtualStorage object out of the commonResult object. +func (r commonResult) Extract() (*VirtualStorage, error) { + var s VirtualStorage + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "virtual_storage") +} + +// ExtractVirtualStoragesInto is information expander for virtual storage +func ExtractVirtualStoragesInto(r pagination.Page, v interface{}) error { + return r.(VirtualStoragePage).Result.ExtractIntoSlicePtr(v, "virtual_storages") +} + +// 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 +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/storage/v1/virtualstorages/testing/doc.go b/v4/ecl/storage/v1/virtualstorages/testing/doc.go new file mode 100644 index 0000000..2b09490 --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains virtual storage unit tests +package testing diff --git a/v4/ecl/storage/v1/virtualstorages/testing/fixtures.go b/v4/ecl/storage/v1/virtualstorages/testing/fixtures.go new file mode 100644 index 0000000..93e06a8 --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/testing/fixtures.go @@ -0,0 +1,435 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/storage/v1/virtualstorages" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idVirtualStorage1 = "fb3efc23-ca8c-4eb5-b7f6-6fc66ff24f9c" +const idVirtualStorage2 = "3535de20-192d-4f5a-a74a-cd1a9c1bf747" + +const idVolumeType = "4f4971a5-899d-42b4-8442-24f17eac9683" + +const nameVirtualStorage1 = "virtual_storage_name_1" +const descriptionVirtualStorage1 = "virtual_storage_description_1" + +const nameVirtualStorage1Update = "virtual_storage_name_1-update" +const descriptionVirtualStorage1Update = "virtual_storage_description_1-update" + +const tenantID = "2d5b878c-147a-4d7c-87fd-90a8be9d255f" + +const networkID = "511f266e-a8bf-4547-ab2a-fc4d2bda9f81" +const subnetID = "9f3fd369-e4d4-4c3a-84f1-9c5ba7686297" + +const storageTime = "2015-05-17T18:14:34+0000" + +const hostRoute1Destination = "0.0.0.0/0" +const hostRoute1Nexthop = "123.123.123.1" +const hostRoute2Destination = "192.168.0.0/24" +const hostRoute2Nexthop = "123.123.123.1" +const hostRoute3Destination = "192.168.1.0/24" +const hostRoute3Nexthop = "123.123.123.1" + +const ipAddrPoolStart = "192.168.1.10" +const ipAddrPoolEnd = "192.168.1.20" + +const ipAddrPoolStartUpdate = "192.168.1.9" +const ipAddrPoolEndUpdate = "192.168.1.21" + +// ListResponse is mocked response of virtualstorages.List +var ListResponse = fmt.Sprintf(` +{ + "virtual_storages": [ + { + "id" : "%s", + "volume_type_id" : "%s", + "name" : "%s", + "description" : "%s", + "tenant_id" : "%s", + "network_id" : "%s", + "subnet_id" : "%s", + "ip_addr_pool" : { + "start" : "%s", + "end" : "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination":"%s", + "nexthop": "%s" + }], + "status" : "available", + "links": [{ + "href": "http://storage.sdp.url:port/v1.0/virtual_storages/440cf918-3ee0-4143-b289-f63e1d2000e6", + "rel": "self" + }], + "created_at" : "%s", + "updated_at" : "%s" + }, + { + "id" : "%s", + "volume_type_id" : "%s", + "name" : "virtual_storage_name_2", + "description" : "virtual_storage_description_2", + "tenant_id" : "%s", + "network_id" : "%s", + "subnet_id" : "%s", + "ip_addr_pool" : { + "start" : "%s", + "end" : "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination":"%s", + "nexthop": "%s" + }], + "status": "available", + "links": [{ + "href": "http://storage.sdp.url:port/v1.0/virtual_storages/440cf918-3ee0-4143-b289-f63e1d2000e6", + "rel": "self" + }], + "created_at" : "%s", + "updated_at" : "%s" + } + ] +}`, + // for virtual storage 1 + idVirtualStorage1, + idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + tenantID, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + storageTime, + storageTime, + // for virtual storage 2 + idVirtualStorage1, + idVolumeType, + tenantID, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + storageTime, + storageTime) + +// GetResponse is mocked format of virtualstorages.Get +var GetResponse = fmt.Sprintf(` +{ + "virtual_storage": { + "id": "%s", + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }], + "status": "available", + "created_at": "%s", + "updated_at" : "%s", + "error_message": "" + } +}`, idVirtualStorage1, + idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + storageTime, + storageTime) + +// CreateRequest is mocked request for virtualstorages.Create +var CreateRequest = fmt.Sprintf(` +{ + "virtual_storage": { + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }] + } +}`, idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, +) + +// CreateResponse is mocked response of virtualstorages.Create +var CreateResponse = fmt.Sprintf(` +{ + "virtual_storage": { + "id": "%s", + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }], + "status": "creating", + "created_at": "null", + "error_message": "" + } +}`, idVirtualStorage1, + idVolumeType, + nameVirtualStorage1, + descriptionVirtualStorage1, + networkID, + subnetID, + ipAddrPoolStart, + ipAddrPoolEnd, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, +) + +// UpdateRequest is mocked request of virtualstorages.Update +var UpdateRequest = fmt.Sprintf(` +{ + "virtual_storage": { + "name": "%s", + "description": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }] + } +}`, nameVirtualStorage1Update, + descriptionVirtualStorage1Update, + ipAddrPoolStartUpdate, + ipAddrPoolEndUpdate, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + hostRoute3Destination, + hostRoute3Nexthop, +) + +// UpdateResponse is mocked response of virtualstorages.Update +var UpdateResponse = fmt.Sprintf(` +{ + "virtual_storage": { + "id": "%s", + "volume_type_id": "%s", + "name": "%s", + "description": "%s", + "network_id": "%s", + "subnet_id": "%s", + "ip_addr_pool": { + "start": "%s", + "end": "%s" + }, + "host_routes":[{ + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }, + { + "destination": "%s", + "nexthop": "%s" + }], + "status": "available", + "created_at": "%s", + "updated_at" : "%s", + "error_message": "" + } +}`, idVirtualStorage1, + idVolumeType, + nameVirtualStorage1Update, + descriptionVirtualStorage1Update, + networkID, + subnetID, + ipAddrPoolStartUpdate, + ipAddrPoolEndUpdate, + hostRoute1Destination, + hostRoute1Nexthop, + hostRoute2Destination, + hostRoute2Nexthop, + hostRoute3Destination, + hostRoute3Nexthop, + storageTime, + storageTime) + +func getExpectedVirtualStoragesSlice() []virtualstorages.VirtualStorage { + storageParsedTime, _ := time.Parse(eclcloud.ISO8601, storageTime) + + var virtualStorage1 = virtualstorages.VirtualStorage{ + ID: idVirtualStorage1, + VolumeTypeID: idVolumeType, + Name: nameVirtualStorage1, + Description: descriptionVirtualStorage1, + NetworkID: networkID, + SubnetID: subnetID, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + Status: "available", + } + + var virtualStorage2 = virtualstorages.VirtualStorage{ + ID: idVirtualStorage1, + VolumeTypeID: idVolumeType, + Name: "virtual_storage_name_2", + Description: "virtual_storage_description_2", + NetworkID: networkID, + SubnetID: subnetID, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + Status: "available", + } + + // ExpectedVirtualStoragesSlice is expected assertion target + ExpectedVirtualStoragesSlice := []virtualstorages.VirtualStorage{ + virtualStorage1, + virtualStorage2, + } + + return ExpectedVirtualStoragesSlice +} + +func getHostRoutes(isUpdate bool) []virtualstorages.HostRoute { + hostRoutes := []virtualstorages.HostRoute{ + { + Destination: hostRoute1Destination, + Nexthop: hostRoute1Nexthop, + }, + { + Destination: hostRoute2Destination, + Nexthop: hostRoute2Nexthop, + }, + } + + if isUpdate { + hostRoutes = append( + hostRoutes, + virtualstorages.HostRoute{ + Destination: hostRoute3Destination, + Nexthop: hostRoute3Nexthop, + }, + ) + } + + return hostRoutes +} + +func getIPAddrPool(isUpdate bool) virtualstorages.IPAddressPool { + var ipAddrPool virtualstorages.IPAddressPool + + if isUpdate { + ipAddrPool = virtualstorages.IPAddressPool{ + Start: ipAddrPoolStartUpdate, + End: ipAddrPoolEndUpdate, + } + return ipAddrPool + } + + ipAddrPool = virtualstorages.IPAddressPool{ + Start: ipAddrPoolStart, + End: ipAddrPoolEnd, + } + return ipAddrPool +} + +func getExpectedCreateVirtualStorage() virtualstorages.VirtualStorage { + + result := virtualstorages.VirtualStorage{ + ID: idVirtualStorage1, + VolumeTypeID: idVolumeType, + Name: nameVirtualStorage1, + Description: descriptionVirtualStorage1, + NetworkID: networkID, + SubnetID: subnetID, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + Status: "creating", + ErrorMessage: "", + } + return result +} diff --git a/v4/ecl/storage/v1/virtualstorages/testing/requests_test.go b/v4/ecl/storage/v1/virtualstorages/testing/requests_test.go new file mode 100644 index 0000000..5b10375 --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/testing/requests_test.go @@ -0,0 +1,167 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/storage/v1/virtualstorages" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/virtual_storages/detail", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fakeclient.ServiceClient() + count := 0 + + virtualstorages.List(client, virtualstorages.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := virtualstorages.ExtractVirtualStorages(page) + if err != nil { + t.Errorf("Failed to extract virtual storages: %v", err) + return false, err + } + + th.CheckDeepEquals(t, getExpectedVirtualStoragesSlice(), actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } + +} + +func TestGetVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/virtual_storages/%s", idVirtualStorage1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + vsActual, err := virtualstorages.Get( + fakeclient.ServiceClient(), idVirtualStorage1).Extract() + th.AssertNoErr(t, err) + vsExpected := getExpectedVirtualStoragesSlice()[0] + th.CheckDeepEquals(t, &vsExpected, vsActual) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/virtual_storages", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) // 202 + + fmt.Fprintf(w, CreateResponse) + }) + + createOpts := virtualstorages.CreateOpts{ + VolumeTypeID: idVolumeType, + Name: nameVirtualStorage1, + Description: descriptionVirtualStorage1, + NetworkID: networkID, + SubnetID: subnetID, + IPAddrPool: getIPAddrPool(false), + HostRoutes: getHostRoutes(false), + } + vsActual, err := virtualstorages.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, vsActual.Status, "creating") + + vsExpected := getExpectedCreateVirtualStorage() + th.AssertDeepEquals(t, &vsExpected, vsActual) +} + +func TestUpdateVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/virtual_storages/%s", idVirtualStorage1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameVirtualStorage1Update + description := descriptionVirtualStorage1Update + ipAddrPool := getIPAddrPool(true) + hostRoutes := getHostRoutes(true) + + updateOpts := virtualstorages.UpdateOpts{ + Name: &name, + Description: &description, + IPAddrPool: &ipAddrPool, + HostRoutes: &hostRoutes, + } + vsActual, err := virtualstorages.Update( + fakeclient.ServiceClient(), idVirtualStorage1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, vsActual.Name, nameVirtualStorage1Update) + th.AssertEquals(t, vsActual.Description, descriptionVirtualStorage1Update) + th.AssertEquals(t, vsActual.ID, idVirtualStorage1) + + th.AssertEquals(t, vsActual.IPAddrPool.Start, ipAddrPoolStartUpdate) + th.AssertEquals(t, vsActual.IPAddrPool.End, ipAddrPoolEndUpdate) + + th.AssertEquals(t, vsActual.HostRoutes[2].Destination, hostRoute3Destination) + th.AssertEquals(t, vsActual.HostRoutes[2].Nexthop, hostRoute3Nexthop) +} + +func TestDeleteVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/virtual_storages/%s", idVirtualStorage1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusOK) + }) + + res := virtualstorages.Delete(fakeclient.ServiceClient(), idVirtualStorage1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/storage/v1/virtualstorages/urls.go b/v4/ecl/storage/v1/virtualstorages/urls.go new file mode 100644 index 0000000..24057bc --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/urls.go @@ -0,0 +1,23 @@ +package virtualstorages + +import "github.com/nttcom/eclcloud/v4" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_storages") +} + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_storages", "detail") +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("virtual_storages", id) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/v4/ecl/storage/v1/virtualstorages/util.go b/v4/ecl/storage/v1/virtualstorages/util.go new file mode 100644 index 0000000..9aebc0e --- /dev/null +++ b/v4/ecl/storage/v1/virtualstorages/util.go @@ -0,0 +1,22 @@ +package virtualstorages + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v4/ecl/storage/v1/volumes/doc.go b/v4/ecl/storage/v1/volumes/doc.go new file mode 100644 index 0000000..e75c49c --- /dev/null +++ b/v4/ecl/storage/v1/volumes/doc.go @@ -0,0 +1,3 @@ +// Package volume provides information and interaction with volume in the +// Storage service. A volume is a detachable block storage device. +package volumes diff --git a/v4/ecl/storage/v1/volumes/requests.go b/v4/ecl/storage/v1/volumes/requests.go new file mode 100644 index 0000000..3d73e9e --- /dev/null +++ b/v4/ecl/storage/v1/volumes/requests.go @@ -0,0 +1,187 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v4" + // "github.com/nttcom/eclcloud/v4/ecl/storage/v1/virtualstorages" + // "github.com/nttcom/eclcloud/v4/ecl/storage/v1/volumetypes" + "github.com/nttcom/eclcloud/v4/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 { + // The volume name + Name string `json:"name" required:"true"` + // The volume description + Description string `json:"description,omitempty"` + // The volume size + Size int `json:"size" required:"true"` + // The volume IOPS as IOPS/GB + IOPSPerGB string `json:"iops_per_gb,omitempty"` + // The volume Throughput + Throughput string `json:"throughput,omitempty"` + // The initiator_iqns for volume (in case ISCSI) + InitiatorIQNs []string `json:"initiator_iqns,omitempty"` + // The availability zone of volume + AvailabilityZone string `json:"availability_zone,omitempty"` + // The parent virtual storage ID to connect volume + VirtualStorageID string `json:"virtual_storage_id" required:"true"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete( + deleteURL(client, id), + &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// 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 *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// 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 { + // Now there are no definitions as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volume optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.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 + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// 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 volume.Update function. +// For more information about the parameters, see the Volume object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + InitiatorIQNs *[]string `json:"initiator_iqns,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +// Volume of Storage SDP only allows to send "initiator_iqns" when +// the service type is "File Storage" type +// So in "ToVolumeUpdateMap" function, check volume type of virtual storage +// related to volume first +// And if service type is "File Storage's one", add initiator_iqns as request parameter +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "volume") +} + +// 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 *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// IDFromName is a convenience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + // Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/v4/ecl/storage/v1/volumes/results.go b/v4/ecl/storage/v1/volumes/results.go new file mode 100644 index 0000000..3044e86 --- /dev/null +++ b/v4/ecl/storage/v1/volumes/results.go @@ -0,0 +1,130 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Volume contains all the information associated with a Volume. +type Volume struct { + // API error in volume creation. + APIErrorMessage string `json:"api_error_message"` + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The volume size + Size int `json:"size"` + // The volume IOPS GB + IOPSPerGB string `json:"iops_per_gb"` + // The volume Throughput + Throughput string `json:"throughput"` + // The initiator_iqns for volume (in case ISCSI) + InitiatorIQNs []string `json:"initiator_iqns"` + // Relevant snapshot's IDs of this volume + SnapshotIDs []string `json:"snapshot_ids"` + // IP Addresses to connect this volume as target device. + TargetIPs []string `json:"target_ips"` + // The metadata of volume + Metadata map[string]string `json:"metadata"` + // The parent virtual storage ID to connect volume + VirtualStorageID string `json:"virtual_storage_id"` + // The availability zone of volume + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Export rule of the volum + ExportRules []string `json:"export_rules"` + // Reservation parcentage about snapshot reservation capacity of the volume + PercentSnapshotReserveUsed int `json:"percent_snapshot_reserve_used"` + // Error in volume creation. + ErrorMessage string `json:"error_message"` +} + +// UnmarshalJSON creates JSON format of volume +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt eclcloud.JSONISO8601 `json:"created_at"` + UpdatedAt eclcloud.JSONISO8601 `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + vss, err := ExtractVolumes(r) + return len(vss) == 0, err +} + +// ExtractVolumes extracts and returns Volumes. +// It is used while iterating over a Volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +// ExtractVolumesInto is information expander for volume +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// 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 +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + eclcloud.ErrResult +} diff --git a/v4/ecl/storage/v1/volumes/testing/doc.go b/v4/ecl/storage/v1/volumes/testing/doc.go new file mode 100644 index 0000000..d083d20 --- /dev/null +++ b/v4/ecl/storage/v1/volumes/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains volume unit tests +package testing diff --git a/v4/ecl/storage/v1/volumes/testing/fixtures.go b/v4/ecl/storage/v1/volumes/testing/fixtures.go new file mode 100644 index 0000000..1d818be --- /dev/null +++ b/v4/ecl/storage/v1/volumes/testing/fixtures.go @@ -0,0 +1,371 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/storage/v1/volumes" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idVolume1 = "fb3efc23-ca8c-4eb5-b7f6-6fc66ff24f9c" +const idVolume2 = "3535de20-192d-4f5a-a74a-cd1a9c1bf747" + +const idVirtualStorage = "4f4971a5-899d-42b4-8442-24f17eac9683" + +const nameVolume1 = "virtual_storage_name_1" +const descriptionVolume1 = "virtual_storage_description_1" + +const nameVolume1Update = "virtual_storage_name_1-update" +const descriptionVolume1Update = "virtual_storage_description_1-update" + +const storageTime = "2015-05-17T18:14:34+0000" + +const idVolumeType = "3f4971a5-899d-42b4-8442-24f17eac9684" + +const IQN1 = "iqn.1986-03.com.nttcom:iscsihost.0" +const IQN2 = "iqn.1986-03.com.nttcom:iscsihost.1" + +// ListResponse is mocked response of volumes.List +var ListResponse = fmt.Sprintf(` +{ + "volumes": [ + { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "available" + }, + { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "virtual_storage_name_2", + "description": "virtual_storage_description_2", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "available" + } + ] +}`, + // for volume 1 + idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, + storageTime, + storageTime, + // for volume 2 + idVolume2, + idVirtualStorage, + IQN1, + storageTime, + storageTime, +) + +// GetResponse is mocked format of volumes.Get +var GetResponse = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "available" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, + storageTime, + storageTime, +) + +// CreateRequestBlock is mocked request for volumes.Create +var CreateRequestBlock = fmt.Sprintf(` +{ + "volume": { + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "availability_zone": "zone1_groupa" + } +}`, idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, +) + +// CreateResponseBlock is mocked response of volumes.Create +var CreateResponseBlock = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "null", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "creating" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, + IQN1, +) + +// CreateRequestFile is mocked request for volumes.Create +var CreateRequestFile = fmt.Sprintf(` +{ + "volume": { + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 256, + "throughput": "50", + "availability_zone": "zone1_groupa" + } +}`, idVirtualStorage, + nameVolume1, + descriptionVolume1, +) + +// CreateResponseFile is mocked response of volumes.Create +var CreateResponseFile = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 256, + "throughput": "50", + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "null", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "creating" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1, + descriptionVolume1, +) + +// UpdateRequest is mocked request of volumes.Update +var UpdateRequest = fmt.Sprintf(` +{ + "volume": { + "name": "%s", + "description": "%s", + "initiator_iqns": [ + "%s", + "%s" + ] + } +}`, nameVolume1Update, + descriptionVolume1Update, + IQN1, + IQN2, +) + +// UpdateResponse is mocked response of volumes.Update +var UpdateResponse = fmt.Sprintf(` +{ + "volume": { + "id" : "%s", + "virtual_storage_id": "%s", + "name" : "%s", + "description": "%s", + "size": 100, + "iops_per_gb": "2", + "initiator_iqns": [ + "%s", + "%s" + ], + "snapshot_ids": [], + "availability_zone": "zone1_groupa", + "created_at": "%s", + "updated_at": "%s", + "links": [ + { + "href": "http://storage.sdp.url:port/v1.0/0c2eba2c5af04d3f9e9d0d410b371fde/volumes/13fea5a0-a36f-43e8-92ef-1cf472725dbe", + "rel": "self" + } + ], + "metadata": {"lun_id": "1"}, + "error_message": "", + "status": "updating" + } +}`, idVolume1, + idVirtualStorage, + nameVolume1Update, + descriptionVolume1Update, + IQN1, + IQN2, + storageTime, + storageTime, +) + +func getExpectedVolumesSlice() []volumes.Volume { + storageParsedTime, _ := time.Parse(eclcloud.ISO8601, storageTime) + + var volume1 = volumes.Volume{ + ID: idVolume1, + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + AvailabilityZone: "zone1_groupa", + Status: "available", + ErrorMessage: "", + } + + var volume2 = volumes.Volume{ + ID: idVolume2, + VirtualStorageID: idVirtualStorage, + Name: "virtual_storage_name_2", + Description: "virtual_storage_description_2", + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + CreatedAt: storageParsedTime, + UpdatedAt: storageParsedTime, + AvailabilityZone: "zone1_groupa", + Status: "available", + ErrorMessage: "", + } + + // ExpectedVolumesSlice is expected assertion target + ExpectedVolumesSlice := []volumes.Volume{ + volume1, + volume2, + } + + return ExpectedVolumesSlice +} + +func getExpectedCreateBlockStorageTypeVolume() volumes.Volume { + + result := volumes.Volume{ + ID: idVolume1, + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + AvailabilityZone: "zone1_groupa", + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + Status: "creating", + ErrorMessage: "", + } + return result +} + +func getExpectedCreateFileStorageTypeVolume() volumes.Volume { + + result := volumes.Volume{ + ID: idVolume1, + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 256, + Throughput: "50", + AvailabilityZone: "zone1_groupa", + SnapshotIDs: []string{}, + Metadata: map[string]string{"lun_id": "1"}, + Status: "creating", + ErrorMessage: "", + } + return result +} diff --git a/v4/ecl/storage/v1/volumes/testing/requests_test.go b/v4/ecl/storage/v1/volumes/testing/requests_test.go new file mode 100644 index 0000000..e3b3375 --- /dev/null +++ b/v4/ecl/storage/v1/volumes/testing/requests_test.go @@ -0,0 +1,199 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/storage/v1/volumes" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/volumes/detail", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fakeclient.ServiceClient() + count := 0 + + volumes.List(client, volumes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumes.ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + th.CheckDeepEquals(t, getExpectedVolumesSlice(), actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } + +} + +func TestGetVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + volActual, err := volumes.Get( + fakeclient.ServiceClient(), idVolume1).Extract() + th.AssertNoErr(t, err) + volExpected := getExpectedVolumesSlice()[0] + th.CheckDeepEquals(t, &volExpected, volActual) +} + +func TestCreateBlockStorageTypeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequestBlock) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) // 202 + + fmt.Fprintf(w, CreateResponseBlock) + }) + + createOpts := volumes.CreateOpts{ + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 100, + IOPSPerGB: "2", + InitiatorIQNs: []string{IQN1}, + AvailabilityZone: "zone1_groupa", + } + volActual, err := volumes.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, volActual.Status, "creating") + + volExpected := getExpectedCreateBlockStorageTypeVolume() + th.AssertDeepEquals(t, &volExpected, volActual) +} + +func TestCreateFileStorageTypeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequestFile) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) // 202 + + fmt.Fprintf(w, CreateResponseFile) + }) + + createOpts := volumes.CreateOpts{ + VirtualStorageID: idVirtualStorage, + Name: nameVolume1, + Description: descriptionVolume1, + Size: 256, + Throughput: "50", + AvailabilityZone: "zone1_groupa", + } + volActual, err := volumes.Create(fakeclient.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, volActual.Status, "creating") + + volExpected := getExpectedCreateFileStorageTypeVolume() + th.AssertDeepEquals(t, &volExpected, volActual) +} + +// TestUpdateBlockStorageTypeVolume covers file storage type's codes +// So, contrary to creation tests, tests for file storage type updating is not implemented +func TestUpdateBlockStorageTypeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(fmt.Sprintf("/volumes/%s", idVolume1), + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, UpdateResponse) + }) + + name := nameVolume1Update + description := descriptionVolume1Update + initiatorIQNs := []string{IQN1, IQN2} + + updateOpts := volumes.UpdateOpts{ + Name: &name, + Description: &description, + InitiatorIQNs: &initiatorIQNs, + } + + volActual, err := volumes.Update( + fakeclient.ServiceClient(), idVolume1, updateOpts).Extract() + + th.AssertNoErr(t, err) + + th.AssertEquals(t, volActual.Name, nameVolume1Update) + th.AssertEquals(t, volActual.Description, descriptionVolume1Update) + + th.AssertEquals(t, volActual.InitiatorIQNs[0], IQN1) + th.AssertEquals(t, volActual.InitiatorIQNs[1], IQN2) +} + +func TestDeleteVirtualStorage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volumes/%s", idVolume1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + w.WriteHeader(http.StatusOK) + }) + + res := volumes.Delete(fakeclient.ServiceClient(), idVolume1) + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/storage/v1/volumes/urls.go b/v4/ecl/storage/v1/volumes/urls.go new file mode 100644 index 0000000..56d1b8b --- /dev/null +++ b/v4/ecl/storage/v1/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/nttcom/eclcloud/v4" + +func createURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/v4/ecl/storage/v1/volumes/util.go b/v4/ecl/storage/v1/volumes/util.go new file mode 100644 index 0000000..5ea926e --- /dev/null +++ b/v4/ecl/storage/v1/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/nttcom/eclcloud/v4" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *eclcloud.ServiceClient, id, status string, secs int) error { + return eclcloud.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/v4/ecl/storage/v1/volumetypes/doc.go b/v4/ecl/storage/v1/volumetypes/doc.go new file mode 100644 index 0000000..ad96f88 --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/doc.go @@ -0,0 +1 @@ +package volumetypes diff --git a/v4/ecl/storage/v1/volumetypes/requests.go b/v4/ecl/storage/v1/volumetypes/requests.go new file mode 100644 index 0000000..1e9ced0 --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/requests.go @@ -0,0 +1,85 @@ +package volumetypes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// Get retrieves the VolumeType with the provided ID. +// To extract the VolumeType object from the response, +// call the Extract method on the GetResult. +func Get(client *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeTypeListQuery() (string, error) +} + +// ListOpts holds options for listing ToVolumeTypes. +// It is passed to the volumetypes.List function. +type ListOpts struct { + // Now there are no definiton as query params in API specification + // But do not remove this struct in future specification change. +} + +// ToVolumeTypeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeTypeListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns VolumeType optionally limited by the conditions provided in ListOpts. +func List(client *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeTypeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumeTypePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + // Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumeTypes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "volume_type"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_storage"} + } +} diff --git a/v4/ecl/storage/v1/volumetypes/results.go b/v4/ecl/storage/v1/volumetypes/results.go new file mode 100644 index 0000000..0a29344 --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/results.go @@ -0,0 +1,71 @@ +package volumetypes + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ExtraSpec is struct which corresponds to extra_specs object. +type ExtraSpec struct { + AvailableVolumeSize []int `json:"available_volume_size"` + AvailableVolumeThroughput []string `json:"available_volume_throughput"` + AvailableIOPSPerGB []string `json:"available_iops_per_gb"` +} + +// VolumeType contains all the information associated with a Virtual Storage. +type VolumeType struct { + // API error in virtual storage creation. + APIErrorMessage string `json:"api_error_message"` + // Unique identifier for the volume type. + ID string `json:"id"` + // Human-readable display name for the volume type. + Name string `json:"name"` + // Extra specification of volume type. + // This includes available_volume_size, and available_iops_per_gb, + // or available_throughput depending on storage service type. + ExtraSpecs ExtraSpec `json:"extra_specs"` +} + +// VolumeTypePage is a pagination.pager that is returned from a call to the List function. +type VolumeTypePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no VirtualStorages. +func (r VolumeTypePage) IsEmpty() (bool, error) { + vtypes, err := ExtractVolumeTypes(r) + return len(vtypes) == 0, err +} + +// ExtractVolumeTypes extracts and returns VolumeTypes. +// It is used while iterating over a volumetypes.List call. +func ExtractVolumeTypes(r pagination.Page) ([]VolumeType, error) { + var s []VolumeType + err := ExtractVolumeTypesInto(r, &s) + return s, err +} + +type commonResult struct { + eclcloud.Result +} + +// Extract will get the VolumeType object out of the commonResult object. +func (r commonResult) Extract() (*VolumeType, error) { + var s VolumeType + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume_type") +} + +// ExtractVolumeTypesInto is information expander for volume types +func ExtractVolumeTypesInto(r pagination.Page, v interface{}) error { + return r.(VolumeTypePage).Result.ExtractIntoSlicePtr(v, "volume_types") +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} diff --git a/v4/ecl/storage/v1/volumetypes/testing/doc.go b/v4/ecl/storage/v1/volumetypes/testing/doc.go new file mode 100644 index 0000000..fc6042c --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains volume type unit tests +package testing diff --git a/v4/ecl/storage/v1/volumetypes/testing/fixtures.go b/v4/ecl/storage/v1/volumetypes/testing/fixtures.go new file mode 100644 index 0000000..4234e7e --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/testing/fixtures.go @@ -0,0 +1,170 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/storage/v1/volumetypes" +) + +// Define parameters which are used in assertion. +// Additionally, kind of IDs are defined here. +const idVolumeType1 = "6328d234-7939-4d61-9216-736de66d15f9" +const idVolumeType2 = "bf33db2a-d13e-11e5-8949-005056ab5d30" +const idVolumeType3 = "704db6e5-8a93-41a5-850d-405913600341" + +// ListResponse is mocked response of volumetypes.List +var ListResponse = fmt.Sprintf(` +{ + "volume_types": [ + { + "extra_specs": { + "available_volume_size": [ + 100, + 250, + 500, + 1000, + 2000, + 4000, + 8000, + 12000 + ], + "available_iops_per_gb": [ + "2", + "4" + ] + }, + "id": "%s", + "name": "piops_iscsi_na" + }, + { + "extra_specs": { + "available_volume_size": [ + 256, + 512 + ], + "available_volume_throughput": [ + "50", + "100", + "250", + "400" + ] + }, + "id": "%s", + "name": "pre_nfs_na" + }, + { + "extra_specs": { + "available_volume_size": [ + 1024, + 2048, + 3072, + 4096, + 5120, + 10240, + 15360, + 20480, + 25600, + 30720, + 35840, + 40960, + 46080, + 51200, + 56320, + 61440, + 66560, + 71680, + 76800, + 81920, + 87040, + 92160, + 97280, + 102400 + ] + }, + "id": "%s", + "name": "standard_nfs_na" + } + ] +}`, + idVolumeType1, + idVolumeType2, + idVolumeType3, +) + +// GetResponse is mocked format of volumetypes.Get +var GetResponse = fmt.Sprintf(` +{ + "volume_type": { + "extra_specs": { + "available_volume_size": [ + 100, + 250, + 500, + 1000, + 2000, + 4000, + 8000, + 12000 + ], + "available_iops_per_gb": [ + "2", + "4" + ] + }, + "id": "%s", + "name": "piops_iscsi_na" + } +}`, idVolumeType1, +) + +func getExpectedVolumeTypesSlice() []volumetypes.VolumeType { + + // For Block Storage Type + var volumetype1 = volumetypes.VolumeType{ + ID: idVolumeType1, + Name: "piops_iscsi_na", + ExtraSpecs: volumetypes.ExtraSpec{ + AvailableVolumeSize: []int{ + 100, 250, 500, 1000, 2000, 4000, 8000, 12000, + }, + AvailableIOPSPerGB: []string{"2", "4"}, + }, + } + + // For File Storage(Premium) Type + var volumetype2 = volumetypes.VolumeType{ + ID: idVolumeType2, + Name: "pre_nfs_na", + ExtraSpecs: volumetypes.ExtraSpec{ + AvailableVolumeSize: []int{ + 256, 512, + }, + AvailableVolumeThroughput: []string{ + "50", "100", "250", "400", + }, + }, + } + + // For File Storage(Standard) Type + var volumetype3 = volumetypes.VolumeType{ + ID: idVolumeType3, + Name: "standard_nfs_na", + ExtraSpecs: volumetypes.ExtraSpec{ + AvailableVolumeSize: []int{ + 1024, 2048, 3072, 4096, 5120, 10240, + 15360, 20480, 25600, 30720, 35840, 40960, + 46080, 51200, 56320, 61440, 66560, 71680, + 76800, 81920, 87040, 92160, 97280, 102400, + }, + }, + } + + // ExpectedVolumeTypesSlice is expected assertion target + ExpectedVolumeTypesSlice := []volumetypes.VolumeType{ + volumetype1, + volumetype2, + volumetype3, + } + + return ExpectedVolumeTypesSlice +} diff --git a/v4/ecl/storage/v1/volumetypes/testing/requests_test.go b/v4/ecl/storage/v1/volumetypes/testing/requests_test.go new file mode 100644 index 0000000..dea90b9 --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/testing/requests_test.go @@ -0,0 +1,73 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/ecl/storage/v1/volumetypes" + "github.com/nttcom/eclcloud/v4/pagination" + + th "github.com/nttcom/eclcloud/v4/testhelper" + fakeclient "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestListVolumeType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/volume_types/detail", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fakeclient.ServiceClient() + count := 0 + + volumetypes.List(client, volumetypes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumetypes.ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + th.CheckDeepEquals(t, getExpectedVolumeTypesSlice(), actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } + +} + +func TestGetVolumeType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/volume_types/%s", idVolumeType1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + vtActual, err := volumetypes.Get( + fakeclient.ServiceClient(), idVolumeType1).Extract() + th.AssertNoErr(t, err) + vtExpected := getExpectedVolumeTypesSlice()[0] + th.CheckDeepEquals(t, &vtExpected, vtActual) +} diff --git a/v4/ecl/storage/v1/volumetypes/urls.go b/v4/ecl/storage/v1/volumetypes/urls.go new file mode 100644 index 0000000..260ff10 --- /dev/null +++ b/v4/ecl/storage/v1/volumetypes/urls.go @@ -0,0 +1,13 @@ +package volumetypes + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func getURL(client *eclcloud.ServiceClient, id string) string { + return client.ServiceURL("volume_types", id) +} + +func listURL(client *eclcloud.ServiceClient) string { + return client.ServiceURL("volume_types", "detail") +} diff --git a/v4/ecl/utils/base_endpoint.go b/v4/ecl/utils/base_endpoint.go new file mode 100644 index 0000000..40080f7 --- /dev/null +++ b/v4/ecl/utils/base_endpoint.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", err + } + + u.RawQuery, u.Fragment = "", "" + + path := u.Path + versionRe := regexp.MustCompile("v[0-9.]+/?") + + if version := versionRe.FindString(path); version != "" { + versionIndex := strings.Index(path, version) + u.Path = path[:versionIndex] + } + + return u.String(), nil +} diff --git a/v4/ecl/utils/choose_version.go b/v4/ecl/utils/choose_version.go new file mode 100644 index 0000000..edd0054 --- /dev/null +++ b/v4/ecl/utils/choose_version.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/nttcom/eclcloud/v4" +) + +// 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 *eclcloud.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, &eclcloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + 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) + } + } + + for _, version := range recognized { + if strings.Contains(value.ID, version.ID) { + // 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 version, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || version.Priority > highest.Priority { + highest = version + 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/v4/ecl/vna/v1/appliance_plans/doc.go b/v4/ecl/vna/v1/appliance_plans/doc.go new file mode 100644 index 0000000..2b9d5dd --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/doc.go @@ -0,0 +1,37 @@ +/* +Package appliance_plans contains functionality for working with +ECL Virtual Network Appliance Plan resources. + +Example to List Virtual Network Appliance Plans + + listOpts := appliance_plans.ListOpts{ + Description: "general", + } + + allPages, err := appliance_plans.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allVirtualNetworkAppliancePlans, err := appliance_plans.ExtractVirtualNetworkAppliancePlans(allPages) + if err != nil { + panic(err) + } + + for _, virtualNetworkAppliancePlan := range allVirtualNetworkAppliancePlans { + fmt.Printf("%+v\n", virtualNetworkAppliancePlan) + } + +Example to Show Virtual Network Appliance Plan + + virtualNetworkAppliancePlanID := "37556569-87f2-4699-b5ff-bf38e7cbf8a7" + + virtualNetworkAppliancePlan, err := appliance_plans.Get(networkClient, virtualNetworkAppliancePlanID, nil).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", virtualNetworkAppliancePlan) + +*/ +package appliance_plans diff --git a/v4/ecl/vna/v1/appliance_plans/requests.go b/v4/ecl/vna/v1/appliance_plans/requests.go new file mode 100644 index 0000000..3c85a96 --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/requests.go @@ -0,0 +1,111 @@ +package appliance_plans + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/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 Virtual Network Appliance Plan attributes you want to see returned. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + ApplianceType string `q:"appliance_type"` + Version string `q:"version"` + Flavor string `q:"flavor"` + NumberOfInterfaces int `q:"number_of_interfaces"` + Enabled bool `q:"enabled"` + MaxNumberOfAap int `q:"max_number_of_aap"` + Details bool `q:"details"` + AvailabilityZone string `q:"availability_zone"` + AvailabilityZoneAvailable bool `q:"availability_zone.available"` +} + +// ToVirtualNetworkAppliancePlanListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVirtualNetworkAppliancePlanListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Virtual Network Appliance Plans. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(c) + query, err := opts.ToVirtualNetworkAppliancePlanListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return VirtualNetworkAppliancePlanPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetOptsBuilder allows extensions to add additional parameters to +// the Virtual Network Appliance Plan API request +type GetOptsBuilder interface { + ToProcessQuery() (string, error) +} + +// GetOpts represents result of Virtual Network Appliance Plan API response. +type GetOpts struct { + VirtualNetworkAppliancePlanId string `q:"virtual_network_appliance_plan_id"` + Details bool `q:"details"` +} + +// ToProcessQuery formats a GetOpts into a query string. +func (opts GetOpts) ToProcessQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// Get retrieves a specific Virtual Network Appliance Plan based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string, opts GetOptsBuilder) (r GetResult) { + url := getURL(c, id) + if opts != nil { + query, _ := opts.ToProcessQuery() + url += query + } + _, r.Err = c.Get(url, &r.Body, nil) + return +} + +// IDFromName is a convenience function that returns a Virtual Network Appliance Plan's ID, +// given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVirtualNetworkAppliancePlans(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "virtual_network_appliance_plan"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_network_appliance_plan"} + } +} diff --git a/v4/ecl/vna/v1/appliance_plans/results.go b/v4/ecl/vna/v1/appliance_plans/results.go new file mode 100644 index 0000000..354fa17 --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/results.go @@ -0,0 +1,98 @@ +package appliance_plans + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result and extracts a Virtual Network Appliance Plan resource. +func (r commonResult) Extract() (*VirtualNetworkAppliancePlan, error) { + var s struct { + VirtualNetworkAppliancePlan *VirtualNetworkAppliancePlan `json:"virtual_network_appliance_plan"` + } + err := r.ExtractInto(&s) + return s.VirtualNetworkAppliancePlan, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Virtual Network Appliance Plan. +type GetResult struct { + commonResult +} + +// License of Virtual Network Appliance +type License struct { + LicenseType string `json:"license_type"` +} + +// Availability Zone of Virtual Network Appliance +type AvailabilityZone struct { + AvailabilityZone string `json:"availability_zone"` + Available bool `json:"available"` + Rank int `json:"rank"` +} + +// VirtualNetworkAppliancePlan represents a Virtual Network Appliance Plan. +// See package documentation for a top-level description of what this is. +type VirtualNetworkAppliancePlan struct { + + // UUID representing the Virtual Network Appliance Plan. + ID string `json:"id"` + + // Name of the Virtual Network Appliance Plan. + Name string `json:"name"` + + // Description is description + Description string `json:"description"` + + // Type of appliance + ApplianceType string `json:"appliance_type"` + + // Version name + Version string `json:"version"` + + // Nova flavor + Flavor string `json:"flavor"` + + // Number of Interfaces + NumberOfInterfaces int `json:"number_of_interfaces"` + + // Is user allowed to create new firewalls with this plan. + Enabled bool `json:"enabled"` + + // Max Number of allowed_address_pairs + MaxNumberOfAap int `json:"max_number_of_aap"` + + // Licenses + Licenses []License `json:"licenses"` + + // AvailabilityZones + AvailabilityZones []AvailabilityZone `json:"availability_zones"` +} + +// VirtualNetworkAppliancePlanPage is the page returned by a pager when traversing over a collection +// of virtual network appliance plans. +type VirtualNetworkAppliancePlanPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a VirtualNetworkAppliancePlanPage struct is empty. +func (r VirtualNetworkAppliancePlanPage) IsEmpty() (bool, error) { + is, err := ExtractVirtualNetworkAppliancePlans(r) + return len(is) == 0, err +} + +// ExtractVirtualNetworkAppliancePlans accepts a Page struct, specifically a VirtualNetworkAppliancePlanPage struct, +// and extracts the elements into a slice of Virtual Network Appliance Plan structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVirtualNetworkAppliancePlans(r pagination.Page) ([]VirtualNetworkAppliancePlan, error) { + var s struct { + VirtualNetworkAppliancePlans []VirtualNetworkAppliancePlan `json:"virtual_network_appliance_plans"` + } + err := (r.(VirtualNetworkAppliancePlanPage)).ExtractInto(&s) + return s.VirtualNetworkAppliancePlans, err +} diff --git a/v4/ecl/vna/v1/appliance_plans/testing/doc.go b/v4/ecl/vna/v1/appliance_plans/testing/doc.go new file mode 100644 index 0000000..d17d407 --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/testing/doc.go @@ -0,0 +1,2 @@ +// Virtual Network Appliance Plans unit tests +package testing diff --git a/v4/ecl/vna/v1/appliance_plans/testing/fixtures.go b/v4/ecl/vna/v1/appliance_plans/testing/fixtures.go new file mode 100644 index 0000000..883de32 --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/testing/fixtures.go @@ -0,0 +1,195 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/vna/v1/appliance_plans" +) + +const ListResponse = ` +{ + "virtual_network_appliance_plans": [ + { + "id": "37556569-87f2-4699-b5ff-bf38e7cbf8a7", + "name": "appliance_plans_name", + "description": "appliance_plans_description", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "", + "flavor": "2CPU-8GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + } + ] +} +` +const GetResponse = ` +{ + "virtual_network_appliance_plan": { + "id": "6589b37a-cf82-4918-96fe-255683f78e76", + "name": "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + "description": "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "15.1X49-D100", + "flavor": "VSRX-2CPU-4GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + } +} +` + +var VirtualNetworkAppliancePlan1 = appliance_plans.VirtualNetworkAppliancePlan{ + ID: "37556569-87f2-4699-b5ff-bf38e7cbf8a7", + Name: "appliance_plans_name", + Description: "appliance_plans_description", + ApplianceType: "ECL::VirtualNetworkAppliance::VSRX", + Version: "", + Flavor: "2CPU-8GB", + NumberOfInterfaces: 8, + Enabled: true, + MaxNumberOfAap: 1, + Licenses: []appliance_plans.License{ + { + LicenseType: "STD", + }, + }, + AvailabilityZones: []appliance_plans.AvailabilityZone{ + { + AvailabilityZone: "zone1_groupa", + Available: true, + Rank: 1, + }, + { + AvailabilityZone: "zone1_groupb", + Available: false, + Rank: 2, + }, + }, +} + +var VirtualNetworkApplianceDetail = appliance_plans.VirtualNetworkAppliancePlan{ + ID: "6589b37a-cf82-4918-96fe-255683f78e76", + Name: "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + Description: "vSRX_15.1X49-D100_2CPU_4GB_8IF_STD", + ApplianceType: "ECL::VirtualNetworkAppliance::VSRX", + Version: "15.1X49-D100", + Flavor: "VSRX-2CPU-4GB", + NumberOfInterfaces: 8, + Enabled: true, + MaxNumberOfAap: 1, + Licenses: []appliance_plans.License{ + { + LicenseType: "STD", + }, + }, + AvailabilityZones: []appliance_plans.AvailabilityZone{ + { + AvailabilityZone: "zone1_groupa", + Available: true, + Rank: 1, + }, + { + AvailabilityZone: "zone1_groupb", + Available: false, + Rank: 2, + }, + }, +} + +var ExpectedVirtualNetworkAppliancePlanSlice = []appliance_plans.VirtualNetworkAppliancePlan{VirtualNetworkAppliancePlan1} + +const ListResponseDuplicatedNames = ` +{ + "virtual_network_appliance_plans": [ + { + "id": "37556569-87f2-4699-b5ff-bf38e7cbf8a7", + "name": "appliance_plans_name", + "description": "appliance_plans_description", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "", + "flavor": "2CPU-8GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + }, + { + "id": "6589b37a-cf82-4918-96fe-255683f78e76", + "name": "appliance_plans_name", + "description": "appliance_plans_description", + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "version": "", + "flavor": "2CPU-8GB", + "number_of_interfaces": 8, + "enabled": true, + "max_number_of_aap": 1, + "licenses": [ + { + "license_type": "STD" + } + ], + "availability_zones": [ + { + "availability_zone": "zone1_groupa", + "available": true, + "rank": 1 + }, + { + "availability_zone": "zone1_groupb", + "available": false, + "rank": 2 + } + ] + } + ] +} +` diff --git a/v4/ecl/vna/v1/appliance_plans/testing/request_test.go b/v4/ecl/vna/v1/appliance_plans/testing/request_test.go new file mode 100644 index 0000000..b317709 --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/testing/request_test.go @@ -0,0 +1,147 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/vna/v1/appliance_plans" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" + cli "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +const TokenID = cli.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := cli.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc +} + +func TestListAppliancePlans(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/virtual_network_appliance_plans", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, ListResponse) + }) + + client := ServiceClient() + count := 0 + + appliance_plans.List(client, appliance_plans.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := appliance_plans.ExtractVirtualNetworkAppliancePlans(page) + if err != nil { + t.Errorf("Failed to extract Virtual Network Appliance Plans: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedVirtualNetworkAppliancePlanSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetAppliancePlan(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans/6589b37a-cf82-4918-96fe-255683f78e76", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + s, err := appliance_plans.Get(ServiceClient(), "6589b37a-cf82-4918-96fe-255683f78e76", appliance_plans.GetOpts{}).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &VirtualNetworkApplianceDetail, s) +} + +func TestIDFromName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := ServiceClient() + + expectedID := "37556569-87f2-4699-b5ff-bf38e7cbf8a7" + actualID, err := appliance_plans.IDFromName(client, "appliance_plans_name") + + th.AssertNoErr(t, err) + th.AssertEquals(t, expectedID, actualID) +} + +func TestIDFromNameNoResult(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := ServiceClient() + + _, err := appliance_plans.IDFromName(client, "appliance_plans_nameX") + + if err == nil { + t.Fatalf("Expected error, got none") + } + +} + +func TestIDFromNameDuplicated(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliance_plans", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponseDuplicatedNames) + }) + + client := ServiceClient() + + _, err := appliance_plans.IDFromName(client, "appliance_plans_name") + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/v4/ecl/vna/v1/appliance_plans/urls.go b/v4/ecl/vna/v1/appliance_plans/urls.go new file mode 100644 index 0000000..267dba3 --- /dev/null +++ b/v4/ecl/vna/v1/appliance_plans/urls.go @@ -0,0 +1,19 @@ +package appliance_plans + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("virtual_network_appliance_plans", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_network_appliance_plans") +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/vna/v1/appliances/doc.go b/v4/ecl/vna/v1/appliances/doc.go new file mode 100644 index 0000000..dca0884 --- /dev/null +++ b/v4/ecl/vna/v1/appliances/doc.go @@ -0,0 +1,57 @@ +/* +Package appliances contains functionality for working with +ECL Commnon Function Gateway resources. + +Example to List VirtualNetworkAppliances + + listOpts := virtual_network_appliances.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := virtual_network_appliances.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allVirtualNetworkAppliances, err := virtual_network_appliances.ExtractVirtualNetworkAppliances(allPages) + if err != nil { + panic(err) + } + + for _, virtual_network_appliances := range allVirtualNetworkAppliances { + fmt.Printf("%+v", virtual_network_appliances) + } + +Example to Create a virtual_network_appliances + + createOpts := virtual_network_appliances.CreateOpts{ + Name: "network_1", + } + + virtual_network_appliances, err := virtual_network_appliances.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a virtual_network_appliances + + virtualNetworkApplianceID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := virtual_network_appliances.UpdateOpts{ + Name: "new_name", + } + + virtual_network_appliances, err := virtual_network_appliances.Update(networkClient, virtualNetworkApplianceID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a virtual_network_appliances + + virtualNetworkApplianceID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := virtual_network_appliances.Delete(networkClient, virtualNetworkApplianceID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package appliances diff --git a/v4/ecl/vna/v1/appliances/requests.go b/v4/ecl/vna/v1/appliances/requests.go new file mode 100644 index 0000000..345601d --- /dev/null +++ b/v4/ecl/vna/v1/appliances/requests.go @@ -0,0 +1,326 @@ +package appliances + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToVirtualNetworkApplianceListQuery() (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 virtual network appliance attributes you want to see returned. +type ListOpts struct { + Name string `q:"name"` + ID string `q:"id"` + ApplianceType string `q:"appliance_type"` + Description string `q:"description"` + AvailabilityZone string `q:"availability_zone"` + OSMonitoringStatus string `q:"os_monitoring_status"` + OSLoginStatus string `q:"os_login_status"` + VMStatus string `q:"vm_status"` + OperationStatus string `q:"operation_status"` + VirtualNetworkAppliancePlanID string `q:"virtual_network_appliance_plan_id"` + TenantID string `q:"tenant_id"` +} + +// ToVirtualNetworkApplianceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVirtualNetworkApplianceListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// virtual network appliances. +// It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToVirtualNetworkApplianceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return AppliancePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific virtual network appliance based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToApplianceCreateMap() (map[string]interface{}, error) +} + +/* +Parameters for Create +*/ + +// CreateOptsFixedIP represents fixed ip information in virtual network appliance creation. +type CreateOptsFixedIP struct { + IPAddress string `json:"ip_address" required:"true"` +} + +// CreateOptsInterface represents each parameters in virtual network appliance creation. +type CreateOptsInterface struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + NetworkID string `json:"network_id" required:"true"` + Tags map[string]string `json:"tags,omitempty"` + FixedIPs *[]CreateOptsFixedIP `json:"fixed_ips,omitempty"` +} + +// CreateOptsInterfaces represents 1st interface in virtual network appliance creation. +type CreateOptsInterfaces struct { + Interface1 *CreateOptsInterface `json:"interface_1,omitempty"` + Interface2 *CreateOptsInterface `json:"interface_2,omitempty"` + Interface3 *CreateOptsInterface `json:"interface_3,omitempty"` + Interface4 *CreateOptsInterface `json:"interface_4,omitempty"` + Interface5 *CreateOptsInterface `json:"interface_5,omitempty"` + Interface6 *CreateOptsInterface `json:"interface_6,omitempty"` + Interface7 *CreateOptsInterface `json:"interface_7,omitempty"` + Interface8 *CreateOptsInterface `json:"interface_8,omitempty"` +} + +// CreateOpts represents options used to create a virtual network appliance. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + DefaultGateway string `json:"default_gateway,omitempty"` + AvailabilityZone string `json:"availability_zone,omitempty"` + VirtualNetworkAppliancePlanID string `json:"virtual_network_appliance_plan_id" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Interfaces *CreateOptsInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToApplianceCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +// Create accepts a CreateOpts struct and creates a new virtual network appliance +// using the values provided. +// This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToApplianceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToApplianceUpdateMap() (map[string]interface{}, error) +} + +/* +Update for Allowed Address Pairs +*/ + +// UpdateAllowedAddressPairAddressInfo represents options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairAddressInfo struct { + IPAddress string `json:"ip_address" required:"true"` + MACAddress *string `json:"mac_address" required:"true"` + Type *string `json:"type" required:"true"` + VRID *interface{} `json:"vrid" required:"true"` +} + +// UpdateAllowedAddressPairInterface represents +// allowed address pairs list in update options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairInterface struct { + AllowedAddressPairs *[]UpdateAllowedAddressPairAddressInfo `json:"allowed_address_pairs,omitempty"` +} + +// UpdateAllowedAddressPairInterfaces represents +// interface list of update options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairInterfaces struct { + Interface1 *UpdateAllowedAddressPairInterface `json:"interface_1,omitempty"` + Interface2 *UpdateAllowedAddressPairInterface `json:"interface_2,omitempty"` + Interface3 *UpdateAllowedAddressPairInterface `json:"interface_3,omitempty"` + Interface4 *UpdateAllowedAddressPairInterface `json:"interface_4,omitempty"` + Interface5 *UpdateAllowedAddressPairInterface `json:"interface_5,omitempty"` + Interface6 *UpdateAllowedAddressPairInterface `json:"interface_6,omitempty"` + Interface7 *UpdateAllowedAddressPairInterface `json:"interface_7,omitempty"` + Interface8 *UpdateAllowedAddressPairInterface `json:"interface_8,omitempty"` +} + +// UpdateAllowedAddressPairOpts represents +// parent element of interfaces in update options used to +// update virtual network appliance allowed address pairs. +type UpdateAllowedAddressPairOpts struct { + Interfaces *UpdateAllowedAddressPairInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceUpdateMap builds a request body from UpdateAllowedAddressPairOpts. +func (opts UpdateAllowedAddressPairOpts) ToApplianceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +/* +Update for FixedIP (includes network_id) +*/ + +// UpdateFixedIPAddressInfo represents ip address part +// of virtual network appliance update. +type UpdateFixedIPAddressInfo struct { + IPAddress string `json:"ip_address" required:"true"` +} + +// UpdateFixedIPInterface represents each interface information +// in updating network connection and fixed ip address +// of virtual network appliance. +type UpdateFixedIPInterface struct { + NetworkID *string `json:"network_id,omitempty"` + FixedIPs *[]UpdateFixedIPAddressInfo `json:"fixed_ips,omitempty"` +} + +// UpdateFixedIPInterfaces represents +// interface list of update options used to +// update virtual network appliance network connection and fixed ips. +type UpdateFixedIPInterfaces struct { + Interface1 *UpdateFixedIPInterface `json:"interface_1,omitempty"` + Interface2 *UpdateFixedIPInterface `json:"interface_2,omitempty"` + Interface3 *UpdateFixedIPInterface `json:"interface_3,omitempty"` + Interface4 *UpdateFixedIPInterface `json:"interface_4,omitempty"` + Interface5 *UpdateFixedIPInterface `json:"interface_5,omitempty"` + Interface6 *UpdateFixedIPInterface `json:"interface_6,omitempty"` + Interface7 *UpdateFixedIPInterface `json:"interface_7,omitempty"` + Interface8 *UpdateFixedIPInterface `json:"interface_8,omitempty"` +} + +// UpdateFixedIPOpts represents +// parent element of interfaces in update options used to +// update virtual network appliance network connection and fixed ips. +type UpdateFixedIPOpts struct { + Interfaces *UpdateFixedIPInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceUpdateMap builds a request body from UpdateFixedIPOpts. +func (opts UpdateFixedIPOpts) ToApplianceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +/* +Update for Metadata +*/ + +// UpdateMetadataInterface represents options used to +// update virtual network appliance metadata of interface. +type UpdateMetadataInterface struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// UpdateMetadataInterfaces represents +// list of interfaces for updating virtual network appliance metadata. +type UpdateMetadataInterfaces struct { + Interface1 *UpdateMetadataInterface `json:"interface_1,omitempty"` + Interface2 *UpdateMetadataInterface `json:"interface_2,omitempty"` + Interface3 *UpdateMetadataInterface `json:"interface_3,omitempty"` + Interface4 *UpdateMetadataInterface `json:"interface_4,omitempty"` + Interface5 *UpdateMetadataInterface `json:"interface_5,omitempty"` + Interface6 *UpdateMetadataInterface `json:"interface_6,omitempty"` + Interface7 *UpdateMetadataInterface `json:"interface_7,omitempty"` + Interface8 *UpdateMetadataInterface `json:"interface_8,omitempty"` +} + +// UpdateMetadataOpts represents +// metadata of virtual network appliance itself and +// pararent element for list of interfaces +// which are used by virtual network appliance metadata update. +type UpdateMetadataOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` + Interfaces *UpdateMetadataInterfaces `json:"interfaces,omitempty"` +} + +// ToApplianceUpdateMap builds a request body from UpdateOpts. +func (opts UpdateMetadataOpts) ToApplianceUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "virtual_network_appliance") +} + +/* +Update Common +*/ + +// Update accepts a UpdateOpts struct and updates an existing virtual network appliance +// using the values provided. For more information, see the Create function. +func Update(c *eclcloud.ServiceClient, virtualNetworkApplianceID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToApplianceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Patch(updateURL(c, virtualNetworkApplianceID), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the virtual network appliance associated with it. +func Delete(c *eclcloud.ServiceClient, virtualNetworkApplianceID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, virtualNetworkApplianceID), nil) + return +} + +// IDFromName is a convenience function that returns a virtual network appliance's +// ID, given its name. +func IDFromName(client *eclcloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractAppliances(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", eclcloud.ErrResourceNotFound{Name: name, ResourceType: "virtual_network_appliance"} + case 1: + return id, nil + default: + return "", eclcloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "virtual_network_appliance"} + } +} diff --git a/v4/ecl/vna/v1/appliances/results.go b/v4/ecl/vna/v1/appliances/results.go new file mode 100644 index 0000000..441ec3d --- /dev/null +++ b/v4/ecl/vna/v1/appliances/results.go @@ -0,0 +1,150 @@ +package appliances + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +// Extract is a function that accepts a result +// and extracts a virtual network appliance resource. +func (r commonResult) Extract() (*Appliance, error) { + var vna Appliance + err := r.ExtractInto(&vna) + return &vna, err +} + +// Extract interprets any commonResult as a Virtual Network Appliance, if possible. +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "virtual_network_appliance") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Virtual Network Appliance. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Virtual Network Appliance. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Virtual Network Appliance. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + eclcloud.ErrResult +} + +// FixedIPInResponse represents each element of fixed ips +// of virtual network appliance. +type FixedIPInResponse struct { + IPAddress string `json:"ip_address"` + SubnetID string `json:"subnet_id"` +} + +// AllowedAddressPairInResponse represents each element of +// allowed address pair of virtual network appliance. +type AllowedAddressPairInResponse struct { + IPAddress string `json:"ip_address"` + MACAddress string `json:"mac_address"` + Type string `json:"type"` + VRID interface{} `json:"vrid"` +} + +// InterfaceInResponse works as parent element of +// each interface of virtual network appliance. +type InterfaceInResponse struct { + Name string `json:"name"` + Description string `json:"description"` + NetworkID string `json:"network_id"` + Updatable bool `json:"updatable"` + Tags map[string]string `json:"tags"` + FixedIPs []FixedIPInResponse `json:"fixed_ips"` + AllowedAddressPairs []AllowedAddressPairInResponse `json:"allowed_address_pairs"` +} + +// InterfacesInResponse works as list of interfaces +// of virtual network appliance. +type InterfacesInResponse struct { + Interface1 InterfaceInResponse `json:"interface_1"` + Interface2 InterfaceInResponse `json:"interface_2"` + Interface3 InterfaceInResponse `json:"interface_3"` + Interface4 InterfaceInResponse `json:"interface_4"` + Interface5 InterfaceInResponse `json:"interface_5"` + Interface6 InterfaceInResponse `json:"interface_6"` + Interface7 InterfaceInResponse `json:"interface_7"` + Interface8 InterfaceInResponse `json:"interface_8"` +} + +// Appliance represents, well, a virtual network appliance. +type Appliance struct { + Name string `json:"name"` + ID string `json:"id"` + ApplianceType string `json:"appliance_type"` + Description string `json:"description"` + DefaultGateway string `json:"default_gateway"` + AvailabilityZone string `json:"availability_zone"` + OSMonitoringStatus string `json:"os_monitoring_status"` + OSLoginStatus string `json:"os_login_status"` + VMStatus string `json:"vm_status"` + OperationStatus string `json:"operation_status"` + AppliancePlanID string `json:"virtual_network_appliance_plan_id"` + TenantID string `json:"tenant_id"` + Username string `json:"username"` + Password string `json:"password"` + Tags map[string]string `json:"tags"` + Interfaces InterfacesInResponse `json:"interfaces"` +} + +// AppliancePage is the page returned by a pager +// when traversing over a collection of virtual network appliance. +type AppliancePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of virtual network appliance +// 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 (r AppliancePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"appliances_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a AppliancePage struct is empty. +func (r AppliancePage) IsEmpty() (bool, error) { + is, err := ExtractAppliances(r) + return len(is) == 0, err +} + +// ExtractAppliances accepts a Page struct, +// specifically a NetworkPage struct, and extracts the elements +// into a slice of Virtual Network Appliance structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractAppliances(r pagination.Page) ([]Appliance, error) { + var s []Appliance + err := ExtractAppliancesInto(r, &s) + return s, err +} + +// ExtractAppliancesInto interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractAppliancesInto(r pagination.Page, v interface{}) error { + return r.(AppliancePage).Result.ExtractIntoSlicePtr(v, "virtual_network_appliances") +} diff --git a/v4/ecl/vna/v1/appliances/testing/doc.go b/v4/ecl/vna/v1/appliances/testing/doc.go new file mode 100644 index 0000000..f740d23 --- /dev/null +++ b/v4/ecl/vna/v1/appliances/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains virtual network appliance unit tests +package testing diff --git a/v4/ecl/vna/v1/appliances/testing/fixtures.go b/v4/ecl/vna/v1/appliances/testing/fixtures.go new file mode 100644 index 0000000..2490142 --- /dev/null +++ b/v4/ecl/vna/v1/appliances/testing/fixtures.go @@ -0,0 +1,1061 @@ +package testing + +import ( + "fmt" + + "github.com/nttcom/eclcloud/v4/ecl/vna/v1/appliances" +) + +const applianceType = "ECL::VirtualNetworkAppliance::VSRX" +const idAppliance1 = "45db3e66-31af-45a6-8ad2-d01521726141" +const idAppliance2 = "45db3e66-31af-45a6-8ad2-d01521726142" +const idAppliance3 = "45db3e66-31af-45a6-8ad2-d01521726143" + +const idVirtualNetworkAppliancePlan = "6589b37a-cf82-4918-96fe-255683f78e76" + +var listResponse = fmt.Sprintf(` +{ + "virtual_network_appliances": [ + { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + }, + { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_2_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "2.2.2.2", + "mac_address": "aa:bb:cc:dd:ee:f2", + "type": "", + "vrid": null + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.52", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_2", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } + ] +}`, + // for appliance1 + idAppliance1, + idVirtualNetworkAppliancePlan, + // for appliance2 + idAppliance2, + idVirtualNetworkAppliancePlan, +) + +var defaultInterface = appliances.InterfaceInResponse{ + Name: "", + Description: "", + NetworkID: "", + Updatable: true, + Tags: map[string]string{}, + FixedIPs: []appliances.FixedIPInResponse{}, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{}, +} + +var appliance1 = appliances.Appliance{ + ID: idAppliance1, + Name: "appliance_1", + ApplianceType: applianceType, + Description: "appliance_1_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + OSMonitoringStatus: "ACTIVE", + OSLoginStatus: "ACTIVE", + VMStatus: "ACTIVE", + OperationStatus: "COMPLETE", + AppliancePlanID: idVirtualNetworkAppliancePlan, + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Tags: map[string]string{"k1": "v1"}, + Interfaces: appliances.InterfacesInResponse{ + Interface1: appliances.InterfaceInResponse{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + Updatable: true, + FixedIPs: []appliances.FixedIPInResponse{ + { + IPAddress: "192.168.1.51", + SubnetID: "dummySubnetID", + }, + }, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{ + { + IPAddress: "1.1.1.1", + MACAddress: "aa:bb:cc:dd:ee:f1", + Type: "vrrp", + VRID: float64(123), + }, + }, + }, + Interface2: defaultInterface, + Interface3: defaultInterface, + Interface4: defaultInterface, + Interface5: defaultInterface, + Interface6: defaultInterface, + Interface7: defaultInterface, + Interface8: defaultInterface, + }, +} + +var appliance2 = appliances.Appliance{ + ID: idAppliance2, + Name: "appliance_2", + ApplianceType: applianceType, + Description: "appliance_2_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + OSMonitoringStatus: "ACTIVE", + OSLoginStatus: "ACTIVE", + VMStatus: "ACTIVE", + OperationStatus: "COMPLETE", + AppliancePlanID: idVirtualNetworkAppliancePlan, + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Tags: map[string]string{"k1": "v1"}, + Interfaces: appliances.InterfacesInResponse{ + Interface1: appliances.InterfaceInResponse{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + Updatable: true, + FixedIPs: []appliances.FixedIPInResponse{ + { + IPAddress: "192.168.1.52", + SubnetID: "dummySubnetID", + }, + }, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{ + { + IPAddress: "2.2.2.2", + MACAddress: "aa:bb:cc:dd:ee:f2", + Type: "", + VRID: interface{}(nil), + }, + }, + }, + Interface2: defaultInterface, + Interface3: defaultInterface, + Interface4: defaultInterface, + Interface5: defaultInterface, + Interface6: defaultInterface, + Interface7: defaultInterface, + Interface8: defaultInterface, + }, +} + +var appliance3 = appliances.Appliance{ + ID: idAppliance3, + Name: "appliance_3", + ApplianceType: applianceType, + Description: "appliance_3_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + OSMonitoringStatus: "ACTIVE", + OSLoginStatus: "ACTIVE", + VMStatus: "ACTIVE", + OperationStatus: "COMPLETE", + AppliancePlanID: idVirtualNetworkAppliancePlan, + TenantID: "9ee80f2a926c49f88f166af47df4e9f5", + Username: "root", + Password: "Passw0rd", + Tags: map[string]string{"k1": "v1"}, + Interfaces: appliances.InterfacesInResponse{ + Interface1: appliances.InterfaceInResponse{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + Updatable: true, + FixedIPs: []appliances.FixedIPInResponse{ + { + IPAddress: "192.168.1.53", + SubnetID: "dummySubnetID", + }, + }, + AllowedAddressPairs: []appliances.AllowedAddressPairInResponse{ + { + IPAddress: "3.3.3.3", + MACAddress: "aa:bb:cc:dd:ee:f3", + Type: "vrrp", + VRID: float64(123), + }, + }, + }, + Interface2: defaultInterface, + Interface3: defaultInterface, + Interface4: defaultInterface, + Interface5: defaultInterface, + Interface6: defaultInterface, + Interface7: defaultInterface, + Interface8: defaultInterface, + }, +} + +var expectedAppliancesSlice = []appliances.Appliance{ + appliance1, + appliance2, +} + +var getResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) + +var createRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "name": "appliance_1", + "description": "appliance_1_description", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "interfaces": { + "interface_1": { + "name": "interface_1", + "description": "interface_1_description", + "fixed_ips": [{ + "ip_address": "192.168.1.51" + }], + "network_id": "dummyNetworkID" + } + }, + "tags": { + "k1": "v1" + }, + "virtual_network_appliance_plan_id": "%s" + } + } +`, + idVirtualNetworkAppliancePlan, +) + +var createResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_3_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "3.3.3.3", + "mac_address": "aa:bb:cc:dd:ee:f3", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.53", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": {}, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_3", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance3, + idVirtualNetworkAppliancePlan, +) + +var updateMetadataRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "name": "appliance_1-update", + "description": "appliance_1_description-update", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "interfaces": { + "interface_1": { + "name": "interface_1", + "description": "interface_1_description", + "tags": { + "k1": "v1", + "k2": "v2" + } + } + } + } + }`, +) + +var updateMetadataResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description-update", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1-update", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) + +var updateNetworkIDAndFixedIPRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "interfaces": { + "interface_1": { + "network_id": "dummyNetworkID2", + "fixed_ips": [ + { + "ip_address": "192.168.1.51" + }, + { + "ip_address": "192.168.1.52" + } + ] + } + } + } + }`, +) + +var updateNetworkIDAndFixedIPResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + }, + { + "ip_address": "192.168.1.52", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID2", + "tags": { + "k1": "v1" + }, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) + +var updateAllowedAddressPairsRequest = fmt.Sprintf(` + { + "virtual_network_appliance": { + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + }, + { + "ip_address": "2.2.2.2", + "mac_address": "aa:bb:cc:dd:ee:f2", + "type": "", + "vrid": null + } + ] + } + } + } + }`, +) + +var updateAllowedAddressPairsResponse = fmt.Sprintf(` +{ + "virtual_network_appliance": { + "appliance_type": "ECL::VirtualNetworkAppliance::VSRX", + "availability_zone": "zone1-groupb", + "default_gateway": "192.168.1.1", + "description": "appliance_1_description", + "id": "%s", + "interfaces": { + "interface_1": { + "allowed_address_pairs": [ + { + "ip_address": "1.1.1.1", + "mac_address": "aa:bb:cc:dd:ee:f1", + "type": "vrrp", + "vrid": 123 + }, + { + "ip_address": "2.2.2.2", + "mac_address": "aa:bb:cc:dd:ee:f2", + "type": "", + "vrid": null + } + ], + "description": "interface_1_description", + "fixed_ips": [ + { + "ip_address": "192.168.1.51", + "subnet_id": "dummySubnetID" + } + ], + "name": "interface_1", + "network_id": "dummyNetworkID", + "tags": { + "k1": "v1" + }, + "updatable": true + }, + "interface_2": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_3": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_4": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_5": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_6": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_7": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + }, + "interface_8": { + "allowed_address_pairs": [], + "description": "", + "fixed_ips": [], + "name": "", + "network_id": "", + "tags": {}, + "updatable": true + } + }, + "name": "appliance_1", + "operation_status": "COMPLETE", + "os_login_status": "ACTIVE", + "os_monitoring_status": "ACTIVE", + "password": "Passw0rd", + "tags": { + "k1": "v1", + "k2": "v2" + }, + "tenant_id": "9ee80f2a926c49f88f166af47df4e9f5", + "username": "root", + "virtual_network_appliance_plan_id": "%s", + "vm_status": "ACTIVE" + } +}`, + idAppliance1, + idVirtualNetworkAppliancePlan, +) diff --git a/v4/ecl/vna/v1/appliances/testing/requests_test.go b/v4/ecl/vna/v1/appliances/testing/requests_test.go new file mode 100644 index 0000000..d031bf2 --- /dev/null +++ b/v4/ecl/vna/v1/appliances/testing/requests_test.go @@ -0,0 +1,313 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/ecl/vna/v1/appliances" + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper/client" + + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +const TokenID = client.TokenID + +func ServiceClient() *eclcloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v1.0/" + return sc +} + +func TestListAppliances(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc( + "/v1.0/virtual_network_appliances", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, listResponse) + }) + + cli := ServiceClient() + count := 0 + + err := appliances.List(cli, appliances.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := appliances.ExtractAppliances(page) + if err != nil { + t.Errorf("Failed to extract virtual network appliances: %v", err) + return false, err + } + + th.CheckDeepEquals(t, expectedAppliancesSlice, actual) + + return true, nil + }) + + if err != nil { + t.Errorf("Failed to get virtual network appliance list: %v", err) + } + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetAppliance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, getResponse) + }) + + ap, err := appliances.Get(ServiceClient(), idAppliance1).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &appliance1, ap) +} + +func TestCreateAppliance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1.0/virtual_network_appliances", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, createResponse) + }) + + createOpts := appliances.CreateOpts{ + Name: "appliance_1", + Description: "appliance_1_description", + DefaultGateway: "192.168.1.1", + AvailabilityZone: "zone1-groupb", + VirtualNetworkAppliancePlanID: idVirtualNetworkAppliancePlan, + Tags: map[string]string{"k1": "v1"}, + Interfaces: &appliances.CreateOptsInterfaces{ + Interface1: &appliances.CreateOptsInterface{ + Name: "interface_1", + Description: "interface_1_description", + NetworkID: "dummyNetworkID", + Tags: map[string]string{}, + FixedIPs: &[]appliances.CreateOptsFixedIP{ + { + IPAddress: "192.168.1.51", + }, + }, + }, + }, + } + ap, err := appliances.Create(ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.OperationStatus, "COMPLETE") + th.AssertDeepEquals(t, &appliance3, ap) +} + +func TestDeleteAppliance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := appliances.Delete(ServiceClient(), idAppliance1) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateApplianceMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateMetadataRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateMetadataResponse) + }) + + name := "appliance_1-update" + description := "appliance_1_description-update" + tags := map[string]string{"k1": "v1", "k2": "v2"} + + interface1Name := "interface_1" + interface1Description := "interface_1_description" + interface1Tags := map[string]string{"k1": "v1", "k2": "v2"} + + updateOptsInterface1 := appliances.UpdateMetadataInterface{ + Name: &interface1Name, + Description: &interface1Description, + Tags: &interface1Tags, + } + updateOpts := appliances.UpdateMetadataOpts{ + Name: &name, + Description: &description, + Tags: &tags, + Interfaces: &appliances.UpdateMetadataInterfaces{ + Interface1: &updateOptsInterface1, + }, + } + ap, err := appliances.Update( + ServiceClient(), idAppliance1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.Name, "appliance_1-update") + th.AssertEquals(t, ap.Description, "appliance_1_description-update") + th.AssertEquals(t, ap.ID, idAppliance1) +} + +func TestUpdateApplianceNetworkIDAndFixedIP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateNetworkIDAndFixedIPRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateNetworkIDAndFixedIPResponse) + }) + + networkID := "dummyNetworkID2" + + updateAddressInfo1 := appliances.UpdateFixedIPAddressInfo{ + IPAddress: "192.168.1.51", + } + + updateAddressInfo2 := appliances.UpdateFixedIPAddressInfo{ + IPAddress: "192.168.1.52", + } + updateFixedIPs := []appliances.UpdateFixedIPAddressInfo{ + updateAddressInfo1, + updateAddressInfo2, + } + + updateOptsInterface1 := appliances.UpdateFixedIPInterface{ + NetworkID: &networkID, + FixedIPs: &updateFixedIPs, + } + updateOpts := appliances.UpdateFixedIPOpts{ + Interfaces: &appliances.UpdateFixedIPInterfaces{ + Interface1: &updateOptsInterface1, + }, + } + ap, err := appliances.Update( + ServiceClient(), idAppliance1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.Interfaces.Interface1.NetworkID, "dummyNetworkID2") + th.AssertEquals(t, ap.Interfaces.Interface1.FixedIPs[0].IPAddress, "192.168.1.51") + th.AssertEquals(t, ap.Interfaces.Interface1.FixedIPs[1].IPAddress, "192.168.1.52") + th.AssertEquals(t, ap.ID, idAppliance1) +} +func TestUpdateApplianceAllowedAddressPairs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + url := fmt.Sprintf("/v1.0/virtual_network_appliances/%s", idAppliance1) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateAllowedAddressPairsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprint(w, updateAllowedAddressPairsResponse) + }) + + mac1 := "aa:bb:cc:dd:ee:f1" + type1 := "vrrp" + + var vrid1 interface{} = 123 + + UpdateAllowedAddressPairAddressInfo1 := appliances.UpdateAllowedAddressPairAddressInfo{ + IPAddress: "1.1.1.1", + MACAddress: &mac1, + Type: &type1, + VRID: &vrid1, + } + + mac2 := "aa:bb:cc:dd:ee:f2" + type2 := "" + + var vrid2 interface{} = nil + + UpdateAllowedAddressPairAddressInfo2 := appliances.UpdateAllowedAddressPairAddressInfo{ + IPAddress: "2.2.2.2", + MACAddress: &mac2, + Type: &type2, + VRID: &vrid2, + } + + updateAllowedAddressPairs := []appliances.UpdateAllowedAddressPairAddressInfo{ + UpdateAllowedAddressPairAddressInfo1, + UpdateAllowedAddressPairAddressInfo2, + } + + updateOptsInterface1 := appliances.UpdateAllowedAddressPairInterface{ + AllowedAddressPairs: &updateAllowedAddressPairs, + } + + updateOpts := appliances.UpdateAllowedAddressPairOpts{ + Interfaces: &appliances.UpdateAllowedAddressPairInterfaces{ + Interface1: &updateOptsInterface1, + }, + } + ap, err := appliances.Update( + ServiceClient(), idAppliance1, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].IPAddress, "1.1.1.1") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].MACAddress, "aa:bb:cc:dd:ee:f1") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].Type, "vrrp") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[0].VRID, float64(123)) + + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].IPAddress, "2.2.2.2") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].MACAddress, "aa:bb:cc:dd:ee:f2") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].Type, "") + th.AssertEquals(t, ap.Interfaces.Interface1.AllowedAddressPairs[1].VRID, interface{}(nil)) + + th.AssertEquals(t, ap.ID, idAppliance1) +} diff --git a/v4/ecl/vna/v1/appliances/urls.go b/v4/ecl/vna/v1/appliances/urls.go new file mode 100644 index 0000000..cbc208d --- /dev/null +++ b/v4/ecl/vna/v1/appliances/urls.go @@ -0,0 +1,33 @@ +package appliances + +import ( + "github.com/nttcom/eclcloud/v4" +) + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("virtual_network_appliances", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("virtual_network_appliances") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/endpoint_search.go b/v4/endpoint_search.go new file mode 100644 index 0000000..a106f6a --- /dev/null +++ b/v4/endpoint_search.go @@ -0,0 +1,74 @@ +package eclcloud + +// 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 +// Enterprise Cloud 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 "ecl.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 + } + eo.Availability = AvailabilityPublic +} diff --git a/v4/errors.go b/v4/errors.go new file mode 100644 index 0000000..d4edadf --- /dev/null +++ b/v4/errors.go @@ -0,0 +1,474 @@ +package eclcloud + +import ( + "fmt" + "strings" +) + +// BaseError is an error type that all other error types embed. +type BaseError struct { + DefaultErrString string + Info string +} + +func (e BaseError) Error() string { + e.DefaultErrString = "An error occurred while executing a Eclcloud request." + return e.choseErrString() +} + +func (e BaseError) choseErrString() string { + if e.Info != "" { + return e.Info + } + return e.DefaultErrString +} + +// ErrMissingInput is the error when input is required in a particular +// situation but not provided by the user +type ErrMissingInput struct { + BaseError + Argument string +} + +func (e ErrMissingInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument) + return e.choseErrString() +} + +// ErrInvalidInput is an error type used for most non-HTTP Eclcloud errors. +type ErrInvalidInput struct { + ErrMissingInput + Value interface{} +} + +func (e ErrInvalidInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value) + return e.choseErrString() +} + +// ErrMissingEnvironmentVariable is the error when environment variable is required +// in a particular situation but not provided by the user +type ErrMissingEnvironmentVariable struct { + BaseError + EnvironmentVariable string +} + +func (e ErrMissingEnvironmentVariable) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing environment variable [%s]", e.EnvironmentVariable) + return e.choseErrString() +} + +// ErrMissingAnyoneOfEnvironmentVariables is the error when anyone of the environment variables +// is required in a particular situation but not provided by the user +type ErrMissingAnyoneOfEnvironmentVariables struct { + BaseError + EnvironmentVariables []string +} + +func (e ErrMissingAnyoneOfEnvironmentVariables) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Missing one of the following environment variables [%s]", + strings.Join(e.EnvironmentVariables, ", "), + ) + return e.choseErrString() +} + +// ErrUnexpectedResponseCode is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type ErrUnexpectedResponseCode struct { + BaseError + URL string + Method string + Expected []int + Actual int + Body []byte +} + +func (e ErrUnexpectedResponseCode) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + e.Expected, e.Method, e.URL, e.Actual, e.Body, + ) + return e.choseErrString() +} + +// ErrDefault400 is the default error type returned on a 400 HTTP response code. +type ErrDefault400 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault401 is the default error type returned on a 401 HTTP response code. +type ErrDefault401 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault403 is the default error type returned on a 403 HTTP response code. +type ErrDefault403 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault404 is the default error type returned on a 404 HTTP response code. +type ErrDefault404 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault405 is the default error type returned on a 405 HTTP response code. +type ErrDefault405 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault408 is the default error type returned on a 408 HTTP response code. +type ErrDefault408 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault409 is the default error type returned on a 409 HTTP response code. +type ErrDefault409 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault429 is the default error type returned on a 429 HTTP response code. +type ErrDefault429 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault500 is the default error type returned on a 500 HTTP response code. +type ErrDefault500 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault503 is the default error type returned on a 503 HTTP response code. +type ErrDefault503 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Bad request with: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault401) Error() string { + return "Authentication failed" +} +func (e ErrDefault403) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Request forbidden: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault404) Error() string { + return "Resource not found" +} +func (e ErrDefault405) Error() string { + return "Method not allowed" +} +func (e ErrDefault408) Error() string { + return "The server timed out waiting for the request" +} +func (e ErrDefault409) Error() string { + return "Request conflicted" +} +func (e ErrDefault429) Error() string { + return "Too many requests have been sent in a given amount of time. Pause" + + " requests, wait up to one minute, and try again." +} +func (e ErrDefault500) Error() string { + return "Internal Server Error" +} +func (e ErrDefault503) Error() string { + return "The service is currently unable to handle the request due to a temporary" + + " overloading or maintenance. This is a temporary condition. Try again later." +} + +// Err400er is the interface resource error types implement to override the error message +// from a 400 error. +type Err400er interface { + Error400(ErrUnexpectedResponseCode) error +} + +// Err401er is the interface resource error types implement to override the error message +// from a 401 error. +type Err401er interface { + Error401(ErrUnexpectedResponseCode) error +} + +// Err403er is the interface resource error types implement to override the error message +// from a 403 error. +type Err403er interface { + Error403(ErrUnexpectedResponseCode) error +} + +// Err404er is the interface resource error types implement to override the error message +// from a 404 error. +type Err404er interface { + Error404(ErrUnexpectedResponseCode) error +} + +// Err405er is the interface resource error types implement to override the error message +// from a 405 error. +type Err405er interface { + Error405(ErrUnexpectedResponseCode) error +} + +// Err408er is the interface resource error types implement to override the error message +// from a 408 error. +type Err408er interface { + Error408(ErrUnexpectedResponseCode) error +} + +// Err409er is the interface resource error types implement to override the error message +// from a 409 error. +type Err409er interface { + Error409(ErrUnexpectedResponseCode) error +} + +// Err429er is the interface resource error types implement to override the error message +// from a 429 error. +type Err429er interface { + Error429(ErrUnexpectedResponseCode) error +} + +// Err500er is the interface resource error types implement to override the error message +// from a 500 error. +type Err500er interface { + Error500(ErrUnexpectedResponseCode) error +} + +// Err503er is the interface resource error types implement to override the error message +// from a 503 error. +type Err503er interface { + Error503(ErrUnexpectedResponseCode) error +} + +// ErrTimeOut is the error type returned when an operations times out. +type ErrTimeOut struct { + BaseError +} + +func (e ErrTimeOut) Error() string { + e.DefaultErrString = "A time out occurred" + return e.choseErrString() +} + +// ErrUnableToReauthenticate is the error type returned when reauthentication fails. +type ErrUnableToReauthenticate struct { + BaseError + ErrOriginal error +} + +func (e ErrUnableToReauthenticate) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrErrorAfterReauthentication is the error type returned when reauthentication +// succeeds, but an error occurs afterword (usually an HTTP error). +type ErrErrorAfterReauthentication struct { + BaseError + ErrOriginal error +} + +func (e ErrErrorAfterReauthentication) Error() string { + e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal) + return e.choseErrString() +} + +// 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. +type ErrServiceNotFound struct { + BaseError +} + +func (e ErrServiceNotFound) Error() string { + e.DefaultErrString = "No suitable service could be found in the service catalog." + return e.choseErrString() +} + +// 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. +type ErrEndpointNotFound struct { + BaseError +} + +func (e ErrEndpointNotFound) Error() string { + e.DefaultErrString = "No suitable endpoint could be found in the service catalog." + return e.choseErrString() +} + +// ErrResourceNotFound is the error when trying to retrieve a resource's +// ID by name and the resource doesn't exist. +type ErrResourceNotFound struct { + BaseError + Name string + ResourceType string +} + +func (e ErrResourceNotFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrMultipleResourcesFound is the error when trying to retrieve a resource's +// ID by name and multiple resources have the user-provided name. +type ErrMultipleResourcesFound struct { + BaseError + Name string + Count int + ResourceType string +} + +func (e ErrMultipleResourcesFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrUnexpectedType is the error when an unexpected type is encountered +type ErrUnexpectedType struct { + BaseError + Expected string + Actual string +} + +func (e ErrUnexpectedType) Error() string { + e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual) + return e.choseErrString() +} + +func unacceptedAttributeErr(attribute string) string { + return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute) +} + +// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. +type ErrAPIKeyProvided struct{ BaseError } + +func (e ErrAPIKeyProvided) Error() string { + return unacceptedAttributeErr("APIKey") +} + +// ErrTenantIDProvided indicates that a TenantID was provided but can't be used. +type ErrTenantIDProvided struct{ BaseError } + +func (e ErrTenantIDProvided) Error() string { + return unacceptedAttributeErr("TenantID") +} + +// ErrTenantNameProvided indicates that a TenantName was provided but can't be used. +type ErrTenantNameProvided struct{ BaseError } + +func (e ErrTenantNameProvided) Error() string { + return unacceptedAttributeErr("TenantName") +} + +// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. +type ErrUsernameWithToken struct{ BaseError } + +func (e ErrUsernameWithToken) Error() string { + return redundantWithTokenErr("Username") +} + +// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. +type ErrUserIDWithToken struct{ BaseError } + +func (e ErrUserIDWithToken) Error() string { + return redundantWithTokenErr("UserID") +} + +// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. +type ErrDomainIDWithToken struct{ BaseError } + +func (e ErrDomainIDWithToken) Error() string { + return redundantWithTokenErr("DomainID") +} + +// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s +type ErrDomainNameWithToken struct{ BaseError } + +func (e ErrDomainNameWithToken) Error() string { + return redundantWithTokenErr("DomainName") +} + +// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. +type ErrUsernameOrUserID struct{ BaseError } + +func (e ErrUsernameOrUserID) Error() string { + return "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. +type ErrDomainIDWithUserID struct{ BaseError } + +func (e ErrDomainIDWithUserID) Error() string { + return redundantWithUserID("DomainID") +} + +// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. +type ErrDomainNameWithUserID struct{ BaseError } + +func (e ErrDomainNameWithUserID) Error() string { + return 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. +type ErrDomainIDOrDomainName struct{ BaseError } + +func (e ErrDomainIDOrDomainName) Error() string { + return "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. +type ErrMissingPassword struct{ BaseError } + +func (e ErrMissingPassword) Error() string { + return "You must provide a password to authenticate" +} + +// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. +type ErrScopeDomainIDOrDomainName struct{ BaseError } + +func (e ErrScopeDomainIDOrDomainName) Error() string { + return "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. +type ErrScopeProjectIDOrProjectName struct{ BaseError } + +func (e ErrScopeProjectIDOrProjectName) Error() string { + return "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. +type ErrScopeProjectIDAlone struct{ BaseError } + +func (e ErrScopeProjectIDAlone) Error() string { + return "ProjectID must be supplied alone in a Scope" +} + +// ErrScopeEmpty indicates that no credentials were provided in a Scope. +type ErrScopeEmpty struct{ BaseError } + +func (e ErrScopeEmpty) Error() string { + return "You must provide either a Project or Domain in a Scope" +} + +// ErrAppCredMissingSecret indicates that no Application Credential Secret was provided with Application Credential ID or Name +type ErrAppCredMissingSecret struct{ BaseError } + +func (e ErrAppCredMissingSecret) Error() string { + return "You must provide an Application Credential Secret" +} diff --git a/v4/go.mod b/v4/go.mod new file mode 100644 index 0000000..392b243 --- /dev/null +++ b/v4/go.mod @@ -0,0 +1,3 @@ +module github.com/nttcom/eclcloud/v4 + +go 1.17 diff --git a/v4/internal/pkg.go b/v4/internal/pkg.go new file mode 100644 index 0000000..5bf0569 --- /dev/null +++ b/v4/internal/pkg.go @@ -0,0 +1 @@ +package internal diff --git a/v4/internal/testing/pkg.go b/v4/internal/testing/pkg.go new file mode 100644 index 0000000..7603f83 --- /dev/null +++ b/v4/internal/testing/pkg.go @@ -0,0 +1 @@ +package testing diff --git a/v4/internal/testing/util_test.go b/v4/internal/testing/util_test.go new file mode 100644 index 0000000..a03029b --- /dev/null +++ b/v4/internal/testing/util_test.go @@ -0,0 +1,42 @@ +package testing + +import ( + "reflect" + "testing" + + "github.com/nttcom/eclcloud/v4/internal" +) + +func TestRemainingKeys(t *testing.T) { + type User struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Location string `json:"-"` + CreatedAt string `json:"-"` + Status string + IsAdmin bool + } + + userResponse := map[string]interface{}{ + "user_id": "abcd1234", + "username": "jdoe", + "location": "Hawaii", + "created_at": "2017-06-08T02:49:03.000000", + "status": "active", + "is_admin": "true", + "custom_field": "foo", + } + + expected := map[string]interface{}{ + "created_at": "2017-06-08T02:49:03.000000", + "is_admin": "true", + "custom_field": "foo", + } + + actual := internal.RemainingKeys(User{}, userResponse) + + isEqual := reflect.DeepEqual(expected, actual) + if !isEqual { + t.Fatalf("expected %s but got %s", expected, actual) + } +} diff --git a/v4/internal/util.go b/v4/internal/util.go new file mode 100644 index 0000000..8efb283 --- /dev/null +++ b/v4/internal/util.go @@ -0,0 +1,34 @@ +package internal + +import ( + "reflect" + "strings" +) + +// RemainingKeys will inspect a struct and compare it to a map. Any struct +// field that does not have a JSON tag that matches a key in the map or +// a matching lower-case field in the map will be returned as an extra. +// +// This is useful for determining the extra fields returned in response bodies +// for resources that can contain an arbitrary or dynamic number of fields. +func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) { + extras = make(map[string]interface{}) + for k, v := range m { + extras[k] = v + } + + valueOf := reflect.ValueOf(s) + typeOf := reflect.TypeOf(s) + for i := 0; i < valueOf.NumField(); i++ { + field := typeOf.Field(i) + + lowerField := strings.ToLower(field.Name) + delete(extras, lowerField) + + if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" { + delete(extras, tagValue) + } + } + + return +} diff --git a/v4/pagination/http.go b/v4/pagination/http.go new file mode 100644 index 0000000..6d37d60 --- /dev/null +++ b/v4/pagination/http.go @@ -0,0 +1,59 @@ +package pagination + +import ( + "encoding/json" + "github.com/nttcom/eclcloud/v4" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + eclcloud.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 PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { + return PageResult{ + Result: eclcloud.Result{ + Body: body, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *eclcloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &eclcloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + }) +} diff --git a/v4/pagination/linked.go b/v4/pagination/linked.go new file mode 100644 index 0000000..8311242 --- /dev/null +++ b/v4/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/nttcom/eclcloud/v4" +) + +// 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 { + err := eclcloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + err := eclcloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := eclcloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := eclcloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// 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/v4/pagination/marker.go b/v4/pagination/marker.go new file mode 100644 index 0000000..8eec45a --- /dev/null +++ b/v4/pagination/marker.go @@ -0,0 +1,57 @@ +package pagination + +import ( + "fmt" + "github.com/nttcom/eclcloud/v4" + "reflect" +) + +// 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 +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := eclcloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// 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/v4/pagination/pager.go b/v4/pagination/pager.go new file mode 100644 index 0000000..6399f52 --- /dev/null +++ b/v4/pagination/pager.go @@ -0,0 +1,250 @@ +package pagination + +import ( + "errors" + "fmt" + "github.com/nttcom/eclcloud/v4" + "net/http" + "reflect" + "strings" +) + +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 *eclcloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + firstPage 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 *eclcloud.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 { + var currentPage Page + + // if first page has already been fetched, no need to fetch it again + if p.firstPage != nil { + currentPage = p.firstPage + p.firstPage = nil + } else { + var err error + 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 first page to ascertain the page body type. + firstPage, 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(firstPage) + + // if it's a single page, just return the firstPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return firstPage, nil + } + + // store the first page to avoid getting it twice + p.firstPage = firstPage + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch pb := firstPage.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, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []interface{} (which is really []map[string]interface{}) + switch vt := v.(type) { + case []interface{}: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + 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 + } + if len(pagesSlice) > 0 { + // 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: + err := eclcloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}/[]byte/[]interface{}" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // 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/v4/pagination/pkg.go b/v4/pagination/pkg.go new file mode 100644 index 0000000..90cb4b2 --- /dev/null +++ b/v4/pagination/pkg.go @@ -0,0 +1,5 @@ +/* +Package pagination contains utilities and convenience structs +that implement common pagination idioms within Enterprise Cloud APIs. +*/ +package pagination diff --git a/v4/pagination/single.go b/v4/pagination/single.go new file mode 100644 index 0000000..af73ae6 --- /dev/null +++ b/v4/pagination/single.go @@ -0,0 +1,32 @@ +package pagination + +import ( + "fmt" + "github.com/nttcom/eclcloud/v4" + "reflect" +) + +// 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 +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := eclcloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// 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/v4/pagination/testing/doc.go b/v4/pagination/testing/doc.go new file mode 100644 index 0000000..0bc1eb3 --- /dev/null +++ b/v4/pagination/testing/doc.go @@ -0,0 +1,2 @@ +// pagination +package testing diff --git a/v4/pagination/testing/linked_test.go b/v4/pagination/testing/linked_test.go new file mode 100644 index 0000000..a30452e --- /dev/null +++ b/v4/pagination/testing/linked_test.go @@ -0,0 +1,112 @@ +package testing + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper" +) + +// LinkedPager sample and test cases. + +type LinkedPageResult struct { + pagination.LinkedPageBase +} + +func (r LinkedPageResult) IsEmpty() (bool, error) { + is, err := ExtractLinkedInts(r) + return len(is) == 0, err +} + +func ExtractLinkedInts(r pagination.Page) ([]int, error) { + var s struct { + Ints []int `json:"ints"` + } + err := (r.(LinkedPageResult)).ExtractInto(&s) + return s.Ints, err +} + +func createLinked(t *testing.T) pagination.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 pagination.PageResult) pagination.Page { + return LinkedPageResult{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.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 pagination.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/v4/pagination/testing/marker_test.go b/v4/pagination/testing/marker_test.go new file mode 100644 index 0000000..0068838 --- /dev/null +++ b/v4/pagination/testing/marker_test.go @@ -0,0 +1,127 @@ +package testing + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper" +) + +// MarkerPager sample and test cases. + +type MarkerPageResult struct { + pagination.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) pagination.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 pagination.PageResult) pagination.Page { + p := MarkerPageResult{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return pagination.NewPager(client, testhelper.Server.URL+"/page", createPage) +} + +func ExtractMarkerStrings(page pagination.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 pagination.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/v4/pagination/testing/pagination_test.go b/v4/pagination/testing/pagination_test.go new file mode 100644 index 0000000..2386adf --- /dev/null +++ b/v4/pagination/testing/pagination_test.go @@ -0,0 +1,13 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/testhelper" +) + +func createClient() *eclcloud.ServiceClient { + return &eclcloud.ServiceClient{ + ProviderClient: &eclcloud.ProviderClient{TokenID: "abc123"}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/v4/pagination/testing/single_test.go b/v4/pagination/testing/single_test.go new file mode 100644 index 0000000..3553b9c --- /dev/null +++ b/v4/pagination/testing/single_test.go @@ -0,0 +1,79 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4/pagination" + "github.com/nttcom/eclcloud/v4/testhelper" +) + +// SinglePage sample and test cases. + +type SinglePageResult struct { + pagination.SinglePageBase +} + +func (r SinglePageResult) IsEmpty() (bool, error) { + is, err := ExtractSingleInts(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +func ExtractSingleInts(r pagination.Page) ([]int, error) { + var s struct { + Ints []int `json:"ints"` + } + err := (r.(SinglePageResult)).ExtractInto(&s) + return s.Ints, err +} + +func setupSinglePaged() pagination.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 pagination.PageResult) pagination.Page { + return SinglePageResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, testhelper.Server.URL+"/only", createPage) +} + +func TestEnumerateSinglePaged(t *testing.T) { + callCount := 0 + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + err := pager.EachPage(func(page pagination.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/v4/params.go b/v4/params.go new file mode 100644 index 0000000..88b06c3 --- /dev/null +++ b/v4/params.go @@ -0,0 +1,488 @@ +package eclcloud + +import ( + "encoding/json" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +/* +BuildRequestBody builds a map[string]interface from the given `struct`. If +parent is not an empty string, the final map[string]interface returned will +encapsulate the built one. For example: + + disk := 1 + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: &disk, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + body, err := eclcloud.BuildRequestBody(createOpts, "flavor") + +The above example can be run as-is, however it is recommended to look at how +BuildRequestBody is used within eclcloud to more fully understand how it +fits within the request process as a whole rather than use it directly as shown +above. +*/ +func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, 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]interface{}) + if optsValue.Kind() == reflect.Struct { + //fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind()) + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + + if f.Name != strings.Title(f.Name) { + //fmt.Printf("Skipping field: %s...\n", f.Name) + continue + } + + //fmt.Printf("Starting on field: %s...\n", f.Name) + + zero := isZero(v) + //fmt.Printf("v is zero?: %v\n", zero) + + // if the field has a required tag that's set to "true" + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + //fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero) + // if the field's value is zero, return a missing-argument error + if zero { + // if the field has a 'required' tag, it can't have a zero-value + err := ErrMissingInput{} + err.Argument = f.Name + return nil, err + } + } + + if xorTag := f.Tag.Get("xor"); xorTag != "" { + //fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag) + xorField := optsValue.FieldByName(xorTag) + var xorFieldIsZero bool + if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) { + xorFieldIsZero = true + } else { + if xorField.Kind() == reflect.Ptr { + xorField = xorField.Elem() + } + xorFieldIsZero = isZero(xorField) + } + if !(zero != xorFieldIsZero) { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag) + err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag) + return nil, err + } + } + + if orTag := f.Tag.Get("or"); orTag != "" { + //fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag) + //fmt.Printf("field is zero?: %v\n", zero) + if zero { + orField := optsValue.FieldByName(orTag) + var orFieldIsZero bool + if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) { + orFieldIsZero = true + } else { + if orField.Kind() == reflect.Ptr { + orField = orField.Elem() + } + orFieldIsZero = isZero(orField) + } + if orFieldIsZero { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag) + err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag) + return nil, err + } + } + } + + jsonTag := f.Tag.Get("json") + if jsonTag == "-" { + continue + } + + if v.Kind() == reflect.Slice || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice) { + sliceValue := v + if sliceValue.Kind() == reflect.Ptr { + sliceValue = sliceValue.Elem() + } + + for i := 0; i < sliceValue.Len(); i++ { + element := sliceValue.Index(i) + if element.Kind() == reflect.Struct || (element.Kind() == reflect.Ptr && element.Elem().Kind() == reflect.Struct) { + _, err := BuildRequestBody(element.Interface(), "") + if err != nil { + return nil, err + } + } + } + } + if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { + if zero { + //fmt.Printf("value before change: %+v\n", optsValue.Field(i)) + if jsonTag != "" { + jsonTagPieces := strings.Split(jsonTag, ",") + if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { + if v.CanSet() { + if !v.IsNil() { + if v.Kind() == reflect.Ptr { + v.Set(reflect.Zero(v.Type())) + } + } + //fmt.Printf("value after change: %+v\n", optsValue.Field(i)) + } + } + } + continue + } + + //fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name) + _, err := BuildRequestBody(v.Interface(), f.Name) + if err != nil { + return nil, err + } + } + } + + //fmt.Printf("opts: %+v \n", opts) + + b, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + //fmt.Printf("string(b): %s\n", string(b)) + + err = json.Unmarshal(b, &optsMap) + if err != nil { + return nil, err + } + + //fmt.Printf("optsMap: %+v\n", optsMap) + + if parent != "" { + optsMap = map[string]interface{}{parent: optsMap} + } + //fmt.Printf("optsMap after parent added: %+v\n", optsMap) + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("options type is not a struct") +} + +// 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 +) + +// IPVersion is a type for the possible IP address versions. Valid instances +// are IPv4 and IPv6 +type IPVersion int + +const ( + // IPv4 is used for IP version 4 addresses + IPv4 IPVersion = 4 + // IPv6 is used for IP version 6 addresses + IPv6 IPVersion = 6 +) + +// 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 +} + +/* +func isUnderlyingStructZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr: + return isUnderlyingStructZero(v.Elem()) + default: + return isZero(v) + } +} +*/ + +var t time.Time + +func isZero(v reflect.Value) bool { + //fmt.Printf("\n\nchecking isZero for value: %+v\n", v) + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return true + } + return false + 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) { + return v.Interface().(time.Time).IsZero() + } + 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()) + //fmt.Printf("zero type for value: %+v\n\n\n", z) + 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) { + loop: + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + goto loop + 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()) + } + } + case reflect.Map: + if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String { + var s []string + for _, k := range v.MapKeys() { + value := v.MapIndex(k).String() + s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) + } + params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", "))) + } + } + } else { + // if the field has a 'required' tag, it can't have a zero-value + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + return &url.URL{}, 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 { + // if the field has a 'required' tag, it can't have a zero-value + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + return optsMap, fmt.Errorf("required header [%s] not set", f.Name) + } + } + } + + } + 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/v4/provider_client.go b/v4/provider_client.go new file mode 100644 index 0000000..5188c75 --- /dev/null +++ b/v4/provider_client.go @@ -0,0 +1,402 @@ +package eclcloud + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "log" + "net/http" + "strings" + "sync" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const DefaultUserAgent = "eclcloud/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 +// Eclcloud 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. + // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. + // To safely read or write this value, call `Token` or `SetToken`, respectively + 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 + + mut *sync.RWMutex + + reauthmut *reauthlock +} + +type reauthlock struct { + sync.RWMutex + reauthing bool +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { + if client.reauthmut != nil { + client.reauthmut.RLock() + if client.reauthmut.reauthing { + client.reauthmut.RUnlock() + return + } + client.reauthmut.RUnlock() + } + t := client.Token() + if t == "" { + return + } + return map[string]string{"X-Auth-Token": t} +} + +// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. +// If the application's ProviderClient is not used concurrently, this doesn't need to be called. +func (client *ProviderClient) UseTokenLock() { + client.mut = new(sync.RWMutex) + client.reauthmut = new(reauthlock) +} + +// Token safely reads the value of the auth token from the ProviderClient. Applications should +// call this method to access the token instead of the TokenID field +func (client *ProviderClient) Token() string { + if client.mut != nil { + client.mut.RLock() + defer client.mut.RUnlock() + } + return client.TokenID +} + +// SetToken safely sets the value of the auth token in the ProviderClient. Applications may +// use this method in a custom ReauthFunc +func (client *ProviderClient) SetToken(t string) { + if client.mut != nil { + client.mut.Lock() + defer client.mut.Unlock() + } + client.TokenID = t +} + +//Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is +//called because of a 401 response, the caller may pass the previous token. In +//this case, the reauthentication can be skipped if another thread has already +//reauthenticated in the meantime. If no previous token is known, an empty +//string should be passed instead to force unconditional reauthentication. +func (client *ProviderClient) Reauthenticate(previousToken string) (err error) { + if client.ReauthFunc == nil { + return nil + } + + if client.mut == nil { + return client.ReauthFunc() + } + client.mut.Lock() + defer client.mut.Unlock() + + client.reauthmut.Lock() + client.reauthmut.reauthing = true + client.reauthmut.Unlock() + + if previousToken == "" || client.TokenID == previousToken { + err = client.ReauthFunc() + } + + client.reauthmut.Lock() + client.reauthmut.reauthing = false + client.reauthmut.Unlock() + return +} + +// 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 + // ErrorContext specifies the resource error type to return if an error is encountered. + // This lets resources override default error messages based on the response status code. + ErrorContext error +} + +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 + + log.Printf("[DEBUG] Request: %s %s", method, url) + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.ReadSeeker 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 eclcloud.Request().") + } + + rendered, err := json.Marshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + log.Printf("[DEBUG] Request body: %s", body) + + } + + if options.RawBody != nil { + body = options.RawBody + log.Printf("[DEBUG] Request body: %s", body) + } + + // 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) + + // 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) + } + } + } + + // get latest token from client + for k, v := range client.AuthenticatedHeaders() { + req.Header.Set(k, v) + } + + // Set connection parameter to close the connection immediately when we've got the response + req.Close = true + + prereqtok := req.Header.Get("X-Auth-Token") + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, 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() + respErr := ErrUnexpectedResponseCode{ + URL: url, + Method: method, + Expected: options.OkCodes, + Actual: resp.StatusCode, + Body: body, + } + + errType := options.ErrorContext + switch resp.StatusCode { + case http.StatusBadRequest: + err = ErrDefault400{respErr} + if error400er, ok := errType.(Err400er); ok { + err = error400er.Error400(respErr) + } + case http.StatusUnauthorized: + if client.ReauthFunc != nil { + err = client.Reauthenticate(prereqtok) + if err != nil { + e := &ErrUnableToReauthenticate{} + e.ErrOriginal = respErr + return nil, e + } + if options.RawBody != nil { + if seeker, ok := options.RawBody.(io.Seeker); ok { + seeker.Seek(0, 0) + } + } + // make a new call to request with a nil reauth func in order to avoid infinite loop + reauthFunc := client.ReauthFunc + client.ReauthFunc = nil + resp, err = client.Request(method, url, options) + client.ReauthFunc = reauthFunc + if err != nil { + switch err.(type) { + case *ErrUnexpectedResponseCode: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err.(*ErrUnexpectedResponseCode) + return nil, e + default: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err + return nil, e + } + } + return resp, nil + } + err = ErrDefault401{respErr} + if error401er, ok := errType.(Err401er); ok { + err = error401er.Error401(respErr) + } + case http.StatusForbidden: + err = ErrDefault403{respErr} + if error403er, ok := errType.(Err403er); ok { + err = error403er.Error403(respErr) + } + case http.StatusNotFound: + err = ErrDefault404{respErr} + if error404er, ok := errType.(Err404er); ok { + err = error404er.Error404(respErr) + } + case http.StatusMethodNotAllowed: + err = ErrDefault405{respErr} + if error405er, ok := errType.(Err405er); ok { + err = error405er.Error405(respErr) + } + case http.StatusRequestTimeout: + err = ErrDefault408{respErr} + if error408er, ok := errType.(Err408er); ok { + err = error408er.Error408(respErr) + } + case http.StatusConflict: + err = ErrDefault409{respErr} + if error409er, ok := errType.(Err409er); ok { + err = error409er.Error409(respErr) + } + case 429: + err = ErrDefault429{respErr} + if error429er, ok := errType.(Err429er); ok { + err = error429er.Error429(respErr) + } + case http.StatusInternalServerError: + err = ErrDefault500{respErr} + if error500er, ok := errType.(Err500er); ok { + err = error500er.Error500(respErr) + } + case http.StatusServiceUnavailable: + err = ErrDefault503{respErr} + if error503er, ok := errType.(Err503er); ok { + err = error503er.Error503(respErr) + } + } + + if err == nil { + err = respErr + } + + return resp, err + } + + // Parse the response body as JSON, if requested to do so. + if options.JSONResponse != nil { + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + return nil, err + } + } + + 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 == "PATCH": + return []int{200, 202, 204} + case method == "DELETE": + return []int{202, 204} + } + + return []int{} +} diff --git a/v4/results.go b/v4/results.go new file mode 100644 index 0000000..30e93ce --- /dev/null +++ b/v4/results.go @@ -0,0 +1,473 @@ +package eclcloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + // "log" + "log" + "net/http" + "reflect" + "strconv" + "time" +) + +/* +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 +} + +// ExtractInto allows users to provide an object into which `Extract` will extract +// the `Result.Body`. This would be useful for Enterprise Cloud providers that have +// different fields in the response object than Enterprise Cloud proper. +func (r Result) ExtractInto(to interface{}) error { + if r.Err != nil { + log.Printf("[DEBUG] Response body: %s", r.Err) + return r.Err + } + + if reader, ok := r.Body.(io.Reader); ok { + if readCloser, ok := reader.(io.Closer); ok { + defer readCloser.Close() + } + return json.NewDecoder(reader).Decode(to) + } + + b, err := json.Marshal(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + log.Printf("[DEBUG] Response body: %s", b) + + return err +} + +func (r Result) extractIntoPtr(to interface{}, label string) error { + if label == "" { + return r.ExtractInto(&to) + } + + var m map[string]interface{} + err := r.ExtractInto(&m) + if err != nil { + return err + } + + b, err := json.Marshal(m[label]) + if err != nil { + return err + } + + toValue := reflect.ValueOf(to) + if toValue.Kind() == reflect.Ptr { + toValue = toValue.Elem() + } + + switch toValue.Kind() { + case reflect.Slice: + typeOfV := toValue.Type().Elem() + if typeOfV.Kind() == reflect.Struct { + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0) + + for _, v := range m[label].([]interface{}) { + // For each iteration of the slice, we create a new struct. + // This is to work around a bug where elements of a slice + // are reused and not overwritten when the same copy of the + // struct is used: + // + // https://github.com/golang/go/issues/21092 + // https://github.com/golang/go/issues/24155 + // https://play.golang.org/p/NHo3ywlPZli + newType := reflect.New(typeOfV).Elem() + + b, err := json.Marshal(v) + if err != nil { + return err + } + + // This is needed for structs with an UnmarshalJSON method. + // Technically this is just unmarshalling the response into + // a struct that is never used, but it's good enough to + // trigger the UnmarshalJSON method. + for i := 0; i < newType.NumField(); i++ { + s := newType.Field(i).Addr().Interface() + + // Unmarshal is used rather than NewDecoder to also work + // around the above-mentioned bug. + err = json.Unmarshal(b, s) + if err != nil { + return err + } + } + + newSlice = reflect.Append(newSlice, newType) + } + + // "to" should now be properly modeled to receive the + // JSON response body and unmarshal into all the correct + // fields of the struct or composed extension struct + // at the end of this method. + toValue.Set(newSlice) + } + } + case reflect.Struct: + typeOfV := toValue.Type() + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + for i := 0; i < toValue.NumField(); i++ { + toField := toValue.Field(i) + if toField.Kind() == reflect.Struct { + s := toField.Addr().Interface() + err = json.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return err + } + } + } + } + } + + err = json.Unmarshal(b, &to) + return err +} + +// ExtractIntoStructPtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying struct type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoStructPtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Struct: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("expected pointer to struct, got: %v", t) + } +} + +// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying slice type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Slice: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("expected pointer to slice, got: %v", t) + } +} + +// 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 +Enterprise Cloud, because most of the operations don't return response bodies, +but do have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractInto allows users to provide an object into which `Extract` will +// extract the http.Header headers of the result. +func (r HeaderResult) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + tmpHeaderMap := map[string]string{} + for k, v := range r.Header { + if len(v) > 0 { + tmpHeaderMap[k] = v[0] + } + } + + b, err := json.Marshal(tmpHeaderMap) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +// ISO8601 describes a common time format used by some API responses. +// Expecially in storage SDP of Enterprise Cloud 2.0 +const ISO8601 = "2006-01-02T15:04:05+0000" + +type JSONISO8601 time.Time + +func (jt *JSONISO8601) UnmarshalJSON(data []byte) error { + // log.Printf("[DEBUG] ISO8601::UnmarshalJSON") + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + + var s string + if err := dec.Decode(&s); err != nil { + return err + } + + t, _ := time.Parse(ISO8601, s) + *jt = JSONISO8601(t) + return nil +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +type JSONRFC3339Milli time.Time + +func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + t, err := time.Parse(RFC3339Milli, s) + if err != nil { + return err + } + *jt = JSONRFC3339Milli(t) + return nil +} + +const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" + +type JSONRFC3339MilliNoZ time.Time + +func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339MilliNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339MilliNoZ(t) + return nil +} + +type JSONRFC1123 time.Time + +func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(time.RFC1123, s) + if err != nil { + return err + } + *jt = JSONRFC1123(t) + return nil +} + +type JSONUnix time.Time + +func (jt *JSONUnix) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + unix, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t = time.Unix(unix, 0) + *jt = JSONUnix(t) + return nil +} + +// RFC3339NoZ is the time format used in Heat (Orchestration). +const RFC3339NoZ = "2006-01-02T15:04:05" + +type JSONRFC3339NoZ time.Time + +func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339NoZ(t) + return nil +} + +// RFC3339ZNoT is the time format used in Zun (Containers Service). +const RFC3339ZNoT = "2006-01-02 15:04:05-07:00" + +type JSONRFC3339ZNoT time.Time + +func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoT, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoT(t) + return nil +} + +// RFC3339ZNoTNoZ is another time format used in Zun (Containers Service). +const RFC3339ZNoTNoZ = "2006-01-02 15:04:05" + +type JSONRFC3339ZNoTNoZ time.Time + +func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoTNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoTNoZ(t) + return nil +} + +/* +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 `json:"href"` + Rel string `json:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It 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/v4/service_client.go b/v4/service_client.go new file mode 100644 index 0000000..9cddab7 --- /dev/null +++ b/v4/service_client.go @@ -0,0 +1,146 @@ +package eclcloud + +import ( + "io" + "net/http" + "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 + + // This is the service client type (e.g. compute, network). + Type string + + // The microversion of the service to use. Set this to use a particular microversion. + Microversion string + + // MoreHeaders allows users (or Eclcloud) to set service-wide headers on requests. Put another way, + // values set in this field will be set on all the HTTP requests the service client sends. + MoreHeaders map[string]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, "/") +} + +func (client *ServiceClient) initReqOpts(url string, JSONBody interface{}, JSONResponse interface{}, 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 + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + + if client.Microversion != "" { + client.setMicroversionHeader(opts) + } +} + +// Get calls `Request` with the "GET" HTTP verb. +func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("GET", url, opts) +} + +// Post calls `Request` with the "POST" HTTP verb. +func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("POST", url, opts) +} + +// Put calls `Request` with the "PUT" HTTP verb. +func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PUT", url, opts) +} + +// Patch calls `Request` with the "PATCH" HTTP verb. +func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PATCH", url, opts) +} + +// Delete calls `Request` with the "DELETE" HTTP verb. +func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("DELETE", url, opts) +} + +// Head calls `Request` with the "HEAD" HTTP verb. +func (client *ServiceClient) Head(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("HEAD", url, opts) +} + +func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + switch client.Type { + case "compute": + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + case "volume": + opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion + } + + if client.Type != "" { + opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + } +} + +// Request carries out the HTTP operation for the service client +func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + if len(client.MoreHeaders) > 0 { + if options == nil { + options = new(RequestOpts) + } + for k, v := range client.MoreHeaders { + options.MoreHeaders[k] = v + } + } + return client.ProviderClient.Request(method, url, options) +} diff --git a/v4/testhelper/client/fake.go b/v4/testhelper/client/fake.go new file mode 100644 index 0000000..0762084 --- /dev/null +++ b/v4/testhelper/client/fake.go @@ -0,0 +1,17 @@ +package client + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/testhelper" +) + +// Fake token to use. +const TokenID = "cbc36478b0bd8e67e89469c7749d4127" + +// ServiceClient returns a generic service client for use in tests. +func ServiceClient() *eclcloud.ServiceClient { + return &eclcloud.ServiceClient{ + ProviderClient: &eclcloud.ProviderClient{TokenID: TokenID}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/v4/testhelper/convenience.go b/v4/testhelper/convenience.go new file mode 100644 index 0000000..25f6720 --- /dev/null +++ b/v4/testhelper/convenience.go @@ -0,0 +1,348 @@ +package testhelper + +import ( + "bytes" + "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 := actual.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)) + }) +} + +func isByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) bool { + return bytes.Equal(expectedBytes, actualBytes) +} + +// AssertByteArrayEquals a convenience function for checking whether two byte arrays are equal +func AssertByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) { + if !isByteArrayEquals(t, expectedBytes, actualBytes) { + logFatal(t, "The bytes differed.") + } +} + +// CheckByteArrayEquals a convenience function for silent checking whether two byte arrays are equal +func CheckByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) { + if !isByteArrayEquals(t, expectedBytes, actualBytes) { + logError(t, "The bytes differed.") + } +} + +// 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/v4/testhelper/doc.go b/v4/testhelper/doc.go new file mode 100644 index 0000000..25b4dfe --- /dev/null +++ b/v4/testhelper/doc.go @@ -0,0 +1,4 @@ +/* +Package testhelper container methods that are useful for writing unit tests. +*/ +package testhelper diff --git a/v4/testhelper/fixture/helper.go b/v4/testhelper/fixture/helper.go new file mode 100644 index 0000000..94d8a07 --- /dev/null +++ b/v4/testhelper/fixture/helper.go @@ -0,0 +1,31 @@ +package fixture + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func SetupHandler(t *testing.T, url, method, requestBody, responseBody string, status int) { + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, method) + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + if requestBody != "" { + th.TestJSONRequest(t, r, requestBody) + } + + if responseBody != "" { + w.Header().Add("Content-Type", "application/json") + } + + w.WriteHeader(status) + + if responseBody != "" { + fmt.Fprintf(w, responseBody) + } + }) +} diff --git a/v4/testhelper/http_responses.go b/v4/testhelper/http_responses.go new file mode 100644 index 0000000..e1f1f9a --- /dev/null +++ b/v4/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/v4/testing/doc.go b/v4/testing/doc.go new file mode 100644 index 0000000..6336d11 --- /dev/null +++ b/v4/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing contains eclcloud tests. +package testing diff --git a/v4/testing/endpoint_search_test.go b/v4/testing/endpoint_search_test.go new file mode 100644 index 0000000..bdf29b0 --- /dev/null +++ b/v4/testing/endpoint_search_test.go @@ -0,0 +1,20 @@ +package testing + +import ( + "testing" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestApplyDefaultsToEndpointOpts(t *testing.T) { + eo := eclcloud.EndpointOpts{Availability: eclcloud.AvailabilityPublic} + eo.ApplyDefaults("compute") + expected := eclcloud.EndpointOpts{Availability: eclcloud.AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) + + eo = eclcloud.EndpointOpts{Type: "compute"} + eo.ApplyDefaults("object-store") + expected = eclcloud.EndpointOpts{Availability: eclcloud.AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) +} diff --git a/v4/testing/params_test.go b/v4/testing/params_test.go new file mode 100644 index 0000000..6e05822 --- /dev/null +++ b/v4/testing/params_test.go @@ -0,0 +1,276 @@ +package testing + +import ( + "net/url" + "reflect" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestMaybeString(t *testing.T) { + testString := "" + var expected *string + actual := eclcloud.MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) + + testString = "carol" + expected = &testString + actual = eclcloud.MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) +} + +func TestMaybeInt(t *testing.T) { + testInt := 0 + var expected *int + actual := eclcloud.MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) + + testInt = 4 + expected = &testInt + actual = eclcloud.MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) +} + +func TestBuildQueryString(t *testing.T) { + type testVar string + iFalse := false + opts := struct { + J int `q:"j"` + R string `q:"r" required:"true"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + F *bool `q:"f"` + M map[string]string `q:"m"` + }{ + J: 2, + R: "red", + C: true, + S: []string{"one", "two", "three"}, + TS: []testVar{"a", "b"}, + TI: []int{1, 2}, + F: &iFalse, + M: map[string]string{"k1": "success1"}, + } + expected := &url.URL{RawQuery: "c=true&f=false&j=2&m=%7B%27k1%27%3A%27success1%27%7D&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"} + actual, err := eclcloud.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:"true"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + F *bool `q:"f"` + M map[string]string `q:"m"` + }{ + J: 2, + C: true, + } + _, err = eclcloud.BuildQueryString(&opts) + if err == nil { + t.Errorf("Expected error: 'Required field not set'") + } + th.CheckDeepEquals(t, expected, actual) + + _, err = eclcloud.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:"true"` + Style bool `h:"Style"` + }{ + Accept: "application/json", + Num: 4, + Style: true, + } + expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"} + actual, err := eclcloud.BuildHeaders(&testStruct) + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) + + testStruct.Num = 0 + _, err = eclcloud.BuildHeaders(&testStruct) + if err == nil { + t.Errorf("Expected error: 'Required header not set'") + } + + _, err = eclcloud.BuildHeaders(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +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 := eclcloud.BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"}) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestBuildRequestBody(t *testing.T) { + type PasswordCredentials struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + } + + type TokenCredentials struct { + ID string `json:"id,omitempty" required:"true"` + } + + type orFields struct { + Filler int `json:"filler,omitempty"` + F1 int `json:"f1,omitempty" or:"F2"` + F2 int `json:"f2,omitempty" or:"F1"` + } + + // AuthOptions wraps a eclcloud AuthOptions in order to adhere to the AuthOptionsBuilder + // interface. + type AuthOptions struct { + PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // 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 string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenCredentials *TokenCredentials `json:"token,omitempty" xor:"PasswordCredentials"` + + OrFields *orFields `json:"or_fields,omitempty"` + } + + var successCases = []struct { + opts AuthOptions + expected map[string]interface{} + }{ + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + }, + map[string]interface{}{ + "auth": map[string]interface{}{ + "passwordCredentials": map[string]interface{}{ + "password": "swordfish", + "username": "me", + }, + }, + }, + }, + { + AuthOptions{ + TokenCredentials: &TokenCredentials{ + ID: "1234567", + }, + }, + map[string]interface{}{ + "auth": map[string]interface{}{ + "token": map[string]interface{}{ + "id": "1234567", + }, + }, + }, + }, + } + + for _, successCase := range successCases { + actual, err := eclcloud.BuildRequestBody(successCase.opts, "auth") + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, successCase.expected, actual) + } + + var failCases = []struct { + opts AuthOptions + expected error + }{ + { + AuthOptions{ + TenantID: "987654321", + TenantName: "me", + }, + eclcloud.ErrMissingInput{}, + }, + { + AuthOptions{ + TokenCredentials: &TokenCredentials{ + ID: "1234567", + }, + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + }, + eclcloud.ErrMissingInput{}, + }, + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Password: "swordfish", + }, + }, + eclcloud.ErrMissingInput{}, + }, + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + OrFields: &orFields{ + Filler: 2, + }, + }, + eclcloud.ErrMissingInput{}, + }, + } + + for _, failCase := range failCases { + _, err := eclcloud.BuildRequestBody(failCase.opts, "auth") + th.AssertDeepEquals(t, reflect.TypeOf(failCase.expected), reflect.TypeOf(err)) + } + + createdAt := time.Date(2018, 1, 4, 10, 00, 12, 0, time.UTC) + var complexFields = struct { + Username string `json:"username" required:"true"` + CreatedAt *time.Time `json:"-"` + }{ + Username: "jdoe", + CreatedAt: &createdAt, + } + + expectedComplexFields := map[string]interface{}{ + "username": "jdoe", + } + + actual, err := eclcloud.BuildRequestBody(complexFields, "") + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expectedComplexFields, actual) + +} diff --git a/v4/testing/provider_client_test.go b/v4/testing/provider_client_test.go new file mode 100644 index 0000000..02f15c4 --- /dev/null +++ b/v4/testing/provider_client_test.go @@ -0,0 +1,155 @@ +package testing + +import ( + "fmt" + "io/ioutil" + "net/http" + "reflect" + "sync" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" + "github.com/nttcom/eclcloud/v4/testhelper/client" +) + +func TestAuthenticatedHeaders(t *testing.T) { + p := &eclcloud.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 := &eclcloud.ProviderClient{} + + p.UserAgent.Prepend("custom-user-agent/2.4.0") + expected := "custom-user-agent/2.4.0 eclcloud/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 eclcloud/1.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) + + p.UserAgent = eclcloud.UserAgent{} + expected = "eclcloud/1.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) +} + +func TestConcurrentReauth(t *testing.T) { + var info = struct { + numreauths int + mut *sync.RWMutex + }{ + 0, + new(sync.RWMutex), + } + + numconc := 20 + + prereauthTok := client.TokenID + postreauthTok := "12345678" + + p := new(eclcloud.ProviderClient) + p.UseTokenLock() + p.SetToken(prereauthTok) + p.ReauthFunc = func() error { + time.Sleep(1 * time.Second) + p.AuthenticatedHeaders() + info.mut.Lock() + info.numreauths++ + info.mut.Unlock() + p.TokenID = postreauthTok + return nil + } + + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("X-Auth-Token") != postreauthTok { + w.WriteHeader(http.StatusUnauthorized) + return + } + info.mut.RLock() + hasReauthed := info.numreauths != 0 + info.mut.RUnlock() + + if hasReauthed { + th.CheckEquals(t, p.Token(), postreauthTok) + } + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{}`) + }) + + wg := new(sync.WaitGroup) + reqopts := new(eclcloud.RequestOpts) + reqopts.MoreHeaders = map[string]string{ + "X-Auth-Token": prereauthTok, + } + + for i := 0; i < numconc; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := p.Request("GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts) + th.CheckNoErr(t, err) + if resp == nil { + t.Errorf("got a nil response") + return + } + if resp.Body == nil { + t.Errorf("response body was nil") + return + } + defer resp.Body.Close() + actual, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("error reading response body: %s", err) + return + } + th.CheckByteArrayEquals(t, []byte(`{}`), actual) + }() + } + + wg.Wait() + + th.AssertEquals(t, 1, info.numreauths) +} + +func TestReauthEndLoop(t *testing.T) { + + p := new(eclcloud.ProviderClient) + p.UseTokenLock() + p.SetToken(client.TokenID) + p.ReauthFunc = func() error { + // Reauth func is working and returns no error + return nil + } + + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + // route always return 401 + w.WriteHeader(http.StatusUnauthorized) + }) + + reqopts := new(eclcloud.RequestOpts) + _, err := p.Request("GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts) + if err == nil { + t.Errorf("request ends with a nil error") + return + } + + if reflect.TypeOf(err) != reflect.TypeOf(&eclcloud.ErrErrorAfterReauthentication{}) { + t.Errorf("error is not an ErrErrorAfterReauthentication") + } +} diff --git a/v4/testing/results_test.go b/v4/testing/results_test.go new file mode 100644 index 0000000..ed62ad4 --- /dev/null +++ b/v4/testing/results_test.go @@ -0,0 +1,208 @@ +package testing + +import ( + "encoding/json" + "testing" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +var singleResponse = ` +{ + "person": { + "name": "Bill", + "email": "bill@example.com", + "location": "Canada" + } +} +` + +var multiResponse = ` +{ + "people": [ + { + "name": "Bill", + "email": "bill@example.com", + "location": "Canada" + }, + { + "name": "Ted", + "email": "ted@example.com", + "location": "Mexico" + } + ] +} +` + +type TestPerson struct { + Name string `json:"-"` + Email string `json:"email"` +} + +func (r *TestPerson) UnmarshalJSON(b []byte) error { + type tmp TestPerson + var s struct { + tmp + Name string `json:"name"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = TestPerson(s.tmp) + r.Name = s.Name + " unmarshalled" + + return nil +} + +type TestPersonExt struct { + Location string `json:"-"` +} + +func (r *TestPersonExt) UnmarshalJSON(b []byte) error { + type tmp TestPersonExt + var s struct { + tmp + Location string `json:"location"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = TestPersonExt(s.tmp) + r.Location = s.Location + " unmarshalled" + + return nil +} + +type TestPersonWithExtensions struct { + TestPerson + TestPersonExt +} + +type TestPersonWithExtensionsNamed struct { + TestPerson TestPerson + TestPersonExt TestPersonExt +} + +// TestUnmarshalAnonymousStruct tests if UnmarshalJSON is called on each +// of the anonymous structs contained in an overarching struct. +func TestUnmarshalAnonymousStructs(t *testing.T) { + var actual TestPersonWithExtensions + + var dejson interface{} + sejson := []byte(singleResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var singleResult = eclcloud.Result{ + Body: dejson, + } + + err = singleResult.ExtractIntoStructPtr(&actual, "person") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "Bill unmarshalled", actual.Name) + th.AssertEquals(t, "Canada unmarshalled", actual.Location) +} + +// TestUnmarshalSliceofAnonymousStructs tests if UnmarshalJSON is called on each +// of the anonymous structs contained in an overarching struct slice. +func TestUnmarshalSliceOfAnonymousStructs(t *testing.T) { + var actual []TestPersonWithExtensions + + var dejson interface{} + sejson := []byte(multiResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var multiResult = eclcloud.Result{ + Body: dejson, + } + + err = multiResult.ExtractIntoSlicePtr(&actual, "people") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "Bill unmarshalled", actual[0].Name) + th.AssertEquals(t, "Canada unmarshalled", actual[0].Location) + th.AssertEquals(t, "Ted unmarshalled", actual[1].Name) + th.AssertEquals(t, "Mexico unmarshalled", actual[1].Location) +} + +// TestUnmarshalSliceOfStruct tests if extracting results from a "normal" +// struct still works correctly. +func TestUnmarshalSliceofStruct(t *testing.T) { + var actual []TestPerson + + var dejson interface{} + sejson := []byte(multiResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var multiResult = eclcloud.Result{ + Body: dejson, + } + + err = multiResult.ExtractIntoSlicePtr(&actual, "people") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "Bill unmarshalled", actual[0].Name) + th.AssertEquals(t, "Ted unmarshalled", actual[1].Name) +} + +// TestUnmarshalNamedStruct tests if the result is empty. +func TestUnmarshalNamedStructs(t *testing.T) { + var actual TestPersonWithExtensionsNamed + + var dejson interface{} + sejson := []byte(singleResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var singleResult = eclcloud.Result{ + Body: dejson, + } + + err = singleResult.ExtractIntoStructPtr(&actual, "person") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", actual.TestPerson.Name) + th.AssertEquals(t, "", actual.TestPersonExt.Location) +} + +// TestUnmarshalSliceofNamedStructs tests if the result is empty. +func TestUnmarshalSliceOfNamedStructs(t *testing.T) { + var actual []TestPersonWithExtensionsNamed + + var dejson interface{} + sejson := []byte(multiResponse) + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatal(err) + } + + var multiResult = eclcloud.Result{ + Body: dejson, + } + + err = multiResult.ExtractIntoSlicePtr(&actual, "people") + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", actual[0].TestPerson.Name) + th.AssertEquals(t, "", actual[0].TestPersonExt.Location) + th.AssertEquals(t, "", actual[1].TestPerson.Name) + th.AssertEquals(t, "", actual[1].TestPersonExt.Location) +} diff --git a/v4/testing/service_client_test.go b/v4/testing/service_client_test.go new file mode 100644 index 0000000..cec9713 --- /dev/null +++ b/v4/testing/service_client_test.go @@ -0,0 +1,34 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestServiceURL(t *testing.T) { + c := &eclcloud.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) +} + +func TestMoreHeaders(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + c := new(eclcloud.ServiceClient) + c.MoreHeaders = map[string]string{ + "custom": "header", + } + c.ProviderClient = new(eclcloud.ProviderClient) + resp, err := c.Get(fmt.Sprintf("%s/route", th.Endpoint()), nil, nil) + th.AssertNoErr(t, err) + th.AssertEquals(t, resp.Request.Header.Get("custom"), "header") +} diff --git a/v4/testing/util_test.go b/v4/testing/util_test.go new file mode 100644 index 0000000..6a6ca03 --- /dev/null +++ b/v4/testing/util_test.go @@ -0,0 +1,121 @@ +package testing + +import ( + "errors" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nttcom/eclcloud/v4" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestWaitFor(t *testing.T) { + err := eclcloud.WaitFor(2, func() (bool, error) { + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestWaitForTimeout(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := eclcloud.WaitFor(1, func() (bool, error) { + return false, nil + }) + th.AssertEquals(t, "A timeout occurred", err.Error()) +} + +func TestWaitForError(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := eclcloud.WaitFor(2, func() (bool, error) { + return false, errors.New("error has occurred") + }) + th.AssertEquals(t, "error has occurred", err.Error()) +} + +func TestWaitForPredicateExceed(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := eclcloud.WaitFor(1, func() (bool, error) { + time.Sleep(4 * time.Second) + return false, errors.New("just wasting time") + }) + th.AssertEquals(t, "A timeout occurred", err.Error()) +} + +func TestNormalizeURL(t *testing.T) { + urls := []string{ + "NoSlashAtEnd", + "SlashAtEnd/", + } + expected := []string{ + "NoSlashAtEnd/", + "SlashAtEnd/", + } + for i := 0; i < len(expected); i++ { + th.CheckEquals(t, expected[i], eclcloud.NormalizeURL(urls[i])) + } + +} + +func TestNormalizePathURL(t *testing.T) { + baseDir := "/test/path" + + rawPath := "template.yaml" + basePath := "/test/path" + result, _ := eclcloud.NormalizePathURL(basePath, rawPath) + expected := strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "template.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "http://www.google.com" + basePath = "/test/path" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "/test/path" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "http://www.google.com" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml/" + basePath = "http://www.google.com/" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "http://www.google.com/even/more" + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/even/more/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/") + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml/" + basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/") + result, _ = eclcloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + +} diff --git a/v4/util.go b/v4/util.go new file mode 100644 index 0000000..5726475 --- /dev/null +++ b/v4/util.go @@ -0,0 +1,99 @@ +package eclcloud + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// This is useful to wait for a resource to transition to a certain state. +// To handle situations when the predicate might hang indefinitely, the +// predicate will be prematurely cancelled after the timeout. +// 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 { + type WaitForResult struct { + Success bool + Error error + } + + start := time.Now().Unix() + + for { + // If a timeout is set, and that's been exceeded, shut it down. + if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) { + return fmt.Errorf("A timeout occurred") + } + + time.Sleep(1 * time.Second) + + var result WaitForResult + ch := make(chan bool, 1) + go func() { + defer close(ch) + satisfied, err := predicate() + result.Success = satisfied + result.Error = err + }() + + select { + case <-ch: + if result.Error != nil { + return result.Error + } + if result.Success { + return nil + } + // If the predicate has not finished by the timeout, cancel it. + case <-time.After(time.Duration(timeout) * time.Second): + return fmt.Errorf("A timeout occurred") + } + } +} + +// 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 +} + +// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as +// a reference in the filesystem, if necessary. basePath is assumed to contain +// either '.' when first used, or the file:// type fqdn of the parent resource. +// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml +func NormalizePathURL(basePath, rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", err + } + // if a scheme is defined, it must be a fqdn already + if u.Scheme != "" { + return u.String(), nil + } + // if basePath is a url, then child resources are assumed to be relative to it + bu, err := url.Parse(basePath) + if err != nil { + return "", err + } + var basePathSys, absPathSys string + if bu.Scheme != "" { + basePathSys = filepath.FromSlash(bu.Path) + absPathSys = filepath.Join(basePathSys, rawPath) + bu.Path = filepath.ToSlash(absPathSys) + return bu.String(), nil + } + + absPathSys = filepath.Join(basePath, rawPath) + u.Path = filepath.ToSlash(absPathSys) + u.Scheme = "file" + return u.String(), nil + +}