// Run is the main execution entrypoint to run mgmt. func (obj *Main) Run() error { var start = time.Now().UnixNano() var flags int if obj.DEBUG || true { // TODO: remove || true flags = log.LstdFlags | log.Lshortfile } flags = (flags - log.Ldate) // remove the date for now log.SetFlags(flags) // un-hijack from capnslog... log.SetOutput(os.Stderr) if obj.VERBOSE { capnslog.SetFormatter(capnslog.NewLogFormatter(os.Stderr, "(etcd) ", flags)) } else { capnslog.SetFormatter(capnslog.NewNilFormatter()) } log.Printf("This is: %s, version: %s", obj.Program, obj.Version) log.Printf("Main: Start: %v", start) hostname, err := os.Hostname() // a sensible default // allow passing in the hostname, instead of using the system setting if h := obj.Hostname; h != nil && *h != "" { // override by cli hostname = *h } else if err != nil { return errwrap.Wrapf(err, "Can't get default hostname!") } if hostname == "" { // safety check return fmt.Errorf("Hostname cannot be empty!") } var prefix = fmt.Sprintf("/var/lib/%s/", obj.Program) // default prefix if p := obj.Prefix; p != nil { prefix = *p } // make sure the working directory prefix exists if obj.TmpPrefix || os.MkdirAll(prefix, 0770) != nil { if obj.TmpPrefix || obj.AllowTmpPrefix { var err error if prefix, err = ioutil.TempDir("", obj.Program+"-"+hostname+"-"); err != nil { return fmt.Errorf("Main: Error: Can't create temporary prefix!") } log.Println("Main: Warning: Working prefix directory is temporary!") } else { return fmt.Errorf("Main: Error: Can't create prefix!") } } log.Printf("Main: Working prefix is: %s", prefix) var wg sync.WaitGroup var G, oldGraph *pgraph.Graph // exit after `max-runtime` seconds for no reason at all... if i := obj.MaxRuntime; i > 0 { go func() { time.Sleep(time.Duration(i) * time.Second) obj.Exit(nil) }() } // setup converger converger := converger.NewConverger( obj.ConvergedTimeout, nil, // stateFn gets added in by EmbdEtcd ) go converger.Loop(true) // main loop for converger, true to start paused // embedded etcd if len(obj.seeds) == 0 { log.Printf("Main: Seeds: No seeds specified!") } else { log.Printf("Main: Seeds(%d): %v", len(obj.seeds), obj.seeds) } EmbdEtcd := etcd.NewEmbdEtcd( hostname, obj.seeds, obj.clientURLs, obj.serverURLs, obj.NoServer, obj.idealClusterSize, prefix, converger, ) if EmbdEtcd == nil { // TODO: verify EmbdEtcd is not nil below... obj.Exit(fmt.Errorf("Main: Etcd: Creation failed!")) } else if err := EmbdEtcd.Startup(); err != nil { // startup (returns when etcd main loop is running) obj.Exit(fmt.Errorf("Main: Etcd: Startup failed: %v", err)) } convergerStateFn := func(b bool) error { // exit if we are using the converged timeout and we are the // root node. otherwise, if we are a child node in a remote // execution hierarchy, we should only notify our converged // state and wait for the parent to trigger the exit. if t := obj.ConvergedTimeout; obj.Depth == 0 && t >= 0 { if b { log.Printf("Converged for %d seconds, exiting!", t) obj.Exit(nil) // trigger an exit! } return nil } // send our individual state into etcd for others to see return etcd.EtcdSetHostnameConverged(EmbdEtcd, hostname, b) // TODO: what should happen on error? } if EmbdEtcd != nil { converger.SetStateFn(convergerStateFn) } var gapiChan chan error // stream events are nil errors if obj.GAPI != nil { data := gapi.Data{ Hostname: hostname, EmbdEtcd: EmbdEtcd, Noop: obj.Noop, NoWatch: obj.NoWatch, } if err := obj.GAPI.Init(data); err != nil { obj.Exit(fmt.Errorf("Main: GAPI: Init failed: %v", err)) } else if !obj.NoWatch { gapiChan = obj.GAPI.SwitchStream() // stream of graph switch events! } } exitchan := make(chan struct{}) // exit on close go func() { startChan := make(chan struct{}) // start signal go func() { startChan <- struct{}{} }() log.Println("Etcd: Starting...") etcdChan := etcd.EtcdWatch(EmbdEtcd) first := true // first loop or not for { log.Println("Main: Waiting...") select { case <-startChan: // kick the loop once at start // pass case b := <-etcdChan: if !b { // ignore the message continue } // everything else passes through to cause a compile! case err, ok := <-gapiChan: if !ok { // channel closed if obj.DEBUG { log.Printf("Main: GAPI exited") } gapiChan = nil // disable it continue } if err != nil { obj.Exit(err) // trigger exit continue //return // TODO: return or wait for exitchan? } if obj.NoWatch { // extra safety for bad GAPI's log.Printf("Main: GAPI stream should be quiet with NoWatch!") // fix the GAPI! continue // no stream events should be sent } case <-exitchan: return } if obj.GAPI == nil { log.Printf("Config: GAPI is empty!") continue } // we need the vertices to be paused to work on them, so // run graph vertex LOCK... if !first { // TODO: we can flatten this check out I think converger.Pause() // FIXME: add sync wait? G.Pause() // sync //G.UnGroup() // FIXME: implement me if needed! } // make the graph from yaml, lib, puppet->yaml, or dsl! newGraph, err := obj.GAPI.Graph() // generate graph! if err != nil { log.Printf("Config: Error creating new graph: %v", err) // unpause! if !first { G.Start(&wg, first) // sync converger.Start() // after G.Start() } continue } // apply the global noop parameter if requested if obj.Noop { for _, m := range newGraph.GraphMetas() { m.Noop = obj.Noop } } // FIXME: make sure we "UnGroup()" any semi-destructive // changes to the resources so our efficient GraphSync // will be able to re-use and cmp to the old graph. newFullGraph, err := newGraph.GraphSync(oldGraph) if err != nil { log.Printf("Config: Error running graph sync: %v", err) // unpause! if !first { G.Start(&wg, first) // sync converger.Start() // after G.Start() } continue } oldGraph = newFullGraph // save old graph G = oldGraph.Copy() // copy to active graph G.AutoEdges() // add autoedges; modifies the graph G.AutoGroup() // run autogroup; modifies the graph // TODO: do we want to do a transitive reduction? log.Printf("Graph: %v", G) // show graph if obj.GraphvizFilter != "" { if err := G.ExecGraphviz(obj.GraphvizFilter, obj.Graphviz); err != nil { log.Printf("Graphviz: %v", err) } else { log.Printf("Graphviz: Successfully generated graph!") } } G.AssociateData(converger) // G.Start(...) needs to be synchronous or wait, // because if half of the nodes are started and // some are not ready yet and the EtcdWatch // loops, we'll cause G.Pause(...) before we // even got going, thus causing nil pointer errors G.Start(&wg, first) // sync converger.Start() // after G.Start() first = false } }() configWatcher := recwatch.NewConfigWatcher() events := configWatcher.Events() if !obj.NoWatch { configWatcher.Add(obj.Remotes...) // add all the files... } else { events = nil // signal that no-watch is true } go func() { select { case err := <-configWatcher.Error(): obj.Exit(err) // trigger an exit! case <-exitchan: return } }() // initialize the add watcher, which calls the f callback on map changes convergerCb := func(f func(map[string]bool) error) (func(), error) { return etcd.EtcdAddHostnameConvergedWatcher(EmbdEtcd, f) } // build remotes struct for remote ssh remotes := remote.NewRemotes( EmbdEtcd.LocalhostClientURLs().StringSlice(), []string{etcd.DefaultClientURL}, obj.Noop, obj.Remotes, // list of files events, // watch for file changes obj.CConns, obj.AllowInteractive, obj.SSHPrivIDRsa, !obj.NoCaching, obj.Depth, prefix, converger, convergerCb, obj.Program, ) // TODO: is there any benefit to running the remotes above in the loop? // wait for etcd to be running before we remote in, which we do above! go remotes.Run() if obj.GAPI == nil { converger.Start() // better start this for empty graphs } log.Println("Main: Running...") reterr := <-obj.exit // wait for exit signal log.Println("Destroy...") if obj.GAPI != nil { if err := obj.GAPI.Close(); err != nil { err = errwrap.Wrapf(err, "GAPI closed poorly!") reterr = multierr.Append(reterr, err) // list of errors } } configWatcher.Close() // stop sending file changes to remotes if err := remotes.Exit(); err != nil { // tell all the remote connections to shutdown; waits! err = errwrap.Wrapf(err, "Remote exited poorly!") reterr = multierr.Append(reterr, err) // list of errors } G.Exit() // tell all the children to exit // tell inner main loop to exit close(exitchan) // cleanup etcd main loop last so it can process everything first if err := EmbdEtcd.Destroy(); err != nil { // shutdown and cleanup etcd err = errwrap.Wrapf(err, "Etcd exited poorly!") reterr = multierr.Append(reterr, err) // list of errors } if obj.DEBUG { log.Printf("Main: Graph: %v", G) } wg.Wait() // wait for primary go routines to exit // TODO: wait for each vertex to exit... log.Println("Goodbye!") return reterr }
func main() { var flags int if global.DEBUG || true { // TODO: remove || true flags = log.LstdFlags | log.Lshortfile } flags = (flags - log.Ldate) // remove the date for now log.SetFlags(flags) // un-hijack from capnslog... log.SetOutput(os.Stderr) if global.VERBOSE { capnslog.SetFormatter(capnslog.NewLogFormatter(os.Stderr, "(etcd) ", flags)) } else { capnslog.SetFormatter(capnslog.NewNilFormatter()) } // test for sanity if program == "" || version == "" { log.Fatal("Program was not compiled correctly. Please see Makefile.") } app := cli.NewApp() app.Name = program app.Usage = "next generation config management" app.Version = version //app.Action = ... // without a default action, help runs app.Commands = []cli.Command{ { Name: "run", Aliases: []string{"r"}, Usage: "run", Action: run, Flags: []cli.Flag{ cli.StringFlag{ Name: "file, f", Value: "", Usage: "graph definition to run", EnvVar: "MGMT_FILE", }, cli.BoolFlag{ Name: "no-watch", Usage: "do not update graph on watched graph definition file changes", }, cli.StringFlag{ Name: "code, c", Value: "", Usage: "code definition to run", }, cli.StringFlag{ Name: "graphviz, g", Value: "", Usage: "output file for graphviz data", }, cli.StringFlag{ Name: "graphviz-filter, gf", Value: "dot", // directed graph default Usage: "graphviz filter to use", }, // useful for testing multiple instances on same machine cli.StringFlag{ Name: "hostname", Value: "", Usage: "hostname to use", }, // if empty, it will startup a new server cli.StringSliceFlag{ Name: "seeds, s", Value: &cli.StringSlice{}, // empty slice Usage: "default etc client endpoint", EnvVar: "MGMT_SEEDS", }, // port 2379 and 4001 are common cli.StringSliceFlag{ Name: "client-urls", Value: &cli.StringSlice{}, Usage: "list of URLs to listen on for client traffic", EnvVar: "MGMT_CLIENT_URLS", }, // port 2380 and 7001 are common cli.StringSliceFlag{ Name: "server-urls, peer-urls", Value: &cli.StringSlice{}, Usage: "list of URLs to listen on for server (peer) traffic", EnvVar: "MGMT_SERVER_URLS", }, cli.BoolFlag{ Name: "no-server", Usage: "do not let other servers peer with me", }, cli.IntFlag{ Name: "ideal-cluster-size", Value: etcd.DefaultIdealClusterSize, Usage: "ideal number of server peers in cluster, only read by initial server", EnvVar: "MGMT_IDEAL_CLUSTER_SIZE", }, cli.IntFlag{ Name: "converged-timeout, t", Value: -1, Usage: "exit after approximately this many seconds in a converged state", EnvVar: "MGMT_CONVERGED_TIMEOUT", }, cli.IntFlag{ Name: "max-runtime", Value: 0, Usage: "exit after a maximum of approximately this many seconds", EnvVar: "MGMT_MAX_RUNTIME", }, cli.BoolFlag{ Name: "noop", Usage: "globally force all resources into no-op mode", }, cli.StringFlag{ Name: "puppet, p", Value: "", Usage: "load graph from puppet, optionally takes a manifest or path to manifest file", }, cli.StringFlag{ Name: "puppet-conf", Value: "", Usage: "supply the path to an alternate puppet.conf file to use", }, cli.StringSliceFlag{ Name: "remote", Value: &cli.StringSlice{}, Usage: "list of remote graph definitions to run", }, cli.BoolFlag{ Name: "allow-interactive", Usage: "allow interactive prompting, such as for remote passwords", }, cli.StringFlag{ Name: "ssh-priv-id-rsa", Value: "~/.ssh/id_rsa", Usage: "default path to ssh key file, set empty to never touch", EnvVar: "MGMT_SSH_PRIV_ID_RSA", }, cli.IntFlag{ Name: "cconns", Value: 0, Usage: "number of maximum concurrent remote ssh connections to run, 0 for unlimited", EnvVar: "MGMT_CCONNS", }, cli.BoolFlag{ Name: "no-caching", Usage: "don't allow remote caching of remote execution binary", }, cli.IntFlag{ Name: "depth", Hidden: true, // internal use only Value: 0, Usage: "specify depth in remote hierarchy", }, cli.StringFlag{ Name: "prefix", Usage: "specify a path to the working prefix directory", EnvVar: "MGMT_PREFIX", }, cli.BoolFlag{ Name: "tmp-prefix", Usage: "request a pseudo-random, temporary prefix to be used", }, cli.BoolFlag{ Name: "allow-tmp-prefix", Usage: "allow creation of a new temporary prefix if main prefix is unavailable", }, }, }, } app.EnableBashCompletion = true app.Run(os.Args) }