diff --git a/controller.go b/controller.go index 8ce50bd..912e4c4 100644 --- a/controller.go +++ b/controller.go @@ -222,6 +222,26 @@ func (c *controller) Zones() ([]Zone, error) { 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") @@ -247,6 +267,7 @@ type DevicesArgs struct { SystemIDs []string Domain string Zone string + Pool string AgentName string } @@ -258,6 +279,7 @@ func (c *controller) Devices(args DevicesArgs) ([]Device, error) { 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 { @@ -321,6 +343,7 @@ type MachinesArgs struct { SystemIDs []string Domain string Zone string + Pool string AgentName string OwnerData map[string]string } @@ -333,6 +356,7 @@ func (c *controller) Machines(args MachinesArgs) ([]Machine, error) { 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) // At the moment the MAAS API doesn't support filtering by owner // data so we do that ourselves below. @@ -371,7 +395,7 @@ type StorageSpec struct { Label string // Size is required and refers to the required minimum size in GB. Size int - // Zero or more tags assocated to with the disks. + // Zero or more tags associated to the disks. Tags []string } @@ -402,7 +426,7 @@ func (s *StorageSpec) String() string { return fmt.Sprintf("%s%d%s", label, s.Size, tags) } -// InterfaceSpec represents one elemenet of network related constraints. +// 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. @@ -451,7 +475,9 @@ type AllocateMachineArgs struct { 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 @@ -466,8 +492,9 @@ type AllocateMachineArgs struct { DryRun bool } -// Validate makes sure that any labels specifed in Storage or Interfaces -// are unique, and that the required specifications are valid. +// 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 { @@ -554,7 +581,9 @@ func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, Constra 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) diff --git a/controller_test.go b/controller_test.go index f916dbf..69bf315 100644 --- a/controller_test.go +++ b/controller_test.go @@ -53,6 +53,7 @@ func (s *controllerSuite) SetUpTest(c *gc.C) { 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 @@ -343,6 +344,13 @@ func (s *controllerSuite) TestZones(c *gc.C) { 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{}) @@ -409,11 +417,12 @@ func (s *controllerSuite) TestMachinesArgs(c *gc.C) { 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, 6) + c.Assert(request.URL.Query(), gc.HasLen, 7) } func (s *controllerSuite) TestStorageSpec(c *gc.C) { @@ -696,6 +705,7 @@ func (s *controllerSuite) TestAllocateMachineArgsForm(c *gc.C) { Interfaces: []InterfaceSpec{{Label: "default", Space: "magic"}}, NotSpace: []string{"special"}, Zone: "magic", + Pool: "swimming_is_fun", NotInZone: []string{"not-magic"}, AgentName: "agent 42", Comment: "testing", @@ -707,7 +717,7 @@ func (s *controllerSuite) TestAllocateMachineArgsForm(c *gc.C) { 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, 15) + c.Assert(form, gc.HasLen, 16) // Positive space check. c.Assert(form.Get("interfaces"), gc.Equals, "default:space=magic") // Negative space check. diff --git a/device.go b/device.go index 7c9bc70..6dc4f9b 100644 --- a/device.go +++ b/device.go @@ -28,6 +28,7 @@ type device struct { ipAddresses []string interfaceSet []*interface_ zone *zone + pool *pool } // SystemID implements Device. @@ -68,6 +69,14 @@ func (d *device) Zone() Zone { 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)) @@ -252,6 +261,7 @@ func device_2_0(source map[string]interface{}) (*device, error) { "ip_addresses": schema.List(schema.String()), "interface_set": schema.List(schema.StringMap(schema.Any())), "zone": schema.StringMap(schema.Any()), + "pool": schema.StringMap(schema.Any()), } defaults := schema.Defaults{ "owner": "", @@ -270,7 +280,14 @@ func device_2_0(source map[string]interface{}) (*device, error) { 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) + } + + pool, err := pool_2_0(valid["pool"].(map[string]interface{})) + if err != nil { return nil, errors.Trace(err) } @@ -288,6 +305,7 @@ func device_2_0(source map[string]interface{}) (*device, error) { ipAddresses: convertToStringSlice(valid["ip_addresses"]), interfaceSet: interfaceSet, zone: zone, + pool: pool, } return result, nil } diff --git a/device_test.go b/device_test.go index 13d303a..0528353 100644 --- a/device_test.go +++ b/device_test.go @@ -233,6 +233,11 @@ const ( "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, diff --git a/interface.go b/interface.go index f30a9a8..9865833 100644 --- a/interface.go +++ b/interface.go @@ -12,7 +12,7 @@ import ( "github.com/juju/version" ) -// Can't use interface as a type, so add an underscore. Yay. +// Can't use "interface" as a type, so add an underscore. Yay. type interface_ struct { controller *controller diff --git a/interfaces.go b/interfaces.go index 7623ef9..ee3a225 100644 --- a/interfaces.go +++ b/interfaces.go @@ -38,6 +38,9 @@ type Controller interface { // 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) @@ -145,6 +148,13 @@ type Zone interface { 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 @@ -168,6 +178,7 @@ type Device interface { FQDN() string IPAddresses() []string Zone() Zone + Pool() Pool // Parent returns the SystemID of the Parent. Most often this will be a // Machine. @@ -240,6 +251,7 @@ type Machine interface { Partition(id int) Partition Zone() Zone + Pool() Pool // Start the machine and install the operating system specified in the args. Start(StartArgs) error diff --git a/machine.go b/machine.go index f923f72..79fc6c6 100644 --- a/machine.go +++ b/machine.go @@ -40,6 +40,7 @@ type machine struct { bootInterface *interface_ interfaceSet []*interface_ zone *zone + pool *pool // Don't really know the difference between these two lists: physicalBlockDevices []*blockdevice blockDevices []*blockdevice @@ -60,6 +61,7 @@ func (m *machine) updateFrom(other *machine) { m.statusName = other.statusName m.statusMessage = other.statusMessage m.zone = other.zone + m.pool = other.pool m.tags = other.tags m.ownerData = other.ownerData } @@ -84,6 +86,14 @@ 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 @@ -510,6 +520,7 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { "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.StringMap(schema.Any()), "physicalblockdevice_set": schema.List(schema.StringMap(schema.Any())), "blockdevice_set": schema.List(schema.StringMap(schema.Any())), @@ -517,6 +528,7 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { defaults := schema.Defaults{ "architecture": "", } + checker := schema.FieldMap(fields, defaults) coerced, err := checker.Coerce(source, nil) if err != nil { @@ -538,14 +550,23 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { 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) } + + pool, err := pool_2_0(valid["pool"].(map[string]interface{})) + if 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) @@ -575,6 +596,7 @@ func machine_2_0(source map[string]interface{}) (*machine, error) { bootInterface: bootInterface, interfaceSet: interfaceSet, zone: zone, + pool: pool, physicalBlockDevices: physicalBlockDevices, blockDevices: blockDevices, } diff --git a/machine_test.go b/machine_test.go index 23985e5..133220d 100644 --- a/machine_test.go +++ b/machine_test.go @@ -63,6 +63,7 @@ func (*machineSuite) TestReadMachines(c *gc.C) { 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") @@ -732,6 +733,12 @@ const ( "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", @@ -842,6 +849,11 @@ const ( "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, @@ -866,6 +878,11 @@ const ( "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/", @@ -1154,6 +1171,11 @@ var ( "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", "storage": 8589.934592, "node_type": 0, @@ -1400,6 +1422,11 @@ var ( "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", "storage": 8589.934592, "node_type": 0, diff --git a/pool.go b/pool.go new file mode 100644 index 0000000..c1375e3 --- /dev/null +++ b/pool.go @@ -0,0 +1,106 @@ +// 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 +} + diff --git a/pool_test.go b/pool_test.go new file mode 100644 index 0000000..1c087e6 --- /dev/null +++ b/pool_test.go @@ -0,0 +1,60 @@ +// 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" + } +] +` +