Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allocate machine with interface and storage constraints #28

Merged
merged 7 commits into from
Apr 8, 2016
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 101 additions & 20 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,35 +286,65 @@ type AllocateMachineArgs struct {
MinMemory int
Tags []string
NotTags []string
// Networks - list of networks (defined in MAAS) to which the machine must be
// attached. A network can be identified by the name assigned to it in MAAS;
// or by an ip: prefix followed by any IP address that falls within the
// network; or a vlan: prefix followed by a numeric VLAN tag, e.g. vlan:23
// for VLAN number 23. Valid VLAN tags must be in the range of 1 to 4094
// inclusive.
Networks []string
NotNetworks []string
Zone string
NotInZone []string
AgentName string
Comment string
DryRun bool
Zone string
NotInZone []string
// Storage is an optional value listing one or more storage constraints.
// Format is an optional label, followed by an optional colon, then size
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Format/The format/

// (which is mandatory) followed by an optional comma seperated list of tags
// in parentheses.
//
// Examples:
//
// 200(ssd,removable),400(ssd),300
// - 200GB disk with ssd and removable tag
// - 400GB disk with ssd tag
// - 300GB disk
//
// root:80(ssd),data:400
// - 80+GB disk with ssd tag, name the constraint "root"
// - 400+GB disk, name the consrtaint "data"
Storage string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be cleaner to accept this as a []string or better yet a []StorageSpec, where StorageSpec has fields for the size, labels, and tags. The caller shouldn't have to care about how MAAS wants this to be formatted.

// Interfaces is an optional value listing one or more interface constraints.
// Format of the string is semi-colon delimited labelled constraints. Each
// labelled constraint has the format:
// <label>:<key>=<value>[,<key2>=<value2>[,...]]'
// The label is used in the ConstraintMatch structure returned from
// AllocateMachine. Valid keys are:
// 'space', 'not_space', 'fabric_class', 'not_fabric_class',
// 'subnet_cidr', 'not_subnet_cidr', 'vid', 'not_vid',
// 'fabric', 'not_fabric', 'subnet', 'not_subnet', 'mode'
Interfaces string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

AgentName string
Comment string
DryRun bool
}

// 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 {
// Storage matches will come when we support interfaces for the block devices.

// Interface is a mapping of the constraint label specified to the Interface
// that matches that constraint.
Interfaces map[string]Interface
}

// AllocateMachine implements Controller.
//
// Returns an error that satisfies IsNoMatchError if the requested
// constraints cannot be met.
func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, error) {
func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, ConstraintMatches, error) {
var matches ConstraintMatches
params := NewURLParams()
params.MaybeAdd("name", args.Hostname)
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.MaybeAddMany("networks", args.Networks)
params.MaybeAddMany("not_networks", args.NotNetworks)
params.MaybeAdd("storage", args.Storage)
params.MaybeAdd("interfaces", args.Interfaces)
params.MaybeAdd("zone", args.Zone)
params.MaybeAddMany("not_in_zone", args.NotInZone)
params.MaybeAdd("agent_name", args.AgentName)
Expand All @@ -325,19 +355,26 @@ func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, error)
// A 409 Status code is "No Matching Machines"
if svrErr, ok := errors.Cause(err).(ServerError); ok {
if svrErr.StatusCode == http.StatusConflict {
return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
return nil, matches, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
}
}
// Translate http errors.
return nil, NewUnexpectedError(err)
return nil, matches, NewUnexpectedError(err)
}

machine, err := readMachine(c.apiVersion, result)
if err != nil {
return nil, errors.Trace(err)
return nil, matches, errors.Trace(err)
}
machine.controller = c
return machine, nil

// 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
Expand Down Expand Up @@ -618,3 +655,47 @@ func (c *controller) readAPIVersion(apiVersion version.Number) (set.Strings, ver

return capabilities, apiVersion, nil
}

func parseAllocateConstraintsResponse(source interface{}, machine *machine) (ConstraintMatches, error) {
var empty ConstraintMatches
// Do the schema check in two passes.
fields := schema.Fields{
"constraints_by_type": schema.StringMap(schema.Any()),
}
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")
}
constraintsMap := coerced.(map[string]interface{})["constraints_by_type"]
fields = schema.Fields{
"storage": schema.StringMap(schema.ForceInt()),
"interfaces": schema.StringMap(schema.ForceInt()),
}
defaults := schema.Defaults{
"storage": schema.Omit,
"interfaces": schema.Omit,
}
checker = schema.FieldMap(fields, defaults)
coerced, err = checker.Coerce(constraintsMap, nil)
if err != nil {
return empty, WrapWithDeserializationError(err, "allocation constraints response schema check failed")
}
valid := coerced.(map[string]interface{})
result := ConstraintMatches{
// TODO: make storage map
Interfaces: make(map[string]Interface),
}
// TODO: handle storage when we export block devices.
if interfaceMatches, found := valid["interfaces"]; found {
for label, value := range interfaceMatches.(map[string]interface{}) {
id := value.(int)
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)
}
result.Interfaces[label] = iface
}
}
return result, nil
}
58 changes: 49 additions & 9 deletions controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var _ = gc.Suite(&controllerSuite{})

