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 }