updated with changes to support automated triggering of post-deploymet provisioning
diff --git a/automation/Dockerfile b/automation/Dockerfile
index 5e1be43..d1b6318 100644
--- a/automation/Dockerfile
+++ b/automation/Dockerfile
@@ -3,6 +3,14 @@
 RUN apk --update add git
 
 WORKDIR /go
-RUN go get github.com/ciena/cord-maas-automation
+RUN go get github.com/tools/godep
+ADD . /go/src/github.com/ciena/cord-maas-automation
+
+WORKDIR /go/src/github.com/ciena/cord-maas-automation
+RUN /go/bin/godep restore
+
+WORKDIR /go
+
+RUN go install github.com/ciena/cord-maas-automation
 
 ENTRYPOINT ["/go/bin/cord-maas-automation"]
diff --git a/automation/Dockerfile.ansible b/automation/Dockerfile.ansible
new file mode 100644
index 0000000..4b5a239
--- /dev/null
+++ b/automation/Dockerfile.ansible
@@ -0,0 +1,21 @@
+FROM ubuntu:14.04
+
+RUN apt-get update -y && \
+    sudo apt-get install -y  software-properties-common && \
+    apt-add-repository ppa:ansible/ansible && \
+    apt-get update -y && \
+    apt-get install -y git ansible golang
+
+RUN mkdir /go
+WORKDIR /go
+ENV GOPATH=/go
+RUN go get github.com/tools/godep
+ADD . /go/src/github.com/ciena/cord-maas-automation
+
+WORKDIR /go/src/github.com/ciena/cord-maas-automation
+RUN /go/bin/godep restore
+
+WORKDIR /go
+RUN go install github.com/ciena/cord-maas-automation
+
+ENTRYPOINT ["/go/bin/cord-maas-automation"]
diff --git a/automation/Godeps/Godeps.json b/automation/Godeps/Godeps.json
index 77ff54f..c55b337 100644
--- a/automation/Godeps/Godeps.json
+++ b/automation/Godeps/Godeps.json
@@ -6,6 +6,11 @@
 	],
 	"Deps": [
 		{
+			"ImportPath": "github.com/fzzy/radix/redis",
+			"Comment": "v0.5.6-2-g3528e87",
+			"Rev": "3528e87a910c6730810cec1be187f989d7d9442a"
+		},
+		{
 			"ImportPath": "github.com/juju/gomaasapi",
 			"Rev": "e173bc8d8d3304ff11b0ded5f6d4eea0cb560a40"
 		},
diff --git a/automation/README.md b/automation/README.md
index ee49648..aa76475 100644
--- a/automation/README.md
+++ b/automation/README.md
@@ -84,3 +84,43 @@
 Currently (January 26, 2016) the automation only supports a deployed target
 state and will not act on hosts that are in a failed, broken, or error state.
 ![](lifecycle.png)
+
+### Post Deployment Provisioning
+All the states in the state machine are defined and maintained by
+MAAS except the states Provisioning, ProvisionError, and Provisioned. These
+states are used to track the post-deployment provisioning that is part of the
+automation.
+
+Post deployment provisioning can be accomplished either by the specification of
+a script to execute or the specification of a URL to trigger.
+
+#### Executing a Script
+A script to execute to post deploy provision a node can be specified via the
+environment as `PROVISION_URL`. After a node is deployed this script will be
+executed with three (3) parameters:
+- node ID - the node ID that MAAS uses to track the node
+- name - the name of the node in MAAS
+- ip - the IP address assigned to the node
+
+It is important to note that when executing a script that the script is run
+within the docker container that is running the automation. Thus any script
+must be mounted as a volume into the container.
+
+#### Triggering a URL
+Alternatively the automation can trigger a URL to POST deploy provision a node.
+In this instance, automation will `POST` a `JSON` object to the specified URL
+with the values:
+- "id" - the node ID that MAAS uses to track the node
+- "name" - the name of the node in MAAS
+- "ip" - the IP address assigned to the node
+
+The provider specified should return "202 Accept" to acknowledge the acceptance
+of the request. The automation controller will poll for status on the
+provisioning so the provider should accept the request but not process it
+while the client is blocked.
+
+The automation controller will periodically poll for provisioning status for a
+given node by doing a `HTTP GET` on the specified provisioning URL appended with
+`/` and the `ID` of the node. The provider should either return `202 Accept` if
+the node is still being provisioned, `200 OK` if the provisioning is complete
+and successful, or any other response which will be treated as an error.
diff --git a/automation/lifecycle.png b/automation/lifecycle.png
index 422f247..a81847e 100644
--- a/automation/lifecycle.png
+++ b/automation/lifecycle.png
Binary files differ
diff --git a/automation/maas-flow.go b/automation/maas-flow.go
index 1514e32..f443cef 100644
--- a/automation/maas-flow.go
+++ b/automation/maas-flow.go
@@ -26,6 +26,9 @@
            }
 	}`
 	defaultMapping = "{}"
+	PROVISION_URL  = "PROVISION_URL"
+	PROVISION_TTL  = "PROVISION_TTL"
+	DEFAULT_TTL    = "30m"
 )
 
 var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
@@ -92,6 +95,20 @@
 		Preview:      *preview,
 		Verbose:      *verbose,
 		AlwaysRename: *always,
+		ProvTracker:  NewTracker(),
+		ProvisionURL: os.Getenv(PROVISION_URL),
+	}
+
+	var ttl string
+	if ttl = os.Getenv(PROVISION_TTL); ttl == "" {
+		ttl = "30m"
+	}
+
+	var err error
+	options.ProvisionTTL, err = time.ParseDuration(ttl)
+	if err != nil {
+		log.Printf("[warn] unable to parse specified duration of '%s', defaulting to '%s'",
+			ttl, DEFAULT_TTL)
 	}
 
 	// Determine the filter, this can either be specified on the the command
diff --git a/automation/node.go b/automation/node.go
index d364fe0..fe61add 100644
--- a/automation/node.go
+++ b/automation/node.go
@@ -80,6 +80,27 @@
 	return hn
 }
 
+// IPs get the IP Addresses
+func (n *MaasNode) IPs() []string {
+	ifaceObj, _ := n.GetMap()["interface_set"]
+	ifaces, _ := ifaceObj.GetArray()
+	result := []string{}
+
+	for _, iface := range ifaces {
+		obj, _ := iface.GetMap()
+		linksObj, _ := obj["links"]
+		links, _ := linksObj.GetArray()
+		for _, link := range links {
+			linkObj, _ := link.GetMap()
+			ipObj, _ := linkObj["ip_address"]
+			ip, _ := ipObj.GetString()
+			result = append(result, ip)
+		}
+	}
+
+	return result
+}
+
 // MACs get the MAC Addresses
 func (n *MaasNode) MACs() []string {
 	macsObj, _ := n.GetMap()["macaddress_set"]
diff --git a/automation/state.go b/automation/state.go
index 4b94089..696f58f 100644
--- a/automation/state.go
+++ b/automation/state.go
@@ -1,12 +1,17 @@
 package main
 
 import (
+	"bytes"
+	"encoding/json"
 	"fmt"
 	"log"
+	"net/http"
 	"net/url"
+	"os/exec"
 	"regexp"
 	"strconv"
 	"strings"
+	"time"
 
 	maas "github.com/juju/gomaasapi"
 )
@@ -37,6 +42,9 @@
 	Verbose      bool
 	Preview      bool
 	AlwaysRename bool
+	ProvTracker  Tracker
+	ProvisionURL string
+	ProvisionTTL time.Duration
 }
 
 // Transitions the actual map
@@ -45,24 +53,24 @@
 // 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{
+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,
+		"New":                 []Action{Reset, Commission},
+		"Deployed":            []Action{Provision, Done},
+		"Ready":               []Action{Reset, Aquire},
+		"Allocated":           []Action{Reset, Deploy},
+		"Retired":             []Action{Reset, AdminState},
+		"Reserved":            []Action{Reset, AdminState},
+		"Releasing":           []Action{Reset, Wait},
+		"DiskErasing":         []Action{Reset, Wait},
+		"Deploying":           []Action{Reset, Wait},
+		"Commissioning":       []Action{Reset, Wait},
+		"Missing":             []Action{Reset, Fail},
+		"FailedReleasing":     []Action{Reset, Fail},
+		"FailedDiskErasing":   []Action{Reset, Fail},
+		"FailedDeployment":    []Action{Reset, Fail},
+		"Broken":              []Action{Reset, Fail},
+		"FailedCommissioning": []Action{Reset, Fail},
 	},
 }
 
@@ -87,7 +95,11 @@
         (FailedEraseDisk)->(Broken)
         (Releasing)->(Ready)
         (DiskErasing)->(Ready)
-        (Broken)->(Ready)`
+        (Broken)->(Ready)
+	(Deployed)->(Provisioning)
+	(Provisioning)->(ProvisionError)
+	(ProvisionError)->(Provisioning)
+	(Provisioning)->(Provisioned)`
 )
 
 // updateName - changes the name of the MAAS node based on the configuration file
@@ -115,6 +127,178 @@
 	return nil
 }
 
+// Reset we are at the target state, nothing to do
+var Reset = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+	if options.Verbose {
+		log.Printf("RESET: %s", node.Hostname())
+	}
+
+	if options.AlwaysRename {
+		updateNodeName(client, node, options)
+	}
+
+	options.ProvTracker.Clear(node.ID())
+
+	return nil
+}
+
+// Provision we are at the target state, nothing to do
+var Provision = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+	if options.Verbose {
+		log.Printf("CHECK PROVISION: %s", node.Hostname())
+	}
+
+	if options.AlwaysRename {
+		updateNodeName(client, node, options)
+	}
+
+	record, err := options.ProvTracker.Get(node.ID())
+	if options.Verbose {
+		log.Printf("[info] Current state of node '%s' is '%s'", node.Hostname(), record.State.String())
+	}
+	if err != nil {
+		log.Printf("[warn] unable to retrieve provisioning state of node '%s' : %s", node.Hostname(), err)
+	} else if record.State == Unprovisioned || record.State == ProvisionError {
+		var err error = nil
+		var callout *url.URL
+		log.Printf("PROVISION '%s'", node.Hostname())
+		if len(options.ProvisionURL) > 0 {
+			if options.Verbose {
+				log.Printf("[info] Provisioning callout to '%s'", options.ProvisionURL)
+			}
+			callout, err = url.Parse(options.ProvisionURL)
+			if err != nil {
+				log.Printf("[error] Failed to parse provisioning URL '%s' : %s", options.ProvisionURL, err)
+			} else {
+				ips := node.IPs()
+				ip := ""
+				if len(ips) > 0 {
+					ip = ips[0]
+				}
+				switch callout.Scheme {
+				// If the scheme is a file, then we will execute the refereced file
+				case "", "file":
+					if options.Verbose {
+						log.Printf("[info] executing local script file '%s'", callout.Path)
+					}
+					record.State = Provisioning
+					record.Timestamp = time.Now().Unix()
+					options.ProvTracker.Set(node.ID(), record)
+					err = exec.Command(callout.Path, node.ID(), node.Hostname(), ip).Run()
+					if err != nil {
+						log.Printf("[error] Failed to execute '%s' : %s", options.ProvisionURL, err)
+					} else {
+						if options.Verbose {
+							log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
+								node.Hostname(), node.ID())
+						}
+						record.State = Provisioned
+						options.ProvTracker.Set(node.ID(), record)
+					}
+
+				default:
+					if options.Verbose {
+						log.Printf("[info] POSTing to '%s'", options.ProvisionURL)
+					}
+					data := map[string]string{
+						"id":   node.ID(),
+						"name": node.Hostname(),
+						"ip":   ip,
+					}
+					hc := http.Client{}
+					var b []byte
+					b, err = json.Marshal(data)
+					if err != nil {
+						log.Printf("[error] Unable to marshal node data : %s", err)
+					} else {
+						var req *http.Request
+						var resp *http.Response
+						if options.Verbose {
+							log.Printf("[debug] POSTing data '%s'", string(b))
+						}
+						req, err = http.NewRequest("POST", options.ProvisionURL, bytes.NewReader(b))
+						if err != nil {
+							log.Printf("[error] Unable to construct POST request to provisioner : %s",
+								err)
+						} else {
+							req.Header.Add("Content-Type", "application/json")
+							resp, err = hc.Do(req)
+							if err != nil {
+								log.Printf("[error] Unable to process POST request : %s",
+									err)
+							} else {
+								defer resp.Body.Close()
+								if resp.StatusCode == 202 {
+									record.State = Provisioning
+								} else {
+									record.State = ProvisionError
+								}
+								options.ProvTracker.Set(node.ID(), record)
+							}
+						}
+					}
+				}
+			}
+		}
+
+		if err != nil {
+			if options.Verbose {
+				log.Printf("[warn] Not marking node '%s' with ID '%s' as provisioned, because of error '%s'",
+					node.Hostname(), node.ID(), err)
+				record.State = ProvisionError
+				options.ProvTracker.Set(node.ID(), record)
+			}
+		}
+	} else if record.State == Provisioning && time.Since(time.Unix(record.Timestamp, 0)) > options.ProvisionTTL {
+		log.Printf("[error] Provisioning of node '%s' has passed provisioning TTL of '%v'",
+			node.Hostname(), options.ProvisionTTL)
+		record.State = ProvisionError
+		options.ProvTracker.Set(node.ID(), record)
+	} else if record.State == Provisioning {
+		callout, err := url.Parse(options.ProvisionURL)
+		if err != nil {
+			log.Printf("[error] Unable to parse provisioning URL '%s' : %s", options.ProvisionURL, err)
+		} else if callout.Scheme != "file" {
+			var req *http.Request
+			var resp *http.Response
+			if options.Verbose {
+				log.Printf("[info] Fetching provisioning state for node '%s'", node.Hostname())
+			}
+			req, err = http.NewRequest("GET", options.ProvisionURL+"/"+node.ID(), nil)
+			if err != nil {
+				log.Printf("[error] Unable to construct GET request to provisioner : %s", err)
+			} else {
+				hc := http.Client{}
+				resp, err = hc.Do(req)
+				if err != nil {
+					log.Printf("[error] Failed to quest provision state for node '%s' : %s",
+						node.Hostname(), err)
+				} else {
+					switch resp.StatusCode {
+					case 200: // OK - provisioning completed
+						if options.Verbose {
+							log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
+								node.Hostname(), node.ID())
+						}
+						record.State = Provisioned
+						options.ProvTracker.Set(node.ID(), record)
+					case 202: // Accepted - in the provisioning state
+						// Noop, presumably alread in this state
+					default: // Consider anything else an erorr
+						record.State = ProvisionError
+						options.ProvTracker.Set(node.ID(), record)
+					}
+				}
+			}
+		}
+	} else if options.Verbose {
+		log.Printf("[info] Not invoking provisioning for '%s', currned state is '%s'", node.Hostname(),
+			record.State.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
@@ -145,7 +329,7 @@
 		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"}})
+		_, err := myNode.CallPost("start", url.Values{"distro_series": []string{"trusty"}})
 		if err != nil {
 			log.Printf("ERROR: DEPLOY '%s' : '%s'", node.Hostname(), err)
 			return err
@@ -269,10 +453,10 @@
 		// 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
+			//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"}})
+			_, 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)
 			}
@@ -321,14 +505,14 @@
 	return nil
 }
 
-func findAction(target string, current string) (Action, error) {
+func findActions(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]
+	actions, ok := targets[current]
 	if !ok {
 		log.Printf("[warn] unable to find transition from current state '%s' to target state '%s'",
 			current, target)
@@ -336,7 +520,19 @@
 			current, target)
 	}
 
-	return action, nil
+	return actions, nil
+}
+
+// ProcessActions
+func ProcessActions(actions []Action, client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
+	var err error
+	for _, action := range actions {
+		if err = action(client, node, options); err != nil {
+			log.Printf("[error] Error while processing action for node '%s' : %s", node.Hostname, err)
+			break
+		}
+	}
+	return err
 }
 
 // ProcessNode something
@@ -345,15 +541,15 @@
 	if err != nil {
 		return err
 	}
-	action, err := findAction("Deployed", MaasNodeStatus(substatus).String())
+	actions, err := findActions("Deployed", MaasNodeStatus(substatus).String())
 	if err != nil {
 		return err
 	}
 
 	if options.Preview {
-		action(client, node, options)
+		ProcessActions(actions, client, node, options)
 	} else {
-		go action(client, node, options)
+		go ProcessActions(actions, client, node, options)
 	}
 	return nil
 }
diff --git a/automation/tracker.go b/automation/tracker.go
new file mode 100644
index 0000000..9f38b23
--- /dev/null
+++ b/automation/tracker.go
@@ -0,0 +1,137 @@
+package main
+
+import (
+	"encoding/json"
+	"github.com/fzzy/radix/redis"
+	"log"
+	"net/url"
+	"os"
+)
+
+type ProvisionState int8
+
+const (
+	Unprovisioned ProvisionState = iota
+	ProvisionError
+	Provisioning
+	Provisioned
+)
+
+func (s *ProvisionState) String() string {
+	switch *s {
+	case Unprovisioned:
+		return "UNPROVISIONED"
+	case ProvisionError:
+		return "PROVISIONERROR"
+	case Provisioning:
+		return "PROVISIONING"
+	case Provisioned:
+		return "PROVISIONED"
+	default:
+		return "UNKNOWN"
+	}
+}
+
+// TrackerRecord state kept for each node to be provisioned
+type TrackerRecord struct {
+	State ProvisionState
+
+	// Timeestamp maintains the time the node started provisioning, eventually will be used to time out
+	// provisinion states
+	Timestamp int64
+}
+
+// Tracker used to track if a node has been post deployed provisioned
+type Tracker interface {
+	Get(key string) (*TrackerRecord, error)
+	Set(key string, record *TrackerRecord) error
+	Clear(key string) error
+}
+
+// RedisTracker redis implementation of the tracker interface
+type RedisTracker struct {
+	client *redis.Client
+}
+
+func (t *RedisTracker) Get(key string) (*TrackerRecord, error) {
+	reply := t.client.Cmd("get", key)
+	if reply.Err != nil {
+		return nil, reply.Err
+	}
+	if reply.Type == redis.NilReply {
+		var record TrackerRecord
+		record.State = Unprovisioned
+		return &record, nil
+	}
+
+	value, err := reply.Str()
+	if err != nil {
+		return nil, err
+	}
+	var record TrackerRecord
+	err = json.Unmarshal([]byte(value), &record)
+	if err != nil {
+		return nil, err
+	}
+	return &record, nil
+}
+
+func (t *RedisTracker) Set(key string, record *TrackerRecord) error {
+	reply := t.client.Cmd("set", key, true)
+	return reply.Err
+}
+
+func (t *RedisTracker) Clear(key string) error {
+	reply := t.client.Cmd("del", key)
+	return reply.Err
+}
+
+// MemoryTracker in memory implementation of the tracker interface
+type MemoryTracker struct {
+	data map[string]TrackerRecord
+}
+
+func (m *MemoryTracker) Get(key string) (*TrackerRecord, error) {
+	if value, ok := m.data[key]; ok {
+		return &value, nil
+	}
+	var record TrackerRecord
+	record.State = Unprovisioned
+	return &record, nil
+}
+
+func (m *MemoryTracker) Set(key string, record *TrackerRecord) error {
+	m.data[key] = *record
+	return nil
+}
+
+func (m *MemoryTracker) Clear(key string) error {
+	delete(m.data, key)
+	return nil
+}
+
+// NetTracker constructs an implemetation of the Tracker interface. Which implementation selected
+//            depends on the environment. If a link to a redis instance is defined then this will
+//            be used, else an in memory version will be used.
+func NewTracker() Tracker {
+	// Check the environment to see if we are linked to a redis DB
+	if os.Getenv("AUTODB_ENV_REDIS_VERSION") != "" {
+		tracker := new(RedisTracker)
+		if spec := os.Getenv("AUTODB_PORT"); spec != "" {
+			port, err := url.Parse(spec)
+			checkError(err, "[error] unable to lookup to redis database : %s", err)
+			tracker.client, err = redis.Dial(port.Scheme, port.Host)
+			checkError(err, "[error] unable to connect to redis database : '%s' : %s", port, err)
+			log.Println("[info] Using REDIS to track provisioning status of nodes")
+			return tracker
+		} else {
+			log.Fatalf("[error] looks like we are configured for REDIS, but no PORT defined in environment")
+		}
+	}
+
+	// Else fallback to an in memory tracker
+	tracker := new(MemoryTracker)
+	tracker.data = make(map[string]TrackerRecord)
+	log.Println("[info] Using memory based structures to track provisioning status of nodes")
+	return tracker
+}
diff --git a/build.gradle b/build.gradle
index dc49de6..8a32e71 100644
--- a/build.gradle
+++ b/build.gradle
@@ -43,7 +43,16 @@
 }
 
 task buildAutomationImage(type: Exec) {
-    commandLine '/usr/bin/docker', 'build', '-t', "cord-maas-automation", "./automation"
+    commandLine '/usr/bin/docker', 'build', '-t', "cord-maas-automation", "-f", "./automation/Dockerfile", "./automation"
+}
+
+task buildAutomationImageAnsible(type: Exec) {
+    commandLine '/usr/bin/docker', 'build', '-t', "cord-maas-automation:ansible", "-f", "./automation/Dockerfile.ansible", "./automation"
+}
+
+task buildAutomationImages {
+    dependsOn buildAutomationImage
+    dependsOn buildAutomationImageAnsible
 }
 
 task tagAutomationImage(type: Exec) {
@@ -51,11 +60,31 @@
     commandLine '/usr/bin/docker', 'tag', 'cord-maas-automation', "$targetReg/cord-maas-automation:$targetTag"
 }
 
+task tagAutomationImageAnsible(type: Exec) {
+    dependsOn buildAutomationImageAnsible
+    commandLine '/usr/bin/docker', 'tag', 'cord-maas-automation:ansible', "$targetReg/cord-maas-automation:$targetTag-ansible"
+}
+
+task tagAutomationImages {
+    dependsOn tagAutomationImage
+    dependsOn tagAutomationImageAnsible
+}
+
 task publishAutomationImage(type: Exec) {
     dependsOn tagAutomationImage
     commandLine '/usr/bin/docker', 'push', "$targetReg/cord-maas-automation:$targetTag"
 }
 
+task publishAutomationImageAnsible(type: Exec) {
+    dependsOn tagAutomationImageAnsible
+    commandLine '/usr/bin/docker', 'push', "$targetReg/cord-maas-automation:$targetTag-ansible"
+}
+
+task publishAutomationImages {
+    dependsOn publishAutomationImage
+    dependsOn publishAutomationImageAnsible
+}
+
 task buildHarvesterImage(type: Exec) {
     commandLine '/usr/bin/docker', 'build', '-t', "cord-maas-dhcp-harvester", "./harvester"
 }
@@ -85,19 +114,23 @@
 task buildImages {
     dependsOn buildBootstrapImage
     dependsOn buildHarvesterImage
-    dependsOn buildAutomationImage
+    dependsOn buildAutomationImages
 }
 
 task tagImages {
     dependsOn tagBootstrapImage
     dependsOn tagHarvesterImage
-    dependsOn tagAutomationImage
+    dependsOn tagAutomationImages
 }
 
 task publish {
     dependsOn publishBootstrapImage
     dependsOn publishHarvesterImage
-    dependsOn publishAutomationImage
+    dependsOn publishAutomationImages
+}
+
+task build {
+    dependsOn buildImages
 }
 
 // ~~~~~~~~~~~~~~~~~~~ Deployment / Test Tasks  ~~~~~~~~~~~~~~~~~~~~~~~