github.com/vmware/govmomi@v0.43.0/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 }