diff --git a/blockdevice.go b/blockdevice.go index b107635..b353979 100644 --- a/blockdevice.go +++ b/blockdevice.go @@ -4,6 +4,9 @@ package gomaasapi import ( + "fmt" + "net/http" + "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" @@ -11,6 +14,7 @@ import ( type blockdevice struct { resourceURI string + controller *controller id int uuid string @@ -29,6 +33,24 @@ type blockdevice struct { partitions []*partition } +func (b *blockdevice) updateFrom(other *blockdevice) { + b.resourceURI = other.resourceURI + b.controller = other.controller + b.id = other.id + b.uuid = other.uuid + b.name = other.name + b.model = other.model + b.idPath = other.idPath + b.path = other.path + b.usedFor = other.usedFor + b.tags = other.tags + b.blockSize = other.blockSize + b.usedSize = other.usedSize + b.size = other.size + b.filesystem = other.filesystem + b.partitions = other.partitions +} + // Type implements BlockDevice func (b *blockdevice) Type() string { return "blockdevice" @@ -98,19 +120,135 @@ func (b *blockdevice) FileSystem() FileSystem { func (b *blockdevice) Partitions() []Partition { result := make([]Partition, len(b.partitions)) for i, v := range b.partitions { + v.controller = b.controller result[i] = v } return result } -func readBlockDevices(controllerVersion version.Number, source interface{}) ([]*blockdevice, error) { - checker := schema.List(schema.StringMap(schema.Any())) - coerced, err := checker.Coerce(source, nil) +// FormatStorageDeviceArgs are options for formatting BlockDevices and Partitions +type FormatStorageDeviceArgs struct { + FSType string // Required. Type of filesystem. + UUID string // Optional. The UUID for the filesystem. + Label string // Optional. The label for the filesystem, only applies to partitions. +} + +// Validate ensures correct args +func (a *FormatStorageDeviceArgs) Validate() error { + if a.FSType == "" { + return fmt.Errorf("A filesystem type must be specified") + } + + return nil +} + +func (b *blockdevice) Format(args FormatStorageDeviceArgs) error { + if err := args.Validate(); err != nil { + return errors.Trace(err) + } + + params := NewURLParams() + params.MaybeAdd("fs_type", args.FSType) + params.MaybeAdd("uuid", args.UUID) + + result, err := b.controller.post(b.resourceURI, "format", params.Values) if err != nil { - return nil, WrapWithDeserializationError(err, "blockdevice base schema check failed") + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + case http.StatusForbidden: + return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + case http.StatusServiceUnavailable: + return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) + } + } + return NewUnexpectedError(err) } - valid := coerced.([]interface{}) + blockDevice, err := readBlockDevice(b.controller.apiVersion, result) + if err != nil { + return errors.Trace(err) + } + b.updateFrom(blockDevice) + return nil +} + +// CreatePartitionArgs options for creating partitions +type CreatePartitionArgs struct { + Size int // Optional. The size of the partition. If not specified, all available space will be used. + UUID string // Optional. UUID for the partition. Only used if the partition table type for the block device is GPT. + Bootable bool // Optional. If the partition should be marked bootable. +} + +func (a *CreatePartitionArgs) toParams() *URLParams { + params := NewURLParams() + params.MaybeAddInt("size", a.Size) + params.MaybeAdd("uuid", a.UUID) + params.MaybeAddBool("bootable", a.Bootable) + return params +} + +func (b *blockdevice) CreatePartition(args CreatePartitionArgs) (Partition, error) { + params := args.toParams() + source, err := b.controller.post(b.resourceURI+"partitions/", "", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) + case http.StatusForbidden: + return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + } + } + return nil, NewUnexpectedError(err) + } + + response, err := readPartition(b.controller.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + response.controller = b.controller + return response, nil +} + +// MountStorageDeviceArgs options for creating partitions +type MountStorageDeviceArgs struct { + MountPoint string // Required. Path on the filesystem to mount. + MountOptions string // Optional. Options to pass to mount(8). +} + +func (a *MountStorageDeviceArgs) toParams() *URLParams { + params := NewURLParams() + params.MaybeAdd("mount_point", a.MountPoint) + params.MaybeAdd("mount_options", a.MountOptions) + return params +} + +func (b *blockdevice) Mount(args MountStorageDeviceArgs) error { + params := args.toParams() + source, err := b.controller.post(b.resourceURI, "mount", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) + case http.StatusForbidden: + return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + } + } + return NewUnexpectedError(err) + } + + response, err := readBlockDevice(b.controller.apiVersion, source) + if err != nil { + return errors.Trace(err) + } + b.updateFrom(response) + return nil +} + +func getBlockDeviceDeserializationFunc(controllerVersion version.Number) (blockdeviceDeserializationFunc, error) { var deserialisationVersion version.Number for v := range blockdeviceDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { @@ -120,7 +258,37 @@ func readBlockDevices(controllerVersion version.Number, source interface{}) ([]* if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no blockdevice read func for version %s", controllerVersion) } - readFunc := blockdeviceDeserializationFuncs[deserialisationVersion] + return blockdeviceDeserializationFuncs[deserialisationVersion], nil +} + +func readBlockDevice(controllerVersion version.Number, source interface{}) (*blockdevice, error) { + readFunc, err := getBlockDeviceDeserializationFunc(controllerVersion) + if err != nil { + return nil, err + } + + checker := schema.StringMap(schema.Any()) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, WrapWithDeserializationError(err, "machine base schema check failed") + } + valid := coerced.(map[string]interface{}) + + return readFunc(valid) +} + +func readBlockDevices(controllerVersion version.Number, source interface{}) ([]*blockdevice, error) { + checker := schema.List(schema.StringMap(schema.Any())) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, WrapWithDeserializationError(err, "blockdevice base schema check failed") + } + valid := coerced.([]interface{}) + + readFunc, err := getBlockDeviceDeserializationFunc(controllerVersion) + if err != nil { + return nil, err + } return readBlockDeviceList(valid, readFunc) } @@ -153,19 +321,19 @@ func blockdevice_2_0(source map[string]interface{}) (*blockdevice, error) { "id": schema.ForceInt(), "uuid": schema.OneOf(schema.Nil(""), schema.String()), - "name": schema.String(), + "name": schema.OneOf(schema.Nil(""), schema.String()), "model": schema.OneOf(schema.Nil(""), schema.String()), "id_path": schema.OneOf(schema.Nil(""), schema.String()), "path": schema.String(), "used_for": schema.String(), - "tags": schema.List(schema.String()), + "tags": schema.OneOf(schema.Nil(""), schema.List(schema.String())), - "block_size": schema.ForceUint(), - "used_size": schema.ForceUint(), + "block_size": schema.OneOf(schema.Nil(""), schema.ForceUint()), + "used_size": schema.OneOf(schema.Nil(""), schema.ForceUint()), "size": schema.ForceUint(), "filesystem": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())), - "partitions": schema.List(schema.StringMap(schema.Any())), + "partitions": schema.OneOf(schema.Nil(""), schema.List(schema.StringMap(schema.Any()))), } checker := schema.FieldMap(fields, nil) coerced, err := checker.Coerce(source, nil) @@ -182,28 +350,36 @@ func blockdevice_2_0(source map[string]interface{}) (*blockdevice, error) { return nil, errors.Trace(err) } } - partitions, err := readPartitionList(valid["partitions"].([]interface{}), partition_2_0) - if err != nil { - return nil, errors.Trace(err) + + partitions := []*partition{} + if valid["partitions"] != nil { + var err error + partitions, err = readPartitionList(valid["partitions"].([]interface{}), partition_2_0) + if err != nil { + return nil, errors.Trace(err) + } } uuid, _ := valid["uuid"].(string) model, _ := valid["model"].(string) idPath, _ := valid["id_path"].(string) + name, _ := valid["name"].(string) + blockSize, _ := valid["block_size"].(uint64) + usedSize, _ := valid["used_size"].(uint64) result := &blockdevice{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), uuid: uuid, - name: valid["name"].(string), + name: name, model: model, idPath: idPath, path: valid["path"].(string), usedFor: valid["used_for"].(string), tags: convertToStringSlice(valid["tags"]), - blockSize: valid["block_size"].(uint64), - usedSize: valid["used_size"].(uint64), + blockSize: blockSize, + usedSize: usedSize, size: valid["size"].(uint64), filesystem: filesystem, diff --git a/client.go b/client.go index eab8d87..4d7855d 100644 --- a/client.go +++ b/client.go @@ -12,27 +12,18 @@ import ( "net/http" "net/url" "regexp" - "strconv" "strings" - "time" + "github.com/hashicorp/go-retryablehttp" "github.com/juju/errors" ) -const ( - // Number of retries performed when the server returns a 503 - // response with a 'Retry-after' header. A request will be issued - // at most NumberOfRetries + 1 times. - NumberOfRetries = 4 - - RetryAfterHeaderName = "Retry-After" -) - // Client represents a way to communicating with a MAAS API instance. // It is stateless, so it can have concurrent requests in progress. type Client struct { - APIURL *url.URL - Signer OAuthSigner + APIURL *url.URL + Signer OAuthSigner + httpClient *retryablehttp.Client } // ServerError is an http error (or at least, a non-2xx result) received from @@ -70,48 +61,14 @@ func readAndClose(stream io.ReadCloser) ([]byte, error) { // returned error will be ServerError and the returned body will reflect the // server's response. If the server returns a 503 response with a 'Retry-after' // header, the request will be transparenty retried. -func (client Client) dispatchRequest(request *http.Request) ([]byte, error) { - // First, store the request's body into a byte[] to be able to restore it - // after each request. - bodyContent, err := readAndClose(request.Body) - if err != nil { - return nil, err - } - for retry := 0; retry < NumberOfRetries; retry++ { - // Restore body before issuing request. - newBody := ioutil.NopCloser(bytes.NewReader(bodyContent)) - request.Body = newBody - body, err := client.dispatchSingleRequest(request) - // If this is a 503 response with a non-void "Retry-After" header: wait - // as instructed and retry the request. - if err != nil { - serverError, ok := errors.Cause(err).(ServerError) - if ok && serverError.StatusCode == http.StatusServiceUnavailable { - retry_time_int, errConv := strconv.Atoi(serverError.Header.Get(RetryAfterHeaderName)) - if errConv == nil { - select { - case <-time.After(time.Duration(retry_time_int) * time.Second): - } - continue - } - } - } - return body, err - } - // Restore body before issuing request. - newBody := ioutil.NopCloser(bytes.NewReader(bodyContent)) - request.Body = newBody - return client.dispatchSingleRequest(request) -} +func (client Client) dispatchRequest(request *retryablehttp.Request) ([]byte, error) { + client.Signer.OAuthSign(&request.Header) -func (client Client) dispatchSingleRequest(request *http.Request) ([]byte, error) { - client.Signer.OAuthSign(request) - httpClient := http.Client{} // See https://code.google.com/p/go/issues/detail?id=4677 // We need to force the connection to close each time so that we don't // hit the above Go bug. request.Close = true - response, err := httpClient.Do(request) + response, err := client.httpClient.Do(request) if err != nil { return nil, err } @@ -148,9 +105,9 @@ func (client Client) Get(uri *url.URL, operation string, parameters url.Values) if operation != "" { parameters.Set("op", operation) } - queryUrl := client.GetURL(uri) - queryUrl.RawQuery = parameters.Encode() - request, err := http.NewRequest("GET", queryUrl.String(), nil) + queryURL := client.GetURL(uri) + queryURL.RawQuery = parameters.Encode() + request, err := retryablehttp.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } @@ -204,7 +161,7 @@ func (client Client) nonIdempotentRequestFiles(method string, uri *url.URL, para } writer.Close() url := client.GetURL(uri) - request, err := http.NewRequest(method, url.String(), buf) + request, err := retryablehttp.NewRequest(method, url.String(), buf) if err != nil { return nil, err } @@ -217,7 +174,7 @@ func (client Client) nonIdempotentRequestFiles(method string, uri *url.URL, para // requests (but not GET or DELETE requests). func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values) ([]byte, error) { url := client.GetURL(uri) - request, err := http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode()))) + request, err := retryablehttp.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode()))) if err != nil { return nil, err } @@ -245,7 +202,7 @@ func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) { // Delete deletes an object on the API, using an HTTP "DELETE" request. func (client Client) Delete(uri *url.URL) error { url := client.GetURL(uri) - request, err := http.NewRequest("DELETE", url.String(), strings.NewReader("")) + request, err := retryablehttp.NewRequest("DELETE", url.String(), strings.NewReader("")) if err != nil { return err } @@ -259,7 +216,7 @@ func (client Client) Delete(uri *url.URL) error { // Anonymous "signature method" implementation. type anonSigner struct{} -func (signer anonSigner) OAuthSign(request *http.Request) error { +func (signer anonSigner) OAuthSign(request *http.Header) error { return nil } @@ -330,5 +287,16 @@ func NewAuthenticatedClient(versionedURL, apiKey string) (*Client, error) { if err != nil { return nil, err } - return &Client{Signer: signer, APIURL: parsedURL}, nil + + httpClient := retryablehttp.NewClient() + + // Need to re-sign the request before each retry + httpClient.RequestLogHook = func(logger retryablehttp.Logger, request *http.Request, count int) { + err := signer.OAuthSign(&request.Header) + if err != nil { + logger.Printf("[ERROR] Failed to sign request: %v", err) + } + } + + return &Client{Signer: signer, APIURL: parsedURL, httpClient: httpClient}, nil } diff --git a/controller.go b/controller.go index 912e4c4..d3d9e63 100644 --- a/controller.go +++ b/controller.go @@ -335,6 +335,67 @@ func (c *controller) CreateDevice(args CreateDeviceArgs) (Device, error) { return device, nil } +// CreateMachineArgs is a argument struct for passing information into CreateDevice. +type CreateMachineArgs struct { + UpdateMachineArgs + Architecture string + Description string + Commission bool + MACAddresses []string +} + +// Validate ensures the arguments are acceptable +func (a *CreateMachineArgs) Validate() error { + if err := a.UpdateMachineArgs.Validate(); err != nil { + return err + } + if len(a.MACAddresses) == 0 { + return fmt.Errorf("at least one MAC address must be specified") + } + + return nil +} + +// ToParams converts arguments to URL parameters +func (a *CreateMachineArgs) ToParams() *URLParams { + params := a.UpdateMachineArgs.ToParams() + params.MaybeAdd("architecture", a.Architecture) + params.MaybeAdd("description", a.Description) + params.MaybeAddMany("mac_addresses", a.MACAddresses) + if a.Commission { + params.MaybeAdd("commission", "true") + } else { + params.MaybeAdd("commission", "false") + } + return params +} + +// CreateMachine implements Controller. +func (c *controller) CreateMachine(args CreateMachineArgs) (Machine, error) { + // There must be at least one mac address. + if err := args.Validate(); err != nil { + return nil, errors.NewBadRequest(err, "Invalid CreateMachine arguments") + } + params := args.ToParams() + result, err := c.post("machines", "", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + if svrErr.StatusCode == http.StatusBadRequest { + return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + } + } + // Translate http errors. + return nil, NewUnexpectedError(err) + } + + machine, err := readMachine(c.apiVersion, result) + if err != nil { + return nil, errors.Trace(err) + } + machine.controller = c + return machine, nil +} + // MachinesArgs is a argument struct for selecting Machines. // Only machines that match the specified criteria are returned. type MachinesArgs struct { @@ -378,6 +439,19 @@ func (c *controller) Machines(args MachinesArgs) ([]Machine, error) { return result, nil } +func (c *controller) GetMachine(systemID string) (Machine, error) { + source, err := c.getQuery("machines/"+systemID, url.Values{}) + if err != nil { + return nil, NewUnexpectedError(err) + } + m, err := readMachine(c.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + m.controller = c + return m, nil +} + func ownerDataMatches(ownerData, filter map[string]string) bool { for key, value := range filter { if ownerData[key] != value { @@ -617,8 +691,12 @@ func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, Constra // ReleaseMachinesArgs is an argument struct for passing the machine system IDs // and an optional comment into the ReleaseMachines method. type ReleaseMachinesArgs struct { - SystemIDs []string - Comment string + SystemIDs []string + Comment string + Erase bool + SecureErase bool + QuickErase bool + Force bool } // ReleaseMachines implements Controller. @@ -631,6 +709,10 @@ func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error { params := NewURLParams() params.MaybeAddMany("machines", args.SystemIDs) params.MaybeAdd("comment", args.Comment) + params.MaybeAddBool("erase", args.Erase) + params.MaybeAddBool("secure_erase", args.SecureErase) + params.MaybeAddBool("quick_erase", args.QuickErase) + params.MaybeAddBool("force", args.Force) _, err := c.post("machines", "release", params.Values) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { @@ -754,6 +836,66 @@ func (c *controller) AddFile(args AddFileArgs) error { return nil } +func (c *controller) Tags() ([]Tag, error) { + source, err := c.getQuery("tags", url.Values{}) + if err != nil { + return nil, NewUnexpectedError(err) + } + tags, err := readTags(c.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + var result []Tag + for _, t := range tags { + t.controller = c + result = append(result, t) + } + return result, nil +} + +func (c *controller) GetTag(name string) (Tag, error) { + source, err := c.getQuery("tags/"+name, url.Values{}) + if err != nil { + return nil, NewUnexpectedError(err) + } + tag, err := readTag(c.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + tag.controller = c + return tag, nil +} + +// CreateTagArgs are creation parameters +type CreateTagArgs struct { + Name string + Comment string + Definition string +} + +// Validate ensures arguments are valid +func (a *CreateTagArgs) Validate() error { + if a.Name == "" { + return fmt.Errorf("Missing name value") + } + return nil +} + +func (c *controller) CreateTag(args CreateTagArgs) (Tag, error) { + if err := args.Validate(); err != nil { + return nil, err + } + params := NewURLParams() + params.MaybeAdd("name", args.Name) + params.MaybeAdd("comment", args.Comment) + params.MaybeAdd("definition", args.Definition) + result, err := c.post("tags", "", params.Values) + if err != nil { + return nil, err + } + return readTag(c.apiVersion, result) +} + func (c *controller) checkCreds() error { if _, err := c.getOp("users", "whoami"); err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { diff --git a/dependencies.tsv b/dependencies.tsv deleted file mode 100644 index e1143b8..0000000 --- a/dependencies.tsv +++ /dev/null @@ -1,13 +0,0 @@ -github.com/juju/collections git 520e0549d51ae2b50f44f4df2b145a780a5bc6e0 2018-05-15T20:37:31Z -github.com/juju/errors git 1b5e39b83d1835fa480e0c2ddefb040ee82d58b3 2015-09-16T12:56:42Z -github.com/juju/loggo git 8232ab8918d91c72af1a9fb94d3edbe31d88b790 2017-06-05T01:46:07Z -github.com/juju/retry git 62c62032529169c7ec02fa48f93349604c345e1f 2015-10-29T02:48:21Z -github.com/juju/schema git 075de04f9b7d7580d60a1e12a0b3f50bb18e6998 2016-04-20T04:42:03Z -github.com/juju/testing git 44801989f0f7f280bd16b58e898ba9337807f147 2018-04-02T13:06:37Z -github.com/juju/utils git 2000ea4ff0431598aec2b7e1d11d5d49b5384d63 2018-04-24T09:41:59Z -github.com/juju/version git 1f41e27e54f21acccf9b2dddae063a782a8a7ceb 2016-10-31T05:19:06Z -golang.org/x/crypto git 650f4a345ab4e5b245a3034b110ebc7299e68186 2018-02-14T00:00:28Z -golang.org/x/net git 61147c48b25b599e5b561d2e9c4f3e1ef489ca41 2018-04-06T21:48:16Z -gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z -gopkg.in/mgo.v2 git f2b6f6c918c452ad107eec89615f074e3bd80e33 2016-08-18T01:52:18Z -gopkg.in/yaml.v2 git 1be3d31502d6eabc0dd7ce5b0daab022e14a5538 2017-07-12T05:45:46Z diff --git a/enum.go b/enum.go index a516d6b..1aaa218 100644 --- a/enum.go +++ b/enum.go @@ -54,4 +54,25 @@ const ( // The node failed to erase its disks. NodeStatusFailedDiskErasing = "15" + + // The node is in rescue mode. + NodeStatusRescueMode = "16" + + // The node is entering rescue mode. + NodeStatusEnteringRescueMode = "17" + + // The node failed to enter rescue mode. + NodeStatusFailedEnteringRescueMode = "18" + + // The node is exiting rescue mode. + NodeStatusExitingRescueMode = "19" + + // The node failed to exit rescue mode. + NodeStatusFailedExitingRescueMode = "20" + + // Running tests on Node + NodeStatusTesting = "21" + + // Testing has failed + NodeStatusFailedTesting = "22" ) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..62051a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/juju/gomaasapi + +go 1.13 + +require ( + github.com/hashicorp/go-retryablehttp v0.6.4 + github.com/juju/collections v0.0.0-20180515203731-520e0549d51a + github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18 + github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9 + github.com/juju/retry v0.0.0-20151029024821-62c620325291 // indirect + github.com/juju/schema v0.0.0-20160420044203-075de04f9b7d + github.com/juju/testing v0.0.0-20180402130637-44801989f0f7 + github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043 // indirect + github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2 + golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4 // indirect + golang.org/x/net v0.0.0-20180406214816-61147c48b25b // indirect + gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2 + gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4 + gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6 // indirect + launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8032d1d --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY= +github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/juju/collections v0.0.0-20180515203731-520e0549d51a h1:PPCCWrZzJMhFu4PxX3vRM65dq7LZMyreWAMPsvttUQk= +github.com/juju/collections v0.0.0-20180515203731-520e0549d51a/go.mod h1:Ep+c0vnxsgmmTtsMibPgEEleZyi0b4uVvyzJ+8ka9EI= +github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18 h1:Sem5Flzxj8ZdAgY2wfHBUlOYyP4wrpIfM8IZgANNGh8= +github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9 h1:Y+lzErDTURqeXqlqYi4YBYbDd7ycU74gW1ADt57/bgY= +github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/retry v0.0.0-20151029024821-62c620325291 h1:Rp0pLxDOsLDDwh2S73oHLI2KTFFyrF6oM/DgP0FhhBk= +github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/schema v0.0.0-20160420044203-075de04f9b7d h1:JYANSZLNBXFgnNfGDOUAV+atWFDmOqJ1WPNmyS+YCCw= +github.com/juju/schema v0.0.0-20160420044203-075de04f9b7d/go.mod h1:7dL+43wADDfx5rD9ibr5H9Dgr4iOM3uHOa1i4IVLak8= +github.com/juju/testing v0.0.0-20180402130637-44801989f0f7 h1:IOzyKRl+7X8/fDIqNUDQH73yo8bqDrMEh90y9Il158A= +github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043 h1:kjdsJcIYzmK2k4X2yVCi5Nip6sGoAuc7CLbp+qQnQUM= +github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2 h1:loQDi5MyxxNm7Q42mBGuPD6X+F6zw8j5S9yexLgn/BE= +github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4 h1:OfaUle5HH9Y0obNU74mlOZ/Igdtwi3eGOKcljJsTnbw= +golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180406214816-61147c48b25b h1:7rskAFQwNXGW6AD8E/6y0LDHW5mT9rsLD7ViLVFfh5w= +golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2 h1:+j1SppRob9bAgoYmsdW9NNBdKZfgYuWpqnYHv78Qt8w= +gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4 h1:hILp2hNrRnYjZpmIbx70psAHbBSEcQ1NIzDcUbJ1b6g= +gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6 h1:CvAnnm1XvMjfib69SZzDwgWfOk+PxYz0hA0HBupilBA= +gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= diff --git a/interface.go b/interface.go index 9865833..e59f010 100644 --- a/interface.go +++ b/interface.go @@ -113,9 +113,17 @@ func (i *interface_) EffectiveMTU() int { // UpdateInterfaceArgs is an argument struct for calling Interface.Update. type UpdateInterfaceArgs struct { - Name string - MACAddress string - VLAN VLAN + Name string + MACAddress string + VLAN VLAN + BridgeSTP bool + BridgeFD int + BondMiimon int + BondDownDelay int + BondUpDelay int + BondLACPRate string + BondXmitHashPolicy string + BondMode string } func (a *UpdateInterfaceArgs) vlanID() int { @@ -125,16 +133,32 @@ func (a *UpdateInterfaceArgs) vlanID() int { return a.VLAN.ID() } +func (a *UpdateInterfaceArgs) toParams() *URLParams { + params := NewURLParams() + params.MaybeAdd("name", a.Name) + params.MaybeAdd("mac_address", a.MACAddress) + params.MaybeAddInt("vlan", a.vlanID()) + if a.BridgeSTP { + params.MaybeAdd("bridge_stp", "1") + } + params.MaybeAddInt("bridge_fd", a.BridgeFD) + params.MaybeAddInt("bond_miimon ", a.BondMiimon) + params.MaybeAddInt("bond_down_delay", a.BondDownDelay) + params.MaybeAddInt("bond_up_delay", a.BondUpDelay) + params.MaybeAdd("bond_lacp_rate", a.BondLACPRate) + params.MaybeAdd("bond_xmit_hash_policy", a.BondXmitHashPolicy) + params.MaybeAdd("bond_mode", a.BondMode) + return params +} + // Update implements Interface. func (i *interface_) Update(args UpdateInterfaceArgs) error { var empty UpdateInterfaceArgs if args == empty { return nil } - params := NewURLParams() - params.MaybeAdd("name", args.Name) - params.MaybeAdd("mac_address", args.MACAddress) - params.MaybeAddInt("vlan", args.vlanID()) + + params := args.toParams() source, err := i.controller.put(i.resourceURI, params.Values) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { diff --git a/interfaces.go b/interfaces.go index ee3a225..ac225ca 100644 --- a/interfaces.go +++ b/interfaces.go @@ -44,6 +44,9 @@ type Controller interface { // Machines returns a list of machines that match the params. Machines(MachinesArgs) ([]Machine, error) + // GetMachine gets a single machine + GetMachine(systemID string) (Machine, error) + // AllocateMachine will attempt to allocate a machine to the user. // If successful, the allocated machine is returned. AllocateMachine(AllocateMachineArgs) (Machine, ConstraintMatches, error) @@ -52,6 +55,9 @@ type Controller interface { // from the user making them available to be allocated again. ReleaseMachines(ReleaseMachinesArgs) error + // CreateMachine will create a new machine with the provided parameters + CreateMachine(CreateMachineArgs) (Machine, error) + // Devices returns a list of devices that match the params. Devices(DevicesArgs) ([]Device, error) @@ -72,6 +78,15 @@ type Controller interface { // Returns the DNS Domain Managed By MAAS Domains() ([]Domain, error) + + // Returns the set of all tags + Tags() ([]Tag, error) + + // Retuns a aspecific tag or an error if it doesn't exist + GetTag(name string) (Tag, error) + + // Creates a new tag, or returns an error if the tag already exists + CreateTag(args CreateTagArgs) (Tag, error) } // File represents a file stored in the MAAS controller. @@ -204,10 +219,12 @@ type Machine interface { SystemID() string Hostname() string FQDN() string + Owner() string Tags() []string OperatingSystem() string DistroSeries() string + HWEKernel() string Architecture() string Memory() int CPUCount() int @@ -234,6 +251,13 @@ type Machine interface { // specified. If there is no match, nil is returned. Interface(id int) Interface + // Update allows editing of some of the machine's properties + Update(args UpdateMachineArgs) error + + // CreateBond creates a bond with the provided interfaces and returns the + // newly created bond interface. + CreateBond(args CreateMachineBondArgs) (Interface, error) + // PhysicalBlockDevices returns all the physical block devices on the machine. PhysicalBlockDevices() []BlockDevice // PhysicalBlockDevice returns the physical block device for the machine @@ -246,19 +270,34 @@ type Machine interface { // id specified. If there is no match, nil is returned. BlockDevice(id int) BlockDevice + // CreateBlockDevice will create a new block device + CreateBlockDevice(args CreateBlockDeviceArgs) (BlockDevice, error) + // Partition returns the partition for the machine that matches the // id specified. If there is no match, nil is returned. Partition(id int) Partition + // VolumeGroups returns all volume groups on the machine (dynamically loaded) + VolumeGroups() ([]VolumeGroup, error) + + // CreateVolumeGroup creates a volume group with the provided block devices and partitions + CreateVolumeGroup(args CreateVolumeGroupArgs) (VolumeGroup, error) + Zone() Zone Pool() Pool + // Commision makes a new node Ready + Commission(CommissionArgs) error + // Start the machine and install the operating system specified in the args. Start(StartArgs) error // CreateDevice creates a new Device with this Machine as the parent. // The device will have one interface that is linked to the specified subnet. CreateDevice(CreateMachineDeviceArgs) (Device, error) + + // Delete removes the machine from maas + Delete() error } // Space is a name for a collection of Subnets. @@ -378,6 +417,12 @@ type StorageDevice interface { // FileSystem may be nil if not mounted. FileSystem() FileSystem + + // Format places a filesystem on the block device + Format(FormatStorageDeviceArgs) error + + // Mount mounts device at a specific path + Mount(args MountStorageDeviceArgs) error } // Partition represents a partition of a block device. It may be mounted @@ -399,10 +444,22 @@ type BlockDevice interface { Partitions() []Partition + // CreatePartition creates a partition on the provided block device + CreatePartition(CreatePartitionArgs) (Partition, error) + // There are some other attributes for block devices, but we can // expose them on an as needed basis. } +// VolumeGroup represents a collection of logical volumes +type VolumeGroup interface { + Name() string + Size() uint64 + UUID() string + Devices() []BlockDevice + CreateLogicalVolume(CreateLogicalVolumeArgs) (BlockDevice, error) +} + // OwnerDataHolder represents any MAAS object that can store key/value // data. type OwnerDataHolder interface { @@ -417,3 +474,13 @@ type OwnerDataHolder interface { // released. SetOwnerData(map[string]string) error } + +// Tag represents a MaaS device tag +type Tag interface { + Name() string + Definition() string + Comment() string + Machines() ([]Machine, error) + AddToMachine(systemID string) error + RemoveFromMachine(systemID string) error +} diff --git a/machine.go b/machine.go index c2c5bdb..995f43f 100644 --- a/machine.go +++ b/machine.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/juju/errors" "github.com/juju/schema" @@ -21,11 +22,13 @@ type machine struct { systemID string hostname string fqdn string + owner string tags []string ownerData map[string]string operatingSystem string distroSeries string + hweKernel string architecture string memory int cpuCount int @@ -51,6 +54,7 @@ func (m *machine) updateFrom(other *machine) { m.systemID = other.systemID m.hostname = other.hostname m.fqdn = other.fqdn + m.owner = other.owner m.operatingSystem = other.operatingSystem m.distroSeries = other.distroSeries m.architecture = other.architecture @@ -64,6 +68,8 @@ func (m *machine) updateFrom(other *machine) { m.pool = other.pool m.tags = other.tags m.ownerData = other.ownerData + m.physicalBlockDevices = other.physicalBlockDevices + m.blockDevices = other.blockDevices } // SystemID implements Machine. @@ -81,6 +87,11 @@ func (m *machine) FQDN() string { return m.fqdn } +// Owner implements Machine. +func (m *machine) Owner() string { + return m.owner +} + // Tags implements Machine. func (m *machine) Tags() []string { return m.tags @@ -162,6 +173,11 @@ func (m *machine) DistroSeries() string { return m.distroSeries } +// HWEKernel implements Machine. +func (m *machine) HWEKernel() string { + return m.hweKernel +} + // Architecture implements Machine. func (m *machine) Architecture() string { return m.architecture @@ -181,6 +197,7 @@ func (m *machine) StatusMessage() string { func (m *machine) PhysicalBlockDevices() []BlockDevice { result := make([]BlockDevice, len(m.physicalBlockDevices)) for i, v := range m.physicalBlockDevices { + v.controller = m.controller result[i] = v } return result @@ -195,6 +212,7 @@ func (m *machine) PhysicalBlockDevice(id int) BlockDevice { func (m *machine) BlockDevices() []BlockDevice { result := make([]BlockDevice, len(m.blockDevices)) for i, v := range m.blockDevices { + v.controller = m.controller result[i] = v } return result @@ -216,13 +234,17 @@ func blockDeviceById(id int, blockDevices []BlockDevice) BlockDevice { // Partition implements Machine. func (m *machine) Partition(id int) Partition { - return partitionById(id, m.BlockDevices()) + p := partitionById(id, m.blockDevices) + if p != nil { + p.controller = m.controller + } + return p } -func partitionById(id int, blockDevices []BlockDevice) Partition { +func partitionById(id int, blockDevices []*blockdevice) *partition { for _, blockDevice := range blockDevices { - for _, partition := range blockDevice.Partitions() { - if partition.ID() == id { + for _, partition := range blockDevice.partitions { + if partition.id == id { return partition } } @@ -230,6 +252,36 @@ func partitionById(id int, blockDevices []BlockDevice) Partition { return nil } +// BlockDevices implements Machine (loaded dynamically) +func (m *machine) VolumeGroups() ([]VolumeGroup, error) { + source, err := m.controller.get(m.nodesURI() + "volume-groups/") + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound, http.StatusConflict: + return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + case http.StatusForbidden: + return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + case http.StatusServiceUnavailable: + return nil, errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) + } + } + return nil, NewUnexpectedError(err) + } + + vgs, err := readVolumeGroups(m.controller.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + + result := make([]VolumeGroup, len(vgs)) + for i, v := range vgs { + v.controller = m.controller + result[i] = v + } + return result, nil +} + // Devices implements Machine. func (m *machine) Devices(args DevicesArgs) ([]Device, error) { // Perhaps in the future, MAAS will give us a way to query just for the @@ -247,6 +299,107 @@ func (m *machine) Devices(args DevicesArgs) ([]Device, error) { return result, nil } +// UpdateMachineArgs is arguments for machine.Update +type UpdateMachineArgs struct { + Hostname string + Domain string + PowerType string + PowerAddress string + PowerUser string + PowerPassword string + PowerOpts map[string]string +} + +// Validate ensures the arguments are acceptable +func (a *UpdateMachineArgs) Validate() error { + return nil +} + +// ToParams converts arguments to URL parameters +func (a *UpdateMachineArgs) ToParams() *URLParams { + params := NewURLParams() + params.MaybeAdd("hostname", a.Hostname) + params.MaybeAdd("domain", a.Domain) + params.MaybeAdd("power_type", a.PowerType) + params.MaybeAdd("power_parameters_power_user", a.PowerUser) + params.MaybeAdd("power_parameters_power_password", a.PowerUser) + params.MaybeAdd("power_parameters_power_address", a.PowerAddress) + if a.PowerOpts != nil { + for k, v := range a.PowerOpts { + params.MaybeAdd(fmt.Sprintf("power_parameters_%s", k), v) + } + } + return params +} + +// Update implementes Machine +func (m *machine) Update(args UpdateMachineArgs) error { + params := args.ToParams() + result, err := m.controller.put(m.resourceURI, params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound, http.StatusConflict: + return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + case http.StatusForbidden: + return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + case http.StatusServiceUnavailable: + return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) + } + } + return NewUnexpectedError(err) + } + + machine, err := readMachine(m.controller.apiVersion, result) + if err != nil { + return errors.Trace(err) + } + m.updateFrom(machine) + return nil +} + +// CommissionArgs is an argument struct for Machine.Commission +type CommissionArgs struct { + EnableSSH bool + SkipBMCConfig bool + SkipNetworking bool + SkipStorage bool + CommissioningScripts []string + TestingScripts []string +} + +func (m *machine) Commission(args CommissionArgs) error { + params := NewURLParams() + params.MaybeAddBool("enableSSH", args.EnableSSH) + params.MaybeAddBool("skip_bmc_config", args.SkipBMCConfig) + params.MaybeAddBool("skip_networking", args.SkipNetworking) + params.MaybeAddBool("skip_storage", args.SkipStorage) + params.MaybeAdd("commissioning_scripts", strings.Join(args.CommissioningScripts, ",")) + params.MaybeAdd("testing_scripts", strings.Join(args.TestingScripts, ",")) + + result, err := m.controller.post(m.resourceURI, "commission", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound, http.StatusConflict: + return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + case http.StatusForbidden: + return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + case http.StatusServiceUnavailable: + return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) + } + } + return NewUnexpectedError(err) + } + + machine, err := readMachine(m.controller.apiVersion, result) + if err != nil { + return errors.Trace(err) + } + m.updateFrom(machine) + return nil +} + // StartArgs is an argument struct for passing parameters to the Machine.Start // method. type StartArgs struct { @@ -287,6 +440,54 @@ func (m *machine) Start(args StartArgs) error { return nil } +// CreateMachineBondArgs is the argument structure for Machine.CreateBond +type CreateMachineBondArgs struct { + UpdateInterfaceArgs + Parents []Interface +} + +func (a *CreateMachineBondArgs) toParams() *URLParams { + params := a.UpdateInterfaceArgs.toParams() + parents := []string{} + for _, p := range a.Parents { + parents = append(parents, fmt.Sprintf("%d", p.ID())) + } + params.MaybeAdd("parents", strings.Join(parents, ",")) + return params +} + +// Validate ensures that all required values are non-emtpy. +func (a *CreateMachineBondArgs) Validate() error { + return nil +} + +// CreateBond implements Machine +func (m *machine) CreateBond(args CreateMachineBondArgs) (_ Interface, err error) { + if err := args.Validate(); err != nil { + return nil, errors.Trace(err) + } + + params := args.toParams() + source, err := m.controller.post(m.resourceURI+"interfaces/", "create_bond", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) + case http.StatusForbidden: + return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + } + } + return nil, NewUnexpectedError(err) + } + + response, err := readInterface(m.controller.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + return response, nil +} + // CreateMachineDeviceArgs is an argument structure for Machine.CreateDevice. // Only InterfaceName and MACAddress fields are required, the others are only // used if set. If Subnet and VLAN are both set, Subnet.VLAN() must match the @@ -431,6 +632,14 @@ func (m *machine) SetOwnerData(ownerData map[string]string) error { return nil } +func (m *machine) Delete() error { + err := m.controller.delete(m.resourceURI) + if err != nil { + return errors.Trace(err) + } + return nil +} + func readMachine(controllerVersion version.Number, source interface{}) (*machine, error) { readFunc, err := getMachineDeserializationFunc(controllerVersion) if err != nil { @@ -503,11 +712,13 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { "system_id": schema.String(), "hostname": schema.String(), "fqdn": schema.String(), + "owner": schema.OneOf(schema.Nil(""), schema.String()), "tag_names": schema.List(schema.String()), "owner_data": schema.StringMap(schema.String()), "osystem": schema.String(), "distro_series": schema.String(), + "hwe_kernel": schema.OneOf(schema.Nil(""), schema.String()), "architecture": schema.OneOf(schema.Nil(""), schema.String()), "memory": schema.ForceInt(), "cpu_count": schema.ForceInt(), @@ -524,6 +735,7 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { "physicalblockdevice_set": schema.List(schema.StringMap(schema.Any())), "blockdevice_set": schema.List(schema.StringMap(schema.Any())), + "volume_groups": schema.List(schema.StringMap(schema.Any())), } defaults := schema.Defaults{ "architecture": "", @@ -572,19 +784,24 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { if err != nil { return nil, errors.Trace(err) } + architecture, _ := valid["architecture"].(string) statusMessage, _ := valid["status_message"].(string) + hweKernel, _ := valid["hwe_kernel"].(string) + owner, _ := valid["owner"].(string) result := &machine{ resourceURI: valid["resource_uri"].(string), systemID: valid["system_id"].(string), hostname: valid["hostname"].(string), fqdn: valid["fqdn"].(string), + owner: owner, tags: convertToStringSlice(valid["tag_names"]), ownerData: convertToStringMap(valid["owner_data"]), operatingSystem: valid["osystem"].(string), distroSeries: valid["distro_series"].(string), + hweKernel: hweKernel, architecture: architecture, memory: valid["memory"].(int), cpuCount: valid["cpu_count"].(int), @@ -630,3 +847,129 @@ func convertToStringMap(field interface{}) map[string]string { } return result } + +// CreateBlockDeviceArgs are required parameters +type CreateBlockDeviceArgs struct { + Name string // Required. Name of the block device. + Model string // Optional. Model of the block device. + Serial string // Optional. Serial number of the block device. + IDPath string // Optional. Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version. + Size int // Required. Size of the block device. + BlockSize int // Required. Block size of the block device. +} + +// ToParams converts arguments to URL parameters +func (a *CreateBlockDeviceArgs) toParams() *URLParams { + params := NewURLParams() + params.MaybeAdd("name", a.Name) + params.MaybeAdd("model", a.Model) + params.MaybeAdd("serial", a.Serial) + params.MaybeAdd("id_path", a.IDPath) + params.MaybeAddInt("size", a.Size) + params.MaybeAddInt("block_size", a.BlockSize) + return params +} + +// Validate checks for invalid configuration +func (a *CreateBlockDeviceArgs) Validate() error { + if a.Name == "" { + return fmt.Errorf("Name must be provided") + } + if a.Size <= 0 { + return fmt.Errorf("Size must be > 0") + } + if a.BlockSize <= 0 { + return fmt.Errorf("Block size must be > 0") + } + return nil +} + +func (m *machine) nodesURI() string { + return strings.Replace(m.resourceURI, "machines", "nodes", 1) +} + +// CreateBlockDevice implementes Machine +func (m *machine) CreateBlockDevice(args CreateBlockDeviceArgs) (BlockDevice, error) { + if err := args.Validate(); err != nil { + return nil, errors.Trace(err) + } + + params := args.toParams() + source, err := m.controller.post(m.nodesURI()+"blockdevices/", "", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) + case http.StatusForbidden: + return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + } + } + return nil, NewUnexpectedError(err) + } + + response, err := readBlockDevice(m.controller.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + return response, nil +} + +// CreateVolumeGroupArgs control creation of a volume group +type CreateVolumeGroupArgs struct { + Name string // Required. Name of the volume group. + UUID string // Optional. (optional) UUID of the volume group. + BlockDevices []BlockDevice // Optional. Block devices to add to the volume group. + Partitions []Partition // Optional. Partitions to add to the volume group. +} + +func (a *CreateVolumeGroupArgs) toParams() *URLParams { + params := NewURLParams() + params.MaybeAdd("name", a.Name) + params.MaybeAdd("uuid", a.UUID) + if a.BlockDevices != nil { + deviceIDs := []string{} + for _, device := range a.BlockDevices { + deviceIDs = append(deviceIDs, fmt.Sprintf("%d", device.ID())) + } + params.MaybeAdd("block_devices", strings.Join(deviceIDs, ",")) + } + if a.Partitions != nil { + partitionIDs := []string{} + for _, partition := range a.Partitions { + partitionIDs = append(partitionIDs, fmt.Sprintf("%d", partition.ID())) + } + params.MaybeAdd("partitions", strings.Join(partitionIDs, ",")) + } + return params +} + +// Validate checks for errors +func (a *CreateVolumeGroupArgs) Validate() error { + if a.Name == "" { + return fmt.Errorf("Name required") + } + return nil +} + +func (m *machine) CreateVolumeGroup(args CreateVolumeGroupArgs) (VolumeGroup, error) { + params := args.toParams() + source, err := m.controller.post(m.nodesURI()+"volume-groups", "", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) + case http.StatusForbidden: + return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + } + } + return nil, NewUnexpectedError(err) + } + + response, err := readVolumeGroup(m.controller.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + return response, nil +} diff --git a/oauth.go b/oauth.go index 920960d..75a095d 100644 --- a/oauth.go +++ b/oauth.go @@ -28,7 +28,7 @@ func generateTimestamp() string { } type OAuthSigner interface { - OAuthSign(request *http.Request) error + OAuthSign(headers *http.Header) error } type OAuthToken struct { @@ -52,7 +52,7 @@ func NewPlainTestOAuthSigner(token *OAuthToken, realm string) (OAuthSigner, erro // OAuthSignPLAINTEXT signs the provided request using the OAuth PLAINTEXT // method: http://oauth.net/core/1.0/#anchor22. -func (signer plainTextOAuthSigner) OAuthSign(request *http.Request) error { +func (signer plainTextOAuthSigner) OAuthSign(headers *http.Header) error { signature := signer.token.ConsumerSecret + `&` + signer.token.TokenSecret nonce, err := generateNonce() @@ -75,6 +75,6 @@ func (signer plainTextOAuthSigner) OAuthSign(request *http.Request) error { authHeader = append(authHeader, fmt.Sprintf(`%s="%s"`, key, url.QueryEscape(value))) } strHeader := "OAuth " + strings.Join(authHeader, ", ") - request.Header.Add("Authorization", strHeader) + headers.Add("Authorization", strHeader) return nil } diff --git a/partition.go b/partition.go index 26401e5..e8708d8 100644 --- a/partition.go +++ b/partition.go @@ -4,12 +4,15 @@ package gomaasapi import ( + "net/http" + "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type partition struct { + controller *controller resourceURI string id int @@ -22,6 +25,17 @@ type partition struct { filesystem *filesystem } +func (p *partition) updateFrom(other *partition) { + p.resourceURI = other.resourceURI + p.id = other.id + p.path = other.path + p.uuid = other.uuid + p.usedFor = other.usedFor + p.size = other.size + p.tags = other.tags + p.filesystem = other.filesystem +} + // Type implements Partition. func (p *partition) Type() string { return "partition" @@ -65,14 +79,78 @@ func (p *partition) Tags() []string { return p.tags } -func readPartitions(controllerVersion version.Number, source interface{}) ([]*partition, error) { - checker := schema.List(schema.StringMap(schema.Any())) +func (p *partition) Format(args FormatStorageDeviceArgs) error { + if err := args.Validate(); err != nil { + return errors.Trace(err) + } + + params := NewURLParams() + params.MaybeAdd("fs_type", args.FSType) + params.MaybeAdd("uuid", args.UUID) + params.MaybeAdd("label", args.Label) + + result, err := p.controller.post(p.resourceURI, "format", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + case http.StatusForbidden: + return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + case http.StatusServiceUnavailable: + return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) + } + } + return NewUnexpectedError(err) + } + + partition, err := readPartition(p.controller.apiVersion, result) + if err != nil { + return errors.Trace(err) + } + p.updateFrom(partition) + return nil +} + +func (p *partition) Mount(args MountStorageDeviceArgs) error { + params := args.toParams() + source, err := p.controller.post(p.resourceURI, "mount", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) + case http.StatusForbidden: + return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + } + } + return NewUnexpectedError(err) + } + + response, err := readPartition(p.controller.apiVersion, source) + if err != nil { + return errors.Trace(err) + } + p.updateFrom(response) + return nil +} + +func readPartition(controllerVersion version.Number, source interface{}) (*partition, error) { + readFunc, err := getPartitionDeserializationFunc(controllerVersion) + if err != nil { + return nil, errors.Trace(err) + } + + checker := schema.StringMap(schema.Any()) coerced, err := checker.Coerce(source, nil) if err != nil { - return nil, WrapWithDeserializationError(err, "partition base schema check failed") + return nil, WrapWithDeserializationError(err, "machine base schema check failed") } - valid := coerced.([]interface{}) + valid := coerced.(map[string]interface{}) + return readFunc(valid) +} +func getPartitionDeserializationFunc(controllerVersion version.Number) (partitionDeserializationFunc, error) { var deserialisationVersion version.Number for v := range partitionDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { @@ -82,7 +160,21 @@ func readPartitions(controllerVersion version.Number, source interface{}) ([]*pa if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no partition read func for version %s", controllerVersion) } - readFunc := partitionDeserializationFuncs[deserialisationVersion] + return partitionDeserializationFuncs[deserialisationVersion], nil +} + +func readPartitions(controllerVersion version.Number, source interface{}) ([]*partition, error) { + checker := schema.List(schema.StringMap(schema.Any())) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, WrapWithDeserializationError(err, "partition base schema check failed") + } + valid := coerced.([]interface{}) + + readFunc, err := getPartitionDeserializationFunc(controllerVersion) + if err != nil { + return nil, err + } return readPartitionList(valid, readFunc) } diff --git a/tag.go b/tag.go new file mode 100644 index 0000000..385a6bc --- /dev/null +++ b/tag.go @@ -0,0 +1,150 @@ +package gomaasapi + +import ( + "net/url" + + "github.com/juju/errors" + "github.com/juju/schema" + "github.com/juju/version" +) + +type tag struct { + controller *controller + resourceURI string + + name string + definition string + comment string +} + +// Name implements Tag. +func (s *tag) Name() string { + return s.name +} + +// Definition implements Tag. +func (s *tag) Definition() string { + return s.definition +} + +// Comment implements Tag. +func (s *tag) Comment() string { + return s.definition +} + +func (s *tag) Machines() ([]Machine, error) { + source, err := s.controller.getOp(s.resourceURI, "machines") + if err != nil { + return nil, NewUnexpectedError(err) + } + machines, err := readMachines(s.controller.apiVersion, source) + if err != nil { + return nil, errors.Trace(err) + } + var result []Machine + for _, m := range machines { + //m.controller = c + result = append(result, m) + } + return result, nil +} + +func (s *tag) AddToMachine(systemID string) error { + params := url.Values{ + "add": []string{systemID}, + } + _, err := s.controller.post(s.resourceURI, "update_nodes", params) + return err +} + +func (s *tag) RemoveFromMachine(systemID string) error { + params := url.Values{ + "remove": []string{systemID}, + } + _, err := s.controller.post(s.resourceURI, "update_nodes", params) + return err +} + +func readTags(controllerVersion version.Number, source interface{}) ([]*tag, error) { + checker := schema.List(schema.StringMap(schema.Any())) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "tag base schema check failed") + } + valid := coerced.([]interface{}) + + var deserialisationVersion version.Number + for v := range tagDeserializationFuncs { + if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { + deserialisationVersion = v + } + } + if deserialisationVersion == version.Zero { + return nil, errors.Errorf("no tag read func for version %s", controllerVersion) + } + readFunc := tagDeserializationFuncs[deserialisationVersion] + return readTagList(valid, readFunc) +} + +func readTag(controllerVersion version.Number, source interface{}) (*tag, error) { + var deserialisationVersion version.Number + for v := range tagDeserializationFuncs { + if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { + deserialisationVersion = v + } + } + + if deserialisationVersion == version.Zero { + return nil, errors.Errorf("no tag read func for version %s", controllerVersion) + } + readFunc := tagDeserializationFuncs[deserialisationVersion] + return readFunc(source.(map[string]interface{})) +} + +// readTagList expects the values of the sourceList to be string maps. +func readTagList(sourceList []interface{}, readFunc tagDeserializationFunc) ([]*tag, error) { + result := make([]*tag, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for tag %d, %T", i, value) + } + tag, err := readFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "tag %d", i) + } + result = append(result, tag) + } + return result, nil +} + +type tagDeserializationFunc func(map[string]interface{}) (*tag, error) + +var tagDeserializationFuncs = map[version.Number]tagDeserializationFunc{ + twoDotOh: tag_2_0, +} + +func tag_2_0(source map[string]interface{}) (*tag, error) { + fields := schema.Fields{ + "resource_uri": schema.String(), + "name": schema.String(), + "definition": schema.String(), + "comment": schema.String(), + } + checker := schema.FieldMap(fields, nil) // no defaults + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "tag 2.0 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + + result := &tag{ + resourceURI: valid["resource_uri"].(string), + name: valid["name"].(string), + comment: valid["comment"].(string), + definition: valid["definition"].(string), + } + return result, nil +} diff --git a/volumegroup.go b/volumegroup.go new file mode 100644 index 0000000..f86bf74 --- /dev/null +++ b/volumegroup.go @@ -0,0 +1,207 @@ +package gomaasapi + +import ( + "fmt" + "net/http" + + "github.com/juju/errors" + "github.com/juju/schema" + "github.com/juju/version" +) + +type volumegroup struct { + // Add the controller in when we need to do things with the zone. + controller *controller + + resourceURI string + + id int + name string + description string + uuid string + size uint64 + devices []*blockdevice +} + +func (vg *volumegroup) Name() string { + return vg.name +} + +func (vg *volumegroup) Size() uint64 { + return vg.size +} + +func (vg *volumegroup) UUID() string { + return vg.uuid +} + +func (vg *volumegroup) Devices() []BlockDevice { + result := make([]BlockDevice, len(vg.devices)) + for i, v := range vg.devices { + //v.controller = d + result[i] = v + } + return result +} + +// CreateLogicalVolumeArgs creates a logical volume in a volume group +type CreateLogicalVolumeArgs struct { + Name string // Required. Name of the logical volume. + UUID string // Optional. (optional) UUID of the logical volume. + Size int // Required. Size of the logical volume. Must be larger than or equal to 4,194,304 bytes. E.g. 4194304. +} + +// Validate ensures arguments are valid +func (a *CreateLogicalVolumeArgs) Validate() error { + if a.Name == "" { + return fmt.Errorf("Name must be specified") + } + if a.Size <= 0 { + return fmt.Errorf("A size > 0 must be specified") + } + return nil +} + +// CreateLogicalVolume creates a logical volume in a volume group +func (vg *volumegroup) CreateLogicalVolume(args CreateLogicalVolumeArgs) (BlockDevice, error) { + if err := args.Validate(); err != nil { + return nil, errors.Trace(err) + } + + params := NewURLParams() + params.MaybeAdd("name", args.Name) + params.MaybeAdd("uuid", args.UUID) + params.MaybeAddInt("size", args.Size) + + result, err := vg.controller.post(vg.resourceURI, "create_logical_volume", params.Values) + if err != nil { + if svrErr, ok := errors.Cause(err).(ServerError); ok { + switch svrErr.StatusCode { + case http.StatusNotFound: + return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) + case http.StatusForbidden: + return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) + case http.StatusServiceUnavailable: + return nil, errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) + } + } + return nil, NewUnexpectedError(err) + } + + device, err := readBlockDevice(vg.controller.apiVersion, result) + if err != nil { + return nil, errors.Trace(err) + } + return device, nil +} + +func readVolumeGroup(controllerVersion version.Number, source interface{}) (*volumegroup, error) { + readFunc, err := getVolumeGroupDeserializationFunc(controllerVersion) + if err != nil { + return nil, errors.Trace(err) + } + + checker := schema.StringMap(schema.Any()) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, WrapWithDeserializationError(err, "machine base schema check failed") + } + valid := coerced.(map[string]interface{}) + return readFunc(valid) +} + +func getVolumeGroupDeserializationFunc(controllerVersion version.Number) (volumeGroupDeserializationFunc, error) { + var deserialisationVersion version.Number + for v := range volumeGroupDeserializationFuncs { + if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { + deserialisationVersion = v + } + } + if deserialisationVersion == version.Zero { + return nil, NewUnsupportedVersionError("no volumegroup read func for version %s", controllerVersion) + } + return volumeGroupDeserializationFuncs[deserialisationVersion], nil +} + +func readVolumeGroups(controllerVersion version.Number, source interface{}) ([]*volumegroup, error) { + checker := schema.List(schema.StringMap(schema.Any())) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, WrapWithDeserializationError(err, "volumegroup base schema check failed") + } + valid := coerced.([]interface{}) + + readFunc, err := getVolumeGroupDeserializationFunc(controllerVersion) + if err != nil { + return nil, err + } + return readVolumeGroupList(valid, readFunc) +} + +// readPartitionList expects the values of the sourceList to be string maps. +func readVolumeGroupList(sourceList []interface{}, readFunc volumeGroupDeserializationFunc) ([]*volumegroup, error) { + result := make([]*volumegroup, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, NewDeserializationError("unexpected value for volumegroup %d, %T", i, value) + } + volumegroup, err := readFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "volumegroup %d", i) + } + result = append(result, volumegroup) + } + return result, nil +} + +type volumeGroupDeserializationFunc func(map[string]interface{}) (*volumegroup, error) + +var volumeGroupDeserializationFuncs = map[version.Number]volumeGroupDeserializationFunc{ + twoDotOh: volumegroup_2_0, +} + +func volumegroup_2_0(source map[string]interface{}) (*volumegroup, error) { + fields := schema.Fields{ + "resource_uri": schema.String(), + "id": schema.ForceInt(), + "name": schema.String(), + "uuid": schema.OneOf(schema.Nil(""), schema.String()), + "size": schema.ForceUint(), + "devices": schema.List(schema.StringMap(schema.Any())), + //"used_size": schema.String(), + //"human_size": schema.ForceUint(), + //"system_id": schema.String(), + //"available_size": schema.String(), + //"logical_volumes": schema.String(), + //"human_used_size": schema.String(), + //"human_available_size": schema.String(), + } + defaults := schema.Defaults{ + //"tags": []string{}, + } + checker := schema.FieldMap(fields, defaults) + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, WrapWithDeserializationError(err, "volumegroup 2.0 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + + devices, err := readBlockDeviceList(valid["devices"].([]interface{}), blockdevice_2_0) + if err != nil { + return nil, errors.Trace(err) + } + + uuid, _ := valid["uuid"].(string) + result := &volumegroup{ + resourceURI: valid["resource_uri"].(string), + id: valid["id"].(int), + name: valid["name"].(string), + uuid: uuid, + size: valid["size"].(uint64), + devices: devices, + } + return result, nil +}