diff --git a/src/cmd/linuxkit/run.go b/src/cmd/linuxkit/run.go index 34827ec8a..8ead9fb01 100644 --- a/src/cmd/linuxkit/run.go +++ b/src/cmd/linuxkit/run.go @@ -19,6 +19,7 @@ func runUsage() { fmt.Printf(" gcp\n") fmt.Printf(" hyperkit [macOS]\n") fmt.Printf(" qemu [linux]\n") + fmt.Printf(" vcenter\n") fmt.Printf(" vmware\n") fmt.Printf(" packet\n") fmt.Printf("\n") @@ -48,6 +49,8 @@ func run(args []string) { runQemu(args[1:]) case "packet": runPacket(args[1:]) + case "vcenter": + runVcenter(args[1:]) default: switch runtime.GOOS { case "darwin": diff --git a/src/cmd/linuxkit/run_vcenter.go b/src/cmd/linuxkit/run_vcenter.go new file mode 100644 index 000000000..ced00dfdd --- /dev/null +++ b/src/cmd/linuxkit/run_vcenter.go @@ -0,0 +1,292 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/soap" + "github.com/vmware/govmomi/vim25/types" + + log "github.com/Sirupsen/logrus" +) + +type vmConfig struct { + vCenterURL *string + dsName *string + networkName *string + vSphereHost *string + + vmName *string + path *string + persistent *int64 + vCpus *int + mem *int64 + poweron *bool +} + +func runVcenter(args []string) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var newVM vmConfig + + flags := flag.NewFlagSet("vCenter", flag.ExitOnError) + invoked := filepath.Base(os.Args[0]) + + newVM.vCenterURL = flags.String("url", os.Getenv("VCURL"), "URL in the format of https://username:password@host/sdk") + newVM.dsName = flags.String("datastore", os.Getenv("VMDATASTORE"), "The Name of the DataStore to host the VM") + newVM.networkName = flags.String("network", os.Getenv("VMNETWORK"), "The VMware vSwitch the VM will use") + newVM.vSphereHost = flags.String("hostname", os.Getenv("VMHOST"), "The Server that will run the VM") + + newVM.vmName = flags.String("vmname", "", "Specify a name for virtual Machine") + newVM.path = flags.String("path", "", "Path to a specific image") + newVM.persistent = flags.Int64("persistentSize", 0, "Size in MB of persistent storage to allocate to the VM") + newVM.mem = flags.Int64("mem", 1024, "Size in MB of memory to allocate to the VM") + newVM.vCpus = flags.Int("cpus", 1, "Amount of vCPUs to allocate to the VM") + newVM.poweron = flags.Bool("powerOn", false, "Power On the new VM once it has been created") + + flags.Usage = func() { + fmt.Printf("USAGE: %s push vcenter [options] [name]\n\n", invoked) + fmt.Printf("'name' specifies the full path of an image file which will be uploaded\n") + fmt.Printf("Options:\n\n") + flags.PrintDefaults() + } + + if err := flags.Parse(args); err != nil { + log.Fatalln("Unable to parse args") + } + + remArgs := flags.Args() + if len(remArgs) == 0 { + fmt.Printf("Please specify the prefix to the image to push\n") + flags.Usage() + os.Exit(1) + } + prefix := remArgs[0] + + // Allow alternative names for new virtual machines being created in vCenter + if *newVM.vmName == "" { + *newVM.vmName = prefix + } + + // Parse URL from string + u, err := url.Parse(*newVM.vCenterURL) + if err != nil { + log.Fatalf("URL can't be parsed, ensure it is https://username:password/
/sdk") + } + + // Test any passed in files before creating a new VM + if *newVM.path == "" { + *newVM.path = prefix + ".iso" + } + checkFile(*newVM.path) + + // Connect and log in to ESX or vCenter + c, err := govmomi.NewClient(ctx, u, true) + if err != nil { + log.Fatalln("Error logging into vCenter, check address and credentials") + } + + f := find.NewFinder(c.Client, true) + + // Find one and only datacenter, not sure how VMware linked mode will work + dc, err := f.DefaultDatacenter(ctx) + if err != nil { + log.Fatalln("No Datacenter instance could be found inside of vCenter") + } + + // Make future calls local to this datacenter + f.SetDatacenter(dc) + + // Find Datastore/Network + dss, err := f.DatastoreOrDefault(ctx, *newVM.dsName) + if err != nil { + log.Fatalf("Datastore [%s], could not be found", *newVM.dsName) + } + + net, err := f.NetworkOrDefault(ctx, *newVM.networkName) + if err != nil { + log.Fatalf("Network [%s], could not be found", *newVM.networkName) + } + + // Set the host that the VM will be created on + hs, err := f.HostSystemOrDefault(ctx, *newVM.vSphereHost) + if err != nil { + log.Fatalf("vSphere host [%s], could not be found", *newVM.vSphereHost) + } + + var rp *object.ResourcePool + rp, err = hs.ResourcePool(ctx) + if err != nil { + log.Fatalln("Error locating default resource pool") + } + + log.Infof("Creating new LinuxKit Virtual Machine") + spec := types.VirtualMachineConfigSpec{ + Name: *newVM.vmName, + GuestId: "otherLinux64Guest", + Files: &types.VirtualMachineFileInfo{VmPathName: fmt.Sprintf("[%s]", dss.Name())}, + NumCPUs: int32(*newVM.vCpus), + MemoryMB: *newVM.mem, + } + + scsi, err := object.SCSIControllerTypes().CreateSCSIController("pvscsi") + if err != nil { + log.Fatalln("Error creating pvscsi controller as part of new VM") + } + + spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ + Operation: types.VirtualDeviceConfigSpecOperationAdd, + Device: scsi, + }) + + folders, err := dc.Folders(ctx) + if err != nil { + log.Fatalln("Error locating default datacenter folder") + } + + task, err := folders.VmFolder.CreateVM(ctx, spec, rp, hs) + if err != nil { + log.Fatalln("Creating new VM failed, more detail can be found in vCenter tasks") + } + + info, err := task.WaitForResult(ctx, nil) + if err != nil { + log.Fatalln("Creating new VM failed, more detail can be found in vCenter tasks") + } + + // Retrieve the new VM + vm := object.NewVirtualMachine(c.Client, info.Result.(types.ManagedObjectReference)) + + uploadFile(c, newVM, dss) + addISO(ctx, newVM, vm, dss) + + if *newVM.persistent != 0 { + addVMDK(ctx, vm, dss, newVM) + } + + if *newVM.networkName != "" { + addNIC(ctx, vm, net) + } + + if *newVM.poweron == true { + log.Infoln("Powering on LinuxKit VM") + powerOnVM(ctx, vm) + } +} + +func powerOnVM(ctx context.Context, vm *object.VirtualMachine) { + task, err := vm.PowerOn(ctx) + if err != nil { + log.Errorln("Power On operation has failed, more detail can be found in vCenter tasks") + } + + _, err = task.WaitForResult(ctx, nil) + if err != nil { + log.Errorln("Power On Task has failed, more detail can be found in vCenter tasks") + } +} + +func uploadFile(c *govmomi.Client, newVM vmConfig, dss *object.Datastore) { + _, fileName := path.Split(*newVM.path) + log.Infof("Uploading LinuxKit file [%s]", *newVM.path) + if *newVM.path == "" { + log.Fatalf("No file specified") + } + dsurl := dss.NewURL(fmt.Sprintf("%s/%s", *newVM.vmName, fileName)) + + p := soap.DefaultUpload + if err := c.Client.UploadFile(*newVM.path, dsurl, &p); err != nil { + log.Fatalf("Unable to upload file to vCenter Datastore\n%v", err) + } +} + +func addNIC(ctx context.Context, vm *object.VirtualMachine, net object.NetworkReference) { + backing, err := net.EthernetCardBackingInfo(ctx) + if err != nil { + log.Fatalf("Unable to determine vCenter network backend\n%v", err) + } + + netdev, err := object.EthernetCardTypes().CreateEthernetCard("vmxnet3", backing) + if err != nil { + log.Fatalf("Unable to create vmxnet3 network interface\n%v", err) + } + + log.Infof("Adding VM Networking") + var add []types.BaseVirtualDevice + add = append(add, netdev) + + if vm.AddDevice(ctx, add...); err != nil { + log.Fatalf("Unable to add new networking device to VM configuration\n%v", err) + } +} + +func addVMDK(ctx context.Context, vm *object.VirtualMachine, dss *object.Datastore, newVM vmConfig) { + devices, err := vm.Device(ctx) + if err != nil { + log.Fatalf("Unable to read devices from VM configuration\n%v", err) + } + + controller, err := devices.FindDiskController("scsi") + if err != nil { + log.Fatalf("Unable to find SCSI device from VM configuration\n%v", err) + } + // The default is to have all persistent disks named linuxkit.vmdk + disk := devices.CreateDisk(controller, dss.Reference(), dss.Path(fmt.Sprintf("%s/%s", *newVM.vmName, "linuxkit.vmdk"))) + + disk.CapacityInKB = *newVM.persistent * 1024 + + var add []types.BaseVirtualDevice + add = append(add, disk) + + log.Infof("Adding a persistent disk to the Virtual Machine") + + if vm.AddDevice(ctx, add...); err != nil { + log.Fatalf("Unable to add new storage device to VM configuration\n%v", err) + } +} + +func addISO(ctx context.Context, newVM vmConfig, vm *object.VirtualMachine, dss *object.Datastore) { + devices, err := vm.Device(ctx) + if err != nil { + log.Fatalf("Unable to read devices from VM configuration\n%v", err) + } + + ide, err := devices.FindIDEController("") + if err != nil { + log.Fatalf("Unable to find IDE device from VM configuration\n%v", err) + } + + cdrom, err := devices.CreateCdrom(ide) + if err != nil { + log.Fatalf("Unable to create new CDROM device\n%v", err) + } + + var add []types.BaseVirtualDevice + add = append(add, devices.InsertIso(cdrom, dss.Path(fmt.Sprintf("%s/%s", *newVM.vmName, "linuxkit.iso")))) + + log.Infof("Adding ISO to the Virtual Machine") + + if vm.AddDevice(ctx, add...); err != nil { + log.Fatalf("Unable to add new CD-ROM device to VM configuration\n%v", err) + } +} + +func checkFile(file string) { + if _, err := os.Stat(file); err != nil { + if os.IsPermission(err) { + log.Fatalf("Unable to read file [%s], please check permissions", file) + } + if os.IsNotExist(err) { + log.Fatalf("File [%s], does not exist", file) + } + } +} diff --git a/vendor.conf b/vendor.conf index ea6fda7cf..9f30d5390 100644 --- a/vendor.conf +++ b/vendor.conf @@ -10,6 +10,7 @@ github.com/packethost/packngo 91d54000aa56874149d348a884ba083c41d38091 github.com/rneugeba/iso9660wrap 4606f848a055435cdef85305960b0e1bb788d506 github.com/satori/go.uuid b061729afc07e77a8aa4fad0a2fd840958f1942a github.com/surma/gocpio fcb68777e7dc4ea43ffce871b552c0d073c17495 +github.com/vmware/govmomi 6f8ebd89d521d9f9af7a6c2219c4deee511020dd golang.org/x/crypto 573951cbe80bb6352881271bb276f48749eab6f4 golang.org/x/net a6577fac2d73be281a500b310739095313165611 golang.org/x/oauth2 1611bb46e67abc64a71ecc5c3ae67f1cbbc2b921