// Get the VM info from all hosts optionally applying column/row filters. // Returns a map with keys for the hostnames and values as the tabular data // from the host. func globalVmInfo() map[string]VMs { cmdStr := "vm info" res := map[string]VMs{} cmd := minicli.MustCompile(cmdStr) cmd.Record = false for resps := range runCommandGlobally(cmd) { for _, resp := range resps { if resp.Error != "" { log.Errorln(resp.Error) continue } switch data := resp.Data.(type) { case VMs: res[resp.Host] = data default: log.Error("unknown data field in vm info") } } } return res }
func webHosts(w http.ResponseWriter, r *http.Request) { table := htmlTable{ Header: []string{}, Tabular: [][]interface{}{}, ID: "example", Class: "hover", } cmd := minicli.MustCompile("host") cmd.Record = false for resps := range runCommandGlobally(cmd) { for _, resp := range resps { if resp.Error != "" { log.Errorln(resp.Error) continue } if len(table.Header) == 0 && len(resp.Header) > 0 { table.Header = append(table.Header, resp.Header...) } for _, row := range resp.Tabular { res := []interface{}{} for _, v := range row { res = append(res, v) } table.Tabular = append(table.Tabular, res) } } } webRenderTemplate(w, "hosts.html", table) }
// Get the VM info from all hosts optionally applying column/row filters. // Returns a map with keys for the hostnames and values as the tabular data // from the host. func globalVmInfo(masks []string, filters []string) (map[string]VMs, map[string]minicli.Responses) { cmdStr := "vm info" for _, v := range filters { cmdStr = fmt.Sprintf(".filter %s %s", v, cmdStr) } if len(masks) > 0 { cmdStr = fmt.Sprintf(".columns %s %s", strings.Join(masks, ","), cmdStr) } res := map[string]VMs{} res2 := map[string]minicli.Responses{} for resps := range runCommandGlobally(minicli.MustCompile(cmdStr), false) { for _, resp := range resps { if resp.Error != "" { log.Errorln(resp.Error) continue } switch data := resp.Data.(type) { case VMs: res[resp.Host] = data default: log.Error("unknown data field in vm info") } res2[resp.Host] = append(res2[resp.Host], resp) } } return res, res2 }
func webHosts(w http.ResponseWriter, r *http.Request) { hosts := [][]interface{}{} cmd := minicli.MustCompile("host") cmd.SetRecord(false) for resps := range runCommandGlobally(cmd) { for _, resp := range resps { if resp.Error != "" { log.Errorln(resp.Error) continue } for _, row := range resp.Tabular { res := []interface{}{} for _, v := range row { res = append(res, v) } hosts = append(hosts, res) } } } js, err := json.Marshal(hosts) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(js) }
// hostVMs is HostVMs without locking cmdLock. func hostVMs(host string) VMs { // Compile info command and set it not to record cmd := minicli.MustCompile("vm info") cmd.SetRecord(false) cmd.SetSource(GetNamespaceName()) cmds := makeCommandHosts([]string{host}, cmd) var vms VMs // LOCK: see func description. for resps := range runCommands(cmds...) { for _, resp := range resps { if resp.Error != "" { log.Errorln(resp.Error) continue } if vms2, ok := resp.Data.(VMs); ok { if vms != nil { // odd... should only be one vms per host and we're // querying a single host log.Warn("so many vms") } vms = vms2 } } } return vms }
// webScreenshot serves routes like /screenshot/<host>/<id>.png. Optional size // query parameter dictates the size of the screenshot. func webScreenshot(w http.ResponseWriter, r *http.Request) { fields := strings.Split(r.URL.Path, "/") if len(fields) != 4 { http.NotFound(w, r) return } fields = fields[2:] size := r.URL.Query().Get("size") host := fields[0] id := strings.TrimSuffix(fields[1], ".png") cmdStr := fmt.Sprintf("vm screenshot %s file /dev/null %s", id, size) if host != hostname { cmdStr = fmt.Sprintf("mesh send %s .record false %s", host, cmdStr) } cmd := minicli.MustCompile(cmdStr) cmd.Record = false var screenshot []byte for resps := range runCommand(cmd) { for _, resp := range resps { if resp.Error != "" { if strings.HasPrefix(resp.Error, "vm not running:") { continue } else if strings.HasPrefix(resp.Error, "vm does not support screenshots") { continue } // Unknown error log.Errorln(resp.Error) http.Error(w, friendlyError, http.StatusInternalServerError) return } if resp.Data == nil { http.NotFound(w, r) } if screenshot == nil { screenshot = resp.Data.([]byte) } else { log.Error("received more than one response for vm screenshot") } } } if screenshot != nil { w.Write(screenshot) } else { http.NotFound(w, r) } }
// findRemoteVM attempts to find the VM ID of a VM by name or ID on a remote // minimega node. It returns the ID of the VM on the remote host or an error, // which may also be an error communicating with the remote node. func findRemoteVM(host, vm string) (int, string, error) { log.Debug("findRemoteVM: %v %v", host, vm) // check for our own host if host == hostname || host == Localhost { log.Debugln("host is local node") vm := vms.findVm(vm) if vm != nil { log.Debug("got vm: %v %v %v", host, vm.ID, vm.Name) return vm.ID, vm.Name, nil } } else { log.Debugln("remote host") var cmdStr string v, err := strconv.Atoi(vm) if err == nil { cmdStr = fmt.Sprintf(".filter id=%v .columns name,id vm info", v) } else { cmdStr = fmt.Sprintf(".filter name=%v .columns name,id vm info", v) } cmd := minicli.MustCompile(cmdStr) remoteRespChan := make(chan minicli.Responses) go func() { meshageSend(cmd, host, remoteRespChan) close(remoteRespChan) }() for resps := range remoteRespChan { // Find a response that is not an error for _, resp := range resps { if resp.Error == "" && len(resp.Tabular) > 0 { // Found it! row := resp.Tabular[0] // should be name,id name := row[0] id, err := strconv.Atoi(row[1]) if err != nil { log.Debug("malformed response: %#v", resp) } else { return id, name, nil } } } } } return 0, "", vmNotFound(vm) }
// lookupVLAN uses the allocatedVLANs and active namespace to turn a string // into a VLAN. If the VLAN didn't already exist, broadcasts the update to the // cluster. func lookupVLAN(alias string) (int, error) { namespace := GetNamespaceName() vlan, err := allocatedVLANs.ParseVLAN(namespace, alias) if err != vlans.ErrUnallocated { // nil or other error return vlan, err } vlan, created, err := allocatedVLANs.Allocate(namespace, alias) if err != nil { return 0, err } if !created { return vlan, nil } // Broadcast out vlan alias to everyone so that we have a record of the // aliases, should this node crash. s := fmt.Sprintf("vlans add %q %v", alias, vlan) if namespace != "" && !strings.Contains(alias, vlans.AliasSep) { s = fmt.Sprintf("namespace %q %v", namespace, s) } cmd := minicli.MustCompile(s) cmd.SetRecord(false) cmd.SetSource(namespace) respChan, err := meshageSend(cmd, Wildcard) if err != nil { // don't propagate the error since this is supposed to be best-effort. log.Error("unable to broadcast alias update: %v", err) return vlan, nil } // read all the responses, looking for errors go func() { for resps := range respChan { for _, resp := range resps { if resp.Error != "" { log.Info("unable to send alias %v -> %v to %v: %v", alias, vlan, resp.Host, resp.Error) } } } }() return vlan, nil }
// makeCommandHosts creates commands to run the given command on a set of hosts // handling the special case where the local node is included in the list. // makeCommandHosts is namespace-aware -- it generates commands based on the // currently active namespace. func makeCommandHosts(hosts []string, cmd *minicli.Command) []*minicli.Command { // filter out the local host, if included var includeLocal bool var hosts2 []string for _, host := range hosts { if host == hostname { includeLocal = true } else { // Quote the hostname in case there are spaces hosts2 = append(hosts2, fmt.Sprintf("%q", host)) } } var cmds = []*minicli.Command{} if includeLocal { // Create a deep copy of the command by recompiling it cmd2 := minicli.MustCompile(cmd.Original) cmd2.SetRecord(cmd.Record) cmd2.SetSource(cmd.Source) cmds = append(cmds, cmd2) } if len(hosts2) > 0 { ns := GetNamespace() targets := strings.Join(hosts2, ",") // Keep the original CLI input original := cmd.Original // Prefix with namespace, if one is set if ns != nil { original = fmt.Sprintf("namespace %q %v", ns.Name, original) } cmd2 := minicli.MustCompilef("mesh send %s %s", targets, original) cmd2.SetRecord(cmd.Record) cmd2.SetSource(cmd.Source) cmds = append(cmds, cmd2) } return cmds }
// globalVMs is GlobalVMs without locking cmdLock. func globalVMs() VMs { // Compile info command and set it not to record cmd := minicli.MustCompile("vm info") cmd.SetRecord(false) cmd.SetSource(GetNamespaceName()) // Figure out which hosts to query: // * Hosts in the active namespace // * Hosts connected via meshage plus ourselves var hosts []string if ns := GetNamespace(); ns != nil { hosts = ns.hostSlice() } else { hosts = meshageNode.BroadcastRecipients() hosts = append(hosts, hostname) } cmds := makeCommandHosts(hosts, cmd) // Collected VMs vms := VMs{} // LOCK: see func description. for resps := range runCommands(cmds...) { for _, resp := range resps { if resp.Error != "" { log.Errorln(resp.Error) continue } if vms2, ok := resp.Data.(VMs); ok { for _, vm := range vms2 { vms[len(vms)] = vm } } else { log.Error("unknown data field in vm info from %v", resp.Host) } } } return vms }
func main() { var err error flag.Usage = usage flag.Parse() logSetup() // see containerShim() if flag.NArg() > 1 && flag.Arg(0) == CONTAINER_MAGIC { containerShim() } cliSetup() if *f_cli { if err := minicli.Validate(); err != nil { log.Fatalln(err) } doc, err := minicli.Doc() if err != nil { log.Fatal("failed to generate docs: %v", err) } fmt.Println(doc) os.Exit(0) } // rebase f_iomBase if f_base changed but iomBase did not if *f_base != BASE_PATH && *f_iomBase == IOM_PATH { *f_iomBase = filepath.Join(*f_base, "files") } if *f_version { fmt.Println("minimega", version.Revision, version.Date) fmt.Println(version.Copyright) os.Exit(0) } hostname, err = os.Hostname() if err != nil { log.Fatalln(err) } if isReserved(hostname) { log.Warn("hostname `%s` is a reserved word -- abandon all hope, ye who enter here", hostname) } // special case, catch -e and execute a command on an already running // minimega instance if *f_e || *f_attach { // try to connect to the local minimega mm, err := miniclient.Dial(*f_base) if err != nil { log.Fatalln(err) } mm.Pager = minipager.DefaultPager if *f_e { a := flag.Args() log.Debugln("got args:", a) // TODO: Need to escape? cmd := minicli.MustCompile(strings.Join(a, " ")) log.Infoln("got command:", cmd) mm.RunAndPrint(cmd, false) } else { mm.Attach() } return } // warn if we're not root user, err := user.Current() if err != nil { log.Fatalln(err) } if user.Uid != "0" { log.Warnln("not running as root") } // check for a running instance of minimega _, err = os.Stat(filepath.Join(*f_base, "minimega")) if err == nil { if !*f_force { log.Fatalln("minimega appears to already be running, override with -force") } log.Warn("minimega may already be running, proceed with caution") err = os.Remove(filepath.Join(*f_base, "minimega")) if err != nil { log.Fatalln(err) } } // set up signal handling sig := make(chan os.Signal, 1024) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) go func() { first := true for s := range sig { if s == os.Interrupt && first { // do nothing continue } if *f_panic { panic("teardown") } if first { log.Info("caught signal, tearing down, ctrl-c again will force quit") go teardown() first = false } else { os.Exit(1) } } }() err = checkExternal() if err != nil { log.Warnln(err.Error()) } // attempt to set up the base path err = os.MkdirAll(*f_base, os.FileMode(0770)) if err != nil { log.Fatal("mkdir base path: %v", err) } pid := os.Getpid() writeOrDie(filepath.Join(*f_base, "minimega.pid"), strconv.Itoa(pid)) go commandSocketStart() // create a node for meshage host, err := os.Hostname() if err != nil { log.Fatalln(err) } meshageInit(host, *f_context, *f_degree, *f_msaTimeout, *f_port) // start the cc service ccStart() // start tap reaper go periodicReapTaps() fmt.Println(banner) // fan out to the number of cpus on the system if GOMAXPROCS env variable is // not set. if os.Getenv("GOMAXPROCS") == "" { cpus := runtime.NumCPU() runtime.GOMAXPROCS(cpus) } if !*f_nostdin { cliLocal() } else { <-sig if *f_panic { panic("teardown") } } teardown() }
// webScreenshot serves routes like /screenshot/<host>/<id>.png. Optional size // query parameter dictates the size of the screenshot. func webScreenshot(w http.ResponseWriter, r *http.Request) { fields := strings.Split(r.URL.Path, "/") if len(fields) != 4 { http.NotFound(w, r) return } fields = fields[2:] size := r.URL.Query().Get("size") host := fields[0] id := strings.TrimSuffix(fields[1], ".png") do_encode := r.URL.Query().Get("base64") != "" cmdStr := fmt.Sprintf("vm screenshot %s file /dev/null %s", id, size) if host != hostname { cmdStr = fmt.Sprintf("mesh send %s %s", host, cmdStr) } cmd := minicli.MustCompile(cmdStr) cmd.SetRecord(false) var screenshot []byte for resps := range RunCommands(cmd) { for _, resp := range resps { if resp.Error != "" { if strings.HasPrefix(resp.Error, "vm not running:") { continue } else if resp.Error == "cannot take screenshot of container" { continue } else if strings.HasPrefix(resp.Error, "cannot take screenshot of container") { continue } // Unknown error log.Errorln(resp.Error) http.Error(w, friendlyError, http.StatusInternalServerError) return } if resp.Data == nil { http.NotFound(w, r) } if screenshot == nil { screenshot = resp.Data.([]byte) } else { log.Error("received more than one response for vm screenshot") } } } if screenshot != nil { if do_encode { base64string := "data:image/png;base64," + base64.StdEncoding.EncodeToString(screenshot) w.Write([]byte(base64string)) } else { w.Write(screenshot) } } else { http.NotFound(w, r) } }
// dot returns a graphviz 'dotfile' string representing the experiment topology // from the perspective of this node. func cliDot(c *minicli.Command) *minicli.Response { resp := &minicli.Response{Host: hostname} // Create the file before running any commands incase there is an error fout, err := os.Create(c.StringArgs["filename"]) if err != nil { resp.Error = err.Error() return resp } defer fout.Close() // TODO: Rewrite to use runCommandGlobally cmd := minicli.MustCompile(".columns host,name,id,ip,ip6,state,vlan vm info") writer := bufio.NewWriter(fout) fmt.Fprintln(writer, "graph minimega {") fmt.Fprintln(writer, `size=\"8,11\";`) fmt.Fprintln(writer, "overlap=false;") //fmt.Fprintf(fout, "Legend [shape=box, shape=plaintext, label=\"total=%d\"];\n", len(n.effectiveNetwork)) var expVms []*dotVM // Get info from local hosts by invoking command directly for resp := range minicli.ProcessCommand(cmd, false) { expVms = append(expVms, dotProcessInfo(resp)...) } // Get info from remote hosts over meshage remoteRespChan := make(chan minicli.Responses) go func() { meshageBroadcast(cmd, remoteRespChan) close(remoteRespChan) }() for resp := range remoteRespChan { expVms = append(expVms, dotProcessInfo(resp)...) } vlans := make(map[string]bool) for _, v := range expVms { color := stateToColor[v.State] fmt.Fprintf(writer, "%s [style=filled, color=%s];\n", v.Text, color) for _, c := range v.Vlans { fmt.Fprintf(writer, "%s -- %s\n", v.Text, c) vlans[c] = true } } for k, _ := range vlans { fmt.Fprintf(writer, "%s;\n", k) } fmt.Fprint(writer, "}") err = writer.Flush() if err != nil { resp.Error = err.Error() } return resp }
func main() { var err error flag.Usage = usage flag.Parse() if !strings.HasSuffix(*f_base, "/") { *f_base += "/" } if *f_cli { doc, err := minicli.Doc() if err != nil { log.Fatal("failed to generate docs: %v", err) } fmt.Println(doc) os.Exit(0) } // rebase f_iomBase if f_base changed but iomBase did not if *f_base != BASE_PATH && *f_iomBase == IOM_PATH { *f_iomBase = *f_base + "files" } if !strings.HasSuffix(*f_iomBase, "/") { *f_iomBase += "/" } if *f_version { fmt.Println("minimega", version.Revision, version.Date) fmt.Println(version.Copyright) os.Exit(0) } logSetup() hostname, err = os.Hostname() if err != nil { log.Fatalln(err) } if isReserved(hostname) { log.Warn("hostname `%s` is a reserved word -- abandon all hope, ye who enter here", hostname) } vms = make(map[int]VM) // special case, catch -e and execute a command on an already running // minimega instance if *f_e || *f_attach { // try to connect to the local minimega mm, err := miniclient.Dial(*f_base) if err != nil { log.Fatalln(err) } if *f_e { a := flag.Args() log.Debugln("got args:", a) // TODO: Need to escape? cmd := minicli.MustCompile(strings.Join(a, " ")) log.Infoln("got command:", cmd) mm.RunAndPrint(cmd, false) } else { mm.Attach() } return } // warn if we're not root user, err := user.Current() if err != nil { log.Fatalln(err) } if user.Uid != "0" { log.Warnln("not running as root") } // check for a running instance of minimega _, err = os.Stat(*f_base + "minimega") if err == nil { if !*f_force { log.Fatalln("minimega appears to already be running, override with -force") } log.Warn("minimega may already be running, proceed with caution") err = os.Remove(*f_base + "minimega") if err != nil { log.Fatalln(err) } } // set up signal handling sig := make(chan os.Signal, 1024) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) go func() { first := true for { <-sig if *f_panic { panic("teardown") } if first { log.Info("caught signal, tearing down, ctrl-c again will force quit") go teardown() first = false } else { os.Exit(1) } } }() err = checkExternal() if err != nil { log.Warnln(err.Error()) } // attempt to set up the base path err = os.MkdirAll(*f_base, os.FileMode(0770)) if err != nil { log.Fatal("mkdir base path: %v", err) } pid := os.Getpid() err = ioutil.WriteFile(*f_base+"minimega.pid", []byte(fmt.Sprintf("%v", pid)), 0664) if err != nil { log.Error("write minimega pid: %v", err) teardown() } go commandSocketStart() // create a node for meshage host, err := os.Hostname() if err != nil { log.Fatalln(err) } meshageInit(host, *f_namespace, uint(*f_degree), *f_port) fmt.Println(banner) // fan out to the number of cpus on the system if GOMAXPROCS env variable is // not set. if os.Getenv("GOMAXPROCS") == "" { cpus := runtime.NumCPU() runtime.GOMAXPROCS(cpus) } if !*f_nostdin { cliLocal() } else { <-sig if *f_panic { panic("teardown") } } teardown() }