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  }