github.com/vmware/govmomi@v0.51.0/vapi/namespace/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 "archive/tar" 9 "context" 10 "encoding/json" 11 "fmt" 12 "net/http" 13 "net/url" 14 "path" 15 "strings" 16 "time" 17 18 "github.com/google/uuid" 19 20 "github.com/vmware/govmomi" 21 "github.com/vmware/govmomi/property" 22 "github.com/vmware/govmomi/simulator" 23 "github.com/vmware/govmomi/vapi/namespace" 24 vapi "github.com/vmware/govmomi/vapi/simulator" 25 "github.com/vmware/govmomi/view" 26 "github.com/vmware/govmomi/vim25" 27 "github.com/vmware/govmomi/vim25/mo" 28 "github.com/vmware/govmomi/vim25/types" 29 30 "github.com/vmware/govmomi/vapi/namespace/internal" 31 ) 32 33 func init() { 34 simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) { 35 New(s.Listen).Register(s, r) 36 }) 37 } 38 39 // Handler implements the Namespace Management Modules API simulator 40 type Handler struct { 41 URL *url.URL 42 } 43 44 // New creates a Handler instance 45 func New(u *url.URL) *Handler { 46 return &Handler{ 47 URL: u, 48 } 49 } 50 51 // Register Namespace Management API paths with the vapi simulator's http.ServeMux 52 func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) { 53 if r.IsVPX() { 54 s.HandleFunc(internal.NamespacesPath, h.namespaces) 55 s.HandleFunc(internal.NamespacesPath+"/", h.namespaces) 56 s.HandleFunc(internal.NamespaceClusterPath, h.clusters) 57 s.HandleFunc(internal.NamespaceClusterPath+"/", h.clustersID) 58 s.HandleFunc(internal.NamespaceDistributedSwitchCompatibility+"/", h.listCompatibleDistributedSwitches) 59 s.HandleFunc(internal.NamespaceEdgeClusterCompatibility+"/", h.listCompatibleEdgeClusters) 60 61 s.HandleFunc(internal.SupervisorServicesPath, h.listServices) 62 s.HandleFunc(internal.SupervisorServicesPath+"/", h.getService) 63 s.HandleFunc(internal.SupervisorServicesPath+"/{id}"+internal.SupervisorServicesVersionsPath, h.versionsForService) 64 s.HandleFunc(internal.SupervisorServicesPath+"/{id}"+internal.SupervisorServicesVersionsPath+"/{version}", h.getServiceVersion) 65 66 s.HandleFunc(internal.VmClassesPath, h.vmClasses) 67 s.HandleFunc(internal.VmClassesPath+"/", h.vmClasses) 68 } 69 } 70 71 // enabledClusters returns refs for cluster names with a "WCP-" prefix. 72 // Using the name as a simple hack until we add support for enabling via the API. 73 func enabledClusters(c *govmomi.Client) ([]types.ManagedObjectReference, error) { 74 ctx := context.Background() 75 kind := []string{"ClusterComputeResource"} 76 77 m := view.NewManager(c.Client) 78 v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, kind, true) 79 if err != nil { 80 return nil, err 81 } 82 defer func() { _ = v.Destroy(ctx) }() 83 84 return v.Find(ctx, kind, property.Match{"name": "WCP-*"}) 85 } 86 87 func (h *Handler) clusters(w http.ResponseWriter, r *http.Request) { 88 c, err := govmomi.NewClient(context.Background(), h.URL, true) 89 if err != nil { 90 panic(err) 91 } 92 93 switch r.Method { 94 case http.MethodGet: 95 refs, err := enabledClusters(c) 96 if err != nil { 97 panic(err) 98 } 99 100 clusters := make([]namespace.ClusterSummary, len(refs)) 101 for i, ref := range refs { 102 clusters[i] = namespace.ClusterSummary{ 103 ID: ref.Value, 104 ConfigStatus: &namespace.RunningConfigStatus, 105 KubernetesStatus: &namespace.ReadyKubernetesStatus, 106 } 107 } 108 vapi.StatusOK(w, clusters) 109 } 110 } 111 112 func (h *Handler) clustersSupportBundle(w http.ResponseWriter, r *http.Request) { 113 var token internal.SupportBundleToken 114 _ = json.NewDecoder(r.Body).Decode(&token) 115 _ = r.Body.Close() 116 117 if token.Value == "" { 118 u := *h.URL 119 u.Path = r.URL.Path 120 // Create support bundle request 121 location := namespace.SupportBundleLocation{ 122 Token: namespace.SupportBundleToken{ 123 Token: uuid.New().String(), 124 }, 125 URL: u.String(), 126 } 127 128 vapi.StatusOK(w, &location) 129 return 130 } 131 132 // Get support bundle 133 id := path.Base(path.Dir(r.URL.Path)) 134 name := fmt.Sprintf("wcp-support-bundle-%s-%s--00-00.tar", id, time.Now().Format("2006Jan02")) 135 136 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name)) 137 w.Header().Set("Content-Type", "application/octet-stream") 138 139 readme := "vcsim generated support bundle" 140 tw := tar.NewWriter(w) 141 _ = tw.WriteHeader(&tar.Header{ 142 Name: "README", 143 Size: int64(len(readme) + 1), 144 Mode: 0444, 145 ModTime: time.Now(), 146 }) 147 _, _ = fmt.Fprintln(tw, readme) 148 _ = tw.Close() 149 } 150 151 func (h *Handler) clustersID(w http.ResponseWriter, r *http.Request) { 152 id := path.Base(r.URL.Path) 153 route := map[string]func(http.ResponseWriter, *http.Request){ 154 "support-bundle": h.clustersSupportBundle, 155 }[id] 156 157 if route != nil { 158 route(w, r) 159 return 160 } 161 162 // TODO: 163 // https://vmware.github.io/vsphere-automation-sdk-rest/vsphere/index.html#SVC_com.vmware.vcenter.namespace_management.clusters 164 } 165 166 func (h *Handler) listCompatibleDistributedSwitches(w http.ResponseWriter, r *http.Request) { 167 switch r.Method { 168 case http.MethodGet: 169 170 // normally expect to get exactly one result back 171 switches := []namespace.DistributedSwitchCompatibilitySummary{ 172 { 173 Compatible: true, 174 DistributedSwitch: "Compatible-DVS-1", 175 }, 176 } 177 vapi.StatusOK(w, switches) 178 } 179 } 180 181 func (h *Handler) listCompatibleEdgeClusters(w http.ResponseWriter, r *http.Request) { 182 switch r.Method { 183 case http.MethodGet: 184 185 // CLI is able to filter in case we get multiple results 186 switches := []namespace.EdgeClusterCompatibilitySummary{ 187 { 188 Compatible: true, 189 EdgeCluster: "Compat-Edge-ID1", 190 DisplayName: "Edge-Cluster-1", 191 }, 192 { 193 Compatible: true, 194 EdgeCluster: "Compat-Edge-ID2", 195 DisplayName: "Edge-Cluster-2", 196 }, 197 } 198 vapi.StatusOK(w, switches) 199 } 200 } 201 202 // Some fake services, service 1 has 2 versions, service 2 has 1 version 203 // Summary for services 204 var supervisorServicesMap = map[string]namespace.SupervisorServiceSummary{ 205 "service1": { 206 ID: "service1", 207 Name: "mock-service-1", 208 State: "ACTIVATED", 209 }, 210 "service2": { 211 ID: "service2", 212 Name: "mock-service-2", 213 State: "DEACTIVATED", 214 }, 215 } 216 217 // Summary for service versions 218 var supervisorServiceVersionsMap = map[string][]namespace.SupervisorServiceVersionSummary{ 219 "service1": { 220 { 221 SupervisorServiceInfo: namespace.SupervisorServiceInfo{ 222 Name: "mock-service-1 v1 display name", 223 State: "ACTIVATED", 224 Description: "This is service 1 version 1.0.0", 225 }, 226 Version: "1.0.0", 227 }, 228 { 229 SupervisorServiceInfo: namespace.SupervisorServiceInfo{ 230 Name: "mock-service-1 v2 display name", 231 State: "DEACTIVATED", 232 Description: "This is service 1 version 2.0.0", 233 }, 234 Version: "2.0.0", 235 }}, 236 "service2": { 237 { 238 SupervisorServiceInfo: namespace.SupervisorServiceInfo{ 239 Name: "mock-service-2 v1 display name", 240 State: "ACTIVATED", 241 Description: "This is service 2 version 1.1.0", 242 }, 243 Version: "1.1.0", 244 }, 245 }, 246 } 247 248 func (h *Handler) listServices(w http.ResponseWriter, r *http.Request) { 249 switch r.Method { 250 case http.MethodGet: 251 supervisorServices := make([]namespace.SupervisorServiceSummary, len(supervisorServicesMap)) 252 i := 0 253 for _, service := range supervisorServicesMap { 254 supervisorServices[i] = service 255 i++ 256 } 257 vapi.StatusOK(w, supervisorServices) 258 } 259 } 260 261 func (h *Handler) getService(w http.ResponseWriter, r *http.Request) { 262 id := path.Base(r.URL.Path) 263 switch r.Method { 264 case http.MethodGet: 265 if result, contains := supervisorServicesMap[id]; contains { 266 vapi.StatusOK(w, result) 267 } else { 268 vapi.ApiErrorNotFound(w) 269 } 270 return 271 } 272 } 273 274 // versionsForService returns the list of versions for a particular service 275 func (h *Handler) versionsForService(w http.ResponseWriter, r *http.Request) { 276 fmt.Printf("In versionsForService: %v\n", r.URL.Path) 277 id := r.PathValue("id") 278 switch r.Method { 279 case http.MethodGet: 280 if result, contains := supervisorServiceVersionsMap[id]; contains { 281 vapi.StatusOK(w, result) 282 } else { 283 vapi.ApiErrorNotFound(w) 284 } 285 return 286 } 287 } 288 289 // getServiceVersion returns info on a particular service version 290 func (h *Handler) getServiceVersion(w http.ResponseWriter, r *http.Request) { 291 id := r.PathValue("id") 292 version := r.PathValue("version") 293 294 switch r.Method { 295 case http.MethodGet: 296 if versions, contains := supervisorServiceVersionsMap[id]; contains { 297 for _, v := range versions { 298 if v.Version == version { 299 info := namespace.SupervisorServiceVersionInfo{ 300 SupervisorServiceInfo: namespace.SupervisorServiceInfo{ 301 Description: v.Description, 302 State: v.State, 303 Name: v.Name, 304 }, 305 ContentType: "CARVEL_APPS_YAML", // return Carvel by default 306 Content: "abc", // in reality this is base 64 encoded of content 307 } 308 vapi.StatusOK(w, info) 309 return 310 } 311 } 312 vapi.ApiErrorNotFound(w) 313 } else { 314 vapi.ApiErrorNotFound(w) 315 } 316 return 317 } 318 } 319 320 var namespacesMap = make(map[string]*namespace.NamespacesInstanceInfo) 321 322 func (h *Handler) namespaces(w http.ResponseWriter, r *http.Request) { 323 subpath := r.URL.Path[len(internal.NamespacesPath):] 324 subpath = strings.TrimPrefix(subpath, "/") 325 // TODO: move to 1.22's https://go.dev/blog/routing-enhancements 326 route := strings.Split(subpath, "/") 327 subpath = route[0] 328 action := "" 329 if len(route) > 1 { 330 action = route[1] 331 } 332 333 switch r.Method { 334 case http.MethodGet: 335 if len(subpath) > 0 { 336 if result, contains := namespacesMap[subpath]; contains { 337 vapi.StatusOK(w, result) 338 } else { 339 vapi.ApiErrorNotFound(w) 340 } 341 return 342 } else { 343 result := make([]namespace.NamespacesInstanceSummary, 0, len(namespacesMap)) 344 345 for k, v := range namespacesMap { 346 entry := namespace.NamespacesInstanceSummary{ 347 ClusterId: v.ClusterId, 348 Namespace: k, 349 ConfigStatus: v.ConfigStatus, 350 Description: v.Description, 351 Stats: v.Stats, 352 } 353 result = append(result, entry) 354 } 355 356 vapi.StatusOK(w, result) 357 } 358 case http.MethodPatch: 359 if len(subpath) > 0 { 360 if entry, contains := namespacesMap[subpath]; contains { 361 var spec namespace.NamespacesInstanceUpdateSpec 362 if vapi.Decode(r, w, &spec) { 363 entry.VmServiceSpec = spec.VmServiceSpec 364 vapi.StatusOK(w) 365 } 366 } 367 } 368 369 vapi.ApiErrorNotFound(w) 370 case http.MethodPost: 371 if action == "registervm" { 372 var spec namespace.RegisterVMSpec 373 if !vapi.Decode(r, w, &spec) { 374 return 375 } 376 377 ref := types.ManagedObjectReference{Type: "VirtualMachine", Value: spec.VM} 378 task := types.CreateTask{Obj: ref} 379 key := &mo.Field{Path: "config.extraConfig", Key: "vmservice.virtualmachine.resource.yaml"} 380 381 vapi.StatusOK(w, vapi.RunTask(*h.URL, task, func(ctx context.Context, c *vim25.Client) error { 382 var vm mo.VirtualMachine 383 _ = property.DefaultCollector(c).RetrieveOne(ctx, task.Obj, []string{key.String()}, &vm) 384 if vm.Config == nil || len(vm.Config.ExtraConfig) == 0 { 385 return fmt.Errorf("%s %s not found", task.Obj, key) 386 } 387 return nil 388 })) 389 return 390 } 391 392 var spec namespace.NamespacesInstanceCreateSpec 393 if !vapi.Decode(r, w, &spec) { 394 return 395 } 396 397 newNamespace := namespace.NamespacesInstanceInfo{ 398 ClusterId: spec.Cluster, 399 ConfigStatus: namespace.RunningConfigStatus.String(), 400 VmServiceSpec: spec.VmServiceSpec, 401 } 402 403 namespacesMap[spec.Namespace] = &newNamespace 404 405 vapi.StatusOK(w) 406 case http.MethodDelete: 407 if len(subpath) > 0 { 408 if _, contains := namespacesMap[subpath]; contains { 409 delete(namespacesMap, subpath) 410 vapi.StatusOK(w) 411 return 412 } 413 } 414 vapi.ApiErrorNotFound(w) 415 } 416 } 417 418 var vmClassesMap = make(map[string]*namespace.VirtualMachineClassInfo) 419 420 func (h *Handler) vmClasses(w http.ResponseWriter, r *http.Request) { 421 subpath := r.URL.Path[len(internal.VmClassesPath):] 422 subpath = strings.Replace(subpath, "/", "", -1) 423 424 switch r.Method { 425 case http.MethodGet: 426 if len(subpath) > 0 { 427 if result, contains := vmClassesMap[subpath]; contains { 428 vapi.StatusOK(w, result) 429 } else { 430 vapi.ApiErrorNotFound(w) 431 } 432 return 433 } else { 434 result := make([]*namespace.VirtualMachineClassInfo, 0, len(vmClassesMap)) 435 436 for _, v := range vmClassesMap { 437 result = append(result, v) 438 } 439 440 vapi.StatusOK(w, result) 441 } 442 case http.MethodPatch: 443 if len(subpath) > 0 { 444 if entry, contains := vmClassesMap[subpath]; contains { 445 var spec namespace.VirtualMachineClassUpdateSpec 446 if !vapi.Decode(r, w, &spec) { 447 return 448 } 449 450 entry.CpuCount = spec.CpuCount 451 entry.MemoryMb = spec.MemoryMb 452 entry.CpuReservation = spec.CpuReservation 453 entry.MemoryReservation = spec.MemoryReservation 454 entry.Devices = spec.Devices 455 456 vapi.StatusOK(w) 457 return 458 } 459 } 460 461 vapi.ApiErrorNotFound(w) 462 case http.MethodPost: 463 var spec namespace.VirtualMachineClassCreateSpec 464 if !vapi.Decode(r, w, &spec) { 465 return 466 } 467 468 newClass := namespace.VirtualMachineClassInfo{ 469 Id: spec.Id, 470 CpuCount: spec.CpuCount, 471 MemoryMb: spec.MemoryMb, 472 MemoryReservation: spec.MemoryReservation, 473 CpuReservation: spec.CpuReservation, 474 Devices: spec.Devices, 475 } 476 477 vmClassesMap[spec.Id] = &newClass 478 479 vapi.StatusOK(w) 480 case http.MethodDelete: 481 if len(subpath) > 0 { 482 if _, contains := vmClassesMap[subpath]; contains { 483 delete(vmClassesMap, subpath) 484 vapi.StatusOK(w) 485 return 486 } 487 } 488 vapi.ApiErrorNotFound(w) 489 } 490 }