mirror of
https://github.com/linuxkit/linuxkit.git
synced 2025-10-01 19:40:51 +00:00
moby: Add a 'run' command to execute an image on hyperkit
'moby run' will use the kernel and initrd image produced by 'moby build' and, on macOS, will run it inside a hyperkit VM. This assumes that you have a recent version of Docker for Mac installed as it re-uses the hyperkit and VPNKit from it. Signed-off-by: Rolf Neugebauer <rolf.neugebauer@docker.com>
This commit is contained in:
533
vendor/github.com/docker/hyperkit/go/hyperkit.go
generated
vendored
Normal file
533
vendor/github.com/docker/hyperkit/go/hyperkit.go
generated
vendored
Normal file
@@ -0,0 +1,533 @@
|
||||
// +build darwin
|
||||
|
||||
// Package hyperkit provides a Go wrapper around the hyperkit
|
||||
// command. It currently shells out to start hyperkit with the
|
||||
// provided configuration.
|
||||
//
|
||||
// Most of the arguments should be self explanatory, but console
|
||||
// handling deserves a mention. If the Console is configured with
|
||||
// ConsoleStdio, the hyperkit is started with stdin, stdout, and
|
||||
// stderr plumbed through to the VM console. If Console is set to
|
||||
// ConsoleFile hyperkit the console output is redirected to a file and
|
||||
// console input is disabled. For this mode StateDir has to be set and
|
||||
// the interactive console is accessible via a 'tty' file created
|
||||
// there.
|
||||
//
|
||||
// Currently this module has some limitations:
|
||||
// - Only supports zero or one disk image
|
||||
// - Only support zero or one network interface connected to VPNKit
|
||||
// - Only kexec boot
|
||||
//
|
||||
// This package is currently implemented by shelling out a hyperkit
|
||||
// process. In the future we may change this to become a wrapper
|
||||
// around the hyperkit library.
|
||||
package hyperkit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-ps"
|
||||
"github.com/rneugeba/iso9660wrap"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConsoleStdio configures console to use Stdio
|
||||
ConsoleStdio = iota
|
||||
// ConsoleFile configures console to a tty and output to a file
|
||||
ConsoleFile
|
||||
|
||||
defaultVPNKitSock = "Library/Containers/com.docker.docker/Data/s50"
|
||||
defaultDiskImage = "disk.img"
|
||||
|
||||
defaultCPUs = 1
|
||||
defaultMemory = 1024 // 1G
|
||||
|
||||
jsonFile = "hyperkit.json"
|
||||
pidFile = "hyperkit.pid"
|
||||
)
|
||||
|
||||
var defaultHyperKits = []string{"hyperkit",
|
||||
"com.docker.hyperkit",
|
||||
"/usr/local/bin/hyperkit",
|
||||
"/Applications/Docker.app/Contents/MacOS/com.docker.hyperkit"}
|
||||
|
||||
// HyperKit contains the configuration of the hyperkit VM
|
||||
type HyperKit struct {
|
||||
// HyperKit is the path to the hyperkit binary
|
||||
HyperKit string `json:"hyperkit"`
|
||||
// StateDir is the directory where runtime state is kept. If left empty, no state will be kept.
|
||||
StateDir string `json:"state_dir"`
|
||||
// VPNKitSock is the location of the VPNKit socket used for networking.
|
||||
VPNKitSock string `json:"vpnkit_sock"`
|
||||
// DiskImage is the path to the disk image to use
|
||||
DiskImage string `json:"disk"`
|
||||
// ISOImage is the (optional) path to a ISO image to attach
|
||||
ISOImage string `json:"iso"`
|
||||
|
||||
// Kernel is the path to the kernel image to boot
|
||||
Kernel string `json:"kernel"`
|
||||
// Initrd is the path to the initial ramdisk to boot off
|
||||
Initrd string `json:"initrd"`
|
||||
|
||||
// CPUs is the number CPUs to configure
|
||||
CPUs int `json:"cpus"`
|
||||
// Memory is the amount of megabytes of memory for the VM
|
||||
Memory int `json:"memory"`
|
||||
// DiskSize is the size of the disk image in megabytes. If zero and DiskImage does not exist, no disk will be attached.
|
||||
DiskSize int `json:"disk_size"`
|
||||
|
||||
// Console defines where the console of the VM should be
|
||||
// connected to. ConsoleStdio and ConsoleFile are supported.
|
||||
Console int `json:"console"`
|
||||
|
||||
// UserData, if non empty, will be added to a ISO under the
|
||||
// filename `config` and passed to the VM.
|
||||
UserData string `json:"user_data"`
|
||||
|
||||
// Below here are internal members, but they are exported so
|
||||
// that they are written to the state json file, if configured.
|
||||
|
||||
// Pid of the hyperkit process
|
||||
Pid int `json:"pid"`
|
||||
// Arguments used to execute the hyperkit process
|
||||
Arguments []string `json:"arguments"`
|
||||
// CmdLine is a single string of the command line
|
||||
CmdLine string `json:"cmdline"`
|
||||
|
||||
process *os.Process
|
||||
background bool
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
// New creates a template config structure.
|
||||
// - If hyperkit can't be found an error is returned.
|
||||
// - If vpnkitsock is empty no networking is configured. If it is set
|
||||
// to "auto" it tries to re-use the Docker for Mac VPNKit connection.
|
||||
// - If statedir is "" no state is written to disk.
|
||||
func New(hyperkit, statedir, vpnkitsock, diskimage string) (*HyperKit, error) {
|
||||
h := HyperKit{}
|
||||
var err error
|
||||
|
||||
h.HyperKit, err = checkHyperKit(hyperkit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.StateDir = statedir
|
||||
h.VPNKitSock, err = checkVPNKitSock(vpnkitsock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.DiskImage = diskimage
|
||||
|
||||
h.CPUs = defaultCPUs
|
||||
h.Memory = defaultMemory
|
||||
|
||||
h.Console = ConsoleStdio
|
||||
h.UserData = ""
|
||||
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
// FromState reads a json file from statedir and populates a HyperKit structure.
|
||||
func FromState(statedir string) (*HyperKit, error) {
|
||||
b, err := ioutil.ReadFile(filepath.Join(statedir, jsonFile))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't read json file: ", err)
|
||||
}
|
||||
h := &HyperKit{}
|
||||
err = json.Unmarshal(b, h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't parse json file: ", err)
|
||||
}
|
||||
|
||||
// Make sure the pid written by hyperkit is the same as in the json
|
||||
d, err := ioutil.ReadFile(filepath.Join(statedir, pidFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pid, err := strconv.Atoi(string(d[:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.Pid != pid {
|
||||
return nil, fmt.Errorf("pids do not match %d != %d", h.Pid, pid)
|
||||
}
|
||||
|
||||
h.process, err = os.FindProcess(h.Pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// SetLogger sets the log instance to use for the output of the hyperkit process itself (not the console of the VM).
|
||||
// This is only relevant when Console is set to ConsoleFile
|
||||
func (h *HyperKit) SetLogger(logger *log.Logger) {
|
||||
h.log = logger
|
||||
}
|
||||
|
||||
// Run the VM with a given command line until it exits
|
||||
func (h *HyperKit) Run(cmdline string) error {
|
||||
h.background = false
|
||||
return h.execute(cmdline)
|
||||
}
|
||||
|
||||
// Start the VM with a given command line in the background
|
||||
func (h *HyperKit) Start(cmdline string) error {
|
||||
h.background = true
|
||||
return h.execute(cmdline)
|
||||
}
|
||||
|
||||
func (h *HyperKit) execute(cmdline string) error {
|
||||
var err error
|
||||
// Sanity checks on configuration
|
||||
if h.Console == ConsoleFile && h.StateDir == "" {
|
||||
return fmt.Errorf("If ConsoleFile is set, StateDir was be specified")
|
||||
}
|
||||
if h.UserData != "" && h.ISOImage != "" {
|
||||
return fmt.Errorf("If UserData is supplied, ISOImage must not be set")
|
||||
}
|
||||
if h.ISOImage != "" {
|
||||
if _, err = os.Stat(h.ISOImage); os.IsNotExist(err) {
|
||||
return fmt.Errorf("ISO %s does not exist", h.ISOImage)
|
||||
}
|
||||
}
|
||||
if h.UserData != "" && h.StateDir == "" {
|
||||
return fmt.Errorf("If UserData is supplied, StateDir was be specified")
|
||||
}
|
||||
if _, err = os.Stat(h.Kernel); os.IsNotExist(err) {
|
||||
return fmt.Errorf("Kernel %s does not exist", h.Kernel)
|
||||
}
|
||||
if _, err = os.Stat(h.Initrd); os.IsNotExist(err) {
|
||||
return fmt.Errorf("initrd %s does not exist", h.Initrd)
|
||||
}
|
||||
if h.DiskImage == "" && h.StateDir == "" && h.DiskSize != 0 {
|
||||
return fmt.Errorf("Can't create disk, because neither DiskImage nor StateDir is set")
|
||||
}
|
||||
|
||||
// Create files
|
||||
if h.StateDir != "" {
|
||||
err = os.MkdirAll(h.StateDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if h.DiskImage == "" && h.DiskSize != 0 {
|
||||
h.DiskImage = filepath.Join(h.StateDir, "disk.img")
|
||||
}
|
||||
if _, err = os.Stat(h.DiskImage); os.IsNotExist(err) {
|
||||
if h.DiskSize != 0 {
|
||||
err = CreateDiskImage(h.DiskImage, h.DiskSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if h.UserData != "" {
|
||||
h.ISOImage, err = createUserDataISO(h.StateDir, h.UserData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Run
|
||||
h.buildArgs(cmdline)
|
||||
err = h.execHyperKit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop the running VM
|
||||
func (h *HyperKit) Stop() error {
|
||||
if h.process == nil {
|
||||
return fmt.Errorf("hyperkit process not known")
|
||||
}
|
||||
if !h.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
err := h.process.Kill()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns true if the hyperkit process is running.
|
||||
func (h *HyperKit) IsRunning() bool {
|
||||
// os.FindProcess on Unix always returns a process object even
|
||||
// if the process does not exists. There does not seem to be
|
||||
// a call to find out if the process is running either, so we
|
||||
// use another package to find out.
|
||||
proc, err := ps.FindProcess(h.Pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if proc == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Remove deletes all statefiles if present.
|
||||
// This also removes the StateDir if empty.
|
||||
// If keepDisk is set, the diskimage will not get removed.
|
||||
func (h *HyperKit) Remove(keepDisk bool) error {
|
||||
if h.IsRunning() {
|
||||
return fmt.Errorf("Can't remove state as process is running")
|
||||
}
|
||||
if h.StateDir == "" {
|
||||
// If there is not state directory we don't mess with the disk
|
||||
return nil
|
||||
}
|
||||
|
||||
if !keepDisk {
|
||||
return os.RemoveAll(h.StateDir)
|
||||
}
|
||||
|
||||
files, _ := ioutil.ReadDir(h.StateDir)
|
||||
for _, f := range files {
|
||||
fn := filepath.Clean(filepath.Join(h.StateDir, f.Name()))
|
||||
if fn == h.DiskImage {
|
||||
continue
|
||||
}
|
||||
err := os.Remove(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to json string
|
||||
func (h *HyperKit) String() string {
|
||||
s, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// CreateDiskImage creates a empty file suitable for use as a disk image for a hyperkit VM.
|
||||
func CreateDiskImage(location string, sizeMB int) error {
|
||||
f, err := os.Create(location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := make([]byte, 1048676)
|
||||
for i := 0; i < sizeMB; i++ {
|
||||
f.Write(buf)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HyperKit) buildArgs(cmdline string) {
|
||||
a := []string{"-A", "-u"}
|
||||
if h.StateDir != "" {
|
||||
a = append(a, "-F", filepath.Join(h.StateDir, pidFile))
|
||||
}
|
||||
|
||||
a = append(a, "-c", fmt.Sprintf("%d", h.CPUs))
|
||||
a = append(a, "-m", fmt.Sprintf("%dM", h.Memory))
|
||||
|
||||
a = append(a, "-s", "0:0,hostbridge")
|
||||
if h.VPNKitSock != "" {
|
||||
a = append(a, "-s", fmt.Sprintf("1:0,virtio-vpnkit,path=%s", h.VPNKitSock))
|
||||
}
|
||||
if h.DiskImage != "" {
|
||||
a = append(a, "-s", fmt.Sprintf("2:0,virtio-blk,%s", h.DiskImage))
|
||||
}
|
||||
if h.ISOImage != "" {
|
||||
a = append(a, "-s", fmt.Sprintf("4,ahci-cd,%s", h.ISOImage))
|
||||
}
|
||||
|
||||
a = append(a, "-s", "10,virtio-rnd")
|
||||
a = append(a, "-s", "31,lpc")
|
||||
|
||||
if h.Console == ConsoleFile {
|
||||
a = append(a, "-l", fmt.Sprintf("com1,autopty=%s/tty,log=%s/console-ring", h.StateDir, h.StateDir))
|
||||
} else {
|
||||
a = append(a, "-l", "com1,stdio")
|
||||
}
|
||||
|
||||
kernArgs := fmt.Sprintf("kexec,%s,%s,earlyprintk=serial %s", h.Kernel, h.Initrd, cmdline)
|
||||
a = append(a, "-f", kernArgs)
|
||||
|
||||
h.Arguments = a
|
||||
h.CmdLine = h.HyperKit + " " + strings.Join(a, " ")
|
||||
}
|
||||
|
||||
// Execute hyperkit and plumb stdin/stdout/stderr.
|
||||
func (h *HyperKit) execHyperKit() error {
|
||||
|
||||
cmd := exec.Command(h.HyperKit, h.Arguments...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// Plumb in stdin/stdout/stderr. If ConsoleStdio is configured
|
||||
// plumb them to the system streams. If a logger is specified,
|
||||
// use it for stdout/stderr logging. Otherwise use the default
|
||||
// /dev/null.
|
||||
if h.Console == ConsoleStdio {
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
} else if h.log != nil {
|
||||
stdoutChan := make(chan string)
|
||||
stderrChan := make(chan string)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream(stdout, stdoutChan)
|
||||
stream(stderr, stderrChan)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case stderrl := <-stderrChan:
|
||||
log.Printf("%s", stderrl)
|
||||
case stdoutl := <-stdoutChan:
|
||||
log.Printf("%s", stdoutl)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Pid = cmd.Process.Pid
|
||||
h.process = cmd.Process
|
||||
err = h.writeState()
|
||||
if err != nil {
|
||||
h.process.Kill()
|
||||
return err
|
||||
}
|
||||
if !h.background {
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeState write the state to a JSON file
|
||||
func (h *HyperKit) writeState() error {
|
||||
if h.StateDir == "" {
|
||||
// This is not an error
|
||||
return nil
|
||||
}
|
||||
|
||||
s, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filepath.Join(h.StateDir, jsonFile), []byte(s), 0644)
|
||||
}
|
||||
|
||||
func stream(r io.ReadCloser, dest chan<- string) {
|
||||
go func() {
|
||||
defer r.Close()
|
||||
reader := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dest <- line
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Create a ISO with Userdata in the specified directory
|
||||
func createUserDataISO(dir string, init string) (string, error) {
|
||||
cfgName := filepath.Join(dir, "config")
|
||||
isoName := cfgName + ".iso"
|
||||
|
||||
if err := ioutil.WriteFile(cfgName, []byte(init), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
outfh, err := os.OpenFile(isoName, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
infh, err := os.Open(cfgName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = iso9660wrap.WriteFile(outfh, infh)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return isoName, nil
|
||||
}
|
||||
|
||||
// checkHyperKit tries to find and/or validate the path of hyperkit
|
||||
func checkHyperKit(hyperkit string) (string, error) {
|
||||
if hyperkit != "" {
|
||||
p, err := exec.LookPath(hyperkit)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not find hyperkit executable: ", hyperkit)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Look in a number of default locations
|
||||
for _, hyperkit := range defaultHyperKits {
|
||||
p, err := exec.LookPath(hyperkit)
|
||||
if err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Could not find hyperkit executable")
|
||||
}
|
||||
|
||||
// checkVPNKitSock tries to find and/or validate the path of the VPNKit socket
|
||||
func checkVPNKitSock(vpnkitsock string) (string, error) {
|
||||
if vpnkitsock == "auto" {
|
||||
vpnkitsock = filepath.Join(getHome(), defaultVPNKitSock)
|
||||
}
|
||||
|
||||
vpnkitsock = filepath.Clean(vpnkitsock)
|
||||
_, err := os.Stat(vpnkitsock)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return vpnkitsock, nil
|
||||
}
|
||||
|
||||
func getHome() string {
|
||||
if usr, err := user.Current(); err == nil {
|
||||
return usr.HomeDir
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
Reference in New Issue
Block a user