check point commit to ensure things are saved in more than one place. this commit contains the first integration of the docker build artifacts as well as the first integration of an automation test environment for MAAS based on virtual box
Change-Id: I236f12392501b4ed589aba2b748ba0c45e148f2e
diff --git a/automation/state.go b/automation/state.go
new file mode 100644
index 0000000..4b94089
--- /dev/null
+++ b/automation/state.go
@@ -0,0 +1,422 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+
+ maas "github.com/juju/gomaasapi"
+)
+
+// Action how to get from there to here
+type Action func(*maas.MAASObject, MaasNode, ProcessingOptions) error
+
+// Transition the map from where i want to be from where i might be
+type Transition struct {
+ Target string
+ Current string
+ Using Action
+}
+
+// ProcessingOptions used to determine on what hosts to operate
+type ProcessingOptions struct {
+ Filter struct {
+ Zones struct {
+ Include []string
+ Exclude []string
+ }
+ Hosts struct {
+ Include []string
+ Exclude []string
+ }
+ }
+ Mappings map[string]interface{}
+ Verbose bool
+ Preview bool
+ AlwaysRename bool
+}
+
+// Transitions the actual map
+//
+// Currently this is a hand compiled / optimized "next step" table. This should
+// really be generated from the state machine chart input. Once this has been
+// accomplished you should be able to determine the action to take given your
+// target state and your current state.
+var Transitions = map[string]map[string]Action{
+ "Deployed": {
+ "New": Commission,
+ "Deployed": Done,
+ "Ready": Aquire,
+ "Allocated": Deploy,
+ "Retired": AdminState,
+ "Reserved": AdminState,
+ "Releasing": Wait,
+ "DiskErasing": Wait,
+ "Deploying": Wait,
+ "Commissioning": Wait,
+ "Missing": Fail,
+ "FailedReleasing": Fail,
+ "FailedDiskErasing": Fail,
+ "FailedDeployment": Fail,
+ "Broken": Fail,
+ "FailedCommissioning": Fail,
+ },
+}
+
+const (
+ // defaultStateMachine Would be nice to drive from a graph language
+ defaultStateMachine string = `
+ (New)->(Commissioning)
+ (Commissioning)->(FailedCommissioning)
+ (FailedCommissioning)->(New)
+ (Commissioning)->(Ready)
+ (Ready)->(Deploying)
+ (Ready)->(Allocated)
+ (Allocated)->(Deploying)
+ (Deploying)->(Deployed)
+ (Deploying)->(FailedDeployment)
+ (FailedDeployment)->(Broken)
+ (Deployed)->(Releasing)
+ (Releasing)->(FailedReleasing)
+ (FailedReleasing)->(Broken)
+ (Releasing)->(DiskErasing)
+ (DiskErasing)->(FailedEraseDisk)
+ (FailedEraseDisk)->(Broken)
+ (Releasing)->(Ready)
+ (DiskErasing)->(Ready)
+ (Broken)->(Ready)`
+)
+
+// updateName - changes the name of the MAAS node based on the configuration file
+func updateNodeName(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ macs := node.MACs()
+
+ // Get current node name and strip off domain name
+ current := node.Hostname()
+ if i := strings.IndexRune(current, '.'); i != -1 {
+ current = current[:i]
+ }
+ for _, mac := range macs {
+ if entry, ok := options.Mappings[mac]; ok {
+ if name, ok := entry.(map[string]interface{})["hostname"]; ok && current != name.(string) {
+ nodesObj := client.GetSubObject("nodes")
+ nodeObj := nodesObj.GetSubObject(node.ID())
+ log.Printf("RENAME '%s' to '%s'\n", node.Hostname(), name.(string))
+
+ if !options.Preview {
+ nodeObj.Update(url.Values{"hostname": []string{name.(string)}})
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// Done we are at the target state, nothing to do
+var Done = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ // As devices are normally in the "COMPLETED" state we don't want to
+ // log this fact unless we are in verbose mode. I suspect it would be
+ // nice to log it once when the device transitions from a non COMPLETE
+ // state to a complete state, but that would require keeping state.
+ if options.Verbose {
+ log.Printf("COMPLETE: %s", node.Hostname())
+ }
+
+ if options.AlwaysRename {
+ updateNodeName(client, node, options)
+ }
+
+ return nil
+}
+
+// Deploy cause a node to deploy
+var Deploy = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ log.Printf("DEPLOY: %s", node.Hostname())
+
+ if options.AlwaysRename {
+ updateNodeName(client, node, options)
+ }
+
+ if !options.Preview {
+ nodesObj := client.GetSubObject("nodes")
+ myNode := nodesObj.GetSubObject(node.ID())
+ // Start the node with the trusty distro. This should really be looked up or
+ // a parameter default
+ _, err := myNode.CallPost("start", url.Values {"distro_series" : []string{"trusty"}})
+ if err != nil {
+ log.Printf("ERROR: DEPLOY '%s' : '%s'", node.Hostname(), err)
+ return err
+ }
+ }
+ return nil
+}
+
+// Aquire aquire a machine to a specific operator
+var Aquire = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ log.Printf("AQUIRE: %s", node.Hostname())
+ nodesObj := client.GetSubObject("nodes")
+
+ if options.AlwaysRename {
+ updateNodeName(client, node, options)
+ }
+
+ if !options.Preview {
+ // With a new version of MAAS we have to make sure the node is linked
+ // to the subnet vid DHCP before we move to the Aquire state. To do this
+ // We need to unlink the interface to the subnet and then relink it.
+ //
+ // Iterate through all the interfaces on the node, searching for ones
+ // that are valid and not DHCP and move them to DHCP
+ ifcsObj := client.GetSubObject("nodes").GetSubObject(node.ID()).GetSubObject("interfaces")
+ ifcsListObj, err := ifcsObj.CallGet("", url.Values{})
+ if err != nil {
+ return err
+ }
+
+ ifcsArray, err := ifcsListObj.GetArray()
+ if err != nil {
+ return err
+ }
+
+ for _, ifc := range ifcsArray {
+ ifcMap, err := ifc.GetMap()
+ if err != nil {
+ return err
+ }
+
+ // Iterate over the links assocated with the interface, looking for
+ // links with a subnect as well as a mode of "auto"
+ links, ok := ifcMap["links"]
+ if ok {
+ linkArray, err := links.GetArray()
+ if err != nil {
+ return err
+ }
+
+ for _, link := range linkArray {
+ linkMap, err := link.GetMap()
+ if err != nil {
+ return err
+ }
+ subnet, ok := linkMap["subnet"]
+ if ok {
+ subnetMap, err := subnet.GetMap()
+ if err != nil {
+ return err
+ }
+
+ val, err := linkMap["mode"].GetString()
+ if err != nil {
+ return err
+ }
+
+ if val == "auto" {
+ // Found one we like, so grab the subnet from the data and
+ // then relink this as DHCP
+ cidr, err := subnetMap["cidr"].GetString()
+ if err != nil {
+ return err
+ }
+
+ fifcID, err := ifcMap["id"].GetFloat64()
+ if err != nil {
+ return err
+ }
+ ifcID := strconv.Itoa(int(fifcID))
+
+ flID, err := linkMap["id"].GetFloat64()
+ if err != nil {
+ return err
+ }
+ lID := strconv.Itoa(int(flID))
+
+ ifcObj := ifcsObj.GetSubObject(ifcID)
+ _, err = ifcObj.CallPost("unlink_subnet", url.Values{"id": []string{lID}})
+ if err != nil {
+ return err
+ }
+ _, err = ifcObj.CallPost("link_subnet", url.Values{"mode": []string{"DHCP"}, "subnet": []string{cidr}})
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ }
+ _, err = nodesObj.CallPost("acquire",
+ url.Values{"name": []string{node.Hostname()}})
+ if err != nil {
+ log.Printf("ERROR: AQUIRE '%s' : '%s'", node.Hostname(), err)
+ return err
+ }
+ }
+ return nil
+}
+
+// Commission cause a node to be commissioned
+var Commission = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ updateNodeName(client, node, options)
+
+ // Need to understand the power state of the node. We only want to move to "Commissioning" if the node
+ // power is off. If the node power is not off, then turn it off.
+ state := node.PowerState()
+ switch state {
+ case "on":
+ // Attempt to turn the node off
+ log.Printf("POWER DOWN: %s", node.Hostname())
+ if !options.Preview {
+ //POST /api/1.0/nodes/{system_id}/ op=stop
+ nodesObj := client.GetSubObject("nodes")
+ nodeObj := nodesObj.GetSubObject(node.ID())
+ _, err := nodeObj.CallPost("stop", url.Values{"stop_mode" : []string{"soft"}})
+ if err != nil {
+ log.Printf("ERROR: Commission '%s' : changing power start to off : '%s'", node.Hostname(), err)
+ }
+ return err
+ }
+ break
+ case "off":
+ // We are off so move to commissioning
+ log.Printf("COMISSION: %s", node.Hostname())
+ if !options.Preview {
+ nodesObj := client.GetSubObject("nodes")
+ nodeObj := nodesObj.GetSubObject(node.ID())
+
+ updateNodeName(client, node, options)
+
+ _, err := nodeObj.CallPost("commission", url.Values{})
+ if err != nil {
+ log.Printf("ERROR: Commission '%s' : '%s'", node.Hostname(), err)
+ }
+ return err
+ }
+ break
+ default:
+ // We are in a state from which we can't move forward.
+ log.Printf("ERROR: %s has invalid power state '%s'", node.Hostname(), state)
+ break
+ }
+ return nil
+}
+
+// Wait a do nothing state, while work is being done
+var Wait = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ log.Printf("WAIT: %s", node.Hostname())
+ return nil
+}
+
+// Fail a state from which we cannot, currently, automatically recover
+var Fail = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ log.Printf("FAIL: %s", node.Hostname())
+ return nil
+}
+
+// AdminState an administrative state from which we should make no automatic transition
+var AdminState = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ log.Printf("ADMIN: %s", node.Hostname())
+ return nil
+}
+
+func findAction(target string, current string) (Action, error) {
+ targets, ok := Transitions[target]
+ if !ok {
+ log.Printf("[warn] unable to find transitions to target state '%s'", target)
+ return nil, fmt.Errorf("Could not find transition to target state '%s'", target)
+ }
+
+ action, ok := targets[current]
+ if !ok {
+ log.Printf("[warn] unable to find transition from current state '%s' to target state '%s'",
+ current, target)
+ return nil, fmt.Errorf("Could not find transition from current state '%s' to target state '%s'",
+ current, target)
+ }
+
+ return action, nil
+}
+
+// ProcessNode something
+func ProcessNode(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+ substatus, err := node.GetInteger("substatus")
+ if err != nil {
+ return err
+ }
+ action, err := findAction("Deployed", MaasNodeStatus(substatus).String())
+ if err != nil {
+ return err
+ }
+
+ if options.Preview {
+ action(client, node, options)
+ } else {
+ go action(client, node, options)
+ }
+ return nil
+}
+
+func buildFilter(filter []string) ([]*regexp.Regexp, error) {
+
+ results := make([]*regexp.Regexp, len(filter))
+ for i, v := range filter {
+ r, err := regexp.Compile(v)
+ if err != nil {
+ return nil, err
+ }
+ results[i] = r
+ }
+ return results, nil
+}
+
+func matchedFilter(include []*regexp.Regexp, target string) bool {
+ for _, e := range include {
+ if e.MatchString(target) {
+ return true
+ }
+ }
+ return false
+}
+
+// ProcessAll something
+func ProcessAll(client *maas.MAASObject, nodes []MaasNode, options ProcessingOptions) []error {
+ errors := make([]error, len(nodes))
+ includeHosts, err := buildFilter(options.Filter.Hosts.Include)
+ if err != nil {
+ log.Fatalf("[error] invalid regular expression for include filter '%s' : %s", options.Filter.Hosts.Include, err)
+ }
+
+ includeZones, err := buildFilter(options.Filter.Zones.Include)
+ if err != nil {
+ log.Fatalf("[error] invalid regular expression for include filter '%v' : %s", options.Filter.Zones.Include, err)
+ }
+
+ for i, node := range nodes {
+ // For hostnames we always match on an empty filter
+ if len(includeHosts) >= 0 && matchedFilter(includeHosts, node.Hostname()) {
+
+ // For zones we don't match on an empty filter
+ if len(includeZones) >= 0 && matchedFilter(includeZones, node.Zone()) {
+ err := ProcessNode(client, node, options)
+ if err != nil {
+ errors[i] = err
+ } else {
+ errors[i] = nil
+ }
+ } else {
+ if options.Verbose {
+ log.Printf("[info] ignoring node '%s' as its zone '%s' didn't match include zone name filter '%v'",
+ node.Hostname(), node.Zone(), options.Filter.Zones.Include)
+ }
+ }
+ } else {
+ if options.Verbose {
+ log.Printf("[info] ignoring node '%s' as it didn't match include hostname filter '%v'",
+ node.Hostname(), options.Filter.Hosts.Include)
+ }
+ }
+ }
+ return errors
+}