github.com/mfpierre/corectl@v0.5.6/run.go (about)

     1  // Copyright 2015 - António Meireles  <antonio.meireles@reformi.st>
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  
    16  package main
    17  
    18  import (
    19  	"encoding/base64"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"log"
    24  	"net/http"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/TheNewNormal/corectl/uuid2ip"
    33  	"github.com/TheNewNormal/libxhyve"
    34  	"github.com/satori/go.uuid"
    35  	"github.com/spf13/cobra"
    36  	"github.com/spf13/pflag"
    37  	"github.com/spf13/viper"
    38  	// until github.com/mitchellh/go-ps consumes it
    39  	"github.com/yeonsh/go-ps"
    40  )
    41  
    42  var (
    43  	runCmd = &cobra.Command{
    44  		Use:     "run",
    45  		Aliases: []string{"start"},
    46  		Short:   "Starts a new CoreOS instance",
    47  		PreRunE: func(cmd *cobra.Command, args []string) (err error) {
    48  			if len(args) != 0 {
    49  				return fmt.Errorf("Incorrect usage. " +
    50  					"This command doesn't accept any arguments.")
    51  			}
    52  			engine.rawArgs.BindPFlags(cmd.Flags())
    53  
    54  			return engine.allowedToRun()
    55  		},
    56  		RunE: runCommand,
    57  	}
    58  	xhyveCmd = &cobra.Command{
    59  		Use:    "xhyve",
    60  		Hidden: true,
    61  		PreRunE: func(cmd *cobra.Command, args []string) error {
    62  			if len(args) != 3 {
    63  				return fmt.Errorf("Incorrect usage. " +
    64  					"This command accepts exactly 3 arguments.")
    65  			}
    66  			return nil
    67  		},
    68  		RunE: xhyveCommand,
    69  	}
    70  )
    71  
    72  func runCommand(cmd *cobra.Command, args []string) error {
    73  	engine.VMs = append(engine.VMs, vmContext{})
    74  	return engine.boot(0, engine.rawArgs)
    75  }
    76  
    77  func xhyveCommand(cmd *cobra.Command, args []string) (err error) {
    78  	var (
    79  		a0, a1, a2 string
    80  		strDecode  = func(s string) (string, error) {
    81  			b, e := base64.StdEncoding.DecodeString(s)
    82  			return string(b), e
    83  		}
    84  	)
    85  
    86  	if a0, err = strDecode(args[0]); err != nil {
    87  		return err
    88  	}
    89  	if a1, err = strDecode(args[1]); err != nil {
    90  		return err
    91  	}
    92  	if a2, err = strDecode(args[2]); err != nil {
    93  		return err
    94  	}
    95  	return xhyve.Run(append(strings.Split(a0, " "),
    96  		"-f", fmt.Sprintf("%s%v", a1, a2)), make(chan string))
    97  }
    98  
    99  func vmBootstrap(args *viper.Viper) (vm *VMInfo, err error) {
   100  	vm = new(VMInfo)
   101  	vm.publicIP = make(chan string)
   102  	vm.errch, vm.done = make(chan error), make(chan bool)
   103  
   104  	vm.PreferLocalImages = args.GetBool("local")
   105  	vm.Detached = args.GetBool("detached")
   106  	vm.Cpus = args.GetInt("cpus")
   107  	vm.Extra = args.GetString("extra")
   108  	vm.SSHkey = args.GetString("sshkey")
   109  	vm.Root, vm.Pid = -1, -1
   110  
   111  	vm.Name, vm.UUID = args.GetString("name"), args.GetString("uuid")
   112  
   113  	if vm.UUID == "random" {
   114  		vm.UUID = uuid.NewV4().String()
   115  	} else if _, err = uuid.FromString(vm.UUID); err != nil {
   116  		log.Printf("%s not a valid UUID as it doesn't follow RFC 4122. %s\n",
   117  			vm.UUID, "    using a randomly generated one")
   118  		vm.UUID = uuid.NewV4().String()
   119  	}
   120  	for {
   121  		if vm.MacAddress, err = uuid2ip.GuestMACfromUUID(vm.UUID); err != nil {
   122  			original := args.GetString("uuid")
   123  			if original != "random" {
   124  				log.Printf("unable to guess the MAC Address from the provided "+
   125  					"UUID (%s). Using a randomly generated one one\n", original)
   126  			}
   127  			vm.UUID = uuid.NewV4().String()
   128  		} else {
   129  			break
   130  		}
   131  	}
   132  
   133  	if vm.Name == "" {
   134  		vm.Name = vm.UUID
   135  	}
   136  
   137  	if _, err = vmInfo(vm.Name); err == nil {
   138  		if vm.Name == vm.UUID {
   139  			return vm, fmt.Errorf("%s %s (%s)\n", "Aborting.",
   140  				"Another VM is running with same UUID.", vm.UUID)
   141  		}
   142  		return vm, fmt.Errorf("%s %s (%s)\n", "Aborting.",
   143  			"Another VM is running with same name.", vm.Name)
   144  	}
   145  
   146  	vm.Memory = args.GetInt("memory")
   147  	if vm.Memory < 1024 {
   148  		log.Printf("'%v' not a reasonable memory value. %s\n", vm.Memory,
   149  			"Using '1024', the default")
   150  		vm.Memory = 1024
   151  	} else if vm.Memory > 8192 {
   152  		log.Printf("'%v' not a reasonable memory value. %s %s\n", vm.Memory,
   153  			"as presently we only support VMs with up to 8GB of RAM.",
   154  			"setting it to '8192'")
   155  		vm.Memory = 8192
   156  	}
   157  
   158  	if vm.Channel, vm.Version, err =
   159  		lookupImage(normalizeChannelName(args.GetString("channel")),
   160  			normalizeVersion(args.GetString("version")),
   161  			false, vm.PreferLocalImages); err != nil {
   162  		return
   163  	}
   164  
   165  	if err = vm.validateCDROM(args.GetString("cdrom")); err != nil {
   166  		return
   167  	}
   168  
   169  	if err = vm.validateVolumes([]string{args.GetString("root")},
   170  		true); err != nil {
   171  		return
   172  	}
   173  	if err = vm.validateVolumes(pSlice(args.GetStringSlice("volume")),
   174  		false); err != nil {
   175  		return
   176  	}
   177  
   178  	vm.Ethernet = append(vm.Ethernet, NetworkInterface{Type: Raw})
   179  	if err = vm.addTAPinterface(args.GetString("tap")); err != nil {
   180  		return
   181  	}
   182  
   183  	err = vm.validateCloudConfig(args.GetString("cloud_config"))
   184  	if err != nil {
   185  		return
   186  	}
   187  
   188  	vm.InternalSSHprivKey, vm.InternalSSHauthKey, err = sshKeyGen()
   189  	if err != nil {
   190  		return vm, fmt.Errorf("%v (%v)",
   191  			"Aborting: unable to generate internal SSH key pair (!)", err)
   192  	}
   193  
   194  	return vm, err
   195  }
   196  
   197  func (running *sessionContext) boot(slt int, rawArgs *viper.Viper) (err error) {
   198  	var c = new(exec.Cmd)
   199  
   200  	if running.VMs[slt].vm, err = vmBootstrap(rawArgs); err != nil {
   201  		return
   202  	}
   203  	vm := running.VMs[slt].vm
   204  
   205  	rundir := filepath.Join(running.runDir, vm.UUID)
   206  	if err = os.RemoveAll(rundir); err != nil {
   207  		return
   208  	}
   209  	if err = os.MkdirAll(rundir, 0755); err != nil {
   210  		return
   211  	}
   212  
   213  	if err = nfsSetup(); err != nil {
   214  		return
   215  	}
   216  
   217  	if c, err = vm.assembleBootPayload(); err != nil {
   218  		return
   219  	}
   220  	vm.CreatedAt = time.Now()
   221  	// saving now, in advance, without Pid to ensure {name,UUID,volumes}
   222  	// atomicity
   223  	if err = vm.storeConfig(); err != nil {
   224  		return
   225  	}
   226  
   227  	go func() {
   228  		timeout := time.After(30 * time.Second)
   229  		select {
   230  		case <-timeout:
   231  			if p, ee := os.FindProcess(c.Process.Pid); ee == nil {
   232  				p.Signal(os.Interrupt)
   233  			}
   234  			vm.errch <- fmt.Errorf("Unable to grab VM's IP after " +
   235  				"30s (!)... Aborting")
   236  		case ip := <-vm.publicIP:
   237  			// afaict there's no race here, regardless of what `go build -race`
   238  			// claims as vm.publicIP will only be triggered well after the
   239  			// c.{Start,Run} calls...
   240  			vm.Pid, vm.PublicIP = c.Process.Pid, ip
   241  			if ee := vm.storeConfig(); ee != nil {
   242  				vm.errch <- ee
   243  			} else {
   244  				if vm.Detached {
   245  					log.Printf("started '%s' in background with IP %v and "+
   246  						"PID %v\n", vm.Name, vm.PublicIP, c.Process.Pid)
   247  				}
   248  				close(vm.publicIP)
   249  				close(vm.done)
   250  			}
   251  		}
   252  	}()
   253  
   254  	go func() {
   255  		if !vm.Detached {
   256  			c.Stdout, c.Stdin, c.Stderr = os.Stdout, os.Stdin, os.Stderr
   257  			vm.errch <- c.Run()
   258  		} else if ee := c.Start(); ee != nil {
   259  			vm.errch <- ee
   260  		} else {
   261  			select {
   262  			default:
   263  				if ee := c.Wait(); ee != nil {
   264  					log.Println(ee)
   265  					vm.errch <- fmt.Errorf("VM exited with error " +
   266  						"while attempting to start in background")
   267  				}
   268  			case <-vm.errch:
   269  			}
   270  		}
   271  	}()
   272  
   273  	for {
   274  		select {
   275  		case <-vm.done:
   276  			if vm.Detached {
   277  				return
   278  			}
   279  		case ee := <-vm.errch:
   280  			return ee
   281  		}
   282  		time.Sleep(250 * time.Millisecond)
   283  	}
   284  }
   285  
   286  func runFlagsDefaults(setFlag *pflag.FlagSet) {
   287  	setFlag.String("channel", "alpha", "CoreOS channel")
   288  	setFlag.String("version", "latest", "CoreOS version")
   289  	setFlag.String("uuid", "random", "VM's UUID")
   290  	setFlag.Int("memory", 1024,
   291  		"VM's RAM, in MB, per instance (1024 < memory < 8192)")
   292  	setFlag.Int("cpus", 1, "VM's vCPUS")
   293  	setFlag.String("cloud_config", "",
   294  		"cloud-config file location (either a remote URL or a local path)")
   295  	setFlag.String("sshkey", "", "VM's default ssh key")
   296  	setFlag.String("root", "", "append a (persistent) root volume to VM")
   297  	setFlag.String("cdrom", "", "append an CDROM (.iso) to VM")
   298  	setFlag.StringSlice("volume", nil, "append disk volumes to VM")
   299  	setFlag.String("tap", "", "append tap interface to VM")
   300  	setFlag.BoolP("detached", "d", false,
   301  		"starts the VM in detached (background) mode")
   302  	setFlag.BoolP("local", "l", false,
   303  		"consumes whatever image is `latest` locally instead of looking "+
   304  			"online unless there's nothing available.")
   305  	setFlag.StringP("name", "n", "",
   306  		"names the VM. (if absent defaults to VM's UUID)")
   307  
   308  	// available but hidden...
   309  	setFlag.String("extra", "", "additional arguments to xhyve hypervisor")
   310  	setFlag.MarkHidden("extra")
   311  }
   312  
   313  func init() {
   314  	runFlagsDefaults(runCmd.Flags())
   315  	RootCmd.AddCommand(runCmd)
   316  	RootCmd.AddCommand(xhyveCmd)
   317  }
   318  
   319  func nfsSetup() (err error) {
   320  	const exportsF = "/etc/exports"
   321  	var (
   322  		buf, bufN []byte
   323  		shared    bool
   324  		oldSig    = "/Users -network 192.168.64.0 " +
   325  			"-mask 255.255.255.0 -alldirs -mapall="
   326  		signature = fmt.Sprintf("%v -network %v -mask %v -alldirs "+
   327  			"-mapall=%v:%v", engine.homedir, engine.network, engine.netmask,
   328  			engine.uid, engine.gid)
   329  		exportSet = func() (ok bool) {
   330  			for _, line := range strings.Split(string(buf), "\n") {
   331  				if strings.HasPrefix(line, signature) {
   332  					ok = true
   333  				}
   334  				if !strings.HasPrefix(line, oldSig) {
   335  					bufN = append(bufN, []byte(line+"\n")...)
   336  				} else {
   337  					bufN = append(bufN, []byte("\n")...)
   338  				}
   339  			}
   340  			return
   341  		}
   342  		nfsIsRunning = func() bool {
   343  			all, _ := ps.Processes()
   344  			for _, p := range all {
   345  				if strings.HasSuffix(p.Executable(), "nfsd") {
   346  					return true
   347  				}
   348  			}
   349  			return false
   350  		}()
   351  		exportsCheck = func(previous []byte) (err error) {
   352  			var out []byte
   353  			if out, err = exec.Command("nfsd", "-F",
   354  				exportsF, "checkexports").Output(); err != nil {
   355  				err = fmt.Errorf("unable to validate %s ('%v')", exportsF, out)
   356  				// getting back to where we were
   357  				ioutil.WriteFile(exportsF, previous, os.ModeAppend)
   358  			}
   359  			return
   360  		}
   361  	)
   362  	// check if /etc/exports exists, and if not create an empty one
   363  	if _, err = os.Stat(exportsF); os.IsNotExist(err) {
   364  		if err = ioutil.WriteFile(exportsF, []byte(""), 0644); err != nil {
   365  			return
   366  		}
   367  	}
   368  
   369  	if buf, err = ioutil.ReadFile(exportsF); err != nil {
   370  		return
   371  	}
   372  
   373  	if shared = exportSet(); !shared {
   374  		if err = ioutil.WriteFile(exportsF, append(bufN,
   375  			[]byte(signature+"\n")...), os.ModeAppend); err != nil {
   376  			return
   377  		}
   378  	}
   379  
   380  	if err = exportsCheck(buf); err != nil {
   381  		return
   382  	}
   383  
   384  	if nfsIsRunning {
   385  		if !shared {
   386  			if err = exec.Command("nfsd", "update").Run(); err != nil {
   387  				return fmt.Errorf("unable to update NFS "+
   388  					"service definitions... (%v)", err)
   389  			}
   390  			log.Printf("'%s' was made available to VMs via NFS\n",
   391  				engine.homedir)
   392  		} else {
   393  			log.Printf("'%s' was already available to VMs via NFS\n",
   394  				engine.homedir)
   395  		}
   396  	} else {
   397  		if err = exec.Command("nfsd", "start").Run(); err != nil {
   398  			return fmt.Errorf("unable to start NFS service... (%v)", err)
   399  		}
   400  		log.Printf("NFS started in order for '%s' to be "+
   401  			"made available to the VMs\n", engine.homedir)
   402  	}
   403  	return
   404  }
   405  
   406  func (vm *VMInfo) storeConfig() (err error) {
   407  	rundir := filepath.Join(engine.runDir, vm.UUID)
   408  	cfg, _ := json.MarshalIndent(vm, "", "    ")
   409  
   410  	if engine.debug {
   411  		fmt.Println(string(cfg))
   412  	}
   413  
   414  	if err = ioutil.WriteFile(fmt.Sprintf("%s/config", rundir),
   415  		[]byte(cfg), 0644); err != nil {
   416  		return
   417  	}
   418  
   419  	return normalizeOnDiskPermissions(rundir)
   420  }
   421  
   422  func (vm *VMInfo) assembleBootPayload() (cmd *exec.Cmd, err error) {
   423  	var (
   424  		cmdline = fmt.Sprintf("%s %s %s %s",
   425  			"earlyprintk=serial", "console=ttyS0", "coreos.autologin",
   426  			"uuid="+vm.UUID)
   427  		prefix  = "coreos_production_pxe"
   428  		vmlinuz = fmt.Sprintf("%s/%s/%s/%s.vmlinuz",
   429  			engine.imageDir, vm.Channel, vm.Version, prefix)
   430  		initrd = fmt.Sprintf("%s/%s/%s/%s_image.cpio.gz",
   431  			engine.imageDir, vm.Channel, vm.Version, prefix)
   432  		instr = []string{
   433  			"libxhyve_bug",
   434  			"-s", "0:0,hostbridge",
   435  			"-l", "com1,stdio",
   436  			"-s", "31,lpc",
   437  			"-U", vm.UUID,
   438  			"-m", fmt.Sprintf("%vM", vm.Memory),
   439  			"-c", fmt.Sprintf("%v", vm.Cpus),
   440  			"-A",
   441  		}
   442  		endpoint string
   443  	)
   444  
   445  	if vm.SSHkey != "" {
   446  		cmdline = fmt.Sprintf("%s sshkey=\"%s\"", cmdline, vm.SSHkey)
   447  	}
   448  
   449  	if vm.Root != -1 {
   450  		cmdline = fmt.Sprintf("%s root=/dev/vd%s", cmdline, string(vm.Root+'a'))
   451  	}
   452  
   453  	if endpoint, err = vm.metadataService(); err != nil {
   454  		return
   455  	}
   456  	cmdline = fmt.Sprintf("%s endpoint=%s", cmdline, endpoint)
   457  
   458  	if vm.CloudConfig != "" {
   459  		if vm.CClocation == Local {
   460  			cmdline = fmt.Sprintf("%s cloud-config-url=%s",
   461  				cmdline, endpoint+"/cloud-config")
   462  		} else {
   463  			cmdline = fmt.Sprintf("%s cloud-config-url=%s",
   464  				cmdline, vm.CloudConfig)
   465  		}
   466  	}
   467  
   468  	if vm.Extra != "" {
   469  		instr = append(instr, vm.Extra)
   470  	}
   471  
   472  	for v, vv := range vm.Ethernet {
   473  		if vv.Type == Tap {
   474  			instr = append(instr,
   475  				"-s", fmt.Sprintf("2:%d,virtio-tap,%v", v, vv.Path))
   476  		} else {
   477  			instr = append(instr, "-s", fmt.Sprintf("2:%d,virtio-net", v))
   478  		}
   479  	}
   480  
   481  	for _, v := range vm.Storage.CDDrives {
   482  		instr = append(instr, "-s", fmt.Sprintf("3:%d,ahci-cd,%s",
   483  			v.Slot, v.Path))
   484  	}
   485  
   486  	for _, v := range vm.Storage.HardDrives {
   487  		instr = append(instr, "-s", fmt.Sprintf("4:%d,virtio-blk,%s",
   488  			v.Slot, v.Path))
   489  	}
   490  	strEncode := func(s string) string {
   491  		return base64.StdEncoding.EncodeToString([]byte(s))
   492  	}
   493  	return exec.Command(os.Args[0], "xhyve",
   494  			strEncode(strings.Join(instr, " ")),
   495  			strEncode(fmt.Sprintf("kexec,%s,%s,", vmlinuz, initrd)),
   496  			strEncode(fmt.Sprintf("%v", cmdline))),
   497  		err
   498  }
   499  
   500  func (vm *VMInfo) validateCloudConfig(config string) (err error) {
   501  	if len(config) == 0 {
   502  		return
   503  	}
   504  
   505  	var response *http.Response
   506  	if response, err = http.Get(config); response != nil {
   507  		response.Body.Close()
   508  	}
   509  	vm.CloudConfig = config
   510  	if err == nil && (response.StatusCode == http.StatusOK ||
   511  		response.StatusCode == http.StatusNoContent) {
   512  		vm.CClocation = Remote
   513  		return
   514  	}
   515  	if _, err = os.Stat(config); err != nil {
   516  		return
   517  	}
   518  	vm.CloudConfig = filepath.Join(engine.pwd, config)
   519  	vm.CClocation = Local
   520  	return
   521  }
   522  
   523  func (vm *VMInfo) validateCDROM(path string) (err error) {
   524  	if path == "" {
   525  		return
   526  	}
   527  	var abs string
   528  	if !strings.HasSuffix(path, ".iso") {
   529  		return fmt.Errorf("Aborting: --cdrom payload MUST end in '.iso'"+
   530  			" ('%s' doesn't)", path)
   531  	}
   532  	if _, err = os.Stat(path); err != nil {
   533  		return err
   534  	}
   535  	if abs, err = filepath.Abs(path); err != nil {
   536  		return
   537  	}
   538  	vm.Storage.CDDrives = make(map[string]StorageDevice, 0)
   539  	vm.Storage.CDDrives["0"] = StorageDevice{
   540  		Type: CDROM, Slot: 0, Path: abs,
   541  	}
   542  	return
   543  }
   544  
   545  func (vm *VMInfo) addTAPinterface(tap string) (err error) {
   546  	if tap == "" {
   547  		return
   548  	}
   549  	var dir, dev string
   550  	if dir = filepath.Dir(tap); !strings.HasPrefix(dir, "/dev") {
   551  		return fmt.Errorf("Aborting: '%v' not a valid tap device...", tap)
   552  	}
   553  	if dev = filepath.Base(tap); !strings.HasPrefix(dev, "tap") {
   554  		return fmt.Errorf("Aborting: '%v' not a valid tap device...", tap)
   555  	}
   556  	if _, err = os.Stat(tap); err != nil {
   557  		return
   558  	}
   559  	// check atomicity
   560  	var up []VMInfo
   561  	if up, err = allRunningInstances(); err != nil {
   562  		return
   563  	}
   564  	for _, d := range up {
   565  		for _, vv := range d.Ethernet {
   566  			if dev == vv.Path {
   567  				return fmt.Errorf("Aborting: %s already being used  "+
   568  					"by another VM (%s)", dev,
   569  					d.Name)
   570  			}
   571  		}
   572  	}
   573  	vm.Ethernet = append(vm.Ethernet, NetworkInterface{
   574  		Type: Tap, Path: dev,
   575  	})
   576  	return
   577  }
   578  
   579  func (vm *VMInfo) validateVolumes(volumes []string, root bool) (err error) {
   580  	var abs string
   581  	for _, j := range volumes {
   582  		if j != "" {
   583  			if _, err = os.Stat(j); err != nil {
   584  				return
   585  			}
   586  			if abs, err = filepath.Abs(j); err != nil {
   587  				return
   588  			}
   589  			if !strings.HasSuffix(j, ".img") {
   590  				return fmt.Errorf("Aborting: --volume payload MUST end"+
   591  					" in '.img' ('%s' doesn't)", j)
   592  			}
   593  			// check atomicity
   594  			var up []VMInfo
   595  			if up, err = allRunningInstances(); err != nil {
   596  				return
   597  			}
   598  			for _, d := range up {
   599  				for _, vv := range d.Storage.HardDrives {
   600  					if abs == vv.Path {
   601  						return fmt.Errorf("Aborting: %s %s (%s)", abs,
   602  							"already being used as a volume by another VM.",
   603  							vv.Path)
   604  					}
   605  				}
   606  			}
   607  
   608  			if vm.Storage.HardDrives == nil {
   609  				vm.Storage.HardDrives = make(map[string]StorageDevice, 0)
   610  			}
   611  
   612  			slot := len(vm.Storage.HardDrives)
   613  			for _, z := range vm.Storage.HardDrives {
   614  				if z.Path == abs {
   615  					return fmt.Errorf("Aborting: attempting to set '%v' "+
   616  						"as base of multiple volumes", j)
   617  				}
   618  			}
   619  			vm.Storage.HardDrives[strconv.Itoa(slot)] = StorageDevice{
   620  				Type: HDD, Slot: slot, Path: abs,
   621  			}
   622  			if root {
   623  				vm.Root = slot
   624  			}
   625  		}
   626  	}
   627  	return
   628  }