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 }