// Copyright 2018 ThousandEyes Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package polling import ( "bytes" "errors" "fmt" "sort" "text/template" "time" "github.com/thousandeyes/shoelaces/internal/event" "github.com/thousandeyes/shoelaces/internal/log" "github.com/thousandeyes/shoelaces/internal/mappings" "github.com/thousandeyes/shoelaces/internal/server" "github.com/thousandeyes/shoelaces/internal/templates" "github.com/thousandeyes/shoelaces/internal/utils" ) // ManualAction represent an action taken when no automatic boot is available. type ManualAction int const ( maxRetry = 10 retryScript = "#!ipxe\n" + "prompt --key 0x02 --timeout 10000 shoelaces: Press Ctrl-B for manual override... && " + "chain -ar http://{{.baseURL}}/ipxemenu || " + "chain -ar http://{{.baseURL}}/poll/1/{{.macAddress}}\n" timeoutScript = "#!ipxe\n" + "exit\n" // BootAction is used when a user selects a script for the polling // server. The server polls once again, so it gets the selected script // as answer. BootAction ManualAction = 0 // RetryAction is used when a server polling does not yet have a script // selected by the user, hence it has to retry. RetryAction ManualAction = 1 // TimeoutAction is used when a server polling is timing out. TimeoutAction ManualAction = 2 ) // ListServers provides a list of the servers that tried to boot // but did not match the hostname regex or network mappings. func ListServers(serverStates *server.States) server.Servers { ret := make([]server.Server, 0) serverStates.RLock() for _, s := range serverStates.Servers { if s.Target == server.InitTarget { ret = append(ret, s.Server) } } defer serverStates.RUnlock() sort.Sort(server.Servers(ret)) return ret } // UpdateTarget receives parameters for booting manually. When a host // didn't match any of the automatic methods for booting, it's going to be // put on hold. This method is called when something is finally chosen for // that host. func UpdateTarget(logger log.Logger, serverStates *server.States, templateRenderer *templates.ShoelacesTemplates, eventLog *event.Log, baseURL string, srv server.Server, scriptName string, envName string, params map[string]interface{}) (inputErr bool, err error) { if !utils.IsValidMAC(srv.Mac) { return true, errors.New("Invalid MAC") } // Test the template with user inputs setHostName(params, srv.Mac) params["baseURL"] = utils.BaseURLforEnvName(baseURL, envName) _, err = templateRenderer.RenderTemplate(logger, scriptName, params, envName) if err != nil { inputErr = true return } serverStates.Lock() defer serverStates.Unlock() servers := serverStates.Servers if servers[srv.Mac] == nil { return true, errors.New("MAC is not in the booting state") } hostname := servers[srv.Mac].Server.Hostname logger.Debug("component", "polling", "msg", "Setting server override", "server", srv.Mac, "target", scriptName, "environment", envName, "hostname", hostname, "params", params) eventLog.AddEvent(event.UserSelection, srv, "", scriptName, nil) servers[srv.Mac].Target = scriptName servers[srv.Mac].Environment = envName servers[srv.Mac].Params = params return false, nil } // Poll contains the main logic of Shoelaces. It uses several heuristics to find // the right script to return, as network maps, hostname maps and manual // selection. func Poll(logger log.Logger, serverStates *server.States, hostnameMaps []mappings.HostnameMap, networkMaps []mappings.NetworkMap, eventLog *event.Log, templateRenderer *templates.ShoelacesTemplates, baseURL string, srv server.Server) (scriptText string, err error) { script, found := attemptAutomaticBoot(logger, hostnameMaps, networkMaps, templateRenderer, eventLog, baseURL, srv) if found { return script, nil } return manualAction(logger, serverStates, templateRenderer, eventLog, baseURL, srv) } func attemptAutomaticBoot(logger log.Logger, hostnameMaps []mappings.HostnameMap, networkMaps []mappings.NetworkMap, templateRenderer *templates.ShoelacesTemplates, eventLog *event.Log, baseURL string, srv server.Server) (scriptText string, found bool) { // Find with reverse hostname matched with the hostname regexps if script, found := mappings.FindScriptForHostname(hostnameMaps, srv.Hostname); found { logger.Debug("component", "polling", "msg", "Host found", "where", "hostname-mapping", "host", srv.Hostname) eventLog.AddEvent(event.HostBoot, srv, event.PtrMatchBoot, script.Name, script.Params) script.Params["hostname"] = srv.Hostname return genBootScript(logger, templateRenderer, baseURL, script), found } logger.Debug("component", "polling", "msg", "Host not found", "where", "hostname-mapping", "host", srv.Hostname) // Find with IP belonging to a configured subnet if script, found := mappings.FindScriptForNetwork(networkMaps, srv.IP); found { logger.Debug("component", "polling", "msg", "Host found", "where", "network-mapping", "ip", srv.IP) setHostName(script.Params, srv.Mac) srv.Hostname = script.Params["hostname"].(string) eventLog.AddEvent(event.HostBoot, srv, event.SubnetMatchBoot, script.Name, script.Params) return genBootScript(logger, templateRenderer, baseURL, script), found } logger.Debug("component", "polling", "msg", "Host not found", "where", "network-mapping", "ip", srv.IP) return "", false } func manualAction(logger log.Logger, serverStates *server.States, templateRenderer *templates.ShoelacesTemplates, eventLog *event.Log, baseURL string, srv server.Server) (scriptText string, err error) { script, action := chooseManualAction(logger, serverStates, eventLog, srv) logger.Debug("component", "polling", "target-script-name", script, "action", action) switch action { case BootAction: setHostName(script.Params, srv.Mac) srv.Hostname = script.Params["hostname"].(string) eventLog.AddEvent(event.HostBoot, srv, event.ManualBoot, script.Name, script.Params) return genBootScript(logger, templateRenderer, baseURL, script), nil case RetryAction: return genRetryScript(logger, baseURL, srv.Mac), nil case TimeoutAction: return timeoutScript, nil default: logger.Info("component", "polling", "msg", "Unknown action") return "", fmt.Errorf("%s", "Unknown action") } } func chooseManualAction(logger log.Logger, serverStates *server.States, eventLog *event.Log, srv server.Server) (*mappings.Script, ManualAction) { serverStates.Lock() defer serverStates.Unlock() if m := serverStates.Servers[srv.Mac]; m != nil { if m.Target != server.InitTarget { serverStates.DeleteServer(srv.Mac) logger.Debug("component", "polling", "msg", "Server boot", "mac", srv.Mac) return &mappings.Script{ Name: m.Target, Environment: m.Environment, Params: m.Params}, BootAction } else if m.Retry <= maxRetry { m.Retry++ m.LastAccess = int(time.Now().UTC().Unix()) logger.Debug("component", "polling", "msg", "Retrying reboot", "mac", srv.Mac) return nil, RetryAction } else { serverStates.DeleteServer(srv.Mac) logger.Debug("component", "polling", "msg", "Timing out server", "mac", srv.Mac) return nil, TimeoutAction } } serverStates.AddServer(srv) logger.Debug("component", "polling", "msg", "New server", "mac", srv.Mac) eventLog.AddEvent(event.HostPoll, srv, "", "", nil) return nil, RetryAction } func setHostName(params map[string]interface{}, mac string) { if _, ok := params["hostname"]; !ok { hostname := utils.MacColonToDash(mac) if hnPrefix, ok := params["hostnamePrefix"]; ok { hnPrefixStr, isString := hnPrefix.(string) if !isString { hnPrefixStr = "" } params["hostname"] = hnPrefixStr + hostname } else { params["hostname"] = hostname } } } func genBootScript(logger log.Logger, templateRenderer *templates.ShoelacesTemplates, baseURL string, script *mappings.Script) string { script.Params["baseURL"] = utils.BaseURLforEnvName(baseURL, script.Environment) text, err := templateRenderer.RenderTemplate(logger, script.Name, script.Params, script.Environment) if err != nil { panic(err) } return text } func genRetryScript(logger log.Logger, baseURL string, mac string) string { variablesMap := map[string]interface{}{} parsedTemplate := &bytes.Buffer{} tmpl, err := template.New("retry").Parse(retryScript) if err != nil { logger.Info("component", "polling", "msg", "Error parsing retry template", "mac", mac) panic(err) } variablesMap["baseURL"] = baseURL variablesMap["macAddress"] = utils.MacColonToDash(mac) err = tmpl.Execute(parsedTemplate, variablesMap) if err != nil { logger.Info("component", "polling", "msg", "Error executing retry template", "mac", mac) panic(err) } return parsedTemplate.String() }