github.com/vmware/govmomi@v0.51.0/vapi/vm/simulator/simulator.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 "context" 9 "log" 10 "net/http" 11 "net/url" 12 "strings" 13 14 "github.com/google/uuid" 15 16 "github.com/vmware/govmomi/simulator" 17 "github.com/vmware/govmomi/vapi/rest" 18 vapi "github.com/vmware/govmomi/vapi/simulator" 19 "github.com/vmware/govmomi/vapi/vm/dataset" 20 "github.com/vmware/govmomi/vapi/vm/internal" 21 "github.com/vmware/govmomi/vim25/methods" 22 "github.com/vmware/govmomi/vim25/types" 23 ) 24 25 const ( 26 // Minimal VM hardware version which supports DataSets feature 27 minVmHardwareVersionDataSets = "vmx-20" 28 29 typeVM = "VirtualMachine" 30 ) 31 32 func init() { 33 simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) { 34 New(s.Listen).Register(s, r) 35 }) 36 } 37 38 type Handler struct { 39 u *url.URL 40 registry *simulator.Registry 41 } 42 43 func New(u *url.URL) *Handler { 44 h := &Handler{ 45 u: u, 46 } 47 return h 48 } 49 50 const ( 51 restPathPrefix = rest.Path + internal.LegacyVCenterVMPath + "/" 52 apiPathPrefix = internal.VCenterVMPath + "/" 53 ) 54 55 func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) { 56 if r.IsVPX() { 57 h.registry = r 58 s.HandleFunc(restPathPrefix, h.handle) 59 s.HandleFunc(apiPathPrefix, h.handle) 60 } 61 } 62 63 // path starts with "/api/vcenter/vm" 64 func (h *Handler) handle(w http.ResponseWriter, r *http.Request) { 65 // The standard http.ServeMux does not support placeholders, so traverse the path segment by segment. 66 // 'tail' tracks the remaining path segments. 67 p := r.URL.Path 68 p = strings.TrimPrefix(p, restPathPrefix) 69 p = strings.TrimPrefix(p, apiPathPrefix) 70 tail := strings.Split(p, "/") 71 if len(tail) == 0 { 72 // "/api/vcenter/vm" 73 http.NotFound(w, r) 74 } else { 75 // "/api/vcenter/vm/..." 76 switch tail[0] { 77 case "": 78 http.NotFound(w, r) 79 return 80 default: 81 vmId := tail[0] 82 h.handleVm(w, r, tail[1:], vmId) 83 } 84 } 85 } 86 87 // path starts with "/api/vcenter/vm/{}" 88 func (h *Handler) handleVm(w http.ResponseWriter, r *http.Request, tail []string, vmId string) { 89 vm := h.validateVmExists(w, r, vmId) 90 if vm == nil { 91 return 92 } 93 ctx := &simulator.Context{ 94 Context: context.Background(), 95 Session: &simulator.Session{ 96 UserSession: types.UserSession{ 97 Key: uuid.New().String(), 98 }, 99 Registry: h.registry, 100 }, 101 Map: h.registry, 102 } 103 h.registry.WithLock(ctx, vm.Reference(), func() { 104 if len(tail) == 0 { 105 // "/api/vcenter/vm/{}" 106 switch r.Method { 107 case http.MethodDelete: 108 h.deleteVM(w, r, ctx, vm) 109 default: 110 http.NotFound(w, r) 111 } 112 } else { 113 // "/api/vcenter/vm/{}/..." 114 switch tail[0] { 115 case "data-sets": 116 h.handleVmDataSets(w, r, tail[1:], vm) 117 default: 118 http.NotFound(w, r) 119 } 120 } 121 }) 122 } 123 124 // path starts with "/api/vcenter/vm/{}/data-sets" 125 func (h *Handler) handleVmDataSets(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine) { 126 if !h.validateVmHardwareVersionDataSets(w, r, vm) { 127 return 128 } 129 if len(tail) == 0 { 130 // "/api/vcenter/vm/{}/data-sets" 131 switch r.Method { 132 case http.MethodGet: 133 h.listDataSets(w, r, vm) 134 case http.MethodPost: 135 h.createDataSet(w, r, vm) 136 default: 137 http.NotFound(w, r) 138 } 139 } else { 140 // "/api/vcenter/vm/{}/data-sets/..." 141 switch tail[0] { 142 case "": 143 http.NotFound(w, r) 144 return 145 default: 146 dataSetId := tail[0] 147 h.handleVmDataSet(w, r, tail[1:], vm, dataSetId) 148 } 149 } 150 } 151 152 // path starts with "/api/vcenter/vm/{}/data-sets/{}" 153 func (h *Handler) handleVmDataSet(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSetId string) { 154 dataSet := h.validateDataSetExists(w, r, vm, dataSetId) 155 if dataSet == nil { 156 return 157 } 158 if len(tail) == 0 { 159 // "/api/vcenter/vm/{}/data-sets/{}" 160 switch r.Method { 161 case http.MethodGet: 162 vapi.StatusOK(w, dataSet.Info) 163 case http.MethodPatch: 164 h.updateDataSet(w, r, vm, dataSet) 165 case http.MethodDelete: 166 h.deleteDataSet(w, r, vm, dataSet) 167 default: 168 http.NotFound(w, r) 169 } 170 } else { 171 // "/api/vcenter/vm/{}/data-sets/{}/..." 172 switch tail[0] { 173 case "entries": 174 h.handleVmDataSetEntries(w, r, tail[1:], vm, dataSet) 175 default: 176 http.NotFound(w, r) 177 } 178 179 } 180 } 181 182 // path starts with "/api/vcenter/vm/{}/data-sets/{}/entries" 183 func (h *Handler) handleVmDataSetEntries(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) { 184 if len(tail) == 0 { 185 // "/api/vcenter/vm/{}/data-sets/{}/entries" 186 switch r.Method { 187 case http.MethodGet: 188 h.listDataSetEntries(w, r, vm, dataSet) 189 default: 190 http.NotFound(w, r) 191 } 192 } else { 193 // "/api/vcenter/vm/{}/data-sets/{}/entries/..." 194 switch tail[0] { 195 case "": 196 http.NotFound(w, r) 197 return 198 default: 199 entryKey := tail[0] 200 h.handleVmDataSetEntry(w, r, tail[1:], vm, dataSet, entryKey) 201 } 202 } 203 } 204 205 // path starts with "/api/vcenter/vm/{}/data-sets/{}/entries/{}" 206 func (h *Handler) handleVmDataSetEntry(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) { 207 if len(tail) > 0 { 208 http.NotFound(w, r) 209 return 210 } 211 switch r.Method { 212 case http.MethodGet: 213 h.getDataSetEntry(w, r, vm, dataSet, entryKey) 214 case http.MethodPut: 215 h.setDataSetEntry(w, r, vm, dataSet, entryKey) 216 case http.MethodDelete: 217 h.deleteDataSetEntry(w, r, vm, dataSet, entryKey) 218 default: 219 http.NotFound(w, r) 220 } 221 } 222 223 func (h *Handler) deleteVM(w http.ResponseWriter, r *http.Request, ctx *simulator.Context, vm *simulator.VirtualMachine) { 224 taskRef := vm.DestroyTask(ctx, &types.Destroy_Task{This: vm.Self}).(*methods.Destroy_TaskBody).Res.Returnval 225 task := ctx.Map.Get(taskRef).(*simulator.Task) 226 task.Wait() 227 if task.Info.Error != nil { 228 log.Printf("%s %s: %v", r.Method, r.RequestURI, task.Info.Error) 229 vapi.ApiErrorGeneral(w) 230 return 231 } 232 vapi.StatusOK(w) 233 } 234 235 func (h *Handler) createDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) { 236 if !h.validateVmIsNotSuspended(w, r, vm) { 237 return 238 } 239 var createSpec dataset.CreateSpec 240 if !vapi.Decode(r, w, &createSpec) { 241 return 242 } 243 if createSpec.Name == "" { 244 vapi.ApiErrorInvalidArgument(w) 245 return 246 } 247 if createSpec.Host == "" { 248 vapi.ApiErrorInvalidArgument(w) 249 return 250 } 251 if createSpec.Guest == "" { 252 vapi.ApiErrorInvalidArgument(w) 253 return 254 } 255 dataSetId := createSpec.Name 256 _, ok := vm.DataSets[dataSetId] 257 if ok { 258 vapi.ApiErrorAlreadyExists(w) 259 return 260 } 261 dataSet := &simulator.DataSet{ 262 Info: &dataset.Info{ 263 Name: createSpec.Name, 264 Description: createSpec.Description, 265 Host: createSpec.Host, 266 Guest: createSpec.Guest, 267 Used: 0, 268 OmitFromSnapshotAndClone: getOrDefault(createSpec.OmitFromSnapshotAndClone, false), 269 }, 270 ID: dataSetId, 271 Entries: make(map[string]string), 272 } 273 vm.DataSets[dataSetId] = dataSet 274 vapi.StatusOK(w, dataSetId) 275 } 276 277 func (h *Handler) listDataSets(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) { 278 var result []dataset.Summary = make([]dataset.Summary, 0, len(vm.DataSets)) 279 for _, v := range vm.DataSets { 280 summary := dataset.Summary{ 281 DataSet: v.ID, 282 Name: v.Name, 283 Description: v.Description, 284 } 285 result = append(result, summary) 286 } 287 vapi.StatusOK(w, result) 288 } 289 290 func (h *Handler) deleteDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) { 291 if !h.validateVmIsNotSuspended(w, r, vm) { 292 return 293 } 294 force := strings.EqualFold(r.URL.Query().Get("force"), "true") 295 if len(dataSet.Entries) > 0 && !force { 296 // cannot delete non-empty data set without force 297 vapi.ApiErrorResourceInUse(w) 298 return 299 } 300 delete(vm.DataSets, dataSet.ID) 301 vapi.StatusOK(w) 302 } 303 304 func (h *Handler) updateDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) { 305 if !h.validateVmIsNotSuspended(w, r, vm) { 306 return 307 } 308 var updateSpec dataset.UpdateSpec 309 if !vapi.Decode(r, w, &updateSpec) { 310 return 311 } 312 if updateSpec.Description != nil { 313 dataSet.Description = *updateSpec.Description 314 } 315 if updateSpec.Host != nil { 316 dataSet.Host = *updateSpec.Host 317 } 318 if updateSpec.Guest != nil { 319 dataSet.Guest = *updateSpec.Guest 320 } 321 if updateSpec.OmitFromSnapshotAndClone != nil { 322 dataSet.OmitFromSnapshotAndClone = *updateSpec.OmitFromSnapshotAndClone 323 } 324 vapi.StatusOK(w) 325 } 326 327 func (h *Handler) listDataSetEntries(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) { 328 if dataSet.Host == dataset.AccessNone { 329 vapi.ApiErrorUnauthorized(w) 330 return 331 } 332 var result []string = make([]string, 0, len(dataSet.Entries)) 333 for k := range dataSet.Entries { 334 result = append(result, k) 335 } 336 vapi.StatusOK(w, result) 337 } 338 339 func (h *Handler) getDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) { 340 if dataSet.Host == dataset.AccessNone { 341 vapi.ApiErrorUnauthorized(w) 342 return 343 } 344 val, ok := dataSet.Entries[entryKey] 345 if !ok { 346 vapi.ApiErrorNotFound(w) 347 return 348 } 349 vapi.StatusOK(w, val) 350 } 351 352 const ( 353 // A key can be at most 4096 bytes. 354 entryKeyMaxLen = 4096 355 // A value can be at most 1MB. 356 entryValueMaxLen = 1024 * 1024 357 ) 358 359 func (h *Handler) setDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) { 360 var val string 361 if !vapi.Decode(r, w, &val) { 362 return 363 } 364 if !h.validateVmIsNotSuspended(w, r, vm) { 365 return 366 } 367 if dataSet.Host != dataset.AccessReadWrite { 368 vapi.ApiErrorUnauthorized(w) 369 return 370 } 371 if len(entryKey) > entryKeyMaxLen { 372 vapi.ApiErrorInvalidArgument(w) 373 return 374 } 375 if len(val) > entryValueMaxLen { 376 vapi.ApiErrorInvalidArgument(w) 377 return 378 } 379 old, ok := dataSet.Entries[entryKey] 380 if ok { 381 dataSet.Used -= len(entryKey) 382 dataSet.Used -= len(old) 383 } 384 dataSet.Entries[entryKey] = val 385 dataSet.Used += len(entryKey) 386 dataSet.Used += len(val) 387 vapi.StatusOK(w) 388 } 389 390 func (h *Handler) deleteDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) { 391 if !h.validateVmIsNotSuspended(w, r, vm) { 392 return 393 } 394 if dataSet.Host != dataset.AccessReadWrite { 395 vapi.ApiErrorUnauthorized(w) 396 return 397 } 398 val, ok := dataSet.Entries[entryKey] 399 if !ok { 400 vapi.ApiErrorNotFound(w) 401 return 402 } 403 dataSet.Used -= len(entryKey) 404 dataSet.Used -= len(val) 405 delete(dataSet.Entries, entryKey) 406 vapi.StatusOK(w) 407 } 408 409 func getOrDefault(b *bool, defaultValue bool) bool { 410 if b == nil { 411 return defaultValue 412 } 413 return *b 414 } 415 416 func (h *Handler) validateVmExists(w http.ResponseWriter, r *http.Request, vmId string) *simulator.VirtualMachine { 417 vm, ok := h.registry.Get(types.ManagedObjectReference{Type: typeVM, Value: vmId}).(*simulator.VirtualMachine) 418 if !ok { 419 vapi.ApiErrorNotFound(w) 420 return nil 421 } 422 return vm 423 } 424 425 func (h *Handler) validateDataSetExists(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSetId string) *simulator.DataSet { 426 dataSet, ok := vm.DataSets[dataSetId] 427 if !ok { 428 vapi.ApiErrorNotFound(w) 429 return nil 430 } 431 return dataSet 432 } 433 434 func (h *Handler) validateVmHardwareVersionDataSets(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) bool { 435 version := vm.Config.Version 436 if len(version) < len(minVmHardwareVersionDataSets) { 437 vapi.ApiErrorUnsupported(w) 438 return false 439 } 440 if len(version) == len(minVmHardwareVersionDataSets) && version < minVmHardwareVersionDataSets { 441 vapi.ApiErrorUnsupported(w) 442 return false 443 } 444 return true 445 } 446 447 func (h *Handler) validateVmIsNotSuspended(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) bool { 448 if vm.Summary.Runtime.PowerState == types.VirtualMachinePowerStateSuspended { 449 vapi.ApiErrorNotAllowedInCurrentState(w) 450 return false 451 } 452 return true 453 }