func (s *controllerSuite) SetUpTest(c *gc.C) {
s.LoggingCleanupSuite.SetUpTest(c)
loggo.GetLogger("").SetLogLevel(loggo.DEBUG)
loggo.GetLogger("").SetLogLevel(loggo.TRACE)

server := NewSimpleServer()
server.AddGetResponse("/api/2.0/boot-resources/", http.StatusOK, bootResourcesResponse)
Expand Down Expand Up @@ -264,16 +264,56 @@ func (s *controllerSuite) TestMachinesArgs(c *gc.C) {
c.Assert(request.URL.Query(), gc.HasLen, 6)
}

func (s *controllerSuite) addAllocateReponse(c *gc.C, status int, interfaceMatches map[string]int) {
constraints := make(map[string]interface{})
if interfaceMatches != nil {
constraints["interfaces"] = interfaceMatches
}
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.server.AddPostResponse("/api/2.0/machines/?op=allocate", http.StatusOK, machineResponse)
s.addAllocateReponse(c, http.StatusOK, nil)
controller := s.getController(c)
machine, err := controller.AllocateMachine(AllocateMachineArgs{})
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.addAllocateReponse(c, http.StatusOK, map[string]int{
"database": 35,
})
controller := s.getController(c)
_, match, err := controller.AllocateMachine(AllocateMachineArgs{
// This isn't actually used, but here to show how it should be used.
Interfaces: "database:space=space-0",
})
c.Assert(err, jc.ErrorIsNil)
c.Assert(match.Interfaces, gc.HasLen, 1)
iface := match.Interfaces["database"]
c.Assert(iface.ID(), gc.Equals, 35)
}

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.addAllocateReponse(c, http.StatusOK, map[string]int{
"database": 40,
})
controller := s.getController(c)
_, _, err := controller.AllocateMachine(AllocateMachineArgs{
// This isn't actually used, but here to show how it should be used.
Interfaces: "database:space=space-0",
})
c.Assert(err, jc.Satisfies, IsDeserializationError)
}

func (s *controllerSuite) TestAllocateMachineArgs(c *gc.C) {
s.server.AddPostResponse("/api/2.0/machines/?op=allocate", http.StatusOK, machineResponse)
s.addAllocateReponse(c, http.StatusOK, nil)
controller := s.getController(c)
// Create an arg structure that sets all the values.
args := AllocateMachineArgs{
Expand All @@ -283,15 +323,15 @@ func (s *controllerSuite) TestAllocateMachineArgs(c *gc.C) {
MinMemory: 20000,
Tags: []string{"good"},
NotTags: []string{"bad"},
Networks: []string{"fast"},
NotNetworks: []string{"slow"},
Storage: "root:200(ssd)",
Interfaces: "default:vid=1;db:space=magic",
Zone: "magic",
NotInZone: []string{"not-magic"},
AgentName: "agent 42",
Comment: "testing",
DryRun: true,
}
_, err := controller.AllocateMachine(args)
_, _, err := controller.AllocateMachine(args)
c.Assert(err, jc.ErrorIsNil)

request := s.server.LastRequest()
Expand All @@ -302,14 +342,14 @@ func (s *controllerSuite) TestAllocateMachineArgs(c *gc.C) {
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{})
_, _, 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{})
_, _, err := controller.AllocateMachine(AllocateMachineArgs{})
c.Assert(err, jc.Satisfies, IsUnexpectedError)
}

Expand Down
9 changes: 8 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ func WrapWithDeserializationError(err error, format string, args ...interface{})
// previous error, but wrap it in the new type.
derr := &DeserializationError{Err: errors.NewErr(message + ": " + err.Error())}
derr.SetLocation(1)
return errors.Wrap(err, derr)
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.
Expand Down
5 changes: 4 additions & 1 deletion interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type Controller interface {

// AllocateMachine will attempt to allocate a machine to the user.
// If successful, the allocated machine is returned.
AllocateMachine(AllocateMachineArgs) (Machine, error)
AllocateMachine(AllocateMachineArgs) (Machine, ConstraintMatches, error)

// ReleaseMachines will stop the specified machines, and release them
// from the user making them available to be allocated again.
Expand Down Expand Up @@ -191,6 +191,9 @@ type Machine interface {
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

Zone() Zone

Expand Down
10 changes: 10 additions & 0 deletions machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ func (m *machine) InterfaceSet() []Interface {
return result
}

// Interface implements Machine.
func (m *machine) Interface(id int) Interface {
for _, iface := range m.interfaceSet {
if iface.ID() == id {
return iface
}
}
return nil
}

// OperatingSystem implements Machine.
func (m *machine) OperatingSystem() string {
return m.operatingSystem
Expand Down
3 changes: 3 additions & 0 deletions machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ func (*machineSuite) TestReadMachines(c *gc.C) {

interfaceSet := machine.InterfaceSet()
c.Assert(interfaceSet, gc.HasLen, 1)
id := interfaceSet[0].ID()
c.Assert(machine.Interface(id), jc.DeepEquals, interfaceSet[0])
c.Assert(machine.Interface(id+5), gc.IsNil)
}

func (*machineSuite) TestLowVersion(c *gc.C) {
Expand Down