pax_global_header 0000666 0000000 0000000 00000000064 14517321721 0014515 g ustar 00root root 0000000 0000000 52 comment=4a5c51482a8b9824c18f2cc4e5629405dbb8e696
golang-github-juju-gomaasapi-2.2.0/ 0000775 0000000 0000000 00000000000 14517321721 0017157 5 ustar 00root root 0000000 0000000 golang-github-juju-gomaasapi-2.2.0/.gitignore 0000664 0000000 0000000 00000000030 14517321721 0021140 0 ustar 00root root 0000000 0000000 *.sw[nop]
example/[^.]*
golang-github-juju-gomaasapi-2.2.0/LICENSE 0000664 0000000 0000000 00000021506 14517321721 0020170 0 ustar 00root root 0000000 0000000 All 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/Makefile 0000664 0000000 0000000 00000001033 14517321721 0020614 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000000503 14517321721 0020644 0 ustar 00root root 0000000 0000000 .. -*- 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.go 0000664 0000000 0000000 00000012761 14517321721 0021767 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000011563 14517321721 0023025 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007615 14517321721 0022232 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005765 14517321721 0023275 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000026132 14517321721 0020770 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000033676 14517321721 0022042 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000077437 14517321721 0021713 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000074607 14517321721 0022746 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000020755 14517321721 0020756 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000025107 14517321721 0022011 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000004552 14517321721 0020763 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000002233 14517321721 0022014 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003110 14517321721 0020445 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000012265 14517321721 0021030 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005012 14517321721 0022057 0 ustar 00root root 0000000 0000000 // 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/ 0000775 0000000 0000000 00000000000 14517321721 0020612 5 ustar 00root root 0000000 0000000 golang-github-juju-gomaasapi-2.2.0/example/live_example.go 0000664 0000000 0000000 00000011747 14517321721 0023625 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006747 14517321721 0020752 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000004537 14517321721 0022004 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000011764 14517321721 0020436 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007412 14517321721 0021470 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003566 14517321721 0021704 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000002514 14517321721 0022733 0 ustar 00root root 0000000 0000000 // 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.mod 0000664 0000000 0000000 00000002323 14517321721 0020265 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000041674 14517321721 0020326 0 ustar 00root root 0000000 0000000 github.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.go 0000664 0000000 0000000 00000000165 14517321721 0021451 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000000430 14517321721 0022503 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000030071 14517321721 0021447 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000040705 14517321721 0022513 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000031405 14517321721 0021634 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000016343 14517321721 0021655 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000032206 14517321721 0022710 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006302 14517321721 0020444 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005757 14517321721 0021520 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000000540 14517321721 0020426 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000000744 14517321721 0021473 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000014045 14517321721 0021622 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000014442 14517321721 0022662 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000042643 14517321721 0021123 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000157147 14517321721 0022170 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000004036 14517321721 0020631 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000010062 14517321721 0021516 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005733 14517321721 0022566 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005271 14517321721 0020464 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003220 14517321721 0021513 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006067 14517321721 0020612 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005563 14517321721 0021651 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007522 14517321721 0022062 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007004 14517321721 0023114 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007737 14517321721 0021024 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005436 14517321721 0022055 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006061 14517321721 0020447 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000002220 14517321721 0021477 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14517321721 0021155 5 ustar 00root root 0000000 0000000 golang-github-juju-gomaasapi-2.2.0/templates/source.go 0000664 0000000 0000000 00000000165 14517321721 0023006 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000000532 14517321721 0024043 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000017212 14517321721 0021166 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000162036 14517321721 0022056 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006664 14517321721 0023420 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000011634 14517321721 0024664 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000027245 14517321721 0023623 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000211260 14517321721 0023107 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005157 14517321721 0023276 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000001307 14517321721 0023067 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000002175 14517321721 0021521 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003530 14517321721 0022554 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000001617 14517321721 0020470 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003231 14517321721 0021521 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007727 14517321721 0020463 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005631 14517321721 0021512 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005261 14517321721 0020465 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003033 14517321721 0021517 0 ustar 00root root 0000000 0000000 // 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"
}
]
`