pax_global_header00006660000000000000000000000064145173217210014515gustar00rootroot0000000000000052 comment=4a5c51482a8b9824c18f2cc4e5629405dbb8e696 golang-github-juju-gomaasapi-2.2.0/000077500000000000000000000000001451732172100171575ustar00rootroot00000000000000golang-github-juju-gomaasapi-2.2.0/.gitignore000066400000000000000000000000301451732172100211400ustar00rootroot00000000000000*.sw[nop] example/[^.]* golang-github-juju-gomaasapi-2.2.0/LICENSE000066400000000000000000000215061451732172100201700ustar00rootroot00000000000000All files in this repository are licensed as follows. If you contribute to this repository, it is assumed that you license your contribution under the same license unless you state otherwise. All files Copyright (C) 2012-2016 Canonical Ltd. unless otherwise specified in the file. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. golang-github-juju-gomaasapi-2.2.0/Makefile000066400000000000000000000010331451732172100206140ustar00rootroot00000000000000# Build, and run tests. check: examples go test ./... example_source := $(wildcard example/*.go) example_binaries := $(patsubst %.go,%,$(example_source)) # Clean up binaries. clean: $(RM) $(example_binaries) # Reformat the source files to match our layout standards. format: gofmt -w . # Invoke gofmt's "simplify" option to streamline the source code. simplify: gofmt -w -s . # Build the examples (we have no tests for them). examples: $(example_binaries) %: %.go go build -o $@ $< .PHONY: check clean format examples simplify golang-github-juju-gomaasapi-2.2.0/README.rst000066400000000000000000000005031451732172100206440ustar00rootroot00000000000000.. -*- mode: rst -*- ****************************** MAAS API client library for Go ****************************** This library serves as a minimal client for communicating with the MAAS web API in Go programs. For more information see the `project homepage`_. .. _project homepage: https://github.com/juju/gomaasapi/v2 golang-github-juju-gomaasapi-2.2.0/blockdevice.go000066400000000000000000000127611451732172100217670ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type blockdevice struct { resourceURI string id int uuid string name string model string idPath string path string usedFor string tags []string blockSize uint64 usedSize uint64 size uint64 filesystem *filesystem partitions []*partition } // Type implements BlockDevice func (b *blockdevice) Type() string { return "blockdevice" } // ID implements BlockDevice. func (b *blockdevice) ID() int { return b.id } // UUID implements BlockDevice. func (b *blockdevice) UUID() string { return b.uuid } // Name implements BlockDevice. func (b *blockdevice) Name() string { return b.name } // Model implements BlockDevice. func (b *blockdevice) Model() string { return b.model } // IDPath implements BlockDevice. func (b *blockdevice) IDPath() string { return b.idPath } // Path implements BlockDevice. func (b *blockdevice) Path() string { return b.path } // UsedFor implements BlockDevice. func (b *blockdevice) UsedFor() string { return b.usedFor } // Tags implements BlockDevice. func (b *blockdevice) Tags() []string { return b.tags } // BlockSize implements BlockDevice. func (b *blockdevice) BlockSize() uint64 { return b.blockSize } // UsedSize implements BlockDevice. func (b *blockdevice) UsedSize() uint64 { return b.usedSize } // Size implements BlockDevice. func (b *blockdevice) Size() uint64 { return b.size } // FileSystem implements BlockDevice. func (b *blockdevice) FileSystem() FileSystem { return b.filesystem } // Partitions implements BlockDevice. func (b *blockdevice) Partitions() []Partition { result := make([]Partition, len(b.partitions)) for i, v := range b.partitions { 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) if err != nil { return nil, WrapWithDeserializationError(err, "blockdevice base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range blockdeviceDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no blockdevice read func for version %s", controllerVersion) } readFunc := blockdeviceDeserializationFuncs[deserialisationVersion] return readBlockDeviceList(valid, readFunc) } // readBlockDeviceList expects the values of the sourceList to be string maps. func readBlockDeviceList(sourceList []interface{}, readFunc blockdeviceDeserializationFunc) ([]*blockdevice, error) { result := make([]*blockdevice, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for blockdevice %d, %T", i, value) } blockdevice, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "blockdevice %d", i) } result = append(result, blockdevice) } return result, nil } type blockdeviceDeserializationFunc func(map[string]interface{}) (*blockdevice, error) var blockdeviceDeserializationFuncs = map[version.Number]blockdeviceDeserializationFunc{ twoDotOh: blockdevice_2_0, } func blockdevice_2_0(source map[string]interface{}) (*blockdevice, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "uuid": schema.OneOf(schema.Nil(""), schema.String()), "name": 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()), "block_size": schema.ForceUint(), "used_size": schema.ForceUint(), "size": schema.ForceUint(), "filesystem": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())), "partitions": schema.List(schema.StringMap(schema.Any())), } checker := schema.FieldMap(fields, nil) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "blockdevice 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. var filesystem *filesystem if fsSource, ok := valid["filesystem"].(map[string]interface{}); ok { if filesystem, err = filesystem2_0(fsSource); err != nil { return nil, errors.Trace(err) } } 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) result := &blockdevice{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), uuid: uuid, name: valid["name"].(string), 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), size: valid["size"].(uint64), filesystem: filesystem, partitions: partitions, } return result, nil } golang-github-juju-gomaasapi-2.2.0/blockdevice_test.go000066400000000000000000000115631451732172100230250ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type blockdeviceSuite struct{} var _ = gc.Suite(&blockdeviceSuite{}) func (*blockdeviceSuite) TestReadBlockDevicesBadSchema(c *gc.C) { _, err := readBlockDevices(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `blockdevice base schema check failed: expected list, got string("wat?")`) } func (*blockdeviceSuite) TestReadBlockDevices(c *gc.C) { blockdevices, err := readBlockDevices(twoDotOh, parseJSON(c, blockdevicesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(blockdevices, gc.HasLen, 1) blockdevice := blockdevices[0] c.Check(blockdevice.ID(), gc.Equals, 34) c.Check(blockdevice.Name(), gc.Equals, "sda") c.Check(blockdevice.Model(), gc.Equals, "QEMU HARDDISK") c.Check(blockdevice.Path(), gc.Equals, "/dev/disk/by-dname/sda") c.Check(blockdevice.IDPath(), gc.Equals, "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001") c.Check(blockdevice.UUID(), gc.Equals, "6199b7c9-b66f-40f6-a238-a938a58a0adf") c.Check(blockdevice.UsedFor(), gc.Equals, "MBR partitioned with 1 partition") c.Check(blockdevice.Tags(), jc.DeepEquals, []string{"rotary"}) c.Check(blockdevice.BlockSize(), gc.Equals, uint64(4096)) c.Check(blockdevice.UsedSize(), gc.Equals, uint64(8586788864)) c.Check(blockdevice.Size(), gc.Equals, uint64(8589934592)) partitions := blockdevice.Partitions() c.Assert(partitions, gc.HasLen, 1) partition := partitions[0] c.Check(partition.ID(), gc.Equals, 1) c.Check(partition.UsedFor(), gc.Equals, "ext4 formatted filesystem mounted at /") fs := blockdevice.FileSystem() c.Assert(fs, gc.NotNil) c.Assert(fs.Type(), gc.Equals, "ext4") c.Assert(fs.MountPoint(), gc.Equals, "/srv") } func (*blockdeviceSuite) TestReadBlockDevicesWithNulls(c *gc.C) { blockdevices, err := readBlockDevices(twoDotOh, parseJSON(c, blockdevicesWithNullsResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(blockdevices, gc.HasLen, 1) blockdevice := blockdevices[0] c.Check(blockdevice.Model(), gc.Equals, "") c.Check(blockdevice.IDPath(), gc.Equals, "") c.Check(blockdevice.FileSystem(), gc.IsNil) } func (*blockdeviceSuite) TestLowVersion(c *gc.C) { _, err := readBlockDevices(version.MustParse("1.9.0"), parseJSON(c, blockdevicesResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (*blockdeviceSuite) TestHighVersion(c *gc.C) { blockdevices, err := readBlockDevices(version.MustParse("2.1.9"), parseJSON(c, blockdevicesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(blockdevices, gc.HasLen, 1) } var blockdevicesResponse = ` [ { "path": "/dev/disk/by-dname/sda", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 1, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b752" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/partition/1", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0adf", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": { "fstype": "ext4", "mount_point": "/srv", "label": "root", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b752" }, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/", "id": 34, "serial": "QM00001", "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": "MBR", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0adf", "size": 8589934592, "model": "QEMU HARDDISK", "tags": [ "rotary" ] } ] ` var blockdevicesWithNullsResponse = ` [ { "path": "/dev/disk/by-dname/sda", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [], "filesystem": null, "id_path": null, "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/", "id": 34, "serial": null, "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": null, "uuid": null, "size": 8589934592, "model": null, "tags": [] } ] ` golang-github-juju-gomaasapi-2.2.0/bootresource.go000066400000000000000000000076151451732172100222320ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "strings" "github.com/juju/collections/set" "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type bootResource struct { // Add the controller in when we need to do things with the bootResource. // controller Controller resourceURI string id int name string type_ string architecture string subArches string kernelFlavor string } // ID implements BootResource. func (b *bootResource) ID() int { return b.id } // Name implements BootResource. func (b *bootResource) Name() string { return b.name } // Name implements BootResource. func (b *bootResource) Type() string { return b.type_ } // Name implements BootResource. func (b *bootResource) Architecture() string { return b.architecture } // SubArchitectures implements BootResource. func (b *bootResource) SubArchitectures() set.Strings { return set.NewStrings(strings.Split(b.subArches, ",")...) } // KernelFlavor implements BootResource. func (b *bootResource) KernelFlavor() string { return b.kernelFlavor } func readBootResources(controllerVersion version.Number, source interface{}) ([]*bootResource, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "boot resource base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range bootResourceDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no boot resource read func for version %s", controllerVersion) } readFunc := bootResourceDeserializationFuncs[deserialisationVersion] return readBootResourceList(valid, readFunc) } // readBootResourceList expects the values of the sourceList to be string maps. func readBootResourceList(sourceList []interface{}, readFunc bootResourceDeserializationFunc) ([]*bootResource, error) { result := make([]*bootResource, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for boot resource %d, %T", i, value) } bootResource, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "boot resource %d", i) } result = append(result, bootResource) } return result, nil } type bootResourceDeserializationFunc func(map[string]interface{}) (*bootResource, error) var bootResourceDeserializationFuncs = map[version.Number]bootResourceDeserializationFunc{ twoDotOh: bootResource_2_0, } func bootResource_2_0(source map[string]interface{}) (*bootResource, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "name": schema.String(), "type": schema.String(), "architecture": schema.String(), "subarches": schema.String(), "kflavor": schema.String(), } defaults := schema.Defaults{ "subarches": "", "kflavor": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "boot resource 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 := &bootResource{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), name: valid["name"].(string), type_: valid["type"].(string), architecture: valid["architecture"].(string), subArches: valid["subarches"].(string), kernelFlavor: valid["kflavor"].(string), } return result, nil } golang-github-juju-gomaasapi-2.2.0/bootresource_test.go000066400000000000000000000057651451732172100232750ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/collections/set" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type bootResourceSuite struct{} var _ = gc.Suite(&bootResourceSuite{}) func (*bootResourceSuite) TestReadBootResourcesBadSchema(c *gc.C) { _, err := readBootResources(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `boot resource base schema check failed: expected list, got string("wat?")`) } func (*bootResourceSuite) TestReadBootResources(c *gc.C) { bootResources, err := readBootResources(twoDotOh, parseJSON(c, bootResourcesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(bootResources, gc.HasLen, 5) trusty := bootResources[0] subarches := set.NewStrings("generic", "hwe-p", "hwe-q", "hwe-r", "hwe-s", "hwe-t") c.Assert(trusty.ID(), gc.Equals, 5) c.Assert(trusty.Name(), gc.Equals, "ubuntu/trusty") c.Assert(trusty.Type(), gc.Equals, "Synced") c.Assert(trusty.Architecture(), gc.Equals, "amd64/hwe-t") c.Assert(trusty.SubArchitectures(), jc.DeepEquals, subarches) c.Assert(trusty.KernelFlavor(), gc.Equals, "generic") } func (*bootResourceSuite) TestLowVersion(c *gc.C) { _, err := readBootResources(version.MustParse("1.9.0"), parseJSON(c, bootResourcesResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (*bootResourceSuite) TestHighVersion(c *gc.C) { bootResources, err := readBootResources(version.MustParse("2.1.9"), parseJSON(c, bootResourcesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(bootResources, gc.HasLen, 5) } var bootResourcesResponse = ` [ { "architecture": "amd64/hwe-t", "type": "Synced", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s,hwe-t", "kflavor": "generic", "name": "ubuntu/trusty", "id": 5, "resource_uri": "/MAAS/api/2.0/boot-resources/5/" }, { "architecture": "amd64/hwe-u", "type": "Synced", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s,hwe-t,hwe-u", "name": "ubuntu/trusty", "id": 1, "resource_uri": "/MAAS/api/2.0/boot-resources/1/" }, { "architecture": "amd64/hwe-v", "type": "Synced", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s,hwe-t,hwe-u,hwe-v", "kflavor": "generic", "name": "ubuntu/trusty", "id": 3, "resource_uri": "/MAAS/api/2.0/boot-resources/3/" }, { "architecture": "amd64/hwe-w", "type": "Synced", "kflavor": "generic", "name": "ubuntu/trusty", "id": 4, "resource_uri": "/MAAS/api/2.0/boot-resources/4/" }, { "architecture": "amd64/hwe-x", "type": "Synced", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s,hwe-t,hwe-u,hwe-v,hwe-w,hwe-x", "kflavor": "generic", "name": "ubuntu/xenial", "id": 2, "resource_uri": "/MAAS/api/2.0/boot-resources/2/" } ] ` golang-github-juju-gomaasapi-2.2.0/client.go000066400000000000000000000261321451732172100207700ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "net/url" "regexp" "strconv" "strings" "time" "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 HTTPClient *http.Client } // ServerError is an http error (or at least, a non-2xx result) received from // the server. It contains the numerical HTTP status code as well as an error // string and the response's headers. type ServerError struct { error StatusCode int Header http.Header BodyMessage string } // GetServerError returns the ServerError from the cause of the error if it is a // ServerError, and also returns the bool to indicate if it was a ServerError or // not. func GetServerError(err error) (ServerError, bool) { svrErr, ok := errors.Cause(err).(ServerError) return svrErr, ok } // readAndClose reads and closes the given ReadCloser. // // Trying to read from a nil simply returns nil, no error. func readAndClose(stream io.ReadCloser) ([]byte, error) { if stream == nil { return nil, nil } defer stream.Close() return io.ReadAll(stream) } // dispatchRequest sends a request to the server, and interprets the response. // Client-side errors will return an empty response and a non-nil error. For // server-side errors however (i.e. responses with a non 2XX status code), the // 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 transparently 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. if request.Body != nil { newBody := io.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 || serverError.StatusCode == http.StatusConflict) { retryTimeInt, errConv := strconv.Atoi(serverError.Header.Get(RetryAfterHeaderName)) if errConv == nil { select { case <-time.After(time.Duration(retryTimeInt) * time.Second): } continue } } } return body, err } // Restore body before issuing request. if request.Body != nil { newBody := io.NopCloser(bytes.NewReader(bodyContent)) request.Body = newBody } return client.dispatchSingleRequest(request) } func (client Client) dispatchSingleRequest(request *http.Request) ([]byte, error) { client.Signer.OAuthSign(request) httpClient := &http.Client{} if client.HTTPClient != nil { httpClient = client.HTTPClient } // 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) if err != nil { return nil, err } body, err := readAndClose(response.Body) if err != nil { return nil, err } if response.StatusCode < 200 || response.StatusCode > 299 { err := errors.Errorf("ServerError: %v (%s)", response.Status, body) return body, errors.Trace(ServerError{error: err, StatusCode: response.StatusCode, Header: response.Header, BodyMessage: string(body)}) } return body, nil } // GetURL returns the URL to a given resource on the API, based on its URI. // The resource URI may be absolute or relative; either way the result is a // full absolute URL including the network part. func (client Client) GetURL(uri *url.URL) *url.URL { return client.APIURL.ResolveReference(uri) } // Get performs an HTTP "GET" to the API. This may be either an API method // invocation (if you pass its name in "operation") or plain resource // retrieval (if you leave "operation" blank). func (client Client) Get(uri *url.URL, operation string, parameters url.Values) ([]byte, error) { if parameters == nil { parameters = make(url.Values) } opParameter := parameters.Get("op") if opParameter != "" { msg := errors.Errorf("reserved parameter 'op' passed (with value '%s')", opParameter) return nil, msg } if operation != "" { parameters.Set("op", operation) } queryUrl := client.GetURL(uri) queryUrl.RawQuery = parameters.Encode() request, err := http.NewRequest("GET", queryUrl.String(), nil) if err != nil { return nil, err } return client.dispatchRequest(request) } // writeMultiPartFiles writes the given files as parts of a multipart message // using the given writer. func writeMultiPartFiles(writer *multipart.Writer, files map[string][]byte) error { for fileName, fileContent := range files { fw, err := writer.CreateFormFile(fileName, fileName) if err != nil { return err } io.Copy(fw, bytes.NewBuffer(fileContent)) } return nil } // writeMultiPartParams writes the given parameters as parts of a multipart // message using the given writer. func writeMultiPartParams(writer *multipart.Writer, parameters url.Values) error { for key, values := range parameters { for _, value := range values { fw, err := writer.CreateFormField(key) if err != nil { return err } buffer := bytes.NewBufferString(value) io.Copy(fw, buffer) } } return nil } // nonIdempotentRequestFiles implements the common functionality of PUT and // POST requests (but not GET or DELETE requests) when uploading files is // needed. func (client Client) nonIdempotentRequestFiles(method string, uri *url.URL, parameters url.Values, files map[string][]byte) ([]byte, error) { buf := new(bytes.Buffer) writer := multipart.NewWriter(buf) err := writeMultiPartFiles(writer, files) if err != nil { return nil, err } err = writeMultiPartParams(writer, parameters) if err != nil { return nil, err } writer.Close() url := client.GetURL(uri) request, err := http.NewRequest(method, url.String(), buf) if err != nil { return nil, err } request.Header.Set("Content-Type", writer.FormDataContentType()) return client.dispatchRequest(request) } // nonIdempotentRequest implements the common functionality of PUT and POST // 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()))) if err != nil { return nil, err } request.Header.Set("Content-Type", "application/x-www-form-urlencoded") return client.dispatchRequest(request) } // Post performs an HTTP "POST" to the API. This may be either an API method // invocation (if you pass its name in "operation") or plain resource // retrieval (if you leave "operation" blank). func (client Client) Post(uri *url.URL, operation string, parameters url.Values, files map[string][]byte) ([]byte, error) { queryParams := url.Values{"op": {operation}} uri.RawQuery = queryParams.Encode() if files != nil { return client.nonIdempotentRequestFiles("POST", uri, parameters, files) } return client.nonIdempotentRequest("POST", uri, parameters) } // Put updates an object on the API, using an HTTP "PUT" request. func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) { return client.nonIdempotentRequest("PUT", uri, parameters) } // 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("")) if err != nil { return err } _, err = client.dispatchRequest(request) if err != nil { return err } return nil } // Anonymous "signature method" implementation. type anonSigner struct{} func (signer anonSigner) OAuthSign(request *http.Request) error { return nil } // *anonSigner implements the OAuthSigner interface. var _ OAuthSigner = anonSigner{} // AddAPIVersionToURL will add the version// suffix to the // given URL, handling trailing slashes. It shouldn't be called with a // URL that already includes a version. func AddAPIVersionToURL(BaseURL, apiVersion string) string { baseurl := EnsureTrailingSlash(BaseURL) return fmt.Sprintf("%sapi/%s/", baseurl, apiVersion) } var apiVersionPattern = regexp.MustCompile(`^(?P.*/)api/(?P\d+\.\d+)/?$`) // SplitVersionedURL splits a versioned API URL (like // http://maas.server/MAAS/api/2.0/) into a base URL // (http://maas.server/MAAS/) and API version (2.0). If the URL // doesn't include a version component the bool return value will be // false. func SplitVersionedURL(url string) (string, string, bool) { if !apiVersionPattern.MatchString(url) { return url, "", false } version := apiVersionPattern.ReplaceAllString(url, "$version") baseURL := apiVersionPattern.ReplaceAllString(url, "$base") return baseURL, version, true } // NewAnonymousClient creates a client that issues anonymous requests. // BaseURL should refer to the root of the MAAS server path, e.g. // http://my.maas.server.example.com/MAAS/ // apiVersion should contain the version of the MAAS API that you want to use. func NewAnonymousClient(BaseURL string, apiVersion string) (*Client, error) { versionedURL := AddAPIVersionToURL(BaseURL, apiVersion) parsedURL, err := url.Parse(versionedURL) if err != nil { return nil, err } return &Client{Signer: &anonSigner{}, APIURL: parsedURL}, nil } // NewAuthenticatedClient parses the given MAAS API key into the // individual OAuth tokens and creates an Client that will use these // tokens to sign the requests it issues. // versionedURL should be the location of the versioned API root of // the MAAS server, e.g.: // http://my.maas.server.example.com/MAAS/api/2.0/ func NewAuthenticatedClient(versionedURL, apiKey string) (*Client, error) { elements := strings.Split(apiKey, ":") if len(elements) != 3 { errString := fmt.Sprintf("invalid API key %q; expected \"::\"", apiKey) return nil, errors.NewNotValid(nil, errString) } token := &OAuthToken{ ConsumerKey: elements[0], // The consumer secret is the empty string in MAAS' authentication. ConsumerSecret: "", TokenKey: elements[1], TokenSecret: elements[2], } signer, err := NewPlainTestOAuthSigner(token, "MAAS API") if err != nil { return nil, err } parsedURL, err := url.Parse(EnsureTrailingSlash(versionedURL)) if err != nil { return nil, err } return &Client{Signer: signer, APIURL: parsedURL}, nil } golang-github-juju-gomaasapi-2.2.0/client_test.go000066400000000000000000000336761451732172100220420ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "bytes" "crypto/tls" "fmt" "io" "net/http" "net/url" "strings" "time" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) type ClientSuite struct{} var _ = gc.Suite(&ClientSuite{}) func (*ClientSuite) TestReadAndCloseReturnsNilForNilBuffer(c *gc.C) { data, err := readAndClose(nil) c.Assert(err, jc.ErrorIsNil) c.Check(data, gc.IsNil) } func (*ClientSuite) TestReadAndCloseReturnsContents(c *gc.C) { content := "Stream contents." stream := io.NopCloser(strings.NewReader(content)) data, err := readAndClose(stream) c.Assert(err, jc.ErrorIsNil) c.Check(string(data), gc.Equals, content) } func (suite *ClientSuite) TestClientDispatchRequestReturnsServerError(c *gc.C) { URI := "/some/url/?param1=test" expectedResult := "expected:result" server := newSingleServingServer(URI, expectedResult, http.StatusBadRequest, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) request, err := http.NewRequest("GET", server.URL+URI, nil) result, err := client.dispatchRequest(request) expectedErrorString := fmt.Sprintf("ServerError: 400 Bad Request (%v)", expectedResult) c.Check(err.Error(), gc.Equals, expectedErrorString) svrError, ok := GetServerError(err) c.Assert(ok, jc.IsTrue) c.Check(svrError.StatusCode, gc.Equals, 400) c.Check(string(result), gc.Equals, expectedResult) } func (suite *ClientSuite) TestClientDispatchRequestRetries503(c *gc.C) { URI := "/some/url/?param1=test" server := newFlakyServer(URI, 503, NumberOfRetries) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) content := "content" request, err := http.NewRequest("GET", server.URL+URI, io.NopCloser(strings.NewReader(content))) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Assert(err, jc.ErrorIsNil) c.Check(*server.nbRequests, gc.Equals, NumberOfRetries+1) expectedRequestsContent := make([][]byte, NumberOfRetries+1) for i := 0; i < NumberOfRetries+1; i++ { expectedRequestsContent[i] = []byte(content) } c.Check(*server.requests, jc.DeepEquals, expectedRequestsContent) } func (suite *ClientSuite) TestClientDispatchRequestRetries409(c *gc.C) { URI := "/some/url/?param1=test" server := newFlakyServer(URI, 409, NumberOfRetries) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) content := "content" request, err := http.NewRequest("GET", server.URL+URI, io.NopCloser(strings.NewReader(content))) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Assert(err, jc.ErrorIsNil) c.Check(*server.nbRequests, gc.Equals, NumberOfRetries+1) } // See https://bugs.launchpad.net/maas/+bug/2039105 func (suite *ClientSuite) TestClientDispatchRequestRetries409WithoutDuplicatedHeaders(c *gc.C) { URI := "/some/url/?param1=test" server := newFlakyServer(URI, 409, NumberOfRetries) defer server.Close() client, err := NewAuthenticatedClient(server.URL, "the:api:key") c.Assert(err, jc.ErrorIsNil) content := "content" request, err := http.NewRequest("GET", server.URL+URI, io.NopCloser(strings.NewReader(content))) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Assert(err, jc.ErrorIsNil) c.Check(*server.nbRequests, gc.Equals, NumberOfRetries+1) for _, headers := range *server.headers { c.Check(len(headers.Values("Authorization")), gc.Equals, 1) } } func (suite *ClientSuite) TestTLSClientDispatchRequestRetries503NilBody(c *gc.C) { URI := "/some/path" server := newFlakyTLSServer(URI, 503, NumberOfRetries) defer server.Close() client, err := NewAnonymousClient(server.URL, "2.0") c.Assert(err, jc.ErrorIsNil) client.HTTPClient = &http.Client{Transport: http.DefaultTransport} client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } request, err := http.NewRequest("GET", server.URL+URI, nil) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Assert(err, jc.ErrorIsNil) c.Check(*server.nbRequests, gc.Equals, NumberOfRetries+1) } func (suite *ClientSuite) TestClientDispatchRequestDoesntRetry200(c *gc.C) { URI := "/some/url/?param1=test" server := newFlakyServer(URI, 200, 10) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) request, err := http.NewRequest("GET", server.URL+URI, nil) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Assert(err, jc.ErrorIsNil) c.Check(*server.nbRequests, gc.Equals, 1) } func (suite *ClientSuite) TestClientDispatchRequestRetriesIsLimited(c *gc.C) { URI := "/some/url/?param1=test" // Make the server return 503 responses NumberOfRetries + 1 times. server := newFlakyServer(URI, 503, NumberOfRetries+1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) request, err := http.NewRequest("GET", server.URL+URI, nil) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Check(*server.nbRequests, gc.Equals, NumberOfRetries+1) svrError, ok := GetServerError(err) c.Assert(ok, jc.IsTrue) c.Assert(svrError.StatusCode, gc.Equals, 503) } func (suite *ClientSuite) TestClientDispatchRequestReturnsNonServerError(c *gc.C) { client, err := NewAnonymousClient("/foo", "1.0") c.Assert(err, jc.ErrorIsNil) // Create a bad request that will fail to dispatch. request, err := http.NewRequest("GET", "/", nil) c.Assert(err, jc.ErrorIsNil) result, err := client.dispatchRequest(request) c.Check(err, gc.NotNil) // This type of failure is an error, but not a ServerError. _, ok := GetServerError(err) c.Assert(ok, jc.IsFalse) // For this kind of error, result is guaranteed to be nil. c.Check(result, gc.IsNil) } func (suite *ClientSuite) TestClientDispatchRequestSignsRequest(c *gc.C) { URI := "/some/url/?param1=test" expectedResult := "expected:result" server := newSingleServingServer(URI, expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAuthenticatedClient(server.URL, "the:api:key") c.Assert(err, jc.ErrorIsNil) request, err := http.NewRequest("GET", server.URL+URI, nil) c.Assert(err, jc.ErrorIsNil) result, err := client.dispatchRequest(request) c.Assert(err, jc.ErrorIsNil) c.Check(string(result), gc.Equals, expectedResult) c.Check((*server.requestHeader)["Authorization"][0], gc.Matches, "^OAuth .*") } func (suite *ClientSuite) TestClientDispatchRequestUsesConfiguredHTTPClient(c *gc.C) { URI := "/some/url/" server := newSingleServingServer(URI, "", 0, 2*time.Second) defer server.Close() client, err := NewAnonymousClient(server.URL, "2.0") c.Assert(err, jc.ErrorIsNil) client.HTTPClient = &http.Client{Timeout: time.Second} request, err := http.NewRequest("GET", server.URL+URI, nil) c.Assert(err, jc.ErrorIsNil) _, err = client.dispatchRequest(request) c.Assert(err, gc.ErrorMatches, `Get "http://127.0.0.1:\d+/some/url/": context deadline exceeded \(Client\.Timeout exceeded while awaiting headers\)`) } func (suite *ClientSuite) TestClientGetFormatsGetParameters(c *gc.C) { URI, err := url.Parse("/some/url") c.Assert(err, jc.ErrorIsNil) expectedResult := "expected:result" params := url.Values{"test": {"123"}} fullURI := URI.String() + "?test=123" server := newSingleServingServer(fullURI, expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) result, err := client.Get(URI, "", params) c.Assert(err, jc.ErrorIsNil) c.Check(string(result), gc.Equals, expectedResult) } func (suite *ClientSuite) TestClientGetFormatsOperationAsGetParameter(c *gc.C) { URI, err := url.Parse("/some/url") c.Assert(err, jc.ErrorIsNil) expectedResult := "expected:result" fullURI := URI.String() + "?op=list" server := newSingleServingServer(fullURI, expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) result, err := client.Get(URI, "list", nil) c.Assert(err, jc.ErrorIsNil) c.Check(string(result), gc.Equals, expectedResult) } func (suite *ClientSuite) TestClientPostSendsRequestWithParams(c *gc.C) { URI, err := url.Parse("/some/url") c.Assert(err, jc.ErrorIsNil) expectedResult := "expected:result" fullURI := URI.String() + "?op=list" params := url.Values{"test": {"123"}} server := newSingleServingServer(fullURI, expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) result, err := client.Post(URI, "list", params, nil) c.Assert(err, jc.ErrorIsNil) c.Check(string(result), gc.Equals, expectedResult) postedValues, err := url.ParseQuery(*server.requestContent) c.Assert(err, jc.ErrorIsNil) expectedPostedValues, err := url.ParseQuery("test=123") c.Assert(err, jc.ErrorIsNil) c.Check(postedValues, jc.DeepEquals, expectedPostedValues) } // extractFileContent extracts from the request built using 'requestContent', // 'requestHeader' and 'requestURL', the file named 'filename'. func extractFileContent(requestContent string, requestHeader *http.Header, requestURL string, _ string) ([]byte, error) { // Recreate the request from server.requestContent to use the parsing // utility from the http package (http.Request.FormFile). request, err := http.NewRequest("POST", requestURL, bytes.NewBufferString(requestContent)) if err != nil { return nil, err } request.Header.Set("Content-Type", requestHeader.Get("Content-Type")) file, _, err := request.FormFile("testfile") if err != nil { return nil, err } fileContent, err := io.ReadAll(file) if err != nil { return nil, err } return fileContent, nil } func (suite *ClientSuite) TestClientPostSendsMultipartRequest(c *gc.C) { URI, err := url.Parse("/some/url") c.Assert(err, jc.ErrorIsNil) expectedResult := "expected:result" fullURI := URI.String() + "?op=add" server := newSingleServingServer(fullURI, expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) fileContent := []byte("content") files := map[string][]byte{"testfile": fileContent} result, err := client.Post(URI, "add", nil, files) c.Assert(err, jc.ErrorIsNil) c.Check(string(result), gc.Equals, expectedResult) receivedFileContent, err := extractFileContent(*server.requestContent, server.requestHeader, fullURI, "testfile") c.Assert(err, jc.ErrorIsNil) c.Check(receivedFileContent, jc.DeepEquals, fileContent) } func (suite *ClientSuite) TestClientPutSendsRequest(c *gc.C) { URI, err := url.Parse("/some/url") c.Assert(err, jc.ErrorIsNil) expectedResult := "expected:result" params := url.Values{"test": {"123"}} server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) result, err := client.Put(URI, params) c.Assert(err, jc.ErrorIsNil) c.Check(string(result), gc.Equals, expectedResult) c.Check(*server.requestContent, gc.Equals, "test=123") } func (suite *ClientSuite) TestClientDeleteSendsRequest(c *gc.C) { URI, err := url.Parse("/some/url") c.Assert(err, jc.ErrorIsNil) expectedResult := "expected:result" server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK, -1) defer server.Close() client, err := NewAnonymousClient(server.URL, "1.0") c.Assert(err, jc.ErrorIsNil) err = client.Delete(URI) c.Assert(err, jc.ErrorIsNil) } func (suite *ClientSuite) TestNewAnonymousClientEnsuresTrailingSlash(c *gc.C) { client, err := NewAnonymousClient("http://example.com/", "1.0") c.Assert(err, jc.ErrorIsNil) expectedURL, err := url.Parse("http://example.com/api/1.0/") c.Assert(err, jc.ErrorIsNil) c.Check(client.APIURL, jc.DeepEquals, expectedURL) } func (suite *ClientSuite) TestNewAuthenticatedClientEnsuresTrailingSlash(c *gc.C) { client, err := NewAuthenticatedClient("http://example.com/api/1.0", "a:b:c") c.Assert(err, jc.ErrorIsNil) expectedURL, err := url.Parse("http://example.com/api/1.0/") c.Assert(err, jc.ErrorIsNil) c.Check(client.APIURL, jc.DeepEquals, expectedURL) } func (suite *ClientSuite) TestNewAuthenticatedClientParsesApiKey(c *gc.C) { // NewAuthenticatedClient returns a plainTextOAuthSigneri configured // to use the given API key. consumerKey := "consumerKey" tokenKey := "tokenKey" tokenSecret := "tokenSecret" keyElements := []string{consumerKey, tokenKey, tokenSecret} apiKey := strings.Join(keyElements, ":") client, err := NewAuthenticatedClient("http://example.com/api/1.0/", apiKey) c.Assert(err, jc.ErrorIsNil) signer := client.Signer.(*plainTextOAuthSigner) c.Check(signer.token.ConsumerKey, gc.Equals, consumerKey) c.Check(signer.token.TokenKey, gc.Equals, tokenKey) c.Check(signer.token.TokenSecret, gc.Equals, tokenSecret) } func (suite *ClientSuite) TestNewAuthenticatedClientFailsIfInvalidKey(c *gc.C) { client, err := NewAuthenticatedClient("", "invalid-key") c.Check(err, gc.ErrorMatches, "invalid API key.*") c.Check(client, gc.IsNil) } func (suite *ClientSuite) TestAddAPIVersionToURL(c *gc.C) { addVersion := AddAPIVersionToURL c.Assert(addVersion("http://example.com/MAAS", "1.0"), gc.Equals, "http://example.com/MAAS/api/1.0/") c.Assert(addVersion("http://example.com/MAAS/", "2.0"), gc.Equals, "http://example.com/MAAS/api/2.0/") } func (suite *ClientSuite) TestSplitVersionedURL(c *gc.C) { check := func(url, expectedBase, expectedVersion string, expectedResult bool) { base, version, ok := SplitVersionedURL(url) c.Check(ok, gc.Equals, expectedResult) c.Check(base, gc.Equals, expectedBase) c.Check(version, gc.Equals, expectedVersion) } check("http://maas.server/MAAS", "http://maas.server/MAAS", "", false) check("http://maas.server/MAAS/api/3.0", "http://maas.server/MAAS/", "3.0", true) check("http://maas.server/MAAS/api/3.0/", "http://maas.server/MAAS/", "3.0", true) check("http://maas.server/MAAS/api/maas", "http://maas.server/MAAS/api/maas", "", false) } golang-github-juju-gomaasapi-2.2.0/controller.go000066400000000000000000000774371451732172100217130ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "path" "strconv" "strings" "sync/atomic" "github.com/juju/collections/set" "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/schema" "github.com/juju/version" ) var ( logger = loggo.GetLogger("maas") // The supported versions should be ordered from most desirable version to // least as they will be tried in order. supportedAPIVersions = []string{"2.0"} // Each of the api versions that change the request or response structure // for any given call should have a value defined for easy definition of // the deserialization functions. twoDotOh = version.Number{Major: 2, Minor: 0} // Current request number. Informational only for logging. requestNumber int64 ) // ControllerArgs is an argument struct for passing the required parameters // to the NewController method. type ControllerArgs struct { BaseURL string APIKey string HTTPClient *http.Client } // NewController creates an authenticated client to the MAAS API, and // checks the capabilities of the server. If the BaseURL specified // includes the API version, that version of the API will be used, // otherwise the controller will use the highest supported version // available. // // If the APIKey is not valid, a NotValid error is returned. // If the credentials are incorrect, a PermissionError is returned. func NewController(args ControllerArgs) (Controller, error) { base, apiVersion, includesVersion := SplitVersionedURL(args.BaseURL) if includesVersion { if !supportedVersion(apiVersion) { return nil, NewUnsupportedVersionError("version %s", apiVersion) } return newControllerWithVersion(base, apiVersion, args.APIKey, args.HTTPClient) } return newControllerUnknownVersion(args) } func supportedVersion(value string) bool { for _, version := range supportedAPIVersions { if value == version { return true } } return false } func newControllerWithVersion(baseURL, apiVersion, apiKey string, httpClient *http.Client) (Controller, error) { major, minor, err := version.ParseMajorMinor(apiVersion) // We should not get an error here. See the test. if err != nil { return nil, errors.Errorf("bad version defined in supported versions: %q", apiVersion) } client, err := NewAuthenticatedClient(AddAPIVersionToURL(baseURL, apiVersion), apiKey) if err != nil { // If the credentials aren't valid, return now. if errors.IsNotValid(err) { return nil, errors.Trace(err) } // Any other error attempting to create the authenticated client // is an unexpected error and return now. return nil, NewUnexpectedError(err) } client.HTTPClient = httpClient controllerVersion := version.Number{ Major: major, Minor: minor, } controller := &controller{client: client, apiVersion: controllerVersion} _, _, controller.capabilities, err = controller.readAPIVersionInfo() if err != nil { logger.Debugf("read version failed: %#v", err) return nil, errors.Trace(err) } if err := controller.checkCreds(); err != nil { return nil, errors.Trace(err) } return controller, nil } func newControllerUnknownVersion(args ControllerArgs) (Controller, error) { // For now we don't need to test multiple versions. It is expected that at // some time in the future, we will try the most up to date version and then // work our way backwards. for _, apiVersion := range supportedAPIVersions { controller, err := newControllerWithVersion(args.BaseURL, apiVersion, args.APIKey, args.HTTPClient) switch { case err == nil: return controller, nil case IsUnsupportedVersionError(err): // This will only come back from APIVersionInfo for 410/404. continue default: return nil, errors.Trace(err) } } return nil, NewUnsupportedVersionError("controller at %s does not support any of %s", args.BaseURL, supportedAPIVersions) } type controller struct { client *Client apiVersion version.Number capabilities set.Strings } // Capabilities implements Controller. func (c *controller) Capabilities() set.Strings { return c.capabilities } // BootResources implements Controller. func (c *controller) BootResources() ([]BootResource, error) { source, err := c.get("boot-resources") if err != nil { return nil, NewUnexpectedError(err) } resources, err := readBootResources(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []BootResource for _, r := range resources { result = append(result, r) } return result, nil } // Fabrics implements Controller. func (c *controller) Fabrics() ([]Fabric, error) { source, err := c.get("fabrics") if err != nil { return nil, NewUnexpectedError(err) } fabrics, err := readFabrics(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []Fabric for _, f := range fabrics { result = append(result, f) } return result, nil } // Spaces implements Controller. func (c *controller) Spaces() ([]Space, error) { source, err := c.get("spaces") if err != nil { return nil, NewUnexpectedError(err) } spaces, err := readSpaces(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []Space for _, space := range spaces { result = append(result, space) } return result, nil } // StaticRoutes implements Controller. func (c *controller) StaticRoutes() ([]StaticRoute, error) { source, err := c.get("static-routes") if err != nil { return nil, NewUnexpectedError(err) } staticRoutes, err := readStaticRoutes(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []StaticRoute for _, staticRoute := range staticRoutes { result = append(result, staticRoute) } return result, nil } // Zones implements Controller. func (c *controller) Zones() ([]Zone, error) { source, err := c.get("zones") if err != nil { return nil, NewUnexpectedError(err) } zones, err := readZones(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []Zone for _, z := range zones { result = append(result, z) } return result, nil } // Pools implements Controller. func (c *controller) Pools() ([]Pool, error) { var result []Pool source, err := c.get("pools") if err != nil { return nil, NewUnexpectedError(err) } pools, err := readPools(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } for _, p := range pools { result = append(result, p) } return result, nil } // Domains implements Controller func (c *controller) Domains() ([]Domain, error) { source, err := c.get("domains") if err != nil { return nil, NewUnexpectedError(err) } domains, err := readDomains(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []Domain for _, domain := range domains { result = append(result, domain) } return result, nil } // DevicesArgs is a argument struct for selecting Devices. // Only devices that match the specified criteria are returned. type DevicesArgs struct { Hostname []string MACAddresses []string SystemIDs []string Domain string Zone string Pool string AgentName string } // Devices implements Controller. func (c *controller) Devices(args DevicesArgs) ([]Device, error) { params := NewURLParams() params.MaybeAddMany("hostname", args.Hostname) params.MaybeAddMany("mac_address", args.MACAddresses) params.MaybeAddMany("id", args.SystemIDs) params.MaybeAdd("domain", args.Domain) params.MaybeAdd("zone", args.Zone) params.MaybeAdd("pool", args.Pool) params.MaybeAdd("agent_name", args.AgentName) source, err := c.getQuery("devices", params.Values) if err != nil { return nil, NewUnexpectedError(err) } devices, err := readDevices(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []Device for _, d := range devices { d.controller = c result = append(result, d) } return result, nil } // CreateDeviceArgs is a argument struct for passing information into CreateDevice. type CreateDeviceArgs struct { Hostname string MACAddresses []string Domain string Parent string } // Devices implements Controller. func (c *controller) CreateDevice(args CreateDeviceArgs) (Device, error) { // There must be at least one mac address. if len(args.MACAddresses) == 0 { return nil, NewBadRequestError("at least one MAC address must be specified") } params := NewURLParams() params.MaybeAdd("hostname", args.Hostname) params.MaybeAdd("domain", args.Domain) params.MaybeAddMany("mac_addresses", args.MACAddresses) params.MaybeAdd("parent", args.Parent) result, err := c.post("devices", "", 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) } device, err := readDevice(c.apiVersion, result) if err != nil { return nil, errors.Trace(err) } device.controller = c return device, nil } // MachinesArgs is a argument struct for selecting Machines. // Only machines that match the specified criteria are returned. type MachinesArgs struct { Hostnames []string MACAddresses []string SystemIDs []string Domain string Zone string Pool string AgentName string Tags []string OwnerData map[string]string } // Machines implements Controller. func (c *controller) Machines(args MachinesArgs) ([]Machine, error) { params := NewURLParams() params.MaybeAddMany("hostname", args.Hostnames) params.MaybeAddMany("mac_address", args.MACAddresses) params.MaybeAddMany("id", args.SystemIDs) params.MaybeAdd("domain", args.Domain) params.MaybeAdd("zone", args.Zone) params.MaybeAdd("pool", args.Pool) params.MaybeAdd("agent_name", args.AgentName) params.MaybeAddMany("tags", args.Tags) // At the moment the MAAS API doesn't support filtering by owner // data so we do that ourselves below. source, err := c.getQuery("machines", params.Values) if err != nil { return nil, NewUnexpectedError(err) } machines, err := readMachines(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []Machine for _, m := range machines { m.controller = c if ownerDataMatches(m.ownerData, args.OwnerData) { result = append(result, m) } } return result, nil } func ownerDataMatches(ownerData, filter map[string]string) bool { for key, value := range filter { if ownerData[key] != value { return false } } return true } // StorageSpec represents one element of storage constraints necessary // to be satisfied to allocate a machine. type StorageSpec struct { // Label is optional and an arbitrary string. Labels need to be unique // across the StorageSpec elements specified in the AllocateMachineArgs. Label string // Size is required and refers to the required minimum size in GB. Size int // Zero or more tags associated to the disks. Tags []string } // Validate ensures that there is a positive size and that there are no Empty // tag values. func (s *StorageSpec) Validate() error { if s.Size <= 0 { return errors.NotValidf("Size value %d", s.Size) } for _, v := range s.Tags { if v == "" { return errors.NotValidf("empty tag") } } return nil } // String returns the string representation of the storage spec. func (s *StorageSpec) String() string { label := s.Label if label != "" { label += ":" } tags := strings.Join(s.Tags, ",") if tags != "" { tags = "(" + tags + ")" } return fmt.Sprintf("%s%d%s", label, s.Size, tags) } // InterfaceSpec represents one element of network related constraints. type InterfaceSpec struct { // Label is required and an arbitrary string. Labels need to be unique // across the InterfaceSpec elements specified in the AllocateMachineArgs. // The label is returned in the ConstraintMatches response from // AllocateMachine. Label string Space string // NOTE: there are other interface spec values that we are not exposing at // this stage that can be added on an as needed basis. Other possible values are: // 'fabric_class', 'not_fabric_class', // 'subnet_cidr', 'not_subnet_cidr', // 'vid', 'not_vid', // 'fabric', 'not_fabric', // 'subnet', 'not_subnet', // 'mode' } // Validate ensures that a Label is specified and that there is at least one // Space or NotSpace value set. func (a *InterfaceSpec) Validate() error { if a.Label == "" { return errors.NotValidf("missing Label") } // Perhaps at some stage in the future there will be other possible specs // supported (like vid, subnet, etc), but until then, just space to check. if a.Space == "" { return errors.NotValidf("empty Space constraint") } return nil } // String returns the interface spec as MaaS requires it. func (a *InterfaceSpec) String() string { return fmt.Sprintf("%s:space=%s", a.Label, a.Space) } // AllocateMachineArgs is an argument struct for passing args into Machine.Allocate. type AllocateMachineArgs struct { Hostname string SystemId string Architecture string MinCPUCount int // MinMemory represented in MB. MinMemory int Tags []string NotTags []string Zone string Pool string NotInZone []string NotInPool []string // Storage represents the required disks on the Machine. If any are specified // the first value is used for the root disk. Storage []StorageSpec // Interfaces represents a number of required interfaces on the machine. // Each InterfaceSpec relates to an individual network interface. Interfaces []InterfaceSpec // NotSpace is a machine level constraint, and applies to the entire machine // rather than specific interfaces. NotSpace []string AgentName string Comment string DryRun bool } // Validate makes sure that any labels specified in Storage or Interfaces // are unique, and that the required specifications are valid. It // also makes sure that any pools specified exist. func (a *AllocateMachineArgs) Validate() error { storageLabels := set.NewStrings() for _, spec := range a.Storage { if err := spec.Validate(); err != nil { return errors.Annotate(err, "Storage") } if spec.Label != "" { if storageLabels.Contains(spec.Label) { return errors.NotValidf("reusing storage label %q", spec.Label) } storageLabels.Add(spec.Label) } } interfaceLabels := set.NewStrings() for _, spec := range a.Interfaces { if err := spec.Validate(); err != nil { return errors.Annotate(err, "Interfaces") } if interfaceLabels.Contains(spec.Label) { return errors.NotValidf("reusing interface label %q", spec.Label) } interfaceLabels.Add(spec.Label) } for _, v := range a.NotSpace { if v == "" { return errors.NotValidf("empty NotSpace constraint") } } return nil } func (a *AllocateMachineArgs) storage() string { var values []string for _, spec := range a.Storage { values = append(values, spec.String()) } return strings.Join(values, ",") } func (a *AllocateMachineArgs) interfaces() string { var values []string for _, spec := range a.Interfaces { values = append(values, spec.String()) } return strings.Join(values, ";") } func (a *AllocateMachineArgs) notSubnets() []string { var values []string for _, v := range a.NotSpace { values = append(values, "space:"+v) } return values } // ConstraintMatches provides a way for the caller of AllocateMachine to determine //.how the allocated machine matched the storage and interfaces constraints specified. // The labels that were used in the constraints are the keys in the maps. type ConstraintMatches struct { // Interface is a mapping of the constraint label specified to the Interfaces // that match that constraint. Interfaces map[string][]Interface // Storage is a mapping of the constraint label specified to the StorageDevice // that match that constraint. Storage map[string][]StorageDevice } // AllocateMachine implements Controller. // // Returns an error that satisfies IsNoMatchError if the requested // constraints cannot be met. func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, ConstraintMatches, error) { var matches ConstraintMatches params := NewURLParams() params.MaybeAdd("name", args.Hostname) params.MaybeAdd("system_id", args.SystemId) params.MaybeAdd("arch", args.Architecture) params.MaybeAddInt("cpu_count", args.MinCPUCount) params.MaybeAddInt("mem", args.MinMemory) params.MaybeAddMany("tags", args.Tags) params.MaybeAddMany("not_tags", args.NotTags) params.MaybeAdd("storage", args.storage()) params.MaybeAdd("interfaces", args.interfaces()) params.MaybeAddMany("not_subnets", args.notSubnets()) params.MaybeAdd("zone", args.Zone) params.MaybeAdd("pool", args.Pool) params.MaybeAddMany("not_in_zone", args.NotInZone) params.MaybeAddMany("not_in_pool", args.NotInPool) params.MaybeAdd("agent_name", args.AgentName) params.MaybeAdd("comment", args.Comment) params.MaybeAddBool("dry_run", args.DryRun) result, err := c.post("machines", "allocate", params.Values) if err != nil { // A 409 Status code is "No Matching Machines" if svrErr, ok := errors.Cause(err).(ServerError); ok { if svrErr.StatusCode == http.StatusConflict { return nil, matches, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) } } // Translate http errors. return nil, matches, NewUnexpectedError(err) } machine, err := readMachine(c.apiVersion, result) if err != nil { return nil, matches, errors.Trace(err) } machine.controller = c // Parse the constraint matches. matches, err = parseAllocateConstraintsResponse(result, machine) if err != nil { return nil, matches, errors.Trace(err) } return machine, matches, nil } // 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 } // ReleaseMachines implements Controller. // // Release multiple machines at once. Returns // - BadRequestError if any of the machines cannot be found // - PermissionError if the user does not have permission to release any of the machines // - CannotCompleteError if any of the machines could not be released due to their current state func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error { params := NewURLParams() params.MaybeAddMany("machines", args.SystemIDs) params.MaybeAdd("comment", args.Comment) _, err := c.post("machines", "release", params.Values) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { switch svrErr.StatusCode { case http.StatusBadRequest: return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) case http.StatusForbidden: return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) case http.StatusConflict: return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage)) } } return NewUnexpectedError(err) } return nil } // Files implements Controller. func (c *controller) Files(prefix string) ([]File, error) { params := NewURLParams() params.MaybeAdd("prefix", prefix) source, err := c.getQuery("files", params.Values) if err != nil { return nil, NewUnexpectedError(err) } files, err := readFiles(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } var result []File for _, f := range files { f.controller = c result = append(result, f) } return result, nil } // GetFile implements Controller. func (c *controller) GetFile(filename string) (File, error) { if filename == "" { return nil, errors.NotValidf("missing filename") } source, err := c.get("files/" + filename) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { if svrErr.StatusCode == http.StatusNotFound { return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) } } return nil, NewUnexpectedError(err) } file, err := readFile(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } file.controller = c return file, nil } // AddFileArgs is a argument struct for passing information into AddFile. // One of Content or (Reader, Length) must be specified. type AddFileArgs struct { Filename string Content []byte Reader io.Reader Length int64 } // Validate checks to make sure the filename has no slashes, and that one of // Content or (Reader, Length) is specified. func (a *AddFileArgs) Validate() error { dir, _ := path.Split(a.Filename) if dir != "" { return errors.NotValidf("paths in Filename %q", a.Filename) } if a.Filename == "" { return errors.NotValidf("missing Filename") } if a.Content == nil { if a.Reader == nil { return errors.NotValidf("missing Content or Reader") } if a.Length == 0 { return errors.NotValidf("missing Length") } } else { if a.Reader != nil { return errors.NotValidf("specifying Content and Reader") } if a.Length != 0 { return errors.NotValidf("specifying Length and Content") } } return nil } // AddFile implements Controller. func (c *controller) AddFile(args AddFileArgs) error { if err := args.Validate(); err != nil { return errors.Trace(err) } fileContent := args.Content if fileContent == nil { content, err := ioutil.ReadAll(io.LimitReader(args.Reader, args.Length)) if err != nil { return errors.Annotatef(err, "cannot read file content") } fileContent = content } params := url.Values{"filename": {args.Filename}} _, err := c.postFile("files", "", params, fileContent) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { if svrErr.StatusCode == http.StatusBadRequest { return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) } } return NewUnexpectedError(err) } return nil } func (c *controller) checkCreds() error { if _, err := c.getOp("users", "whoami"); err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { if svrErr.StatusCode == http.StatusUnauthorized { return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) } } return NewUnexpectedError(err) } return nil } func (c *controller) put(path string, params url.Values) (interface{}, error) { path = EnsureTrailingSlash(path) requestID := nextRequestID() logger.Tracef("request %x: PUT %s%s, params: %s", requestID, c.client.APIURL, path, params.Encode()) bytes, err := c.client.Put(&url.URL{Path: path}, params) if err != nil { logger.Tracef("response %x: error: %q", requestID, err.Error()) logger.Tracef("error detail: %#v", err) return nil, errors.Trace(err) } logger.Tracef("response %x: %s", requestID, string(bytes)) var parsed interface{} err = json.Unmarshal(bytes, &parsed) if err != nil { return nil, errors.Trace(err) } return parsed, nil } func (c *controller) post(path, op string, params url.Values) (interface{}, error) { bytes, err := c._postRaw(path, op, params, nil) if err != nil { return nil, errors.Trace(err) } var parsed interface{} err = json.Unmarshal(bytes, &parsed) if err != nil { return nil, errors.Trace(err) } return parsed, nil } func (c *controller) postFile(path, op string, params url.Values, fileContent []byte) (interface{}, error) { // Only one file is ever sent at a time. files := map[string][]byte{"file": fileContent} return c._postRaw(path, op, params, files) } func (c *controller) _postRaw(path, op string, params url.Values, files map[string][]byte) ([]byte, error) { path = EnsureTrailingSlash(path) requestID := nextRequestID() if logger.IsTraceEnabled() { opArg := "" if op != "" { opArg = "?op=" + op } logger.Tracef("request %x: POST %s%s%s, params=%s", requestID, c.client.APIURL, path, opArg, params.Encode()) } bytes, err := c.client.Post(&url.URL{Path: path}, op, params, files) if err != nil { logger.Tracef("response %x: error: %q", requestID, err.Error()) logger.Tracef("error detail: %#v", err) return nil, errors.Trace(err) } logger.Tracef("response %x: %s", requestID, string(bytes)) return bytes, nil } func (c *controller) delete(path string) error { path = EnsureTrailingSlash(path) requestID := nextRequestID() logger.Tracef("request %x: DELETE %s%s", requestID, c.client.APIURL, path) err := c.client.Delete(&url.URL{Path: path}) if err != nil { logger.Tracef("response %x: error: %q", requestID, err.Error()) logger.Tracef("error detail: %#v", err) return errors.Trace(err) } logger.Tracef("response %x: complete", requestID) return nil } func (c *controller) getQuery(path string, params url.Values) (interface{}, error) { return c._get(path, "", params) } func (c *controller) get(path string) (interface{}, error) { return c._get(path, "", nil) } func (c *controller) getOp(path, op string) (interface{}, error) { return c._get(path, op, nil) } func (c *controller) _get(path, op string, params url.Values) (interface{}, error) { bytes, err := c._getRaw(path, op, params) if err != nil { return nil, errors.Trace(err) } var parsed interface{} err = json.Unmarshal(bytes, &parsed) if err != nil { return nil, errors.Trace(err) } return parsed, nil } func (c *controller) _getRaw(path, op string, params url.Values) ([]byte, error) { path = EnsureTrailingSlash(path) requestID := nextRequestID() if logger.IsTraceEnabled() { var query string if params != nil { query = "?" + params.Encode() } logger.Tracef("request %x: GET %s%s%s", requestID, c.client.APIURL, path, query) } bytes, err := c.client.Get(&url.URL{Path: path}, op, params) if err != nil { logger.Tracef("response %x: error: %q", requestID, err.Error()) logger.Tracef("error detail: %#v", err) return nil, errors.Trace(err) } logger.Tracef("response %x: %s", requestID, string(bytes)) return bytes, nil } func nextRequestID() int64 { return atomic.AddInt64(&requestNumber, 1) } func indicatesUnsupportedVersion(err error) bool { if err == nil { return false } if serverErr, ok := errors.Cause(err).(ServerError); ok { code := serverErr.StatusCode return code == http.StatusNotFound || code == http.StatusGone } // Workaround for bug in MAAS 1.9.4 - instead of a 404 we get a // redirect to the HTML login page, which doesn't parse as JSON. // https://bugs.launchpad.net/maas/+bug/1583715 if syntaxErr, ok := errors.Cause(err).(*json.SyntaxError); ok { message := "invalid character '<' looking for beginning of value" return syntaxErr.Offset == 1 && syntaxErr.Error() == message } return false } // APIVersionInfo returns the version and subversion strings for the MAAS // controller. func (c *controller) APIVersionInfo() (string, string, error) { version, subversion, _, err := c.readAPIVersionInfo() return version, subversion, err } func (c *controller) readAPIVersionInfo() (string, string, set.Strings, error) { parsed, err := c.get("version") if indicatesUnsupportedVersion(err) { return "", "", nil, WrapWithUnsupportedVersionError(err) } else if err != nil { return "", "", nil, errors.Trace(err) } fields := schema.Fields{ "capabilities": schema.List(schema.String()), "version": schema.String(), "subversion": schema.String(), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(parsed, nil) if err != nil { return "", "", nil, WrapWithDeserializationError(err, "version response") } // For now, we don't append any subversion, but as it becomes used, we // should parse and check. valid := coerced.(map[string]interface{}) // From here we know that the map returned from the schema coercion // contains fields of the right type. capabilities := set.NewStrings() capabilityValues := valid["capabilities"].([]interface{}) for _, value := range capabilityValues { capabilities.Add(value.(string)) } version := valid["version"].(string) subversion := valid["subversion"].(string) return version, subversion, capabilities, nil } func parseAllocateConstraintsResponse(source interface{}, machine *machine) (ConstraintMatches, error) { var empty ConstraintMatches matchFields := schema.Fields{ "storage": schema.StringMap(schema.List(schema.Any())), "interfaces": schema.StringMap(schema.List(schema.ForceInt())), } matchDefaults := schema.Defaults{ "storage": schema.Omit, "interfaces": schema.Omit, } fields := schema.Fields{ "constraints_by_type": schema.FieldMap(matchFields, matchDefaults), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return empty, WrapWithDeserializationError(err, "allocation constraints response schema check failed") } valid := coerced.(map[string]interface{}) constraintsMap := valid["constraints_by_type"].(map[string]interface{}) result := ConstraintMatches{ Interfaces: make(map[string][]Interface), Storage: make(map[string][]StorageDevice), } if interfaceMatches, found := constraintsMap["interfaces"]; found { matches := convertConstraintMatchesInt(interfaceMatches) for label, ids := range matches { interfaces := make([]Interface, len(ids)) for index, id := range ids { iface := machine.Interface(id) if iface == nil { return empty, NewDeserializationError("constraint match interface %q: %d does not match an interface for the machine", label, id) } interfaces[index] = iface } result.Interfaces[label] = interfaces } } if storageMatches, found := constraintsMap["storage"]; found { matches := convertConstraintMatchesAny(storageMatches) for label, ids := range matches { storageDevices := make([]StorageDevice, len(ids)) for index, storageId := range ids { // The key value can be either an `int` which `json.Unmarshal` converts to a `float64` or a // `string` when the key is "partition:{part_id}". if id, ok := storageId.(float64); ok { // Links to a block device. blockDevice := machine.BlockDevice(int(id)) if blockDevice == nil { return empty, NewDeserializationError("constraint match storage %q: %d does not match a block device for the machine", label, int(id)) } storageDevices[index] = blockDevice } else if id, ok := storageId.(string); ok { // Should link to a partition. const partPrefix = "partition:" if !strings.HasPrefix(id, partPrefix) { return empty, NewDeserializationError("constraint match storage %q: %s is not prefixed with partition", label, id) } partId, err := strconv.Atoi(id[len(partPrefix):]) if err != nil { return empty, NewDeserializationError("constraint match storage %q: %s cannot convert to int.", label, id[len(partPrefix):]) } partition := machine.Partition(partId) if partition == nil { return empty, NewDeserializationError("constraint match storage %q: %d does not match a partition for the machine", label, partId) } storageDevices[index] = partition } else { return empty, NewDeserializationError("constraint match storage %q: %v is not an int or string", label, storageId) } } result.Storage[label] = storageDevices } } return result, nil } func convertConstraintMatchesInt(source interface{}) map[string][]int { // These casts are all safe because of the schema check. result := make(map[string][]int) matchMap := source.(map[string]interface{}) for label, values := range matchMap { items := values.([]interface{}) result[label] = make([]int, len(items)) for index, value := range items { result[label][index] = value.(int) } } return result } func convertConstraintMatchesAny(source interface{}) map[string][]interface{} { // These casts are all safe because of the schema check. result := make(map[string][]interface{}) matchMap := source.(map[string]interface{}) for label, values := range matchMap { items := values.([]interface{}) result[label] = make([]interface{}, len(items)) for index, value := range items { result[label][index] = value } } return result } // Tags implements Controller. func (c *controller) Tags() ([]Tag, error) { source, err := c.getQuery("tags", nil) if err != nil { return nil, NewUnexpectedError(err) } tags, err := readTags(c.apiVersion, source) if err != nil { return nil, errors.Trace(err) } result := make([]Tag, len(tags)) for i, tag := range tags { result[i] = tag } return result, nil } golang-github-juju-gomaasapi-2.2.0/controller_test.go000066400000000000000000000746071451732172100227460ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "bytes" "io/ioutil" "net/http" "net/url" "github.com/juju/collections/set" "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type versionSuite struct { } var _ = gc.Suite(&versionSuite{}) func (*versionSuite) TestSupportedVersions(c *gc.C) { for _, apiVersion := range supportedAPIVersions { _, _, err := version.ParseMajorMinor(apiVersion) c.Check(err, jc.ErrorIsNil) } } type controllerSuite struct { testing.LoggingCleanupSuite server *SimpleTestServer } var _ = gc.Suite(&controllerSuite{}) func (s *controllerSuite) SetUpTest(c *gc.C) { s.LoggingCleanupSuite.SetUpTest(c) loggo.GetLogger("").SetLogLevel(loggo.TRACE) server := NewSimpleServer() server.AddGetResponse("/api/2.0/boot-resources/", http.StatusOK, bootResourcesResponse) server.AddGetResponse("/api/2.0/devices/", http.StatusOK, devicesResponse) server.AddGetResponse("/api/2.0/fabrics/", http.StatusOK, fabricResponse) server.AddGetResponse("/api/2.0/files/", http.StatusOK, filesResponse) server.AddGetResponse("/api/2.0/machines/", http.StatusOK, machinesResponse) server.AddGetResponse("/api/2.0/machines/?hostname=untasted-markita", http.StatusOK, "["+machineResponse+"]") server.AddGetResponse("/api/2.0/spaces/", http.StatusOK, spacesResponse) server.AddGetResponse("/api/2.0/static-routes/", http.StatusOK, staticRoutesResponse) server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, `"captain awesome"`) server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse) server.AddGetResponse("/api/2.0/zones/", http.StatusOK, zoneResponse) server.AddGetResponse("/api/2.0/pools/", http.StatusOK, poolResponse) server.Start() s.AddCleanup(func(*gc.C) { server.Close() }) s.server = server } func (s *controllerSuite) getController(c *gc.C) Controller { controller, err := NewController(ControllerArgs{ BaseURL: s.server.URL, APIKey: "fake:as:key", }) c.Assert(err, jc.ErrorIsNil) return controller } func (s *controllerSuite) TestNewController(c *gc.C) { controller := s.getController(c) expectedCapabilities := set.NewStrings( NetworksManagement, StaticIPAddresses, IPv6DeploymentUbuntu, DevicesManagement, StorageDeploymentUbuntu, NetworkDeploymentUbuntu, ) capabilities := controller.Capabilities() c.Assert(capabilities.Difference(expectedCapabilities), gc.HasLen, 0) c.Assert(expectedCapabilities.Difference(capabilities), gc.HasLen, 0) } func (s *controllerSuite) TestNewControllerBadAPIKeyFormat(c *gc.C) { server := NewSimpleServer() server.Start() defer server.Close() _, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "invalid", }) c.Assert(err, jc.Satisfies, errors.IsNotValid) } func (s *controllerSuite) TestNewControllerNoSupport(c *gc.C) { server := NewSimpleServer() server.Start() defer server.Close() _, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (s *controllerSuite) TestNewControllerBadCreds(c *gc.C) { server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusUnauthorized, "naughty") server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse) server.Start() defer server.Close() _, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(err, jc.Satisfies, IsPermissionError) } func (s *controllerSuite) TestNewControllerUnexpected(c *gc.C) { server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusConflict, "naughty") server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse) server.Start() defer server.Close() _, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(err, jc.Satisfies, IsUnexpectedError) } func (s *controllerSuite) TestNewControllerKnownVersion(c *gc.C) { // Using a server URL including the version should work. officialController, err := NewController(ControllerArgs{ BaseURL: s.server.URL + "/api/2.0/", APIKey: "fake:as:key", }) c.Assert(err, jc.ErrorIsNil) rawController, ok := officialController.(*controller) c.Assert(ok, jc.IsTrue) c.Assert(rawController.apiVersion, gc.Equals, version.Number{ Major: 2, Minor: 0, }) } func (s *controllerSuite) TestNewControllerUnsupportedVersionSpecified(c *gc.C) { // Ensure the server would actually respond to the version if it // was asked. s.server.AddGetResponse("/api/3.0/users/?op=whoami", http.StatusOK, `"captain awesome"`) s.server.AddGetResponse("/api/3.0/version/", http.StatusOK, versionResponse) // Using a server URL including a version that isn't in the known // set should be denied. controller, err := NewController(ControllerArgs{ BaseURL: s.server.URL + "/api/3.0/", APIKey: "fake:as:key", }) c.Assert(controller, gc.IsNil) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (s *controllerSuite) TestNewControllerNotHidingErrors(c *gc.C) { // We should only treat 404 and 410 as "this version isn't // supported". Other errors should be returned up the stack // unchanged, so we don't confuse transient network errors with // version mismatches. lp:1667095 server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, "underwater woman") server.AddGetResponse("/api/2.0/version/", http.StatusInternalServerError, "kablooey") server.Start() defer server.Close() controller, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(controller, gc.IsNil) c.Assert(err, gc.ErrorMatches, `ServerError: 500 Internal Server Error \(kablooey\)`) } func (s *controllerSuite) TestNewController410(c *gc.C) { // We should only treat 404 and 410 as "this version isn't // supported". Other errors should be returned up the stack // unchanged, so we don't confuse transient network errors with // version mismatches. lp:1667095 server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, "the answer to all your prayers") server.AddGetResponse("/api/2.0/version/", http.StatusGone, "cya") server.Start() defer server.Close() controller, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(controller, gc.IsNil) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (s *controllerSuite) TestNewController404(c *gc.C) { // We should only treat 404 and 410 as "this version isn't // supported". Other errors should be returned up the stack // unchanged, so we don't confuse transient network errors with // version mismatches. lp:1667095 server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, "the answer to all your prayers") server.AddGetResponse("/api/2.0/version/", http.StatusNotFound, "huh?") server.Start() defer server.Close() controller, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(controller, gc.IsNil) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (s *controllerSuite) TestNewControllerWith194Bug(c *gc.C) { // 1.9.4 has a bug where if you ask for /api/2.0/version/ without // being logged in (rather than OAuth connection) it redirects you // to the login page. This is fixed in 1.9.5, but we should work // around it anyway. https://bugs.launchpad.net/maas/+bug/1583715 server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, "the answer to all your prayers") server.AddGetResponse("/api/2.0/version/", http.StatusOK, "") server.Start() defer server.Close() controller, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(controller, gc.IsNil) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (s *controllerSuite) TestBootResources(c *gc.C) { controller := s.getController(c) resources, err := controller.BootResources() c.Assert(err, jc.ErrorIsNil) c.Assert(resources, gc.HasLen, 5) } func (s *controllerSuite) TestAPIVersionInfo(c *gc.C) { s.server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse) controller := s.getController(c) version, subversion, err := controller.APIVersionInfo() c.Assert(err, jc.ErrorIsNil) c.Assert(version, gc.Equals, "2.5.0 from source") c.Assert(subversion, gc.Equals, "git+2f25a2cc0930c0e411106f119bc455c161d75b1a") } func (s *controllerSuite) TestDevices(c *gc.C) { controller := s.getController(c) devices, err := controller.Devices(DevicesArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 1) } func (s *controllerSuite) TestDevicesArgs(c *gc.C) { controller := s.getController(c) // This will fail with a 404 due to the test server not having something at // that address, but we don't care, all we want to do is capture the request // and make sure that all the values were set. controller.Devices(DevicesArgs{ Hostname: []string{"untasted-markita"}, MACAddresses: []string{"something"}, SystemIDs: []string{"something-else"}, Domain: "magic", Zone: "foo", AgentName: "agent 42", }) request := s.server.LastRequest() // There should be one entry in the form values for each of the args. c.Assert(request.URL.Query(), gc.HasLen, 6) } func (s *controllerSuite) TestCreateDevice(c *gc.C) { s.server.AddPostResponse("/api/2.0/devices/?op=", http.StatusOK, deviceResponse) controller := s.getController(c) device, err := controller.CreateDevice(CreateDeviceArgs{ MACAddresses: []string{"a-mac-address"}, }) c.Assert(err, jc.ErrorIsNil) c.Assert(device.SystemID(), gc.Equals, "4y3haf") } func (s *controllerSuite) TestCreateDeviceMissingAddress(c *gc.C) { controller := s.getController(c) _, err := controller.CreateDevice(CreateDeviceArgs{}) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "at least one MAC address must be specified") } func (s *controllerSuite) TestCreateDeviceBadRequest(c *gc.C) { s.server.AddPostResponse("/api/2.0/devices/?op=", http.StatusBadRequest, "some error") controller := s.getController(c) _, err := controller.CreateDevice(CreateDeviceArgs{ MACAddresses: []string{"a-mac-address"}, }) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "some error") } func (s *controllerSuite) TestCreateDeviceArgs(c *gc.C) { s.server.AddPostResponse("/api/2.0/devices/?op=", http.StatusOK, deviceResponse) controller := s.getController(c) // Create an arg structure that sets all the values. args := CreateDeviceArgs{ Hostname: "foobar", MACAddresses: []string{"an-address"}, Domain: "a domain", Parent: "parent", } _, err := controller.CreateDevice(args) c.Assert(err, jc.ErrorIsNil) request := s.server.LastRequest() // There should be one entry in the form values for each of the args. c.Assert(request.PostForm, gc.HasLen, 4) } func (s *controllerSuite) TestFabrics(c *gc.C) { controller := s.getController(c) fabrics, err := controller.Fabrics() c.Assert(err, jc.ErrorIsNil) c.Assert(fabrics, gc.HasLen, 2) } func (s *controllerSuite) TestSpaces(c *gc.C) { controller := s.getController(c) spaces, err := controller.Spaces() c.Assert(err, jc.ErrorIsNil) c.Assert(spaces, gc.HasLen, 1) } func (s *controllerSuite) TestStaticRoutes(c *gc.C) { controller := s.getController(c) staticRoutes, err := controller.StaticRoutes() c.Assert(err, jc.ErrorIsNil) c.Assert(staticRoutes, gc.HasLen, 1) } func (s *controllerSuite) TestZones(c *gc.C) { controller := s.getController(c) zones, err := controller.Zones() c.Assert(err, jc.ErrorIsNil) c.Assert(zones, gc.HasLen, 2) } func (s *controllerSuite) TestPools(c *gc.C) { controller := s.getController(c) pools, err := controller.Pools() c.Assert(err, jc.ErrorIsNil) c.Assert(pools, gc.HasLen, 2) } func (s *controllerSuite) TestMachines(c *gc.C) { controller := s.getController(c) machines, err := controller.Machines(MachinesArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 3) } func (s *controllerSuite) TestMachinesFilter(c *gc.C) { controller := s.getController(c) machines, err := controller.Machines(MachinesArgs{ Hostnames: []string{"untasted-markita"}, }) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 1) c.Assert(machines[0].Hostname(), gc.Equals, "untasted-markita") } func (s *controllerSuite) TestMachinesFilterWithOwnerData(c *gc.C) { controller := s.getController(c) machines, err := controller.Machines(MachinesArgs{ Hostnames: []string{"untasted-markita"}, OwnerData: map[string]string{ "fez": "jim crawford", }, }) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 0) } func (s *controllerSuite) TestMachinesFilterWithOwnerData_MultipleMatches(c *gc.C) { controller := s.getController(c) machines, err := controller.Machines(MachinesArgs{ OwnerData: map[string]string{ "braid": "jonathan blow", }, }) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 2) c.Assert(machines[0].Hostname(), gc.Equals, "lowlier-glady") c.Assert(machines[1].Hostname(), gc.Equals, "icier-nina") } func (s *controllerSuite) TestMachinesFilterWithOwnerData_RequiresAllMatch(c *gc.C) { controller := s.getController(c) machines, err := controller.Machines(MachinesArgs{ OwnerData: map[string]string{ "braid": "jonathan blow", "frog-fractions": "jim crawford", }, }) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 1) c.Assert(machines[0].Hostname(), gc.Equals, "lowlier-glady") } func (s *controllerSuite) TestMachinesArgs(c *gc.C) { controller := s.getController(c) // This will fail with a 404 due to the test server not having something at // that address, but we don't care, all we want to do is capture the request // and make sure that all the values were set. controller.Machines(MachinesArgs{ Hostnames: []string{"untasted-markita"}, MACAddresses: []string{"something"}, SystemIDs: []string{"something-else"}, Domain: "magic", Zone: "foo", Pool: "swimming_is_fun", AgentName: "agent 42", }) request := s.server.LastRequest() // There should be one entry in the form values for each of the args. c.Assert(request.URL.Query(), gc.HasLen, 7) } func (s *controllerSuite) TestStorageSpec(c *gc.C) { for i, test := range []struct { spec StorageSpec err string repr string }{{ spec: StorageSpec{}, err: "Size value 0 not valid", }, { spec: StorageSpec{Size: -10}, err: "Size value -10 not valid", }, { spec: StorageSpec{Size: 200}, repr: "200", }, { spec: StorageSpec{Label: "foo", Size: 200}, repr: "foo:200", }, { spec: StorageSpec{Size: 200, Tags: []string{"foo", ""}}, err: "empty tag not valid", }, { spec: StorageSpec{Size: 200, Tags: []string{"foo"}}, repr: "200(foo)", }, { spec: StorageSpec{Label: "omg", Size: 200, Tags: []string{"foo", "bar"}}, repr: "omg:200(foo,bar)", }} { c.Logf("test %d", i) err := test.spec.Validate() if test.err == "" { c.Assert(err, jc.ErrorIsNil) c.Assert(test.spec.String(), gc.Equals, test.repr) } else { c.Assert(err, jc.Satisfies, errors.IsNotValid) c.Assert(err.Error(), gc.Equals, test.err) } } } func (s *controllerSuite) TestInterfaceSpec(c *gc.C) { for i, test := range []struct { spec InterfaceSpec err string repr string }{{ spec: InterfaceSpec{}, err: "missing Label not valid", }, { spec: InterfaceSpec{Label: "foo"}, err: "empty Space constraint not valid", }, { spec: InterfaceSpec{Label: "foo", Space: "magic"}, repr: "foo:space=magic", }} { c.Logf("test %d", i) err := test.spec.Validate() if test.err == "" { c.Check(err, jc.ErrorIsNil) c.Check(test.spec.String(), gc.Equals, test.repr) } else { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, test.err) } } } func (s *controllerSuite) TestAllocateMachineArgs(c *gc.C) { for i, test := range []struct { args AllocateMachineArgs err string storage string interfaces string notSubnets []string }{{ args: AllocateMachineArgs{}, }, { args: AllocateMachineArgs{ Storage: []StorageSpec{{}}, }, err: "Storage: Size value 0 not valid", }, { args: AllocateMachineArgs{ Storage: []StorageSpec{{Size: 200}, {Size: 400, Tags: []string{"ssd"}}}, }, storage: "200,400(ssd)", }, { args: AllocateMachineArgs{ Storage: []StorageSpec{ {Label: "foo", Size: 200}, {Label: "foo", Size: 400, Tags: []string{"ssd"}}, }, }, err: `reusing storage label "foo" not valid`, }, { args: AllocateMachineArgs{ Interfaces: []InterfaceSpec{{}}, }, err: "Interfaces: missing Label not valid", }, { args: AllocateMachineArgs{ Interfaces: []InterfaceSpec{ {Label: "foo", Space: "magic"}, {Label: "bar", Space: "other"}, }, }, interfaces: "foo:space=magic;bar:space=other", }, { args: AllocateMachineArgs{ Interfaces: []InterfaceSpec{ {Label: "foo", Space: "magic"}, {Label: "foo", Space: "other"}, }, }, err: `reusing interface label "foo" not valid`, }, { args: AllocateMachineArgs{ NotSpace: []string{""}, }, err: "empty NotSpace constraint not valid", }, { args: AllocateMachineArgs{ NotSpace: []string{"foo"}, }, notSubnets: []string{"space:foo"}, }, { args: AllocateMachineArgs{ NotSpace: []string{"foo", "bar"}, }, notSubnets: []string{"space:foo", "space:bar"}, }} { c.Logf("test %d", i) err := test.args.Validate() if test.err == "" { c.Check(err, jc.ErrorIsNil) c.Check(test.args.storage(), gc.Equals, test.storage) c.Check(test.args.interfaces(), gc.Equals, test.interfaces) c.Check(test.args.notSubnets(), jc.DeepEquals, test.notSubnets) } else { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, test.err) } } } type constraintMatchInfo map[string][]int func (s *controllerSuite) addAllocateResponse(c *gc.C, status int, interfaceMatches, storageMatches constraintMatchInfo) { constraints := make(map[string]interface{}) if interfaceMatches != nil { constraints["interfaces"] = interfaceMatches } if storageMatches != nil { constraints["storage"] = storageMatches } allocateJSON := updateJSONMap(c, machineResponse, map[string]interface{}{ "constraints_by_type": constraints, }) s.server.AddPostResponse("/api/2.0/machines/?op=allocate", status, allocateJSON) } func (s *controllerSuite) TestAllocateMachine(c *gc.C) { s.addAllocateResponse(c, http.StatusOK, nil, nil) controller := s.getController(c) machine, _, err := controller.AllocateMachine(AllocateMachineArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(machine.SystemID(), gc.Equals, "4y3ha3") } func (s *controllerSuite) TestAllocateMachineInterfacesMatch(c *gc.C) { s.addAllocateResponse(c, http.StatusOK, constraintMatchInfo{ "database": []int{35, 99}, }, nil) controller := s.getController(c) _, match, err := controller.AllocateMachine(AllocateMachineArgs{ // This isn't actually used, but here to show how it should be used. Interfaces: []InterfaceSpec{{ Label: "database", Space: "space-0", }}, }) c.Assert(err, jc.ErrorIsNil) c.Assert(match.Interfaces, gc.HasLen, 1) ifaces := match.Interfaces["database"] c.Assert(ifaces, gc.HasLen, 2) c.Assert(ifaces[0].ID(), gc.Equals, 35) c.Assert(ifaces[1].ID(), gc.Equals, 99) } func (s *controllerSuite) TestAllocateMachineInterfacesMatchMissing(c *gc.C) { // This should never happen, but if it does it is a clear indication of a // bug somewhere. s.addAllocateResponse(c, http.StatusOK, constraintMatchInfo{ "database": []int{40}, }, nil) controller := s.getController(c) _, _, err := controller.AllocateMachine(AllocateMachineArgs{ Interfaces: []InterfaceSpec{{ Label: "database", Space: "space-0", }}, }) c.Assert(err, jc.Satisfies, IsDeserializationError) } func (s *controllerSuite) TestAllocateMachineStorageMatches(c *gc.C) { s.addAllocateResponse(c, http.StatusOK, nil, constraintMatchInfo{ "root": []int{34, 98}, }) controller := s.getController(c) _, match, err := controller.AllocateMachine(AllocateMachineArgs{ Storage: []StorageSpec{{ Label: "root", Size: 50, Tags: []string{"hefty", "tangy"}, }}, }) c.Assert(err, jc.ErrorIsNil) c.Assert(match.Storage, gc.HasLen, 1) storages := match.Storage["root"] c.Assert(storages, gc.HasLen, 2) c.Assert(storages[0].ID(), gc.Equals, 34) c.Assert(storages[1].ID(), gc.Equals, 98) } func (s *controllerSuite) TestAllocateMachineStorageLogicalMatches(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=allocate", http.StatusOK, machineResponse) controller := s.getController(c) machine, matches, err := controller.AllocateMachine(AllocateMachineArgs{ Storage: []StorageSpec{ { Tags: []string{"raid0"}, }, { Tags: []string{"partition"}, }, }, }) c.Assert(err, jc.ErrorIsNil) var virtualDeviceID = 23 var partitionID = 1 //matches storage must contain the "raid0" virtual block device c.Assert(matches.Storage["0"][0], gc.Equals, machine.BlockDevice(virtualDeviceID)) //matches storage must contain the partition from physical block device c.Assert(matches.Storage["1"][0], gc.Equals, machine.Partition(partitionID)) } func (s *controllerSuite) TestAllocateMachineStorageMatchMissing(c *gc.C) { // This should never happen, but if it does it is a clear indication of a // bug somewhere. s.addAllocateResponse(c, http.StatusOK, nil, constraintMatchInfo{ "root": []int{50}, }) controller := s.getController(c) _, _, err := controller.AllocateMachine(AllocateMachineArgs{ Storage: []StorageSpec{{ Label: "root", Size: 50, Tags: []string{"hefty", "tangy"}, }}, }) c.Assert(err, jc.Satisfies, IsDeserializationError) } func (s *controllerSuite) TestAllocateMachineArgsForm(c *gc.C) { s.addAllocateResponse(c, http.StatusOK, nil, nil) controller := s.getController(c) // Create an arg structure that sets all the values. args := AllocateMachineArgs{ Hostname: "foobar", SystemId: "some_id", Architecture: "amd64", MinCPUCount: 42, MinMemory: 20000, Tags: []string{"good"}, NotTags: []string{"bad"}, Storage: []StorageSpec{{Label: "root", Size: 200}}, Interfaces: []InterfaceSpec{{Label: "default", Space: "magic"}}, NotSpace: []string{"special"}, Zone: "magic", Pool: "swimming_is_fun", NotInZone: []string{"not-magic"}, AgentName: "agent 42", Comment: "testing", DryRun: true, } _, _, err := controller.AllocateMachine(args) c.Assert(err, jc.ErrorIsNil) request := s.server.LastRequest() // There should be one entry in the form values for each of the args. form := request.PostForm c.Assert(form, gc.HasLen, 16) // Positive space check. c.Assert(form.Get("interfaces"), gc.Equals, "default:space=magic") // Negative space check. c.Assert(form.Get("not_subnets"), gc.Equals, "space:special") } func (s *controllerSuite) TestAllocateMachineNoMatch(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=allocate", http.StatusConflict, "boo") controller := s.getController(c) _, _, err := controller.AllocateMachine(AllocateMachineArgs{}) c.Assert(err, jc.Satisfies, IsNoMatchError) } func (s *controllerSuite) TestAllocateMachineUnexpected(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=allocate", http.StatusBadRequest, "boo") controller := s.getController(c) _, _, err := controller.AllocateMachine(AllocateMachineArgs{}) c.Assert(err, jc.Satisfies, IsUnexpectedError) } func (s *controllerSuite) TestReleaseMachines(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=release", http.StatusOK, "[]") controller := s.getController(c) err := controller.ReleaseMachines(ReleaseMachinesArgs{ SystemIDs: []string{"this", "that"}, Comment: "all good", }) c.Assert(err, jc.ErrorIsNil) request := s.server.LastRequest() // There should be one entry in the form values for each of the args. c.Assert(request.PostForm["machines"], jc.SameContents, []string{"this", "that"}) c.Assert(request.PostForm.Get("comment"), gc.Equals, "all good") } func (s *controllerSuite) TestReleaseMachinesBadRequest(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=release", http.StatusBadRequest, "unknown machines") controller := s.getController(c) err := controller.ReleaseMachines(ReleaseMachinesArgs{ SystemIDs: []string{"this", "that"}, }) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "unknown machines") } func (s *controllerSuite) TestReleaseMachinesForbidden(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=release", http.StatusForbidden, "bzzt denied") controller := s.getController(c) err := controller.ReleaseMachines(ReleaseMachinesArgs{ SystemIDs: []string{"this", "that"}, }) c.Assert(err, jc.Satisfies, IsPermissionError) c.Assert(err.Error(), gc.Equals, "bzzt denied") } func (s *controllerSuite) TestReleaseMachinesConflict(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=release", http.StatusConflict, "machine busy") controller := s.getController(c) err := controller.ReleaseMachines(ReleaseMachinesArgs{ SystemIDs: []string{"this", "that"}, }) c.Assert(err, jc.Satisfies, IsCannotCompleteError) c.Assert(err.Error(), gc.Equals, "machine busy") } func (s *controllerSuite) TestReleaseMachinesUnexpected(c *gc.C) { s.server.AddPostResponse("/api/2.0/machines/?op=release", http.StatusBadGateway, "wat") controller := s.getController(c) err := controller.ReleaseMachines(ReleaseMachinesArgs{ SystemIDs: []string{"this", "that"}, }) c.Assert(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 502 Bad Gateway (wat)") } func (s *controllerSuite) TestFiles(c *gc.C) { controller := s.getController(c) files, err := controller.Files("") c.Assert(err, jc.ErrorIsNil) c.Assert(files, gc.HasLen, 2) file := files[0] c.Assert(file.Filename(), gc.Equals, "test") uri, err := url.Parse(file.AnonymousURL()) c.Assert(err, jc.ErrorIsNil) c.Assert(uri.Scheme, gc.Equals, "http") c.Assert(uri.RequestURI(), gc.Equals, "/MAAS/api/2.0/files/?op=get_by_key&key=3afba564-fb7d-11e5-932f-52540051bf22") } func (s *controllerSuite) TestGetFile(c *gc.C) { s.server.AddGetResponse("/api/2.0/files/testing/", http.StatusOK, fileResponse) controller := s.getController(c) file, err := controller.GetFile("testing") c.Assert(err, jc.ErrorIsNil) c.Assert(file.Filename(), gc.Equals, "testing") uri, err := url.Parse(file.AnonymousURL()) c.Assert(err, jc.ErrorIsNil) c.Assert(uri.Scheme, gc.Equals, "http") c.Assert(uri.RequestURI(), gc.Equals, "/MAAS/api/2.0/files/?op=get_by_key&key=88e64b76-fb82-11e5-932f-52540051bf22") } func (s *controllerSuite) TestGetFileMissing(c *gc.C) { controller := s.getController(c) _, err := controller.GetFile("missing") c.Assert(err, jc.Satisfies, IsNoMatchError) } func (s *controllerSuite) TestAddFileArgsValidate(c *gc.C) { reader := bytes.NewBufferString("test") for i, test := range []struct { args AddFileArgs errText string }{{ errText: "missing Filename not valid", }, { args: AddFileArgs{Filename: "/foo"}, errText: `paths in Filename "/foo" not valid`, }, { args: AddFileArgs{Filename: "a/foo"}, errText: `paths in Filename "a/foo" not valid`, }, { args: AddFileArgs{Filename: "foo.txt"}, errText: `missing Content or Reader not valid`, }, { args: AddFileArgs{ Filename: "foo.txt", Reader: reader, }, errText: `missing Length not valid`, }, { args: AddFileArgs{ Filename: "foo.txt", Reader: reader, Length: 4, }, }, { args: AddFileArgs{ Filename: "foo.txt", Content: []byte("foo"), Reader: reader, }, errText: `specifying Content and Reader not valid`, }, { args: AddFileArgs{ Filename: "foo.txt", Content: []byte("foo"), Length: 20, }, errText: `specifying Length and Content not valid`, }, { args: AddFileArgs{ Filename: "foo.txt", Content: []byte("foo"), }, }} { c.Logf("test %d", i) err := test.args.Validate() if test.errText == "" { c.Check(err, jc.ErrorIsNil) } else { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, test.errText) } } } func (s *controllerSuite) TestAddFileValidates(c *gc.C) { controller := s.getController(c) err := controller.AddFile(AddFileArgs{}) c.Assert(err, jc.Satisfies, errors.IsNotValid) } func (s *controllerSuite) assertFile(c *gc.C, request *http.Request, filename, content string) { form := request.Form c.Check(form.Get("filename"), gc.Equals, filename) fileHeader := request.MultipartForm.File["file"][0] f, err := fileHeader.Open() c.Assert(err, jc.ErrorIsNil) bytes, err := ioutil.ReadAll(f) c.Assert(err, jc.ErrorIsNil) c.Assert(string(bytes), gc.Equals, content) } func (s *controllerSuite) TestAddFileContent(c *gc.C) { s.server.AddPostResponse("/api/2.0/files/?op=", http.StatusOK, "") controller := s.getController(c) err := controller.AddFile(AddFileArgs{ Filename: "foo.txt", Content: []byte("foo"), }) c.Assert(err, jc.ErrorIsNil) request := s.server.LastRequest() s.assertFile(c, request, "foo.txt", "foo") } func (s *controllerSuite) TestAddFileReader(c *gc.C) { reader := bytes.NewBufferString("test\n extra over length ignored") s.server.AddPostResponse("/api/2.0/files/?op=", http.StatusOK, "") controller := s.getController(c) err := controller.AddFile(AddFileArgs{ Filename: "foo.txt", Reader: reader, Length: 5, }) c.Assert(err, jc.ErrorIsNil) request := s.server.LastRequest() s.assertFile(c, request, "foo.txt", "test\n") } var versionResponse = `{"version": "2.5.0 from source", "subversion": "git+2f25a2cc0930c0e411106f119bc455c161d75b1a", "capabilities": ["networks-management", "static-ipaddresses", "ipv6-deployment-ubuntu", "devices-management", "storage-deployment-ubuntu", "network-deployment-ubuntu"]}` type cleanup interface { AddCleanup(func(*gc.C)) } // createTestServerController creates a controller backed on to a test server // that has sufficient knowledge of versions and users to be able to create a // valid controller. func createTestServerController(c *gc.C, suite cleanup) (*SimpleTestServer, Controller) { server := NewSimpleServer() server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, `"captain awesome"`) server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse) server.Start() suite.AddCleanup(func(*gc.C) { server.Close() }) controller, err := NewController(ControllerArgs{ BaseURL: server.URL, APIKey: "fake:as:key", }) c.Assert(err, jc.ErrorIsNil) return server, controller } golang-github-juju-gomaasapi-2.2.0/device.go000066400000000000000000000207551451732172100207560ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/http" "strings" "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type device struct { controller *controller resourceURI string systemID string hostname string fqdn string parent string owner string ipAddresses []string interfaceSet []*interface_ zone *zone pool *pool } // SystemID implements Device. func (d *device) SystemID() string { return d.systemID } // Hostname implements Device. func (d *device) Hostname() string { return d.hostname } // FQDN implements Device. func (d *device) FQDN() string { return d.fqdn } // Parent implements Device. func (d *device) Parent() string { return d.parent } // Owner implements Device. func (d *device) Owner() string { return d.owner } // IPAddresses implements Device. func (d *device) IPAddresses() []string { return d.ipAddresses } // Zone implements Device. func (d *device) Zone() Zone { if d.zone == nil { return nil } return d.zone } // Pool implements Device. func (d *device) Pool() Pool { if d.pool == nil { return nil } return d.pool } // InterfaceSet implements Device. func (d *device) InterfaceSet() []Interface { result := make([]Interface, len(d.interfaceSet)) for i, v := range d.interfaceSet { v.controller = d.controller result[i] = v } return result } // CreateInterfaceArgs is an argument struct for passing parameters to // the Machine.CreateInterface method. type CreateInterfaceArgs struct { // Name of the interface (required). Name string // MACAddress is the MAC address of the interface (required). MACAddress string // VLAN is the untagged VLAN the interface is connected to (required). VLAN VLAN // Tags to attach to the interface (optional). Tags []string // MTU - Maximum transmission unit. (optional) MTU int // AcceptRA - Accept router advertisements. (IPv6 only) AcceptRA bool // Autoconf - Perform stateless autoconfiguration. (IPv6 only) Autoconf bool } // Validate checks the required fields are set for the arg structure. func (a *CreateInterfaceArgs) Validate() error { if a.Name == "" { return errors.NotValidf("missing Name") } if a.MACAddress == "" { return errors.NotValidf("missing MACAddress") } if a.VLAN == nil { return errors.NotValidf("missing VLAN") } return nil } // interfacesURI used to add interfaces for this device. The operations // are on the nodes endpoint, not devices. func (d *device) interfacesURI() string { return strings.Replace(d.resourceURI, "devices", "nodes", 1) + "interfaces/" } // CreateInterface implements Device. func (d *device) CreateInterface(args CreateInterfaceArgs) (Interface, error) { if err := args.Validate(); err != nil { return nil, errors.Trace(err) } params := NewURLParams() params.Values.Add("name", args.Name) params.Values.Add("mac_address", args.MACAddress) params.Values.Add("vlan", fmt.Sprint(args.VLAN.ID())) params.MaybeAdd("tags", strings.Join(args.Tags, ",")) params.MaybeAddInt("mtu", args.MTU) params.MaybeAddBool("accept_ra", args.AcceptRA) params.MaybeAddBool("autoconf", args.Autoconf) result, err := d.controller.post(d.interfacesURI(), "create_physical", params.Values) 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) } iface, err := readInterface(d.controller.apiVersion, result) if err != nil { return nil, errors.Trace(err) } iface.controller = d.controller // TODO: add to the interfaces for the device when the interfaces are returned. // lp:bug 1567213. return iface, nil } // Delete implements Device. func (d *device) Delete() error { err := d.controller.delete(d.resourceURI) 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) } return nil } func readDevice(controllerVersion version.Number, source interface{}) (*device, error) { readFunc, err := getDeviceDeserializationFunc(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, "device base schema check failed") } valid := coerced.(map[string]interface{}) return readFunc(valid) } func readDevices(controllerVersion version.Number, source interface{}) ([]*device, error) { readFunc, err := getDeviceDeserializationFunc(controllerVersion) if err != nil { return nil, errors.Trace(err) } checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "device base schema check failed") } valid := coerced.([]interface{}) return readDeviceList(valid, readFunc) } func getDeviceDeserializationFunc(controllerVersion version.Number) (deviceDeserializationFunc, error) { var deserialisationVersion version.Number for v := range deviceDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no device read func for version %s", controllerVersion) } return deviceDeserializationFuncs[deserialisationVersion], nil } // readDeviceList expects the values of the sourceList to be string maps. func readDeviceList(sourceList []interface{}, readFunc deviceDeserializationFunc) ([]*device, error) { result := make([]*device, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for device %d, %T", i, value) } device, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "device %d", i) } result = append(result, device) } return result, nil } type deviceDeserializationFunc func(map[string]interface{}) (*device, error) var deviceDeserializationFuncs = map[version.Number]deviceDeserializationFunc{ twoDotOh: device_2_0, } func device_2_0(source map[string]interface{}) (*device, error) { fields := schema.Fields{ "resource_uri": schema.String(), "system_id": schema.String(), "hostname": schema.String(), "fqdn": schema.String(), "parent": schema.OneOf(schema.Nil(""), schema.String()), "owner": schema.OneOf(schema.Nil(""), schema.String()), "ip_addresses": schema.List(schema.String()), "interface_set": schema.List(schema.StringMap(schema.Any())), "zone": schema.StringMap(schema.Any()), "pool": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())), } defaults := schema.Defaults{ "owner": "", "parent": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "device 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. interfaceSet, err := readInterfaceList(valid["interface_set"].([]interface{}), interface_2_0) if err != nil { return nil, errors.Trace(err) } zone, err := zone_2_0(valid["zone"].(map[string]interface{})) if err != nil { return nil, errors.Trace(err) } var pool *pool if valid["pool"] != nil { if pool, err = pool_2_0(valid["pool"].(map[string]interface{})); err != nil { return nil, errors.Trace(err) } } owner, _ := valid["owner"].(string) parent, _ := valid["parent"].(string) result := &device{ resourceURI: valid["resource_uri"].(string), systemID: valid["system_id"].(string), hostname: valid["hostname"].(string), fqdn: valid["fqdn"].(string), parent: parent, owner: owner, ipAddresses: convertToStringSlice(valid["ip_addresses"]), interfaceSet: interfaceSet, zone: zone, pool: pool, } return result, nil } golang-github-juju-gomaasapi-2.2.0/device_test.go000066400000000000000000000251071451732172100220110ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "net/http" "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type deviceSuite struct { testing.CleanupSuite } var _ = gc.Suite(&deviceSuite{}) func (*deviceSuite) TestNilZone(c *gc.C) { var empty device c.Check(empty.Zone() == nil, jc.IsTrue) } func (*deviceSuite) TestReadDevicesBadSchema(c *gc.C) { _, err := readDevices(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `device base schema check failed: expected list, got string("wat?")`) } func (*deviceSuite) TestReadDevices(c *gc.C) { devices, err := readDevices(twoDotOh, parseJSON(c, devicesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 1) device := devices[0] c.Check(device.SystemID(), gc.Equals, "4y3haf") c.Check(device.Hostname(), gc.Equals, "furnacelike-brittney") c.Check(device.FQDN(), gc.Equals, "furnacelike-brittney.maas") c.Check(device.IPAddresses(), jc.DeepEquals, []string{"192.168.100.11"}) zone := device.Zone() c.Check(zone, gc.NotNil) c.Check(zone.Name(), gc.Equals, "default") pool := device.Pool() c.Check(pool, gc.NotNil) c.Check(pool.Name(), gc.Equals, "default") } func (*deviceSuite) TestReadDevicesNils(c *gc.C) { json := parseJSON(c, devicesResponse) deviceMap := json.([]interface{})[0].(map[string]interface{}) deviceMap["owner"] = nil deviceMap["parent"] = nil deviceMap["pool"] = nil devices, err := readDevices(twoDotOh, json) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 1) device := devices[0] c.Check(device.Owner(), gc.Equals, "") c.Check(device.Parent(), gc.Equals, "") c.Check(device.Pool(), gc.IsNil) } func (*deviceSuite) TestLowVersion(c *gc.C) { _, err := readDevices(version.MustParse("1.9.0"), parseJSON(c, devicesResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (*deviceSuite) TestHighVersion(c *gc.C) { devices, err := readDevices(version.MustParse("2.1.9"), parseJSON(c, devicesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 1) } func (s *deviceSuite) TestInterfaceSet(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddGetResponse(device.interfacesURI(), http.StatusOK, interfacesResponse) ifaces := device.InterfaceSet() c.Assert(ifaces, gc.HasLen, 2) } type fakeVLAN struct { VLAN id int } func (f *fakeVLAN) ID() int { return f.id } func (s *controllerSuite) TestCreateInterfaceArgsValidate(c *gc.C) { for i, test := range []struct { args CreateInterfaceArgs errText string }{{ errText: "missing Name not valid", }, { args: CreateInterfaceArgs{Name: "eth3"}, errText: "missing MACAddress not valid", }, { args: CreateInterfaceArgs{Name: "eth3", MACAddress: "a-mac-address"}, errText: `missing VLAN not valid`, }, { args: CreateInterfaceArgs{Name: "eth3", MACAddress: "a-mac-address", VLAN: &fakeVLAN{}}, }} { c.Logf("test %d", i) err := test.args.Validate() if test.errText == "" { c.Check(err, jc.ErrorIsNil) } else { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, test.errText) } } } func (s *deviceSuite) TestCreateInterfaceValidates(c *gc.C) { _, device := s.getServerAndDevice(c) _, err := device.CreateInterface(CreateInterfaceArgs{}) c.Assert(err, jc.Satisfies, errors.IsNotValid) } func (s *deviceSuite) TestCreateInterface(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusOK, interfaceResponse) iface, err := device.CreateInterface(CreateInterfaceArgs{ Name: "eth43", MACAddress: "some-mac-address", VLAN: &fakeVLAN{id: 33}, Tags: []string{"foo", "bar"}, }) c.Assert(err, jc.ErrorIsNil) c.Assert(iface, gc.NotNil) request := server.LastRequest() form := request.PostForm c.Assert(form.Get("name"), gc.Equals, "eth43") c.Assert(form.Get("mac_address"), gc.Equals, "some-mac-address") c.Assert(form.Get("vlan"), gc.Equals, "33") c.Assert(form.Get("tags"), gc.Equals, "foo,bar") } func minimalCreateInterfaceArgs() CreateInterfaceArgs { return CreateInterfaceArgs{ Name: "eth43", MACAddress: "some-mac-address", VLAN: &fakeVLAN{id: 33}, } } func (s *deviceSuite) TestCreateInterfaceNotFound(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusNotFound, "can't find device") _, err := device.CreateInterface(minimalCreateInterfaceArgs()) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "can't find device") } func (s *deviceSuite) TestCreateInterfaceConflict(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusConflict, "device not allocated") _, err := device.CreateInterface(minimalCreateInterfaceArgs()) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "device not allocated") } func (s *deviceSuite) TestCreateInterfaceForbidden(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusForbidden, "device not yours") _, err := device.CreateInterface(minimalCreateInterfaceArgs()) c.Assert(err, jc.Satisfies, IsPermissionError) c.Assert(err.Error(), gc.Equals, "device not yours") } func (s *deviceSuite) TestCreateInterfaceServiceUnavailable(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusServiceUnavailable, "no ip addresses available") _, err := device.CreateInterface(minimalCreateInterfaceArgs()) c.Assert(err, jc.Satisfies, IsCannotCompleteError) c.Assert(err.Error(), gc.Equals, "no ip addresses available") } func (s *deviceSuite) TestCreateInterfaceUnknown(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusMethodNotAllowed, "wat?") _, err := device.CreateInterface(minimalCreateInterfaceArgs()) c.Assert(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 405 Method Not Allowed (wat?)") } func (s *deviceSuite) getServerAndDevice(c *gc.C) (*SimpleTestServer, *device) { server, controller := createTestServerController(c, s) server.AddGetResponse("/api/2.0/devices/", http.StatusOK, devicesResponse) devices, err := controller.Devices(DevicesArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 1) return server, devices[0].(*device) } func (s *deviceSuite) TestDelete(c *gc.C) { server, device := s.getServerAndDevice(c) // Successful delete is 204 - StatusNoContent server.AddDeleteResponse(device.resourceURI, http.StatusNoContent, "") err := device.Delete() c.Assert(err, jc.ErrorIsNil) } func (s *deviceSuite) TestDelete404(c *gc.C) { _, device := s.getServerAndDevice(c) // No path, so 404 err := device.Delete() c.Assert(err, jc.Satisfies, IsNoMatchError) } func (s *deviceSuite) TestDeleteForbidden(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddDeleteResponse(device.resourceURI, http.StatusForbidden, "") err := device.Delete() c.Assert(err, jc.Satisfies, IsPermissionError) } func (s *deviceSuite) TestDeleteUnknown(c *gc.C) { server, device := s.getServerAndDevice(c) server.AddDeleteResponse(device.resourceURI, http.StatusConflict, "") err := device.Delete() c.Assert(err, jc.Satisfies, IsUnexpectedError) } const ( deviceResponse = ` { "zone": { "description": "", "resource_uri": "/MAAS/api/2.0/zones/default/", "name": "default" }, "pool": { "description": "", "resource_uri": "/MAAS/api/2.0/pools/default/", "name": "default" }, "domain": { "resource_record_count": 0, "resource_uri": "/MAAS/api/2.0/domains/0/", "authoritative": true, "name": "maas", "ttl": null, "id": 0 }, "node_type_name": "Device", "address_ttl": null, "hostname": "furnacelike-brittney", "node_type": 1, "resource_uri": "/MAAS/api/2.0/devices/4y3haf/", "ip_addresses": ["192.168.100.11"], "owner": "thumper", "tag_names": [], "fqdn": "furnacelike-brittney.maas", "system_id": "4y3haf", "parent": "4y3ha3", "interface_set": [ { "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", "type": "physical", "mac_address": "78:f0:f1:16:a7:46", "params": "", "discovered": null, "effective_mtu": 1500, "id": 48, "children": [], "links": [], "name": "eth0", "vlan": { "secondary_rack": null, "dhcp_on": true, "fabric": "fabric-0", "mtu": 1500, "primary_rack": "4y3h7n", "resource_uri": "/MAAS/api/2.0/vlans/1/", "external_dhcp": null, "name": "untagged", "id": 1, "vid": 0 }, "tags": [], "parents": [], "enabled": true }, { "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/49/", "type": "physical", "mac_address": "15:34:d3:2d:f7:a7", "params": {}, "discovered": null, "effective_mtu": 1500, "id": 49, "children": [], "links": [ { "mode": "link_up", "id": 101 } ], "name": "eth1", "vlan": { "secondary_rack": null, "dhcp_on": true, "fabric": "fabric-0", "mtu": 1500, "primary_rack": "4y3h7n", "resource_uri": "/MAAS/api/2.0/vlans/1/", "external_dhcp": null, "name": "untagged", "id": 1, "vid": 0 }, "tags": [], "parents": [], "enabled": true } ] } ` devicesResponse = "[" + deviceResponse + "]" ) golang-github-juju-gomaasapi-2.2.0/domain.go000066400000000000000000000045521451732172100207630ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type domain struct { authoritative bool resourceRecordCount int ttl *int resourceURI string id int name string } // Name implements Domain interface func (domain *domain) Name() string { return domain.name } func readDomains(controllerVersion version.Number, source interface{}) ([]*domain, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "domain base schema check failed") } valid := coerced.([]interface{}) return readDomainList(valid) } func domain_(source map[string]interface{}) (*domain, error) { fields := schema.Fields{ "authoritative": schema.Bool(), "resource_record_count": schema.ForceInt(), "ttl": schema.OneOf(schema.Nil("null"), schema.ForceInt()), "resource_uri": schema.String(), "id": schema.ForceInt(), "name": schema.String(), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "domain schema check failed") } valid := coerced.(map[string]interface{}) var ttl *int = nil if valid["ttl"] != nil { i := valid["ttl"].(int) ttl = &i } result := &domain{ authoritative: valid["authoritative"].(bool), id: valid["id"].(int), name: valid["name"].(string), resourceRecordCount: valid["resource_record_count"].(int), resourceURI: valid["resource_uri"].(string), ttl: ttl, } return result, nil } // readDomainList expects the values of the sourceList to be string maps. func readDomainList(sourceList []interface{}) ([]*domain, error) { result := make([]*domain, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for domain %d, %T", i, value) } domain, err := domain_(source) if err != nil { return nil, errors.Annotatef(err, "domain %d", i) } result = append(result, domain) } return result, nil } golang-github-juju-gomaasapi-2.2.0/domain_test.go000066400000000000000000000022331451732172100220140ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) type domainSuite struct{} var _ = gc.Suite(&domainSuite{}) func (*domainSuite) TestReadDomainsBadSchema(c *gc.C) { _, err := readDomains(twoDotOh, "something") c.Assert(err.Error(), gc.Equals, `domain base schema check failed: expected list, got string("something")`) } func (*domainSuite) TestReadDomains(c *gc.C) { domains, err := readDomains(twoDotOh, parseJSON(c, domainResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(domains, gc.HasLen, 2) c.Assert(domains[0].Name(), gc.Equals, "maas") c.Assert(domains[1].Name(), gc.Equals, "anotherDomain.com") } var domainResponse = ` [ { "authoritative": "true", "resource_uri": "/MAAS/api/2.0/domains/0/", "name": "maas", "id": 0, "ttl": null, "resource_record_count": 3 }, { "authoritative": "true", "resource_uri": "/MAAS/api/2.0/domains/1/", "name": "anotherDomain.com", "id": 1, "ttl": 10, "resource_record_count": 3 } ] ` golang-github-juju-gomaasapi-2.2.0/enum.go000066400000000000000000000031101451732172100204450ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi const ( // NodeStatus* values represent the vocabulary of a Node‘s possible statuses. // The node has been created and has a system ID assigned to it. NodeStatusDeclared = "0" //Testing and other commissioning steps are taking place. NodeStatusCommissioning = "1" // Smoke or burn-in testing has a found a problem. NodeStatusFailedTests = "2" // The node can’t be contacted. NodeStatusMissing = "3" // The node is in the general pool ready to be deployed. NodeStatusReady = "4" // The node is ready for named deployment. NodeStatusReserved = "5" // The node is powering a service from a charm or is ready for use with a fresh Ubuntu install. NodeStatusDeployed = "6" // The node has been removed from service manually until an admin overrides the retirement. NodeStatusRetired = "7" // The node is broken: a step in the node lifecyle failed. More details // can be found in the node's event log. NodeStatusBroken = "8" // The node is being installed. NodeStatusDeploying = "9" // The node has been allocated to a user and is ready for deployment. NodeStatusAllocated = "10" // The deployment of the node failed. NodeStatusFailedDeployment = "11" // The node is powering down after a release request. NodeStatusReleasing = "12" // The releasing of the node failed. NodeStatusFailedReleasing = "13" // The node is erasing its disks. NodeStatusDiskErasing = "14" // The node failed to erase its disks. NodeStatusFailedDiskErasing = "15" ) golang-github-juju-gomaasapi-2.2.0/errors.go000066400000000000000000000122651451732172100210300ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "github.com/juju/errors" ) // NoMatchError is returned when the requested action cannot be performed // due to being unable to service due to no entities available that match the // request. type NoMatchError struct { errors.Err } // NewNoMatchError constructs a new NoMatchError and sets the location. func NewNoMatchError(message string) error { err := &NoMatchError{Err: errors.NewErr(message)} err.SetLocation(1) return err } // IsNoMatchError returns true if err is a NoMatchError. func IsNoMatchError(err error) bool { _, ok := errors.Cause(err).(*NoMatchError) return ok } // UnexpectedError is an error for a condition that hasn't been determined. type UnexpectedError struct { errors.Err } // NewUnexpectedError constructs a new UnexpectedError and sets the location. func NewUnexpectedError(err error) error { uerr := &UnexpectedError{Err: errors.NewErr("unexpected: %v", err)} uerr.SetLocation(1) return errors.Wrap(err, uerr) } // IsUnexpectedError returns true if err is an UnexpectedError. func IsUnexpectedError(err error) bool { _, ok := errors.Cause(err).(*UnexpectedError) return ok } // UnsupportedVersionError refers to calls made to an unsupported api version. type UnsupportedVersionError struct { errors.Err } // NewUnsupportedVersionError constructs a new UnsupportedVersionError and sets the location. func NewUnsupportedVersionError(format string, args ...interface{}) error { err := &UnsupportedVersionError{Err: errors.NewErr(format, args...)} err.SetLocation(1) return err } // IsUnsupportedVersionError returns true if err is an UnsupportedVersionError. func IsUnsupportedVersionError(err error) bool { _, ok := errors.Cause(err).(*UnsupportedVersionError) return ok } // WrapWithUnsupportedVersionError constructs a new // UnsupportedVersionError wrapping the passed error. func WrapWithUnsupportedVersionError(err error) error { uerr := &UnsupportedVersionError{Err: errors.NewErr("unsupported version: %v", err)} uerr.SetLocation(1) return errors.Wrap(err, uerr) } // DeserializationError types are returned when the returned JSON data from // the controller doesn't match the code's expectations. type DeserializationError struct { errors.Err } // NewDeserializationError constructs a new DeserializationError and sets the location. func NewDeserializationError(format string, args ...interface{}) error { err := &DeserializationError{Err: errors.NewErr(format, args...)} err.SetLocation(1) return err } // WrapWithDeserializationError constructs a new DeserializationError with the // specified message, and sets the location and returns a new error with the // full error stack set including the error passed in. func WrapWithDeserializationError(err error, format string, args ...interface{}) error { message := fmt.Sprintf(format, args...) // We want the deserialization error message to include the error text of the // previous error, but wrap it in the new type. derr := &DeserializationError{Err: errors.NewErr(message + ": " + err.Error())} derr.SetLocation(1) wrapped := errors.Wrap(err, derr) // We want the location of the wrapped error to be the caller of this function, // not the line above. if errType, ok := wrapped.(*errors.Err); ok { // We know it is because that is what Wrap returns. errType.SetLocation(1) } return wrapped } // IsDeserializationError returns true if err is a DeserializationError. func IsDeserializationError(err error) bool { _, ok := errors.Cause(err).(*DeserializationError) return ok } // BadRequestError is returned when the requested action cannot be performed // due to bad or incorrect parameters passed to the server. type BadRequestError struct { errors.Err } // NewBadRequestError constructs a new BadRequestError and sets the location. func NewBadRequestError(message string) error { err := &BadRequestError{Err: errors.NewErr(message)} err.SetLocation(1) return err } // IsBadRequestError returns true if err is a NoMatchError. func IsBadRequestError(err error) bool { _, ok := errors.Cause(err).(*BadRequestError) return ok } // PermissionError is returned when the user does not have permission to do the // requested action. type PermissionError struct { errors.Err } // NewPermissionError constructs a new PermissionError and sets the location. func NewPermissionError(message string) error { err := &PermissionError{Err: errors.NewErr(message)} err.SetLocation(1) return err } // IsPermissionError returns true if err is a NoMatchError. func IsPermissionError(err error) bool { _, ok := errors.Cause(err).(*PermissionError) return ok } // CannotCompleteError is returned when the requested action is unable to // complete for some server side reason. type CannotCompleteError struct { errors.Err } // NewCannotCompleteError constructs a new CannotCompleteError and sets the location. func NewCannotCompleteError(message string) error { err := &CannotCompleteError{Err: errors.NewErr(message)} err.SetLocation(1) return err } // IsCannotCompleteError returns true if err is a NoMatchError. func IsCannotCompleteError(err error) bool { _, ok := errors.Cause(err).(*CannotCompleteError) return ok } golang-github-juju-gomaasapi-2.2.0/errors_test.go000066400000000000000000000050121451732172100220570ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "strings" "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) type errorTypesSuite struct{} var _ = gc.Suite(&errorTypesSuite{}) func (*errorTypesSuite) TestNoMatchError(c *gc.C) { err := NewNoMatchError("foo") c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsNoMatchError) } func (*errorTypesSuite) TestUnexpectedError(c *gc.C) { err := errors.New("wat") err = NewUnexpectedError(err) c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: wat") } func (*errorTypesSuite) TestUnsupportedVersionError(c *gc.C) { err := NewUnsupportedVersionError("foo %d", 42) c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) c.Assert(err.Error(), gc.Equals, "foo 42") } func (*errorTypesSuite) TestWrapWithUnsupportedVersionError(c *gc.C) { err := WrapWithUnsupportedVersionError(errors.New("bad")) c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) c.Assert(err.Error(), gc.Equals, "unsupported version: bad") stack := errors.ErrorStack(err) c.Assert(strings.Split(stack, "\n"), gc.HasLen, 2) } func (*errorTypesSuite) TestDeserializationError(c *gc.C) { err := NewDeserializationError("foo %d", 42) c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, "foo 42") } func (*errorTypesSuite) TestWrapWithDeserializationError(c *gc.C) { err := errors.New("base error") err = WrapWithDeserializationError(err, "foo %d", 42) c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, "foo 42: base error") stack := errors.ErrorStack(err) c.Assert(strings.Split(stack, "\n"), gc.HasLen, 2) } func (*errorTypesSuite) TestBadRequestError(c *gc.C) { err := NewBadRequestError("omg") c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "omg") } func (*errorTypesSuite) TestPermissionError(c *gc.C) { err := NewPermissionError("naughty") c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsPermissionError) c.Assert(err.Error(), gc.Equals, "naughty") } func (*errorTypesSuite) TestCannotCompleteError(c *gc.C) { err := NewCannotCompleteError("server says no") c.Assert(err, gc.NotNil) c.Assert(err, jc.Satisfies, IsCannotCompleteError) c.Assert(err.Error(), gc.Equals, "server says no") } golang-github-juju-gomaasapi-2.2.0/example/000077500000000000000000000000001451732172100206125ustar00rootroot00000000000000golang-github-juju-gomaasapi-2.2.0/example/live_example.go000066400000000000000000000117471451732172100236250ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. /* This is an example on how the Go library gomaasapi can be used to interact with a real MAAS server. Note that this is a provided only as an example and that real code should probably do something more sensible with errors than ignoring them or panicking. */ package main import ( "bytes" "fmt" "net/url" "github.com/juju/gomaasapi/v2" ) var apiKey string var apiURL string var apiVersion string func getParams() { fmt.Println("Warning: this will create a node on the MAAS server; it should be deleted at the end of the run but if something goes wrong, that test node might be left over. You've been warned.") fmt.Print("Enter API key: ") _, err := fmt.Scanf("%s", &apiKey) if err != nil { panic(err) } fmt.Print("Enter API URL: ") _, err = fmt.Scanf("%s", &apiURL) if err != nil { panic(err) } fmt.Print("Enter API version: ") _, err = fmt.Scanf("%s", &apiVersion) if err != nil { panic(err) } } func checkError(err error) { if err != nil { panic(err) } } func main() { getParams() // Create API server endpoint. authClient, err := gomaasapi.NewAuthenticatedClient( gomaasapi.AddAPIVersionToURL(apiURL, apiVersion), apiKey) checkError(err) maas := gomaasapi.NewMAAS(*authClient) // Exercise the API. ManipulateNodes(maas) ManipulateFiles(maas) fmt.Println("All done.") } // ManipulateFiles exercises the /api/1.0/files/ API endpoint. Most precisely, // it uploads a files and then fetches it, making sure the received content // is the same as the one that was sent. func ManipulateFiles(maas *gomaasapi.MAASObject) { files := maas.GetSubObject("files") fileContent := []byte("test file content") fileName := "filename" filesToUpload := map[string][]byte{"file": fileContent} // Upload a file. fmt.Println("Uploading a file...") _, err := files.CallPostFiles("add", url.Values{"filename": {fileName}}, filesToUpload) checkError(err) fmt.Println("File sent.") // Fetch the file. fmt.Println("Fetching the file...") fileResult, err := files.CallGet("get", url.Values{"filename": {fileName}}) checkError(err) receivedFileContent, err := fileResult.GetBytes() checkError(err) if bytes.Compare(receivedFileContent, fileContent) != 0 { panic("Received content differs from the content sent!") } fmt.Println("Got file.") // Fetch list of files. listFiles, err := files.CallGet("list", url.Values{}) checkError(err) listFilesArray, err := listFiles.GetArray() checkError(err) fmt.Printf("We've got %v file(s)\n", len(listFilesArray)) // Delete the file. fmt.Println("Deleting the file...") fileObject, err := listFilesArray[0].GetMAASObject() checkError(err) errDelete := fileObject.Delete() checkError(errDelete) // Count the files. listFiles, err = files.CallGet("list", url.Values{}) checkError(err) listFilesArray, err = listFiles.GetArray() checkError(err) fmt.Printf("We've got %v file(s)\n", len(listFilesArray)) } // ManipulateFiles exercises the /api/1.0/nodes/ API endpoint. Most precisely, // it lists the existing nodes, creates a new node, updates it and then // deletes it. func ManipulateNodes(maas *gomaasapi.MAASObject) { nodeListing := maas.GetSubObject("nodes") // List nodes. fmt.Println("Fetching list of nodes...") listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) checkError(err) listNodes, err := listNodeObjects.GetArray() checkError(err) fmt.Printf("Got list of %v nodes\n", len(listNodes)) for index, nodeObj := range listNodes { node, err := nodeObj.GetMAASObject() checkError(err) hostname, err := node.GetField("hostname") checkError(err) fmt.Printf("Node #%d is named '%v' (%v)\n", index, hostname, node.URL()) } // Create a node. fmt.Println("Creating a new node...") params := url.Values{"architecture": {"i386/generic"}, "mac_addresses": {"AA:BB:CC:DD:EE:FF"}} newNodeObj, err := nodeListing.CallPost("new", params) checkError(err) newNode, err := newNodeObj.GetMAASObject() checkError(err) newNodeName, err := newNode.GetField("hostname") checkError(err) fmt.Printf("New node created: %s (%s)\n", newNodeName, newNode.URL()) // Update the new node. fmt.Println("Updating the new node...") updateParams := url.Values{"hostname": {"mynewname"}} newNodeObj2, err := newNode.Update(updateParams) checkError(err) newNodeName2, err := newNodeObj2.GetField("hostname") checkError(err) fmt.Printf("New node updated, now named: %s\n", newNodeName2) // Count the nodes. listNodeObjects2, err := nodeListing.CallGet("list", url.Values{}) checkError(err) listNodes2, err := listNodeObjects2.GetArray() checkError(err) fmt.Printf("We've got %v nodes\n", len(listNodes2)) // Delete the new node. fmt.Println("Deleting the new node...") errDelete := newNode.Delete() checkError(errDelete) // Count the nodes. listNodeObjects3, err := nodeListing.CallGet("list", url.Values{}) checkError(err) listNodes3, err := listNodeObjects3.GetArray() checkError(err) fmt.Printf("We've got %v nodes\n", len(listNodes3)) } golang-github-juju-gomaasapi-2.2.0/fabric.go000066400000000000000000000067471451732172100207520ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type fabric struct { // Add the controller in when we need to do things with the fabric. // controller Controller resourceURI string id int name string classType string vlans []*vlan } // ID implements Fabric. func (f *fabric) ID() int { return f.id } // Name implements Fabric. func (f *fabric) Name() string { return f.name } // ClassType implements Fabric. func (f *fabric) ClassType() string { return f.classType } // VLANs implements Fabric. func (f *fabric) VLANs() []VLAN { var result []VLAN for _, v := range f.vlans { result = append(result, v) } return result } func readFabrics(controllerVersion version.Number, source interface{}) ([]*fabric, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "fabric base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range fabricDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no fabric read func for version %s", controllerVersion) } readFunc := fabricDeserializationFuncs[deserialisationVersion] return readFabricList(valid, readFunc) } // readFabricList expects the values of the sourceList to be string maps. func readFabricList(sourceList []interface{}, readFunc fabricDeserializationFunc) ([]*fabric, error) { result := make([]*fabric, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for fabric %d, %T", i, value) } fabric, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "fabric %d", i) } result = append(result, fabric) } return result, nil } type fabricDeserializationFunc func(map[string]interface{}) (*fabric, error) var fabricDeserializationFuncs = map[version.Number]fabricDeserializationFunc{ twoDotOh: fabric_2_0, } func fabric_2_0(source map[string]interface{}) (*fabric, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "name": schema.String(), "class_type": schema.OneOf(schema.Nil(""), schema.String()), "vlans": schema.List(schema.StringMap(schema.Any())), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "fabric 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. vlans, err := readVLANList(valid["vlans"].([]interface{}), vlan_2_0) if err != nil { return nil, errors.Trace(err) } // Since the class_type is optional, we use the two part cast assignment. If // the cast fails, then we get the default value we care about, which is the // empty string. classType, _ := valid["class_type"].(string) result := &fabric{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), name: valid["name"].(string), classType: classType, vlans: vlans, } return result, nil } golang-github-juju-gomaasapi-2.2.0/fabric_test.go000066400000000000000000000045371451732172100220040ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type fabricSuite struct{} var _ = gc.Suite(&fabricSuite{}) func (*fabricSuite) TestReadFabricsBadSchema(c *gc.C) { _, err := readFabrics(twoDotOh, "wat?") c.Assert(err.Error(), gc.Equals, `fabric base schema check failed: expected list, got string("wat?")`) } func (*fabricSuite) TestReadFabrics(c *gc.C) { fabrics, err := readFabrics(twoDotOh, parseJSON(c, fabricResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(fabrics, gc.HasLen, 2) fabric := fabrics[0] c.Assert(fabric.ID(), gc.Equals, 0) c.Assert(fabric.Name(), gc.Equals, "fabric-0") c.Assert(fabric.ClassType(), gc.Equals, "") vlans := fabric.VLANs() c.Assert(vlans, gc.HasLen, 1) c.Assert(vlans[0].Name(), gc.Equals, "untagged") } func (*fabricSuite) TestLowVersion(c *gc.C) { _, err := readFabrics(version.MustParse("1.9.0"), parseJSON(c, fabricResponse)) c.Assert(err.Error(), gc.Equals, `no fabric read func for version 1.9.0`) } func (*fabricSuite) TestHighVersion(c *gc.C) { fabrics, err := readFabrics(version.MustParse("2.1.9"), parseJSON(c, fabricResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(fabrics, gc.HasLen, 2) } var fabricResponse = ` [ { "name": "fabric-0", "id": 0, "class_type": null, "vlans": [ { "name": "untagged", "vid": 0, "primary_rack": "4y3h7n", "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "fabric": "fabric-0", "mtu": 1500, "dhcp_on": true } ], "resource_uri": "/MAAS/api/2.0/fabrics/0/" }, { "name": "fabric-1", "id": 1, "class_type": null, "vlans": [ { "name": "untagged", "vid": 0, "primary_rack": null, "resource_uri": "/MAAS/api/2.0/vlans/5001/", "id": 5001, "secondary_rack": null, "fabric": "fabric-1", "mtu": 1500, "dhcp_on": false } ], "resource_uri": "/MAAS/api/2.0/fabrics/1/" } ] ` golang-github-juju-gomaasapi-2.2.0/file.go000066400000000000000000000117641451732172100204360ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/base64" "net/http" "net/url" "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type file struct { controller *controller resourceURI string filename string anonymousURI *url.URL content string } // Filename implements File. func (f *file) Filename() string { return f.filename } // AnonymousURL implements File. func (f *file) AnonymousURL() string { url := f.controller.client.GetURL(f.anonymousURI) return url.String() } // Delete implements File. func (f *file) Delete() error { err := f.controller.delete(f.resourceURI) 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) } return nil } // ReadAll implements File. func (f *file) ReadAll() ([]byte, error) { if f.content == "" { return f.readFromServer() } bytes, err := base64.StdEncoding.DecodeString(f.content) if err != nil { return nil, NewUnexpectedError(err) } return bytes, nil } func (f *file) readFromServer() ([]byte, error) { // If the content is available, it is base64 encoded, so args := make(url.Values) args.Add("filename", f.filename) bytes, err := f.controller._getRaw("files", "get", args) 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) } return bytes, nil } func readFiles(controllerVersion version.Number, source interface{}) ([]*file, error) { readFunc, err := getFileDeserializationFunc(controllerVersion) if err != nil { return nil, errors.Trace(err) } checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "file base schema check failed") } valid := coerced.([]interface{}) return readFileList(valid, readFunc) } func readFile(controllerVersion version.Number, source interface{}) (*file, error) { readFunc, err := getFileDeserializationFunc(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, "file base schema check failed") } valid := coerced.(map[string]interface{}) return readFunc(valid) } func getFileDeserializationFunc(controllerVersion version.Number) (fileDeserializationFunc, error) { var deserialisationVersion version.Number for v := range fileDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no file read func for version %s", controllerVersion) } return fileDeserializationFuncs[deserialisationVersion], nil } // readFileList expects the values of the sourceList to be string maps. func readFileList(sourceList []interface{}, readFunc fileDeserializationFunc) ([]*file, error) { result := make([]*file, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for file %d, %T", i, value) } file, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "file %d", i) } result = append(result, file) } return result, nil } type fileDeserializationFunc func(map[string]interface{}) (*file, error) var fileDeserializationFuncs = map[version.Number]fileDeserializationFunc{ twoDotOh: file_2_0, } func file_2_0(source map[string]interface{}) (*file, error) { fields := schema.Fields{ "resource_uri": schema.String(), "filename": schema.String(), "anon_resource_uri": schema.String(), "content": schema.String(), } defaults := schema.Defaults{ "content": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "file 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. anonURI, err := url.ParseRequestURI(valid["anon_resource_uri"].(string)) if err != nil { return nil, NewUnexpectedError(err) } result := &file{ resourceURI: valid["resource_uri"].(string), filename: valid["filename"].(string), anonymousURI: anonURI, content: valid["content"].(string), } return result, nil } golang-github-juju-gomaasapi-2.2.0/file_test.go000066400000000000000000000074121451732172100214700ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "net/http" "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type fileSuite struct { testing.CleanupSuite } var _ = gc.Suite(&fileSuite{}) func (*fileSuite) TestReadFilesBadSchema(c *gc.C) { _, err := readFiles(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `file base schema check failed: expected list, got string("wat?")`) } func (*fileSuite) TestReadFiles(c *gc.C) { files, err := readFiles(twoDotOh, parseJSON(c, filesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(files, gc.HasLen, 2) file := files[0] c.Assert(file.Filename(), gc.Equals, "test") } func (*fileSuite) TestLowVersion(c *gc.C) { _, err := readFiles(version.MustParse("1.9.0"), parseJSON(c, filesResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (*fileSuite) TestHighVersion(c *gc.C) { files, err := readFiles(version.MustParse("2.1.9"), parseJSON(c, filesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(files, gc.HasLen, 2) } func (s *fileSuite) TestReadAllFromGetFile(c *gc.C) { // When get file is used, the response includes the body of the file // base64 encoded, so ReadAll just decodes it. server, controller := createTestServerController(c, s) server.AddGetResponse("/api/2.0/files/testing/", http.StatusOK, fileResponse) file, err := controller.GetFile("testing") c.Assert(err, jc.ErrorIsNil) content, err := file.ReadAll() c.Assert(err, jc.ErrorIsNil) c.Assert(string(content), gc.Equals, "this is a test\n") } func (s *fileSuite) TestReadAllFromFiles(c *gc.C) { // When get file is used, the response includes the body of the file // base64 encoded, so ReadAll just decodes it. server, controller := createTestServerController(c, s) server.AddGetResponse("/api/2.0/files/", http.StatusOK, filesResponse) server.AddGetResponse("/api/2.0/files/?filename=test&op=get", http.StatusOK, "some content\n") files, err := controller.Files("") c.Assert(err, jc.ErrorIsNil) file := files[0] content, err := file.ReadAll() c.Assert(err, jc.ErrorIsNil) c.Assert(string(content), gc.Equals, "some content\n") } func (s *fileSuite) TestDeleteMissing(c *gc.C) { // If we get a file, but someone else deletes it first, we get a ... server, controller := createTestServerController(c, s) server.AddGetResponse("/api/2.0/files/testing/", http.StatusOK, fileResponse) file, err := controller.GetFile("testing") c.Assert(err, jc.ErrorIsNil) err = file.Delete() c.Assert(err, jc.Satisfies, IsNoMatchError) } func (s *fileSuite) TestDelete(c *gc.C) { // If we get a file, but someone else deletes it first, we get a ... server, controller := createTestServerController(c, s) server.AddGetResponse("/api/2.0/files/testing/", http.StatusOK, fileResponse) server.AddDeleteResponse("/api/2.0/files/testing/", http.StatusOK, "") file, err := controller.GetFile("testing") c.Assert(err, jc.ErrorIsNil) err = file.Delete() c.Assert(err, jc.Satisfies, IsNoMatchError) } var ( fileResponse = ` { "resource_uri": "/MAAS/api/2.0/files/testing/", "content": "dGhpcyBpcyBhIHRlc3QK", "anon_resource_uri": "/MAAS/api/2.0/files/?op=get_by_key&key=88e64b76-fb82-11e5-932f-52540051bf22", "filename": "testing" } ` filesResponse = ` [ { "resource_uri": "/MAAS/api/2.0/files/test/", "anon_resource_uri": "/MAAS/api/2.0/files/?op=get_by_key&key=3afba564-fb7d-11e5-932f-52540051bf22", "filename": "test" }, { "resource_uri": "/MAAS/api/2.0/files/test-file.txt/", "anon_resource_uri": "/MAAS/api/2.0/files/?op=get_by_key&key=69913e62-fad2-11e5-932f-52540051bf22", "filename": "test-file.txt" } ] ` ) golang-github-juju-gomaasapi-2.2.0/filesystem.go000066400000000000000000000035661451732172100217040ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import "github.com/juju/schema" type filesystem struct { fstype string mountPoint string label string uuid string // no idea what the mount_options are as a value type, so ignoring for now. } // Type implements FileSystem. func (f *filesystem) Type() string { return f.fstype } // MountPoint implements FileSystem. func (f *filesystem) MountPoint() string { return f.mountPoint } // Label implements FileSystem. func (f *filesystem) Label() string { return f.label } // UUID implements FileSystem. func (f *filesystem) UUID() string { return f.uuid } // There is no need for controller based parsing of filesystems until we need it. // Currently the filesystem reading is only called by the Partition parsing. func filesystem2_0(source map[string]interface{}) (*filesystem, error) { fields := schema.Fields{ "fstype": schema.String(), "mount_point": schema.OneOf(schema.Nil(""), schema.String()), "label": schema.OneOf(schema.Nil(""), schema.String()), "uuid": schema.String(), // TODO: mount_options when we know the type (note it can be // nil). } defaults := schema.Defaults{ "mount_point": "", "label": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "filesystem 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. mount_point, _ := valid["mount_point"].(string) label, _ := valid["label"].(string) result := &filesystem{ fstype: valid["fstype"].(string), mountPoint: mount_point, label: label, uuid: valid["uuid"].(string), } return result, nil } golang-github-juju-gomaasapi-2.2.0/filesystem_test.go000066400000000000000000000025141451732172100227330ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) type filesystemSuite struct{} var _ = gc.Suite(&filesystemSuite{}) func (*filesystemSuite) TestParse2_0(c *gc.C) { source := map[string]interface{}{ "fstype": "ext4", "mount_point": "/", "label": "root", "uuid": "fake-uuid", } fs, err := filesystem2_0(source) c.Assert(err, jc.ErrorIsNil) c.Check(fs.Type(), gc.Equals, "ext4") c.Check(fs.MountPoint(), gc.Equals, "/") c.Check(fs.Label(), gc.Equals, "root") c.Check(fs.UUID(), gc.Equals, "fake-uuid") } func (*filesystemSuite) TestParse2_Defaults(c *gc.C) { source := map[string]interface{}{ "fstype": "ext4", "mount_point": nil, "label": nil, "uuid": "fake-uuid", } fs, err := filesystem2_0(source) c.Assert(err, jc.ErrorIsNil) c.Check(fs.Type(), gc.Equals, "ext4") c.Check(fs.MountPoint(), gc.Equals, "") c.Check(fs.Label(), gc.Equals, "") c.Check(fs.UUID(), gc.Equals, "fake-uuid") } func (*filesystemSuite) TestParse2_0BadSchema(c *gc.C) { source := map[string]interface{}{ "mount_point": "/", "label": "root", "uuid": "fake-uuid", } _, err := filesystem2_0(source) c.Assert(err, jc.Satisfies, IsDeserializationError) } golang-github-juju-gomaasapi-2.2.0/go.mod000066400000000000000000000023231451732172100202650ustar00rootroot00000000000000module github.com/juju/gomaasapi/v2 go 1.17 require ( github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9 github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 github.com/juju/schema v1.0.1-0.20190814234152-1f8aaeef0989 github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) require ( github.com/juju/clock v0.0.0-20220203021603-d9deb868a28a // indirect github.com/juju/retry v0.0.0-20220204093819-62423bf33287 // indirect github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0 // indirect github.com/juju/version/v2 v2.0.0-20220204124744-fc9915e3d935 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) golang-github-juju-gomaasapi-2.2.0/go.sum000066400000000000000000000416741451732172100203260ustar00rootroot00000000000000github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20210706145210-9283cdf370b5/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/clock v0.0.0-20220202072423-1b0f830854c4/go.mod h1:zDZCPSgCJQINeZtQwHx2/cFk4seaBC8Yiqe8V82xiP0= github.com/juju/clock v0.0.0-20220203021603-d9deb868a28a h1:Az/6CM/P5guGHNy7r6TkOCctv3lDmN3W1uhku7QMupk= github.com/juju/clock v0.0.0-20220203021603-d9deb868a28a/go.mod h1:GZ/FY8Cqw3KHG6DwRVPUKbSPTAwyrU28xFi5cqZnLsc= github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18= github.com/juju/cmd/v3 v3.0.0-20220202061353-b1cc80b193b0/go.mod h1:EoGJiEG+vbMwO9l+Es0SDTlaQPjH6nLcnnc4NfZB3cY= github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY= github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a h1:d7eZO8OS/ZXxdP0uq3E8CdoA1qNFaecAv90UxrxaY2k= github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a/go.mod h1:JWeZdyttIEbkR51z2S13+J+aCuHVe0F6meRy+P0YGDo= github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20210818161939-5560c4c073ff/go.mod h1:i1eL7XREII6aHpQ2gApI/v6FkVUDEBremNkcBCKYAcY= github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9 h1:EJHbsNpQyupmMeWTq7inn+5L/WZ7JfzCVPJ+DP9McCQ= github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9/go.mod h1:TRm7EVGA3mQOqSVcBySRY7a9Y1/gyVhh/WTCnc5sD4U= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767/go.mod h1:+MaLYz4PumRkkyHYeXJ2G5g5cIW0sli2bOfpmbaMV/g= github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 h1:NO5tuyw++EGLnz56Q8KMyDZRwJwWO8jQnj285J3FOmY= github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg= github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 h1:zX5GoH3Jp8k1EjUFkApu/YZAYEn0PYQfg/U6IDyNyYs= github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090/go.mod h1:N614SE0a4e+ih2rg96Vi2PeC3cTpUOWgCTv3Cgk974c= github.com/juju/mutex v0.0.0-20171110020013-1fe2a4bf0a3a/go.mod h1:Y3oOzHH8CQ0Ppt0oCKJ2JFO81/EsWenH5AEqigLH+yY= github.com/juju/mutex/v2 v2.0.0-20220128011612-57176ebdcfa3/go.mod h1:TTCG9BJD9rCC4DZFz3jA0QvCqFDHw8Eqz0jstwY7RTQ= github.com/juju/mutex/v2 v2.0.0-20220203023141-11eeddb42c6c/go.mod h1:jwCfBs/smYDaeZLqeaCi8CB8M+tOes4yf827HoOEoqk= github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20220204093819-62423bf33287 h1:U+7oMWEglXfiikIppNexButZRwKPlzLBGKYSNCXzXf8= github.com/juju/retry v0.0.0-20220204093819-62423bf33287/go.mod h1:SssN1eYeK3A2qjnFGTiVMbdzGJ2BfluaJblJXvuvgqA= github.com/juju/schema v1.0.1-0.20190814234152-1f8aaeef0989 h1:qx1Zh1bnHHVIMmRxq0fehYk7npCG50GhUwEkYeUg/t4= github.com/juju/schema v1.0.1-0.20190814234152-1f8aaeef0989/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20180517134105-72703b1e95eb/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= github.com/juju/testing v0.0.0-20220202055744-1ad0816210a6/go.mod h1:QgWc2UdIPJ8t3rnvv95tFNOsQDfpXYEZDbP281o3b2c= github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 h1:XEDzpuZb8Ma7vLja3+5hzUqVTvAqm5Y+ygvnDs5iTMM= github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494/go.mod h1:rUquetT0ALL48LHZhyRGvjjBH8xZaZ8dFClulKK5wK4= github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647 h1:wQpkHVbIIpz1PCcLYku9KFWsJ7aEMQXHBBmLy3tRBTk= github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils/v2 v2.0.0-20200923005554-4646bfea2ef1/go.mod h1:fdlDtQlzundleLLz/ggoYinEt/LmnrpNKcNTABQATNI= github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a/go.mod h1:LzwbbEN7buYjySp4nqnti6c6olSqRXUk6RkbSUUP1n8= github.com/juju/utils/v3 v3.0.0-20220202114721-338bb0530e89/go.mod h1:wf5w+8jyTh2IYnSX0sHnMJo4ZPwwuiBWn+xN3DkQg4k= github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0 h1:bn+2Adl1yWqYjm3KSFlFqsvfLg2eq+XNL7GGMYApdVw= github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4= github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6 h1:nrqc9b4YKpKV4lPI3GPPFbo5FUuxkWxgZE2Z8O4lgaw= github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23/go.mod h1:Ljlbryh9sYaUSGXucslAEDf0A2XUSGvDbHJgW8ps6nc= github.com/juju/version/v2 v2.0.0-20220204124744-fc9915e3d935 h1:6YoyzXVW1XkqN86y2s/rz365Jm7EiAy39v2G5ikzvHU= github.com/juju/version/v2 v2.0.0-20220204124744-fc9915e3d935/go.mod h1:ZeFjNy+UFEWJDDPdzW7Cm9NeU6dsViGaFYhXzycLQrw= github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/masterzen/azure-sdk-for-go v3.2.0-beta.0.20161014135628-ee4f0065d00c+incompatible/go.mod h1:mf8fjOu33zCqxUjuiU3I8S1lJMyEAlH+0F2+M5xl3hE= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20161014151040-7a535cd943fc/go.mod h1:CfZSN7zwz5gJiFhZJz49Uzk7mEBHIceWmbFmYx7Hf7E= github.com/masterzen/winrm v0.0.0-20211231115050-232efb40349e/go.mod h1:Iju3u6NzoTAvjuhsGCZc+7fReNnr/Bd6DsWj3WTokIU= github.com/masterzen/xmlpath v0.0.0-20140218185901-13f4951698ad/go.mod h1:A0zPC53iKKKcXYxr4ROjpQRQ5FgJXtelNdSmHHuq/tY= github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg= gopkg.in/httprequest.v1 v1.1.1/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= launchpad.net/xmlpath v0.0.0-20130614043138-000000000004/go.mod h1:vqyExLOM3qBx7mvYRkoxjSCF945s0mbe7YynlKYXtsA= golang-github-juju-gomaasapi-2.2.0/gomaasapi.go000066400000000000000000000001651451732172100214510ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi golang-github-juju-gomaasapi-2.2.0/gomaasapi_test.go000066400000000000000000000004301451732172100225030ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "testing" . "gopkg.in/check.v1" ) func Test(t *testing.T) { TestingT(t) } type GomaasapiTestSuite struct { } var _ = Suite(&GomaasapiTestSuite{}) golang-github-juju-gomaasapi-2.2.0/interface.go000066400000000000000000000300711451732172100214470ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/http" "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) // Can't use "interface" as a type, so add an underscore. Yay. type interface_ struct { controller *controller resourceURI string id int name string type_ string enabled bool tags []string vlan *vlan links []*link macAddress string effectiveMTU int parents []string children []string } func (i *interface_) updateFrom(other *interface_) { i.resourceURI = other.resourceURI i.id = other.id i.name = other.name i.type_ = other.type_ i.enabled = other.enabled i.tags = other.tags i.vlan = other.vlan i.links = other.links i.macAddress = other.macAddress i.effectiveMTU = other.effectiveMTU i.parents = other.parents i.children = other.children } // ID implements Interface. func (i *interface_) ID() int { return i.id } // Name implements Interface. func (i *interface_) Name() string { return i.name } // Parents implements Interface. func (i *interface_) Parents() []string { return i.parents } // Children implements Interface. func (i *interface_) Children() []string { return i.children } // Type implements Interface. func (i *interface_) Type() string { return i.type_ } // Enabled implements Interface. func (i *interface_) Enabled() bool { return i.enabled } // Tags implements Interface. func (i *interface_) Tags() []string { return i.tags } // VLAN implements Interface. func (i *interface_) VLAN() VLAN { if i.vlan == nil { return nil } return i.vlan } // Links implements Interface. func (i *interface_) Links() []Link { result := make([]Link, len(i.links)) for i, link := range i.links { result[i] = link } return result } // MACAddress implements Interface. func (i *interface_) MACAddress() string { return i.macAddress } // EffectiveMTU implements Interface. func (i *interface_) EffectiveMTU() int { return i.effectiveMTU } // UpdateInterfaceArgs is an argument struct for calling Interface.Update. type UpdateInterfaceArgs struct { Name string MACAddress string VLAN VLAN } func (a *UpdateInterfaceArgs) vlanID() int { if a.VLAN == nil { return 0 } return a.VLAN.ID() } // 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()) source, err := i.controller.put(i.resourceURI, 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 := readInterface(i.controller.apiVersion, source) if err != nil { return errors.Trace(err) } i.updateFrom(response) return nil } // Delete implements Interface. func (i *interface_) Delete() error { err := i.controller.delete(i.resourceURI) 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) } return nil } // InterfaceLinkMode is the type of the various link mode constants used for // LinkSubnetArgs. type InterfaceLinkMode string const ( // LinkModeDHCP - Bring the interface up with DHCP on the given subnet. Only // one subnet can be set to DHCP. If the subnet is managed this interface // will pull from the dynamic IP range. LinkModeDHCP InterfaceLinkMode = "DHCP" // LinkModeStatic - Bring the interface up with a STATIC IP address on the // given subnet. Any number of STATIC links can exist on an interface. LinkModeStatic InterfaceLinkMode = "STATIC" // LinkModeLinkUp - Bring the interface up only on the given subnet. No IP // address will be assigned to this interface. The interface cannot have any // current DHCP or STATIC links. LinkModeLinkUp InterfaceLinkMode = "LINK_UP" ) // LinkSubnetArgs is an argument struct for passing parameters to // the Interface.LinkSubnet method. type LinkSubnetArgs struct { // Mode is used to describe how the address is provided for the Link. // Required field. Mode InterfaceLinkMode // Subnet is the subnet to link to. Required field. Subnet Subnet // IPAddress is only valid when the Mode is set to LinkModeStatic. If // not specified with a Mode of LinkModeStatic, an IP address from the // subnet will be auto selected. IPAddress string // DefaultGateway will set the gateway IP address for the Subnet as the // default gateway for the machine or device the interface belongs to. // Option can only be used with mode LinkModeStatic. DefaultGateway bool } // Validate ensures that the Mode and Subnet are set, and that the other options // are consistent with the Mode. func (a *LinkSubnetArgs) Validate() error { switch a.Mode { case LinkModeDHCP, LinkModeLinkUp, LinkModeStatic: case "": return errors.NotValidf("missing Mode") default: return errors.NotValidf("unknown Mode value (%q)", a.Mode) } if a.Subnet == nil { return errors.NotValidf("missing Subnet") } if a.IPAddress != "" && a.Mode != LinkModeStatic { return errors.NotValidf("setting IP Address when Mode is not LinkModeStatic") } if a.DefaultGateway && a.Mode != LinkModeStatic { return errors.NotValidf("specifying DefaultGateway for Mode %q", a.Mode) } return nil } // LinkSubnet implements Interface. func (i *interface_) LinkSubnet(args LinkSubnetArgs) error { if err := args.Validate(); err != nil { return errors.Trace(err) } params := NewURLParams() params.Values.Add("mode", string(args.Mode)) params.Values.Add("subnet", fmt.Sprint(args.Subnet.ID())) params.MaybeAdd("ip_address", args.IPAddress) params.MaybeAddBool("default_gateway", args.DefaultGateway) source, err := i.controller.post(i.resourceURI, "link_subnet", params.Values) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { switch svrErr.StatusCode { case http.StatusNotFound, http.StatusBadRequest: 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) } response, err := readInterface(i.controller.apiVersion, source) if err != nil { return errors.Trace(err) } i.updateFrom(response) return nil } func (i *interface_) linkForSubnet(subnet Subnet) *link { for _, link := range i.links { if s := link.Subnet(); s != nil && s.ID() == subnet.ID() { return link } } return nil } // LinkSubnet implements Interface. func (i *interface_) UnlinkSubnet(subnet Subnet) error { if subnet == nil { return errors.NotValidf("missing Subnet") } link := i.linkForSubnet(subnet) if link == nil { return errors.NotValidf("unlinked Subnet") } params := NewURLParams() params.Values.Add("id", fmt.Sprint(link.ID())) source, err := i.controller.post(i.resourceURI, "unlink_subnet", params.Values) if err != nil { if svrErr, ok := errors.Cause(err).(ServerError); ok { switch svrErr.StatusCode { case http.StatusNotFound, http.StatusBadRequest: return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage)) case http.StatusForbidden: return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) } } return NewUnexpectedError(err) } response, err := readInterface(i.controller.apiVersion, source) if err != nil { return errors.Trace(err) } i.updateFrom(response) return nil } func readInterface(controllerVersion version.Number, source interface{}) (*interface_, error) { readFunc, err := getInterfaceDeserializationFunc(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, "interface base schema check failed") } valid := coerced.(map[string]interface{}) return readFunc(valid) } func readInterfaces(controllerVersion version.Number, source interface{}) ([]*interface_, error) { readFunc, err := getInterfaceDeserializationFunc(controllerVersion) if err != nil { return nil, errors.Trace(err) } checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "interface base schema check failed") } valid := coerced.([]interface{}) return readInterfaceList(valid, readFunc) } func getInterfaceDeserializationFunc(controllerVersion version.Number) (interfaceDeserializationFunc, error) { var deserialisationVersion version.Number for v := range interfaceDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no interface read func for version %s", controllerVersion) } return interfaceDeserializationFuncs[deserialisationVersion], nil } func readInterfaceList(sourceList []interface{}, readFunc interfaceDeserializationFunc) ([]*interface_, error) { result := make([]*interface_, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for interface %d, %T", i, value) } read, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "interface %d", i) } result = append(result, read) } return result, nil } type interfaceDeserializationFunc func(map[string]interface{}) (*interface_, error) var interfaceDeserializationFuncs = map[version.Number]interfaceDeserializationFunc{ twoDotOh: interface_2_0, } func interface_2_0(source map[string]interface{}) (*interface_, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "name": schema.String(), "type": schema.String(), "enabled": schema.Bool(), "tags": schema.OneOf(schema.Nil(""), schema.List(schema.String())), "vlan": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())), "links": schema.List(schema.StringMap(schema.Any())), "mac_address": schema.OneOf(schema.Nil(""), schema.String()), "effective_mtu": schema.ForceInt(), "parents": schema.List(schema.String()), "children": schema.List(schema.String()), } defaults := schema.Defaults{ "mac_address": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "interface 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. var vlan *vlan // If it's not an attribute map then we know it's nil from the schema check. if vlanMap, ok := valid["vlan"].(map[string]interface{}); ok { vlan, err = vlan_2_0(vlanMap) if err != nil { return nil, errors.Trace(err) } } links, err := readLinkList(valid["links"].([]interface{}), link_2_0) if err != nil { return nil, errors.Trace(err) } macAddress, _ := valid["mac_address"].(string) result := &interface_{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), name: valid["name"].(string), type_: valid["type"].(string), enabled: valid["enabled"].(bool), tags: convertToStringSlice(valid["tags"]), vlan: vlan, links: links, macAddress: macAddress, effectiveMTU: valid["effective_mtu"].(int), parents: convertToStringSlice(valid["parents"]), children: convertToStringSlice(valid["children"]), } return result, nil } golang-github-juju-gomaasapi-2.2.0/interface_test.go000066400000000000000000000407051451732172100225130ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "net/http" "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type interfaceSuite struct { testing.CleanupSuite } var _ = gc.Suite(&interfaceSuite{}) func (*interfaceSuite) TestNilVLAN(c *gc.C) { var empty interface_ c.Check(empty.VLAN() == nil, jc.IsTrue) } func (*interfaceSuite) TestReadInterfacesBadSchema(c *gc.C) { _, err := readInterfaces(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `interface base schema check failed: expected list, got string("wat?")`) _, err = readInterfaces(twoDotOh, []map[string]interface{}{ { "wat": "?", }, }) c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err, gc.ErrorMatches, `interface 0: interface 2.0 schema check failed: .*`) } func (*interfaceSuite) TestReadInterfacesNulls(c *gc.C) { iface, err := readInterface(twoDotOh, parseJSON(c, interfaceNullsResponse)) c.Assert(err, jc.ErrorIsNil) c.Check(iface.MACAddress(), gc.Equals, "") c.Check(iface.Tags(), jc.DeepEquals, []string{}) c.Check(iface.VLAN(), gc.IsNil) } func (s *interfaceSuite) checkInterface(c *gc.C, iface *interface_) { c.Check(iface.ID(), gc.Equals, 40) c.Check(iface.Name(), gc.Equals, "eth0") c.Check(iface.Type(), gc.Equals, "physical") c.Check(iface.Enabled(), jc.IsTrue) c.Check(iface.Tags(), jc.DeepEquals, []string{"foo", "bar"}) c.Check(iface.MACAddress(), gc.Equals, "52:54:00:c9:6a:45") c.Check(iface.EffectiveMTU(), gc.Equals, 1500) c.Check(iface.Parents(), jc.DeepEquals, []string{"bond0"}) c.Check(iface.Children(), jc.DeepEquals, []string{"eth0.1", "eth0.2"}) vlan := iface.VLAN() c.Assert(vlan, gc.NotNil) c.Check(vlan.Name(), gc.Equals, "untagged") links := iface.Links() c.Assert(links, gc.HasLen, 1) c.Check(links[0].ID(), gc.Equals, 69) } func (s *interfaceSuite) TestReadInterfaces(c *gc.C) { interfaces, err := readInterfaces(twoDotOh, parseJSON(c, interfacesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(interfaces, gc.HasLen, 1) s.checkInterface(c, interfaces[0]) } func (s *interfaceSuite) TestReadInterface(c *gc.C) { result, err := readInterface(twoDotOh, parseJSON(c, interfaceResponse)) c.Assert(err, jc.ErrorIsNil) s.checkInterface(c, result) } func (s *interfaceSuite) TestReadInterfaceNilMAC(c *gc.C) { json := parseJSON(c, interfaceResponse) json.(map[string]interface{})["mac_address"] = nil result, err := readInterface(twoDotOh, json) c.Assert(err, jc.ErrorIsNil) c.Assert(result.MACAddress(), gc.Equals, "") } func (*interfaceSuite) TestLowVersion(c *gc.C) { _, err := readInterfaces(version.MustParse("1.9.0"), parseJSON(c, interfacesResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) c.Assert(err.Error(), gc.Equals, `no interface read func for version 1.9.0`) _, err = readInterface(version.MustParse("1.9.0"), parseJSON(c, interfaceResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) c.Assert(err.Error(), gc.Equals, `no interface read func for version 1.9.0`) } func (*interfaceSuite) TestHighVersion(c *gc.C) { read, err := readInterfaces(version.MustParse("2.1.9"), parseJSON(c, interfacesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(read, gc.HasLen, 1) _, err = readInterface(version.MustParse("2.1.9"), parseJSON(c, interfaceResponse)) c.Assert(err, jc.ErrorIsNil) } func (s *interfaceSuite) getServerAndNewInterface(c *gc.C) (*SimpleTestServer, *interface_) { server, controller := createTestServerController(c, s) server.AddGetResponse("/api/2.0/devices/", http.StatusOK, devicesResponse) devices, err := controller.Devices(DevicesArgs{}) c.Assert(err, jc.ErrorIsNil) device := devices[0].(*device) server.AddPostResponse(device.interfacesURI()+"?op=create_physical", http.StatusOK, interfaceResponse) iface, err := device.CreateInterface(minimalCreateInterfaceArgs()) c.Assert(err, jc.ErrorIsNil) return server, iface.(*interface_) } func (s *interfaceSuite) TestDelete(c *gc.C) { server, iface := s.getServerAndNewInterface(c) // Successful delete is 204 - StatusNoContent - We hope, would be consistent // with device deletions. server.AddDeleteResponse(iface.resourceURI, http.StatusNoContent, "") err := iface.Delete() c.Assert(err, jc.ErrorIsNil) } func (s *interfaceSuite) TestDelete404(c *gc.C) { _, iface := s.getServerAndNewInterface(c) // No path, so 404 err := iface.Delete() c.Assert(err, jc.Satisfies, IsNoMatchError) } func (s *interfaceSuite) TestDeleteForbidden(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddDeleteResponse(iface.resourceURI, http.StatusForbidden, "") err := iface.Delete() c.Assert(err, jc.Satisfies, IsPermissionError) } func (s *interfaceSuite) TestDeleteUnknown(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddDeleteResponse(iface.resourceURI, http.StatusConflict, "") err := iface.Delete() c.Assert(err, jc.Satisfies, IsUnexpectedError) } type fakeSubnet struct { Subnet id int cidr string vlan VLAN } func (f *fakeSubnet) ID() int { return f.id } func (f *fakeSubnet) CIDR() string { return f.cidr } func (f *fakeSubnet) VLAN() VLAN { return f.vlan } func (s *interfaceSuite) TestLinkSubnetArgs(c *gc.C) { for i, test := range []struct { args LinkSubnetArgs errText string }{{ errText: "missing Mode not valid", }, { args: LinkSubnetArgs{Mode: LinkModeDHCP}, errText: "missing Subnet not valid", }, { args: LinkSubnetArgs{Mode: InterfaceLinkMode("foo")}, errText: `unknown Mode value ("foo") not valid`, }, { args: LinkSubnetArgs{Mode: LinkModeDHCP, Subnet: &fakeSubnet{}}, }, { args: LinkSubnetArgs{Mode: LinkModeStatic, Subnet: &fakeSubnet{}}, }, { args: LinkSubnetArgs{Mode: LinkModeLinkUp, Subnet: &fakeSubnet{}}, }, { args: LinkSubnetArgs{Mode: LinkModeDHCP, Subnet: &fakeSubnet{}, IPAddress: "10.10.10.10"}, errText: `setting IP Address when Mode is not LinkModeStatic not valid`, }, { args: LinkSubnetArgs{Mode: LinkModeStatic, Subnet: &fakeSubnet{}, IPAddress: "10.10.10.10"}, }, { args: LinkSubnetArgs{Mode: LinkModeLinkUp, Subnet: &fakeSubnet{}, IPAddress: "10.10.10.10"}, errText: `setting IP Address when Mode is not LinkModeStatic not valid`, }, { args: LinkSubnetArgs{Mode: LinkModeDHCP, Subnet: &fakeSubnet{}, DefaultGateway: true}, errText: `specifying DefaultGateway for Mode "DHCP" not valid`, }, { args: LinkSubnetArgs{Mode: LinkModeStatic, Subnet: &fakeSubnet{}, DefaultGateway: true}, }, { args: LinkSubnetArgs{Mode: LinkModeLinkUp, Subnet: &fakeSubnet{}, DefaultGateway: true}, errText: `specifying DefaultGateway for Mode "LINK_UP" not valid`, }} { c.Logf("test %d", i) err := test.args.Validate() if test.errText == "" { c.Check(err, jc.ErrorIsNil) } else { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, test.errText) } } } func (s *interfaceSuite) TestLinkSubnetValidates(c *gc.C) { _, iface := s.getServerAndNewInterface(c) err := iface.LinkSubnet(LinkSubnetArgs{}) c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, "missing Mode not valid") } func (s *interfaceSuite) TestLinkSubnetGood(c *gc.C) { server, iface := s.getServerAndNewInterface(c) // The changed information is there just for the test to show that the response // is parsed and the interface updated response := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth42", }) server.AddPostResponse(iface.resourceURI+"?op=link_subnet", http.StatusOK, response) args := LinkSubnetArgs{ Mode: LinkModeStatic, Subnet: &fakeSubnet{id: 42}, IPAddress: "10.10.10.10", DefaultGateway: true, } err := iface.LinkSubnet(args) c.Check(err, jc.ErrorIsNil) c.Check(iface.Name(), gc.Equals, "eth42") request := server.LastRequest() form := request.PostForm c.Assert(form.Get("mode"), gc.Equals, "STATIC") c.Assert(form.Get("subnet"), gc.Equals, "42") c.Assert(form.Get("ip_address"), gc.Equals, "10.10.10.10") c.Assert(form.Get("default_gateway"), gc.Equals, "true") } func (s *interfaceSuite) TestLinkSubnetMissing(c *gc.C) { _, iface := s.getServerAndNewInterface(c) args := LinkSubnetArgs{ Mode: LinkModeStatic, Subnet: &fakeSubnet{id: 42}, } err := iface.LinkSubnet(args) c.Check(err, jc.Satisfies, IsBadRequestError) } func (s *interfaceSuite) TestLinkSubnetForbidden(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPostResponse(iface.resourceURI+"?op=link_subnet", http.StatusForbidden, "bad user") args := LinkSubnetArgs{ Mode: LinkModeStatic, Subnet: &fakeSubnet{id: 42}, } err := iface.LinkSubnet(args) c.Check(err, jc.Satisfies, IsPermissionError) c.Check(err.Error(), gc.Equals, "bad user") } func (s *interfaceSuite) TestLinkSubnetNoAddressesAvailable(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPostResponse(iface.resourceURI+"?op=link_subnet", http.StatusServiceUnavailable, "no addresses") args := LinkSubnetArgs{ Mode: LinkModeStatic, Subnet: &fakeSubnet{id: 42}, } err := iface.LinkSubnet(args) c.Check(err, jc.Satisfies, IsCannotCompleteError) c.Check(err.Error(), gc.Equals, "no addresses") } func (s *interfaceSuite) TestLinkSubnetUnknown(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPostResponse(iface.resourceURI+"?op=link_subnet", http.StatusMethodNotAllowed, "wat?") args := LinkSubnetArgs{ Mode: LinkModeStatic, Subnet: &fakeSubnet{id: 42}, } err := iface.LinkSubnet(args) c.Check(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 405 Method Not Allowed (wat?)") } func (s *interfaceSuite) TestUnlinkSubnetValidates(c *gc.C) { _, iface := s.getServerAndNewInterface(c) err := iface.UnlinkSubnet(nil) c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, "missing Subnet not valid") } func (s *interfaceSuite) TestUnlinkSubnetNotLinked(c *gc.C) { _, iface := s.getServerAndNewInterface(c) err := iface.UnlinkSubnet(&fakeSubnet{id: 42}) c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, "unlinked Subnet not valid") } func (s *interfaceSuite) TestUnlinkSubnetGood(c *gc.C) { server, iface := s.getServerAndNewInterface(c) // The changed information is there just for the test to show that the response // is parsed and the interface updated response := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth42", }) server.AddPostResponse(iface.resourceURI+"?op=unlink_subnet", http.StatusOK, response) err := iface.UnlinkSubnet(&fakeSubnet{id: 1}) c.Check(err, jc.ErrorIsNil) c.Check(iface.Name(), gc.Equals, "eth42") request := server.LastRequest() form := request.PostForm // The link id that contains subnet 1 has an internal id of 69. c.Assert(form.Get("id"), gc.Equals, "69") } func (s *interfaceSuite) TestUnlinkSubnetMissing(c *gc.C) { _, iface := s.getServerAndNewInterface(c) err := iface.UnlinkSubnet(&fakeSubnet{id: 1}) c.Check(err, jc.Satisfies, IsBadRequestError) } func (s *interfaceSuite) TestUnlinkSubnetForbidden(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPostResponse(iface.resourceURI+"?op=unlink_subnet", http.StatusForbidden, "bad user") err := iface.UnlinkSubnet(&fakeSubnet{id: 1}) c.Check(err, jc.Satisfies, IsPermissionError) c.Check(err.Error(), gc.Equals, "bad user") } func (s *interfaceSuite) TestUnlinkSubnetUnknown(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPostResponse(iface.resourceURI+"?op=unlink_subnet", http.StatusMethodNotAllowed, "wat?") err := iface.UnlinkSubnet(&fakeSubnet{id: 1}) c.Check(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 405 Method Not Allowed (wat?)") } func (s *interfaceSuite) TestUpdateNoChangeNoRequest(c *gc.C) { server, iface := s.getServerAndNewInterface(c) count := server.RequestCount() err := iface.Update(UpdateInterfaceArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(server.RequestCount(), gc.Equals, count) } func (s *interfaceSuite) TestUpdateMissing(c *gc.C) { _, iface := s.getServerAndNewInterface(c) err := iface.Update(UpdateInterfaceArgs{Name: "eth2"}) c.Check(err, jc.Satisfies, IsNoMatchError) } func (s *interfaceSuite) TestUpdateForbidden(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPutResponse(iface.resourceURI, http.StatusForbidden, "bad user") err := iface.Update(UpdateInterfaceArgs{Name: "eth2"}) c.Check(err, jc.Satisfies, IsPermissionError) c.Check(err.Error(), gc.Equals, "bad user") } func (s *interfaceSuite) TestUpdateUnknown(c *gc.C) { server, iface := s.getServerAndNewInterface(c) server.AddPutResponse(iface.resourceURI, http.StatusMethodNotAllowed, "wat?") err := iface.Update(UpdateInterfaceArgs{Name: "eth2"}) c.Check(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 405 Method Not Allowed (wat?)") } func (s *interfaceSuite) TestUpdateGood(c *gc.C) { server, iface := s.getServerAndNewInterface(c) // The changed information is there just for the test to show that the response // is parsed and the interface updated response := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth42", }) server.AddPutResponse(iface.resourceURI, http.StatusOK, response) args := UpdateInterfaceArgs{ Name: "eth42", MACAddress: "c3-52-51-b4-50-cd", VLAN: &fakeVLAN{id: 13}, } err := iface.Update(args) c.Check(err, jc.ErrorIsNil) c.Check(iface.Name(), gc.Equals, "eth42") request := server.LastRequest() form := request.PostForm c.Assert(form.Get("name"), gc.Equals, "eth42") c.Assert(form.Get("mac_address"), gc.Equals, "c3-52-51-b4-50-cd") c.Assert(form.Get("vlan"), gc.Equals, "13") } const ( interfacesResponse = "[" + interfaceResponse + "]" interfaceResponse = ` { "effective_mtu": 1500, "mac_address": "52:54:00:c9:6a:45", "children": ["eth0.1", "eth0.2"], "discovered": [], "params": "some params", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": ["bond0"], "id": 40, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/interfaces/40/", "tags": ["foo", "bar"], "links": [ { "id": 69, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] } ` interfaceNullsResponse = ` { "effective_mtu": 1500, "mac_address": null, "children": ["eth0.1", "eth0.2"], "discovered": [], "params": "some params", "vlan": null, "name": "eth0", "enabled": true, "parents": ["bond0"], "id": 40, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/interfaces/40/", "tags": null, "links": [ { "id": 69, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] } ` ) golang-github-juju-gomaasapi-2.2.0/interfaces.go000066400000000000000000000314051451732172100216340ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import "github.com/juju/collections/set" const ( // Capability constants. NetworksManagement = "networks-management" StaticIPAddresses = "static-ipaddresses" IPv6DeploymentUbuntu = "ipv6-deployment-ubuntu" DevicesManagement = "devices-management" StorageDeploymentUbuntu = "storage-deployment-ubuntu" NetworkDeploymentUbuntu = "network-deployment-ubuntu" ) // Controller represents an API connection to a MAAS Controller. Since the API // is restful, there is no long held connection to the API server, but instead // HTTP calls are made and JSON response structures parsed. type Controller interface { // APIVersionInfo returns the version and subversion strings for the MAAS // controller. APIVersionInfo() (string, string, error) // Capabilities returns a set of capabilities as defined by the string // constants. Capabilities() set.Strings BootResources() ([]BootResource, error) // Fabrics returns the list of Fabrics defined in the MAAS controller. Fabrics() ([]Fabric, error) // Spaces returns the list of Spaces defined in the MAAS controller. Spaces() ([]Space, error) // StaticRoutes returns the list of StaticRoutes defined in the MAAS controller. StaticRoutes() ([]StaticRoute, error) // Zones lists all the zones known to the MAAS controller. Zones() ([]Zone, error) // Pools lists all the pools known to the MAAS controller. Pools() ([]Pool, error) // Machines returns a list of machines that match the params. Machines(MachinesArgs) ([]Machine, error) // AllocateMachine will attempt to allocate a machine to the user. // If successful, the allocated machine is returned. AllocateMachine(AllocateMachineArgs) (Machine, ConstraintMatches, error) // ReleaseMachines will stop the specified machines, and release them // from the user making them available to be allocated again. ReleaseMachines(ReleaseMachinesArgs) error // Devices returns a list of devices that match the params. Devices(DevicesArgs) ([]Device, error) // CreateDevice creates and returns a new Device. CreateDevice(CreateDeviceArgs) (Device, error) // Files returns all the files that match the specified prefix. Files(prefix string) ([]File, error) // Return a single file by its filename. GetFile(filename string) (File, error) // AddFile adds or replaces the content of the specified filename. // If or when the MAAS api is able to return metadata about a single // file without sending the content of the file, we can return a File // instance here too. AddFile(AddFileArgs) error // Returns the DNS Domain Managed By MAAS Domains() ([]Domain, error) // Returns the list of MAAS tags Tags() ([]Tag, error) } // File represents a file stored in the MAAS controller. type File interface { // Filename is the name of the file. No path, just the filename. Filename() string // AnonymousURL is a URL that can be used to retrieve the conents of the // file without credentials. AnonymousURL() string // Delete removes the file from the MAAS controller. Delete() error // ReadAll returns the content of the file. ReadAll() ([]byte, error) } // Fabric represents a set of interconnected VLANs that are capable of mutual // communication. A fabric can be thought of as a logical grouping in which // VLANs can be considered unique. // // For example, a distributed network may have a fabric in London containing // VLAN 100, while a separate fabric in San Francisco may contain a VLAN 100, // whose attached subnets are completely different and unrelated. type Fabric interface { ID() int Name() string ClassType() string VLANs() []VLAN } // VLAN represents an instance of a Virtual LAN. VLANs are a common way to // create logically separate networks using the same physical infrastructure. // // Managed switches can assign VLANs to each port in either a “tagged” or an // “untagged” manner. A VLAN is said to be “untagged” on a particular port when // it is the default VLAN for that port, and requires no special configuration // in order to access. // // “Tagged” VLANs (traditionally used by network administrators in order to // aggregate multiple networks over inter-switch “trunk” lines) can also be used // with nodes in MAAS. That is, if a switch port is configured such that // “tagged” VLAN frames can be sent and received by a MAAS node, that MAAS node // can be configured to automatically bring up VLAN interfaces, so that the // deployed node can make use of them. // // A “Default VLAN” is created for every Fabric, to which every new VLAN-aware // object in the fabric will be associated to by default (unless otherwise // specified). type VLAN interface { ID() int Name() string Fabric() string // VID is the VLAN ID. eth0.10 -> VID = 10. VID() int // MTU (maximum transmission unit) is the largest size packet or frame, // specified in octets (eight-bit bytes), that can be sent. MTU() int DHCP() bool PrimaryRack() string SecondaryRack() string } // Zone represents a physical zone that a Machine is in. The meaning of a // physical zone is up to you: it could identify e.g. a server rack, a network, // or a data centre. Users can then allocate nodes from specific physical zones, // to suit their redundancy or performance requirements. type Zone interface { Name() string Description() string } // Pool is just a logical separation of resources. type Pool interface { // The name of the resource pool Name() string Description() string } type Domain interface { // The name of the Domain Name() string } // BootResource is the bomb... find something to say here. type BootResource interface { ID() int Name() string Type() string Architecture() string SubArchitectures() set.Strings KernelFlavor() string } // Device represents some form of device in MAAS. type Device interface { // TODO: add domain SystemID() string Hostname() string FQDN() string IPAddresses() []string Zone() Zone Pool() Pool // Parent returns the SystemID of the Parent. Most often this will be a // Machine. Parent() string // Owner is the username of the user that created the device. Owner() string // InterfaceSet returns all the interfaces for the Device. InterfaceSet() []Interface // CreateInterface will create a physical interface for this machine. CreateInterface(CreateInterfaceArgs) (Interface, error) // Delete will remove this Device. Delete() error } // Machine represents a physical machine. type Machine interface { OwnerDataHolder SystemID() string Hostname() string FQDN() string Tags() []string OperatingSystem() string DistroSeries() string Architecture() string Memory() int CPUCount() int HardwareInfo() map[string]string IPAddresses() []string PowerState() string // Devices returns a list of devices that match the params and have // this Machine as the parent. Devices(DevicesArgs) ([]Device, error) // Consider bundling the status values into a single struct. // but need to check for consistent representation if exposed on other // entities. StatusName() string StatusMessage() string // BootInterface returns the interface that was used to boot the Machine. BootInterface() Interface // InterfaceSet returns all the interfaces for the Machine. InterfaceSet() []Interface // Interface returns the interface for the machine that matches the id // specified. If there is no match, nil is returned. Interface(id int) Interface // PhysicalBlockDevices returns all the physical block devices on the machine. PhysicalBlockDevices() []BlockDevice // PhysicalBlockDevice returns the physical block device for the machine // that matches the id specified. If there is no match, nil is returned. PhysicalBlockDevice(id int) BlockDevice // BlockDevices returns all the physical and virtual block devices on the machine. BlockDevices() []BlockDevice // BlockDevice returns the block device for the machine that matches the // id specified. If there is no match, nil is returned. BlockDevice(id int) BlockDevice // Partition returns the partition for the machine that matches the // id specified. If there is no match, nil is returned. Partition(id int) Partition Zone() Zone Pool() Pool // 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) } // Space is a name for a collection of Subnets. type Space interface { ID() int Name() string Subnets() []Subnet } // Subnet refers to an IP range on a VLAN. type Subnet interface { ID() int Name() string Space() string VLAN() VLAN Gateway() string CIDR() string // dns_mode // DNSServers is a list of ip addresses of the DNS servers for the subnet. // This list may be empty. DNSServers() []string } // StaticRoute defines an explicit route that users have requested to be added // for a given subnet. type StaticRoute interface { // Source is the subnet that should have the route configured. (Machines // inside Source should use GatewayIP to reach Destination addresses.) Source() Subnet // Destination is the subnet that a machine wants to send packets to. We // want to configure a route to that subnet via GatewayIP. Destination() Subnet // GatewayIP is the IPAddress to direct traffic to. GatewayIP() string // Metric is the routing metric that determines whether this route will // take precedence over similar routes (there may be a route for 10/8, but // also a more concrete route for 10.0/16 that should take precedence if it // applies.) Metric should be a non-negative integer. Metric() int } // Interface represents a physical or virtual network interface on a Machine. type Interface interface { ID() int Name() string // The parents of an interface are the names of interfaces that must exist // for this interface to exist. For example a parent of "eth0.100" would be // "eth0". Parents may be empty. Parents() []string // The children interfaces are the names of those that are dependent on this // interface existing. Children may be empty. Children() []string Type() string Enabled() bool Tags() []string VLAN() VLAN Links() []Link MACAddress() string EffectiveMTU() int // Params is a JSON field, and defaults to an empty string, but is almost // always a JSON object in practice. Gleefully ignoring it until we need it. // Update the name, mac address or VLAN. Update(UpdateInterfaceArgs) error // Delete this interface. Delete() error // LinkSubnet will attempt to make this interface available on the specified // Subnet. LinkSubnet(LinkSubnetArgs) error // UnlinkSubnet will remove the Link to the subnet, and release the IP // address associated if there is one. UnlinkSubnet(Subnet) error } // Link represents a network link between an Interface and a Subnet. type Link interface { ID() int Mode() string Subnet() Subnet // IPAddress returns the address if one has been assigned. // If unavailble, the address will be empty. IPAddress() string } // FileSystem represents a formatted filesystem mounted at a location. type FileSystem interface { // Type is the format type, e.g. "ext4". Type() string MountPoint() string Label() string UUID() string } // StorageDevice represents any piece of storage on a machine. Partition // and BlockDevice are storage devices. type StorageDevice interface { // Type is the type of item. Type() string // ID is the unique ID of the item of that type. ID() int Path() string UsedFor() string Size() uint64 UUID() string Tags() []string // FileSystem may be nil if not mounted. FileSystem() FileSystem } // Partition represents a partition of a block device. It may be mounted // as a filesystem. type Partition interface { StorageDevice } // BlockDevice represents an entire block device on the machine. type BlockDevice interface { StorageDevice Name() string Model() string IDPath() string BlockSize() uint64 UsedSize() uint64 Partitions() []Partition // There are some other attributes for block devices, but we can // expose them on an as needed basis. } // OwnerDataHolder represents any MAAS object that can store key/value // data. type OwnerDataHolder interface { // OwnerData returns a copy of the key/value data stored for this // object. OwnerData() map[string]string // SetOwnerData updates the key/value data stored for this object // with the values passed in. Existing keys that aren't specified // in the map passed in will be left in place; to clear a key set // its value to "". All owner data is cleared when the object is // released. SetOwnerData(map[string]string) error } // Tag represents a MAAS tag. type Tag interface { Name() string Comment() string Definition() string KernelOpts() string } golang-github-juju-gomaasapi-2.2.0/jsonobject.go000066400000000000000000000163431451732172100216550ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "errors" "fmt" ) // JSONObject is a wrapper around a JSON structure which provides // methods to extract data from that structure. // A JSONObject provides a simple structure consisting of the data types // defined in JSON: string, number, object, list, and bool. To get the // value you want out of a JSONObject, you must know (or figure out) which // kind of value you have, and then call the appropriate Get*() method to // get at it. Reading an item as the wrong type will return an error. // For instance, if your JSONObject consists of a number, call GetFloat64() // to get the value as a float64. If it's a list, call GetArray() to get // a slice of JSONObjects. To read any given item from the slice, you'll // need to "Get" that as the right type as well. // There is one exception: a MAASObject is really a special kind of map, // so you can read it as either. // Reading a null item is also an error. So before you try obj.Get*(), // first check obj.IsNil(). type JSONObject struct { // Parsed value. May actually be any of the types a JSONObject can // wrap, except raw bytes. If the object can only be interpreted // as raw bytes, this will be nil. value interface{} // Raw bytes, if this object was parsed directly from an API response. // Is nil for sub-objects found within other objects. An object that // was parsed directly from a response can be both raw bytes and some // other value at the same time. // For example, "[]" looks like a JSON list, so you can read it as an // array. But it may also be the raw contents of a file that just // happens to look like JSON, and so you can read it as raw bytes as // well. bytes []byte // Client for further communication with the API. client Client // Is this a JSON null? isNull bool } // Our JSON processor distinguishes a MAASObject from a jsonMap by the fact // that it contains a key "resource_uri". (A regular map might contain the // same key through sheer coincide, but never mind: you can still treat it // as a jsonMap and never notice the difference.) const resourceURI = "resource_uri" // maasify turns a completely untyped json.Unmarshal result into a JSONObject // (with the appropriate implementation of course). This function is // recursive. Maps and arrays are deep-copied, with each individual value // being converted to a JSONObject type. func maasify(client Client, value interface{}) JSONObject { if value == nil { return JSONObject{isNull: true} } switch value.(type) { case string, float64, bool: return JSONObject{value: value} case map[string]interface{}: original := value.(map[string]interface{}) result := make(map[string]JSONObject, len(original)) for key, value := range original { result[key] = maasify(client, value) } return JSONObject{value: result, client: client} case []interface{}: original := value.([]interface{}) result := make([]JSONObject, len(original)) for index, value := range original { result[index] = maasify(client, value) } return JSONObject{value: result} } msg := fmt.Sprintf("Unknown JSON type, can't be converted to JSONObject: %v", value) panic(msg) } // Parse a JSON blob into a JSONObject. func Parse(client Client, input []byte) (JSONObject, error) { var obj JSONObject if input == nil { panic(errors.New("Parse() called with nil input")) } var parsed interface{} err := json.Unmarshal(input, &parsed) if err == nil { obj = maasify(client, parsed) obj.bytes = input } else { switch err.(type) { case *json.InvalidUTF8Error: case *json.SyntaxError: // This isn't JSON. Treat it as raw binary data. default: return obj, err } obj = JSONObject{value: nil, client: client, bytes: input} } return obj, nil } // JSONObjectFromStruct takes a struct and converts it to a JSONObject func JSONObjectFromStruct(client Client, input interface{}) (JSONObject, error) { j, err := json.MarshalIndent(input, "", " ") if err != nil { return JSONObject{}, err } return Parse(client, j) } // Return error value for failed type conversion. func failConversion(wantedType string, obj JSONObject) error { msg := fmt.Sprintf("Requested %v, got %T.", wantedType, obj.value) return errors.New(msg) } // MarshalJSON tells the standard json package how to serialize a JSONObject // back to JSON. func (obj JSONObject) MarshalJSON() ([]byte, error) { if obj.IsNil() { return json.Marshal(nil) } return json.MarshalIndent(obj.value, "", " ") } // With MarshalJSON, JSONObject implements json.Marshaler. var _ json.Marshaler = (*JSONObject)(nil) // IsNil tells you whether a JSONObject is a JSON "null." // There is one irregularity. If the original JSON blob was actually raw // data, not JSON, then its IsNil will return false because the object // contains the binary data as a non-nil value. But, if the original JSON // blob consisted of a null, then IsNil returns true even though you can // still retrieve binary data from it. func (obj JSONObject) IsNil() bool { if obj.value != nil { return false } if obj.bytes == nil { return true } // This may be a JSON null. We can't expect every JSON null to look // the same; there may be leading or trailing space. return obj.isNull } // GetString retrieves the object's value as a string. If the value wasn't // a JSON string, that's an error. func (obj JSONObject) GetString() (value string, err error) { value, ok := obj.value.(string) if !ok { err = failConversion("string", obj) } return } // GetFloat64 retrieves the object's value as a float64. If the value wasn't // a JSON number, that's an error. func (obj JSONObject) GetFloat64() (value float64, err error) { value, ok := obj.value.(float64) if !ok { err = failConversion("float64", obj) } return } // GetMap retrieves the object's value as a map. If the value wasn't a JSON // object, that's an error. func (obj JSONObject) GetMap() (value map[string]JSONObject, err error) { value, ok := obj.value.(map[string]JSONObject) if !ok { err = failConversion("map", obj) } return } // GetArray retrieves the object's value as an array. If the value wasn't a // JSON list, that's an error. func (obj JSONObject) GetArray() (value []JSONObject, err error) { value, ok := obj.value.([]JSONObject) if !ok { err = failConversion("array", obj) } return } // GetBool retrieves the object's value as a bool. If the value wasn't a JSON // bool, that's an error. func (obj JSONObject) GetBool() (value bool, err error) { value, ok := obj.value.(bool) if !ok { err = failConversion("bool", obj) } return } // GetBytes retrieves the object's value as raw bytes. A JSONObject that was // parsed from the original input (as opposed to one that's embedded in // another JSONObject) can contain both the raw bytes and the parsed JSON // value, but either can be the case without the other. // If this object wasn't parsed directly from the original input, that's an // error. // If the object was parsed from an original input that just said "null", then // IsNil will return true but the raw bytes are still available from GetBytes. func (obj JSONObject) GetBytes() ([]byte, error) { if obj.bytes == nil { return nil, failConversion("bytes", obj) } return obj.bytes, nil } golang-github-juju-gomaasapi-2.2.0/jsonobject_test.go000066400000000000000000000322061451732172100227100ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "fmt" . "gopkg.in/check.v1" ) type JSONObjectSuite struct { } var _ = Suite(&JSONObjectSuite{}) // maasify() converts nil. func (suite *JSONObjectSuite) TestMaasifyConvertsNil(c *C) { c.Check(maasify(Client{}, nil).IsNil(), Equals, true) } // maasify() converts strings. func (suite *JSONObjectSuite) TestMaasifyConvertsString(c *C) { const text = "Hello" out, err := maasify(Client{}, text).GetString() c.Assert(err, IsNil) c.Check(out, Equals, text) } // maasify() converts float64 numbers. func (suite *JSONObjectSuite) TestMaasifyConvertsNumber(c *C) { const number = 3.1415926535 num, err := maasify(Client{}, number).GetFloat64() c.Assert(err, IsNil) c.Check(num, Equals, number) } // maasify() converts array slices. func (suite *JSONObjectSuite) TestMaasifyConvertsArray(c *C) { original := []interface{}{3.0, 2.0, 1.0} output, err := maasify(Client{}, original).GetArray() c.Assert(err, IsNil) c.Check(len(output), Equals, len(original)) } // When maasify() converts an array slice, the result contains JSONObjects. func (suite *JSONObjectSuite) TestMaasifyArrayContainsJSONObjects(c *C) { arr, err := maasify(Client{}, []interface{}{9.9}).GetArray() c.Assert(err, IsNil) var _ JSONObject = arr[0] entry, err := arr[0].GetFloat64() c.Assert(err, IsNil) c.Check(entry, Equals, 9.9) } // maasify() converts maps. func (suite *JSONObjectSuite) TestMaasifyConvertsMap(c *C) { original := map[string]interface{}{"1": "one", "2": "two", "3": "three"} output, err := maasify(Client{}, original).GetMap() c.Assert(err, IsNil) c.Check(len(output), Equals, len(original)) } // When maasify() converts a map, the result contains JSONObjects. func (suite *JSONObjectSuite) TestMaasifyMapContainsJSONObjects(c *C) { jsonobj := maasify(Client{}, map[string]interface{}{"key": "value"}) mp, err := jsonobj.GetMap() var _ JSONObject = mp["key"] c.Assert(err, IsNil) entry, err := mp["key"].GetString() c.Check(entry, Equals, "value") } // maasify() converts MAAS objects. func (suite *JSONObjectSuite) TestMaasifyConvertsMAASObject(c *C) { original := map[string]interface{}{ "resource_uri": "http://example.com/foo", "size": "3", } obj, err := maasify(Client{}, original).GetMAASObject() c.Assert(err, IsNil) c.Check(len(obj.GetMap()), Equals, len(original)) size, err := obj.GetMap()["size"].GetString() c.Assert(err, IsNil) c.Check(size, Equals, "3") } // maasify() passes its client to a MAASObject it creates. func (suite *JSONObjectSuite) TestMaasifyPassesClientToMAASObject(c *C) { client := Client{} original := map[string]interface{}{"resource_uri": "/foo"} output, err := maasify(client, original).GetMAASObject() c.Assert(err, IsNil) c.Check(output.client, Equals, client) } // maasify() passes its client into an array of MAASObjects it creates. func (suite *JSONObjectSuite) TestMaasifyPassesClientIntoArray(c *C) { client := Client{} obj := map[string]interface{}{"resource_uri": "/foo"} list := []interface{}{obj} jsonobj, err := maasify(client, list).GetArray() c.Assert(err, IsNil) out, err := jsonobj[0].GetMAASObject() c.Assert(err, IsNil) c.Check(out.client, Equals, client) } // maasify() passes its client into a map of MAASObjects it creates. func (suite *JSONObjectSuite) TestMaasifyPassesClientIntoMap(c *C) { client := Client{} obj := map[string]interface{}{"resource_uri": "/foo"} mp := map[string]interface{}{"key": obj} jsonobj, err := maasify(client, mp).GetMap() c.Assert(err, IsNil) out, err := jsonobj["key"].GetMAASObject() c.Assert(err, IsNil) c.Check(out.client, Equals, client) } // maasify() passes its client all the way down into any MAASObjects in the // object structure it creates. func (suite *JSONObjectSuite) TestMaasifyPassesClientAllTheWay(c *C) { client := Client{} obj := map[string]interface{}{"resource_uri": "/foo"} mp := map[string]interface{}{"key": obj} list := []interface{}{mp} jsonobj, err := maasify(client, list).GetArray() c.Assert(err, IsNil) outerMap, err := jsonobj[0].GetMap() c.Assert(err, IsNil) out, err := outerMap["key"].GetMAASObject() c.Assert(err, IsNil) c.Check(out.client, Equals, client) } // maasify() converts Booleans. func (suite *JSONObjectSuite) TestMaasifyConvertsBool(c *C) { t, err := maasify(Client{}, true).GetBool() c.Assert(err, IsNil) f, err := maasify(Client{}, false).GetBool() c.Assert(err, IsNil) c.Check(t, Equals, true) c.Check(f, Equals, false) } // Parse takes you from a JSON blob to a JSONObject. func (suite *JSONObjectSuite) TestParseMaasifiesJSONBlob(c *C) { blob := []byte("[12]") obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) arr, err := obj.GetArray() c.Assert(err, IsNil) out, err := arr[0].GetFloat64() c.Assert(err, IsNil) c.Check(out, Equals, 12.0) } func (suite *JSONObjectSuite) TestParseKeepsBinaryOriginal(c *C) { blob := []byte(`"Hi"`) obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) text, err := obj.GetString() c.Assert(err, IsNil) c.Check(text, Equals, "Hi") binary, err := obj.GetBytes() c.Assert(err, IsNil) c.Check(binary, DeepEquals, blob) } func (suite *JSONObjectSuite) TestParseTreatsInvalidJSONAsBinary(c *C) { blob := []byte("?x]}y![{z") obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) c.Check(obj.IsNil(), Equals, false) c.Check(obj.value, IsNil) binary, err := obj.GetBytes() c.Assert(err, IsNil) c.Check(binary, DeepEquals, blob) } func (suite *JSONObjectSuite) TestParseTreatsInvalidUTF8AsBinary(c *C) { // Arbitrary data that is definitely not UTF-8. blob := []byte{220, 8, 129} obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) c.Check(obj.IsNil(), Equals, false) c.Check(obj.value, IsNil) binary, err := obj.GetBytes() c.Assert(err, IsNil) c.Check(binary, DeepEquals, blob) } func (suite *JSONObjectSuite) TestParseTreatsEmptyJSONAsBinary(c *C) { blob := []byte{} obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) c.Check(obj.IsNil(), Equals, false) data, err := obj.GetBytes() c.Assert(err, IsNil) c.Check(data, DeepEquals, blob) } func (suite *JSONObjectSuite) TestParsePanicsOnNilJSON(c *C) { defer func() { failure := recover() c.Assert(failure, NotNil) c.Check(failure.(error).Error(), Matches, ".*nil input") }() Parse(Client{}, nil) } func (suite *JSONObjectSuite) TestParseNullProducesIsNil(c *C) { blob := []byte("null") obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) c.Check(obj.IsNil(), Equals, true) } func (suite *JSONObjectSuite) TestParseNonNullProducesNonIsNil(c *C) { blob := []byte("1") obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) c.Check(obj.IsNil(), Equals, false) } func (suite *JSONObjectSuite) TestParseSpacedNullProducesIsNil(c *C) { blob := []byte(" null ") obj, err := Parse(Client{}, blob) c.Assert(err, IsNil) c.Check(obj.IsNil(), Equals, true) } // String-type JSONObjects convert only to string. func (suite *JSONObjectSuite) TestConversionsString(c *C) { obj := maasify(Client{}, "Test string") value, err := obj.GetString() c.Check(err, IsNil) c.Check(value, Equals, "Test string") _, err = obj.GetFloat64() c.Check(err, NotNil) _, err = obj.GetMap() c.Check(err, NotNil) _, err = obj.GetMAASObject() c.Check(err, NotNil) _, err = obj.GetArray() c.Check(err, NotNil) _, err = obj.GetBool() c.Check(err, NotNil) } // Number-type JSONObjects convert only to float64. func (suite *JSONObjectSuite) TestConversionsFloat64(c *C) { obj := maasify(Client{}, 1.1) value, err := obj.GetFloat64() c.Check(err, IsNil) c.Check(value, Equals, 1.1) _, err = obj.GetString() c.Check(err, NotNil) _, err = obj.GetMap() c.Check(err, NotNil) _, err = obj.GetMAASObject() c.Check(err, NotNil) _, err = obj.GetArray() c.Check(err, NotNil) _, err = obj.GetBool() c.Check(err, NotNil) } // Map-type JSONObjects convert only to map. func (suite *JSONObjectSuite) TestConversionsMap(c *C) { obj := maasify(Client{}, map[string]interface{}{"x": "y"}) value, err := obj.GetMap() c.Check(err, IsNil) text, err := value["x"].GetString() c.Check(err, IsNil) c.Check(text, Equals, "y") _, err = obj.GetString() c.Check(err, NotNil) _, err = obj.GetFloat64() c.Check(err, NotNil) _, err = obj.GetMAASObject() c.Check(err, NotNil) _, err = obj.GetArray() c.Check(err, NotNil) _, err = obj.GetBool() c.Check(err, NotNil) } // Array-type JSONObjects convert only to array. func (suite *JSONObjectSuite) TestConversionsArray(c *C) { obj := maasify(Client{}, []interface{}{"item"}) value, err := obj.GetArray() c.Check(err, IsNil) text, err := value[0].GetString() c.Check(err, IsNil) c.Check(text, Equals, "item") _, err = obj.GetString() c.Check(err, NotNil) _, err = obj.GetFloat64() c.Check(err, NotNil) _, err = obj.GetMap() c.Check(err, NotNil) _, err = obj.GetMAASObject() c.Check(err, NotNil) _, err = obj.GetBool() c.Check(err, NotNil) } // Boolean-type JSONObjects convert only to bool. func (suite *JSONObjectSuite) TestConversionsBool(c *C) { obj := maasify(Client{}, false) value, err := obj.GetBool() c.Check(err, IsNil) c.Check(value, Equals, false) _, err = obj.GetString() c.Check(err, NotNil) _, err = obj.GetFloat64() c.Check(err, NotNil) _, err = obj.GetMap() c.Check(err, NotNil) _, err = obj.GetMAASObject() c.Check(err, NotNil) _, err = obj.GetArray() c.Check(err, NotNil) } func (suite *JSONObjectSuite) TestNilSerializesToJSON(c *C) { output, err := json.Marshal(maasify(Client{}, nil)) c.Assert(err, IsNil) c.Check(output, DeepEquals, []byte("null")) } func (suite *JSONObjectSuite) TestEmptyStringSerializesToJSON(c *C) { output, err := json.Marshal(maasify(Client{}, "")) c.Assert(err, IsNil) c.Check(string(output), Equals, `""`) } func (suite *JSONObjectSuite) TestStringSerializesToJSON(c *C) { text := "Text wrapped in JSON" output, err := json.Marshal(maasify(Client{}, text)) c.Assert(err, IsNil) c.Check(output, DeepEquals, []byte(fmt.Sprintf(`"%s"`, text))) } func (suite *JSONObjectSuite) TestStringIsEscapedInJSON(c *C) { text := `\"Quote,\" \\backslash, and \'apostrophe\'.` output, err := json.Marshal(maasify(Client{}, text)) c.Assert(err, IsNil) var deserialized string err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized, Equals, text) } func (suite *JSONObjectSuite) TestFloat64SerializesToJSON(c *C) { number := 3.1415926535 output, err := json.Marshal(maasify(Client{}, number)) c.Assert(err, IsNil) var deserialized float64 err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized, Equals, number) } func (suite *JSONObjectSuite) TestEmptyMapSerializesToJSON(c *C) { mp := map[string]interface{}{} output, err := json.Marshal(maasify(Client{}, mp)) c.Assert(err, IsNil) var deserialized interface{} err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized.(map[string]interface{}), DeepEquals, mp) } func (suite *JSONObjectSuite) TestMapSerializesToJSON(c *C) { // Sample data: counting in Japanese. mp := map[string]interface{}{"one": "ichi", "two": "nii", "three": "san"} output, err := json.Marshal(maasify(Client{}, mp)) c.Assert(err, IsNil) var deserialized interface{} err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized.(map[string]interface{}), DeepEquals, mp) } func (suite *JSONObjectSuite) TestEmptyArraySerializesToJSON(c *C) { arr := []interface{}{} output, err := json.Marshal(maasify(Client{}, arr)) c.Assert(err, IsNil) var deserialized interface{} err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) // The deserialized value is a slice, and it contains no elements. // Can't do a regular comparison here because at least in the current // json implementation, an empty list deserializes as a nil slice, // not as an empty slice! // (It doesn't work that way for maps though, for some reason). c.Check(len(deserialized.([]interface{})), Equals, len(arr)) } func (suite *JSONObjectSuite) TestArrayOfStringsSerializesToJSON(c *C) { value := "item" output, err := json.Marshal(maasify(Client{}, []interface{}{value})) c.Assert(err, IsNil) var deserialized []string err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized, DeepEquals, []string{value}) } func (suite *JSONObjectSuite) TestArrayOfNumbersSerializesToJSON(c *C) { value := 9.0 output, err := json.Marshal(maasify(Client{}, []interface{}{value})) c.Assert(err, IsNil) var deserialized []float64 err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized, DeepEquals, []float64{value}) } func (suite *JSONObjectSuite) TestArrayPreservesOrderInJSON(c *C) { // Sample data: counting in Korean. arr := []interface{}{"jong", "il", "ee", "sam"} output, err := json.Marshal(maasify(Client{}, arr)) c.Assert(err, IsNil) var deserialized []interface{} err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized, DeepEquals, arr) } func (suite *JSONObjectSuite) TestBoolSerializesToJSON(c *C) { f, err := json.Marshal(maasify(Client{}, false)) c.Assert(err, IsNil) t, err := json.Marshal(maasify(Client{}, true)) c.Assert(err, IsNil) c.Check(f, DeepEquals, []byte("false")) c.Check(t, DeepEquals, []byte("true")) } golang-github-juju-gomaasapi-2.2.0/link.go000066400000000000000000000063021451732172100204440ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type link struct { id int mode string subnet *subnet ipAddress string } // NOTE: not using lowercase L as the receiver as it is a horrible idea. // Instead using 'k'. // ID implements Link. func (k *link) ID() int { return k.id } // Mode implements Link. func (k *link) Mode() string { return k.mode } // Subnet implements Link. func (k *link) Subnet() Subnet { if k.subnet == nil { return nil } return k.subnet } // IPAddress implements Link. func (k *link) IPAddress() string { return k.ipAddress } func readLinks(controllerVersion version.Number, source interface{}) ([]*link, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "link base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range linkDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no link read func for version %s", controllerVersion) } readFunc := linkDeserializationFuncs[deserialisationVersion] return readLinkList(valid, readFunc) } // readLinkList expects the values of the sourceList to be string maps. func readLinkList(sourceList []interface{}, readFunc linkDeserializationFunc) ([]*link, error) { result := make([]*link, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for link %d, %T", i, value) } link, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "link %d", i) } result = append(result, link) } return result, nil } type linkDeserializationFunc func(map[string]interface{}) (*link, error) var linkDeserializationFuncs = map[version.Number]linkDeserializationFunc{ twoDotOh: link_2_0, } func link_2_0(source map[string]interface{}) (*link, error) { fields := schema.Fields{ "id": schema.ForceInt(), "mode": schema.String(), "subnet": schema.StringMap(schema.Any()), "ip_address": schema.String(), } defaults := schema.Defaults{ "ip_address": "", "subnet": schema.Omit, } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "link 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. var subnet *subnet if value, ok := valid["subnet"]; ok { subnet, err = subnet_2_0(value.(map[string]interface{})) if err != nil { return nil, errors.Trace(err) } } result := &link{ id: valid["id"].(int), mode: valid["mode"].(string), subnet: subnet, ipAddress: valid["ip_address"].(string), } return result, nil } golang-github-juju-gomaasapi-2.2.0/link_test.go000066400000000000000000000057571451732172100215200ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type linkSuite struct{} var _ = gc.Suite(&linkSuite{}) func (*linkSuite) TestNilSubnet(c *gc.C) { var empty link c.Check(empty.Subnet() == nil, jc.IsTrue) } func (*linkSuite) TestReadLinksBadSchema(c *gc.C) { _, err := readLinks(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `link base schema check failed: expected list, got string("wat?")`) } func (*linkSuite) TestReadLinks(c *gc.C) { links, err := readLinks(twoDotOh, parseJSON(c, linksResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(links, gc.HasLen, 2) link := links[0] c.Assert(link.ID(), gc.Equals, 69) c.Assert(link.Mode(), gc.Equals, "auto") c.Assert(link.IPAddress(), gc.Equals, "192.168.100.5") subnet := link.Subnet() c.Assert(subnet, gc.NotNil) c.Assert(subnet.Name(), gc.Equals, "192.168.100.0/24") // Second link has missing ip_address c.Assert(links[1].IPAddress(), gc.Equals, "") } func (*linkSuite) TestLowVersion(c *gc.C) { _, err := readLinks(version.MustParse("1.9.0"), parseJSON(c, linksResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (*linkSuite) TestHighVersion(c *gc.C) { links, err := readLinks(version.MustParse("2.1.9"), parseJSON(c, linksResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(links, gc.HasLen, 2) } var linksResponse = ` [ { "id": 69, "mode": "auto", "ip_address": "192.168.100.5", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } }, { "id": 70, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] ` golang-github-juju-gomaasapi-2.2.0/maas.go000066400000000000000000000005401451732172100204260ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi // NewMAAS returns an interface to the MAAS API as a *MAASObject. func NewMAAS(client Client) *MAASObject { attrs := map[string]interface{}{resourceURI: client.APIURL.String()} obj := newJSONMAASObject(attrs, client) return &obj } golang-github-juju-gomaasapi-2.2.0/maas_test.go000066400000000000000000000007441451732172100214730ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "net/url" . "gopkg.in/check.v1" ) type MAASSuite struct{} var _ = Suite(&MAASSuite{}) func (suite *MAASSuite) TestNewMAASUsesBaseURLFromClient(c *C) { baseURLString := "https://server.com:888/" baseURL, _ := url.Parse(baseURLString) client := Client{APIURL: baseURL} maas := NewMAAS(client) URL := maas.URL() c.Check(URL, DeepEquals, baseURL) } golang-github-juju-gomaasapi-2.2.0/maasobject.go000066400000000000000000000140451451732172100216220ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "errors" "fmt" "net/url" ) // MAASObject represents a MAAS object as returned by the MAAS API, such as a // Node or a Tag. // You can extract a MAASObject out of a JSONObject using // JSONObject.GetMAASObject. A MAAS API call will usually return either a // MAASObject or a list of MAASObjects. The list itself would be wrapped in // a JSONObject, so if an API call returns a list of objects "l," you first // obtain the array using l.GetArray(). Then, for each item "i" in the array, // obtain the matching MAASObject using i.GetMAASObject(). type MAASObject struct { values map[string]JSONObject client Client uri *url.URL } // newJSONMAASObject creates a new MAAS object. It will panic if the given map // does not contain a valid URL for the 'resource_uri' key. func newJSONMAASObject(jmap map[string]interface{}, client Client) MAASObject { obj, err := maasify(client, jmap).GetMAASObject() if err != nil { panic(err) } return obj } // MarshalJSON tells the standard json package how to serialize a MAASObject. func (obj MAASObject) MarshalJSON() ([]byte, error) { return json.MarshalIndent(obj.GetMap(), "", " ") } // With MarshalJSON, MAASObject implements json.Marshaler. var _ json.Marshaler = (*MAASObject)(nil) func marshalNode(node MAASObject) string { res, _ := json.MarshalIndent(node, "", " ") return string(res) } var noResourceURI = errors.New("not a MAAS object: no 'resource_uri' key") // extractURI obtains the "resource_uri" string from a JSONObject map. func extractURI(attrs map[string]JSONObject) (*url.URL, error) { uriEntry, ok := attrs[resourceURI] if !ok { return nil, noResourceURI } uri, err := uriEntry.GetString() if err != nil { return nil, fmt.Errorf("invalid resource_uri: %v", uri) } resourceURL, err := url.Parse(uri) if err != nil { return nil, fmt.Errorf("resource_uri does not contain a valid URL: %v", uri) } return resourceURL, nil } // JSONObject getter for a MAAS object. From a decoding perspective, a // MAASObject is just like a map except it contains a key "resource_uri", and // it keeps track of the Client you got it from so that you can invoke API // methods directly on their MAAS objects. func (obj JSONObject) GetMAASObject() (MAASObject, error) { attrs, err := obj.GetMap() if err != nil { return MAASObject{}, err } uri, err := extractURI(attrs) if err != nil { return MAASObject{}, err } return MAASObject{values: attrs, client: obj.client, uri: uri}, nil } // GetField extracts a string field from this MAAS object. func (obj MAASObject) GetField(name string) (string, error) { return obj.values[name].GetString() } // URI is the resource URI for this MAAS object. It is an absolute path, but // without a network part. func (obj MAASObject) URI() *url.URL { // Duplicate the URL. uri, err := url.Parse(obj.uri.String()) if err != nil { panic(err) } return uri } // URL returns a full absolute URL (including network part) for this MAAS // object on the API. func (obj MAASObject) URL() *url.URL { return obj.client.GetURL(obj.URI()) } // GetMap returns all of the object's attributes in the form of a map. func (obj MAASObject) GetMap() map[string]JSONObject { return obj.values } // GetSubObject returns a new MAASObject representing the API resource found // at a given sub-path of the current object's resource URI. func (obj MAASObject) GetSubObject(name string) MAASObject { uri := obj.URI() newURL := url.URL{Path: name} resUrl := uri.ResolveReference(&newURL) resUrl.Path = EnsureTrailingSlash(resUrl.Path) input := map[string]interface{}{resourceURI: resUrl.String()} return newJSONMAASObject(input, obj.client) } var NotImplemented = errors.New("Not implemented") // Get retrieves a fresh copy of this MAAS object from the API. func (obj MAASObject) Get() (MAASObject, error) { uri := obj.URI() result, err := obj.client.Get(uri, "", url.Values{}) if err != nil { return MAASObject{}, err } jsonObj, err := Parse(obj.client, result) if err != nil { return MAASObject{}, err } return jsonObj.GetMAASObject() } // Post overwrites this object's existing value on the API with those given // in "params." It returns the object's new value as received from the API. func (obj MAASObject) Post(params url.Values) (JSONObject, error) { uri := obj.URI() result, err := obj.client.Post(uri, "", params, nil) if err != nil { return JSONObject{}, err } return Parse(obj.client, result) } // Update modifies this object on the API, based on the values given in // "params." It returns the object's new value as received from the API. func (obj MAASObject) Update(params url.Values) (MAASObject, error) { uri := obj.URI() result, err := obj.client.Put(uri, params) if err != nil { return MAASObject{}, err } jsonObj, err := Parse(obj.client, result) if err != nil { return MAASObject{}, err } return jsonObj.GetMAASObject() } // Delete removes this object on the API. func (obj MAASObject) Delete() error { uri := obj.URI() return obj.client.Delete(uri) } // CallGet invokes an idempotent API method on this object. func (obj MAASObject) CallGet(operation string, params url.Values) (JSONObject, error) { uri := obj.URI() result, err := obj.client.Get(uri, operation, params) if err != nil { return JSONObject{}, err } return Parse(obj.client, result) } // CallPost invokes a non-idempotent API method on this object. func (obj MAASObject) CallPost(operation string, params url.Values) (JSONObject, error) { return obj.CallPostFiles(operation, params, nil) } // CallPostFiles invokes a non-idempotent API method on this object. It is // similar to CallPost but has an extra parameter, 'files', which should // contain the files that will be uploaded to the API. func (obj MAASObject) CallPostFiles(operation string, params url.Values, files map[string][]byte) (JSONObject, error) { uri := obj.URI() result, err := obj.client.Post(uri, operation, params, files) if err != nil { return JSONObject{}, err } return Parse(obj.client, result) } golang-github-juju-gomaasapi-2.2.0/maasobject_test.go000066400000000000000000000144421451732172100226620ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "fmt" "math/rand" "net/url" . "gopkg.in/check.v1" ) type MAASObjectSuite struct{} var _ = Suite(&MAASObjectSuite{}) func makeFakeResourceURI() string { return "http://example.com/" + fmt.Sprint(rand.Int31()) } // JSONObjects containing MAAS objects convert only to map or to MAASObject. func (suite *MAASObjectSuite) TestConversionsMAASObject(c *C) { input := map[string]interface{}{resourceURI: "someplace"} obj := maasify(Client{}, input) mp, err := obj.GetMap() c.Check(err, IsNil) text, err := mp[resourceURI].GetString() c.Check(err, IsNil) c.Check(text, Equals, "someplace") var maasobj MAASObject maasobj, err = obj.GetMAASObject() c.Assert(err, IsNil) c.Check(maasobj, NotNil) _, err = obj.GetString() c.Check(err, NotNil) _, err = obj.GetFloat64() c.Check(err, NotNil) _, err = obj.GetArray() c.Check(err, NotNil) _, err = obj.GetBool() c.Check(err, NotNil) } func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfNoResourceURI(c *C) { defer func() { recoveredError := recover() c.Check(recoveredError, NotNil) msg := recoveredError.(error).Error() c.Check(msg, Matches, ".*no 'resource_uri' key.*") }() input := map[string]interface{}{"test": "test"} newJSONMAASObject(input, Client{}) } func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfResourceURINotString(c *C) { defer func() { recoveredError := recover() c.Check(recoveredError, NotNil) msg := recoveredError.(error).Error() c.Check(msg, Matches, ".*invalid resource_uri.*") }() input := map[string]interface{}{resourceURI: 77.77} newJSONMAASObject(input, Client{}) } func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfResourceURINotURL(c *C) { defer func() { recoveredError := recover() c.Check(recoveredError, NotNil) msg := recoveredError.(error).Error() c.Check(msg, Matches, ".*resource_uri.*valid URL.*") }() input := map[string]interface{}{resourceURI: "%z"} newJSONMAASObject(input, Client{}) } func (suite *MAASObjectSuite) TestNewJSONMAASObjectSetsUpURI(c *C) { URI, err := url.Parse("http://example.com/a/resource") c.Assert(err, IsNil) attrs := map[string]interface{}{resourceURI: URI.String()} obj := newJSONMAASObject(attrs, Client{}) c.Check(obj.uri, DeepEquals, URI) } func (suite *MAASObjectSuite) TestURL(c *C) { baseURL, err := url.Parse("http://example.com/") c.Assert(err, IsNil) uri := "http://example.com/a/resource" resourceURL, err := url.Parse(uri) c.Assert(err, IsNil) input := map[string]interface{}{resourceURI: uri} client := Client{APIURL: baseURL} obj := newJSONMAASObject(input, client) URL := obj.URL() c.Check(URL, DeepEquals, resourceURL) } // makeFakeMAASObject creates a MAASObject for some imaginary resource. // There is no actual HTTP service or resource attached. // serviceURL is the base URL of the service, and resourceURI is the path for // the object, relative to serviceURL. func makeFakeMAASObject(serviceURL, resourcePath string) MAASObject { baseURL, err := url.Parse(serviceURL) if err != nil { panic(fmt.Errorf("creation of fake object failed: %v", err)) } uri := serviceURL + resourcePath input := map[string]interface{}{resourceURI: uri} client := Client{APIURL: baseURL} return newJSONMAASObject(input, client) } // Passing GetSubObject a relative path effectively concatenates that path to // the original object's resource URI. func (suite *MAASObjectSuite) TestGetSubObjectRelative(c *C) { obj := makeFakeMAASObject("http://example.com/", "a/resource/") subObj := obj.GetSubObject("test") subURL := subObj.URL() // uri ends with a slash and subName starts with one, but the two paths // should be concatenated as "http://example.com/a/resource/test/". expectedSubURL, err := url.Parse("http://example.com/a/resource/test/") c.Assert(err, IsNil) c.Check(subURL, DeepEquals, expectedSubURL) } // Passing GetSubObject an absolute path effectively substitutes that path for // the path component in the original object's resource URI. func (suite *MAASObjectSuite) TestGetSubObjectAbsolute(c *C) { obj := makeFakeMAASObject("http://example.com/", "a/resource/") subObj := obj.GetSubObject("/b/test") subURL := subObj.URL() expectedSubURL, err := url.Parse("http://example.com/b/test/") c.Assert(err, IsNil) c.Check(subURL, DeepEquals, expectedSubURL) } // An absolute path passed to GetSubObject is rooted at the server root, not // at the service root. So every absolute resource URI must repeat the part // of the path that leads to the service root. This does not double that part // of the URI. func (suite *MAASObjectSuite) TestGetSubObjectAbsoluteDoesNotDoubleServiceRoot(c *C) { obj := makeFakeMAASObject("http://example.com/service", "a/resource/") subObj := obj.GetSubObject("/service/test") subURL := subObj.URL() // The "/service" part is not repeated; it must be included. expectedSubURL, err := url.Parse("http://example.com/service/test/") c.Assert(err, IsNil) c.Check(subURL, DeepEquals, expectedSubURL) } // The argument to GetSubObject is a relative path, not a URL. So it won't // take a query part. The special characters that mark a query are escaped // so they are recognized as parts of the path. func (suite *MAASObjectSuite) TestGetSubObjectTakesPathNotURL(c *C) { obj := makeFakeMAASObject("http://example.com/", "x/") subObj := obj.GetSubObject("/y?z") c.Check(subObj.URL().String(), Equals, "http://example.com/y%3Fz/") } func (suite *MAASObjectSuite) TestGetField(c *C) { uri := "http://example.com/a/resource" fieldName := "field name" fieldValue := "a value" input := map[string]interface{}{ resourceURI: uri, fieldName: fieldValue, } obj := newJSONMAASObject(input, Client{}) value, err := obj.GetField(fieldName) c.Check(err, IsNil) c.Check(value, Equals, fieldValue) } func (suite *MAASObjectSuite) TestSerializesToJSON(c *C) { attrs := map[string]interface{}{ resourceURI: "http://maas.example.com/", "counter": 5.0, "active": true, "macs": map[string]interface{}{"eth0": "AA:BB:CC:DD:EE:FF"}, } obj := maasify(Client{}, attrs) output, err := json.Marshal(obj) c.Assert(err, IsNil) var deserialized map[string]interface{} err = json.Unmarshal(output, &deserialized) c.Assert(err, IsNil) c.Check(deserialized, DeepEquals, attrs) } golang-github-juju-gomaasapi-2.2.0/machine.go000066400000000000000000000426431451732172100211230ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/http" "net/url" "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type machine struct { controller *controller resourceURI string systemID string hostname string fqdn string tags []string ownerData map[string]string operatingSystem string distroSeries string architecture string memory int cpuCount int hardwareInfo map[string]string ipAddresses []string powerState string // NOTE: consider some form of status struct statusName string statusMessage string bootInterface *interface_ interfaceSet []*interface_ zone *zone pool *pool // Don't really know the difference between these two lists: physicalBlockDevices []*blockdevice blockDevices []*blockdevice } func (m *machine) updateFrom(other *machine) { m.resourceURI = other.resourceURI m.systemID = other.systemID m.hostname = other.hostname m.fqdn = other.fqdn m.operatingSystem = other.operatingSystem m.distroSeries = other.distroSeries m.architecture = other.architecture m.memory = other.memory m.cpuCount = other.cpuCount m.hardwareInfo = other.hardwareInfo m.ipAddresses = other.ipAddresses m.powerState = other.powerState m.statusName = other.statusName m.statusMessage = other.statusMessage m.zone = other.zone m.pool = other.pool m.tags = other.tags m.ownerData = other.ownerData } // SystemID implements Machine. func (m *machine) SystemID() string { return m.systemID } // Hostname implements Machine. func (m *machine) Hostname() string { return m.hostname } // FQDN implements Machine. func (m *machine) FQDN() string { return m.fqdn } // Tags implements Machine. func (m *machine) Tags() []string { return m.tags } // Pool implements Machine func (m *machine) Pool() Pool { if m.pool == nil { return nil } return m.pool } // IPAddresses implements Machine. func (m *machine) IPAddresses() []string { return m.ipAddresses } // Memory implements Machine. func (m *machine) Memory() int { return m.memory } // CPUCount implements Machine. func (m *machine) CPUCount() int { return m.cpuCount } // HardwareInfo implements Machine. func (m *machine) HardwareInfo() map[string]string { if m.hardwareInfo == nil { return nil } info := make(map[string]string, len(m.hardwareInfo)) for k, v := range m.hardwareInfo { info[k] = v } return info } // PowerState implements Machine. func (m *machine) PowerState() string { return m.powerState } // Zone implements Machine. func (m *machine) Zone() Zone { if m.zone == nil { return nil } return m.zone } // BootInterface implements Machine. func (m *machine) BootInterface() Interface { if m.bootInterface == nil { return nil } m.bootInterface.controller = m.controller return m.bootInterface } // InterfaceSet implements Machine. func (m *machine) InterfaceSet() []Interface { result := make([]Interface, len(m.interfaceSet)) for i, v := range m.interfaceSet { v.controller = m.controller result[i] = v } return result } // Interface implements Machine. func (m *machine) Interface(id int) Interface { for _, iface := range m.interfaceSet { if iface.ID() == id { iface.controller = m.controller return iface } } return nil } // OperatingSystem implements Machine. func (m *machine) OperatingSystem() string { return m.operatingSystem } // DistroSeries implements Machine. func (m *machine) DistroSeries() string { return m.distroSeries } // Architecture implements Machine. func (m *machine) Architecture() string { return m.architecture } // StatusName implements Machine. func (m *machine) StatusName() string { return m.statusName } // StatusMessage implements Machine. func (m *machine) StatusMessage() string { return m.statusMessage } // PhysicalBlockDevices implements Machine. func (m *machine) PhysicalBlockDevices() []BlockDevice { result := make([]BlockDevice, len(m.physicalBlockDevices)) for i, v := range m.physicalBlockDevices { result[i] = v } return result } // PhysicalBlockDevice implements Machine. func (m *machine) PhysicalBlockDevice(id int) BlockDevice { return blockDeviceById(id, m.PhysicalBlockDevices()) } // BlockDevices implements Machine. func (m *machine) BlockDevices() []BlockDevice { result := make([]BlockDevice, len(m.blockDevices)) for i, v := range m.blockDevices { result[i] = v } return result } // BlockDevice implements Machine. func (m *machine) BlockDevice(id int) BlockDevice { return blockDeviceById(id, m.BlockDevices()) } func blockDeviceById(id int, blockDevices []BlockDevice) BlockDevice { for _, blockDevice := range blockDevices { if blockDevice.ID() == id { return blockDevice } } return nil } // Partition implements Machine. func (m *machine) Partition(id int) Partition { return partitionById(id, m.BlockDevices()) } func partitionById(id int, blockDevices []BlockDevice) Partition { for _, blockDevice := range blockDevices { for _, partition := range blockDevice.Partitions() { if partition.ID() == id { return partition } } } return 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 // devices for a particular parent. devices, err := m.controller.Devices(args) if err != nil { return nil, errors.Trace(err) } var result []Device for _, device := range devices { if device.Parent() == m.SystemID() { result = append(result, device) } } return result, nil } // StartArgs is an argument struct for passing parameters to the Machine.Start // method. type StartArgs struct { // UserData needs to be Base64 encoded user data for cloud-init. UserData string DistroSeries string Kernel string Comment string } // Start implements Machine. func (m *machine) Start(args StartArgs) error { params := NewURLParams() params.MaybeAdd("user_data", args.UserData) params.MaybeAdd("distro_series", args.DistroSeries) params.MaybeAdd("hwe_kernel", args.Kernel) params.MaybeAdd("comment", args.Comment) result, err := m.controller.post(m.resourceURI, "deploy", 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 } // 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 // given VLAN. On failure, returns an error satisfying errors.IsNotValid(). type CreateMachineDeviceArgs struct { Hostname string Domain string InterfaceName string MACAddress string Subnet Subnet VLAN VLAN } // Validate ensures that all required values are non-emtpy. func (a *CreateMachineDeviceArgs) Validate() error { if a.InterfaceName == "" { return errors.NotValidf("missing InterfaceName") } if a.MACAddress == "" { return errors.NotValidf("missing MACAddress") } if a.Subnet != nil && a.VLAN != nil && a.Subnet.VLAN() != a.VLAN { msg := fmt.Sprintf( "given subnet %q on VLAN %d does not match given VLAN %d", a.Subnet.CIDR(), a.Subnet.VLAN().ID(), a.VLAN.ID(), ) return errors.NewNotValid(nil, msg) } return nil } // CreateDevice implements Machine func (m *machine) CreateDevice(args CreateMachineDeviceArgs) (_ Device, err error) { if err := args.Validate(); err != nil { return nil, errors.Trace(err) } device, err := m.controller.CreateDevice(CreateDeviceArgs{ Hostname: args.Hostname, Domain: args.Domain, MACAddresses: []string{args.MACAddress}, Parent: m.SystemID(), }) if err != nil { return nil, errors.Trace(err) } defer func(err *error) { // If there is an error return, at least try to delete the device we just created. if *err != nil { if innerErr := device.Delete(); innerErr != nil { logger.Warningf("could not delete device %q", device.SystemID()) } } }(&err) // Update the VLAN to use for the interface, if given. vlanToUse := args.VLAN if vlanToUse == nil && args.Subnet != nil { vlanToUse = args.Subnet.VLAN() } // There should be one interface created for each MAC Address, and since we // only specified one, there should just be one response. interfaces := device.InterfaceSet() if count := len(interfaces); count != 1 { err := errors.Errorf("unexpected interface count for device: %d", count) return nil, NewUnexpectedError(err) } iface := interfaces[0] nameToUse := args.InterfaceName if err := m.updateDeviceInterface(iface, nameToUse, vlanToUse); err != nil { return nil, errors.Trace(err) } if args.Subnet == nil { // Nothing further to update. return device, nil } if err := m.linkDeviceInterfaceToSubnet(iface, args.Subnet); err != nil { return nil, errors.Trace(err) } return device, nil } func (m *machine) updateDeviceInterface(iface Interface, nameToUse string, vlanToUse VLAN) error { updateArgs := UpdateInterfaceArgs{} updateArgs.Name = nameToUse if vlanToUse != nil { updateArgs.VLAN = vlanToUse } if err := iface.Update(updateArgs); err != nil { return errors.Annotatef(err, "updating device interface %q failed", iface.Name()) } return nil } func (m *machine) linkDeviceInterfaceToSubnet(iface Interface, subnetToUse Subnet) error { err := iface.LinkSubnet(LinkSubnetArgs{ Mode: LinkModeStatic, Subnet: subnetToUse, }) if err != nil { return errors.Annotatef( err, "linking device interface %q to subnet %q failed", iface.Name(), subnetToUse.CIDR()) } return nil } // OwnerData implements OwnerDataHolder. func (m *machine) OwnerData() map[string]string { result := make(map[string]string) for key, value := range m.ownerData { result[key] = value } return result } // SetOwnerData implements OwnerDataHolder. func (m *machine) SetOwnerData(ownerData map[string]string) error { params := make(url.Values) for key, value := range ownerData { params.Add(key, value) } result, err := m.controller.post(m.resourceURI, "set_owner_data", params) if err != nil { return errors.Trace(err) } machine, err := readMachine(m.controller.apiVersion, result) if err != nil { return errors.Trace(err) } m.updateFrom(machine) return nil } func readMachine(controllerVersion version.Number, source interface{}) (*machine, error) { readFunc, err := getMachineDeserializationFunc(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 readMachines(controllerVersion version.Number, source interface{}) ([]*machine, error) { readFunc, err := getMachineDeserializationFunc(controllerVersion) if err != nil { return nil, errors.Trace(err) } checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "machine base schema check failed") } valid := coerced.([]interface{}) return readMachineList(valid, readFunc) } func getMachineDeserializationFunc(controllerVersion version.Number) (machineDeserializationFunc, error) { var deserialisationVersion version.Number for v := range machineDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no machine read func for version %s", controllerVersion) } return machineDeserializationFuncs[deserialisationVersion], nil } func readMachineList(sourceList []interface{}, readFunc machineDeserializationFunc) ([]*machine, error) { result := make([]*machine, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for machine %d, %T", i, value) } machine, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "machine %d", i) } result = append(result, machine) } return result, nil } type machineDeserializationFunc func(map[string]interface{}) (*machine, error) var machineDeserializationFuncs = map[version.Number]machineDeserializationFunc{ twoDotOh: machine_2_0, } func machine_2_0(source map[string]interface{}) (*machine, error) { fields := schema.Fields{ "resource_uri": schema.String(), "system_id": schema.String(), "hostname": schema.String(), "fqdn": schema.String(), "tag_names": schema.List(schema.String()), "owner_data": schema.StringMap(schema.String()), "osystem": schema.String(), "distro_series": schema.String(), "architecture": schema.OneOf(schema.Nil(""), schema.String()), "memory": schema.ForceInt(), "cpu_count": schema.ForceInt(), "hardware_info": schema.OneOf(schema.Nil(""), schema.StringMap(schema.String())), "ip_addresses": schema.List(schema.String()), "power_state": schema.String(), "status_name": schema.String(), "status_message": schema.OneOf(schema.Nil(""), schema.String()), "boot_interface": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())), "interface_set": schema.List(schema.StringMap(schema.Any())), "zone": schema.StringMap(schema.Any()), "pool": schema.OneOf(schema.Nil(""), schema.Any()), "physicalblockdevice_set": schema.List(schema.StringMap(schema.Any())), "blockdevice_set": schema.List(schema.StringMap(schema.Any())), } defaults := schema.Defaults{ "architecture": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "machine 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. var bootInterface *interface_ if ifaceMap, ok := valid["boot_interface"].(map[string]interface{}); ok { bootInterface, err = interface_2_0(ifaceMap) if err != nil { return nil, errors.Trace(err) } } interfaceSet, err := readInterfaceList(valid["interface_set"].([]interface{}), interface_2_0) if err != nil { return nil, errors.Trace(err) } zone, err := zone_2_0(valid["zone"].(map[string]interface{})) if err != nil { return nil, errors.Trace(err) } var pool *pool if valid["pool"] != nil { if pool, err = pool_2_0(valid["pool"].(map[string]interface{})); err != nil { return nil, errors.Trace(err) } } physicalBlockDevices, err := readBlockDeviceList(valid["physicalblockdevice_set"].([]interface{}), blockdevice_2_0) if err != nil { return nil, errors.Trace(err) } blockDevices, err := readBlockDeviceList(valid["blockdevice_set"].([]interface{}), blockdevice_2_0) if err != nil { return nil, errors.Trace(err) } var hardwareInfo map[string]string if validHardwareInfo, ok := valid["hardware_info"].(map[string]interface{}); ok { hardwareInfo = make(map[string]string, len(validHardwareInfo)) for key, value := range validHardwareInfo { v, ok := value.(string) if !ok { return nil, fmt.Errorf("invalid field %q in \"hardware_info\"", key) } hardwareInfo[key] = v } } architecture, _ := valid["architecture"].(string) statusMessage, _ := valid["status_message"].(string) result := &machine{ resourceURI: valid["resource_uri"].(string), systemID: valid["system_id"].(string), hostname: valid["hostname"].(string), fqdn: valid["fqdn"].(string), tags: convertToStringSlice(valid["tag_names"]), ownerData: convertToStringMap(valid["owner_data"]), operatingSystem: valid["osystem"].(string), distroSeries: valid["distro_series"].(string), architecture: architecture, memory: valid["memory"].(int), cpuCount: valid["cpu_count"].(int), hardwareInfo: hardwareInfo, ipAddresses: convertToStringSlice(valid["ip_addresses"]), powerState: valid["power_state"].(string), statusName: valid["status_name"].(string), statusMessage: statusMessage, bootInterface: bootInterface, interfaceSet: interfaceSet, zone: zone, pool: pool, physicalBlockDevices: physicalBlockDevices, blockDevices: blockDevices, } return result, nil } func convertToStringSlice(field interface{}) []string { if field == nil { return nil } fieldSlice := field.([]interface{}) result := make([]string, len(fieldSlice)) for i, value := range fieldSlice { result[i] = value.(string) } return result } func convertToStringMap(field interface{}) map[string]string { if field == nil { return nil } // This function is only called after a schema Coerce, so it's // safe. fieldMap := field.(map[string]interface{}) result := make(map[string]string) for key, value := range fieldMap { result[key] = value.(string) } return result } golang-github-juju-gomaasapi-2.2.0/machine_test.go000066400000000000000000001571471451732172100221700ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/http" "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type machineSuite struct { testing.LoggingCleanupSuite } var _ = gc.Suite(&machineSuite{}) func (*machineSuite) TestNilGetters(c *gc.C) { var empty machine c.Check(empty.Zone() == nil, jc.IsTrue) c.Check(empty.PhysicalBlockDevice(0) == nil, jc.IsTrue) c.Check(empty.Interface(0) == nil, jc.IsTrue) c.Check(empty.BootInterface() == nil, jc.IsTrue) } func (*machineSuite) TestReadMachinesBadSchema(c *gc.C) { _, err := readMachines(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `machine base schema check failed: expected list, got string("wat?")`) _, err = readMachines(twoDotOh, []map[string]interface{}{ { "wat": "?", }, }) c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err, gc.ErrorMatches, `machine 0: machine 2.0 schema check failed: .*`) } func (s *machineSuite) TestReadMachines(c *gc.C) { machines, err := readMachines(twoDotOh, parseJSON(c, machinesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 3) machine := machines[0] s.checkMachine(c, machine) hardwareInfo := machine.HardwareInfo() c.Check(hardwareInfo, gc.NotNil) c.Check(hardwareInfo["chassis_serial"], gc.Equals, "#dabeef") } func (s *machineSuite) TestReadMachinesWithoutHardwareInfo(c *gc.C) { machines, err := readMachines(twoDotOh, parseJSON(c, machinesResponseWithoutHardwareInfo)) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 3) machine := machines[0] s.checkMachine(c, machine) hardwareInfo := machine.HardwareInfo() c.Check(hardwareInfo, gc.IsNil) } func (*machineSuite) checkMachine(c *gc.C, machine Machine) { c.Check(machine.SystemID(), gc.Equals, "4y3ha3") c.Check(machine.Hostname(), gc.Equals, "untasted-markita") c.Check(machine.FQDN(), gc.Equals, "untasted-markita.maas") c.Check(machine.Tags(), jc.DeepEquals, []string{"virtual", "magic"}) c.Check(machine.OwnerData(), jc.DeepEquals, map[string]string{ "fez": "phil fish", "frog-fractions": "jim crawford", }) c.Check(machine.IPAddresses(), jc.DeepEquals, []string{"192.168.100.4"}) c.Check(machine.Memory(), gc.Equals, 1024) c.Check(machine.CPUCount(), gc.Equals, 1) c.Check(machine.PowerState(), gc.Equals, "on") c.Check(machine.Zone().Name(), gc.Equals, "default") c.Check(machine.Pool().Name(), gc.Equals, "default") c.Check(machine.OperatingSystem(), gc.Equals, "ubuntu") c.Check(machine.DistroSeries(), gc.Equals, "trusty") c.Check(machine.Architecture(), gc.Equals, "amd64/generic") c.Check(machine.StatusName(), gc.Equals, "Deployed") c.Check(machine.StatusMessage(), gc.Equals, "From 'Deploying' to 'Deployed'") bootInterface := machine.BootInterface() c.Assert(bootInterface, gc.NotNil) c.Check(bootInterface.Name(), gc.Equals, "eth0") interfaceSet := machine.InterfaceSet() c.Assert(interfaceSet, gc.HasLen, 2) id := interfaceSet[0].ID() c.Assert(machine.Interface(id), jc.DeepEquals, interfaceSet[0]) c.Assert(machine.Interface(id+5), gc.IsNil) blockDevices := machine.BlockDevices() c.Assert(blockDevices, gc.HasLen, 3) c.Assert(blockDevices[0].Name(), gc.Equals, "sda") c.Assert(blockDevices[1].Name(), gc.Equals, "sdb") c.Assert(blockDevices[2].Name(), gc.Equals, "md0") blockDevices = machine.PhysicalBlockDevices() c.Assert(blockDevices, gc.HasLen, 2) c.Assert(blockDevices[0].Name(), gc.Equals, "sda") c.Assert(blockDevices[1].Name(), gc.Equals, "sdb") id = blockDevices[0].ID() c.Assert(machine.PhysicalBlockDevice(id), jc.DeepEquals, blockDevices[0]) c.Assert(machine.PhysicalBlockDevice(id+5), gc.IsNil) pool := machine.Pool() c.Check(pool, gc.NotNil) c.Check(pool.Name(), gc.Equals, "default") } func (*machineSuite) TestReadMachinesNilValues(c *gc.C) { json := parseJSON(c, machinesResponse) data := json.([]interface{})[0].(map[string]interface{}) data["architecture"] = nil data["status_message"] = nil data["boot_interface"] = nil data["pool"] = nil data["hardware_info"] = nil machines, err := readMachines(twoDotOh, json) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 3) machine := machines[0] c.Check(machine.Architecture(), gc.Equals, "") c.Check(machine.StatusMessage(), gc.Equals, "") c.Check(machine.BootInterface(), gc.IsNil) c.Check(machine.Pool(), gc.IsNil) c.Check(machine.HardwareInfo(), gc.IsNil) } func (*machineSuite) TestLowVersion(c *gc.C) { _, err := readMachines(version.MustParse("1.9.0"), parseJSON(c, machinesResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) c.Assert(err.Error(), gc.Equals, `no machine read func for version 1.9.0`) } func (*machineSuite) TestHighVersion(c *gc.C) { machines, err := readMachines(version.MustParse("2.1.9"), parseJSON(c, machinesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 3) } func (s *machineSuite) getServerAndMachine(c *gc.C) (*SimpleTestServer, *machine) { server, controller := createTestServerController(c, s) // Just have machines return one machine server.AddGetResponse("/api/2.0/machines/", http.StatusOK, "["+machineResponse+"]") machines, err := controller.Machines(MachinesArgs{}) c.Assert(err, jc.ErrorIsNil) c.Check(machines, gc.HasLen, 1) machine := machines[0].(*machine) server.ResetRequests() return server, machine } func (s *machineSuite) TestStart(c *gc.C) { server, machine := s.getServerAndMachine(c) response := updateJSONMap(c, machineResponse, map[string]interface{}{ "status_name": "Deploying", "status_message": "for testing", }) server.AddPostResponse(machine.resourceURI+"?op=deploy", http.StatusOK, response) err := machine.Start(StartArgs{ UserData: "userdata", DistroSeries: "trusty", Kernel: "kernel", Comment: "a comment", }) c.Assert(err, jc.ErrorIsNil) c.Assert(machine.StatusName(), gc.Equals, "Deploying") c.Assert(machine.StatusMessage(), gc.Equals, "for testing") request := server.LastRequest() // There should be one entry in the form values for each of the args. form := request.PostForm c.Assert(form, gc.HasLen, 4) c.Check(form.Get("user_data"), gc.Equals, "userdata") c.Check(form.Get("distro_series"), gc.Equals, "trusty") c.Check(form.Get("hwe_kernel"), gc.Equals, "kernel") c.Check(form.Get("comment"), gc.Equals, "a comment") } func (s *machineSuite) TestStartMachineNotFound(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=deploy", http.StatusNotFound, "can't find machine") err := machine.Start(StartArgs{}) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "can't find machine") } func (s *machineSuite) TestStartMachineConflict(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=deploy", http.StatusConflict, "machine not allocated") err := machine.Start(StartArgs{}) c.Assert(err, jc.Satisfies, IsBadRequestError) c.Assert(err.Error(), gc.Equals, "machine not allocated") } func (s *machineSuite) TestStartMachineForbidden(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=deploy", http.StatusForbidden, "machine not yours") err := machine.Start(StartArgs{}) c.Assert(err, jc.Satisfies, IsPermissionError) c.Assert(err.Error(), gc.Equals, "machine not yours") } func (s *machineSuite) TestStartMachineServiceUnavailable(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=deploy", http.StatusServiceUnavailable, "no ip addresses available") err := machine.Start(StartArgs{}) c.Assert(err, jc.Satisfies, IsCannotCompleteError) c.Assert(err.Error(), gc.Equals, "no ip addresses available") } func (s *machineSuite) TestStartMachineUnknown(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=deploy", http.StatusMethodNotAllowed, "wat?") err := machine.Start(StartArgs{}) c.Assert(err, jc.Satisfies, IsUnexpectedError) c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 405 Method Not Allowed (wat?)") } func (s *machineSuite) TestDevices(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddGetResponse("/api/2.0/devices/", http.StatusOK, devicesResponse) devices, err := machine.Devices(DevicesArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 1) c.Assert(devices[0].Parent(), gc.Equals, machine.SystemID()) } func (s *machineSuite) TestDevicesNone(c *gc.C) { server, machine := s.getServerAndMachine(c) response := updateJSONMap(c, deviceResponse, map[string]interface{}{ "parent": "other", }) server.AddGetResponse("/api/2.0/devices/", http.StatusOK, "["+response+"]") devices, err := machine.Devices(DevicesArgs{}) c.Assert(err, jc.ErrorIsNil) c.Assert(devices, gc.HasLen, 0) } func (s *machineSuite) TestCreateMachineDeviceArgsValidate(c *gc.C) { for i, test := range []struct { args CreateMachineDeviceArgs errText string }{{ errText: "missing InterfaceName not valid", }, { args: CreateMachineDeviceArgs{ InterfaceName: "eth1", }, errText: `missing MACAddress not valid`, }, { args: CreateMachineDeviceArgs{ InterfaceName: "eth1", MACAddress: "something", Subnet: &fakeSubnet{ cidr: "1.2.3.4/5", vlan: &fakeVLAN{id: 42}, }, VLAN: &fakeVLAN{id: 10}, }, errText: `given subnet "1.2.3.4/5" on VLAN 42 does not match given VLAN 10`, }, { args: CreateMachineDeviceArgs{ Hostname: "is-optional", InterfaceName: "eth1", MACAddress: "something", Subnet: nil, VLAN: &fakeVLAN{}, }, }, { args: CreateMachineDeviceArgs{ InterfaceName: "eth1", MACAddress: "something", Subnet: &fakeSubnet{}, VLAN: nil, }, }, { args: CreateMachineDeviceArgs{ InterfaceName: "eth1", MACAddress: "something", Subnet: nil, VLAN: nil, }, }} { c.Logf("test %d", i) err := test.args.Validate() if test.errText == "" { c.Check(err, jc.ErrorIsNil) } else { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err.Error(), gc.Equals, test.errText) } } } func (s *machineSuite) TestCreateDeviceValidates(c *gc.C) { _, machine := s.getServerAndMachine(c) _, err := machine.CreateDevice(CreateMachineDeviceArgs{}) c.Assert(err, jc.Satisfies, errors.IsNotValid) c.Assert(err.Error(), gc.Equals, "missing InterfaceName not valid") } func (s *machineSuite) TestCreateDevice(c *gc.C) { server, machine := s.getServerAndMachine(c) // The createDeviceResponse returns a single interface with the name "eth0". server.AddPostResponse("/api/2.0/devices/?op=", http.StatusOK, createDeviceResponse) updateInterfaceResponse := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth4", "links": []interface{}{}, "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", }) server.AddPutResponse("/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", http.StatusOK, updateInterfaceResponse) linkSubnetResponse := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth4", "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", }) server.AddPostResponse("/MAAS/api/2.0/nodes/4y3haf/interfaces/48/?op=link_subnet", http.StatusOK, linkSubnetResponse) subnet := machine.BootInterface().Links()[0].Subnet() device, err := machine.CreateDevice(CreateMachineDeviceArgs{ InterfaceName: "eth4", MACAddress: "fake-mac-address", Subnet: subnet, VLAN: subnet.VLAN(), }) c.Assert(err, jc.ErrorIsNil) c.Assert(device.InterfaceSet()[0].Name(), gc.Equals, "eth4") c.Assert(device.InterfaceSet()[0].VLAN().ID(), gc.Equals, subnet.VLAN().ID()) } func (s *machineSuite) TestCreateDeviceWithoutSubnetOrVLAN(c *gc.C) { server, machine := s.getServerAndMachine(c) // The createDeviceResponse returns a single interface with the name "eth0". server.AddPostResponse("/api/2.0/devices/?op=", http.StatusOK, createDeviceResponse) updateInterfaceResponse := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth4", "links": []interface{}{}, "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", }) server.AddPutResponse("/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", http.StatusOK, updateInterfaceResponse) device, err := machine.CreateDevice(CreateMachineDeviceArgs{ InterfaceName: "eth4", MACAddress: "fake-mac-address", Subnet: nil, VLAN: nil, }) c.Assert(err, jc.ErrorIsNil) c.Assert(device.InterfaceSet()[0].Name(), gc.Equals, "eth4") // No specifc subnet or VLAN should be set. c.Assert(device.InterfaceSet()[0].VLAN().ID(), gc.Equals, 1) // set in interfaceResponse c.Assert(device.InterfaceSet()[0].Links(), gc.HasLen, 0) // set above } func (s *machineSuite) TestCreateDeviceWithVLANOnly(c *gc.C) { server, machine := s.getServerAndMachine(c) // The createDeviceResponse returns a single interface with the name "eth0". server.AddPostResponse("/api/2.0/devices/?op=", http.StatusOK, createDeviceResponse) updateInterfaceResponse := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth4", "vlan": map[string]interface{}{ "id": 42, "resource_uri": "/MAAS/api/2.0/vlans/42/", "vid": 1234, "fabric": "live", "dhcp_on": false, "mtu": 9001, }, "links": []interface{}{}, "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", }) server.AddPutResponse("/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", http.StatusOK, updateInterfaceResponse) device, err := machine.CreateDevice(CreateMachineDeviceArgs{ InterfaceName: "eth4", MACAddress: "fake-mac-address", Subnet: nil, VLAN: &fakeVLAN{id: 42}, }) c.Assert(err, jc.ErrorIsNil) c.Assert(device.InterfaceSet()[0].Name(), gc.Equals, "eth4") // VLAN should be set. c.Assert(device.InterfaceSet()[0].VLAN().ID(), gc.Equals, 42) } func (s *machineSuite) TestCreateDeviceTriesToDeleteDeviceOnError(c *gc.C) { server, machine := s.getServerAndMachine(c) // The createDeviceResponse returns a single interface with the name "eth0". server.AddPostResponse("/api/2.0/devices/?op=", http.StatusOK, createDeviceResponse) updateInterfaceResponse := updateJSONMap(c, interfaceResponse, map[string]interface{}{ "name": "eth4", "links": []interface{}{}, "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", }) server.AddPutResponse("/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", http.StatusOK, updateInterfaceResponse) server.AddPostResponse("/MAAS/api/2.0/nodes/4y3haf/interfaces/48/?op=link_subnet", http.StatusServiceUnavailable, "no addresses") // We'll ignore that that it fails to delete, all we care about testing is that it tried. subnet := machine.BootInterface().Links()[0].Subnet() _, err := machine.CreateDevice(CreateMachineDeviceArgs{ InterfaceName: "eth4", MACAddress: "fake-mac-address", Subnet: subnet, }) c.Assert(err, jc.Satisfies, IsCannotCompleteError) request := server.LastRequest() c.Assert(request.Method, gc.Equals, "DELETE") c.Assert(request.RequestURI, gc.Equals, "/MAAS/api/2.0/devices/4y3haf/") } func (s *machineSuite) TestOwnerDataCopies(c *gc.C) { machine := machine{ownerData: make(map[string]string)} ownerData := machine.OwnerData() ownerData["sad"] = "children" c.Assert(machine.OwnerData(), gc.DeepEquals, map[string]string{}) } func (s *machineSuite) TestSetOwnerDataWithHardwareInfo(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=set_owner_data", 200, machineWithOwnerDataWithHardwareInfo(`{"returned": "data"}`)) err := machine.SetOwnerData(map[string]string{ "draco": "malfoy", "empty": "", // Check that empty strings get passed along. }) c.Assert(err, jc.ErrorIsNil) c.Assert(machine.OwnerData(), gc.DeepEquals, map[string]string{"returned": "data"}) form := server.LastRequest().PostForm // Looking at the map directly so we can tell the difference // between no value and an explicit empty string. c.Check(form["draco"], gc.DeepEquals, []string{"malfoy"}) c.Check(form["empty"], gc.DeepEquals, []string{""}) } func (s *machineSuite) TestSetOwnerDataWithoutHardwareInfo(c *gc.C) { server, machine := s.getServerAndMachine(c) server.AddPostResponse(machine.resourceURI+"?op=set_owner_data", 200, machineWithOwnerDataWithoutHardwareInfo(`{"returned": "data"}`)) err := machine.SetOwnerData(map[string]string{ "draco": "malfoy", "empty": "", // Check that empty strings get passed along. }) c.Assert(err, jc.ErrorIsNil) c.Assert(machine.OwnerData(), gc.DeepEquals, map[string]string{"returned": "data"}) form := server.LastRequest().PostForm // Looking at the map directly so we can tell the difference // between no value and an explicit empty string. c.Check(form["draco"], gc.DeepEquals, []string{"malfoy"}) c.Check(form["empty"], gc.DeepEquals, []string{""}) } func machineWithOwnerDataWithHardwareInfo(data string) string { return fmt.Sprintf(machineOwnerDataTemplate, data, hardwareInfo) } func machineWithOwnerDataWithoutHardwareInfo(data string) string { return fmt.Sprintf(machineOwnerDataTemplate, data, "") } const ( hardwareInfo = `, "hardware_info": { "chassis_serial": "#dabeef", "chassis_type": "Unknown", "chassis_vendor": "Unknown", "chassis_version": "Unknown", "cpu_model": "Unknown", "mainboard_firmware_date": "Unknown", "mainboard_firmware_vendor": "Unknown", "mainboard_firmware_version": "Unknown", "mainboard_product": "Unknown", "mainboard_serial": "Unknown", "mainboard_vendor": "Unknown", "mainboard_version": "Unknown", "system_family": "Unknown", "system_product": "Unknown", "system_serial": "Unknown", "system_sku": "Unknown", "system_vendor": "Unknown", "system_version": "Unknown" } ` machineOwnerDataTemplate = ` { "netboot": false, "constraints_by_type": { "storage": { "0": [ 23 ], "1": [ "partition:1" ] } }, "system_id": "4y3ha3", "ip_addresses": [ "192.168.100.4" ], "memory": 1024, "cpu_count": 1, "hwe_kernel": "hwe-t", "status_action": "", "osystem": "ubuntu", "node_type_name": "Machine", "macaddress_set": [ { "mac_address": "52:54:00:55:b6:80" } ], "special_filesystems": [], "status": 6, "virtualblockdevice_set": [ { "block_size": 512, "serial": null, "path": "/dev/disk/by-dname/md0", "system_id": "xc3e6q", "available_size": 256599130112, "size": 256599130112, "uuid": "b76de3fd-d05f-4a3f-b515-189de53d6c03", "tags": [ "raid0" ], "used_size": 0, "name": "md0", "type": "virtual", "filesystem": null, "used_for": "Unused", "partitions": [], "id": 23, "partition_table_type": null, "model": null, "id_path": null, "resource_uri": "/MAAS/api/2.0/nodes/xc3e6q/blockdevices/23/" } ], "physicalblockdevice_set": [ { "path": "/dev/disk/by-dname/sda", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 1, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b752" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/partition/1", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0adf", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/", "id": 34, "serial": "QM00001", "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": "MBR", "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK", "tags": [ "rotary" ] }, { "path": "/dev/disk/by-dname/sdb", "name": "sdb", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 101, "path": "/dev/disk/by-dname/sdb-part1", "filesystem": { "fstype": "ext4", "mount_point": "/home", "label": "home", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b753" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/98/partition/101", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0ae0", "used_for": "ext4 formatted filesystem mounted at /home", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00002", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/98/", "id": 98, "serial": "QM00002", "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": "MBR", "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK", "tags": [ "rotary" ] } ], "interface_set": [ { "effective_mtu": 1500, "mac_address": "52:54:00:55:b6:80", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 35, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/interfaces/35/", "tags": [], "links": [ { "id": 82, "ip_address": "192.168.100.4", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" }, "mode": "auto" } ] }, { "effective_mtu": 1500, "mac_address": "52:54:00:55:b6:81", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 99, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/interfaces/99/", "tags": [], "links": [ { "id": 83, "ip_address": "192.168.100.5", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" }, "mode": "auto" } ] } ], "resource_uri": "/MAAS/api/2.0/machines/4y3ha3/", "hostname": "untasted-markita", "status_name": "Deployed", "min_hwe_kernel": "", "address_ttl": null, "boot_interface": { "effective_mtu": 1500, "mac_address": "52:54:00:55:b6:80", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 35, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/interfaces/35/", "tags": [], "links": [ { "id": 82, "ip_address": "192.168.100.4", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" }, "mode": "auto" } ] }, "power_state": "on", "architecture": "amd64/generic", "power_type": "virsh", "distro_series": "trusty", "tag_names": [ "virtual", "magic" ], "disable_ipv4": false, "status_message": "From 'Deploying' to 'Deployed'", "swap_size": null, "pool": { "name": "default", "description": "machines in the default pool", "id": 3, "resource_uri": "/MAAS/api/2.0/resourcepool/3/" }, "blockdevice_set": [ { "path": "/dev/disk/by-dname/sda", "partition_table_type": "MBR", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 1, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b752" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/partition/1", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0adf", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/", "id": 34, "serial": "QM00001", "block_size": 4096, "type": "physical", "used_size": 8586788864, "tags": [ "rotary" ], "available_size": 0, "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK" }, { "path": "/dev/disk/by-dname/sdb", "name": "sdb", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 101, "path": "/dev/disk/by-dname/sdb-part1", "filesystem": { "fstype": "ext4", "mount_point": "/home", "label": "home", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b753" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/98/partition/101", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0ae0", "used_for": "ext4 formatted filesystem mounted at /home", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00002", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/98/", "id": 98, "serial": "QM00002", "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": "MBR", "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK", "tags": [ "rotary" ] }, { "tags": [ "raid0" ], "used_size": 0, "path": "/dev/disk/by-dname/md0", "serial": null, "available_size": 256599130112, "system_id": "xc3e6q", "uuid": "b76de3fd-d05f-4a3f-b515-189de53d6c03", "block_size": 512, "size": 256599130112, "type": "virtual", "filesystem": null, "used_for": "Unused", "partitions": [], "id": 23, "name": "md0", "partition_table_type": null, "model": null, "id_path": null, "resource_uri": "/MAAS/api/2.0/nodes/xc3e6q/blockdevices/23/" } ], "zone": { "description": "", "resource_uri": "/MAAS/api/2.0/zones/default/", "name": "default" }, "pool": { "description": "", "resource_uri": "/MAAS/api/2.0/pools/default/", "name": "default" }, "fqdn": "untasted-markita.maas", "storage": 8589.934592, "node_type": 0, "boot_disk": null, "owner": "thumper", "domain": { "id": 0, "name": "maas", "resource_uri": "/MAAS/api/2.0/domains/0/", "resource_record_count": 0, "ttl": null, "authoritative": true }, "owner_data": %s%s } ` createDeviceResponse = ` { "zone": { "description": "", "resource_uri": "/MAAS/api/2.0/zones/default/", "name": "default" }, "pool": { "description": "", "resource_uri": "/MAAS/api/2.0/pools/default/", "name": "default" }, "domain": { "resource_record_count": 0, "resource_uri": "/MAAS/api/2.0/domains/0/", "authoritative": true, "name": "maas", "ttl": null, "id": 0 }, "node_type_name": "Device", "address_ttl": null, "hostname": "furnacelike-brittney", "node_type": 1, "resource_uri": "/MAAS/api/2.0/devices/4y3haf/", "ip_addresses": ["192.168.100.11"], "owner": "thumper", "tag_names": [], "fqdn": "furnacelike-brittney.maas", "system_id": "4y3haf", "parent": "4y3ha3", "interface_set": [ { "resource_uri": "/MAAS/api/2.0/nodes/4y3haf/interfaces/48/", "type": "physical", "mac_address": "78:f0:f1:16:a7:46", "params": "", "discovered": null, "effective_mtu": 1500, "id": 48, "children": [], "links": [], "name": "eth0", "vlan": { "secondary_rack": null, "dhcp_on": true, "fabric": "fabric-0", "mtu": 1500, "primary_rack": "4y3h7n", "resource_uri": "/MAAS/api/2.0/vlans/1/", "external_dhcp": null, "name": "untagged", "id": 1, "vid": 0 }, "tags": [], "parents": [], "enabled": true } ] } ` ) var ( machineResponse = machineWithOwnerDataWithHardwareInfo(`{ "fez": "phil fish", "frog-fractions": "jim crawford" } `) machineResponseWithoutHardwareInfo = machineWithOwnerDataWithoutHardwareInfo(`{ "fez": "phil fish", "frog-fractions": "jim crawford" } `) altMachineResponse = `{ "netboot": true, "system_id": "4y3ha4", "ip_addresses": [], "virtualblockdevice_set": [], "memory": 1024, "cpu_count": 1, "hwe_kernel": "", "status_action": "", "osystem": "", "node_type_name": "Machine", "macaddress_set": [ { "mac_address": "52:54:00:33:6b:2c" } ], "special_filesystems": [], "status": 4, "physicalblockdevice_set": [ { "path": "/dev/disk/by-dname/sda", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 2, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "7a0e75a8-0bc6-456b-ac92-4769e97baf02" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha4/blockdevices/35/partition/2", "uuid": "6fe782cf-ad1a-4b31-8beb-333401b4d4bb", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha4/blockdevices/35/", "id": 35, "serial": "QM00001", "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": "MBR", "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK", "tags": [ "rotary" ] } ], "interface_set": [ { "effective_mtu": 1500, "mac_address": "52:54:00:33:6b:2c", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 39, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha4/interfaces/39/", "tags": [], "links": [ { "id": 67, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] } ], "resource_uri": "/MAAS/api/2.0/machines/4y3ha4/", "hostname": "lowlier-glady", "status_name": "Ready", "min_hwe_kernel": "", "address_ttl": null, "boot_interface": { "effective_mtu": 1500, "mac_address": "52:54:00:33:6b:2c", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 39, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha4/interfaces/39/", "tags": [], "links": [ { "id": 67, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] }, "power_state": "off", "architecture": "amd64/generic", "power_type": "virsh", "distro_series": "", "tag_names": [ "virtual" ], "disable_ipv4": false, "status_message": "From 'Commissioning' to 'Ready'", "swap_size": null, "blockdevice_set": [ { "path": "/dev/disk/by-dname/sda", "partition_table_type": "MBR", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 2, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "7a0e75a8-0bc6-456b-ac92-4769e97baf02" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha4/blockdevices/35/partition/2", "uuid": "6fe782cf-ad1a-4b31-8beb-333401b4d4bb", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha4/blockdevices/35/", "id": 35, "serial": "QM00001", "block_size": 4096, "type": "physical", "used_size": 8586788864, "tags": [ "rotary" ], "available_size": 0, "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK" } ], "zone": { "description": "", "resource_uri": "/MAAS/api/2.0/zones/default/", "name": "default" }, "pool": { "description": "", "resource_uri": "/MAAS/api/2.0/pools/default/", "name": "default" }, "fqdn": "lowlier-glady.maas", "hardware_info": { "chassis_serial": "#dabeef", "chassis_type": "Unknown", "chassis_vendor": "Unknown", "chassis_version": "Unknown", "cpu_model": "Unknown", "mainboard_firmware_date": "Unknown", "mainboard_firmware_vendor": "Unknown", "mainboard_firmware_version": "Unknown", "mainboard_product": "Unknown", "mainboard_serial": "Unknown", "mainboard_vendor": "Unknown", "mainboard_version": "Unknown", "system_family": "Unknown", "system_product": "Unknown", "system_serial": "Unknown", "system_sku": "Unknown", "system_vendor": "Unknown", "system_version": "Unknown" }, "storage": 8589.934592, "node_type": 0, "boot_disk": null, "owner": null, "domain": { "id": 0, "name": "maas", "resource_uri": "/MAAS/api/2.0/domains/0/", "resource_record_count": 0, "ttl": null, "authoritative": true }, "owner_data": { "braid": "jonathan blow", "frog-fractions": "jim crawford" } }, { "netboot": true, "system_id": "4y3ha6", "ip_addresses": [], "virtualblockdevice_set": [], "memory": 1024, "cpu_count": 1, "hwe_kernel": "", "status_action": "", "osystem": "", "node_type_name": "Machine", "macaddress_set": [ { "mac_address": "52:54:00:c9:6a:45" } ], "special_filesystems": [], "status": 4, "physicalblockdevice_set": [ { "path": "/dev/disk/by-dname/sda", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 3, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "f15b4e94-7dc3-460d-8838-0c299905c799" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/blockdevices/36/partition/3", "uuid": "a20ae130-bd8f-41b5-bdb3-47ab11a621b5", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/blockdevices/36/", "id": 36, "serial": "QM00001", "type": "physical", "block_size": 4096, "used_size": 8586788864, "available_size": 0, "partition_table_type": "MBR", "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK", "tags": [ "rotary" ] } ], "interface_set": [ { "effective_mtu": 1500, "mac_address": "52:54:00:c9:6a:45", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 40, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/interfaces/40/", "tags": [], "links": [ { "id": 69, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] } ], "resource_uri": "/MAAS/api/2.0/machines/4y3ha6/", "hostname": "icier-nina", "status_name": "Ready", "min_hwe_kernel": "", "address_ttl": null, "boot_interface": { "effective_mtu": 1500, "mac_address": "52:54:00:c9:6a:45", "children": [], "discovered": [], "params": "", "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "name": "eth0", "enabled": true, "parents": [], "id": 40, "type": "physical", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/interfaces/40/", "tags": [], "links": [ { "id": 69, "mode": "auto", "subnet": { "resource_uri": "/MAAS/api/2.0/subnets/1/", "id": 1, "rdns_mode": 2, "vlan": { "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "mtu": 1500, "primary_rack": "4y3h7n", "name": "untagged", "fabric": "fabric-0", "dhcp_on": true, "vid": 0 }, "dns_servers": [], "space": "space-0", "name": "192.168.100.0/24", "gateway_ip": "192.168.100.1", "cidr": "192.168.100.0/24" } } ] }, "power_state": "off", "architecture": "amd64/generic", "power_type": "virsh", "distro_series": "", "tag_names": [ "virtual" ], "disable_ipv4": false, "status_message": "From 'Commissioning' to 'Ready'", "swap_size": null, "blockdevice_set": [ { "path": "/dev/disk/by-dname/sda", "partition_table_type": "MBR", "name": "sda", "used_for": "MBR partitioned with 1 partition", "partitions": [ { "bootable": false, "id": 3, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "f15b4e94-7dc3-460d-8838-0c299905c799" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/blockdevices/36/partition/3", "uuid": "a20ae130-bd8f-41b5-bdb3-47ab11a621b5", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984 } ], "filesystem": null, "id_path": "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha6/blockdevices/36/", "id": 36, "serial": "QM00001", "block_size": 4096, "type": "physical", "used_size": 8586788864, "tags": [ "rotary" ], "available_size": 0, "uuid": null, "size": 8589934592, "model": "QEMU HARDDISK" } ], "zone": { "description": "", "resource_uri": "/MAAS/api/2.0/zones/default/", "name": "default" }, "pool": { "description": "", "resource_uri": "/MAAS/api/2.0/pools/default/", "name": "default" }, "fqdn": "icier-nina.maas", "hardware_info": { "chassis_serial": "Unknown", "chassis_type": "Unknown", "chassis_vendor": "Unknown", "chassis_version": "Unknown", "cpu_model": "Unknown", "mainboard_firmware_date": "Unknown", "mainboard_firmware_vendor": "Unknown", "mainboard_firmware_version": "Unknown", "mainboard_product": "Unknown", "mainboard_serial": "Unknown", "mainboard_vendor": "Unknown", "mainboard_version": "Unknown", "system_family": "Unknown", "system_product": "Unknown", "system_serial": "Unknown", "system_sku": "Unknown", "system_vendor": "Unknown", "system_version": "Unknown" }, "storage": 8589.934592, "node_type": 0, "boot_disk": null, "owner": null, "domain": { "id": 0, "name": "maas", "resource_uri": "/MAAS/api/2.0/domains/0/", "resource_record_count": 0, "ttl": null, "authoritative": true }, "owner_data": { "braid": "jonathan blow", "fez": "phil fish" } } ` machinesResponse = "[" + machineResponse + ", " + altMachineResponse + "]" machinesResponseWithoutHardwareInfo = "[" + machineResponseWithoutHardwareInfo + ", " + altMachineResponse + "]" ) golang-github-juju-gomaasapi-2.2.0/oauth.go000066400000000000000000000040361451732172100206310ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "crypto/rand" "fmt" "net/http" "net/url" "strconv" "strings" "time" ) // Not a true uuidgen, but at least creates same length random func generateNonce() (string, error) { randBytes := make([]byte, 16) _, err := rand.Read(randBytes) if err != nil { return "", err } return fmt.Sprintf("%16x", randBytes), nil } func generateTimestamp() string { return strconv.Itoa(int(time.Now().Unix())) } type OAuthSigner interface { OAuthSign(request *http.Request) error } type OAuthToken struct { ConsumerKey string ConsumerSecret string TokenKey string TokenSecret string } // Trick to ensure *plainTextOAuthSigner implements the OAuthSigner interface. var _ OAuthSigner = (*plainTextOAuthSigner)(nil) type plainTextOAuthSigner struct { token *OAuthToken realm string } func NewPlainTestOAuthSigner(token *OAuthToken, realm string) (OAuthSigner, error) { return &plainTextOAuthSigner{token, realm}, nil } // 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 { signature := signer.token.ConsumerSecret + `&` + signer.token.TokenSecret nonce, err := generateNonce() if err != nil { return err } authData := map[string]string{ "realm": signer.realm, "oauth_consumer_key": signer.token.ConsumerKey, "oauth_token": signer.token.TokenKey, "oauth_signature_method": "PLAINTEXT", "oauth_signature": signature, "oauth_timestamp": generateTimestamp(), "oauth_nonce": nonce, "oauth_version": "1.0", } // Build OAuth header. var authHeader []string for key, value := range authData { authHeader = append(authHeader, fmt.Sprintf(`%s="%s"`, key, url.QueryEscape(value))) } strHeader := "OAuth " + strings.Join(authHeader, ", ") request.Header.Set("Authorization", strHeader) return nil } golang-github-juju-gomaasapi-2.2.0/partition.go000066400000000000000000000100621451732172100215160ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type partition struct { resourceURI string id int path string uuid string usedFor string size uint64 tags []string filesystem *filesystem } // Type implements Partition. func (p *partition) Type() string { return "partition" } // ID implements Partition. func (p *partition) ID() int { return p.id } // Path implements Partition. func (p *partition) Path() string { return p.path } // FileSystem implements Partition. func (p *partition) FileSystem() FileSystem { if p.filesystem == nil { return nil } return p.filesystem } // UUID implements Partition. func (p *partition) UUID() string { return p.uuid } // UsedFor implements Partition. func (p *partition) UsedFor() string { return p.usedFor } // Size implements Partition. func (p *partition) Size() uint64 { return p.size } // Tags implements Partition. func (p *partition) Tags() []string { return p.tags } 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{}) var deserialisationVersion version.Number for v := range partitionDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, NewUnsupportedVersionError("no partition read func for version %s", controllerVersion) } readFunc := partitionDeserializationFuncs[deserialisationVersion] return readPartitionList(valid, readFunc) } // readPartitionList expects the values of the sourceList to be string maps. func readPartitionList(sourceList []interface{}, readFunc partitionDeserializationFunc) ([]*partition, error) { result := make([]*partition, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, NewDeserializationError("unexpected value for partition %d, %T", i, value) } partition, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "partition %d", i) } result = append(result, partition) } return result, nil } type partitionDeserializationFunc func(map[string]interface{}) (*partition, error) var partitionDeserializationFuncs = map[version.Number]partitionDeserializationFunc{ twoDotOh: partition_2_0, } func partition_2_0(source map[string]interface{}) (*partition, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "path": schema.String(), "uuid": schema.OneOf(schema.Nil(""), schema.String()), "used_for": schema.String(), "size": schema.ForceUint(), "tags": schema.List(schema.String()), "filesystem": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())), } defaults := schema.Defaults{ "tags": []string{}, } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "partition 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. var filesystem *filesystem if fsSource, ok := valid["filesystem"].(map[string]interface{}); ok { if filesystem, err = filesystem2_0(fsSource); err != nil { return nil, errors.Trace(err) } } uuid, _ := valid["uuid"].(string) result := &partition{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), path: valid["path"].(string), uuid: uuid, usedFor: valid["used_for"].(string), size: valid["size"].(uint64), tags: convertToStringSlice(valid["tags"]), filesystem: filesystem, } return result, nil } golang-github-juju-gomaasapi-2.2.0/partition_test.go000066400000000000000000000057331451732172100225660ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type partitionSuite struct{} var _ = gc.Suite(&partitionSuite{}) func (*partitionSuite) TestTypePartition(c *gc.C) { var empty partition c.Assert(empty.Type() == "partition", jc.IsTrue) } func (*partitionSuite) TestNilFileSystem(c *gc.C) { var empty partition c.Assert(empty.FileSystem() == nil, jc.IsTrue) } func (*partitionSuite) TestReadPartitionsBadSchema(c *gc.C) { _, err := readPartitions(twoDotOh, "wat?") c.Check(err, jc.Satisfies, IsDeserializationError) c.Assert(err.Error(), gc.Equals, `partition base schema check failed: expected list, got string("wat?")`) } func (*partitionSuite) TestReadPartitions(c *gc.C) { partitions, err := readPartitions(twoDotOh, parseJSON(c, partitionsResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(partitions, gc.HasLen, 1) partition := partitions[0] c.Check(partition.Type(), gc.Equals, "partition") c.Check(partition.ID(), gc.Equals, 1) c.Check(partition.Path(), gc.Equals, "/dev/disk/by-dname/sda-part1") c.Check(partition.UUID(), gc.Equals, "6199b7c9-b66f-40f6-a238-a938a58a0adf") c.Check(partition.UsedFor(), gc.Equals, "ext4 formatted filesystem mounted at /") c.Check(partition.Size(), gc.Equals, uint64(8581545984)) c.Check(partition.Tags(), gc.DeepEquals, []string{"ssd-part", "osd-part"}) fs := partition.FileSystem() c.Assert(fs, gc.NotNil) c.Assert(fs.Type(), gc.Equals, "ext4") c.Assert(fs.MountPoint(), gc.Equals, "/") } func (*partitionSuite) TestReadPartitionsNilUUID(c *gc.C) { json := parseJSON(c, partitionsResponse) json.([]interface{})[0].(map[string]interface{})["uuid"] = nil partitions, err := readPartitions(twoDotOh, json) c.Assert(err, jc.ErrorIsNil) c.Assert(partitions, gc.HasLen, 1) partition := partitions[0] c.Check(partition.UUID(), gc.Equals, "") } func (*partitionSuite) TestLowVersion(c *gc.C) { _, err := readPartitions(version.MustParse("1.9.0"), parseJSON(c, partitionsResponse)) c.Assert(err, jc.Satisfies, IsUnsupportedVersionError) } func (*partitionSuite) TestHighVersion(c *gc.C) { partitions, err := readPartitions(version.MustParse("2.1.9"), parseJSON(c, partitionsResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(partitions, gc.HasLen, 1) } var partitionsResponse = ` [ { "bootable": false, "id": 1, "path": "/dev/disk/by-dname/sda-part1", "filesystem": { "fstype": "ext4", "mount_point": "/", "label": "root", "mount_options": null, "uuid": "fcd7745e-f1b5-4f5d-9575-9b0bb796b752" }, "type": "partition", "resource_uri": "/MAAS/api/2.0/nodes/4y3ha3/blockdevices/34/partition/1", "uuid": "6199b7c9-b66f-40f6-a238-a938a58a0adf", "used_for": "ext4 formatted filesystem mounted at /", "size": 8581545984, "tags": ["ssd-part", "osd-part"] } ] ` golang-github-juju-gomaasapi-2.2.0/pool.go000066400000000000000000000052711451732172100204640ustar00rootroot00000000000000// Copyright 2019 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type pool struct { // Add the controller in when we need to do things with the pool. // controller Controller resourceURI string name string description string } // Name implements Pool. func (p *pool) Name() string { return p.name } // Description implements Pool. func (p *pool) Description() string { return p.description } func readPools(controllerVersion version.Number, source interface{}) ([]*pool, error) { var deserialisationVersion version.Number checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "pool base schema check failed") } valid := coerced.([]interface{}) for v := range poolDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no pool read func for version %s", controllerVersion) } readFunc := poolDeserializationFuncs[deserialisationVersion] return readPoolList(valid, readFunc) } // readPoolList expects the values of the sourceList to be string maps. func readPoolList(sourceList []interface{}, readFunc poolDeserializationFunc) ([]*pool, error) { result := make([]*pool, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for pool %d, %T", i, value) } pool, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "pool %d", i) } result = append(result, pool) } return result, nil } type poolDeserializationFunc func(map[string]interface{}) (*pool, error) var poolDeserializationFuncs = map[version.Number]poolDeserializationFunc{ twoDotOh: pool_2_0, } func pool_2_0(source map[string]interface{}) (*pool, error) { fields := schema.Fields{ "name": schema.String(), "description": schema.String(), "resource_uri": schema.String(), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "pool 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 := &pool{ name: valid["name"].(string), description: valid["description"].(string), resourceURI: valid["resource_uri"].(string), } return result, nil } golang-github-juju-gomaasapi-2.2.0/pool_test.go000066400000000000000000000032201451732172100215130ustar00rootroot00000000000000// Copyright 2019 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type poolSuite struct{} var _ = gc.Suite(&poolSuite{}) func (*poolSuite) TestReadPoolsBadSchema(c *gc.C) { _, err := readPools(twoDotOh, "blahfoob") c.Assert(err.Error(), gc.Equals, `pool base schema check failed: expected list, got string("blahfoob")`) } func (*poolSuite) TestReadPools(c *gc.C) { pools, err := readPools(twoDotOh, parseJSON(c, poolResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(pools, gc.HasLen, 2) c.Assert(pools[0].Name(), gc.Equals, "default") c.Assert(pools[0].Description(), gc.Equals, "default description") c.Assert(pools[1].Name(), gc.Equals, "swimming_is_fun") c.Assert(pools[1].Description(), gc.Equals, "swimming is fun description") } // Pools were not introduced until 2.5.x func (*poolSuite) TestLowVersion(c *gc.C) { _, err := readPools(version.MustParse("1.9.0"), parseJSON(c, poolResponse)) c.Assert(err.Error(), gc.Equals, `no pool read func for version 1.9.0`) } // MaaS 2.6.x is GA now. func (*poolSuite) TestHighVersion(c *gc.C) { pools, err := readPools(version.MustParse("2.1.9"), parseJSON(c, poolResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(pools, gc.HasLen, 2) } var poolResponse = ` [ { "description": "default description", "resource_uri": "/MAAS/api/2.0/pools/default/", "name": "default" }, { "description": "swimming is fun description", "resource_uri": "/MAAS/api/2.0/pools/swimming_is_fun/", "name": "swimming_is_fun" } ] ` golang-github-juju-gomaasapi-2.2.0/space.go000066400000000000000000000060671451732172100206120ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type space struct { // Add the controller in when we need to do things with the space. // controller Controller resourceURI string id int name string subnets []*subnet } // Id implements Space. func (s *space) ID() int { return s.id } // Name implements Space. func (s *space) Name() string { return s.name } // Subnets implements Space. func (s *space) Subnets() []Subnet { var result []Subnet for _, subnet := range s.subnets { result = append(result, subnet) } return result } func readSpaces(controllerVersion version.Number, source interface{}) ([]*space, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "space base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range spaceDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no space read func for version %s", controllerVersion) } readFunc := spaceDeserializationFuncs[deserialisationVersion] return readSpaceList(valid, readFunc) } // readSpaceList expects the values of the sourceList to be string maps. func readSpaceList(sourceList []interface{}, readFunc spaceDeserializationFunc) ([]*space, error) { result := make([]*space, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for space %d, %T", i, value) } space, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "space %d", i) } result = append(result, space) } return result, nil } type spaceDeserializationFunc func(map[string]interface{}) (*space, error) var spaceDeserializationFuncs = map[version.Number]spaceDeserializationFunc{ twoDotOh: space_2_0, } func space_2_0(source map[string]interface{}) (*space, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "name": schema.String(), "subnets": schema.List(schema.StringMap(schema.Any())), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "space 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. subnets, err := readSubnetList(valid["subnets"].([]interface{}), subnet_2_0) if err != nil { return nil, errors.Trace(err) } result := &space{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), name: valid["name"].(string), subnets: subnets, } return result, nil } golang-github-juju-gomaasapi-2.2.0/space_test.go000066400000000000000000000055631451732172100216510ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type spaceSuite struct{} var _ = gc.Suite(&spaceSuite{}) func (*spaceSuite) TestReadSpacesBadSchema(c *gc.C) { _, err := readSpaces(twoDotOh, "wat?") c.Assert(err.Error(), gc.Equals, `space base schema check failed: expected list, got string("wat?")`) } func (*spaceSuite) TestReadSpaces(c *gc.C) { spaces, err := readSpaces(twoDotOh, parseJSON(c, spacesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(spaces, gc.HasLen, 1) space := spaces[0] c.Assert(space.ID(), gc.Equals, 0) c.Assert(space.Name(), gc.Equals, "space-0") subnets := space.Subnets() c.Assert(subnets, gc.HasLen, 2) c.Assert(subnets[0].ID(), gc.Equals, 34) } func (*spaceSuite) TestLowVersion(c *gc.C) { _, err := readSpaces(version.MustParse("1.9.0"), parseJSON(c, spacesResponse)) c.Assert(err.Error(), gc.Equals, `no space read func for version 1.9.0`) } func (*spaceSuite) TestHighVersion(c *gc.C) { spaces, err := readSpaces(version.MustParse("2.1.9"), parseJSON(c, spacesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(spaces, gc.HasLen, 1) } var spacesResponse = ` [ { "subnets": [ { "gateway_ip": null, "name": "192.168.122.0/24", "vlan": { "fabric": "fabric-1", "resource_uri": "/MAAS/api/2.0/vlans/5001/", "name": "untagged", "secondary_rack": null, "primary_rack": null, "vid": 0, "dhcp_on": false, "id": 5001, "mtu": 1500 }, "space": "space-0", "id": 34, "resource_uri": "/MAAS/api/2.0/subnets/34/", "dns_servers": [], "cidr": "192.168.122.0/24", "rdns_mode": 2 }, { "gateway_ip": "192.168.100.1", "name": "192.168.100.0/24", "vlan": { "fabric": "fabric-0", "resource_uri": "/MAAS/api/2.0/vlans/1/", "name": "untagged", "secondary_rack": null, "primary_rack": "4y3h7n", "vid": 0, "dhcp_on": true, "id": 1, "mtu": 1500 }, "space": "space-0", "id": 1, "resource_uri": "/MAAS/api/2.0/subnets/1/", "dns_servers": [], "cidr": "192.168.100.0/24", "rdns_mode": 2 } ], "id": 0, "name": "space-0", "resource_uri": "/MAAS/api/2.0/spaces/0/" } ] ` golang-github-juju-gomaasapi-2.2.0/staticroute.go000066400000000000000000000075221451732172100220620ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type staticRoute struct { resourceURI string id int source *subnet destination *subnet gatewayIP string metric int } // Id implements StaticRoute. func (s *staticRoute) ID() int { return s.id } // Source implements StaticRoute. func (s *staticRoute) Source() Subnet { return s.source } // Destination implements StaticRoute. func (s *staticRoute) Destination() Subnet { return s.destination } // GatewayIP implements StaticRoute. func (s *staticRoute) GatewayIP() string { return s.gatewayIP } // Metric implements StaticRoute. func (s *staticRoute) Metric() int { return s.metric } func readStaticRoutes(controllerVersion version.Number, source interface{}) ([]*staticRoute, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "static-route base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range staticRouteDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no static-route read func for version %s", controllerVersion) } readFunc := staticRouteDeserializationFuncs[deserialisationVersion] return readStaticRouteList(valid, readFunc) } // readStaticRouteList expects the values of the sourceList to be string maps. func readStaticRouteList(sourceList []interface{}, readFunc staticRouteDeserializationFunc) ([]*staticRoute, error) { result := make([]*staticRoute, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for static-route %d, %T", i, value) } staticRoute, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "static-route %d", i) } result = append(result, staticRoute) } return result, nil } type staticRouteDeserializationFunc func(map[string]interface{}) (*staticRoute, error) var staticRouteDeserializationFuncs = map[version.Number]staticRouteDeserializationFunc{ twoDotOh: staticRoute_2_0, } func staticRoute_2_0(source map[string]interface{}) (*staticRoute, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "source": schema.StringMap(schema.Any()), "destination": schema.StringMap(schema.Any()), "gateway_ip": schema.String(), "metric": schema.ForceInt(), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "static-route 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. // readSubnetList takes a list of interfaces. We happen to have 2 subnets // to parse, that are in different keys, but we might as well wrap them up // together and pass them in. subnets, err := readSubnetList([]interface{}{valid["source"], valid["destination"]}, subnet_2_0) if err != nil { return nil, errors.Trace(err) } if len(subnets) != 2 { // how could we get here? return nil, errors.Errorf("subnets somehow parsed into the wrong number of items (expected 2): %d", len(subnets)) } result := &staticRoute{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), gatewayIP: valid["gateway_ip"].(string), metric: valid["metric"].(int), source: subnets[0], destination: subnets[1], } return result, nil } golang-github-juju-gomaasapi-2.2.0/staticroute_test.go000066400000000000000000000070041451732172100231140ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type staticRouteSuite struct{} var _ = gc.Suite(&staticRouteSuite{}) func (*staticRouteSuite) TestReadStaticRoutesBadSchema(c *gc.C) { _, err := readStaticRoutes(twoDotOh, "wat?") c.Assert(err.Error(), gc.Equals, `static-route base schema check failed: expected list, got string("wat?")`) } func (*staticRouteSuite) TestReadStaticRoutes(c *gc.C) { staticRoutes, err := readStaticRoutes(twoDotOh, parseJSON(c, staticRoutesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(staticRoutes, gc.HasLen, 1) staticRoute := staticRoutes[0] c.Assert(staticRoute.ID(), gc.Equals, 2) c.Assert(staticRoute.Metric(), gc.Equals, int(0)) c.Assert(staticRoute.GatewayIP(), gc.Equals, "192.168.0.1") source := staticRoute.Source() c.Assert(source, gc.NotNil) c.Assert(source.Name(), gc.Equals, "192.168.0.0/24") c.Assert(source.CIDR(), gc.Equals, "192.168.0.0/24") destination := staticRoute.Destination() c.Assert(destination, gc.NotNil) c.Assert(destination.Name(), gc.Equals, "Local-192") c.Assert(destination.CIDR(), gc.Equals, "192.168.0.0/16") } func (*staticRouteSuite) TestLowVersion(c *gc.C) { _, err := readStaticRoutes(version.MustParse("1.9.0"), parseJSON(c, staticRoutesResponse)) c.Assert(err.Error(), gc.Equals, `no static-route read func for version 1.9.0`) } func (*staticRouteSuite) TestHighVersion(c *gc.C) { staticRoutes, err := readStaticRoutes(version.MustParse("2.1.9"), parseJSON(c, staticRoutesResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(staticRoutes, gc.HasLen, 1) } var staticRoutesResponse = ` [ { "destination": { "active_discovery": false, "id": 3, "resource_uri": "/MAAS/api/2.0/subnets/3/", "allow_proxy": true, "rdns_mode": 2, "dns_servers": [ "8.8.8.8" ], "name": "Local-192", "cidr": "192.168.0.0/16", "space": "space-0", "vlan": { "fabric": "fabric-1", "id": 5002, "dhcp_on": false, "primary_rack": null, "resource_uri": "/MAAS/api/2.0/vlans/5002/", "mtu": 1500, "fabric_id": 1, "secondary_rack": null, "name": "untagged", "external_dhcp": null, "vid": 0 }, "gateway_ip": "192.168.0.1" }, "source": { "active_discovery": false, "id": 1, "resource_uri": "/MAAS/api/2.0/subnets/1/", "allow_proxy": true, "rdns_mode": 2, "dns_servers": [], "name": "192.168.0.0/24", "cidr": "192.168.0.0/24", "space": "space-0", "vlan": { "fabric": "fabric-0", "id": 5001, "dhcp_on": false, "primary_rack": null, "resource_uri": "/MAAS/api/2.0/vlans/5001/", "mtu": 1500, "fabric_id": 0, "secondary_rack": null, "name": "untagged", "external_dhcp": "192.168.0.1", "vid": 0 }, "gateway_ip": null }, "id": 2, "resource_uri": "/MAAS/api/2.0/static-routes/2/", "metric": 0, "gateway_ip": "192.168.0.1" } ] ` golang-github-juju-gomaasapi-2.2.0/subnet.go000066400000000000000000000077371451732172100210240ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type subnet struct { // Add the controller in when we need to do things with the subnet. // controller Controller resourceURI string id int name string space string vlan *vlan gateway string cidr string dnsServers []string } // ID implements Subnet. func (s *subnet) ID() int { return s.id } // Name implements Subnet. func (s *subnet) Name() string { return s.name } // Space implements Subnet. func (s *subnet) Space() string { return s.space } // VLAN implements Subnet. func (s *subnet) VLAN() VLAN { if s.vlan == nil { return nil } return s.vlan } // Gateway implements Subnet. func (s *subnet) Gateway() string { return s.gateway } // CIDR implements Subnet. func (s *subnet) CIDR() string { return s.cidr } // DNSServers implements Subnet. func (s *subnet) DNSServers() []string { return s.dnsServers } func readSubnets(controllerVersion version.Number, source interface{}) ([]*subnet, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "subnet base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range subnetDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no subnet read func for version %s", controllerVersion) } readFunc := subnetDeserializationFuncs[deserialisationVersion] return readSubnetList(valid, readFunc) } // readSubnetList expects the values of the sourceList to be string maps. func readSubnetList(sourceList []interface{}, readFunc subnetDeserializationFunc) ([]*subnet, error) { result := make([]*subnet, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for subnet %d, %T", i, value) } subnet, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "subnet %d", i) } result = append(result, subnet) } return result, nil } type subnetDeserializationFunc func(map[string]interface{}) (*subnet, error) var subnetDeserializationFuncs = map[version.Number]subnetDeserializationFunc{ twoDotOh: subnet_2_0, } func subnet_2_0(source map[string]interface{}) (*subnet, error) { fields := schema.Fields{ "resource_uri": schema.String(), "id": schema.ForceInt(), "name": schema.String(), "space": schema.String(), "gateway_ip": schema.OneOf(schema.Nil(""), schema.String()), "cidr": schema.String(), "vlan": schema.StringMap(schema.Any()), "dns_servers": schema.OneOf(schema.Nil(""), schema.List(schema.String())), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "subnet 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. vlan, err := vlan_2_0(valid["vlan"].(map[string]interface{})) if err != nil { return nil, errors.Trace(err) } // Since the gateway_ip is optional, we use the two part cast assignment. If // the cast fails, then we get the default value we care about, which is the // empty string. gateway, _ := valid["gateway_ip"].(string) result := &subnet{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), name: valid["name"].(string), space: valid["space"].(string), vlan: vlan, gateway: gateway, cidr: valid["cidr"].(string), dnsServers: convertToStringSlice(valid["dns_servers"]), } return result, nil } golang-github-juju-gomaasapi-2.2.0/subnet_test.go000066400000000000000000000054361451732172100220550ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type subnetSuite struct{} var _ = gc.Suite(&subnetSuite{}) func (*subnetSuite) TestNilVLAN(c *gc.C) { var empty subnet c.Check(empty.VLAN() == nil, jc.IsTrue) } func (*subnetSuite) TestReadSubnetsBadSchema(c *gc.C) { _, err := readSubnets(twoDotOh, "wat?") c.Assert(err.Error(), gc.Equals, `subnet base schema check failed: expected list, got string("wat?")`) } func (*subnetSuite) TestReadSubnets(c *gc.C) { subnets, err := readSubnets(twoDotOh, parseJSON(c, subnetResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(subnets, gc.HasLen, 2) subnet := subnets[0] c.Assert(subnet.ID(), gc.Equals, 1) c.Assert(subnet.Name(), gc.Equals, "192.168.100.0/24") c.Assert(subnet.Space(), gc.Equals, "space-0") c.Assert(subnet.Gateway(), gc.Equals, "192.168.100.1") c.Assert(subnet.CIDR(), gc.Equals, "192.168.100.0/24") vlan := subnet.VLAN() c.Assert(vlan, gc.NotNil) c.Assert(vlan.Name(), gc.Equals, "untagged") c.Assert(subnet.DNSServers(), jc.DeepEquals, []string{"8.8.8.8", "8.8.4.4"}) } func (*subnetSuite) TestLowVersion(c *gc.C) { _, err := readSubnets(version.MustParse("1.9.0"), parseJSON(c, subnetResponse)) c.Assert(err.Error(), gc.Equals, `no subnet read func for version 1.9.0`) } func (*subnetSuite) TestHighVersion(c *gc.C) { subnets, err := readSubnets(version.MustParse("2.1.9"), parseJSON(c, subnetResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(subnets, gc.HasLen, 2) } var subnetResponse = ` [ { "gateway_ip": "192.168.100.1", "name": "192.168.100.0/24", "vlan": { "fabric": "fabric-0", "resource_uri": "/MAAS/api/2.0/vlans/1/", "name": "untagged", "secondary_rack": null, "primary_rack": "4y3h7n", "vid": 0, "dhcp_on": true, "id": 1, "mtu": 1500 }, "space": "space-0", "id": 1, "resource_uri": "/MAAS/api/2.0/subnets/1/", "dns_servers": ["8.8.8.8", "8.8.4.4"], "cidr": "192.168.100.0/24", "rdns_mode": 2 }, { "gateway_ip": null, "name": "192.168.122.0/24", "vlan": { "fabric": "fabric-1", "resource_uri": "/MAAS/api/2.0/vlans/5001/", "name": "untagged", "secondary_rack": null, "primary_rack": null, "vid": 0, "dhcp_on": false, "id": 5001, "mtu": 1500 }, "space": "space-0", "id": 34, "resource_uri": "/MAAS/api/2.0/subnets/34/", "dns_servers": null, "cidr": "192.168.122.0/24", "rdns_mode": 2 } ] ` golang-github-juju-gomaasapi-2.2.0/tags.go000066400000000000000000000060611451732172100204470ustar00rootroot00000000000000// Copyright 2022 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type tag struct { resourceURI string name string comment string definition string kernelOpts string } func (tag tag) Name() string { return tag.name } func (tag tag) Comment() string { return tag.comment } func (tag tag) Definition() string { return tag.definition } func (tag tag) KernelOpts() string { return tag.kernelOpts } func readTags(controllerVersion version.Number, source interface{}) ([]*tag, error) { readFunc, err := getTagDeserializationFunc(controllerVersion) if err != nil { return nil, errors.Trace(err) } checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "machine base schema check failed") } // OK to do a direct cast here because we just coerced the interface. valid := coerced.([]interface{}) return readTagList(valid, readFunc) } 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, NewDeserializationError("unexpected value for tag %d, %T", i, value) } machine, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "tag %d", i) } result = append(result, machine) } return result, nil } func getTagDeserializationFunc(controllerVersion version.Number) (tagDeserializationFunc, 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, NewUnsupportedVersionError("no tag read func for version %s", controllerVersion) } return tagDeserializationFuncs[deserialisationVersion], 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(), "comment": schema.String(), "definition": schema.String(), "kernel_opts": schema.String(), } defaults := schema.Defaults{ "comment": "", "definition": "", "kernel_opts": "", } checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, WrapWithDeserializationError(err, "tag 2.0 schema check failed") } valid := coerced.(map[string]interface{}) return &tag{ resourceURI: valid["resource_uri"].(string), name: valid["name"].(string), comment: valid["comment"].(string), definition: valid["definition"].(string), kernelOpts: valid["kernel_opts"].(string), }, nil } golang-github-juju-gomaasapi-2.2.0/tags_test.go000066400000000000000000000022201451732172100214770ustar00rootroot00000000000000package gomaasapi import ( "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) type tagSuite struct { testing.LoggingCleanupSuite } var _ = gc.Suite(&tagSuite{}) func (*tagSuite) TestReadTags(c *gc.C) { tags, err := readTags(twoDotOh, parseJSON(c, tagsResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(tags, gc.HasLen, 3) tag := tags[0] c.Check(tag.Name(), gc.Equals, "virtual") c.Check(tag.Comment(), gc.Equals, "virtual machines") c.Check(tag.Definition(), gc.Equals, "tag for machines that are virtual") c.Check(tag.KernelOpts(), gc.Equals, "nvme_core") } var tagsResponse = `[ { "resource_uri": "/2.0/tags/virtual", "name": "virtual", "comment": "virtual machines", "definition": "tag for machines that are virtual", "kernel_opts": "nvme_core" }, { "resource_uri": "/2.0/tags/physical", "name": "physical", "comment": "physical machines", "definition": "tag for machines that are physical", "kernel_opts": "" }, { "resource_uri": "/2.0/tags/r-pi", "name": "r-pi", "comment": "raspberry pis'", "definition": "tag for machines that are raspberry pis", "kernel_opts": "" } ]` golang-github-juju-gomaasapi-2.2.0/templates/000077500000000000000000000000001451732172100211555ustar00rootroot00000000000000golang-github-juju-gomaasapi-2.2.0/templates/source.go000066400000000000000000000001651451732172100230060ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi golang-github-juju-gomaasapi-2.2.0/templates/source_test.go000066400000000000000000000005321451732172100240430ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( . "gopkg.in/check.v1" ) type MyTestSuite struct{} var _ = Suite(&MyTestSuite{}) // TODO: Replace with real test functions. Give them real names. func (suite *MyTestSuite) TestXXX(c *C) { c.Check(2+2, Equals, 4) } golang-github-juju-gomaasapi-2.2.0/testing.go000066400000000000000000000172121451732172100211660ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/http" "net/http/httptest" "strings" "time" ) type singleServingServer struct { *httptest.Server requestContent *string requestHeader *http.Header } // newSingleServingServer creates a single-serving test http server which will // return only one response as defined by the passed arguments. func newSingleServingServer(uri string, response string, code int, delay time.Duration) *singleServingServer { var requestContent string var requestHeader http.Header var requested bool handler := func(writer http.ResponseWriter, request *http.Request) { if requested { http.Error(writer, "Already requested", http.StatusServiceUnavailable) } res, err := readAndClose(request.Body) if err != nil { panic(err) } requestContent = string(res) requestHeader = request.Header if request.URL.String() != uri { errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String()) http.Error(writer, errorMsg, http.StatusNotFound) } else { time.Sleep(delay) writer.WriteHeader(code) fmt.Fprint(writer, response) } requested = true } server := httptest.NewServer(http.HandlerFunc(handler)) return &singleServingServer{server, &requestContent, &requestHeader} } type flakyServer struct { *httptest.Server nbRequests *int requests *[][]byte headers *[]http.Header } // newFlakyServer creates a "flaky" test http server which will // return `nbFlakyResponses` responses with the given code and then a 200 response. func newFlakyServer(uri string, code int, nbFlakyResponses int) *flakyServer { nbRequests := 0 requests := make([][]byte, nbFlakyResponses+1) headers := make([]http.Header, nbFlakyResponses+1) handler := func(writer http.ResponseWriter, request *http.Request) { nbRequests += 1 body, err := readAndClose(request.Body) if err != nil { panic(err) } requests[nbRequests-1] = body headers[nbRequests-1] = request.Header if request.URL.String() != uri { errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String()) http.Error(writer, errorMsg, http.StatusNotFound) } else if nbRequests <= nbFlakyResponses { if code == http.StatusServiceUnavailable || code == http.StatusConflict { writer.Header().Set("Retry-After", "0") } writer.WriteHeader(code) fmt.Fprint(writer, "flaky") } else { writer.WriteHeader(http.StatusOK) fmt.Fprint(writer, "ok") } } server := httptest.NewServer(http.HandlerFunc(handler)) return &flakyServer{server, &nbRequests, &requests, &headers} } func newFlakyTLSServer(uri string, code int, nbFlakyResponses int) *flakyServer { nbRequests := 0 requests := make([][]byte, nbFlakyResponses+1) headers := make([]http.Header, nbFlakyResponses+1) var server *httptest.Server handler := func(writer http.ResponseWriter, request *http.Request) { nbRequests += 1 body, err := readAndClose(request.Body) if err != nil { panic(err) } requests[nbRequests-1] = body headers[nbRequests-1] = request.Header if request.URL.String() != uri { errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String()) http.Error(writer, errorMsg, http.StatusNotFound) } else if nbRequests <= nbFlakyResponses { if code == http.StatusServiceUnavailable { writer.Header().Set("Retry-After", "0") } writer.WriteHeader(code) fmt.Fprint(writer, "flaky") } else { writer.WriteHeader(http.StatusOK) fmt.Fprint(writer, "ok") } } server = httptest.NewTLSServer(http.HandlerFunc(handler)) return &flakyServer{server, &nbRequests, &requests, &headers} } type simpleResponse struct { status int body string } type SimpleTestServer struct { *httptest.Server getResponses map[string][]simpleResponse getResponseIndex map[string]int putResponses map[string][]simpleResponse putResponseIndex map[string]int postResponses map[string][]simpleResponse postResponseIndex map[string]int deleteResponses map[string][]simpleResponse deleteResponseIndex map[string]int requests []*http.Request } func NewSimpleServer() *SimpleTestServer { server := &SimpleTestServer{ getResponses: make(map[string][]simpleResponse), getResponseIndex: make(map[string]int), putResponses: make(map[string][]simpleResponse), putResponseIndex: make(map[string]int), postResponses: make(map[string][]simpleResponse), postResponseIndex: make(map[string]int), deleteResponses: make(map[string][]simpleResponse), deleteResponseIndex: make(map[string]int), } server.Server = httptest.NewUnstartedServer(http.HandlerFunc(server.handler)) return server } func (s *SimpleTestServer) AddGetResponse(path string, status int, body string) { logger.Debugf("add get response for: %s, %d", path, status) s.getResponses[path] = append(s.getResponses[path], simpleResponse{status: status, body: body}) } func (s *SimpleTestServer) AddPutResponse(path string, status int, body string) { logger.Debugf("add put response for: %s, %d", path, status) s.putResponses[path] = append(s.putResponses[path], simpleResponse{status: status, body: body}) } func (s *SimpleTestServer) AddPostResponse(path string, status int, body string) { logger.Debugf("add post response for: %s, %d", path, status) s.postResponses[path] = append(s.postResponses[path], simpleResponse{status: status, body: body}) } func (s *SimpleTestServer) AddDeleteResponse(path string, status int, body string) { logger.Debugf("add delete response for: %s, %d", path, status) s.deleteResponses[path] = append(s.deleteResponses[path], simpleResponse{status: status, body: body}) } func (s *SimpleTestServer) LastRequest() *http.Request { pos := len(s.requests) - 1 if pos < 0 { return nil } return s.requests[pos] } func (s *SimpleTestServer) LastNRequests(n int) []*http.Request { start := len(s.requests) - n if start < 0 { start = 0 } return s.requests[start:] } func (s *SimpleTestServer) RequestCount() int { return len(s.requests) } func (s *SimpleTestServer) ResetRequests() { s.requests = nil } func (s *SimpleTestServer) handler(writer http.ResponseWriter, request *http.Request) { method := request.Method var ( err error responses map[string][]simpleResponse responseIndex map[string]int ) switch method { case "GET": responses = s.getResponses responseIndex = s.getResponseIndex _, err = readAndClose(request.Body) if err != nil { panic(err) // it is a test, panic should be fine } case "PUT": responses = s.putResponses responseIndex = s.putResponseIndex err = request.ParseForm() if err != nil { panic(err) } case "POST": responses = s.postResponses responseIndex = s.postResponseIndex contentType := request.Header.Get("Content-Type") if strings.HasPrefix(contentType, "multipart/form-data;") { err = request.ParseMultipartForm(2 << 20) } else { err = request.ParseForm() } if err != nil { panic(err) } case "DELETE": responses = s.deleteResponses responseIndex = s.deleteResponseIndex _, err := readAndClose(request.Body) if err != nil { panic(err) } default: panic("unsupported method " + method) } s.requests = append(s.requests, request) uri := request.URL.String() testResponses, found := responses[uri] if !found { errorMsg := fmt.Sprintf("Error 404: page not found ('%v').", uri) http.Error(writer, errorMsg, http.StatusNotFound) } else { index := responseIndex[uri] response := testResponses[index] responseIndex[uri] = index + 1 writer.WriteHeader(response.status) fmt.Fprint(writer, response.body) } } golang-github-juju-gomaasapi-2.2.0/testservice.go000066400000000000000000001620361451732172100220560ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "bufio" "bytes" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "mime/multipart" "net" "net/http" "net/http/httptest" "net/url" "regexp" "sort" "strconv" "strings" "sync" "text/template" "time" "github.com/juju/mgo/v2/bson" ) // TestMAASObject is a fake MAAS server MAASObject. type TestMAASObject struct { MAASObject TestServer *TestServer } // checkError is a shorthand helper that panics if err is not nil. func checkError(err error) { if err != nil { panic(err) } } // NewTestMAAS returns a TestMAASObject that implements the MAASObject // interface and thus can be used as a test object instead of the one returned // by gomaasapi.NewMAAS(). func NewTestMAAS(version string) *TestMAASObject { server := NewTestServer(version) authClient, err := NewAnonymousClient(server.URL, version) checkError(err) maas := NewMAAS(*authClient) return &TestMAASObject{*maas, server} } // Close shuts down the test server. func (testMAASObject *TestMAASObject) Close() { testMAASObject.TestServer.Close() } // A TestServer is an HTTP server listening on a system-chosen port on the // local loopback interface, which simulates the behavior of a MAAS server. // It is intendend for use in end-to-end HTTP tests using the gomaasapi // library. type TestServer struct { *httptest.Server serveMux *http.ServeMux client Client nodes map[string]MAASObject ownedNodes map[string]bool // mapping system_id -> list of operations performed. nodeOperations map[string][]string // list of operations performed at the /nodes/ level. nodesOperations []string // mapping system_id -> list of Values passed when performing // operations nodeOperationRequestValues map[string][]url.Values // list of Values passed when performing operations at the // /nodes/ level. nodesOperationRequestValues []url.Values nodeMetadata map[string]Node files map[string]MAASObject networks map[string]MAASObject networksPerNode map[string][]string ipAddressesPerNetwork map[string][]string version string macAddressesPerNetwork map[string]map[string]JSONObject tagsPerNode map[string][]string nodeDetails map[string]string zones map[string]JSONObject tags map[string]JSONObject // bootImages is a map of nodegroup UUIDs to boot-image objects. bootImages map[string][]JSONObject // nodegroupsInterfaces is a map of nodegroup UUIDs to interface // objects. nodegroupsInterfaces map[string][]JSONObject // versionJSON is the response to the /version/ endpoint listing the // capabilities of the MAAS server. versionJSON string // devices is a map of device UUIDs to devices. devices map[string]*TestDevice subnets map[uint]TestSubnet subnetNameToID map[string]uint nextSubnet uint spaces map[uint]*TestSpace spaceNameToID map[string]uint nextSpace uint vlans map[int]TestVLAN nextVLAN int staticRoutes map[uint]*TestStaticRoute nextStaticRoute uint } type TestDevice struct { IPAddresses []string SystemId string MACAddresses []string Parent string Hostname string // Not part of the device definition but used by the template. APIVersion string } func getNodesEndpoint(version string) string { return fmt.Sprintf("/api/%s/nodes/", version) } func getNodeURL(version, systemId string) string { return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId) } func getNodeURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } func getDevicesEndpoint(version string) string { return fmt.Sprintf("/api/%s/devices/", version) } func getDeviceURL(version, systemId string) string { return fmt.Sprintf("/api/%s/devices/%s/", version, systemId) } func getDeviceURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/devices/([^/]*)/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } func getFilesEndpoint(version string) string { return fmt.Sprintf("/api/%s/files/", version) } func getFileURL(version, filename string) string { // Uses URL object so filename is correctly percent-escaped url := url.URL{} url.Path = fmt.Sprintf("/api/%s/files/%s/", version, filename) return url.String() } func getFileURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/files/(.*)/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } func getNetworksEndpoint(version string) string { return fmt.Sprintf("/api/%s/networks/", version) } func getNetworkURL(version, name string) string { return fmt.Sprintf("/api/%s/networks/%s/", version, name) } func getNetworkURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/networks/(.*)/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } func getIPAddressesEndpoint(version string) string { return fmt.Sprintf("/api/%s/ipaddresses/", version) } func getMACAddressURL(version, systemId, macAddress string) string { return fmt.Sprintf("/api/%s/nodes/%s/macs/%s/", version, systemId, url.QueryEscape(macAddress)) } func getVersionURL(version string) string { return fmt.Sprintf("/api/%s/version/", version) } func getNodegroupsEndpoint(version string) string { return fmt.Sprintf("/api/%s/nodegroups/", version) } func getNodegroupURL(version, uuid string) string { return fmt.Sprintf("/api/%s/nodegroups/%s/", version, uuid) } func getNodegroupsInterfacesURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/interfaces/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } func getBootimagesURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/boot-images/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } func getZonesEndpoint(version string) string { return fmt.Sprintf("/api/%s/zones/", version) } func getTagsEndpoint(version string) string { return fmt.Sprintf("/api/%s/tags/", version) } func getTagURL(version, tag_name string) string { return fmt.Sprintf("/api/%s/tags/%s/", version, tag_name) } func getTagURLRE(version string) *regexp.Regexp { reString := fmt.Sprintf("^/api/%s/tags/([^/]*)/$", regexp.QuoteMeta(version)) return regexp.MustCompile(reString) } // Clear clears all the fake data stored and recorded by the test server // (nodes, recorded operations, etc.). func (server *TestServer) Clear() { server.nodes = make(map[string]MAASObject) server.ownedNodes = make(map[string]bool) server.nodesOperations = make([]string, 0) server.nodeOperations = make(map[string][]string) server.nodesOperationRequestValues = make([]url.Values, 0) server.nodeOperationRequestValues = make(map[string][]url.Values) server.nodeMetadata = make(map[string]Node) server.files = make(map[string]MAASObject) server.networks = make(map[string]MAASObject) server.networksPerNode = make(map[string][]string) server.ipAddressesPerNetwork = make(map[string][]string) server.tagsPerNode = make(map[string][]string) server.macAddressesPerNetwork = make(map[string]map[string]JSONObject) server.nodeDetails = make(map[string]string) server.bootImages = make(map[string][]JSONObject) server.nodegroupsInterfaces = make(map[string][]JSONObject) server.zones = make(map[string]JSONObject) server.tags = make(map[string]JSONObject) server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses","devices-management","network-deployment-ubuntu"]}` server.devices = make(map[string]*TestDevice) server.subnets = make(map[uint]TestSubnet) server.subnetNameToID = make(map[string]uint) server.nextSubnet = 1 server.spaces = make(map[uint]*TestSpace) server.spaceNameToID = make(map[string]uint) server.nextSpace = 1 server.vlans = make(map[int]TestVLAN) server.nextVLAN = 1 server.staticRoutes = make(map[uint]*TestStaticRoute) server.nextStaticRoute = 1 } // SetVersionJSON sets the JSON response (capabilities) returned from the // /version/ endpoint. func (server *TestServer) SetVersionJSON(json string) { server.versionJSON = json } // NodesOperations returns the list of operations performed at the /nodes/ // level. func (server *TestServer) NodesOperations() []string { return server.nodesOperations } // NodeOperations returns the map containing the list of the operations // performed for each node. func (server *TestServer) NodeOperations() map[string][]string { return server.nodeOperations } // NodesOperationRequestValues returns the list of url.Values extracted // from the request used when performing operations at the /nodes/ level. func (server *TestServer) NodesOperationRequestValues() []url.Values { return server.nodesOperationRequestValues } // NodeOperationRequestValues returns the map containing the list of the // url.Values extracted from the request used when performing operations // on nodes. func (server *TestServer) NodeOperationRequestValues() map[string][]url.Values { return server.nodeOperationRequestValues } func parseRequestValues(request *http.Request) url.Values { var requestValues url.Values if request.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { if request.PostForm == nil { if err := request.ParseForm(); err != nil { panic(err) } } requestValues = request.PostForm } return requestValues } func (server *TestServer) addNodesOperation(operation string, request *http.Request) url.Values { requestValues := parseRequestValues(request) server.nodesOperations = append(server.nodesOperations, operation) server.nodesOperationRequestValues = append(server.nodesOperationRequestValues, requestValues) return requestValues } func (server *TestServer) addNodeOperation(systemId, operation string, request *http.Request) url.Values { operations, present := server.nodeOperations[systemId] operationRequestValues, present2 := server.nodeOperationRequestValues[systemId] if present != present2 { panic("inconsistent state: nodeOperations and nodeOperationRequestValues don't have the same keys.") } requestValues := parseRequestValues(request) if !present { operations = []string{operation} operationRequestValues = []url.Values{requestValues} } else { operations = append(operations, operation) operationRequestValues = append(operationRequestValues, requestValues) } server.nodeOperations[systemId] = operations server.nodeOperationRequestValues[systemId] = operationRequestValues return requestValues } // NewNode creates a MAAS node. The provided string should be a valid json // string representing a map and contain a string value for the key // 'system_id'. e.g. `{"system_id": "mysystemid"}`. // If one of these conditions is not met, NewNode panics. func (server *TestServer) NewNode(jsonText string) MAASObject { var attrs map[string]interface{} err := json.Unmarshal([]byte(jsonText), &attrs) checkError(err) systemIdEntry, hasSystemId := attrs["system_id"] if !hasSystemId { panic("The given map json string does not contain a 'system_id' value.") } systemId := systemIdEntry.(string) attrs[resourceURI] = getNodeURL(server.version, systemId) if _, hasStatus := attrs["status"]; !hasStatus { attrs["status"] = NodeStatusDeployed } obj := newJSONMAASObject(attrs, server.client) server.nodes[systemId] = obj return obj } // Nodes returns a map associating all the nodes' system ids with the nodes' // objects. func (server *TestServer) Nodes() map[string]MAASObject { return server.nodes } // OwnedNodes returns a map whose keys represent the nodes that are currently // allocated. func (server *TestServer) OwnedNodes() map[string]bool { return server.ownedNodes } // NewFile creates a file in the test MAAS server. func (server *TestServer) NewFile(filename string, filecontent []byte) MAASObject { attrs := make(map[string]interface{}) attrs[resourceURI] = getFileURL(server.version, filename) base64Content := base64.StdEncoding.EncodeToString(filecontent) attrs["content"] = base64Content attrs["filename"] = filename // Allocate an arbitrary URL here. It would be nice if the caller // could do this, but that would change the API and require many // changes. escapedName := url.QueryEscape(filename) attrs["anon_resource_uri"] = "/maas/1.0/files/?op=get_by_key&key=" + escapedName + "_key" obj := newJSONMAASObject(attrs, server.client) server.files[filename] = obj return obj } func (server *TestServer) Files() map[string]MAASObject { return server.files } // ChangeNode updates a node with the given key/value. func (server *TestServer) ChangeNode(systemId, key, value string) { node, found := server.nodes[systemId] if !found { panic("No node with such 'system_id'.") } node.GetMap()[key] = maasify(server.client, value) } // NewIPAddress creates a new static IP address reservation for the // given network/subnet and ipAddress. While networks is being deprecated // try the given name as both a netowrk and a subnet. func (server *TestServer) NewIPAddress(ipAddress, networkOrSubnet string) { _, foundNetwork := server.networks[networkOrSubnet] subnetID, foundSubnet := server.subnetNameToID[networkOrSubnet] if (foundNetwork || foundSubnet) == false { panic("No such network or subnet: " + networkOrSubnet) } if foundNetwork { ips, found := server.ipAddressesPerNetwork[networkOrSubnet] if found { ips = append(ips, ipAddress) } else { ips = []string{ipAddress} } server.ipAddressesPerNetwork[networkOrSubnet] = ips } else { subnet := server.subnets[subnetID] netIp := net.ParseIP(ipAddress) if netIp == nil { panic(ipAddress + " is invalid") } ip := IPFromNetIP(netIp) ip.Purpose = []string{"assigned-ip"} subnet.InUseIPAddresses = append(subnet.InUseIPAddresses, ip) server.subnets[subnetID] = subnet } } // RemoveIPAddress removes the given existing ipAddress and returns // whether it was actually removed. func (server *TestServer) RemoveIPAddress(ipAddress string) bool { for network, ips := range server.ipAddressesPerNetwork { for i, ip := range ips { if ip == ipAddress { ips = append(ips[:i], ips[i+1:]...) server.ipAddressesPerNetwork[network] = ips return true } } } for _, device := range server.devices { for i, addr := range device.IPAddresses { if addr == ipAddress { device.IPAddresses = append(device.IPAddresses[:i], device.IPAddresses[i+1:]...) return true } } } return false } // IPAddresses returns the map with network names as keys and slices // of IP addresses belonging to each network as values. func (server *TestServer) IPAddresses() map[string][]string { return server.ipAddressesPerNetwork } // NewNetwork creates a network in the test MAAS server func (server *TestServer) NewNetwork(jsonText string) MAASObject { var attrs map[string]interface{} err := json.Unmarshal([]byte(jsonText), &attrs) checkError(err) nameEntry, hasName := attrs["name"] _, hasIP := attrs["ip"] _, hasNetmask := attrs["netmask"] if !hasName || !hasIP || !hasNetmask { panic("The given map json string does not contain a 'name', 'ip', or 'netmask' value.") } // TODO(gz): Sanity checking done on other fields name := nameEntry.(string) attrs[resourceURI] = getNetworkURL(server.version, name) obj := newJSONMAASObject(attrs, server.client) server.networks[name] = obj return obj } // NewNodegroupInterface adds a nodegroup-interface, for the specified // nodegroup, in the test MAAS server. func (server *TestServer) NewNodegroupInterface(uuid, jsonText string) JSONObject { _, ok := server.bootImages[uuid] if !ok { panic("no nodegroup with the given UUID") } var attrs map[string]interface{} err := json.Unmarshal([]byte(jsonText), &attrs) checkError(err) requiredMembers := []string{"ip_range_high", "ip_range_low", "broadcast_ip", "static_ip_range_low", "static_ip_range_high", "name", "ip", "subnet_mask", "management", "interface"} for _, member := range requiredMembers { _, hasMember := attrs[member] if !hasMember { panic(fmt.Sprintf("The given map json string does not contain a required %q", member)) } } obj := maasify(server.client, attrs) server.nodegroupsInterfaces[uuid] = append(server.nodegroupsInterfaces[uuid], obj) return obj } func (server *TestServer) ConnectNodeToNetwork(systemId, name string) { _, hasNode := server.nodes[systemId] if !hasNode { panic("no node with the given system id") } _, hasNetwork := server.networks[name] if !hasNetwork { panic("no network with the given name") } networkNames, _ := server.networksPerNode[systemId] server.networksPerNode[systemId] = append(networkNames, name) } func (server *TestServer) ConnectNodeToNetworkWithMACAddress(systemId, networkName, macAddress string) { node, hasNode := server.nodes[systemId] if !hasNode { panic("no node with the given system id") } if _, hasNetwork := server.networks[networkName]; !hasNetwork { panic("no network with the given name") } networkNames, _ := server.networksPerNode[systemId] server.networksPerNode[systemId] = append(networkNames, networkName) attrs := make(map[string]interface{}) attrs[resourceURI] = getMACAddressURL(server.version, systemId, macAddress) attrs["mac_address"] = macAddress array := []JSONObject{} if set, ok := node.GetMap()["macaddress_set"]; ok { var err error array, err = set.GetArray() if err != nil { panic(err) } } array = append(array, maasify(server.client, attrs)) node.GetMap()["macaddress_set"] = JSONObject{value: array, client: server.client} if _, ok := server.macAddressesPerNetwork[networkName]; !ok { server.macAddressesPerNetwork[networkName] = map[string]JSONObject{} } server.macAddressesPerNetwork[networkName][systemId] = maasify(server.client, attrs) } // AddBootImage adds a boot-image object to the specified nodegroup. func (server *TestServer) AddBootImage(nodegroupUUID string, jsonText string) { var attrs map[string]interface{} err := json.Unmarshal([]byte(jsonText), &attrs) checkError(err) if _, ok := attrs["architecture"]; !ok { panic("The boot-image json string does not contain an 'architecture' value.") } if _, ok := attrs["release"]; !ok { panic("The boot-image json string does not contain a 'release' value.") } obj := maasify(server.client, attrs) server.bootImages[nodegroupUUID] = append(server.bootImages[nodegroupUUID], obj) } // AddZone adds a physical zone to the server. func (server *TestServer) AddZone(name, description string) { attrs := map[string]interface{}{ "name": name, "description": description, } obj := maasify(server.client, attrs) server.zones[name] = obj } // AddTah adds a tag to the server. func (server *TestServer) AddTag(name, comment string) { attrs := map[string]interface{}{ "name": name, "comment": comment, resourceURI: getTagURL(server.version, name), } obj := maasify(server.client, attrs) server.tags[name] = obj } func (server *TestServer) AddDevice(device *TestDevice) { server.devices[device.SystemId] = device } func (server *TestServer) Devices() map[string]*TestDevice { return server.devices } // NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down. func NewTestServer(version string) *TestServer { server := &TestServer{version: version} serveMux := http.NewServeMux() devicesURL := getDevicesEndpoint(server.version) // Register handler for '/api//devices/*'. serveMux.HandleFunc(devicesURL, func(w http.ResponseWriter, r *http.Request) { devicesHandler(server, w, r) }) nodesURL := getNodesEndpoint(server.version) // Register handler for '/api//nodes/*'. serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) { nodesHandler(server, w, r) }) filesURL := getFilesEndpoint(server.version) // Register handler for '/api//files/*'. serveMux.HandleFunc(filesURL, func(w http.ResponseWriter, r *http.Request) { filesHandler(server, w, r) }) networksURL := getNetworksEndpoint(server.version) // Register handler for '/api//networks/'. serveMux.HandleFunc(networksURL, func(w http.ResponseWriter, r *http.Request) { networksHandler(server, w, r) }) ipAddressesURL := getIPAddressesEndpoint(server.version) // Register handler for '/api//ipaddresses/'. serveMux.HandleFunc(ipAddressesURL, func(w http.ResponseWriter, r *http.Request) { ipAddressesHandler(server, w, r) }) versionURL := getVersionURL(server.version) // Register handler for '/api//version/'. serveMux.HandleFunc(versionURL, func(w http.ResponseWriter, r *http.Request) { versionHandler(server, w, r) }) // Register handler for '/api//nodegroups/*'. nodegroupsURL := getNodegroupsEndpoint(server.version) serveMux.HandleFunc(nodegroupsURL, func(w http.ResponseWriter, r *http.Request) { nodegroupsHandler(server, w, r) }) // Register handler for '/api//zones/*'. zonesURL := getZonesEndpoint(server.version) serveMux.HandleFunc(zonesURL, func(w http.ResponseWriter, r *http.Request) { zonesHandler(server, w, r) }) // Register handler for '/api//zones/*'. tagsURL := getTagsEndpoint(server.version) serveMux.HandleFunc(tagsURL, func(w http.ResponseWriter, r *http.Request) { tagsHandler(server, w, r) }) subnetsURL := getSubnetsEndpoint(server.version) serveMux.HandleFunc(subnetsURL, func(w http.ResponseWriter, r *http.Request) { subnetsHandler(server, w, r) }) spacesURL := getSpacesEndpoint(server.version) serveMux.HandleFunc(spacesURL, func(w http.ResponseWriter, r *http.Request) { spacesHandler(server, w, r) }) staticRoutesURL := getStaticRoutesEndpoint(server.version) serveMux.HandleFunc(staticRoutesURL, func(w http.ResponseWriter, r *http.Request) { staticRoutesHandler(server, w, r) }) vlansURL := getVLANsEndpoint(server.version) serveMux.HandleFunc(vlansURL, func(w http.ResponseWriter, r *http.Request) { vlansHandler(server, w, r) }) var mu sync.Mutex singleFile := func(w http.ResponseWriter, req *http.Request) { mu.Lock() defer mu.Unlock() serveMux.ServeHTTP(w, req) } newServer := httptest.NewServer(http.HandlerFunc(singleFile)) client, err := NewAnonymousClient(newServer.URL, "1.0") checkError(err) server.Server = newServer server.serveMux = serveMux server.client = *client server.Clear() return server } // devicesHandler handles requests for '/api//devices/*'. func devicesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") deviceURLRE := getDeviceURLRE(server.version) deviceURLMatch := deviceURLRE.FindStringSubmatch(r.URL.Path) devicesURL := getDevicesEndpoint(server.version) switch { case r.URL.Path == devicesURL: devicesTopLevelHandler(server, w, r, op) case deviceURLMatch != nil: // Request for a single device. deviceHandler(server, w, r, deviceURLMatch[1], op) default: // Default handler: not found. http.NotFoundHandler().ServeHTTP(w, r) } } // devicesTopLevelHandler handles a request for /api//devices/ // (with no device id following as part of the path). func devicesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) { switch { case r.Method == "GET" && op == "list": // Device listing operation. deviceListingHandler(server, w, r) case r.Method == "POST" && op == "new": newDeviceHandler(server, w, r) default: w.WriteHeader(http.StatusBadRequest) } } func macMatches(mac string, device *TestDevice) bool { return contains(device.MACAddresses, mac) } // deviceListingHandler handles requests for '/devices/'. func deviceListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) // TODO(mfoord): support filtering by hostname and id macs, hasMac := values["mac_address"] var matchedDevices []*TestDevice if !hasMac { for _, device := range server.devices { matchedDevices = append(matchedDevices, device) } } else { for _, mac := range macs { for _, device := range server.devices { if macMatches(mac, device) { matchedDevices = append(matchedDevices, device) } } } } deviceChunks := make([]string, len(matchedDevices)) for i := range matchedDevices { deviceChunks[i] = renderDevice(matchedDevices[i]) } json := fmt.Sprintf("[%v]", strings.Join(deviceChunks, ", ")) w.WriteHeader(http.StatusOK) fmt.Fprint(w, json) } var templateFuncs = template.FuncMap{ "quotedList": func(items []string) string { var pieces []string for _, item := range items { pieces = append(pieces, fmt.Sprintf("%q", item)) } return strings.Join(pieces, ", ") }, "last": func(items []string) []string { if len(items) == 0 { return []string{} } return items[len(items)-1:] }, "allButLast": func(items []string) []string { if len(items) < 2 { return []string{} } return items[0 : len(items)-1] }, } const ( // The json template for generating new devices. // TODO(mfoord): set resource_uri in MAC addresses deviceTemplate = `{ "macaddress_set": [{{range .MACAddresses | allButLast}} { "mac_address": "{{.}}" },{{end}}{{range .MACAddresses | last}} { "mac_address": "{{.}}" }{{end}} ], "zone": { "resource_uri": "/MAAS/api/{{.APIVersion}}/zones/default/", "name": "default", "description": "" }, "parent": "{{.Parent}}", "ip_addresses": [{{.IPAddresses | quotedList }}], "hostname": "{{.Hostname}}", "tag_names": [], "owner": "maas-admin", "system_id": "{{.SystemId}}", "resource_uri": "/MAAS/api/{{.APIVersion}}/devices/{{.SystemId}}/" }` ) func renderDevice(device *TestDevice) string { t := template.New("Device template") t = t.Funcs(templateFuncs) t, err := t.Parse(deviceTemplate) checkError(err) var buf bytes.Buffer err = t.Execute(&buf, device) checkError(err) return buf.String() } func getValue(values url.Values, value string) (string, bool) { result, hasResult := values[value] if !hasResult || len(result) != 1 || result[0] == "" { return "", false } return result[0], true } func getValues(values url.Values, key string) ([]string, bool) { result, hasResult := values[key] if !hasResult { return nil, false } var output []string for _, val := range result { if val != "" { output = append(output, val) } } if len(output) == 0 { return nil, false } return output, true } // newDeviceHandler creates, stores and returns new devices. func newDeviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { err := r.ParseForm() checkError(err) values := r.PostForm // TODO(mfood): generate a "proper" uuid for the system Id. uuid, err := generateNonce() checkError(err) systemId := fmt.Sprintf("node-%v", uuid) // At least one MAC address must be specified. // TODO(mfoord) we only support a single MAC in the test server. macs, hasMacs := getValues(values, "mac_addresses") // hostname and parent are optional. // TODO(mfoord): we require both to be set in the test server. hostname, hasHostname := getValue(values, "hostname") parent, hasParent := getValue(values, "parent") if !hasHostname || !hasMacs || !hasParent { w.WriteHeader(http.StatusBadRequest) return } device := &TestDevice{ MACAddresses: macs, APIVersion: server.version, Parent: parent, Hostname: hostname, SystemId: systemId, } deviceJSON := renderDevice(device) server.devices[systemId] = device w.WriteHeader(http.StatusOK) fmt.Fprint(w, deviceJSON) return } // deviceHandler handles requests for '/api//devices//'. func deviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) { device, ok := server.devices[systemId] if !ok { http.NotFoundHandler().ServeHTTP(w, r) return } if r.Method == "GET" { deviceJSON := renderDevice(device) if operation == "" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, deviceJSON) return } else { w.WriteHeader(http.StatusBadRequest) return } } if r.Method == "POST" { if operation == "claim_sticky_ip_address" { err := r.ParseForm() checkError(err) values := r.PostForm // TODO(mfoord): support optional mac_address parameter // TODO(mfoord): requested_address should be optional // and we should generate one if it isn't provided. address, hasAddress := getValue(values, "requested_address") if !hasAddress { w.WriteHeader(http.StatusBadRequest) return } checkError(err) device.IPAddresses = append(device.IPAddresses, address) deviceJSON := renderDevice(device) w.WriteHeader(http.StatusOK) fmt.Fprint(w, deviceJSON) return } else { w.WriteHeader(http.StatusBadRequest) return } } else if r.Method == "DELETE" { delete(server.devices, systemId) w.WriteHeader(http.StatusNoContent) return } // TODO(mfoord): support PUT method for updating device http.NotFoundHandler().ServeHTTP(w, r) } // nodesHandler handles requests for '/api//nodes/*'. func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") nodeURLRE := getNodeURLRE(server.version) nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path) nodesURL := getNodesEndpoint(server.version) switch { case r.URL.Path == nodesURL: nodesTopLevelHandler(server, w, r, op) case nodeURLMatch != nil: // Request for a single node. nodeHandler(server, w, r, nodeURLMatch[1], op) default: // Default handler: not found. http.NotFoundHandler().ServeHTTP(w, r) } } // nodeHandler handles requests for '/api//nodes//'. func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) { node, ok := server.nodes[systemId] if !ok { http.NotFoundHandler().ServeHTTP(w, r) return } UUID, UUIDError := node.values["system_id"].GetString() if UUIDError == nil { i, err := JSONObjectFromStruct(server.client, server.nodeMetadata[UUID].Interfaces) checkError(err) node.values["interface_set"] = i } if r.Method == "GET" { if operation == "" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, marshalNode(node)) return } else if operation == "details" { nodeDetailsHandler(server, w, r, systemId) return } else { w.WriteHeader(http.StatusBadRequest) return } } if r.Method == "POST" { // The only operations supported are "start", "stop" and "release". if operation == "start" || operation == "stop" || operation == "release" { // Record operation on node. server.addNodeOperation(systemId, operation, r) if operation == "release" { delete(server.OwnedNodes(), systemId) } w.WriteHeader(http.StatusOK) fmt.Fprint(w, marshalNode(node)) return } w.WriteHeader(http.StatusBadRequest) return } if r.Method == "DELETE" { delete(server.nodes, systemId) w.WriteHeader(http.StatusOK) return } http.NotFoundHandler().ServeHTTP(w, r) } func contains(slice []string, val string) bool { for _, item := range slice { if item == val { return true } } return false } // nodeListingHandler handles requests for '/nodes/'. func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) ids, hasId := values["id"] var convertedNodes = []map[string]JSONObject{} for systemId, node := range server.nodes { if !hasId || contains(ids, systemId) { convertedNodes = append(convertedNodes, node.GetMap()) } } res, err := json.MarshalIndent(convertedNodes, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // nodeDeploymentStatusHandler handles requests for '/nodes/?op=deployment_status'. func nodeDeploymentStatusHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) nodes, _ := values["nodes"] var nodeStatus = make(map[string]interface{}) for _, systemId := range nodes { node := server.nodes[systemId] field, err := node.GetField("status") if err != nil { continue } switch field { case NodeStatusDeployed: nodeStatus[systemId] = "Deployed" case NodeStatusFailedDeployment: nodeStatus[systemId] = "Failed deployment" default: nodeStatus[systemId] = "Not in Deployment" } } obj := maasify(server.client, nodeStatus) res, err := json.MarshalIndent(obj, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // findFreeNode looks for a node that is currently available, and // matches the specified filter. func findFreeNode(server *TestServer, filter url.Values) *MAASObject { for systemID, node := range server.Nodes() { _, present := server.OwnedNodes()[systemID] if !present { var agentName, nodeName, zoneName, tagName, mem, cpuCores, arch string for k := range filter { switch k { case "agent_name": agentName = filter.Get(k) case "name": nodeName = filter.Get(k) case "zone": zoneName = filter.Get(k) case "tags": tagName = filter.Get(k) case "mem": mem = filter.Get(k) case "arch": arch = filter.Get(k) case "cpu-cores": cpuCores = filter.Get(k) } } if nodeName != "" && !matchField(node, "hostname", nodeName) { continue } if zoneName != "" && !matchField(node, "zone", zoneName) { continue } if tagName != "" && !matchField(node, "tag_names", tagName) { continue } if mem != "" && !matchNumericField(node, "memory", mem) { continue } if arch != "" && !matchArchitecture(node, "architecture", arch) { continue } if cpuCores != "" && !matchNumericField(node, "cpu_count", cpuCores) { continue } if agentName != "" { agentNameObj := maasify(server.client, agentName) node.GetMap()["agent_name"] = agentNameObj } else { delete(node.GetMap(), "agent_name") } return &node } } return nil } func matchArchitecture(node MAASObject, k, v string) bool { field, err := node.GetField(k) if err != nil { return false } baseArch := strings.Split(field, "/") return v == baseArch[0] } func matchNumericField(node MAASObject, k, v string) bool { field, ok := node.GetMap()[k] if !ok { return false } nodeVal, err := field.GetFloat64() if err != nil { return false } constraintVal, err := strconv.ParseFloat(v, 64) if err != nil { return false } return constraintVal <= nodeVal } func matchField(node MAASObject, k, v string) bool { field, err := node.GetField(k) if err != nil { return false } return field == v } // nodesAcquireHandler simulates acquiring a node. func nodesAcquireHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { requestValues := server.addNodesOperation("acquire", r) node := findFreeNode(server, requestValues) if node == nil { w.WriteHeader(http.StatusConflict) } else { systemId, err := node.GetField("system_id") checkError(err) server.OwnedNodes()[systemId] = true res, err := json.MarshalIndent(node, "", " ") checkError(err) // Record operation. server.addNodeOperation(systemId, "acquire", r) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } } // nodesReleaseHandler simulates releasing multiple nodes. func nodesReleaseHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { server.addNodesOperation("release", r) values := server.NodesOperationRequestValues() systemIds := values[len(values)-1]["nodes"] var unknown []string for _, systemId := range systemIds { if _, ok := server.Nodes()[systemId]; !ok { unknown = append(unknown, systemId) } } if len(unknown) > 0 { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Unknown node(s): %s.", strings.Join(unknown, ", ")) return } var releasedNodes = []map[string]JSONObject{} for _, systemId := range systemIds { if _, ok := server.OwnedNodes()[systemId]; !ok { continue } delete(server.OwnedNodes(), systemId) node := server.Nodes()[systemId] releasedNodes = append(releasedNodes, node.GetMap()) } res, err := json.MarshalIndent(releasedNodes, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // nodesTopLevelHandler handles a request for /api//nodes/ // (with no node id following as part of the path). func nodesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) { switch { case r.Method == "GET" && op == "list": // Node listing operation. nodeListingHandler(server, w, r) case r.Method == "GET" && op == "deployment_status": // Node deployment_status operation. nodeDeploymentStatusHandler(server, w, r) case r.Method == "POST" && op == "acquire": nodesAcquireHandler(server, w, r) case r.Method == "POST" && op == "release": nodesReleaseHandler(server, w, r) default: w.WriteHeader(http.StatusBadRequest) } } // AddNodeDetails stores node details, expected in XML format. func (server *TestServer) AddNodeDetails(systemId, xmlText string) { _, hasNode := server.nodes[systemId] if !hasNode { panic("no node with the given system id") } server.nodeDetails[systemId] = xmlText } const lldpXML = ` ` // nodeDetailesHandler handles requests for '/api//nodes//?op=details'. func nodeDetailsHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string) { attrs := make(map[string]interface{}) attrs["lldp"] = lldpXML xmlText, _ := server.nodeDetails[systemId] attrs["lshw"] = []byte(xmlText) res, err := bson.Marshal(attrs) checkError(err) w.Header().Set("Content-Type", "application/bson") w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // filesHandler handles requests for '/api//files/*'. func filesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") fileURLRE := getFileURLRE(server.version) fileURLMatch := fileURLRE.FindStringSubmatch(r.URL.Path) fileListingURL := getFilesEndpoint(server.version) switch { case r.Method == "GET" && op == "list" && r.URL.Path == fileListingURL: // File listing operation. fileListingHandler(server, w, r) case op == "get" && r.Method == "GET" && r.URL.Path == fileListingURL: getFileHandler(server, w, r) case op == "add" && r.Method == "POST" && r.URL.Path == fileListingURL: addFileHandler(server, w, r) case fileURLMatch != nil: // Request for a single file. fileHandler(server, w, r, fileURLMatch[1], op) default: // Default handler: not found. http.NotFoundHandler().ServeHTTP(w, r) } } // listFilenames returns the names of those uploaded files whose names start // with the given prefix, sorted lexicographically. func listFilenames(server *TestServer, prefix string) []string { var filenames = make([]string, 0) for filename := range server.files { if strings.HasPrefix(filename, prefix) { filenames = append(filenames, filename) } } sort.Strings(filenames) return filenames } // stripFileContent copies a map of attributes representing an uploaded file, // but with the "content" attribute removed. func stripContent(original map[string]JSONObject) map[string]JSONObject { newMap := make(map[string]JSONObject, len(original)-1) for key, value := range original { if key != "content" { newMap[key] = value } } return newMap } // fileListingHandler handles requests for '/api//files/?op=list'. func fileListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) prefix := values.Get("prefix") filenames := listFilenames(server, prefix) // Build a sorted list of the files as map[string]JSONObject objects. convertedFiles := make([]map[string]JSONObject, 0) for _, filename := range filenames { // The "content" attribute is not in the listing. fileMap := stripContent(server.files[filename].GetMap()) convertedFiles = append(convertedFiles, fileMap) } res, err := json.MarshalIndent(convertedFiles, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // fileHandler handles requests for '/api//files//'. func fileHandler(server *TestServer, w http.ResponseWriter, r *http.Request, filename string, operation string) { switch { case r.Method == "DELETE": delete(server.files, filename) w.WriteHeader(http.StatusOK) case r.Method == "GET": // Retrieve a file's information (including content) as a JSON // object. file, ok := server.files[filename] if !ok { http.NotFoundHandler().ServeHTTP(w, r) return } jsonText, err := json.MarshalIndent(file, "", " ") if err != nil { panic(err) } w.WriteHeader(http.StatusOK) w.Write(jsonText) default: // Default handler: not found. http.NotFoundHandler().ServeHTTP(w, r) } } // InternalError replies to the request with an HTTP 500 internal error. func InternalError(w http.ResponseWriter, r *http.Request, err error) { http.Error(w, err.Error(), http.StatusInternalServerError) } // getFileHandler handles requests for // '/api//files/?op=get&filename=filename'. func getFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) filename := values.Get("filename") file, found := server.files[filename] if !found { http.NotFoundHandler().ServeHTTP(w, r) return } base64Content, err := file.GetField("content") if err != nil { InternalError(w, r, err) return } content, err := base64.StdEncoding.DecodeString(base64Content) if err != nil { InternalError(w, r, err) return } w.Write(content) } func readMultipart(upload *multipart.FileHeader) ([]byte, error) { file, err := upload.Open() if err != nil { return nil, err } defer file.Close() reader := bufio.NewReader(file) return ioutil.ReadAll(reader) } // filesHandler handles requests for '/api//files/?op=add&filename=filename'. func addFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(10000000) checkError(err) filename := r.Form.Get("filename") if filename == "" { panic("upload has no filename") } uploads := r.MultipartForm.File if len(uploads) != 1 { panic("the payload should contain one file and one file only") } var upload *multipart.FileHeader for _, uploadContent := range uploads { upload = uploadContent[0] } content, err := readMultipart(upload) checkError(err) server.NewFile(filename, content) w.WriteHeader(http.StatusOK) } // networkListConnectedMACSHandler handles requests for '/api//networks//?op=list_connected_macs' func networkListConnectedMACSHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { networkURLRE := getNetworkURLRE(server.version) networkURLREMatch := networkURLRE.FindStringSubmatch(r.URL.Path) if networkURLREMatch == nil { http.NotFoundHandler().ServeHTTP(w, r) return } networkName := networkURLREMatch[1] convertedMacAddresses := []map[string]JSONObject{} if macAddresses, ok := server.macAddressesPerNetwork[networkName]; ok { for _, macAddress := range macAddresses { m, err := macAddress.GetMap() checkError(err) convertedMacAddresses = append(convertedMacAddresses, m) } } res, err := json.MarshalIndent(convertedMacAddresses, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // networksHandler handles requests for '/api//networks/?node=system_id'. func networksHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { panic("only networks GET operation implemented") } values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") systemId := values.Get("node") if op == "list_connected_macs" { networkListConnectedMACSHandler(server, w, r) return } if op != "" { panic("only list_connected_macs and default operations implemented") } if systemId == "" { panic("network missing associated node system id") } networks := []MAASObject{} if networkNames, hasNetworks := server.networksPerNode[systemId]; hasNetworks { networks = make([]MAASObject, len(networkNames)) for i, networkName := range networkNames { networks[i] = server.networks[networkName] } } res, err := json.MarshalIndent(networks, "", " ") checkError(err) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // ipAddressesHandler handles requests for '/api//ipaddresses/'. func ipAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { err := r.ParseForm() checkError(err) values := r.Form op := values.Get("op") switch r.Method { case "GET": if op != "" { panic("expected empty op for GET, got " + op) } listIPAddressesHandler(server, w, r) return case "POST": switch op { case "reserve": reserveIPAddressHandler(server, w, r, values.Get("network"), values.Get("requested_address")) return case "release": releaseIPAddressHandler(server, w, r, values.Get("ip")) return default: panic("expected op=release|reserve for POST, got " + op) } } http.NotFoundHandler().ServeHTTP(w, r) } func marshalIPAddress(server *TestServer, ipAddress string) (JSONObject, error) { jsonTemplate := `{"alloc_type": 4, "ip": %q, "resource_uri": %q, "created": %q}` uri := getIPAddressesEndpoint(server.version) now := time.Now().UTC().Format(time.RFC3339) bytes := []byte(fmt.Sprintf(jsonTemplate, ipAddress, uri, now)) return Parse(server.client, bytes) } func badRequestError(w http.ResponseWriter, err error) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, err.Error()) } func listIPAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { results := []MAASObject{} for _, ips := range server.IPAddresses() { for _, ip := range ips { jsonObj, err := marshalIPAddress(server, ip) if err != nil { badRequestError(w, err) return } maasObj, err := jsonObj.GetMAASObject() if err != nil { badRequestError(w, err) return } results = append(results, maasObj) } } res, err := json.MarshalIndent(results, "", " ") checkError(err) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } func reserveIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, network, reqAddress string) { _, ipNet, err := net.ParseCIDR(network) if err != nil { badRequestError(w, fmt.Errorf("Invalid network parameter %s", network)) return } if reqAddress != "" { // Validate "requested_address" parameter. reqIP := net.ParseIP(reqAddress) if reqIP == nil { badRequestError(w, fmt.Errorf("failed to detect a valid IP address from u'%s'", reqAddress)) return } if !ipNet.Contains(reqIP) { badRequestError(w, fmt.Errorf("%s is not inside the range %s", reqAddress, ipNet.String())) return } } // Find the network name matching the parsed CIDR. foundNetworkName := "" for netName, netObj := range server.networks { // Get the "ip" and "netmask" attributes of the network. netIP, err := netObj.GetField("ip") checkError(err) netMask, err := netObj.GetField("netmask") checkError(err) // Convert the netmask string to net.IPMask. parts := strings.Split(netMask, ".") ipMask := make(net.IPMask, len(parts)) for i, part := range parts { intPart, err := strconv.Atoi(part) checkError(err) ipMask[i] = byte(intPart) } netNet := &net.IPNet{IP: net.ParseIP(netIP), Mask: ipMask} if netNet.String() == network { // Exact match found. foundNetworkName = netName break } } if foundNetworkName == "" { badRequestError(w, fmt.Errorf("No network found matching %s", network)) return } ips, found := server.ipAddressesPerNetwork[foundNetworkName] if !found { // This will be the first address. ips = []string{} } reservedIP := "" if reqAddress != "" { // Use what the user provided. NOTE: Because this is testing // code, no duplicates check is done. reservedIP = reqAddress } else { // Generate an IP in the network range by incrementing the // last byte of the network's IP. firstIP := ipNet.IP firstIP[len(firstIP)-1] += byte(len(ips) + 1) reservedIP = firstIP.String() } ips = append(ips, reservedIP) server.ipAddressesPerNetwork[foundNetworkName] = ips jsonObj, err := marshalIPAddress(server, reservedIP) checkError(err) maasObj, err := jsonObj.GetMAASObject() checkError(err) res, err := json.MarshalIndent(maasObj, "", " ") checkError(err) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } func releaseIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, ip string) { if netIP := net.ParseIP(ip); netIP == nil { http.NotFoundHandler().ServeHTTP(w, r) return } if server.RemoveIPAddress(ip) { w.WriteHeader(http.StatusOK) return } http.NotFoundHandler().ServeHTTP(w, r) } // versionHandler handles requests for '/api//version/'. func versionHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { panic("only version GET operation implemented") } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, server.versionJSON) } // nodegroupsHandler handles requests for '/api//nodegroups/*'. func nodegroupsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") bootimagesURLRE := getBootimagesURLRE(server.version) bootimagesURLMatch := bootimagesURLRE.FindStringSubmatch(r.URL.Path) nodegroupsInterfacesURLRE := getNodegroupsInterfacesURLRE(server.version) nodegroupsInterfacesURLMatch := nodegroupsInterfacesURLRE.FindStringSubmatch(r.URL.Path) nodegroupsURL := getNodegroupsEndpoint(server.version) switch { case r.URL.Path == nodegroupsURL: nodegroupsTopLevelHandler(server, w, r, op) case bootimagesURLMatch != nil: bootimagesHandler(server, w, r, bootimagesURLMatch[1], op) case nodegroupsInterfacesURLMatch != nil: nodegroupsInterfacesHandler(server, w, r, nodegroupsInterfacesURLMatch[1], op) default: // Default handler: not found. http.NotFoundHandler().ServeHTTP(w, r) } } // nodegroupsTopLevelHandler handles requests for '/api//nodegroups/'. func nodegroupsTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) { if r.Method != "GET" || op != "list" { w.WriteHeader(http.StatusBadRequest) return } nodegroups := []JSONObject{} for uuid := range server.bootImages { attrs := map[string]interface{}{ "uuid": uuid, resourceURI: getNodegroupURL(server.version, uuid), } obj := maasify(server.client, attrs) nodegroups = append(nodegroups, obj) } res, err := json.MarshalIndent(nodegroups, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // bootimagesHandler handles requests for '/api//nodegroups//boot-images/'. func bootimagesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) { if r.Method != "GET" { w.WriteHeader(http.StatusBadRequest) return } bootImages, ok := server.bootImages[nodegroupUUID] if !ok { http.NotFoundHandler().ServeHTTP(w, r) return } res, err := json.MarshalIndent(bootImages, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // nodegroupsInterfacesHandler handles requests for '/api//nodegroups//interfaces/' func nodegroupsInterfacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) { if r.Method != "GET" { w.WriteHeader(http.StatusBadRequest) return } _, ok := server.bootImages[nodegroupUUID] if !ok { http.NotFoundHandler().ServeHTTP(w, r) return } interfaces, ok := server.nodegroupsInterfaces[nodegroupUUID] if !ok { // we already checked the nodegroup exists, so return an empty list interfaces = []JSONObject{} } res, err := json.MarshalIndent(interfaces, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // zonesHandler handles requests for '/api//zones/'. func zonesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { w.WriteHeader(http.StatusBadRequest) return } if len(server.zones) == 0 { // Until a zone is registered, behave as if the endpoint // does not exist. This way we can simulate older MAAS // servers that do not support zones. http.NotFoundHandler().ServeHTTP(w, r) return } zones := make([]JSONObject, 0, len(server.zones)) for _, zone := range server.zones { zones = append(zones, zone) } res, err := json.MarshalIndent(zones, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } // tagsHandler handles requests for '/api//tags/'. func tagsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { tagURLRE := getTagURLRE(server.version) tagURLMatch := tagURLRE.FindStringSubmatch(r.URL.Path) tagsURL := getTagsEndpoint(server.version) err := r.ParseForm() checkError(err) values := r.PostForm names, hasName := getValues(values, "name") quary, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := quary.Get("op") if r.URL.Path == tagsURL { if r.Method == "GET" { tags := make([]JSONObject, 0, len(server.zones)) for _, tag := range server.tags { tags = append(tags, tag) } res, err := json.MarshalIndent(tags, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } else if r.Method == "POST" && hasName { if op == "" || op == "new" { for _, name := range names { newTagHandler(server, w, r, name, values) } } else { w.WriteHeader(http.StatusBadRequest) } } else { w.WriteHeader(http.StatusBadRequest) } } else if tagURLMatch != nil { // Request for a single tag tagHandler(server, w, r, tagURLMatch[1], op, values) } else { http.NotFoundHandler().ServeHTTP(w, r) } } // newTagHandler creates, stores and returns new tag. func newTagHandler(server *TestServer, w http.ResponseWriter, r *http.Request, name string, values url.Values) { comment, hascomment := getValue(values, "comment") var attrs map[string]interface{} if hascomment { attrs = map[string]interface{}{ "name": name, "comment": comment, resourceURI: getTagURL(server.version, name), } } else { attrs = map[string]interface{}{ "name": name, resourceURI: getTagURL(server.version, name), } } obj := maasify(server.client, attrs) server.tags[name] = obj res, err := json.MarshalIndent(obj, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, res) } // tagHandler handles requests for '/api//tag//'. func tagHandler(server *TestServer, w http.ResponseWriter, r *http.Request, name string, operation string, values url.Values) { switch r.Method { case "GET": switch operation { case "node": var convertedNodes = []map[string]JSONObject{} for systemID, node := range server.nodes { for _, nodetag := range server.tagsPerNode[systemID] { if name == nodetag { convertedNodes = append(convertedNodes, node.GetMap()) } } } res, err := json.MarshalIndent(convertedNodes, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) default: res, err := json.MarshalIndent(server.tags[name], "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } case "POST": if operation == "update_nodes" { addNodes, hasAdd := getValues(values, "add") delNodes, hasRemove := getValues(values, "remove") addremovecount := map[string]int{"add": len(addNodes), "remove": len(delNodes)} if !hasAdd && !hasRemove { w.WriteHeader(http.StatusBadRequest) return } for _, systemID := range addNodes { _, ok := server.nodes[systemID] if !ok { w.WriteHeader(http.StatusBadRequest) return } var newTags []string for _, tag := range server.tagsPerNode[systemID] { if tag != name { newTags = append(newTags, tag) } } server.tagsPerNode[systemID] = append(newTags, name) newTagsObj := make([]JSONObject, len(server.tagsPerNode[systemID])) for i, tagsofnode := range server.tagsPerNode[systemID] { newTagsObj[i] = server.tags[tagsofnode] } tagNamesObj := JSONObject{ value: newTagsObj, } server.nodes[systemID].values["tag_names"] = tagNamesObj } for _, systemID := range delNodes { _, ok := server.nodes[systemID] if !ok { w.WriteHeader(http.StatusBadRequest) return } var newTags []string for _, tag := range server.tagsPerNode[systemID] { if tag != name { newTags = append(newTags, tag) } } server.tagsPerNode[systemID] = newTags newTagsObj := make([]JSONObject, len(server.tagsPerNode[systemID])) for i, tagsofnode := range server.tagsPerNode[systemID] { newTagsObj[i] = server.tags[tagsofnode] } tagNamesObj := JSONObject{ value: newTagsObj, } server.nodes[systemID].values["tag_names"] = tagNamesObj } res, err := json.MarshalIndent(addremovecount, "", " ") checkError(err) w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(res)) } case "PUT": newTagHandler(server, w, r, name, values) case "DELETE": delete(server.tags, name) w.WriteHeader(http.StatusOK) } } golang-github-juju-gomaasapi-2.2.0/testservice_spaces.go000066400000000000000000000066641451732172100234200ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "fmt" "io" "net/http" "net/url" "regexp" ) func getSpacesEndpoint(version string) string { return fmt.Sprintf("/api/%s/spaces/", version) } // TestSpace is the MAAS API space representation type TestSpace struct { Name string `json:"name"` Subnets []TestSubnet `json:"subnets"` ResourceURI string `json:"resource_uri"` ID uint `json:"id"` } // spacesHandler handles requests for '/api//spaces/'. func spacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") if op != "" { w.WriteHeader(http.StatusBadRequest) return } spacesURLRE := regexp.MustCompile(`/spaces/(.+?)/`) spacesURLMatch := spacesURLRE.FindStringSubmatch(r.URL.Path) spacesURL := getSpacesEndpoint(server.version) var ID uint var gotID bool if spacesURLMatch != nil { ID, err = NameOrIDToID(spacesURLMatch[1], server.spaceNameToID, 1, uint(len(server.spaces))) if err != nil { http.NotFoundHandler().ServeHTTP(w, r) return } gotID = true } switch r.Method { case "GET": w.Header().Set("Content-Type", "application/vnd.api+json") if len(server.spaces) == 0 { // Until a space is registered, behave as if the endpoint // does not exist. This way we can simulate older MAAS // servers that do not support spaces. http.NotFoundHandler().ServeHTTP(w, r) return } if r.URL.Path == spacesURL { var spaces []*TestSpace // Iterating by id rather than a dictionary iteration // preserves the order of the spaces in the result. for i := uint(1); i < server.nextSpace; i++ { s, ok := server.spaces[i] if ok { server.setSubnetsOnSpace(s) spaces = append(spaces, s) } } err = json.NewEncoder(w).Encode(spaces) } else if gotID == false { w.WriteHeader(http.StatusBadRequest) } else { err = json.NewEncoder(w).Encode(server.spaces[ID]) } checkError(err) case "POST": //server.NewSpace(r.Body) case "PUT": //server.UpdateSpace(r.Body) case "DELETE": delete(server.spaces, ID) w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusBadRequest) } } // CreateSpace is used to create new spaces on the server. type CreateSpace struct { Name string `json:"name"` } func decodePostedSpace(spaceJSON io.Reader) CreateSpace { var postedSpace CreateSpace decoder := json.NewDecoder(spaceJSON) err := decoder.Decode(&postedSpace) checkError(err) return postedSpace } // NewSpace creates a space in the test server func (server *TestServer) NewSpace(spaceJSON io.Reader) *TestSpace { postedSpace := decodePostedSpace(spaceJSON) newSpace := &TestSpace{Name: postedSpace.Name} newSpace.ID = server.nextSpace newSpace.ResourceURI = fmt.Sprintf("/api/%s/spaces/%d/", server.version, int(server.nextSpace)) server.spaces[server.nextSpace] = newSpace server.spaceNameToID[newSpace.Name] = newSpace.ID server.nextSpace++ return newSpace } // setSubnetsOnSpace fetches the subnets for the specified space and adds them // to it. func (server *TestServer) setSubnetsOnSpace(space *TestSpace) { subnets := []TestSubnet{} for i := uint(1); i < server.nextSubnet; i++ { subnet, ok := server.subnets[i] if ok && subnet.Space == space.Name { subnets = append(subnets, subnet) } } space.Subnets = subnets } golang-github-juju-gomaasapi-2.2.0/testservice_staticroutes.go000066400000000000000000000116341451732172100246640ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "fmt" "io" "net/http" "net/url" "regexp" ) func getStaticRoutesEndpoint(version string) string { return fmt.Sprintf("/api/%s/static-routes/", version) } // TestStaticRoute is the MAAS API Static Route representation type TestStaticRoute struct { Destination TestSubnet `json:"destination"` Source TestSubnet `json:"source"` Metric uint `json:"metric"` GatewayIP string `json:"gateway_ip"` ResourceURI string `json:"resource_uri"` ID uint `json:"id"` // These are internal bookkeeping, and not part of the public API, so // should not be in the JSON sourceCIDR string `json:"-"` destinationCIDR string `json:"-"` } // staticRoutesHandler handles requests for '/api//static-routes/'. func staticRoutesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") if op != "" { w.WriteHeader(http.StatusBadRequest) return } staticRoutesURLRE := regexp.MustCompile(`/static-routes/(.+?)/`) staticRoutesURLMatch := staticRoutesURLRE.FindStringSubmatch(r.URL.Path) staticRoutesURL := getStaticRoutesEndpoint(server.version) var ID uint var gotID bool if staticRoutesURLMatch != nil { // We pass a nil mapping, as static routes don't have names, but this gives // consistent integers and range checking. ID, err = NameOrIDToID(staticRoutesURLMatch[1], nil, 1, uint(len(server.staticRoutes))) if err != nil { http.NotFoundHandler().ServeHTTP(w, r) return } gotID = true } switch r.Method { case "GET": w.Header().Set("Content-Type", "application/vnd.api+json") if len(server.staticRoutes) == 0 { // Until a static-route is created, behave as if the endpoint // does not exist. This way we can simulate older MAAS // servers that do not support spaces. http.NotFoundHandler().ServeHTTP(w, r) return } if r.URL.Path == staticRoutesURL { var routes []*TestStaticRoute // Iterating by id rather than a dictionary iteration // preserves the order of the routes in the result. for i := uint(1); i < server.nextStaticRoute; i++ { route, ok := server.staticRoutes[i] if ok { server.setSubnetsOnStaticRoute(route) routes = append(routes, route) } } err = json.NewEncoder(w).Encode(routes) } else if gotID == false { w.WriteHeader(http.StatusBadRequest) } else { err = json.NewEncoder(w).Encode(server.staticRoutes[ID]) } checkError(err) case "POST": w.WriteHeader(http.StatusNotImplemented) // TODO(jam) 2017-02-23 we could probably wire this into creating a new // static route if we need the support. //server.NewStaticRoute(r.Body) case "PUT": w.WriteHeader(http.StatusNotImplemented) // TODO(jam): 2017-02-23 if we wanted to implement this, something like: //server.UpdateStaticRoute(r.Body) case "DELETE": delete(server.staticRoutes, ID) w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusBadRequest) } } // CreateStaticRoute is used to create new Static Routes on the server. type CreateStaticRoute struct { SourceCIDR string `json:"source"` DestinationCIDR string `json:"destination"` GatewayIP string `json:"gateway_ip"` Metric uint `json:"metric"` } func decodePostedStaticRoute(staticRouteJSON io.Reader) CreateStaticRoute { var postedStaticRoute CreateStaticRoute decoder := json.NewDecoder(staticRouteJSON) err := decoder.Decode(&postedStaticRoute) checkError(err) return postedStaticRoute } // NewStaticRoute creates a Static Route in the test server. func (server *TestServer) NewStaticRoute(staticRouteJSON io.Reader) *TestStaticRoute { postedStaticRoute := decodePostedStaticRoute(staticRouteJSON) // TODO(jam): 2017-02-03 Validate that sourceSubnet and destinationSubnet really do exist // sourceSubnet := blah // destinationSubnet := blah newStaticRoute := &TestStaticRoute{ destinationCIDR: postedStaticRoute.DestinationCIDR, sourceCIDR: postedStaticRoute.SourceCIDR, Metric: postedStaticRoute.Metric, GatewayIP: postedStaticRoute.GatewayIP, } newStaticRoute.ID = server.nextStaticRoute newStaticRoute.ResourceURI = fmt.Sprintf("/api/%s/static-routes/%d/", server.version, int(server.nextStaticRoute)) server.staticRoutes[server.nextStaticRoute] = newStaticRoute server.nextStaticRoute++ return newStaticRoute } // setSubnetsOnStaticRoutes fetches the subnets for the specified static route // and adds them to it. func (server *TestServer) setSubnetsOnStaticRoute(staticRoute *TestStaticRoute) { for i := uint(1); i < server.nextSubnet; i++ { subnet, ok := server.subnets[i] if ok { if subnet.CIDR == staticRoute.sourceCIDR { staticRoute.Source = subnet } else if subnet.CIDR == staticRoute.destinationCIDR { staticRoute.Destination = subnet } } } } golang-github-juju-gomaasapi-2.2.0/testservice_subnets.go000066400000000000000000000272451451732172100236230ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" "fmt" "io" "net" "net/http" "net/url" "regexp" "sort" "strings" ) func getSubnetsEndpoint(version string) string { return fmt.Sprintf("/api/%s/subnets/", version) } // CreateSubnet is used to receive new subnets via the MAAS API type CreateSubnet struct { DNSServers []string `json:"dns_servers"` Name string `json:"name"` Space string `json:"space"` GatewayIP string `json:"gateway_ip"` CIDR string `json:"cidr"` // VLAN this subnet belongs to. Currently ignored. // TODO: Defaults to the default VLAN // for the provided fabric or defaults to the default VLAN // in the default fabric. VLAN *uint `json:"vlan"` // Fabric for the subnet. Currently ignored. // TODO: Defaults to the fabric the provided // VLAN belongs to or defaults to the default fabric. Fabric *uint `json:"fabric"` // VID of the VLAN this subnet belongs to. Currently ignored. // TODO: Only used when vlan // is not provided. Picks the VLAN with this VID in the provided // fabric or the default fabric if one is not given. VID *uint `json:"vid"` // This is used for updates (PUT) and is ignored by create (POST) ID uint `json:"id"` } // TestSubnet is the MAAS API subnet representation type TestSubnet struct { DNSServers []string `json:"dns_servers"` Name string `json:"name"` Space string `json:"space"` VLAN TestVLAN `json:"vlan"` GatewayIP string `json:"gateway_ip"` CIDR string `json:"cidr"` ResourceURI string `json:"resource_uri"` ID uint `json:"id"` InUseIPAddresses []IP `json:"-"` FixedAddressRanges []AddressRange `json:"-"` } // AddFixedAddressRange adds an AddressRange to the list of fixed address ranges // that subnet stores. func (server *TestServer) AddFixedAddressRange(subnetID uint, ar AddressRange) { subnet := server.subnets[subnetID] ar.startUint = IPFromString(ar.Start).UInt64() ar.endUint = IPFromString(ar.End).UInt64() subnet.FixedAddressRanges = append(subnet.FixedAddressRanges, ar) server.subnets[subnetID] = subnet } // subnetsHandler handles requests for '/api//subnets/'. func subnetsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { var err error values, err := url.ParseQuery(r.URL.RawQuery) checkError(err) op := values.Get("op") includeRangesString := strings.ToLower(values.Get("include_ranges")) subnetsURLRE := regexp.MustCompile(`/subnets/(.+?)/`) subnetsURLMatch := subnetsURLRE.FindStringSubmatch(r.URL.Path) subnetsURL := getSubnetsEndpoint(server.version) var ID uint var gotID bool if subnetsURLMatch != nil { ID, err = NameOrIDToID(subnetsURLMatch[1], server.subnetNameToID, 1, uint(len(server.subnets))) if err != nil { http.NotFoundHandler().ServeHTTP(w, r) return } gotID = true } var includeRanges bool switch includeRangesString { case "true", "yes", "1": includeRanges = true } switch r.Method { case "GET": w.Header().Set("Content-Type", "application/vnd.api+json") if len(server.subnets) == 0 { // Until a subnet is registered, behave as if the endpoint // does not exist. This way we can simulate older MAAS // servers that do not support subnets. http.NotFoundHandler().ServeHTTP(w, r) return } if r.URL.Path == subnetsURL { var subnets []TestSubnet for i := uint(1); i < server.nextSubnet; i++ { s, ok := server.subnets[i] if ok { subnets = append(subnets, s) } } PrettyJsonWriter(subnets, w) } else if gotID == false { w.WriteHeader(http.StatusBadRequest) } else { switch op { case "unreserved_ip_ranges": PrettyJsonWriter(server.subnetUnreservedIPRanges(server.subnets[ID]), w) case "reserved_ip_ranges": PrettyJsonWriter(server.subnetReservedIPRanges(server.subnets[ID]), w) case "statistics": PrettyJsonWriter(server.subnetStatistics(server.subnets[ID], includeRanges), w) default: PrettyJsonWriter(server.subnets[ID], w) } } checkError(err) case "POST": server.NewSubnet(r.Body) case "PUT": server.UpdateSubnet(r.Body) case "DELETE": delete(server.subnets, ID) w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusBadRequest) } } type addressList []IP func (a addressList) Len() int { return len(a) } func (a addressList) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a addressList) Less(i, j int) bool { return a[i].UInt64() < a[j].UInt64() } // AddressRange is used to generate reserved IP address range lists type AddressRange struct { Start string `json:"start"` startUint uint64 End string `json:"end"` endUint uint64 Purpose []string `json:"purpose,omitempty"` NumAddresses uint `json:"num_addresses"` } // AddressRangeList is a list of AddressRange type AddressRangeList struct { ar []AddressRange } // Append appends a new AddressRange to an AddressRangeList func (ranges *AddressRangeList) Append(startIP, endIP IP) { var i AddressRange i.Start, i.End = startIP.String(), endIP.String() i.startUint, i.endUint = startIP.UInt64(), endIP.UInt64() i.NumAddresses = uint(1 + endIP.UInt64() - startIP.UInt64()) i.Purpose = startIP.Purpose ranges.ar = append(ranges.ar, i) } func appendRangesToIPList(subnet TestSubnet, ipAddresses *[]IP) { for _, r := range subnet.FixedAddressRanges { for v := r.startUint; v <= r.endUint; v++ { ip := IPFromInt64(v) ip.Purpose = r.Purpose *ipAddresses = append(*ipAddresses, ip) } } } func (server *TestServer) subnetUnreservedIPRanges(subnet TestSubnet) []AddressRange { // Make a sorted copy of subnet.InUseIPAddresses ipAddresses := make([]IP, len(subnet.InUseIPAddresses)) copy(ipAddresses, subnet.InUseIPAddresses) appendRangesToIPList(subnet, &ipAddresses) sort.Sort(addressList(ipAddresses)) // We need the first and last address in the subnet var ranges AddressRangeList var startIP, endIP, lastUsableIP IP _, ipNet, err := net.ParseCIDR(subnet.CIDR) checkError(err) startIP = IPFromNetIP(ipNet.IP) // Start with the lowest usable address in the range, which is 1 above // what net.ParseCIDR will give back. startIP.SetUInt64(startIP.UInt64() + 1) ones, bits := ipNet.Mask.Size() set := ^((^uint64(0)) << uint(bits-ones)) // The last usable address is one below the broadcast address, which is // what you get by bitwise ORing 'set' with any IP address in the subnet. lastUsableIP.SetUInt64((startIP.UInt64() | set) - 1) for _, endIP = range ipAddresses { end := endIP.UInt64() if endIP.UInt64() == startIP.UInt64() { if endIP.UInt64() != lastUsableIP.UInt64() { startIP.SetUInt64(end + 1) } continue } if end == lastUsableIP.UInt64() { continue } ranges.Append(startIP, IPFromInt64(end-1)) startIP.SetUInt64(end + 1) } if startIP.UInt64() != lastUsableIP.UInt64() { ranges.Append(startIP, lastUsableIP) } return ranges.ar } func (server *TestServer) subnetReservedIPRanges(subnet TestSubnet) []AddressRange { var ranges AddressRangeList var startIP, thisIP IP // Make a sorted copy of subnet.InUseIPAddresses ipAddresses := make([]IP, len(subnet.InUseIPAddresses)) copy(ipAddresses, subnet.InUseIPAddresses) appendRangesToIPList(subnet, &ipAddresses) sort.Sort(addressList(ipAddresses)) if len(ipAddresses) == 0 { ar := ranges.ar if ar == nil { ar = []AddressRange{} } return ar } startIP = ipAddresses[0] lastIP := ipAddresses[0] for _, thisIP = range ipAddresses { var purposeMissmatch bool for i, p := range thisIP.Purpose { if startIP.Purpose[i] != p { purposeMissmatch = true } } if (thisIP.UInt64() != lastIP.UInt64() && thisIP.UInt64() != lastIP.UInt64()+1) || purposeMissmatch { ranges.Append(startIP, lastIP) startIP = thisIP } lastIP = thisIP } if len(ranges.ar) == 0 || ranges.ar[len(ranges.ar)-1].endUint != lastIP.UInt64() { ranges.Append(startIP, lastIP) } return ranges.ar } // SubnetStats holds statistics about a subnet type SubnetStats struct { NumAvailable uint `json:"num_available"` LargestAvailable uint `json:"largest_available"` NumUnavailable uint `json:"num_unavailable"` TotalAddresses uint `json:"total_addresses"` Usage float32 `json:"usage"` UsageString string `json:"usage_string"` Ranges []AddressRange `json:"ranges"` } func (server *TestServer) subnetStatistics(subnet TestSubnet, includeRanges bool) SubnetStats { var stats SubnetStats _, ipNet, err := net.ParseCIDR(subnet.CIDR) checkError(err) ones, bits := ipNet.Mask.Size() stats.TotalAddresses = (1 << uint(bits-ones)) - 2 stats.NumUnavailable = uint(len(subnet.InUseIPAddresses)) stats.NumAvailable = stats.TotalAddresses - stats.NumUnavailable stats.Usage = float32(stats.NumUnavailable) / float32(stats.TotalAddresses) stats.UsageString = fmt.Sprintf("%0.1f%%", stats.Usage*100) // Calculate stats.LargestAvailable - the largest contiguous block of IP addresses available reserved := server.subnetUnreservedIPRanges(subnet) for _, addressRange := range reserved { if addressRange.NumAddresses > stats.LargestAvailable { stats.LargestAvailable = addressRange.NumAddresses } } if includeRanges { stats.Ranges = reserved } return stats } func decodePostedSubnet(subnetJSON io.Reader) CreateSubnet { var postedSubnet CreateSubnet decoder := json.NewDecoder(subnetJSON) err := decoder.Decode(&postedSubnet) checkError(err) if postedSubnet.DNSServers == nil { postedSubnet.DNSServers = []string{} } return postedSubnet } // UpdateSubnet creates a subnet in the test server func (server *TestServer) UpdateSubnet(subnetJSON io.Reader) TestSubnet { postedSubnet := decodePostedSubnet(subnetJSON) updatedSubnet := subnetFromCreateSubnet(postedSubnet) server.subnets[updatedSubnet.ID] = updatedSubnet return updatedSubnet } // NewSubnet creates a subnet in the test server func (server *TestServer) NewSubnet(subnetJSON io.Reader) *TestSubnet { postedSubnet := decodePostedSubnet(subnetJSON) newSubnet := subnetFromCreateSubnet(postedSubnet) newSubnet.ID = server.nextSubnet server.subnets[server.nextSubnet] = newSubnet server.subnetNameToID[newSubnet.Name] = newSubnet.ID server.nextSubnet++ return &newSubnet } // NodeNetworkInterface represents a network interface attached to a node type NodeNetworkInterface struct { Name string `json:"name"` Links []NetworkLink `json:"links"` } // Node represents a node type Node struct { SystemID string `json:"system_id"` Interfaces []NodeNetworkInterface `json:"interface_set"` } // NetworkLink represents a MAAS network link type NetworkLink struct { ID uint `json:"id"` Mode string `json:"mode"` Subnet *TestSubnet `json:"subnet"` } // SetNodeNetworkLink records that the given node + interface are in subnet func (server *TestServer) SetNodeNetworkLink(SystemID string, nodeNetworkInterface NodeNetworkInterface) { for i, ni := range server.nodeMetadata[SystemID].Interfaces { if ni.Name == nodeNetworkInterface.Name { server.nodeMetadata[SystemID].Interfaces[i] = nodeNetworkInterface return } } n := server.nodeMetadata[SystemID] n.Interfaces = append(n.Interfaces, nodeNetworkInterface) server.nodeMetadata[SystemID] = n } // subnetFromCreateSubnet creates a subnet in the test server func subnetFromCreateSubnet(postedSubnet CreateSubnet) TestSubnet { var newSubnet TestSubnet newSubnet.DNSServers = postedSubnet.DNSServers newSubnet.Name = postedSubnet.Name newSubnet.Space = postedSubnet.Space //TODO: newSubnet.VLAN = server.postedSubnetVLAN newSubnet.GatewayIP = postedSubnet.GatewayIP newSubnet.CIDR = postedSubnet.CIDR newSubnet.ID = postedSubnet.ID return newSubnet } golang-github-juju-gomaasapi-2.2.0/testservice_test.go000066400000000000000000002112601451732172100231070ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "math/rand" "mime/multipart" "net" "net/http" "net/url" "sort" "strconv" "strings" "github.com/juju/mgo/v2/bson" jc "github.com/juju/testing/checkers" . "gopkg.in/check.v1" ) type TestServerSuite struct { server *TestServer } var _ = Suite(&TestServerSuite{}) func (suite *TestServerSuite) SetUpTest(c *C) { server := NewTestServer("1.0") suite.server = server } func (suite *TestServerSuite) TearDownTest(c *C) { suite.server.Close() } func (suite *TestServerSuite) TestNewTestServerReturnsTestServer(c *C) { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusAccepted) } suite.server.serveMux.HandleFunc("/test/", handler) resp, err := http.Get(suite.server.Server.URL + "/test/") c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusAccepted) } func (suite *TestServerSuite) TestGetResourceURI(c *C) { c.Check(getNodeURL("0.1", "test"), Equals, "/api/0.1/nodes/test/") } func (suite *TestServerSuite) TestSetVersionJSON(c *C) { capabilities := `{"capabilities": ["networks-management","static-ipaddresses", "devices-management"]}` suite.server.SetVersionJSON(capabilities) url := fmt.Sprintf("/api/%s/version/", suite.server.version) resp, err := http.Get(suite.server.Server.URL + url) c.Assert(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) c.Assert(string(content), Equals, capabilities) } func (suite *TestServerSuite) createDevice(c *C, macs, hostname, parent string) string { devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=new" values := url.Values{} for _, mac := range strings.Split(macs, ",") { values.Add("mac_addresses", mac) } values.Add("hostname", hostname) values.Add("parent", parent) result := suite.post(c, devicesURL, values) resultMap, err := result.GetMap() c.Assert(err, IsNil) systemId, err := resultMap["system_id"].GetString() c.Assert(err, IsNil) return systemId } func getString(c *C, object map[string]JSONObject, key string) string { value, err := object[key].GetString() c.Assert(err, IsNil) return value } func (suite *TestServerSuite) post(c *C, url string, values url.Values) JSONObject { resp, err := http.Post(suite.server.Server.URL+url, "application/x-www-form-urlencoded", strings.NewReader(values.Encode())) c.Assert(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) result, err := Parse(suite.server.client, content) c.Assert(err, IsNil) return result } func (suite *TestServerSuite) get(c *C, url string) JSONObject { resp, err := http.Get(suite.server.Server.URL + url) c.Assert(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) result, err := Parse(suite.server.client, content) c.Assert(err, IsNil) return result } func checkDevice(c *C, device map[string]JSONObject, macs, hostname, parent string) { macSlice := strings.Split(macs, ",") macArray, err := device["macaddress_set"].GetArray() c.Assert(err, IsNil) c.Assert(macArray, HasLen, len(macSlice)) for i := range macArray { macMap, err := macArray[i].GetMap() c.Assert(err, IsNil) actualMac := getString(c, macMap, "mac_address") c.Check(actualMac, Equals, macSlice[i]) } actualParent := getString(c, device, "parent") c.Assert(actualParent, Equals, parent) actualHostname := getString(c, device, "hostname") c.Assert(actualHostname, Equals, hostname) } func (suite *TestServerSuite) TestReleaseIPPAddressFromDevice(c *C) { systemId := suite.createDevice(c, "foo", "bar", "baz") op := "?op=claim_sticky_ip_address" deviceURL := fmt.Sprintf("/api/%s/devices/%s/", suite.server.version, systemId) values := url.Values{} values.Add("requested_address", "127.0.0.1") suite.post(c, deviceURL+op, values) ipaddressesURL := fmt.Sprintf("/api/%s/ipaddresses/", suite.server.version) params := url.Values{"ip": []string{"127.0.0.1"}} suite.post(c, ipaddressesURL+"?op=release", params) } func (suite *TestServerSuite) TestNewDeviceRequiredParameters(c *C) { devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=new" values := url.Values{} values.Add("mac_addresses", "foo") values.Add("hostname", "bar") post := func(values url.Values) int { resp, err := http.Post(suite.server.Server.URL+devicesURL, "application/x-www-form-urlencoded", strings.NewReader(values.Encode())) c.Assert(err, IsNil) return resp.StatusCode } c.Check(post(values), Equals, http.StatusBadRequest) values.Del("hostname") values.Add("parent", "baz") c.Check(post(values), Equals, http.StatusBadRequest) values.Del("mac_addresses") values.Add("hostname", "bam") c.Check(post(values), Equals, http.StatusBadRequest) } func (suite *TestServerSuite) TestNewDevice(c *C) { devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=new" values := url.Values{} values.Add("mac_addresses", "foo") values.Add("hostname", "bar") values.Add("parent", "baz") result := suite.post(c, devicesURL, values) resultMap, err := result.GetMap() c.Assert(err, IsNil) macArray, err := resultMap["macaddress_set"].GetArray() c.Assert(err, IsNil) c.Assert(macArray, HasLen, 1) macMap, err := macArray[0].GetMap() c.Assert(err, IsNil) mac := getString(c, macMap, "mac_address") c.Assert(mac, Equals, "foo") parent := getString(c, resultMap, "parent") c.Assert(parent, Equals, "baz") hostname := getString(c, resultMap, "hostname") c.Assert(hostname, Equals, "bar") addresses, err := resultMap["ip_addresses"].GetArray() c.Assert(err, IsNil) c.Assert(addresses, HasLen, 0) systemId := getString(c, resultMap, "system_id") resourceURI := getString(c, resultMap, "resource_uri") c.Assert(resourceURI, Equals, fmt.Sprintf("/MAAS/api/%v/devices/%v/", suite.server.version, systemId)) } func (suite *TestServerSuite) TestGetDevice(c *C) { systemId := suite.createDevice(c, "foo", "bar", "baz") deviceURL := fmt.Sprintf("/api/%v/devices/%v/", suite.server.version, systemId) result := suite.get(c, deviceURL) resultMap, err := result.GetMap() c.Assert(err, IsNil) checkDevice(c, resultMap, "foo", "bar", "baz") actualId, err := resultMap["system_id"].GetString() c.Assert(actualId, Equals, systemId) } func (suite *TestServerSuite) TestGetDeviceWithMultipleMacs(c *C) { systemId := suite.createDevice(c, "foo,boo", "bar", "baz") deviceURL := fmt.Sprintf("/api/%v/devices/%v/", suite.server.version, systemId) result := suite.get(c, deviceURL) resultMap, err := result.GetMap() c.Assert(err, IsNil) checkDevice(c, resultMap, "foo,boo", "bar", "baz") actualId, err := resultMap["system_id"].GetString() c.Assert(actualId, Equals, systemId) } func (suite *TestServerSuite) TestDevicesList(c *C) { firstId := suite.createDevice(c, "foo", "bar", "baz") c.Assert(firstId, Not(Equals), "") secondId := suite.createDevice(c, "bam", "bing", "bong") c.Assert(secondId, Not(Equals), "") devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=list" result := suite.get(c, devicesURL) devicesArray, err := result.GetArray() c.Assert(err, IsNil) c.Assert(devicesArray, HasLen, 2) for _, device := range devicesArray { deviceMap, err := device.GetMap() c.Assert(err, IsNil) systemId, err := deviceMap["system_id"].GetString() c.Assert(err, IsNil) switch systemId { case firstId: checkDevice(c, deviceMap, "foo", "bar", "baz") case secondId: checkDevice(c, deviceMap, "bam", "bing", "bong") default: c.Fatalf("unknown system id %q", systemId) } } } func (suite *TestServerSuite) TestDevicesListMacFiltering(c *C) { firstId := suite.createDevice(c, "foo", "bar", "baz") c.Assert(firstId, Not(Equals), "") secondId := suite.createDevice(c, "bam", "bing", "bong") c.Assert(secondId, Not(Equals), "") op := fmt.Sprintf("?op=list&mac_address=%v", "foo") devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + op result := suite.get(c, devicesURL) devicesArray, err := result.GetArray() c.Assert(err, IsNil) c.Assert(devicesArray, HasLen, 1) deviceMap, err := devicesArray[0].GetMap() c.Assert(err, IsNil) checkDevice(c, deviceMap, "foo", "bar", "baz") } func (suite *TestServerSuite) TestDevicesListMacFilteringMultipleAddresses(c *C) { firstId := suite.createDevice(c, "foo,boo", "bar", "baz") c.Assert(firstId, Not(Equals), "") secondId := suite.createDevice(c, "bam,boom", "bing", "bong") c.Assert(secondId, Not(Equals), "") op := "?op=list&mac_address=foo&mac_address=boo" devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + op result := suite.get(c, devicesURL) devicesArray, err := result.GetArray() c.Assert(err, IsNil) c.Assert(devicesArray, HasLen, 2) deviceMap, err := devicesArray[0].GetMap() c.Assert(err, IsNil) checkDevice(c, deviceMap, "foo,boo", "bar", "baz") deviceMap, err = devicesArray[1].GetMap() c.Assert(err, IsNil) checkDevice(c, deviceMap, "foo,boo", "bar", "baz") } func (suite *TestServerSuite) TestDeviceClaimStickyIPRequiresAddress(c *C) { systemId := suite.createDevice(c, "foo", "bar", "baz") op := "?op=claim_sticky_ip_address" deviceURL := fmt.Sprintf("/api/%s/devices/%s/%s", suite.server.version, systemId, op) values := url.Values{} resp, err := http.Post(suite.server.Server.URL+deviceURL, "application/x-www-form-urlencoded", strings.NewReader(values.Encode())) c.Assert(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) } func (suite *TestServerSuite) TestDeviceClaimStickyIP(c *C) { systemId := suite.createDevice(c, "foo", "bar", "baz") op := "?op=claim_sticky_ip_address" deviceURL := fmt.Sprintf("/api/%s/devices/%s/", suite.server.version, systemId) values := url.Values{} values.Add("requested_address", "127.0.0.1") result := suite.post(c, deviceURL+op, values) resultMap, err := result.GetMap() c.Assert(err, IsNil) addresses, err := resultMap["ip_addresses"].GetArray() c.Assert(err, IsNil) c.Assert(addresses, HasLen, 1) address, err := addresses[0].GetString() c.Assert(err, IsNil) c.Assert(address, Equals, "127.0.0.1") } func (suite *TestServerSuite) TestDeleteDevice(c *C) { systemId := suite.createDevice(c, "foo", "bar", "baz") deviceURL := fmt.Sprintf("/api/%s/devices/%s/", suite.server.version, systemId) req, err := http.NewRequest("DELETE", suite.server.Server.URL+deviceURL, nil) c.Assert(err, IsNil) resp, err := http.DefaultClient.Do(req) c.Assert(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusNoContent) resp, err = http.Get(suite.server.Server.URL + deviceURL) c.Assert(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestInvalidOperationOnNodesIsBadRequest(c *C) { badURL := getNodesEndpoint(suite.server.version) + "?op=procrastinate" response, err := http.Get(suite.server.Server.URL + badURL) c.Assert(err, IsNil) c.Check(response.StatusCode, Equals, http.StatusBadRequest) } func (suite *TestServerSuite) TestHandlesNodeListingUnknownPath(c *C) { invalidPath := fmt.Sprintf("/api/%s/nodes/invalid/path/", suite.server.version) resp, err := http.Get(suite.server.Server.URL + invalidPath) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestHandlesNodegroupsInterfacesListingUnknownNodegroup(c *C) { invalidPath := fmt.Sprintf("/api/%s/nodegroups/unknown/interfaces/", suite.server.version) resp, err := http.Get(suite.server.Server.URL + invalidPath) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestNewNode(c *C) { input := `{"system_id": "mysystemid"}` newNode := suite.server.NewNode(input) c.Check(len(suite.server.nodes), Equals, 1) c.Check(suite.server.nodes["mysystemid"], DeepEquals, newNode) } func (suite *TestServerSuite) TestNodesReturnsNodes(c *C) { input := `{"system_id": "mysystemid"}` newNode := suite.server.NewNode(input) nodesMap := suite.server.Nodes() c.Check(len(nodesMap), Equals, 1) c.Check(nodesMap["mysystemid"], DeepEquals, newNode) } func (suite *TestServerSuite) TestChangeNode(c *C) { input := `{"system_id": "mysystemid"}` suite.server.NewNode(input) suite.server.ChangeNode("mysystemid", "newfield", "newvalue") node, _ := suite.server.nodes["mysystemid"] field, err := node.GetField("newfield") c.Assert(err, IsNil) c.Check(field, Equals, "newvalue") } func (suite *TestServerSuite) TestClearClearsData(c *C) { input := `{"system_id": "mysystemid"}` suite.server.NewNode(input) suite.server.addNodeOperation("mysystemid", "start", &http.Request{}) suite.server.Clear() c.Check(len(suite.server.nodes), Equals, 0) c.Check(len(suite.server.nodeOperations), Equals, 0) c.Check(len(suite.server.nodeOperationRequestValues), Equals, 0) } func (suite *TestServerSuite) TestAddNodeOperationPopulatesOperations(c *C) { input := `{"system_id": "mysystemid"}` suite.server.NewNode(input) suite.server.addNodeOperation("mysystemid", "start", &http.Request{}) suite.server.addNodeOperation("mysystemid", "stop", &http.Request{}) nodeOperations := suite.server.NodeOperations() operations := nodeOperations["mysystemid"] c.Check(operations, DeepEquals, []string{"start", "stop"}) } func (suite *TestServerSuite) TestAddNodeOperationPopulatesOperationRequestValues(c *C) { input := `{"system_id": "mysystemid"}` suite.server.NewNode(input) reader := strings.NewReader("key=value") request, err := http.NewRequest("POST", "http://example.com/", reader) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") c.Assert(err, IsNil) suite.server.addNodeOperation("mysystemid", "start", request) values := suite.server.NodeOperationRequestValues() value := values["mysystemid"] c.Check(len(value), Equals, 1) c.Check(value[0], DeepEquals, url.Values{"key": []string{"value"}}) } func (suite *TestServerSuite) TestNewNodeRequiresJSONString(c *C) { input := `invalid:json` defer func() { recoveredError := recover().(*json.SyntaxError) c.Check(recoveredError, NotNil) c.Check(recoveredError.Error(), Matches, ".*invalid character.*") }() suite.server.NewNode(input) } func (suite *TestServerSuite) TestNewNodeRequiresSystemIdKey(c *C) { input := `{"test": "test"}` defer func() { recoveredError := recover() c.Check(recoveredError, NotNil) c.Check(recoveredError, Matches, ".*does not contain a 'system_id' value.") }() suite.server.NewNode(input) } func (suite *TestServerSuite) TestHandlesNodeRequestNotFound(c *C) { getURI := fmt.Sprintf("/api/%s/nodes/test/", suite.server.version) resp, err := http.Get(suite.server.Server.URL + getURI) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestHandlesNodeUnknownOperation(c *C) { input := `{"system_id": "mysystemid"}` suite.server.NewNode(input) postURI := fmt.Sprintf("/api/%s/nodes/mysystemid/?op=unknown/", suite.server.version) respStart, err := http.Post(suite.server.Server.URL+postURI, "", nil) c.Check(err, IsNil) c.Check(respStart.StatusCode, Equals, http.StatusBadRequest) } func (suite *TestServerSuite) TestHandlesNodeDelete(c *C) { input := `{"system_id": "mysystemid"}` suite.server.NewNode(input) deleteURI := fmt.Sprintf("/api/%s/nodes/mysystemid/?op=mysystemid", suite.server.version) req, err := http.NewRequest("DELETE", suite.server.Server.URL+deleteURI, nil) var client http.Client resp, err := client.Do(req) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) c.Check(len(suite.server.nodes), Equals, 0) } func uploadTo(url, fileName string, fileContent []byte) (*http.Response, error) { buf := new(bytes.Buffer) w := multipart.NewWriter(buf) fw, err := w.CreateFormFile(fileName, fileName) if err != nil { panic(err) } io.Copy(fw, bytes.NewBuffer(fileContent)) w.Close() req, err := http.NewRequest("POST", url, buf) if err != nil { panic(err) } req.Header.Set("Content-Type", w.FormDataContentType()) client := &http.Client{} return client.Do(req) } func (suite *TestServerSuite) TestHandlesUploadFile(c *C) { fileContent := []byte("test file content") postURL := suite.server.Server.URL + fmt.Sprintf("/api/%s/files/?op=add&filename=filename", suite.server.version) resp, err := uploadTo(postURL, "upload", fileContent) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) c.Check(len(suite.server.files), Equals, 1) file, ok := suite.server.files["filename"] c.Assert(ok, Equals, true) field, err := file.GetField("content") c.Assert(err, IsNil) c.Check(field, Equals, base64.StdEncoding.EncodeToString(fileContent)) } func (suite *TestServerSuite) TestNewFileEscapesName(c *C) { obj := suite.server.NewFile("aa?bb", []byte("bytes")) resourceURI := obj.URI() c.Check(strings.Contains(resourceURI.String(), "aa?bb"), Equals, false) c.Check(strings.Contains(resourceURI.Path, "aa?bb"), Equals, true) anonURI, err := obj.GetField("anon_resource_uri") c.Assert(err, IsNil) c.Check(strings.Contains(anonURI, "aa?bb"), Equals, false) c.Check(strings.Contains(anonURI, url.QueryEscape("aa?bb")), Equals, true) } func (suite *TestServerSuite) TestHandlesFile(c *C) { const filename = "my-file" const fileContent = "test file content" file := suite.server.NewFile(filename, []byte(fileContent)) getURI := fmt.Sprintf("/api/%s/files/%s/", suite.server.version, filename) fileURI, err := file.GetField("anon_resource_uri") c.Assert(err, IsNil) resp, err := http.Get(suite.server.Server.URL + getURI) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) var obj map[string]interface{} err = json.Unmarshal(content, &obj) c.Assert(err, IsNil) anon_url, ok := obj["anon_resource_uri"] c.Check(ok, Equals, true) c.Check(anon_url.(string), Equals, fileURI) base64Content, ok := obj["content"] c.Check(ok, Equals, true) decodedContent, err := base64.StdEncoding.DecodeString(base64Content.(string)) c.Assert(err, IsNil) c.Check(string(decodedContent), Equals, fileContent) } func (suite *TestServerSuite) TestHandlesGetFile(c *C) { fileContent := []byte("test file content") fileName := "filename" suite.server.NewFile(fileName, fileContent) getURI := fmt.Sprintf("/api/%s/files/?op=get&filename=filename", suite.server.version) resp, err := http.Get(suite.server.Server.URL + getURI) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Check(err, IsNil) c.Check(string(content), Equals, string(fileContent)) c.Check(content, DeepEquals, fileContent) } func (suite *TestServerSuite) TestHandlesListReturnsSortedFilenames(c *C) { fileName1 := "filename1" suite.server.NewFile(fileName1, []byte("test file content")) fileName2 := "filename2" suite.server.NewFile(fileName2, []byte("test file content")) getURI := fmt.Sprintf("/api/%s/files/?op=list", suite.server.version) resp, err := http.Get(suite.server.Server.URL + getURI) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) var files []map[string]string err = json.Unmarshal(content, &files) c.Assert(err, IsNil) c.Check(len(files), Equals, 2) c.Check(files[0]["filename"], Equals, fileName1) c.Check(files[1]["filename"], Equals, fileName2) } func (suite *TestServerSuite) TestHandlesListFiltersFiles(c *C) { fileName1 := "filename1" suite.server.NewFile(fileName1, []byte("test file content")) fileName2 := "prefixFilename" suite.server.NewFile(fileName2, []byte("test file content")) getURI := fmt.Sprintf("/api/%s/files/?op=list&prefix=prefix", suite.server.version) resp, err := http.Get(suite.server.Server.URL + getURI) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) var files []map[string]string err = json.Unmarshal(content, &files) c.Assert(err, IsNil) c.Check(len(files), Equals, 1) c.Check(files[0]["filename"], Equals, fileName2) } func (suite *TestServerSuite) TestHandlesListOmitsContent(c *C) { const filename = "myfile" fileContent := []byte("test file content") suite.server.NewFile(filename, fileContent) getURI := fmt.Sprintf("/api/%s/files/?op=list", suite.server.version) resp, err := http.Get(suite.server.Server.URL + getURI) c.Assert(err, IsNil) content, err := readAndClose(resp.Body) c.Assert(err, IsNil) var files []map[string]string err = json.Unmarshal(content, &files) // The resulting dict does not have a "content" entry. file := files[0] _, ok := file["content"] c.Check(ok, Equals, false) // But the original as stored in the test service still has it. contentAfter, err := suite.server.files[filename].GetField("content") c.Assert(err, IsNil) bytes, err := base64.StdEncoding.DecodeString(contentAfter) c.Assert(err, IsNil) c.Check(string(bytes), Equals, string(fileContent)) } func (suite *TestServerSuite) TestDeleteFile(c *C) { fileName1 := "filename1" suite.server.NewFile(fileName1, []byte("test file content")) deleteURI := fmt.Sprintf("/api/%s/files/filename1/", suite.server.version) req, err := http.NewRequest("DELETE", suite.server.Server.URL+deleteURI, nil) c.Check(err, IsNil) var client http.Client resp, err := client.Do(req) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) c.Check(suite.server.Files(), DeepEquals, map[string]MAASObject{}) } func (suite *TestServerSuite) TestListZonesNotSupported(c *C) { // Older versions of MAAS do not support zones. We simulate // this behaviour by returning 404 if no zones are defined. zonesURL := getZonesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + zonesURL) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestSpacesNotFoundWhenEmpty(c *C) { spacesURL := getSpacesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + spacesURL) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestSpacesWithOp(c *C) { spacesURL := getSpacesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + spacesURL + "?op=list") c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusBadRequest) } func (suite *TestServerSuite) TestSpacesSubnetsEmptyNotNil(c *C) { suite.server.NewSpace(spaceJSON(CreateSpace{Name: "foo"})) spacesURL := getSpacesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + spacesURL) c.Check(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusOK) var spaces []TestSpace decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&spaces) c.Assert(err, IsNil) c.Assert(spaces, HasLen, 1) c.Assert(spaces[0].Subnets, NotNil) } func (suite *TestServerSuite) TestSpaces(c *C) { for i, name := range []string{"foo", "bar", "bam"} { space := suite.server.NewSpace(spaceJSON(CreateSpace{Name: name})) c.Assert(space.Name, Equals, name) c.Assert(space.ID, Equals, uint(i+1)) c.Assert(space.ResourceURI, Equals, fmt.Sprintf("/api/%s/spaces/%d/", suite.server.version, i+1)) } sub1 := suite.server.NewSubnet(subnetJSON(newSubnetOnSpace("foo", 1))) sub2 := suite.server.NewSubnet(subnetJSON(newSubnetOnSpace("foo", 2))) sub3 := suite.server.NewSubnet(subnetJSON(newSubnetOnSpace("foo", 3))) sub4 := suite.server.NewSubnet(subnetJSON(newSubnetOnSpace("bar", 4))) sub5 := suite.server.NewSubnet(subnetJSON(newSubnetOnSpace("bar", 5))) suite.server.NewSubnet(subnetJSON(newSubnetOnSpace("baz", 6))) spacesURL := getSpacesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + spacesURL) c.Check(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusOK) var spaces []TestSpace decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&spaces) c.Assert(err, IsNil) getURI := func(id int) string { return fmt.Sprintf("/api/%s/spaces/%d/", suite.server.version, id) } expectedSpaces := []TestSpace{ {Name: "foo", ID: 1, Subnets: []TestSubnet{*sub1, *sub2, *sub3}, ResourceURI: getURI(1)}, {Name: "bar", ID: 2, Subnets: []TestSubnet{*sub4, *sub5}, ResourceURI: getURI(2)}, {Name: "bam", ID: 3, Subnets: []TestSubnet{}, ResourceURI: getURI(3)}, } c.Assert(spaces, DeepEquals, expectedSpaces) } func defaultSubnet() CreateSubnet { var s CreateSubnet s.DNSServers = []string{"192.168.1.2"} s.Name = "maas-eth0" s.Space = "space-0" s.GatewayIP = "192.168.1.1" s.CIDR = "192.168.1.0/24" s.ID = 1 return s } func extraSubnet() CreateSubnet { var s CreateSubnet s.DNSServers = []string{"192.168.1.2"} s.Name = "maas-all-192" s.Space = "space-0" s.GatewayIP = "" s.CIDR = "192.168.0.0/16" s.ID = 2 return s } func newSubnetOnSpace(space string, id uint) CreateSubnet { var s CreateSubnet s.DNSServers = []string{fmt.Sprintf("192.168.%v.2", id)} s.Name = fmt.Sprintf("maas-eth%v", id) s.Space = space s.GatewayIP = fmt.Sprintf("192.168.%v.1", id) s.CIDR = fmt.Sprintf("192.168.%v.0/24", id) s.ID = id return s } func spaceJSON(space CreateSpace) *bytes.Buffer { var out bytes.Buffer err := json.NewEncoder(&out).Encode(space) if err != nil { panic(err) } return &out } func subnetJSON(subnet CreateSubnet) *bytes.Buffer { var out bytes.Buffer err := json.NewEncoder(&out).Encode(subnet) if err != nil { panic(err) } return &out } func staticRouteJSON(staticRoute CreateStaticRoute) *bytes.Buffer { var out bytes.Buffer err := json.NewEncoder(&out).Encode(staticRoute) if err != nil { panic(err) } return &out } func (suite *TestServerSuite) subnetURL(ID int) string { return suite.subnetsURL() + strconv.Itoa(ID) + "/" } func (suite *TestServerSuite) subnetsURL() string { return suite.server.Server.URL + getSubnetsEndpoint(suite.server.version) } func (suite *TestServerSuite) getSubnets(c *C) []TestSubnet { resp, err := http.Get(suite.subnetsURL()) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) var subnets []TestSubnet decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&subnets) c.Check(err, IsNil) return subnets } func (suite *TestServerSuite) TestSubnetAdd(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) subnets := suite.getSubnets(c) c.Check(subnets, HasLen, 1) s := subnets[0] c.Check(s.DNSServers, DeepEquals, []string{"192.168.1.2"}) c.Check(s.Name, Equals, "maas-eth0") c.Check(s.Space, Equals, "space-0") c.Check(s.VLAN.ID, Equals, uint(0)) c.Check(s.CIDR, Equals, "192.168.1.0/24") } func (suite *TestServerSuite) TestSubnetGet(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) subnet2 := defaultSubnet() subnet2.Name = "maas-eth1" subnet2.CIDR = "192.168.2.0/24" suite.server.NewSubnet(subnetJSON(subnet2)) subnets := suite.getSubnets(c) c.Check(subnets, HasLen, 2) c.Check(subnets[0].CIDR, Equals, "192.168.1.0/24") c.Check(subnets[1].CIDR, Equals, "192.168.2.0/24") } func (suite *TestServerSuite) TestSubnetPut(c *C) { subnet1 := defaultSubnet() suite.server.NewSubnet(subnetJSON(subnet1)) subnets := suite.getSubnets(c) c.Check(subnets, HasLen, 1) c.Check(subnets[0].DNSServers, DeepEquals, []string{"192.168.1.2"}) subnet1.DNSServers = []string{"192.168.1.2", "192.168.1.3"} suite.server.UpdateSubnet(subnetJSON(subnet1)) subnets = suite.getSubnets(c) c.Check(subnets, HasLen, 1) c.Check(subnets[0].DNSServers, DeepEquals, []string{"192.168.1.2", "192.168.1.3"}) } func (suite *TestServerSuite) TestSubnetDelete(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) subnets := suite.getSubnets(c) c.Check(subnets, HasLen, 1) c.Check(subnets[0].DNSServers, DeepEquals, []string{"192.168.1.2"}) req, err := http.NewRequest("DELETE", suite.subnetURL(1), nil) c.Check(err, IsNil) resp, err := http.DefaultClient.Do(req) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusOK) resp, err = http.Get(suite.subnetsURL()) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) reserveSomeAddresses() map[int]bool { reserved := make(map[int]bool) rand.Seed(6) // Insert some random test data for i := 0; i < 200; i++ { r := rand.Intn(253) + 1 _, ok := reserved[r] for ok == true { r++ if r == 255 { r = 1 } _, ok = reserved[r] } reserved[r] = true addr := fmt.Sprintf("192.168.1.%d", r) suite.server.NewIPAddress(addr, "maas-eth0") } return reserved } func (suite *TestServerSuite) TestSubnetReservedIPRanges(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) reserved := suite.reserveSomeAddresses() // Fetch from the server reservedIPRangeURL := suite.subnetURL(1) + "?op=reserved_ip_ranges" resp, err := http.Get(reservedIPRangeURL) c.Check(err, IsNil) var reservedFromAPI []AddressRange decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&reservedFromAPI) c.Check(err, IsNil) // Check that anything in a reserved range was an address we allocated // with NewIPAddress for _, addressRange := range reservedFromAPI { var start, end int fmt.Sscanf(addressRange.Start, "192.168.1.%d", &start) fmt.Sscanf(addressRange.End, "192.168.1.%d", &end) c.Check(addressRange.NumAddresses, Equals, uint(1+end-start)) c.Check(start <= end, Equals, true) c.Check(start < 255, Equals, true) c.Check(end < 255, Equals, true) for i := start; i <= end; i++ { _, ok := reserved[int(i)] c.Check(ok, Equals, true) delete(reserved, int(i)) } } c.Check(reserved, HasLen, 0) } func (suite *TestServerSuite) TestSubnetUnreservedIPRanges(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) reserved := suite.reserveSomeAddresses() unreserved := make(map[int]bool) // Fetch from the server reservedIPRangeURL := suite.subnetURL(1) + "?op=unreserved_ip_ranges" resp, err := http.Get(reservedIPRangeURL) c.Check(err, IsNil) var unreservedFromAPI []AddressRange decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&unreservedFromAPI) c.Check(err, IsNil) // Check that anything in an unreserved range wasn't an address we allocated // with NewIPAddress for _, addressRange := range unreservedFromAPI { var start, end int fmt.Sscanf(addressRange.Start, "192.168.1.%d", &start) fmt.Sscanf(addressRange.End, "192.168.1.%d", &end) c.Check(addressRange.NumAddresses, Equals, uint(1+end-start)) c.Check(start <= end, Equals, true) c.Check(start < 255, Equals, true) c.Check(end < 255, Equals, true) for i := start; i <= end; i++ { _, ok := reserved[int(i)] c.Check(ok, Equals, false) unreserved[int(i)] = true } } for i := 1; i < 255; i++ { _, r := reserved[i] _, u := unreserved[i] if (r || u) == false { fmt.Println(i, r, u) } c.Check(r || u, Equals, true) } c.Check(len(reserved)+len(unreserved), Equals, 254) } func (suite *TestServerSuite) TestSubnetReserveRange(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) suite.server.NewIPAddress("192.168.1.10", "maas-eth0") var ar AddressRange ar.Start = "192.168.1.100" ar.End = "192.168.1.200" ar.Purpose = []string{"dynamic"} suite.server.AddFixedAddressRange(1, ar) // Fetch from the server reservedIPRangeURL := suite.subnetURL(1) + "?op=reserved_ip_ranges" resp, err := http.Get(reservedIPRangeURL) c.Check(err, IsNil) var reservedFromAPI []AddressRange decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&reservedFromAPI) c.Check(err, IsNil) // Check that the address ranges we got back were as expected addressRange := reservedFromAPI[0] c.Check(addressRange.Start, Equals, "192.168.1.10") c.Check(addressRange.End, Equals, "192.168.1.10") c.Check(addressRange.NumAddresses, Equals, uint(1)) c.Check(addressRange.Purpose[0], Equals, "assigned-ip") c.Check(addressRange.Purpose, HasLen, 1) addressRange = reservedFromAPI[1] c.Check(addressRange.Start, Equals, "192.168.1.100") c.Check(addressRange.End, Equals, "192.168.1.200") c.Check(addressRange.NumAddresses, Equals, uint(101)) c.Check(addressRange.Purpose[0], Equals, "dynamic") c.Check(addressRange.Purpose, HasLen, 1) } func (suite *TestServerSuite) getSubnetStats(c *C, subnetID int) SubnetStats { URL := suite.subnetURL(1) + "?op=statistics" resp, err := http.Get(URL) c.Check(err, IsNil) var s SubnetStats decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&s) c.Check(err, IsNil) return s } func (suite *TestServerSuite) TestSubnetStats(c *C) { suite.server.NewSubnet(subnetJSON(defaultSubnet())) stats := suite.getSubnetStats(c, 1) // There are 254 usable addresses in a class C subnet, so these // stats are fixed expected := SubnetStats{ NumAvailable: 254, LargestAvailable: 254, NumUnavailable: 0, TotalAddresses: 254, Usage: 0, UsageString: "0.0%", Ranges: nil, } c.Check(stats, DeepEquals, expected) suite.reserveSomeAddresses() stats = suite.getSubnetStats(c, 1) // We have reserved 200 addresses so parts of these // stats are fixed. expected = SubnetStats{ NumAvailable: 54, NumUnavailable: 200, TotalAddresses: 254, Usage: 0.787401556968689, UsageString: "78.7%", Ranges: nil, } reserved := suite.server.subnetUnreservedIPRanges(suite.server.subnets[1]) var largestAvailable uint for _, addressRange := range reserved { if addressRange.NumAddresses > largestAvailable { largestAvailable = addressRange.NumAddresses } } expected.LargestAvailable = largestAvailable c.Check(stats, DeepEquals, expected) } func (suite *TestServerSuite) TestSubnetsInNodes(c *C) { // Create a subnet subnet := suite.server.NewSubnet(subnetJSON(defaultSubnet())) // Create a node var node Node node.SystemID = "node-89d832ca-8877-11e5-b5a5-00163e86022b" suite.server.NewNode(fmt.Sprintf(`{"system_id": "%s"}`, "node-89d832ca-8877-11e5-b5a5-00163e86022b")) // Put the node in the subnet var nni NodeNetworkInterface nni.Name = "eth0" nni.Links = append(nni.Links, NetworkLink{uint(1), "auto", subnet}) suite.server.SetNodeNetworkLink(node.SystemID, nni) // Fetch the node details URL := suite.server.Server.URL + getNodesEndpoint(suite.server.version) + node.SystemID + "/" resp, err := http.Get(URL) c.Check(err, IsNil) var n Node decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&n) c.Check(err, IsNil) c.Check(n.SystemID, Equals, node.SystemID) c.Check(n.Interfaces, HasLen, 1) i := n.Interfaces[0] c.Check(i.Name, Equals, "eth0") c.Check(i.Links, HasLen, 1) c.Check(i.Links[0].ID, Equals, uint(1)) c.Check(i.Links[0].Subnet.Name, Equals, "maas-eth0") } func (suite *TestServerSuite) TestStaticRoutesNotFoundWhenEmpty(c *C) { staticRoutesURL := getStaticRoutesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + staticRoutesURL) c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusNotFound) } func (suite *TestServerSuite) TestStaticRoutesWithOp(c *C) { staticRouteURL := getStaticRoutesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + staticRouteURL + "?op=list") c.Check(err, IsNil) c.Check(resp.StatusCode, Equals, http.StatusBadRequest) } func (suite *TestServerSuite) TestStaticRoutesSubnetsFilledIn(c *C) { subnetSource := suite.server.NewSubnet(subnetJSON(defaultSubnet())) subnetDestination := suite.server.NewSubnet(subnetJSON(extraSubnet())) suite.server.NewStaticRoute(staticRouteJSON(CreateStaticRoute{ SourceCIDR: subnetSource.CIDR, DestinationCIDR: subnetDestination.CIDR, GatewayIP: subnetSource.GatewayIP, Metric: 100, })) staticRoutesURL := getStaticRoutesEndpoint(suite.server.version) resp, err := http.Get(suite.server.Server.URL + staticRoutesURL) c.Check(err, IsNil) c.Assert(resp.StatusCode, Equals, http.StatusOK) var staticRoutes []TestStaticRoute decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&staticRoutes) c.Assert(err, IsNil) c.Assert(staticRoutes, HasLen, 1) c.Assert(staticRoutes[0].Source, NotNil) c.Assert(staticRoutes[0].Source, DeepEquals, *subnetSource) c.Assert(staticRoutes[0].Destination, NotNil) c.Assert(staticRoutes[0].Destination, DeepEquals, *subnetDestination) } type IPSuite struct { } var _ = Suite(&IPSuite{}) func (suite *IPSuite) TestIPFromNetIP(c *C) { ip := IPFromNetIP(net.ParseIP("1.2.3.4")) c.Check(ip.String(), Equals, "1.2.3.4") } func (suite *IPSuite) TestIPUInt64(c *C) { ip := IPFromNetIP(net.ParseIP("1.2.3.4")) v := ip.UInt64() c.Check(v, Equals, uint64(0x01020304)) } func (suite *IPSuite) TestIPSetUInt64(c *C) { var ip IP ip.SetUInt64(0x01020304) c.Check(ip.String(), Equals, "1.2.3.4") } // TestMAASObjectSuite validates that the object created by // NewTestMAAS can be used by the gomaasapi library as if it were a real // MAAS server. type TestMAASObjectSuite struct { TestMAASObject *TestMAASObject } var _ = Suite(&TestMAASObjectSuite{}) func (suite *TestMAASObjectSuite) SetUpSuite(c *C) { suite.TestMAASObject = NewTestMAAS("1.0") } func (suite *TestMAASObjectSuite) TearDownSuite(c *C) { suite.TestMAASObject.Close() } func (suite *TestMAASObjectSuite) TearDownTest(c *C) { suite.TestMAASObject.TestServer.Clear() } func (suite *TestMAASObjectSuite) TestListNodes(c *C) { input := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(input) nodeListing := suite.TestMAASObject.GetSubObject("nodes") listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) c.Check(err, IsNil) listNodes, err := listNodeObjects.GetArray() c.Assert(err, IsNil) c.Check(len(listNodes), Equals, 1) node, err := listNodes[0].GetMAASObject() c.Assert(err, IsNil) systemId, err := node.GetField("system_id") c.Assert(err, IsNil) c.Check(systemId, Equals, "mysystemid") resourceURI, _ := node.GetField(resourceURI) apiVersion := suite.TestMAASObject.TestServer.version expectedResourceURI := fmt.Sprintf("/api/%s/nodes/mysystemid/", apiVersion) c.Check(resourceURI, Equals, expectedResourceURI) } func (suite *TestMAASObjectSuite) TestSubnetReservedIPRangesNoAddresses(c *C) { suite.TestMAASObject.TestServer.NewSubnet(subnetJSON(defaultSubnet())) subnetsListing := suite.TestMAASObject.GetSubObject("subnets").GetSubObject("1") rangesJson, err := subnetsListing.CallGet("reserved_ip_ranges", url.Values{}) c.Check(err, IsNil) ranges, err := rangesJson.GetArray() c.Check(err, IsNil) c.Check(ranges, HasLen, 0) } func (suite *TestMAASObjectSuite) TestListNodesNoNodes(c *C) { nodeListing := suite.TestMAASObject.GetSubObject("nodes") listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) c.Check(err, IsNil) listNodes, err := listNodeObjects.GetArray() c.Check(err, IsNil) c.Check(listNodes, DeepEquals, []JSONObject{}) } func (suite *TestMAASObjectSuite) TestListNodesSelectedNodes(c *C) { input := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(input) input2 := `{"system_id": "mysystemid2"}` suite.TestMAASObject.TestServer.NewNode(input2) nodeListing := suite.TestMAASObject.GetSubObject("nodes") listNodeObjects, err := nodeListing.CallGet("list", url.Values{"id": {"mysystemid2"}}) c.Check(err, IsNil) listNodes, err := listNodeObjects.GetArray() c.Check(err, IsNil) c.Check(len(listNodes), Equals, 1) node, _ := listNodes[0].GetMAASObject() systemId, _ := node.GetField("system_id") c.Check(systemId, Equals, "mysystemid2") } func (suite *TestMAASObjectSuite) TestDeleteNode(c *C) { input := `{"system_id": "mysystemid"}` node := suite.TestMAASObject.TestServer.NewNode(input) err := node.Delete() c.Check(err, IsNil) c.Check(suite.TestMAASObject.TestServer.Nodes(), DeepEquals, map[string]MAASObject{}) } func (suite *TestMAASObjectSuite) TestOperationsOnNode(c *C) { input := `{"system_id": "mysystemid"}` node := suite.TestMAASObject.TestServer.NewNode(input) operations := []string{"start", "stop", "release"} for _, operation := range operations { _, err := node.CallPost(operation, url.Values{}) c.Check(err, IsNil) } } func (suite *TestMAASObjectSuite) TestNodePostPopulatesInterfaces(c *C) { server := suite.TestMAASObject.TestServer input := `{"system_id": "mysystemid"}` node := server.NewNode(input) subnet := server.NewSubnet(subnetJSON(defaultSubnet())) // Put the node in the subnet var nni NodeNetworkInterface nni.Name = "eth0" nni.Links = append(nni.Links, NetworkLink{uint(1), "auto", subnet}) server.SetNodeNetworkLink("mysystemid", nni) result, err := node.CallPost("start", url.Values{}) c.Assert(err, IsNil) resultMap, err := result.GetMap() c.Check(err, IsNil) array, err := resultMap["interface_set"].GetArray() c.Check(err, IsNil) c.Check(array, HasLen, 1) } func (suite *TestMAASObjectSuite) TestOperationsOnNodeGetRecorded(c *C) { input := `{"system_id": "mysystemid"}` node := suite.TestMAASObject.TestServer.NewNode(input) _, err := node.CallPost("start", url.Values{}) c.Check(err, IsNil) nodeOperations := suite.TestMAASObject.TestServer.NodeOperations() operations := nodeOperations["mysystemid"] c.Check(operations, DeepEquals, []string{"start"}) } func (suite *TestMAASObjectSuite) TestAcquireOperationGetsRecorded(c *C) { input := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(input) nodesObj := suite.TestMAASObject.GetSubObject("nodes/") params := url.Values{"key": []string{"value"}} jsonResponse, err := nodesObj.CallPost("acquire", params) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) systemId, err := acquiredNode.GetField("system_id") c.Assert(err, IsNil) // The 'acquire' operation has been recorded. nodeOperations := suite.TestMAASObject.TestServer.NodeOperations() operations := nodeOperations[systemId] c.Check(operations, DeepEquals, []string{"acquire"}) // The parameters used to 'acquire' the node have been recorded as well. values := suite.TestMAASObject.TestServer.NodeOperationRequestValues() value := values[systemId] c.Check(len(value), Equals, 1) c.Check(value[0], DeepEquals, params) } func (suite *TestMAASObjectSuite) TestNodesRelease(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "mysystemid1"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "mysystemid2"}`) suite.TestMAASObject.TestServer.OwnedNodes()["mysystemid2"] = true nodesObj := suite.TestMAASObject.GetSubObject("nodes/") params := url.Values{"nodes": []string{"mysystemid1", "mysystemid2"}} // release should only release mysystemid2, as it is the only one allocated. jsonResponse, err := nodesObj.CallPost("release", params) c.Assert(err, IsNil) releasedNodes, err := jsonResponse.GetArray() c.Assert(err, IsNil) c.Assert(releasedNodes, HasLen, 1) releasedNode, err := releasedNodes[0].GetMAASObject() c.Assert(err, IsNil) systemId, err := releasedNode.GetField("system_id") c.Assert(err, IsNil) c.Assert(systemId, Equals, "mysystemid2") // The 'release' operation has been recorded. nodesOperations := suite.TestMAASObject.TestServer.NodesOperations() c.Check(nodesOperations, DeepEquals, []string{"release"}) nodesOperationRequestValues := suite.TestMAASObject.TestServer.NodesOperationRequestValues() expectedValues := make(url.Values) expectedValues.Add("nodes", "mysystemid1") expectedValues.Add("nodes", "mysystemid2") c.Check(nodesOperationRequestValues, DeepEquals, []url.Values{expectedValues}) } func (suite *TestMAASObjectSuite) TestNodesReleaseUnknown(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "mysystemid"}`) suite.TestMAASObject.TestServer.OwnedNodes()["mysystemid"] = true nodesObj := suite.TestMAASObject.GetSubObject("nodes/") params := url.Values{"nodes": []string{"mysystemid", "what"}} // if there are any unknown nodes, none are released. _, err := nodesObj.CallPost("release", params) c.Assert(err, ErrorMatches, `.* 400 Bad Request \(Unknown node\(s\): what.\)`) c.Assert(suite.TestMAASObject.TestServer.OwnedNodes()["mysystemid"], Equals, true) } func (suite *TestMAASObjectSuite) TestUploadFile(c *C) { const filename = "myfile.txt" const fileContent = "uploaded contents" files := suite.TestMAASObject.GetSubObject("files") params := url.Values{"filename": {filename}} filesMap := map[string][]byte{"file": []byte(fileContent)} // Upload a file. _, err := files.CallPostFiles("add", params, filesMap) c.Assert(err, IsNil) // The file can now be downloaded. downloadedFile, err := files.CallGet("get", params) c.Assert(err, IsNil) bytes, err := downloadedFile.GetBytes() c.Assert(err, IsNil) c.Check(string(bytes), Equals, fileContent) } func (suite *TestMAASObjectSuite) TestFileNamesMayContainSlashes(c *C) { const filename = "filename/with/slashes/in/it" const fileContent = "file contents" files := suite.TestMAASObject.GetSubObject("files") params := url.Values{"filename": {filename}} filesMap := map[string][]byte{"file": []byte(fileContent)} _, err := files.CallPostFiles("add", params, filesMap) c.Assert(err, IsNil) file, err := files.GetSubObject(filename).Get() c.Assert(err, IsNil) field, err := file.GetField("content") c.Assert(err, IsNil) c.Check(field, Equals, base64.StdEncoding.EncodeToString([]byte(fileContent))) } func (suite *TestMAASObjectSuite) TestAcquireNodeGrabsAvailableNode(c *C) { input := `{"system_id": "nodeid"}` suite.TestMAASObject.TestServer.NewNode(input) nodesObj := suite.TestMAASObject.GetSubObject("nodes/") jsonResponse, err := nodesObj.CallPost("acquire", nil) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) systemID, err := acquiredNode.GetField("system_id") c.Assert(err, IsNil) c.Check(systemID, Equals, "nodeid") _, owned := suite.TestMAASObject.TestServer.OwnedNodes()[systemID] c.Check(owned, Equals, true) } func (suite *TestMAASObjectSuite) TestAcquireNodeNeedsANode(c *C) { nodesObj := suite.TestMAASObject.GetSubObject("nodes/") _, err := nodesObj.CallPost("acquire", nil) svrError, ok := GetServerError(err) c.Assert(ok, jc.IsTrue) c.Assert(svrError.StatusCode, Equals, http.StatusConflict) } func (suite *TestMAASObjectSuite) TestAcquireNodeIgnoresOwnedNodes(c *C) { input := `{"system_id": "nodeid"}` suite.TestMAASObject.TestServer.NewNode(input) nodesObj := suite.TestMAASObject.GetSubObject("nodes/") // Ensure that the one node in the MAAS is not available. _, err := nodesObj.CallPost("acquire", nil) c.Assert(err, IsNil) _, err = nodesObj.CallPost("acquire", nil) svrError, ok := GetServerError(err) c.Assert(ok, jc.IsTrue) c.Check(svrError.StatusCode, Equals, http.StatusConflict) } func (suite *TestMAASObjectSuite) TestReleaseNodeReleasesAcquiredNode(c *C) { input := `{"system_id": "nodeid"}` suite.TestMAASObject.TestServer.NewNode(input) nodesObj := suite.TestMAASObject.GetSubObject("nodes/") jsonResponse, err := nodesObj.CallPost("acquire", nil) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) systemID, err := acquiredNode.GetField("system_id") c.Assert(err, IsNil) nodeObj := nodesObj.GetSubObject(systemID) _, err = nodeObj.CallPost("release", nil) c.Assert(err, IsNil) _, owned := suite.TestMAASObject.TestServer.OwnedNodes()[systemID] c.Check(owned, Equals, false) } func (suite *TestMAASObjectSuite) TestGetNetworks(c *C) { nodeJSON := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(nodeJSON) networkJSON := `{"name": "mynetworkname", "ip": "0.1.2.0", "netmask": "255.255.255.0"}` suite.TestMAASObject.TestServer.NewNetwork(networkJSON) suite.TestMAASObject.TestServer.ConnectNodeToNetwork("mysystemid", "mynetworkname") networkMethod := suite.TestMAASObject.GetSubObject("networks") params := url.Values{"node": []string{"mysystemid"}} listNetworkObjects, err := networkMethod.CallGet("", params) c.Assert(err, IsNil) networkJSONArray, err := listNetworkObjects.GetArray() c.Assert(err, IsNil) c.Check(networkJSONArray, HasLen, 1) listNetworks, err := networkJSONArray[0].GetMAASObject() c.Assert(err, IsNil) networkName, err := listNetworks.GetField("name") c.Assert(err, IsNil) ip, err := listNetworks.GetField("ip") c.Assert(err, IsNil) netmask, err := listNetworks.GetField("netmask") c.Assert(err, IsNil) c.Check(networkName, Equals, "mynetworkname") c.Check(ip, Equals, "0.1.2.0") c.Check(netmask, Equals, "255.255.255.0") } func (suite *TestMAASObjectSuite) TestGetNetworksNone(c *C) { nodeJSON := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(nodeJSON) networkMethod := suite.TestMAASObject.GetSubObject("networks") params := url.Values{"node": []string{"mysystemid"}} listNetworkObjects, err := networkMethod.CallGet("", params) c.Assert(err, IsNil) networkJSONArray, err := listNetworkObjects.GetArray() c.Assert(err, IsNil) c.Check(networkJSONArray, HasLen, 0) } func (suite *TestMAASObjectSuite) TestListNodesWithNetworks(c *C) { nodeJSON := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(nodeJSON) networkJSON := `{"name": "mynetworkname", "ip": "0.1.2.0", "netmask": "255.255.255.0"}` suite.TestMAASObject.TestServer.NewNetwork(networkJSON) suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("mysystemid", "mynetworkname", "aa:bb:cc:dd:ee:ff") nodeListing := suite.TestMAASObject.GetSubObject("nodes") listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) c.Assert(err, IsNil) listNodes, err := listNodeObjects.GetArray() c.Assert(err, IsNil) c.Check(listNodes, HasLen, 1) node, err := listNodes[0].GetMAASObject() c.Assert(err, IsNil) systemId, err := node.GetField("system_id") c.Assert(err, IsNil) c.Check(systemId, Equals, "mysystemid") gotResourceURI, err := node.GetField(resourceURI) c.Assert(err, IsNil) apiVersion := suite.TestMAASObject.TestServer.version expectedResourceURI := fmt.Sprintf("/api/%s/nodes/mysystemid/", apiVersion) c.Check(gotResourceURI, Equals, expectedResourceURI) macAddressSet, err := node.GetMap()["macaddress_set"].GetArray() c.Assert(err, IsNil) c.Check(macAddressSet, HasLen, 1) macAddress, err := macAddressSet[0].GetMap() c.Assert(err, IsNil) macAddressString, err := macAddress["mac_address"].GetString() c.Check(macAddressString, Equals, "aa:bb:cc:dd:ee:ff") gotResourceURI, err = macAddress[resourceURI].GetString() c.Assert(err, IsNil) expectedResourceURI = fmt.Sprintf("/api/%s/nodes/mysystemid/macs/%s/", apiVersion, url.QueryEscape("aa:bb:cc:dd:ee:ff")) c.Check(gotResourceURI, Equals, expectedResourceURI) } func (suite *TestMAASObjectSuite) TestListNetworkConnectedMACAddresses(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "node_1"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "node_2"}`) suite.TestMAASObject.TestServer.NewNetwork( `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`, ) suite.TestMAASObject.TestServer.NewNetwork( `{"name": "net_2", "ip": "0.2.2.0", "netmask": "255.255.255.0"}`, ) suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_2", "net_2", "aa:bb:cc:dd:ee:22") suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_1", "net_1", "aa:bb:cc:dd:ee:11") suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_2", "net_1", "aa:bb:cc:dd:ee:21") suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_1", "net_2", "aa:bb:cc:dd:ee:12") nodeListing := suite.TestMAASObject.GetSubObject("networks").GetSubObject("net_1") listNodeObjects, err := nodeListing.CallGet("list_connected_macs", url.Values{}) c.Assert(err, IsNil) listNodes, err := listNodeObjects.GetArray() c.Assert(err, IsNil) c.Check(listNodes, HasLen, 2) node, err := listNodes[0].GetMAASObject() c.Assert(err, IsNil) macAddress, err := node.GetField("mac_address") c.Assert(err, IsNil) c.Check(macAddress == "aa:bb:cc:dd:ee:11" || macAddress == "aa:bb:cc:dd:ee:21", Equals, true) node1_idx := 0 if macAddress == "aa:bb:cc:dd:ee:21" { node1_idx = 1 } node, err = listNodes[node1_idx].GetMAASObject() c.Assert(err, IsNil) macAddress, err = node.GetField("mac_address") c.Assert(err, IsNil) c.Check(macAddress, Equals, "aa:bb:cc:dd:ee:11") nodeResourceURI, err := node.GetField(resourceURI) c.Assert(err, IsNil) apiVersion := suite.TestMAASObject.TestServer.version expectedResourceURI := fmt.Sprintf("/api/%s/nodes/node_1/macs/%s/", apiVersion, url.QueryEscape("aa:bb:cc:dd:ee:11")) c.Check(nodeResourceURI, Equals, expectedResourceURI) node, err = listNodes[1-node1_idx].GetMAASObject() c.Assert(err, IsNil) macAddress, err = node.GetField("mac_address") c.Assert(err, IsNil) c.Check(macAddress, Equals, "aa:bb:cc:dd:ee:21") nodeResourceURI, err = node.GetField(resourceURI) c.Assert(err, IsNil) expectedResourceURI = fmt.Sprintf("/api/%s/nodes/node_2/macs/%s/", apiVersion, url.QueryEscape("aa:bb:cc:dd:ee:21")) c.Check(nodeResourceURI, Equals, expectedResourceURI) } func (suite *TestMAASObjectSuite) TestGetVersion(c *C) { networkMethod := suite.TestMAASObject.GetSubObject("version") params := url.Values{"node": []string{"mysystemid"}} versionObject, err := networkMethod.CallGet("", params) c.Assert(err, IsNil) versionMap, err := versionObject.GetMap() c.Assert(err, IsNil) jsonArray, ok := versionMap["capabilities"] c.Check(ok, Equals, true) capArray, err := jsonArray.GetArray() for _, capJSONName := range capArray { capName, err := capJSONName.GetString() c.Assert(err, IsNil) switch capName { case "networks-management": case "static-ipaddresses": case "devices-management": case "network-deployment-ubuntu": default: c.Fatalf("unknown capability %q", capName) } } } func (suite *TestMAASObjectSuite) assertIPAmong(c *C, jsonObjIP JSONObject, expectIPs ...string) { apiVersion := suite.TestMAASObject.TestServer.version expectedURI := getIPAddressesEndpoint(apiVersion) maasObj, err := jsonObjIP.GetMAASObject() c.Assert(err, IsNil) attrs := maasObj.GetMap() uri, err := attrs["resource_uri"].GetString() c.Assert(err, IsNil) c.Assert(uri, Equals, expectedURI) allocType, err := attrs["alloc_type"].GetFloat64() c.Assert(err, IsNil) c.Assert(allocType, Equals, 4.0) created, err := attrs["created"].GetString() c.Assert(err, IsNil) c.Assert(created, Not(Equals), "") ip, err := attrs["ip"].GetString() c.Assert(err, IsNil) if !contains(expectIPs, ip) { c.Fatalf("expected IP in %v, got %q", expectIPs, ip) } } func (suite *TestMAASObjectSuite) TestListIPAddresses(c *C) { ipAddresses := suite.TestMAASObject.GetSubObject("ipaddresses") // First try without any networks and IPs. listIPObjects, err := ipAddresses.CallGet("", url.Values{}) c.Assert(err, IsNil) items, err := listIPObjects.GetArray() c.Assert(err, IsNil) c.Assert(items, HasLen, 0) // Add two networks and some addresses to each one. suite.TestMAASObject.TestServer.NewNetwork( `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`, ) suite.TestMAASObject.TestServer.NewNetwork( `{"name": "net_2", "ip": "0.2.2.0", "netmask": "255.255.255.0"}`, ) suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.3", "net_1") suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.4", "net_1") suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.5", "net_1") suite.TestMAASObject.TestServer.NewIPAddress("0.2.2.3", "net_2") suite.TestMAASObject.TestServer.NewIPAddress("0.2.2.4", "net_2") // List all addresses and verify the needed response fields are set. listIPObjects, err = ipAddresses.CallGet("", url.Values{}) c.Assert(err, IsNil) items, err = listIPObjects.GetArray() c.Assert(err, IsNil) c.Assert(items, HasLen, 5) for _, ipObj := range items { suite.assertIPAmong( c, ipObj, "0.1.2.3", "0.1.2.4", "0.1.2.5", "0.2.2.3", "0.2.2.4", ) } // Remove all net_1 IPs. removed := suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.3") c.Assert(removed, Equals, true) removed = suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.4") c.Assert(removed, Equals, true) removed = suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.5") c.Assert(removed, Equals, true) // Remove the last IP twice, should be OK and return false. removed = suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.5") c.Assert(removed, Equals, false) // List again. listIPObjects, err = ipAddresses.CallGet("", url.Values{}) c.Assert(err, IsNil) items, err = listIPObjects.GetArray() c.Assert(err, IsNil) c.Assert(items, HasLen, 2) for _, ipObj := range items { suite.assertIPAmong( c, ipObj, "0.2.2.3", "0.2.2.4", ) } } func (suite *TestMAASObjectSuite) TestReserveIPAddress(c *C) { suite.TestMAASObject.TestServer.NewNetwork( `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`, ) ipAddresses := suite.TestMAASObject.GetSubObject("ipaddresses") // First try "reserve" with requested_address set. params := url.Values{"network": []string{"0.1.2.0/24"}, "requested_address": []string{"0.1.2.42"}} res, err := ipAddresses.CallPost("reserve", params) c.Assert(err, IsNil) suite.assertIPAmong(c, res, "0.1.2.42") // Now try "reserve" without requested_address. delete(params, "requested_address") res, err = ipAddresses.CallPost("reserve", params) c.Assert(err, IsNil) suite.assertIPAmong(c, res, "0.1.2.2") } func (suite *TestMAASObjectSuite) TestReleaseIPAddress(c *C) { suite.TestMAASObject.TestServer.NewNetwork( `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`, ) suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.3", "net_1") ipAddresses := suite.TestMAASObject.GetSubObject("ipaddresses") // Try with non-existing address - should return 404. params := url.Values{"ip": []string{"0.2.2.1"}} _, err := ipAddresses.CallPost("release", params) c.Assert(err, ErrorMatches, `(\n|.)*404 Not Found(\n|.)*`) // Now with existing one - all OK. params = url.Values{"ip": []string{"0.1.2.3"}} _, err = ipAddresses.CallPost("release", params) c.Assert(err, IsNil) // Ensure it got removed. c.Assert(suite.TestMAASObject.TestServer.ipAddressesPerNetwork["net_1"], HasLen, 0) // Try again, should return 404. _, err = ipAddresses.CallPost("release", params) c.Assert(err, ErrorMatches, `(\n|.)*404 Not Found(\n|.)*`) } const nodeDetailsXML = ` Computer ` func (suite *TestMAASObjectSuite) TestNodeDetails(c *C) { nodeJSON := `{"system_id": "mysystemid"}` suite.TestMAASObject.TestServer.NewNode(nodeJSON) suite.TestMAASObject.TestServer.AddNodeDetails("mysystemid", nodeDetailsXML) obj := suite.TestMAASObject.GetSubObject("nodes").GetSubObject("mysystemid") uri := obj.URI() result, err := obj.client.Get(uri, "details", nil) c.Assert(err, IsNil) bsonObj := map[string]interface{}{} err = bson.Unmarshal(result, &bsonObj) c.Assert(err, IsNil) _, ok := bsonObj["lldp"] c.Check(ok, Equals, true) gotXMLText, ok := bsonObj["lshw"] c.Check(ok, Equals, true) c.Check(string(gotXMLText.([]byte)), Equals, string(nodeDetailsXML)) } func (suite *TestMAASObjectSuite) TestListNodegroups(c *C) { suite.TestMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "arm64", "release": "trusty"}`) suite.TestMAASObject.TestServer.AddBootImage("uuid-1", `{"architecture": "amd64", "release": "precise"}`) nodegroupListing := suite.TestMAASObject.GetSubObject("nodegroups") result, err := nodegroupListing.CallGet("list", nil) c.Assert(err, IsNil) nodegroups, err := result.GetArray() c.Assert(err, IsNil) c.Check(nodegroups, HasLen, 2) for _, obj := range nodegroups { nodegroup, err := obj.GetMAASObject() c.Assert(err, IsNil) uuid, err := nodegroup.GetField("uuid") c.Assert(err, IsNil) nodegroupResourceURI, err := nodegroup.GetField(resourceURI) c.Assert(err, IsNil) apiVersion := suite.TestMAASObject.TestServer.version expectedResourceURI := fmt.Sprintf("/api/%s/nodegroups/%s/", apiVersion, uuid) c.Check(nodegroupResourceURI, Equals, expectedResourceURI) } } func (suite *TestMAASObjectSuite) TestListNodegroupsEmptyList(c *C) { nodegroupListing := suite.TestMAASObject.GetSubObject("nodegroups") result, err := nodegroupListing.CallGet("list", nil) c.Assert(err, IsNil) nodegroups, err := result.GetArray() c.Assert(err, IsNil) c.Check(nodegroups, HasLen, 0) } func (suite *TestMAASObjectSuite) TestListNodegroupInterfaces(c *C) { suite.TestMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "arm64", "release": "trusty"}`) jsonText := `{ "ip_range_high": "172.16.0.128", "ip_range_low": "172.16.0.2", "broadcast_ip": "172.16.0.255", "static_ip_range_low": "172.16.0.129", "name": "eth0", "ip": "172.16.0.2", "subnet_mask": "255.255.255.0", "management": 2, "static_ip_range_high": "172.16.0.255", "interface": "eth0" }` suite.TestMAASObject.TestServer.NewNodegroupInterface("uuid-0", jsonText) nodegroupsInterfacesListing := suite.TestMAASObject.GetSubObject("nodegroups").GetSubObject("uuid-0").GetSubObject("interfaces") result, err := nodegroupsInterfacesListing.CallGet("list", nil) c.Assert(err, IsNil) nodegroupsInterfaces, err := result.GetArray() c.Assert(err, IsNil) c.Check(nodegroupsInterfaces, HasLen, 1) nodegroupsInterface, err := nodegroupsInterfaces[0].GetMap() c.Assert(err, IsNil) checkMember := func(member, expectedValue string) { value, err := nodegroupsInterface[member].GetString() c.Assert(err, IsNil) c.Assert(value, Equals, expectedValue) } checkMember("ip_range_high", "172.16.0.128") checkMember("ip_range_low", "172.16.0.2") checkMember("broadcast_ip", "172.16.0.255") checkMember("static_ip_range_low", "172.16.0.129") checkMember("static_ip_range_high", "172.16.0.255") checkMember("name", "eth0") checkMember("ip", "172.16.0.2") checkMember("subnet_mask", "255.255.255.0") checkMember("interface", "eth0") value, err := nodegroupsInterface["management"].GetFloat64() c.Assert(err, IsNil) c.Assert(value, Equals, 2.0) } func (suite *TestMAASObjectSuite) TestListNodegroupsInterfacesEmptyList(c *C) { suite.TestMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "arm64", "release": "trusty"}`) nodegroupsInterfacesListing := suite.TestMAASObject.GetSubObject("nodegroups").GetSubObject("uuid-0").GetSubObject("interfaces") result, err := nodegroupsInterfacesListing.CallGet("list", nil) c.Assert(err, IsNil) interfaces, err := result.GetArray() c.Assert(err, IsNil) c.Check(interfaces, HasLen, 0) } func (suite *TestMAASObjectSuite) TestListBootImages(c *C) { suite.TestMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "arm64", "release": "trusty"}`) suite.TestMAASObject.TestServer.AddBootImage("uuid-1", `{"architecture": "amd64", "release": "precise"}`) suite.TestMAASObject.TestServer.AddBootImage("uuid-1", `{"architecture": "ppc64el", "release": "precise"}`) bootImageListing := suite.TestMAASObject.GetSubObject("nodegroups").GetSubObject("uuid-1").GetSubObject("boot-images") result, err := bootImageListing.CallGet("", nil) c.Assert(err, IsNil) bootImageObjects, err := result.GetArray() c.Assert(err, IsNil) c.Check(bootImageObjects, HasLen, 2) expectedBootImages := []string{"amd64.precise", "ppc64el.precise"} bootImages := make([]string, len(bootImageObjects)) for i, obj := range bootImageObjects { bootimage, err := obj.GetMap() c.Assert(err, IsNil) architecture, err := bootimage["architecture"].GetString() c.Assert(err, IsNil) release, err := bootimage["release"].GetString() c.Assert(err, IsNil) bootImages[i] = fmt.Sprintf("%s.%s", architecture, release) } sort.Strings(bootImages) c.Assert(bootImages, DeepEquals, expectedBootImages) } func (suite *TestMAASObjectSuite) TestListZones(c *C) { expected := map[string]string{ "zone0": "zone0 is very nice", "zone1": "zone1 is much nicer than zone0", } for name, desc := range expected { suite.TestMAASObject.TestServer.AddZone(name, desc) } result, err := suite.TestMAASObject.GetSubObject("zones").CallGet("", nil) c.Assert(err, IsNil) c.Assert(result, NotNil) list, err := result.GetArray() c.Assert(err, IsNil) c.Assert(list, HasLen, len(expected)) m := make(map[string]string) for _, item := range list { itemMap, err := item.GetMap() c.Assert(err, IsNil) name, err := itemMap["name"].GetString() c.Assert(err, IsNil) desc, err := itemMap["description"].GetString() c.Assert(err, IsNil) m[name] = desc } c.Assert(m, DeepEquals, expected) } func (suite *TestMAASObjectSuite) TestListTags(c *C) { expected := map[string]string{ "tag0": "Develop", "tag1": "Lack01", } for name, comment := range expected { suite.TestMAASObject.TestServer.AddTag(name, comment) } result, err := suite.TestMAASObject.GetSubObject("tags").CallGet("", nil) c.Assert(err, IsNil) c.Assert(result, NotNil) list, err := result.GetArray() c.Assert(err, IsNil) c.Assert(list, HasLen, len(expected)) m := make(map[string]string) for _, item := range list { itemMap, err := item.GetMap() c.Assert(err, IsNil) name, err := itemMap["name"].GetString() c.Assert(err, IsNil) comment, err := itemMap["comment"].GetString() c.Assert(err, IsNil) m[name] = comment } c.Assert(m, DeepEquals, expected) } func (suite *TestMAASObjectSuite) TestAcquireNodeZone(c *C) { suite.TestMAASObject.TestServer.AddZone("z0", "rox") suite.TestMAASObject.TestServer.AddZone("z1", "sux") suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n0", "zone": "z0"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n1", "zone": "z1"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n2", "zone": "z1"}`) nodesObj := suite.TestMAASObject.GetSubObject("nodes") acquire := func(zone string) (string, string, error) { var params url.Values if zone != "" { params = url.Values{"zone": []string{zone}} } jsonResponse, err := nodesObj.CallPost("acquire", params) if err != nil { return "", "", err } acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) systemId, err := acquiredNode.GetField("system_id") c.Assert(err, IsNil) assignedZone, err := acquiredNode.GetField("zone") c.Assert(err, IsNil) if zone != "" { c.Assert(assignedZone, Equals, zone) } return systemId, assignedZone, nil } id, _, err := acquire("z0") c.Assert(err, IsNil) c.Assert(id, Equals, "n0") id, _, err = acquire("z0") svrError, ok := GetServerError(err) c.Assert(ok, jc.IsTrue) c.Assert(svrError.StatusCode, Equals, http.StatusConflict) id, zone, err := acquire("") c.Assert(err, IsNil) c.Assert(id, Not(Equals), "n0") c.Assert(zone, Equals, "z1") } func (suite *TestMAASObjectSuite) TestAcquireFilterMemory(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n0", "memory": 1024}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n1", "memory": 2048}`) nodeListing := suite.TestMAASObject.GetSubObject("nodes") jsonResponse, err := nodeListing.CallPost("acquire", url.Values{"mem": []string{"2048"}}) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) mem, err := acquiredNode.GetMap()["memory"].GetFloat64() c.Assert(err, IsNil) c.Assert(mem, Equals, float64(2048)) } func (suite *TestMAASObjectSuite) TestAcquireFilterCpuCores(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n0", "cpu_count": 1}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n1", "cpu_count": 2}`) nodeListing := suite.TestMAASObject.GetSubObject("nodes") jsonResponse, err := nodeListing.CallPost("acquire", url.Values{"cpu-cores": []string{"2"}}) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) cpucount, err := acquiredNode.GetMap()["cpu_count"].GetFloat64() c.Assert(err, IsNil) c.Assert(cpucount, Equals, float64(2)) } func (suite *TestMAASObjectSuite) TestAcquireFilterArch(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n0", "architecture": "amd64"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n1", "architecture": "arm/generic"}`) nodeListing := suite.TestMAASObject.GetSubObject("nodes") jsonResponse, err := nodeListing.CallPost("acquire", url.Values{"arch": []string{"arm"}}) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) arch, _ := acquiredNode.GetField("architecture") c.Assert(arch, Equals, "arm/generic") } func (suite *TestMAASObjectSuite) TestAcquireFilterTag(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n0", "tag_names": "Develop"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n1", "tag_names": "GPU"}`) nodeListing := suite.TestMAASObject.GetSubObject("nodes") jsonResponse, err := nodeListing.CallPost("acquire", url.Values{"tags": []string{"GPU"}}) c.Assert(err, IsNil) acquiredNode, err := jsonResponse.GetMAASObject() c.Assert(err, IsNil) tag, _ := acquiredNode.GetField("tag_names") c.Assert(tag, Equals, "GPU") } func (suite *TestMAASObjectSuite) TestDeploymentStatus(c *C) { suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n0", "status": "6"}`) suite.TestMAASObject.TestServer.NewNode(`{"system_id": "n1", "status": "1"}`) nodes := suite.TestMAASObject.GetSubObject("nodes") jsonResponse, err := nodes.CallGet("deployment_status", url.Values{"nodes": []string{"n0", "n1"}}) c.Assert(err, IsNil) deploymentStatus, err := jsonResponse.GetMap() c.Assert(err, IsNil) c.Assert(deploymentStatus, HasLen, 2) expectedStatus := map[string]string{ "n0": "Deployed", "n1": "Not in Deployment", } for systemId, status := range expectedStatus { nodeStatus, err := deploymentStatus[systemId].GetString() c.Assert(err, IsNil) c.Assert(nodeStatus, Equals, status) } } golang-github-juju-gomaasapi-2.2.0/testservice_utils.go000066400000000000000000000051571451732172100232760ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "bytes" "encoding/binary" "encoding/json" "errors" "net" "net/http" "strconv" ) // NameOrIDToID takes a string that contains eiter an integer ID or the // name of a thing. It returns the integer ID contained or mapped to or panics. func NameOrIDToID(v string, nameToID map[string]uint, minID, maxID uint) (ID uint, err error) { ID, ok := nameToID[v] if !ok { intID, err := strconv.Atoi(v) if err != nil { return 0, err } ID = uint(intID) } if ID < minID || ID > maxID { return 0, errors.New("ID out of range") } return ID, nil } // IP is an enhanced net.IP type IP struct { netIP net.IP Purpose []string } // IPFromNetIP creates a IP from a net.IP. func IPFromNetIP(netIP net.IP) IP { var ip IP ip.netIP = netIP return ip } // IPFromString creates a new IP from a string IP address representation func IPFromString(v string) IP { return IPFromNetIP(net.ParseIP(v)) } // IPFromInt64 creates a new IP from a uint64 IP address representation func IPFromInt64(v uint64) IP { var ip IP ip.SetUInt64(v) return ip } // To4 converts the IPv4 address ip to a 4-byte representation. If ip is not // an IPv4 address, To4 returns nil. func (ip IP) To4() net.IP { return ip.netIP.To4() } // To16 converts the IP address ip to a 16-byte representation. If ip is not // an IP address (it is the wrong length), To16 returns nil. func (ip IP) To16() net.IP { return ip.netIP.To16() } func (ip IP) String() string { return ip.netIP.String() } // UInt64 returns a uint64 holding the IP address func (ip IP) UInt64() uint64 { if len(ip.netIP) == 0 { return uint64(0) } if ip.To4() != nil { return uint64(binary.BigEndian.Uint32([]byte(ip.To4()))) } return binary.BigEndian.Uint64([]byte(ip.To16())) } // SetUInt64 sets the IP value to v func (ip *IP) SetUInt64(v uint64) { if len(ip.netIP) == 0 { // If we don't have allocated storage make an educated guess // at if the address we received is an IPv4 or IPv6 address. if v == (v & 0x00000000ffffFFFF) { // Guessing IPv4 ip.netIP = net.ParseIP("0.0.0.0") } else { ip.netIP = net.ParseIP("2001:4860:0:2001::68") } } bb := new(bytes.Buffer) var first int if ip.To4() != nil { binary.Write(bb, binary.BigEndian, uint32(v)) first = len(ip.netIP) - 4 } else { binary.Write(bb, binary.BigEndian, v) } copy(ip.netIP[first:], bb.Bytes()) } func PrettyJsonWriter(thing interface{}, w http.ResponseWriter) { var out bytes.Buffer b, err := json.MarshalIndent(thing, "", " ") checkError(err) out.Write(b) out.WriteTo(w) } golang-github-juju-gomaasapi-2.2.0/testservice_vlan.go000066400000000000000000000013071451732172100230670ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/http" ) func getVLANsEndpoint(version string) string { return fmt.Sprintf("/api/%s/vlans/", version) } // TestVLAN is the MAAS API VLAN representation type TestVLAN struct { Name string `json:"name"` Fabric string `json:"fabric"` VID uint `json:"vid"` ResourceURI string `json:"resource_uri"` ID uint `json:"id"` } // PostedVLAN is the MAAS API posted VLAN representation type PostedVLAN struct { Name string `json:"name"` VID uint `json:"vid"` } func vlansHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { //TODO } golang-github-juju-gomaasapi-2.2.0/urlparams.go000066400000000000000000000021751451732172100215210ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "fmt" "net/url" ) // URLParams wraps url.Values to easily add values, but skipping empty ones. type URLParams struct { Values url.Values } // NewURLParams allocates a new URLParams type. func NewURLParams() *URLParams { return &URLParams{Values: make(url.Values)} } // MaybeAdd adds the (name, value) pair iff value is not empty. func (p *URLParams) MaybeAdd(name, value string) { if value != "" { p.Values.Add(name, value) } } // MaybeAddInt adds the (name, value) pair iff value is not zero. func (p *URLParams) MaybeAddInt(name string, value int) { if value != 0 { p.Values.Add(name, fmt.Sprint(value)) } } // MaybeAddBool adds the (name, value) pair iff value is true. func (p *URLParams) MaybeAddBool(name string, value bool) { if value { p.Values.Add(name, fmt.Sprint(value)) } } // MaybeAddMany adds the (name, value) for each value in values iff // value is not empty. func (p *URLParams) MaybeAddMany(name string, values []string) { for _, value := range values { p.MaybeAdd(name, value) } } golang-github-juju-gomaasapi-2.2.0/urlparams_test.go000066400000000000000000000035301451732172100225540ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi_test import ( "github.com/juju/gomaasapi/v2" gc "gopkg.in/check.v1" ) type urlParamsSuite struct { } var _ = gc.Suite(&urlParamsSuite{}) func (*urlParamsSuite) TestNewParamsNonNilValues(c *gc.C) { params := gomaasapi.NewURLParams() c.Assert(params.Values, gc.NotNil) } func (*urlParamsSuite) TestNewMaybeAddEmpty(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAdd("foo", "") c.Assert(params.Values.Encode(), gc.Equals, "") } func (*urlParamsSuite) TestNewMaybeAddWithValue(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAdd("foo", "bar") c.Assert(params.Values.Encode(), gc.Equals, "foo=bar") } func (*urlParamsSuite) TestNewMaybeAddIntZero(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAddInt("foo", 0) c.Assert(params.Values.Encode(), gc.Equals, "") } func (*urlParamsSuite) TestNewMaybeAddIntWithValue(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAddInt("foo", 42) c.Assert(params.Values.Encode(), gc.Equals, "foo=42") } func (*urlParamsSuite) TestNewMaybeAddBoolFalse(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAddBool("foo", false) c.Assert(params.Values.Encode(), gc.Equals, "") } func (*urlParamsSuite) TestNewMaybeAddBoolTrue(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAddBool("foo", true) c.Assert(params.Values.Encode(), gc.Equals, "foo=true") } func (*urlParamsSuite) TestNewMaybeAddManyNil(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAddMany("foo", nil) c.Assert(params.Values.Encode(), gc.Equals, "") } func (*urlParamsSuite) TestNewMaybeAddManyValues(c *gc.C) { params := gomaasapi.NewURLParams() params.MaybeAddMany("foo", []string{"two", "", "values"}) c.Assert(params.Values.Encode(), gc.Equals, "foo=two&foo=values") } golang-github-juju-gomaasapi-2.2.0/util.go000066400000000000000000000016171451732172100204700ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "strings" ) // JoinURLs joins a base URL and a subpath together. // Regardless of whether baseURL ends in a trailing slash (or even multiple // trailing slashes), or whether there are any leading slashes at the begining // of path, the two will always be joined together by a single slash. func JoinURLs(baseURL, path string) string { return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/") } // EnsureTrailingSlash appends a slash at the end of the given string unless // there already is one. // This is used to create the kind of normalized URLs that Django expects. // (to avoid Django's redirection when an URL does not ends with a slash.) func EnsureTrailingSlash(URL string) string { if strings.HasSuffix(URL, "/") { return URL } return URL + "/" } golang-github-juju-gomaasapi-2.2.0/util_test.go000066400000000000000000000032311451732172100215210ustar00rootroot00000000000000// Copyright 2012-2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "encoding/json" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) func (suite *GomaasapiTestSuite) TestJoinURLsAppendsPathToBaseURL(c *gc.C) { c.Check(JoinURLs("http://example.com/", "foo"), gc.Equals, "http://example.com/foo") } func (suite *GomaasapiTestSuite) TestJoinURLsAddsSlashIfNeeded(c *gc.C) { c.Check(JoinURLs("http://example.com/foo", "bar"), gc.Equals, "http://example.com/foo/bar") } func (suite *GomaasapiTestSuite) TestJoinURLsNormalizesDoubleSlash(c *gc.C) { c.Check(JoinURLs("http://example.com/base/", "/szot"), gc.Equals, "http://example.com/base/szot") } func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashAppendsSlashIfMissing(c *gc.C) { c.Check(EnsureTrailingSlash("test"), gc.Equals, "test/") } func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashDoesNotAppendIfPresent(c *gc.C) { c.Check(EnsureTrailingSlash("test/"), gc.Equals, "test/") } func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashReturnsSlashIfEmpty(c *gc.C) { c.Check(EnsureTrailingSlash(""), gc.Equals, "/") } func parseJSON(c *gc.C, source string) interface{} { var parsed interface{} err := json.Unmarshal([]byte(source), &parsed) c.Assert(err, jc.ErrorIsNil) return parsed } func updateJSONMap(c *gc.C, source string, changes map[string]interface{}) string { var parsed map[string]interface{} err := json.Unmarshal([]byte(source), &parsed) c.Assert(err, jc.ErrorIsNil) for key, value := range changes { parsed[key] = value } bytes, err := json.Marshal(parsed) c.Assert(err, jc.ErrorIsNil) return string(bytes) } golang-github-juju-gomaasapi-2.2.0/vlan.go000066400000000000000000000077271451732172100204630ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type vlan struct { // Add the controller in when we need to do things with the vlan. // controller Controller resourceURI string id int name string fabric string vid int mtu int dhcp bool primaryRack string secondaryRack string } // ID implements VLAN. func (v *vlan) ID() int { return v.id } // Name implements VLAN. func (v *vlan) Name() string { return v.name } // Fabric implements VLAN. func (v *vlan) Fabric() string { return v.fabric } // VID implements VLAN. func (v *vlan) VID() int { return v.vid } // MTU implements VLAN. func (v *vlan) MTU() int { return v.mtu } // DHCP implements VLAN. func (v *vlan) DHCP() bool { return v.dhcp } // PrimaryRack implements VLAN. func (v *vlan) PrimaryRack() string { return v.primaryRack } // SecondaryRack implements VLAN. func (v *vlan) SecondaryRack() string { return v.secondaryRack } func readVLANs(controllerVersion version.Number, source interface{}) ([]*vlan, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "vlan base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range vlanDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no vlan read func for version %s", controllerVersion) } readFunc := vlanDeserializationFuncs[deserialisationVersion] return readVLANList(valid, readFunc) } func readVLANList(sourceList []interface{}, readFunc vlanDeserializationFunc) ([]*vlan, error) { result := make([]*vlan, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for vlan %d, %T", i, value) } vlan, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "vlan %d", i) } result = append(result, vlan) } return result, nil } type vlanDeserializationFunc func(map[string]interface{}) (*vlan, error) var vlanDeserializationFuncs = map[version.Number]vlanDeserializationFunc{ twoDotOh: vlan_2_0, } func vlan_2_0(source map[string]interface{}) (*vlan, error) { fields := schema.Fields{ "id": schema.ForceInt(), "resource_uri": schema.String(), "name": schema.OneOf(schema.Nil(""), schema.String()), "fabric": schema.String(), "vid": schema.ForceInt(), "mtu": schema.ForceInt(), "dhcp_on": schema.Bool(), // racks are not always set. "primary_rack": schema.OneOf(schema.Nil(""), schema.String()), "secondary_rack": schema.OneOf(schema.Nil(""), schema.String()), } checker := schema.FieldMap(fields, nil) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "vlan 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. // Since the primary and secondary racks are optional, we use the two // part cast assignment. If the case fails, then we get the default value // we care about, which is the empty string. primary_rack, _ := valid["primary_rack"].(string) secondary_rack, _ := valid["secondary_rack"].(string) name, _ := valid["name"].(string) result := &vlan{ resourceURI: valid["resource_uri"].(string), id: valid["id"].(int), name: name, fabric: valid["fabric"].(string), vid: valid["vid"].(int), mtu: valid["mtu"].(int), dhcp: valid["dhcp_on"].(bool), primaryRack: primary_rack, secondaryRack: secondary_rack, } return result, nil } golang-github-juju-gomaasapi-2.2.0/vlan_test.go000066400000000000000000000056311451732172100215120ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type vlanSuite struct{} var _ = gc.Suite(&vlanSuite{}) func (*vlanSuite) TestReadVLANsBadSchema(c *gc.C) { _, err := readVLANs(twoDotOh, "wat?") c.Assert(err.Error(), gc.Equals, `vlan base schema check failed: expected list, got string("wat?")`) } func (s *vlanSuite) TestReadVLANsWithName(c *gc.C) { vlans, err := readVLANs(twoDotOh, parseJSON(c, vlanResponseWithName)) c.Assert(err, jc.ErrorIsNil) c.Assert(vlans, gc.HasLen, 1) readVLAN := vlans[0] s.assertVLAN(c, readVLAN, &vlan{ id: 1, name: "untagged", fabric: "fabric-0", vid: 2, mtu: 1500, dhcp: true, primaryRack: "a-rack", secondaryRack: "", }) } func (*vlanSuite) assertVLAN(c *gc.C, givenVLAN, expectedVLAN *vlan) { c.Check(givenVLAN.ID(), gc.Equals, expectedVLAN.id) c.Check(givenVLAN.Name(), gc.Equals, expectedVLAN.name) c.Check(givenVLAN.Fabric(), gc.Equals, expectedVLAN.fabric) c.Check(givenVLAN.VID(), gc.Equals, expectedVLAN.vid) c.Check(givenVLAN.MTU(), gc.Equals, expectedVLAN.mtu) c.Check(givenVLAN.DHCP(), gc.Equals, expectedVLAN.dhcp) c.Check(givenVLAN.PrimaryRack(), gc.Equals, expectedVLAN.primaryRack) c.Check(givenVLAN.SecondaryRack(), gc.Equals, expectedVLAN.secondaryRack) } func (s *vlanSuite) TestReadVLANsWithoutName(c *gc.C) { vlans, err := readVLANs(twoDotOh, parseJSON(c, vlanResponseWithoutName)) c.Assert(err, jc.ErrorIsNil) c.Assert(vlans, gc.HasLen, 1) readVLAN := vlans[0] s.assertVLAN(c, readVLAN, &vlan{ id: 5006, name: "", fabric: "maas-management", vid: 30, mtu: 1500, dhcp: true, primaryRack: "4y3h7n", secondaryRack: "", }) } func (*vlanSuite) TestLowVersion(c *gc.C) { _, err := readVLANs(version.MustParse("1.9.0"), parseJSON(c, vlanResponseWithName)) c.Assert(err.Error(), gc.Equals, `no vlan read func for version 1.9.0`) } func (*vlanSuite) TestHighVersion(c *gc.C) { vlans, err := readVLANs(version.MustParse("2.1.9"), parseJSON(c, vlanResponseWithoutName)) c.Assert(err, jc.ErrorIsNil) c.Assert(vlans, gc.HasLen, 1) } const ( vlanResponseWithName = ` [ { "name": "untagged", "vid": 2, "primary_rack": "a-rack", "resource_uri": "/MAAS/api/2.0/vlans/1/", "id": 1, "secondary_rack": null, "fabric": "fabric-0", "mtu": 1500, "dhcp_on": true } ] ` vlanResponseWithoutName = ` [ { "dhcp_on": true, "id": 5006, "mtu": 1500, "fabric": "maas-management", "vid": 30, "primary_rack": "4y3h7n", "name": null, "external_dhcp": null, "resource_uri": "/MAAS/api/2.0/vlans/5006/", "secondary_rack": null } ] ` ) golang-github-juju-gomaasapi-2.2.0/zone.go000066400000000000000000000052611451732172100204650ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/version" ) type zone struct { // Add the controller in when we need to do things with the zone. // controller Controller resourceURI string name string description string } // Name implements Zone. func (z *zone) Name() string { return z.name } // Description implements Zone. func (z *zone) Description() string { return z.description } func readZones(controllerVersion version.Number, source interface{}) ([]*zone, error) { checker := schema.List(schema.StringMap(schema.Any())) coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "zone base schema check failed") } valid := coerced.([]interface{}) var deserialisationVersion version.Number for v := range zoneDeserializationFuncs { if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { deserialisationVersion = v } } if deserialisationVersion == version.Zero { return nil, errors.Errorf("no zone read func for version %s", controllerVersion) } readFunc := zoneDeserializationFuncs[deserialisationVersion] return readZoneList(valid, readFunc) } // readZoneList expects the values of the sourceList to be string maps. func readZoneList(sourceList []interface{}, readFunc zoneDeserializationFunc) ([]*zone, error) { result := make([]*zone, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for zone %d, %T", i, value) } zone, err := readFunc(source) if err != nil { return nil, errors.Annotatef(err, "zone %d", i) } result = append(result, zone) } return result, nil } type zoneDeserializationFunc func(map[string]interface{}) (*zone, error) var zoneDeserializationFuncs = map[version.Number]zoneDeserializationFunc{ twoDotOh: zone_2_0, } func zone_2_0(source map[string]interface{}) (*zone, error) { fields := schema.Fields{ "name": schema.String(), "description": schema.String(), "resource_uri": schema.String(), } checker := schema.FieldMap(fields, nil) // no defaults coerced, err := checker.Coerce(source, nil) if err != nil { return nil, errors.Annotatef(err, "zone 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 := &zone{ name: valid["name"].(string), description: valid["description"].(string), resourceURI: valid["resource_uri"].(string), } return result, nil } golang-github-juju-gomaasapi-2.2.0/zone_test.go000066400000000000000000000030331451732172100215170ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package gomaasapi import ( jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" ) type zoneSuite struct{} var _ = gc.Suite(&zoneSuite{}) func (*zoneSuite) TestReadZonesBadSchema(c *gc.C) { _, err := readZones(twoDotOh, "wat?") c.Assert(err.Error(), gc.Equals, `zone base schema check failed: expected list, got string("wat?")`) } func (*zoneSuite) TestReadZones(c *gc.C) { zones, err := readZones(twoDotOh, parseJSON(c, zoneResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(zones, gc.HasLen, 2) c.Assert(zones[0].Name(), gc.Equals, "default") c.Assert(zones[0].Description(), gc.Equals, "default description") c.Assert(zones[1].Name(), gc.Equals, "special") c.Assert(zones[1].Description(), gc.Equals, "special description") } func (*zoneSuite) TestLowVersion(c *gc.C) { _, err := readZones(version.MustParse("1.9.0"), parseJSON(c, zoneResponse)) c.Assert(err.Error(), gc.Equals, `no zone read func for version 1.9.0`) } func (*zoneSuite) TestHighVersion(c *gc.C) { zones, err := readZones(version.MustParse("2.1.9"), parseJSON(c, zoneResponse)) c.Assert(err, jc.ErrorIsNil) c.Assert(zones, gc.HasLen, 2) } var zoneResponse = ` [ { "description": "default description", "resource_uri": "/MAAS/api/2.0/zones/default/", "name": "default" }, { "description": "special description", "resource_uri": "/MAAS/api/2.0/zones/special/", "name": "special" } ] `