github.com/vmware/govmomi@v0.37.2/simulator/container_virtual_machine.go (about)

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