github.com/vmware/govmomi@v0.51.0/simulator/container.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package simulator 6 7 import ( 8 "archive/tar" 9 "bufio" 10 "bytes" 11 "context" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "io" 16 "log" 17 "net" 18 "os" 19 "os/exec" 20 "path" 21 "regexp" 22 "strings" 23 "sync" 24 "time" 25 ) 26 27 var ( 28 shell = "/bin/sh" 29 eventWatch eventWatcher 30 ) 31 32 const ( 33 deleteWithContainer = "lifecycle=container" 34 createdByVcsim = "createdBy=vcsim" 35 ) 36 37 func init() { 38 if sh, err := exec.LookPath("bash"); err != nil { 39 shell = sh 40 } 41 } 42 43 type eventWatcher struct { 44 sync.Mutex 45 46 stdin io.WriteCloser 47 stdout io.ReadCloser 48 process *os.Process 49 50 // watches is a map of container IDs to container objects 51 watches map[string]*container 52 } 53 54 // container provides methods to manage a container within a simulator VM lifecycle. 55 type container struct { 56 sync.Mutex 57 58 id string 59 name string 60 61 cancelWatch context.CancelFunc 62 changes chan struct{} 63 } 64 65 type networkSettings struct { 66 Gateway string 67 IPAddress string 68 IPPrefixLen int 69 MacAddress string 70 } 71 72 type containerDetails struct { 73 Config struct { 74 Hostname string 75 Domainname string 76 DNS []string `json:"dns"` 77 } 78 State struct { 79 Running bool 80 Paused bool 81 } 82 NetworkSettings struct { 83 networkSettings 84 Networks map[string]networkSettings 85 } 86 } 87 88 type unknownContainer error 89 type uninitializedContainer error 90 91 var sanitizeNameRx = regexp.MustCompile(`[\(\)\s]`) 92 93 func sanitizeName(name string) string { 94 return sanitizeNameRx.ReplaceAllString(name, "-") 95 } 96 97 func constructContainerName(name, uid string) string { 98 return fmt.Sprintf("vcsim-%s-%s", sanitizeName(name), uid) 99 } 100 101 func constructVolumeName(containerName, uid, volumeName string) string { 102 return constructContainerName(containerName, uid) + "--" + sanitizeName(volumeName) 103 } 104 105 func prefixToMask(prefix int) string { 106 mask := net.CIDRMask(prefix, 32) 107 return fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) 108 } 109 110 type tarEntry struct { 111 header *tar.Header 112 content []byte 113 } 114 115 // From https://docs.docker.com/engine/reference/commandline/cp/ : 116 // > It is not possible to copy certain system files such as resources under /proc, /sys, /dev, tmpfs, and mounts created by the user in the container. 117 // > However, you can still copy such files by manually running tar in docker exec. 118 func copyToGuest(id string, dest string, length int64, reader io.Reader) error { 119 cmd := exec.Command("docker", "exec", "-i", id, "tar", "Cxf", path.Dir(dest), "-") 120 cmd.Stderr = os.Stderr 121 stdin, err := cmd.StdinPipe() 122 if err != nil { 123 return err 124 } 125 126 err = cmd.Start() 127 if err != nil { 128 return err 129 } 130 131 tw := tar.NewWriter(stdin) 132 _ = tw.WriteHeader(&tar.Header{ 133 Name: path.Base(dest), 134 Size: length, 135 Mode: 0444, 136 ModTime: time.Now(), 137 }) 138 139 _, err = io.Copy(tw, reader) 140 141 twErr := tw.Close() 142 stdinErr := stdin.Close() 143 144 waitErr := cmd.Wait() 145 146 if err != nil || twErr != nil || stdinErr != nil || waitErr != nil { 147 return fmt.Errorf("copy: {%s}, tw: {%s}, stdin: {%s}, wait: {%s}", err, twErr, stdinErr, waitErr) 148 } 149 150 return nil 151 } 152 153 func copyFromGuest(id string, src string, sink func(int64, io.Reader) error) error { 154 cmd := exec.Command("docker", "exec", id, "tar", "Ccf", path.Dir(src), "-", path.Base(src)) 155 cmd.Stderr = os.Stderr 156 stdout, err := cmd.StdoutPipe() 157 if err != nil { 158 return err 159 } 160 if err = cmd.Start(); err != nil { 161 return err 162 } 163 164 tr := tar.NewReader(stdout) 165 header, err := tr.Next() 166 if err != nil { 167 return err 168 } 169 170 err = sink(header.Size, tr) 171 waitErr := cmd.Wait() 172 173 if err != nil || waitErr != nil { 174 return fmt.Errorf("err: {%s}, wait: {%s}", err, waitErr) 175 } 176 177 return nil 178 } 179 180 // createVolume creates a volume populated with the provided files 181 // If the header.Size is omitted or set to zero, then len(content+1) is used. 182 // Docker appears to treat this volume create command as idempotent so long as it's identical 183 // to an existing volume, so we can use this both for creating volumes inline in container create (for labelling) and 184 // for population after. 185 // returns: 186 // 187 // uid - string 188 // err - error or nil 189 func createVolume(volumeName string, labels []string, files []tarEntry) (string, error) { 190 image := os.Getenv("VCSIM_BUSYBOX") 191 if image == "" { 192 image = "busybox" 193 } 194 195 name := sanitizeName(volumeName) 196 uid := "" 197 198 // label the volume if specified - this requires the volume be created before use 199 if len(labels) > 0 { 200 run := []string{"volume", "create"} 201 for i := range labels { 202 run = append(run, "--label", labels[i]) 203 } 204 run = append(run, name) 205 cmd := exec.Command("docker", run...) 206 out, err := cmd.Output() 207 if err != nil { 208 return "", err 209 } 210 uid = strings.TrimSpace(string(out)) 211 212 if name == "" { 213 name = uid 214 } 215 } 216 217 run := []string{"run", "--rm", "-i"} 218 run = append(run, "-v", name+":/"+name) 219 run = append(run, image, "tar", "-C", "/"+name, "-xf", "-") 220 cmd := exec.Command("docker", run...) 221 stdin, err := cmd.StdinPipe() 222 if err != nil { 223 return uid, err 224 } 225 226 err = cmd.Start() 227 if err != nil { 228 return uid, err 229 } 230 231 tw := tar.NewWriter(stdin) 232 233 for _, file := range files { 234 header := file.header 235 236 if header.Size == 0 && len(file.content) > 0 { 237 header.Size = int64(len(file.content)) 238 } 239 240 if header.ModTime.IsZero() { 241 header.ModTime = time.Now() 242 } 243 244 if header.Mode == 0 { 245 header.Mode = 0444 246 } 247 248 tarErr := tw.WriteHeader(header) 249 if tarErr == nil { 250 _, tarErr = tw.Write(file.content) 251 } 252 } 253 254 err = nil 255 twErr := tw.Close() 256 stdinErr := stdin.Close() 257 if twErr != nil || stdinErr != nil { 258 err = fmt.Errorf("tw: {%s}, stdin: {%s}", twErr, stdinErr) 259 } 260 261 if waitErr := cmd.Wait(); waitErr != nil { 262 stderr := "" 263 if xerr, ok := waitErr.(*exec.ExitError); ok { 264 stderr = string(xerr.Stderr) 265 } 266 log.Printf("%s %s: %s %s", name, cmd.Args, waitErr, stderr) 267 268 err = fmt.Errorf("%s, wait: {%s}", err, waitErr) 269 return uid, err 270 } 271 272 return uid, err 273 } 274 275 func getBridge(bridgeName string) (string, error) { 276 // {"CreatedAt":"2023-07-11 19:22:25.45027052 +0000 UTC","Driver":"bridge","ID":"fe52c7502c5d","IPv6":"false","Internal":"false","Labels":"goodbye=,hello=","Name":"testnet","Scope":"local"} 277 // podman has distinctly different fields at v4.4.1 so commented out fields that don't match. We only actually care about ID 278 type bridgeNet struct { 279 // CreatedAt string 280 Driver string 281 ID string 282 // IPv6 string 283 // Internal string 284 // Labels string 285 Name string 286 // Scope string 287 } 288 289 // if the underlay bridge already exists, return that 290 // we don't check for a specific label or similar so that it's possible to use a bridge created by other frameworks for composite testing 291 var bridge bridgeNet 292 cmd := exec.Command("docker", "network", "ls", "--format={{json .}}", "-f", fmt.Sprintf("name=%s$", bridgeName)) 293 out, err := cmd.Output() 294 if err != nil { 295 log.Printf("vcsim %s: %s, %s", cmd.Args, err, out) 296 return "", err 297 } 298 299 // unfortunately docker returns an empty string not an empty json doc and podman returns '[]' 300 // podman also returns an array of matches even when there's only one, so we normalize. 301 str := strings.TrimSpace(string(out)) 302 str = strings.TrimPrefix(str, "[") 303 str = strings.TrimSuffix(str, "]") 304 if len(str) == 0 { 305 return "", nil 306 } 307 308 err = json.Unmarshal([]byte(str), &bridge) 309 if err != nil { 310 log.Printf("vcsim %s: %s, %s", cmd.Args, err, str) 311 return "", err 312 } 313 314 return bridge.ID, nil 315 } 316 317 // createBridge creates a bridge network if one does not already exist 318 // returns: 319 // 320 // uid - string 321 // err - error or nil 322 func createBridge(bridgeName string, labels ...string) (string, error) { 323 324 id, err := getBridge(bridgeName) 325 if err != nil { 326 return "", err 327 } 328 329 if id != "" { 330 return id, nil 331 } 332 333 run := []string{"network", "create", "--label", createdByVcsim} 334 for i := range labels { 335 run = append(run, "--label", labels[i]) 336 } 337 run = append(run, bridgeName) 338 339 cmd := exec.Command("docker", run...) 340 out, err := cmd.Output() 341 if err != nil { 342 log.Printf("vcsim %s: %s: %s", cmd.Args, out, err) 343 return "", err 344 } 345 346 // docker returns the ID regardless of whether you supply a name when creating the network, however 347 // podman returns the pretty name, so we have to normalize 348 id, err = getBridge(bridgeName) 349 if err != nil { 350 return "", err 351 } 352 353 return id, nil 354 } 355 356 // create 357 // - name - pretty name, eg. vm name 358 // - id - uuid or similar - this is merged into container name rather than dictating containerID 359 // - networks - set of bridges to connect the container to 360 // - volumes - colon separated tuple of volume name to mount path. Passed directly to docker via -v so mount options can be postfixed. 361 // - env - array of environment vairables in name=value form 362 // - optsAndImage - pass-though options and must include at least the container image to use, including tag if necessary 363 // - args - the command+args to pass to the container 364 func create(ctx *Context, name string, id string, networks []string, volumes []string, ports []string, env []string, image string, args []string) (*container, error) { 365 if len(image) == 0 { 366 return nil, errors.New("cannot create container backing without an image") 367 } 368 369 var c container 370 c.name = constructContainerName(name, id) 371 c.changes = make(chan struct{}) 372 373 for i := range volumes { 374 // we'll pre-create anonymous volumes, simply for labelling consistency 375 volName := strings.Split(volumes[i], ":") 376 createVolume(volName[0], []string{deleteWithContainer, "container=" + c.name}, nil) 377 } 378 379 // assemble env 380 var dockerNet []string 381 var dockerVol []string 382 var dockerPort []string 383 var dockerEnv []string 384 385 for i := range env { 386 dockerEnv = append(dockerEnv, "--env", env[i]) 387 } 388 389 for i := range volumes { 390 dockerVol = append(dockerVol, "-v", volumes[i]) 391 } 392 393 for i := range ports { 394 dockerPort = append(dockerPort, "-p", ports[i]) 395 } 396 397 for i := range networks { 398 dockerNet = append(dockerNet, "--network", networks[i]) 399 } 400 401 run := []string{"docker", "create", "--name", c.name} 402 run = append(run, dockerNet...) 403 run = append(run, dockerVol...) 404 run = append(run, dockerPort...) 405 run = append(run, dockerEnv...) 406 run = append(run, image) 407 run = append(run, args...) 408 409 // this combines all the run options into a single string that's passed to /bin/bash -c as the single argument to force bash parsing. 410 // TODO: make this configurable behaviour so users also have the option of not escaping everything for bash 411 cmd := exec.Command(shell, "-c", strings.Join(run, " ")) 412 out, err := cmd.Output() 413 if err != nil { 414 stderr := "" 415 if xerr, ok := err.(*exec.ExitError); ok { 416 stderr = string(xerr.Stderr) 417 } 418 log.Printf("%s %s: %s %s", name, cmd.Args, err, stderr) 419 420 return nil, err 421 } 422 423 c.id = strings.TrimSpace(string(out)) 424 425 return &c, nil 426 } 427 428 // createVolume takes the specified files and writes them into a volume named for the container. 429 func (c *container) createVolume(name string, labels []string, files []tarEntry) (string, error) { 430 return createVolume(c.name+"--"+name, append(labels, "container="+c.name), files) 431 } 432 433 // inspect retrieves and parses container properties into directly usable struct 434 // returns: 435 // 436 // out - the stdout of the command 437 // detail - basic struct populated with container details 438 // err: 439 // * if c.id is empty, or docker returns "No such object", will return an uninitializedContainer error 440 // * err from either execution or parsing of json output 441 func (c *container) inspect() (out []byte, detail containerDetails, err error) { 442 c.Lock() 443 id := c.id 444 c.Unlock() 445 446 if id == "" { 447 err = uninitializedContainer(errors.New("inspect of uninitialized container")) 448 return 449 } 450 451 var details []containerDetails 452 453 cmd := exec.Command("docker", "inspect", c.id) 454 out, err = cmd.Output() 455 if eErr, ok := err.(*exec.ExitError); ok { 456 if strings.Contains(string(eErr.Stderr), "No such object") { 457 err = uninitializedContainer(errors.New("inspect of uninitialized container")) 458 } 459 } 460 461 if err != nil { 462 return 463 } 464 465 if err = json.NewDecoder(bytes.NewReader(out)).Decode(&details); err != nil { 466 return 467 } 468 469 if len(details) != 1 { 470 err = fmt.Errorf("multiple containers (%d) match ID: %s", len(details), c.id) 471 return 472 } 473 474 detail = details[0] 475 476 // DNS setting 477 f, oerr := os.Open("/etc/docker/daemon.json") 478 if oerr != nil { 479 return 480 } 481 err = json.NewDecoder(f).Decode(&detail.Config) 482 _ = f.Close() 483 484 return 485 } 486 487 // start 488 // - if the container already exists, start it or unpause it. 489 func (c *container) start(ctx *Context) error { 490 c.Lock() 491 id := c.id 492 c.Unlock() 493 494 if id == "" { 495 return uninitializedContainer(errors.New("start of uninitialized container")) 496 } 497 498 start := "start" 499 _, detail, err := c.inspect() 500 if err != nil { 501 return err 502 } 503 504 if detail.State.Paused { 505 start = "unpause" 506 } 507 508 cmd := exec.Command("docker", start, c.id) 509 err = cmd.Run() 510 if err != nil { 511 log.Printf("%s %s: %s", c.name, cmd.Args, err) 512 } 513 514 return err 515 } 516 517 // pause the container (if any) for the given vm. 518 func (c *container) pause(ctx *Context) error { 519 c.Lock() 520 id := c.id 521 c.Unlock() 522 523 if id == "" { 524 return uninitializedContainer(errors.New("pause of uninitialized container")) 525 } 526 527 cmd := exec.Command("docker", "pause", c.id) 528 err := cmd.Run() 529 if err != nil { 530 log.Printf("%s %s: %s", c.name, cmd.Args, err) 531 } 532 533 return err 534 } 535 536 // restart the container (if any) for the given vm. 537 func (c *container) restart(ctx *Context) error { 538 c.Lock() 539 id := c.id 540 c.Unlock() 541 542 if id == "" { 543 return uninitializedContainer(errors.New("restart of uninitialized container")) 544 } 545 546 cmd := exec.Command("docker", "restart", c.id) 547 err := cmd.Run() 548 if err != nil { 549 log.Printf("%s %s: %s", c.name, cmd.Args, err) 550 } 551 552 return err 553 } 554 555 // stop the container (if any) for the given vm. 556 func (c *container) stop(ctx *Context) error { 557 c.Lock() 558 id := c.id 559 c.Unlock() 560 561 if id == "" { 562 return uninitializedContainer(errors.New("stop of uninitialized container")) 563 } 564 565 cmd := exec.Command("docker", "stop", c.id) 566 err := cmd.Run() 567 if err != nil { 568 log.Printf("%s %s: %s", c.name, cmd.Args, err) 569 } 570 571 return err 572 } 573 574 // exec invokes the specified command, with executable being the first of the args, in the specified container 575 // returns 576 // 577 // string - combined stdout and stderr from command 578 // err 579 // * uninitializedContainer error - if c.id is empty 580 // * err from cmd execution 581 func (c *container) exec(ctx *Context, args []string) (string, error) { 582 c.Lock() 583 id := c.id 584 c.Unlock() 585 586 if id == "" { 587 return "", uninitializedContainer(errors.New("exec into uninitialized container")) 588 } 589 590 args = append([]string{"exec", c.id}, args...) 591 cmd := exec.Command("docker", args...) 592 res, err := cmd.CombinedOutput() 593 if err != nil { 594 log.Printf("%s: %s (%s)", c.name, cmd.Args, string(res)) 595 return "", err 596 } 597 598 return strings.TrimSpace(string(res)), nil 599 } 600 601 // remove the container (if any) for the given vm. Considers removal of an uninitialized container success. 602 // Also removes volumes and networks that indicate they are lifecycle coupled with this container. 603 // returns: 604 // 605 // err - joined err from deletion of container and any volumes or networks that have coupled lifecycle 606 func (c *container) remove(ctx *Context) error { 607 c.Lock() 608 defer c.Unlock() 609 610 if c.id == "" { 611 // consider absence success 612 return nil 613 } 614 615 cmd := exec.Command("docker", "rm", "-v", "-f", c.id) 616 err := cmd.Run() 617 if err != nil { 618 log.Printf("%s %s: %s", c.name, cmd.Args, err) 619 return err 620 } 621 622 cmd = exec.Command("docker", "volume", "ls", "-q", "--filter", "label=container="+c.name, "--filter", "label="+deleteWithContainer) 623 volumesToReap, lsverr := cmd.Output() 624 if lsverr != nil { 625 log.Printf("%s %s: %s", c.name, cmd.Args, lsverr) 626 } 627 log.Printf("%s volumes: %s", c.name, volumesToReap) 628 629 var rmverr error 630 if len(volumesToReap) > 0 { 631 run := []string{"volume", "rm", "-f"} 632 run = append(run, strings.Split(string(volumesToReap), "\n")...) 633 cmd = exec.Command("docker", run...) 634 out, rmverr := cmd.Output() 635 if rmverr != nil { 636 log.Printf("%s %s: %s, %s", c.name, cmd.Args, rmverr, out) 637 } 638 } 639 640 cmd = exec.Command("docker", "network", "ls", "-q", "--filter", "label=container="+c.name, "--filter", "label="+deleteWithContainer) 641 networksToReap, lsnerr := cmd.Output() 642 if lsnerr != nil { 643 log.Printf("%s %s: %s", c.name, cmd.Args, lsnerr) 644 } 645 646 var rmnerr error 647 if len(networksToReap) > 0 { 648 run := []string{"network", "rm", "-f"} 649 run = append(run, strings.Split(string(volumesToReap), "\n")...) 650 cmd = exec.Command("docker", run...) 651 rmnerr = cmd.Run() 652 if rmnerr != nil { 653 log.Printf("%s %s: %s", c.name, cmd.Args, rmnerr) 654 } 655 } 656 657 if err != nil || lsverr != nil || rmverr != nil || lsnerr != nil || rmnerr != nil { 658 return fmt.Errorf("err: {%s}, lsverr: {%s}, rmverr: {%s}, lsnerr:{%s}, rmerr: {%s}", err, lsverr, rmverr, lsnerr, rmnerr) 659 } 660 661 if c.cancelWatch != nil { 662 c.cancelWatch() 663 eventWatch.ignore(c) 664 } 665 c.id = "" 666 return nil 667 } 668 669 // updated is a simple trigger allowing a caller to indicate that something has likely changed about the container 670 // and interested parties should re-inspect as needed. 671 func (c *container) updated() { 672 consolidationWindow := 250 * time.Millisecond 673 if d, err := time.ParseDuration(os.Getenv("VCSIM_EVENT_CONSOLIDATION_WINDOW")); err == nil { 674 consolidationWindow = d 675 } 676 677 select { 678 case c.changes <- struct{}{}: 679 time.Sleep(consolidationWindow) 680 // as this is only a hint to avoid waiting for the full inspect interval, we don't care about accumulating 681 // multiple triggers. We do pause to allow large numbers of sequential updates to consolidate 682 default: 683 } 684 } 685 686 // watchContainer monitors the underlying container and updates 687 // properties based on the container status. This occurs until either 688 // the container or the VM is removed. 689 // returns: 690 // 691 // err - uninitializedContainer error - if c.id is empty 692 func (c *container) watchContainer(ctx *Context, updateFn func(*containerDetails, *container) error) error { 693 c.Lock() 694 defer c.Unlock() 695 696 if c.id == "" { 697 return uninitializedContainer(errors.New("Attempt to watch uninitialized container")) 698 } 699 700 eventWatch.watch(c) 701 702 cancelCtx, cancelFunc := context.WithCancel(ctx) 703 c.cancelWatch = cancelFunc 704 705 // Update the VM from the container at regular intervals until the done 706 // channel is closed. 707 go func() { 708 inspectInterval := 10 * time.Second 709 if d, err := time.ParseDuration(os.Getenv("VCSIM_INSPECT_INTERVAL")); err == nil { 710 inspectInterval = d 711 } 712 ticker := time.NewTicker(inspectInterval) 713 714 update := func() { 715 _, details, err := c.inspect() 716 var rmErr error 717 var removing bool 718 if _, ok := err.(uninitializedContainer); ok { 719 removing = true 720 rmErr = c.remove(ctx) 721 } 722 723 updateErr := updateFn(&details, c) 724 // if we don't succeed we want to re-try 725 if removing && rmErr == nil && updateErr == nil { 726 ticker.Stop() 727 return 728 } 729 if updateErr != nil { 730 log.Printf("vcsim container watch: %s %s", c.id, updateErr) 731 } 732 } 733 734 for { 735 select { 736 case <-c.changes: 737 update() 738 case <-ticker.C: 739 update() 740 case <-cancelCtx.Done(): 741 return 742 } 743 } 744 }() 745 746 return nil 747 } 748 749 func (w *eventWatcher) watch(c *container) { 750 w.Lock() 751 defer w.Unlock() 752 753 if w.watches == nil { 754 w.watches = make(map[string]*container) 755 } 756 757 w.watches[c.id] = c 758 759 if w.stdin == nil { 760 cmd := exec.Command("docker", "events", "--format", "'{{.ID}}'", "--filter", "Type=container") 761 w.stdout, _ = cmd.StdoutPipe() 762 w.stdin, _ = cmd.StdinPipe() 763 err := cmd.Start() 764 if err != nil { 765 log.Printf("docker event watcher: %s %s", cmd.Args, err) 766 w.stdin = nil 767 w.stdout = nil 768 w.process = nil 769 770 return 771 } 772 773 w.process = cmd.Process 774 775 go w.monitor() 776 } 777 } 778 779 func (w *eventWatcher) ignore(c *container) { 780 w.Lock() 781 782 delete(w.watches, c.id) 783 784 if len(w.watches) == 0 && w.stdin != nil { 785 w.stop() 786 } 787 788 w.Unlock() 789 } 790 791 func (w *eventWatcher) monitor() { 792 w.Lock() 793 watches := len(w.watches) 794 w.Unlock() 795 796 if watches == 0 { 797 return 798 } 799 800 scanner := bufio.NewScanner(w.stdout) 801 for scanner.Scan() { 802 id := strings.TrimSpace(scanner.Text()) 803 804 w.Lock() 805 container := w.watches[id] 806 w.Unlock() 807 808 if container != nil { 809 // this is called in a routine to allow an event consolidation window 810 go container.updated() 811 } 812 } 813 } 814 815 func (w *eventWatcher) stop() { 816 if w.stdin != nil { 817 w.stdin.Close() 818 w.stdin = nil 819 } 820 if w.stdout != nil { 821 w.stdout.Close() 822 w.stdout = nil 823 } 824 w.process.Kill() 825 }