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