Skip to content

Commit

Permalink
Merge pull request juju#12 from howbazaar/controller-interface
Browse files Browse the repository at this point in the history
Controller interface

Introduce the concepts of interfaces to gomaasapi.

Add Controller, Zone and Machine interfaces.

The machine listing from the controller doesn't yet honour the params, but instead just lists all of them.
  • Loading branch information
jujubot committed Mar 30, 2016
2 parents f585bc9 + 9d9be39 commit accc3f0
Show file tree
Hide file tree
Showing 10 changed files with 1,569 additions and 13 deletions.
174 changes: 174 additions & 0 deletions controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.

package gomaasapi

import (
"encoding/json"
"net/url"
"sync/atomic"

"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/juju/schema"
"github.com/juju/utils/set"
"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
}

// NewController creates an authenticated client to the MAAS API, and checks
// the capabilities of the server.
func NewController(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.
var outerErr error
for _, apiVersion := range supportedAPIVersions {
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(args.BaseURL, args.APIKey, apiVersion)
if err != nil {
outerErr = err
continue
}
controllerVersion := version.Number{
Major: major,
Minor: minor,
}
controller := &controller{client: client}
// The controllerVersion returned from the function will include any patch version.
controller.capabilities, controller.apiVersion, err = controller.readAPIVersion(controllerVersion)
if err != nil {
logger.Debugf("read version failed: %v", err)
outerErr = err
continue
}
return controller, nil
}

return nil, errors.Wrap(outerErr, errors.New("unable to create authenticated client"))
}

type controller struct {
client *Client
apiVersion version.Number
capabilities set.Strings
}

// Capabilities implements Controller.
func (c *controller) Capabilities() set.Strings {
return c.capabilities
}

// Zones implements Controller.
func (c *controller) Zones() ([]Zone, error) {
source, err := c.get("zones")
if err != nil {
return nil, errors.Trace(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
}

// Machines implements Controller.
func (c *controller) Machines(params MachinesArgs) ([]Machine, error) {
// ignore params for now
source, err := c.get("machines")
if err != nil {
return nil, errors.Trace(err)
}
machines, err := readMachines(c.apiVersion, source)
if err != nil {
return nil, errors.Trace(err)
}
var result []Machine
for _, m := range machines {
result = append(result, m)
}
return result, nil
}

func (c *controller) get(path string) (interface{}, error) {
path = EnsureTrailingSlash(path)
requestID := nextrequestID()
logger.Tracef("request %x: GET %s%s", requestID, c.client.APIURL, path)
bytes, err := c.client.Get(&url.URL{Path: path}, "", nil)
if err != nil {
logger.Tracef("response %x: error: %q", requestID, err.Error())
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 nextrequestID() int64 {
return atomic.AddInt64(&requestNumber, 1)
}

func (c *controller) readAPIVersion(apiVersion version.Number) (set.Strings, version.Number, error) {
parsed, err := c.get("version")
if err != nil {
return nil, apiVersion, errors.Trace(err)
}

// As we care about other fields, add them.
fields := schema.Fields{
"capabilities": schema.List(schema.String()),
}
checker := schema.FieldMap(fields, nil) // no defaults
coerced, err := checker.Coerce(parsed, nil)
if err != nil {
return nil, apiVersion, errors.Trace(err)
}
// 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))
}

return capabilities, apiVersion, nil
}
87 changes: 87 additions & 0 deletions controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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/utils/set"
"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.CleanupSuite
server *SimpleTestServer
}

var _ = gc.Suite(&controllerSuite{})

func (s *controllerSuite) SetUpTest(c *gc.C) {
s.CleanupSuite.SetUpTest(c)

server := NewSimpleServer()
server.AddResponse("/api/2.0/version/", http.StatusOK, versionResponse)
server.AddResponse("/api/2.0/zones/", http.StatusOK, zoneResponse)
server.AddResponse("/api/2.0/machines/", http.StatusOK, machinesResponse)
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) 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) TestMachines(c *gc.C) {
controller := s.getController(c)
machines, err := controller.Machines(MachinesArgs{})
c.Assert(err, jc.ErrorIsNil)
c.Assert(machines, gc.HasLen, 3)
}

var versionResponse = `{"version": "unknown", "subversion": "", "capabilities": ["networks-management", "static-ipaddresses", "ipv6-deployment-ubuntu", "devices-management", "storage-deployment-ubuntu", "network-deployment-ubuntu"]}`
11 changes: 11 additions & 0 deletions dependencies.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/juju/errors git 1b5e39b83d1835fa480e0c2ddefb040ee82d58b3 2015-09-16T12:56:42Z
github.com/juju/loggo git 8477fc936adf0e382d680310047ca27e128a309a 2015-05-27T03:58:39Z
github.com/juju/names git ef19de31613af3735aa69ba3b40accce2faf7316 2016-03-01T22:07:10Z
github.com/juju/schema git 1e25943f8c6fd6815282d6f1ac87091d21e14e19 2016-03-01T11:16:46Z
github.com/juju/testing git 45f216f8ef2a6fbed3f38cbcd57f68741d295053 2016-03-17T08:39:30Z
github.com/juju/utils git af32cfaf28093965fdf63373d8447c489efb2875 2016-03-09T18:28:39Z
github.com/juju/version git ef897ad7f130870348ce306f61332f5335355063 2015-11-27T20:34:00Z
golang.org/x/crypto git aedad9a179ec1ea11b7064c57cbc6dc30d7724ec 2015-08-30T18:06:42Z
gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z
gopkg.in/mgo.v2 git 4d04138ffef2791c479c0c8bbffc30b34081b8d9 2015-10-26T16:34:53Z
gopkg.in/yaml.v2 git a83829b6f1293c91addabc89d0571c246397bbf4 2016-03-01T20:40:22Z
70 changes: 70 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.

package gomaasapi

import "github.com/juju/utils/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 {

// Capabilities returns a set of capabilities as defined by the string
// constants.
Capabilities() set.Strings

// Zones lists all the zones known to the MAAS controller.
Zones() ([]Zone, error)

// Machines returns a list of machines that match the params.
Machines(MachinesArgs) ([]Machine, error)
}

// 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
}

// Machine represents a physical machine.
type Machine interface {
SystemId() string
Hostname() string
FQDN() string

OperatingSystem() string
DistroSeries() string
Architecture() string
Memory() int
CpuCount() int

IPAddresses() []string
PowerState() string

// 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

Zone() Zone
}

type MachinesArgs struct {
SystemIds []string
}

0 comments on commit accc3f0

Please sign in to comment.