| package main |
| |
| import ( |
| "encoding/json" |
| "flag" |
| "github.com/kelseyhightower/envconfig" |
| "log" |
| "net/url" |
| "os" |
| "strings" |
| "time" |
| "unicode" |
| |
| maas "github.com/juju/gomaasapi" |
| ) |
| |
| const ( |
| // defaultFilter specifies the default filter to use when none is specified |
| defaultFilter = `{ |
| "hosts" : { |
| "include" : [ ".*" ], |
| "exclude" : [] |
| }, |
| "zones" : { |
| "include" : ["default"], |
| "exclude" : [] |
| } |
| }` |
| defaultMapping = "{}" |
| ) |
| |
| type Config struct { |
| PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER"` |
| PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST"` |
| PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT"` |
| ProvisionUrl string `default:"" envconfig:"PROVISION_URL"` |
| ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL"` |
| } |
| |
| var apiKey = flag.String("apikey", "", "key with which to access MAAS server") |
| var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS") |
| var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access") |
| var queryPeriod = flag.String("period", "15s", "frequency the MAAS service is polled for node states") |
| var preview = flag.Bool("preview", false, "displays the action that would be taken, but does not do the action, in this mode the nodes are processed only once") |
| var mappings = flag.String("mappings", "{}", "the mac to name mappings") |
| var always = flag.Bool("always-rename", true, "attempt to rename at every stage of workflow") |
| var verbose = flag.Bool("verbose", false, "display verbose logging") |
| var filterSpec = flag.String("filter", strings.Map(func(r rune) rune { |
| if unicode.IsSpace(r) { |
| return -1 |
| } |
| return r |
| }, defaultFilter), "constrain by hostname what will be automated") |
| |
| // checkError if the given err is not nil, then fatally log the message, else |
| // return false. |
| func checkError(err error, message string, v ...interface{}) bool { |
| if err != nil { |
| log.Fatalf("[error] "+message, v) |
| } |
| return false |
| } |
| |
| // checkWarn if the given err is not nil, then log the message as a warning and |
| // return true, else return false. |
| func checkWarn(err error, message string, v ...interface{}) bool { |
| if err != nil { |
| log.Printf("[warn] "+message, v) |
| return true |
| } |
| return false |
| } |
| |
| // fetchNodes do a HTTP GET to the MAAS server to query all the nodes |
| func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) { |
| nodeListing := client.GetSubObject("nodes") |
| listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) |
| if checkWarn(err, "unable to get the list of all nodes: %s", err) { |
| return nil, err |
| } |
| listNodes, err := listNodeObjects.GetArray() |
| if checkWarn(err, "unable to get the node objects for the list: %s", err) { |
| return nil, err |
| } |
| |
| var nodes = make([]MaasNode, len(listNodes)) |
| for index, nodeObj := range listNodes { |
| node, err := nodeObj.GetMAASObject() |
| if !checkWarn(err, "unable to retrieve object for node: %s", err) { |
| nodes[index] = MaasNode{node} |
| } |
| } |
| return nodes, nil |
| } |
| |
| func main() { |
| |
| flag.Parse() |
| config := Config{} |
| err := envconfig.Process("AUTOMATION", &config) |
| checkError(err, "[error] unable to parse environment options : %s", err) |
| |
| options := ProcessingOptions{ |
| Preview: *preview, |
| Verbose: *verbose, |
| AlwaysRename: *always, |
| Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}), |
| ProvisionURL: config.ProvisionUrl, |
| PowerHelper: config.PowerHelperScript, |
| PowerHelperUser: config.PowerHelperUser, |
| PowerHelperHost: config.PowerHelperHost, |
| } |
| |
| options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl) |
| checkError(err, "[error] unable to parse specified duration of '%s' : %s", err) |
| |
| // Determine the filter, this can either be specified on the the command |
| // line as a value or a file reference. If none is specified the default |
| // will be used |
| if len(*filterSpec) > 0 { |
| if (*filterSpec)[0] == '@' { |
| name := os.ExpandEnv((*filterSpec)[1:]) |
| file, err := os.OpenFile(name, os.O_RDONLY, 0) |
| checkError(err, "[error] unable to open file '%s' to load the filter : %s", name, err) |
| decoder := json.NewDecoder(file) |
| err = decoder.Decode(&options.Filter) |
| checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err) |
| } else { |
| err := json.Unmarshal([]byte(*filterSpec), &options.Filter) |
| checkError(err, "[error] unable to parse filter specification: '%s' : %s", *filterSpec, err) |
| } |
| } else { |
| err := json.Unmarshal([]byte(defaultFilter), &options.Filter) |
| checkError(err, "[error] unable to parse default filter specificiation: '%s' : %s", defaultFilter, err) |
| } |
| |
| // Determine the mac to name mapping, this can either be specified on the the command |
| // line as a value or a file reference. If none is specified the default |
| // will be used |
| if len(*mappings) > 0 { |
| if (*mappings)[0] == '@' { |
| name := os.ExpandEnv((*mappings)[1:]) |
| file, err := os.OpenFile(name, os.O_RDONLY, 0) |
| checkError(err, "[error] unable to open file '%s' to load the mac name mapping : %s", name, err) |
| decoder := json.NewDecoder(file) |
| err = decoder.Decode(&options.Mappings) |
| checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err) |
| } else { |
| err := json.Unmarshal([]byte(*mappings), &options.Mappings) |
| checkError(err, "[error] unable to parse mac name mapping: '%s' : %s", *mappings, err) |
| } |
| } else { |
| err := json.Unmarshal([]byte(defaultMapping), &options.Mappings) |
| checkError(err, "[error] unable to parse default mac name mappings: '%s' : %s", defaultMapping, err) |
| } |
| |
| // Verify the specified period for queries can be converted into a Go duration |
| period, err := time.ParseDuration(*queryPeriod) |
| checkError(err, "[error] unable to parse specified query period duration: '%s': %s", queryPeriod, err) |
| |
| log.Printf(`Configuration: |
| MAAS URL: %s |
| MAAS API Version: %s |
| MAAS Query Interval: %s |
| Node Filter: %s |
| Node Name Mappings: %s |
| Preview: %v |
| Verbose: %v |
| Always Rename: %v |
| Provision URL: %s |
| Provision TTL: %s |
| Power Helper: %s |
| Power Helper User: %s |
| Power Helper Host: %s`, |
| *maasURL, *apiVersion, *queryPeriod, *filterSpec, *mappings, options.Preview, |
| options.Verbose, options.AlwaysRename, options.ProvisionURL, options.ProvisionTTL, |
| options.PowerHelper, options.PowerHelperUser, options.PowerHelperHost) |
| |
| authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion) |
| if err != nil { |
| checkError(err, "[error] Unable to use specified client key, '%s', to authenticate to the MAAS server: %s", *apiKey, err) |
| } |
| |
| // Create an object through which we will communicate with MAAS |
| client := maas.NewMAAS(*authClient) |
| |
| // This utility essentially polls the MAAS server for node state and |
| // process the node to the next state. This is done by kicking off the |
| // process every specified duration. This means that the first processing of |
| // nodes will have "period" in the future. This is really not the behavior |
| // we want, we really want, do it now, and then do the next one in "period". |
| // So, the code does one now. |
| nodes, _ := fetchNodes(client) |
| ProcessAll(client, nodes, options) |
| |
| if !(*preview) { |
| // Create a ticker and fetch and process the nodes every "period" |
| ticker := time.NewTicker(period) |
| for t := range ticker.C { |
| log.Printf("[info] query server at %s", t) |
| nodes, _ := fetchNodes(client) |
| ProcessAll(client, nodes, options) |
| } |
| } |
| } |