github.com/vmware/govmomi@v0.51.0/simulator/container_virtual_machine.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  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"net/http"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"github.com/google/uuid"
    20  
    21  	"github.com/vmware/govmomi/vim25/methods"
    22  	"github.com/vmware/govmomi/vim25/types"
    23  )
    24  
    25  const ContainerBackingOptionKey = "RUN.container"
    26  
    27  var (
    28  	toolsRunning = []types.PropertyChange{
    29  		{Name: "guest.toolsStatus", Val: types.VirtualMachineToolsStatusToolsOk},
    30  		{Name: "guest.toolsRunningStatus", Val: string(types.VirtualMachineToolsRunningStatusGuestToolsRunning)},
    31  	}
    32  
    33  	toolsNotRunning = []types.PropertyChange{
    34  		{Name: "guest.toolsStatus", Val: types.VirtualMachineToolsStatusToolsNotRunning},
    35  		{Name: "guest.toolsRunningStatus", Val: string(types.VirtualMachineToolsRunningStatusGuestToolsNotRunning)},
    36  	}
    37  )
    38  
    39  type simVM struct {
    40  	vm *VirtualMachine
    41  	c  *container
    42  }
    43  
    44  // createSimulationVM inspects the provided VirtualMachine and creates a simVM binding for it if
    45  // the vm.Config.ExtraConfig set contains a key "RUN.container".
    46  // If the ExtraConfig set does not contain that key, this returns nil.
    47  // Methods on the simVM type are written to check for nil object so the return from this call can be blindly
    48  // assigned and invoked without the caller caring about whether a binding for a backing container was warranted.
    49  func createSimulationVM(vm *VirtualMachine) *simVM {
    50  	svm := &simVM{
    51  		vm: vm,
    52  	}
    53  
    54  	for _, opt := range vm.Config.ExtraConfig {
    55  		val := opt.GetOptionValue()
    56  		if val.Key == ContainerBackingOptionKey {
    57  			return svm
    58  		}
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  // applies container network settings to vm.Guest properties.
    65  func (svm *simVM) syncNetworkConfigToVMGuestProperties() error {
    66  	if svm == nil {
    67  		return nil
    68  	}
    69  
    70  	out, detail, err := svm.c.inspect()
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	svm.vm.Config.Annotation = "inspect"
    76  	svm.vm.logPrintf("%s: %s", svm.vm.Config.Annotation, string(out))
    77  
    78  	netS := detail.NetworkSettings.networkSettings
    79  
    80  	// ? Why is this valid - we're taking the first entry while iterating over a MAP
    81  	for _, n := range detail.NetworkSettings.Networks {
    82  		netS = n
    83  		break
    84  	}
    85  
    86  	if detail.State.Paused {
    87  		svm.vm.Runtime.PowerState = types.VirtualMachinePowerStateSuspended
    88  	} else if detail.State.Running {
    89  		svm.vm.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOn
    90  	} else {
    91  		svm.vm.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOff
    92  	}
    93  
    94  	svm.vm.Guest.IpAddress = netS.IPAddress
    95  	svm.vm.Summary.Guest.IpAddress = netS.IPAddress
    96  	if svm.vm.Guest.HostName == "" {
    97  		svm.vm.Guest.HostName = detail.Config.Hostname
    98  	}
    99  
   100  	if len(svm.vm.Guest.Net) != 0 {
   101  		net := &svm.vm.Guest.Net[0]
   102  		net.IpAddress = []string{netS.IPAddress}
   103  		net.MacAddress = netS.MacAddress
   104  		net.IpConfig = &types.NetIpConfigInfo{
   105  			IpAddress: []types.NetIpConfigInfoIpAddress{{
   106  				IpAddress:    netS.IPAddress,
   107  				PrefixLength: int32(netS.IPPrefixLen),
   108  				State:        string(types.NetIpConfigInfoIpAddressStatusPreferred),
   109  			}},
   110  		}
   111  
   112  		gsi := types.GuestStackInfo{
   113  			DnsConfig: &types.NetDnsConfigInfo{
   114  				Dhcp:         false,
   115  				HostName:     svm.vm.Guest.HostName,
   116  				DomainName:   detail.Config.Domainname,
   117  				IpAddress:    detail.Config.DNS,
   118  				SearchDomain: nil,
   119  			},
   120  			IpRouteConfig: &types.NetIpRouteConfigInfo{
   121  				IpRoute: []types.NetIpRouteConfigInfoIpRoute{{
   122  					Network:      "0.0.0.0",
   123  					PrefixLength: 0,
   124  					Gateway: types.NetIpRouteConfigInfoGateway{
   125  						IpAddress: netS.Gateway,
   126  						Device:    "0",
   127  					},
   128  				}},
   129  			},
   130  		}
   131  		svm.vm.Guest.IpStack = []types.GuestStackInfo{gsi}
   132  	}
   133  
   134  	for _, d := range svm.vm.Config.Hardware.Device {
   135  		if eth, ok := d.(types.BaseVirtualEthernetCard); ok {
   136  			eth.GetVirtualEthernetCard().MacAddress = netS.MacAddress
   137  			break
   138  		}
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func (svm *simVM) prepareGuestOperation(auth types.BaseGuestAuthentication) types.BaseMethodFault {
   145  	if svm == nil || svm.c == nil || svm.c.id == "" {
   146  		return new(types.GuestOperationsUnavailable)
   147  	}
   148  
   149  	if svm.vm.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOn {
   150  		return &types.InvalidPowerState{
   151  			RequestedState: types.VirtualMachinePowerStatePoweredOn,
   152  			ExistingState:  svm.vm.Runtime.PowerState,
   153  		}
   154  	}
   155  
   156  	switch creds := auth.(type) {
   157  	case *types.NamePasswordAuthentication:
   158  		if creds.Username == "" || creds.Password == "" {
   159  			return new(types.InvalidGuestLogin)
   160  		}
   161  	default:
   162  		return new(types.InvalidGuestLogin)
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  // populateDMI writes BIOS UUID DMI files to a container volume
   169  func (svm *simVM) populateDMI() error {
   170  	if svm.c == nil {
   171  		return nil
   172  	}
   173  
   174  	files := []tarEntry{
   175  		{
   176  			&tar.Header{
   177  				Name: "product_uuid",
   178  				Mode: 0444,
   179  			},
   180  			[]byte(productUUID(svm.vm.uid)),
   181  		},
   182  		{
   183  			&tar.Header{
   184  				Name: "product_serial",
   185  				Mode: 0444,
   186  			},
   187  			[]byte(productSerial(svm.vm.uid)),
   188  		},
   189  	}
   190  
   191  	_, err := svm.c.createVolume("dmi", []string{deleteWithContainer}, files)
   192  	return err
   193  }
   194  
   195  // start runs the container if specified by the RUN.container extraConfig property.
   196  // lazily creates a container backing if specified by an ExtraConfig property with key "RUN.container"
   197  func (svm *simVM) start(ctx *Context) error {
   198  	if svm == nil {
   199  		return nil
   200  	}
   201  
   202  	if svm.c != nil && svm.c.id != "" {
   203  		err := svm.c.start(ctx)
   204  		if err != nil {
   205  			log.Printf("%s %s: %s", svm.vm.Name, "start", err)
   206  		} else {
   207  			ctx.Update(svm.vm, toolsRunning)
   208  		}
   209  
   210  		return err
   211  	}
   212  
   213  	var args []string
   214  	var env []string
   215  	var ports []string
   216  	mountDMI := true
   217  
   218  	for _, opt := range svm.vm.Config.ExtraConfig {
   219  		val := opt.GetOptionValue()
   220  		if val.Key == ContainerBackingOptionKey {
   221  			run := val.Value.(string)
   222  			err := json.Unmarshal([]byte(run), &args)
   223  			if err != nil {
   224  				args = []string{run}
   225  			}
   226  
   227  			continue
   228  		}
   229  
   230  		if val.Key == "RUN.mountdmi" {
   231  			var mount bool
   232  			err := json.Unmarshal([]byte(val.Value.(string)), &mount)
   233  			if err == nil {
   234  				mountDMI = mount
   235  			}
   236  
   237  			continue
   238  		}
   239  
   240  		if strings.HasPrefix(val.Key, "RUN.port.") {
   241  			// ? would this not make more sense as a set of tuples in the value?
   242  			// or inlined into the RUN.container freeform string as is the case with the nginx volume in the examples?
   243  			sKey := strings.Split(val.Key, ".")
   244  			containerPort := sKey[len(sKey)-1]
   245  			ports = append(ports, fmt.Sprintf("%s:%s", val.Value.(string), containerPort))
   246  
   247  			continue
   248  		}
   249  
   250  		if strings.HasPrefix(val.Key, "RUN.env.") {
   251  			sKey := strings.Split(val.Key, ".")
   252  			envKey := sKey[len(sKey)-1]
   253  			env = append(env, fmt.Sprintf("%s=%s", envKey, val.Value.(string)))
   254  		}
   255  
   256  		if strings.HasPrefix(val.Key, "guestinfo.") {
   257  			key := strings.Replace(strings.ToUpper(val.Key), ".", "_", -1)
   258  			env = append(env, fmt.Sprintf("VMX_%s=%s", key, val.Value.(string)))
   259  
   260  			continue
   261  		}
   262  	}
   263  
   264  	if len(args) == 0 {
   265  		// not an error - it's simply a simVM that shouldn't be backed by a container
   266  		return nil
   267  	}
   268  
   269  	if len(env) != 0 {
   270  		// Configure env as the data access method for cloud-init-vmware-guestinfo
   271  		env = append(env, "VMX_GUESTINFO=true")
   272  	}
   273  
   274  	volumes := []string{}
   275  	if mountDMI {
   276  		volumes = append(volumes, constructVolumeName(svm.vm.Name, svm.vm.uid.String(), "dmi")+":/sys/class/dmi/id")
   277  	}
   278  
   279  	var err error
   280  	svm.c, err = create(ctx, svm.vm.Name, svm.vm.uid.String(), nil, volumes, ports, env, args[0], args[1:])
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	if mountDMI {
   286  		// not combined with the test assembling volumes because we want to have the container name first.
   287  		// cannot add a label to a volume after creation, so if we want to associate with the container ID the
   288  		// container must come first
   289  		err = svm.populateDMI()
   290  		if err != nil {
   291  			return err
   292  		}
   293  	}
   294  
   295  	err = svm.c.start(ctx)
   296  	if err != nil {
   297  		log.Printf("%s %s: %s %s", svm.vm.Name, "start", args, err)
   298  		return err
   299  	}
   300  
   301  	ctx.Update(svm.vm, toolsRunning)
   302  
   303  	svm.vm.logPrintf("%s: %s", args, svm.c.id)
   304  
   305  	if err = svm.syncNetworkConfigToVMGuestProperties(); err != nil {
   306  		log.Printf("%s inspect %s: %s", svm.vm.Name, svm.c.id, err)
   307  	}
   308  
   309  	callback := func(details *containerDetails, c *container) error {
   310  		if c.id == "" && svm.vm != nil {
   311  			// If the container cannot be found then destroy this VM unless the VM is no longer configured for container backing (svm.vm == nil)
   312  			taskRef := svm.vm.DestroyTask(ctx, &types.Destroy_Task{This: svm.vm.Self}).(*methods.Destroy_TaskBody).Res.Returnval
   313  			task, ok := ctx.Map.Get(taskRef).(*Task)
   314  			if !ok {
   315  				panic(fmt.Sprintf("couldn't retrieve task for moref %+q while deleting VM %s", taskRef, svm.vm.Name))
   316  			}
   317  
   318  			// Wait for the task to complete and see if there is an error.
   319  			task.Wait()
   320  			if task.Info.Error != nil {
   321  				msg := fmt.Sprintf("failed to destroy vm: err=%v", *task.Info.Error)
   322  				svm.vm.logPrintf(msg)
   323  
   324  				return errors.New(msg)
   325  			}
   326  		}
   327  
   328  		return svm.syncNetworkConfigToVMGuestProperties()
   329  	}
   330  
   331  	// Start watching the container resource.
   332  	err = svm.c.watchContainer(ctx, callback)
   333  	if _, ok := err.(uninitializedContainer); ok {
   334  		// the container has been deleted before we could watch, despite successful launch so clean up.
   335  		callback(nil, svm.c)
   336  
   337  		// successful launch so nil the error
   338  		return nil
   339  	}
   340  
   341  	return err
   342  }
   343  
   344  // stop the container (if any) for the given vm.
   345  func (svm *simVM) stop(ctx *Context) error {
   346  	if svm == nil || svm.c == nil {
   347  		return nil
   348  	}
   349  
   350  	err := svm.c.stop(ctx)
   351  	if err != nil {
   352  		log.Printf("%s %s: %s", svm.vm.Name, "stop", err)
   353  
   354  		return err
   355  	}
   356  
   357  	ctx.Update(svm.vm, toolsNotRunning)
   358  
   359  	return nil
   360  }
   361  
   362  // pause the container (if any) for the given vm.
   363  func (svm *simVM) pause(ctx *Context) error {
   364  	if svm == nil || svm.c == nil {
   365  		return nil
   366  	}
   367  
   368  	err := svm.c.pause(ctx)
   369  	if err != nil {
   370  		log.Printf("%s %s: %s", svm.vm.Name, "pause", err)
   371  
   372  		return err
   373  	}
   374  
   375  	ctx.Update(svm.vm, toolsNotRunning)
   376  
   377  	return nil
   378  }
   379  
   380  // restart the container (if any) for the given vm.
   381  func (svm *simVM) restart(ctx *Context) error {
   382  	if svm == nil || svm.c == nil {
   383  		return nil
   384  	}
   385  
   386  	err := svm.c.restart(ctx)
   387  	if err != nil {
   388  		log.Printf("%s %s: %s", svm.vm.Name, "restart", err)
   389  
   390  		return err
   391  	}
   392  
   393  	ctx.Update(svm.vm, toolsRunning)
   394  
   395  	return nil
   396  }
   397  
   398  // remove the container (if any) for the given vm.
   399  func (svm *simVM) remove(ctx *Context) error {
   400  	if svm == nil || svm.c == nil {
   401  		return nil
   402  	}
   403  
   404  	err := svm.c.remove(ctx)
   405  	if err != nil {
   406  		log.Printf("%s %s: %s", svm.vm.Name, "remove", err)
   407  
   408  		return err
   409  	}
   410  
   411  	return nil
   412  }
   413  
   414  func (svm *simVM) exec(ctx *Context, auth types.BaseGuestAuthentication, args []string) (string, types.BaseMethodFault) {
   415  	if svm == nil || svm.c == nil {
   416  		return "", nil
   417  	}
   418  
   419  	fault := svm.prepareGuestOperation(auth)
   420  	if fault != nil {
   421  		return "", fault
   422  	}
   423  
   424  	out, err := svm.c.exec(ctx, args)
   425  	if err != nil {
   426  		log.Printf("%s: %s (%s)", svm.vm.Name, args, string(out))
   427  		return "", new(types.GuestOperationsFault)
   428  	}
   429  
   430  	return strings.TrimSpace(string(out)), nil
   431  }
   432  
   433  func guestUpload(id string, file string, r *http.Request) error {
   434  	// TODO: decide behaviour for no container
   435  	err := copyToGuest(id, file, r.ContentLength, r.Body)
   436  	_ = r.Body.Close()
   437  	return err
   438  }
   439  
   440  func guestDownload(id string, file string, w http.ResponseWriter) error {
   441  	// TODO: decide behaviour for no container
   442  	sink := func(len int64, r io.Reader) error {
   443  		w.Header().Set("Content-Length", strconv.FormatInt(len, 10))
   444  		_, err := io.Copy(w, r)
   445  		return err
   446  	}
   447  
   448  	err := copyFromGuest(id, file, sink)
   449  	return err
   450  }
   451  
   452  const guestPrefix = "/guestFile/"
   453  
   454  // ServeGuest handles container guest file upload/download
   455  func ServeGuest(w http.ResponseWriter, r *http.Request) {
   456  	// Real vCenter form: /guestFile?id=139&token=...
   457  	// vcsim form:        /guestFile/tmp/foo/bar?id=ebc8837b8cb6&token=...
   458  
   459  	id := r.URL.Query().Get("id")
   460  	file := strings.TrimPrefix(r.URL.Path, guestPrefix[:len(guestPrefix)-1])
   461  	var err error
   462  
   463  	switch r.Method {
   464  	case http.MethodPut:
   465  		err = guestUpload(id, file, r)
   466  	case http.MethodGet:
   467  		err = guestDownload(id, file, w)
   468  	default:
   469  		w.WriteHeader(http.StatusMethodNotAllowed)
   470  		return
   471  	}
   472  
   473  	if err != nil {
   474  		log.Printf("%s %s: %s", r.Method, r.URL, err)
   475  		w.WriteHeader(http.StatusInternalServerError)
   476  	}
   477  }
   478  
   479  // productSerial returns the uuid in /sys/class/dmi/id/product_serial format
   480  func productSerial(id uuid.UUID) string {
   481  	var dst [len(id)*2 + len(id) - 1]byte
   482  
   483  	j := 0
   484  	for i := 0; i < len(id); i++ {
   485  		hex.Encode(dst[j:j+2], id[i:i+1])
   486  		j += 3
   487  		if j < len(dst) {
   488  			s := j - 1
   489  			if s == len(dst)/2 {
   490  				dst[s] = '-'
   491  			} else {
   492  				dst[s] = ' '
   493  			}
   494  		}
   495  	}
   496  
   497  	return fmt.Sprintf("VMware-%s", string(dst[:]))
   498  }
   499  
   500  // productUUID returns the uuid in /sys/class/dmi/id/product_uuid format
   501  func productUUID(id uuid.UUID) string {
   502  	var dst [36]byte
   503  
   504  	hex.Encode(dst[0:2], id[3:4])
   505  	hex.Encode(dst[2:4], id[2:3])
   506  	hex.Encode(dst[4:6], id[1:2])
   507  	hex.Encode(dst[6:8], id[0:1])
   508  	dst[8] = '-'
   509  	hex.Encode(dst[9:11], id[5:6])
   510  	hex.Encode(dst[11:13], id[4:5])
   511  	dst[13] = '-'
   512  	hex.Encode(dst[14:16], id[7:8])
   513  	hex.Encode(dst[16:18], id[6:7])
   514  	dst[18] = '-'
   515  	hex.Encode(dst[19:23], id[8:10])
   516  	dst[23] = '-'
   517  	hex.Encode(dst[24:], id[10:])
   518  
   519  	return strings.ToUpper(string(dst[:]))
   520  }