github.com/vmware/govmomi@v0.51.0/vapi/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  	"bytes"
    10  	"context"
    11  	"crypto/md5"
    12  	"crypto/rand"
    13  	"crypto/sha1"
    14  	"crypto/sha256"
    15  	"crypto/sha512"
    16  	"crypto/tls"
    17  	"crypto/x509"
    18  	"encoding/base64"
    19  	"encoding/json"
    20  	"encoding/pem"
    21  	"errors"
    22  	"fmt"
    23  	"hash"
    24  	"io"
    25  	"log"
    26  	"math/big"
    27  	"net/http"
    28  	"net/url"
    29  	"os"
    30  	"path"
    31  	"path/filepath"
    32  	"reflect"
    33  	"regexp"
    34  	"strconv"
    35  	"strings"
    36  	"sync"
    37  	"time"
    38  
    39  	"github.com/google/uuid"
    40  
    41  	"github.com/vmware/govmomi"
    42  	"github.com/vmware/govmomi/nfc"
    43  	"github.com/vmware/govmomi/object"
    44  	"github.com/vmware/govmomi/ovf"
    45  	"github.com/vmware/govmomi/simulator"
    46  	"github.com/vmware/govmomi/vapi"
    47  	"github.com/vmware/govmomi/vapi/internal"
    48  	"github.com/vmware/govmomi/vapi/library"
    49  	"github.com/vmware/govmomi/vapi/rest"
    50  	"github.com/vmware/govmomi/vapi/tags"
    51  	"github.com/vmware/govmomi/vapi/vcenter"
    52  	"github.com/vmware/govmomi/view"
    53  	"github.com/vmware/govmomi/vim25"
    54  	"github.com/vmware/govmomi/vim25/methods"
    55  	"github.com/vmware/govmomi/vim25/soap"
    56  	"github.com/vmware/govmomi/vim25/types"
    57  	vim "github.com/vmware/govmomi/vim25/types"
    58  	"github.com/vmware/govmomi/vim25/xml"
    59  	"github.com/vmware/govmomi/vmdk"
    60  )
    61  
    62  type item struct {
    63  	*library.Item
    64  	File     []library.File
    65  	Template *types.ManagedObjectReference
    66  }
    67  
    68  type content struct {
    69  	*library.Library
    70  	Item map[string]*item
    71  	Subs map[string]*library.Subscriber
    72  	VMTX map[string]*types.ManagedObjectReference
    73  }
    74  
    75  type update struct {
    76  	*sync.WaitGroup
    77  	*library.Session
    78  	Library *library.Library
    79  	File    map[string]*library.UpdateFile
    80  }
    81  
    82  type download struct {
    83  	*library.Session
    84  	Library *library.Library
    85  	File    map[string]*library.DownloadFile
    86  }
    87  
    88  type handler struct {
    89  	sync.Mutex
    90  	Map         *simulator.Registry
    91  	ServeMux    *http.ServeMux
    92  	URL         url.URL
    93  	Category    map[string]*tags.Category
    94  	Tag         map[string]*tags.Tag
    95  	Association map[string]map[internal.AssociatedObject]bool
    96  	Session     map[string]*rest.Session
    97  	Library     map[string]*content
    98  	Update      map[string]update
    99  	Download    map[string]download
   100  	Policies    []library.ContentSecurityPoliciesInfo
   101  	Trust       map[string]library.TrustedCertificate
   102  }
   103  
   104  func init() {
   105  	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
   106  		if r.IsVPX() {
   107  			patterns, h := New(s.Listen, r)
   108  			for _, p := range patterns {
   109  				s.Handle(p, h)
   110  			}
   111  		}
   112  	})
   113  }
   114  
   115  // New creates a vAPI simulator.
   116  func New(u *url.URL, r *simulator.Registry) ([]string, http.Handler) {
   117  	s := &handler{
   118  		Map:         r,
   119  		ServeMux:    http.NewServeMux(),
   120  		URL:         *u,
   121  		Category:    make(map[string]*tags.Category),
   122  		Tag:         make(map[string]*tags.Tag),
   123  		Association: make(map[string]map[internal.AssociatedObject]bool),
   124  		Session:     make(map[string]*rest.Session),
   125  		Library:     make(map[string]*content),
   126  		Update:      make(map[string]update),
   127  		Download:    make(map[string]download),
   128  		Policies:    defaultSecurityPolicies(),
   129  		Trust:       make(map[string]library.TrustedCertificate),
   130  	}
   131  
   132  	handlers := []struct {
   133  		p string
   134  		m http.HandlerFunc
   135  	}{
   136  		// /rest/ patterns.
   137  		{internal.SessionPath, s.session},
   138  		{internal.CategoryPath, s.category},
   139  		{internal.CategoryPath + "/", s.categoryID},
   140  		{internal.TagPath, s.tag},
   141  		{internal.TagPath + "/", s.tagID},
   142  		{internal.AssociationPath, s.association},
   143  		{internal.AssociationPath + "/", s.associationID},
   144  		{internal.LibraryPath, s.library},
   145  		{internal.LocalLibraryPath, s.library},
   146  		{internal.SubscribedLibraryPath, s.library},
   147  		{internal.LibraryPath + "/", s.libraryID},
   148  		{internal.LocalLibraryPath + "/", s.libraryID},
   149  		{internal.SubscribedLibraryPath + "/", s.libraryID},
   150  		{internal.Subscriptions, s.subscriptions},
   151  		{internal.Subscriptions + "/", s.subscriptionsID},
   152  		{internal.LibraryItemPath, s.libraryItem},
   153  		{internal.LibraryItemPath + "/", s.libraryItemID},
   154  		{internal.LibraryItemStoragePath, s.libraryItemStorage},
   155  		{internal.LibraryItemStoragePath + "/", s.libraryItemStorageID},
   156  		{internal.SubscribedLibraryItem + "/", s.libraryItemID},
   157  		{internal.LibraryItemUpdateSession, s.libraryItemUpdateSession},
   158  		{internal.LibraryItemUpdateSession + "/", s.libraryItemUpdateSessionID},
   159  		{internal.LibraryItemUpdateSessionFile, s.libraryItemUpdateSessionFile},
   160  		{internal.LibraryItemUpdateSessionFile + "/", s.libraryItemUpdateSessionFileID},
   161  		{internal.LibraryItemDownloadSession, s.libraryItemDownloadSession},
   162  		{internal.LibraryItemDownloadSession + "/", s.libraryItemDownloadSessionID},
   163  		{internal.LibraryItemDownloadSessionFile, s.libraryItemDownloadSessionFile},
   164  		{internal.LibraryItemDownloadSessionFile + "/", s.libraryItemDownloadSessionFileID},
   165  		{internal.LibraryItemFileData + "/", s.libraryItemFileData},
   166  		{internal.LibraryItemFilePath, s.libraryItemFile},
   167  		{internal.LibraryItemFilePath + "/", s.libraryItemFileID},
   168  		{internal.VCenterOVFLibraryItem, s.libraryItemOVF},
   169  		{internal.VCenterOVFLibraryItem + "/", s.libraryItemOVFID},
   170  		{internal.VCenterVMTXLibraryItem, s.libraryItemCreateTemplate},
   171  		{internal.VCenterVMTXLibraryItem + "/", s.libraryItemTemplateID},
   172  		{"/vcenter/certificate-authority/", s.certificateAuthority},
   173  		{internal.DebugEcho, s.debugEcho},
   174  		// /api/ patterns.
   175  		{vapi.Path, s.jsonRPC},
   176  		{internal.SecurityPoliciesPath, s.librarySecurityPolicies},
   177  		{internal.TrustedCertificatesPath, s.libraryTrustedCertificates},
   178  		{internal.TrustedCertificatesPath + "/", s.libraryTrustedCertificatesID},
   179  	}
   180  
   181  	for i := range handlers {
   182  		h := handlers[i]
   183  		s.HandleFunc(h.p, h.m)
   184  	}
   185  
   186  	return []string{
   187  		rest.Path, rest.Path + "/",
   188  		vapi.Path, vapi.Path + "/",
   189  	}, s
   190  }
   191  
   192  func (s *handler) withClient(f func(context.Context, *vim25.Client) error) error {
   193  	return WithClient(s.URL, f)
   194  }
   195  
   196  // WithClient creates invokes f with an authenticated vim25.Client.
   197  func WithClient(u url.URL, f func(context.Context, *vim25.Client) error) error {
   198  	ctx := context.Background()
   199  	c, err := govmomi.NewClient(ctx, &u, true)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	defer func() {
   204  		_ = c.Logout(ctx)
   205  	}()
   206  	return f(ctx, c.Client)
   207  }
   208  
   209  // RunTask creates a Task with the given spec and sets the task state based on error returned by f.
   210  func RunTask(u url.URL, spec types.CreateTask, f func(context.Context, *vim25.Client) error) string {
   211  	var id string
   212  
   213  	err := WithClient(u, func(ctx context.Context, c *vim25.Client) error {
   214  		spec.This = *c.ServiceContent.TaskManager
   215  		if spec.TaskTypeId == "" {
   216  			spec.TaskTypeId = "com.vmware.govmomi.simulator.test"
   217  		}
   218  		res, err := methods.CreateTask(ctx, c, &spec)
   219  		if err != nil {
   220  			return err
   221  		}
   222  
   223  		ref := res.Returnval.Task
   224  		task := object.NewTask(c, ref)
   225  		id = ref.Value + ":" + uuid.NewString()
   226  
   227  		if err = task.SetState(ctx, types.TaskInfoStateRunning, nil, nil); err != nil {
   228  			return err
   229  		}
   230  
   231  		var fault *types.LocalizedMethodFault
   232  		state := types.TaskInfoStateSuccess
   233  		if f != nil {
   234  			err = f(ctx, c)
   235  		}
   236  
   237  		if err != nil {
   238  			fault = &types.LocalizedMethodFault{
   239  				Fault:            &types.SystemError{Reason: err.Error()},
   240  				LocalizedMessage: err.Error(),
   241  			}
   242  			state = types.TaskInfoStateError
   243  		}
   244  
   245  		return task.SetState(ctx, state, nil, fault)
   246  	})
   247  
   248  	if err != nil {
   249  		panic(err) // should not happen
   250  	}
   251  
   252  	return id
   253  }
   254  
   255  // HandleFunc wraps the given handler with authorization checks and passes to http.ServeMux.HandleFunc
   256  func (s *handler) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
   257  	// Rest paths have been moved from /rest/* to /api/*. Account for both the legacy and new cases here.
   258  	if !strings.HasPrefix(pattern, rest.Path) && !strings.HasPrefix(pattern, vapi.Path) {
   259  		pattern = rest.Path + pattern
   260  	}
   261  
   262  	s.ServeMux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
   263  		s.Lock()
   264  		defer s.Unlock()
   265  
   266  		if !s.isAuthorized(r) {
   267  			w.WriteHeader(http.StatusUnauthorized)
   268  			return
   269  		}
   270  
   271  		handler(w, r)
   272  	})
   273  }
   274  
   275  func (s *handler) isAuthorized(r *http.Request) bool {
   276  	if r.Method == http.MethodPost && s.action(r) == "" {
   277  		if r.URL.Path == vapi.Path {
   278  			return true
   279  		}
   280  		if strings.HasSuffix(r.URL.Path, internal.SessionPath) {
   281  			return true
   282  		}
   283  	}
   284  	id := r.Header.Get(internal.SessionCookieName)
   285  	if id == "" {
   286  		if cookie, err := r.Cookie(internal.SessionCookieName); err == nil {
   287  			id = cookie.Value
   288  			r.Header.Set(internal.SessionCookieName, id)
   289  		}
   290  	}
   291  	info, ok := s.Session[id]
   292  	if ok {
   293  		info.LastAccessed = time.Now()
   294  	} else {
   295  		_, ok = s.Update[id]
   296  	}
   297  	return ok
   298  }
   299  
   300  func (s *handler) hasAuthorization(r *http.Request) (string, bool) {
   301  	u, p, ok := r.BasicAuth()
   302  	if ok { // user+pass auth
   303  		return u, s.Map.SessionManager().Authenticate(s.URL, &vim.Login{UserName: u, Password: p})
   304  	}
   305  	auth := r.Header.Get("Authorization")
   306  	return "TODO", strings.HasPrefix(auth, "SIGN ") // token auth
   307  }
   308  
   309  func (s *handler) findTag(e vim.VslmTagEntry) *tags.Tag {
   310  	for _, c := range s.Category {
   311  		if c.Name == e.ParentCategoryName {
   312  			for _, t := range s.Tag {
   313  				if t.Name == e.TagName && t.CategoryID == c.ID {
   314  					return t
   315  				}
   316  			}
   317  		}
   318  	}
   319  	return nil
   320  }
   321  
   322  // AttachedObjects is meant for internal use via simulator.Registry.tagManager
   323  func (s *handler) AttachedObjects(tag vim.VslmTagEntry) ([]vim.ManagedObjectReference, vim.BaseMethodFault) {
   324  	t := s.findTag(tag)
   325  	if t == nil {
   326  		return nil, new(vim.NotFound)
   327  	}
   328  	var ids []vim.ManagedObjectReference
   329  	for id := range s.Association[t.ID] {
   330  		ids = append(
   331  			ids,
   332  			vim.ManagedObjectReference{
   333  				Type:  id.Type,
   334  				Value: id.Value,
   335  			})
   336  	}
   337  	return ids, nil
   338  }
   339  
   340  // AttachedTags is meant for internal use via simulator.Registry.tagManager
   341  func (s *handler) AttachedTags(ref vim.ManagedObjectReference) ([]vim.VslmTagEntry, vim.BaseMethodFault) {
   342  	oid := internal.AssociatedObject{
   343  		Type:  ref.Type,
   344  		Value: ref.Value,
   345  	}
   346  	var tags []vim.VslmTagEntry
   347  	for id, objs := range s.Association {
   348  		if objs[oid] {
   349  			tag := s.Tag[id]
   350  			cat := s.Category[tag.CategoryID]
   351  			tags = append(tags, vim.VslmTagEntry{
   352  				TagName:            tag.Name,
   353  				ParentCategoryName: cat.Name,
   354  			})
   355  		}
   356  	}
   357  	return tags, nil
   358  }
   359  
   360  // AttachTag is meant for internal use via simulator.Registry.tagManager
   361  func (s *handler) AttachTag(ref vim.ManagedObjectReference, tag vim.VslmTagEntry) vim.BaseMethodFault {
   362  	t := s.findTag(tag)
   363  	if t == nil {
   364  		return new(vim.NotFound)
   365  	}
   366  	s.Association[t.ID][internal.AssociatedObject{
   367  		Type:  ref.Type,
   368  		Value: ref.Value,
   369  	}] = true
   370  	return nil
   371  }
   372  
   373  // DetachTag is meant for internal use via simulator.Registry.tagManager
   374  func (s *handler) DetachTag(id vim.ManagedObjectReference, tag vim.VslmTagEntry) vim.BaseMethodFault {
   375  	t := s.findTag(tag)
   376  	if t == nil {
   377  		return new(vim.NotFound)
   378  	}
   379  	delete(s.Association[t.ID], internal.AssociatedObject{
   380  		Type:  id.Type,
   381  		Value: id.Value,
   382  	})
   383  	return nil
   384  }
   385  
   386  // StatusOK responds with http.StatusOK and encodes val, if specified, to JSON
   387  // For use with "/api" endpoints.
   388  func StatusOK(w http.ResponseWriter, val ...any) {
   389  	w.Header().Set("Content-Type", "application/json")
   390  	w.WriteHeader(http.StatusOK)
   391  	if len(val) == 0 {
   392  		return
   393  	}
   394  
   395  	err := json.NewEncoder(w).Encode(val[0])
   396  
   397  	if err != nil {
   398  		log.Panic(err)
   399  	}
   400  }
   401  
   402  // OK responds with http.StatusOK and encodes val, if specified, to JSON
   403  // For use with "/rest" endpoints where the response is a "value" wrapped structure.
   404  func OK(w http.ResponseWriter, val ...any) {
   405  	if len(val) == 0 {
   406  		w.WriteHeader(http.StatusOK)
   407  		return
   408  	}
   409  
   410  	s := struct {
   411  		Value any `json:"value,omitempty"`
   412  	}{
   413  		val[0],
   414  	}
   415  
   416  	StatusOK(w, s)
   417  }
   418  
   419  // BadRequest responds with http.StatusBadRequest and json encoded vAPI error of type kind.
   420  // For use with "/rest" endpoints where the response is a "value" wrapped structure.
   421  func BadRequest(w http.ResponseWriter, kind string) {
   422  	w.WriteHeader(http.StatusBadRequest)
   423  
   424  	err := json.NewEncoder(w).Encode(struct {
   425  		Type  string `json:"type"`
   426  		Value struct {
   427  			Messages []string `json:"messages,omitempty"`
   428  		} `json:"value,omitempty"`
   429  	}{
   430  		Type: kind,
   431  	})
   432  
   433  	if err != nil {
   434  		log.Panic(err)
   435  	}
   436  }
   437  
   438  // ApiErrorAlreadyExists responds with a REST error of type "ALREADY_EXISTS".
   439  // For use with "/api" endpoints.
   440  func ApiErrorAlreadyExists(w http.ResponseWriter) {
   441  	apiError(w, http.StatusBadRequest, "ALREADY_EXISTS")
   442  }
   443  
   444  // ApiErrorGeneral responds with a REST error of type "ERROR".
   445  // For use with "/api" endpoints.
   446  func ApiErrorGeneral(w http.ResponseWriter) {
   447  	apiError(w, http.StatusInternalServerError, "ERROR")
   448  }
   449  
   450  // ApiErrorInvalidArgument responds with a REST error of type "INVALID_ARGUMENT".
   451  // For use with "/api" endpoints.
   452  func ApiErrorInvalidArgument(w http.ResponseWriter) {
   453  	apiError(w, http.StatusBadRequest, "INVALID_ARGUMENT")
   454  }
   455  
   456  // ApiErrorNotAllowedInCurrentState responds with a REST error of type "NOT_ALLOWED_IN_CURRENT_STATE".
   457  // For use with "/api" endpoints.
   458  func ApiErrorNotAllowedInCurrentState(w http.ResponseWriter) {
   459  	apiError(w, http.StatusBadRequest, "NOT_ALLOWED_IN_CURRENT_STATE")
   460  }
   461  
   462  // ApiErrorNotFound responds with a REST error of type "NOT_FOUND".
   463  // For use with "/api" endpoints.
   464  func ApiErrorNotFound(w http.ResponseWriter) {
   465  	apiError(w, http.StatusNotFound, "NOT_FOUND")
   466  }
   467  
   468  // ApiErrorResourceInUse responds with a REST error of type "RESOURCE_IN_USE".
   469  // For use with "/api" endpoints.
   470  func ApiErrorResourceInUse(w http.ResponseWriter) {
   471  	apiError(w, http.StatusBadRequest, "RESOURCE_IN_USE")
   472  }
   473  
   474  // ApiErrorUnauthorized responds with a REST error of type "UNAUTHORIZED".
   475  // For use with "/api" endpoints.
   476  func ApiErrorUnauthorized(w http.ResponseWriter) {
   477  	apiError(w, http.StatusBadRequest, "UNAUTHORIZED")
   478  }
   479  
   480  // ApiErrorUnsupported responds with a REST error of type "UNSUPPORTED".
   481  // For use with "/api" endpoints.
   482  func ApiErrorUnsupported(w http.ResponseWriter) {
   483  	apiError(w, http.StatusBadRequest, "UNSUPPORTED")
   484  }
   485  
   486  func apiError(w http.ResponseWriter, statusCode int, errorType string) {
   487  	w.Header().Set("Content-Type", "application/json")
   488  	w.WriteHeader(statusCode)
   489  	w.Write([]byte(fmt.Sprintf(`{"error_type":"%s", "messages":[]}`, errorType)))
   490  }
   491  
   492  func (*handler) error(w http.ResponseWriter, err error) {
   493  	http.Error(w, err.Error(), http.StatusInternalServerError)
   494  	log.Print(err)
   495  }
   496  
   497  // ServeHTTP handles vAPI requests.
   498  func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   499  	switch r.Method {
   500  	case http.MethodPost, http.MethodDelete, http.MethodGet, http.MethodPatch, http.MethodPut:
   501  	default:
   502  		w.WriteHeader(http.StatusMethodNotAllowed)
   503  		return
   504  	}
   505  
   506  	// Use ServeHTTP directly and not via handler otherwise the path values like "{id}" are not set
   507  	s.ServeMux.ServeHTTP(w, r)
   508  }
   509  
   510  func (s *handler) decode(r *http.Request, w http.ResponseWriter, val any) bool {
   511  	return Decode(r, w, val)
   512  }
   513  
   514  // Decode the request Body into val.
   515  // Returns true on success, otherwise false and sends the http.StatusBadRequest response.
   516  func Decode(r *http.Request, w http.ResponseWriter, val any) bool {
   517  	defer r.Body.Close()
   518  	err := json.NewDecoder(r.Body).Decode(val)
   519  	if err != nil {
   520  		log.Printf("%s %s: %s", r.Method, r.RequestURI, err)
   521  		w.WriteHeader(http.StatusBadRequest)
   522  		return false
   523  	}
   524  	return true
   525  }
   526  
   527  func (s *handler) expiredSession(id string, now time.Time, timeout time.Duration) bool {
   528  	expired := true
   529  	s.Lock()
   530  	session, ok := s.Session[id]
   531  	if ok {
   532  		expired = now.Sub(session.LastAccessed) > timeout
   533  		if expired {
   534  			delete(s.Session, id)
   535  		}
   536  	}
   537  	s.Unlock()
   538  	return expired
   539  }
   540  
   541  func (s *handler) newContext() *simulator.Context {
   542  	return &simulator.Context{
   543  		Context: context.Background(),
   544  		Map:     s.Map,
   545  	}
   546  }
   547  
   548  func (s *handler) session(w http.ResponseWriter, r *http.Request) {
   549  	id := r.Header.Get(internal.SessionCookieName)
   550  	useHeaderAuthn := strings.ToLower(r.Header.Get(internal.UseHeaderAuthn))
   551  
   552  	switch r.Method {
   553  	case http.MethodPost:
   554  		if s.action(r) != "" {
   555  			if session, ok := s.Session[id]; ok {
   556  				OK(w, session)
   557  			} else {
   558  				w.WriteHeader(http.StatusUnauthorized)
   559  			}
   560  			return
   561  		}
   562  		user, ok := s.hasAuthorization(r)
   563  		if !ok {
   564  			w.WriteHeader(http.StatusUnauthorized)
   565  			return
   566  		}
   567  		id = uuid.New().String()
   568  		now := time.Now()
   569  		s.Session[id] = &rest.Session{User: user, Created: now, LastAccessed: now}
   570  		simulator.SessionIdleWatch(s.newContext(), id, s.expiredSession)
   571  		if useHeaderAuthn != "true" {
   572  			http.SetCookie(w, &http.Cookie{
   573  				Name:  internal.SessionCookieName,
   574  				Value: id,
   575  				Path:  rest.Path,
   576  			})
   577  		}
   578  		OK(w, id)
   579  	case http.MethodDelete:
   580  		delete(s.Session, id)
   581  		OK(w)
   582  	case http.MethodGet:
   583  		OK(w, s.Session[id])
   584  	}
   585  }
   586  
   587  // just enough json-rpc to support Supervisor upgrade testing
   588  func (s *handler) jsonRPC(w http.ResponseWriter, r *http.Request) {
   589  	if r.Method != http.MethodPost {
   590  		w.WriteHeader(http.StatusMethodNotAllowed)
   591  		return
   592  	}
   593  
   594  	var rpc, out map[string]any
   595  
   596  	if Decode(r, w, &rpc) {
   597  		params := rpc["params"].(map[string]any)
   598  
   599  		switch params["serviceId"] {
   600  		case "com.vmware.cis.session":
   601  			switch params["operationId"] {
   602  			case "create":
   603  				id := uuid.New().String()
   604  				now := time.Now()
   605  				s.Session[id] = &rest.Session{User: id, Created: now, LastAccessed: now}
   606  				out = map[string]any{"SECRET": id}
   607  			case "delete":
   608  			}
   609  		}
   610  
   611  		res := map[string]any{
   612  			"jsonrpc": rpc["jsonrpc"],
   613  			"id":      rpc["id"],
   614  			"result": map[string]any{
   615  				"output": out,
   616  			},
   617  		}
   618  
   619  		StatusOK(w, res)
   620  	}
   621  }
   622  
   623  func (s *handler) certificateAuthority(w http.ResponseWriter, r *http.Request) {
   624  	signer := s.Map.SessionManager().TLS().Certificates[0]
   625  
   626  	switch path.Base(r.URL.Path) {
   627  	case "get-root":
   628  		var encoded bytes.Buffer
   629  		_ = pem.Encode(&encoded, &pem.Block{Type: "CERTIFICATE", Bytes: signer.Leaf.Raw})
   630  		OK(w, encoded.String())
   631  	case "sign-cert":
   632  		if r.Method != http.MethodPost {
   633  			w.WriteHeader(http.StatusMethodNotAllowed)
   634  			return
   635  		}
   636  
   637  		var req struct {
   638  			Duration string `json:"duration"`
   639  			CSR      string `json:"csr"`
   640  		}
   641  
   642  		if Decode(r, w, &req) {
   643  			block, _ := pem.Decode([]byte(req.CSR))
   644  			csr, err := x509.ParseCertificateRequest(block.Bytes)
   645  			if err != nil {
   646  				BadRequest(w, err.Error())
   647  				return
   648  			}
   649  			duration, err := strconv.ParseInt(req.Duration, 10, 64)
   650  			if err != nil {
   651  				BadRequest(w, err.Error())
   652  				return
   653  			}
   654  
   655  			serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
   656  			now := time.Now()
   657  			cert := &x509.Certificate{
   658  				SerialNumber:   serialNumber,
   659  				Subject:        csr.Subject,
   660  				DNSNames:       csr.DNSNames,
   661  				IPAddresses:    csr.IPAddresses,
   662  				NotBefore:      now,
   663  				NotAfter:       now.Add(time.Hour * 24 * time.Duration(duration)),
   664  				AuthorityKeyId: signer.Leaf.SubjectKeyId,
   665  			}
   666  
   667  			der, err := x509.CreateCertificate(rand.Reader, cert, signer.Leaf, csr.PublicKey, signer.PrivateKey)
   668  			if err != nil {
   669  				BadRequest(w, err.Error())
   670  				return
   671  			}
   672  
   673  			var encoded bytes.Buffer
   674  			err = pem.Encode(&encoded, &pem.Block{Type: "CERTIFICATE", Bytes: der})
   675  			if err != nil {
   676  				BadRequest(w, err.Error())
   677  				return
   678  			}
   679  
   680  			OK(w, encoded.String())
   681  		}
   682  	default:
   683  		http.NotFound(w, r)
   684  	}
   685  }
   686  
   687  func (s *handler) action(r *http.Request) string {
   688  	return r.URL.Query().Get("~action")
   689  }
   690  
   691  func (s *handler) id(r *http.Request) string {
   692  	base := path.Base(r.URL.Path)
   693  	id := strings.TrimPrefix(base, "id:")
   694  	if id == base {
   695  		return "" // trigger 404 Not Found w/o id: prefix
   696  	}
   697  	return id
   698  }
   699  
   700  func newID(kind string) string {
   701  	return fmt.Sprintf("urn:vmomi:InventoryService%s:%s:GLOBAL", kind, uuid.New().String())
   702  }
   703  
   704  func (s *handler) category(w http.ResponseWriter, r *http.Request) {
   705  	switch r.Method {
   706  	case http.MethodPost:
   707  		var spec struct {
   708  			Category tags.Category `json:"create_spec"`
   709  		}
   710  		if s.decode(r, w, &spec) {
   711  			for _, category := range s.Category {
   712  				if category.Name == spec.Category.Name {
   713  					BadRequest(w, "com.vmware.vapi.std.errors.already_exists")
   714  					return
   715  				}
   716  			}
   717  			id := spec.Category.CategoryID
   718  			if id == "" {
   719  				id = newID("Category")
   720  			} else if !strings.HasPrefix(id, "urn:vmomi:InventoryServiceCategory:") {
   721  				BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
   722  				return
   723  			}
   724  			spec.Category.ID = id
   725  			s.Category[id] = &spec.Category
   726  			OK(w, id)
   727  		}
   728  	case http.MethodGet:
   729  		var ids []string
   730  		for id := range s.Category {
   731  			ids = append(ids, id)
   732  		}
   733  
   734  		OK(w, ids)
   735  	}
   736  }
   737  
   738  func (s *handler) categoryID(w http.ResponseWriter, r *http.Request) {
   739  	id := s.id(r)
   740  
   741  	o, ok := s.Category[id]
   742  	if !ok {
   743  		http.NotFound(w, r)
   744  		return
   745  	}
   746  
   747  	switch r.Method {
   748  	case http.MethodDelete:
   749  		delete(s.Category, id)
   750  		for ix, tag := range s.Tag {
   751  			if tag.CategoryID == id {
   752  				delete(s.Tag, ix)
   753  				delete(s.Association, ix)
   754  			}
   755  		}
   756  		OK(w)
   757  	case http.MethodPatch:
   758  		var spec struct {
   759  			Category tags.Category `json:"update_spec"`
   760  		}
   761  		if s.decode(r, w, &spec) {
   762  			ntypes := len(spec.Category.AssociableTypes)
   763  			if ntypes != 0 {
   764  				// Validate that AssociableTypes is only appended to.
   765  				etypes := len(o.AssociableTypes)
   766  				fail := ntypes < etypes
   767  				if !fail {
   768  					fail = !reflect.DeepEqual(o.AssociableTypes, spec.Category.AssociableTypes[:etypes])
   769  				}
   770  				if fail {
   771  					BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
   772  					return
   773  				}
   774  			}
   775  			o.Patch(&spec.Category)
   776  			OK(w)
   777  		}
   778  	case http.MethodGet:
   779  		OK(w, o)
   780  	}
   781  }
   782  
   783  func (s *handler) tag(w http.ResponseWriter, r *http.Request) {
   784  	switch r.Method {
   785  	case http.MethodPost:
   786  		var spec struct {
   787  			Tag tags.Tag `json:"create_spec"`
   788  		}
   789  		if s.decode(r, w, &spec) {
   790  			for _, tag := range s.Tag {
   791  				if tag.Name == spec.Tag.Name && tag.CategoryID == spec.Tag.CategoryID {
   792  					BadRequest(w, "com.vmware.vapi.std.errors.already_exists")
   793  					return
   794  				}
   795  			}
   796  			id := spec.Tag.TagID
   797  			if id == "" {
   798  				id = newID("Tag")
   799  			} else if !strings.HasPrefix(id, "urn:vmomi:InventoryServiceTag:") {
   800  				BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
   801  				return
   802  			}
   803  			spec.Tag.ID = id
   804  			s.Tag[id] = &spec.Tag
   805  			s.Association[id] = make(map[internal.AssociatedObject]bool)
   806  			OK(w, id)
   807  		}
   808  	case http.MethodGet:
   809  		var ids []string
   810  		for id := range s.Tag {
   811  			ids = append(ids, id)
   812  		}
   813  		OK(w, ids)
   814  	}
   815  }
   816  
   817  func (s *handler) tagID(w http.ResponseWriter, r *http.Request) {
   818  	id := s.id(r)
   819  
   820  	switch s.action(r) {
   821  	case "list-tags-for-category":
   822  		var ids []string
   823  		for _, tag := range s.Tag {
   824  			if tag.CategoryID == id {
   825  				ids = append(ids, tag.ID)
   826  			}
   827  		}
   828  		OK(w, ids)
   829  		return
   830  	}
   831  
   832  	o, ok := s.Tag[id]
   833  	if !ok {
   834  		log.Printf("tag not found: %s", id)
   835  		http.NotFound(w, r)
   836  		return
   837  	}
   838  
   839  	switch r.Method {
   840  	case http.MethodDelete:
   841  		delete(s.Tag, id)
   842  		delete(s.Association, id)
   843  		OK(w)
   844  	case http.MethodPatch:
   845  		var spec struct {
   846  			Tag tags.Tag `json:"update_spec"`
   847  		}
   848  		if s.decode(r, w, &spec) {
   849  			o.Patch(&spec.Tag)
   850  			OK(w)
   851  		}
   852  	case http.MethodGet:
   853  		OK(w, o)
   854  	}
   855  }
   856  
   857  // TODO: support cardinality checks
   858  func (s *handler) association(w http.ResponseWriter, r *http.Request) {
   859  	if r.Method != http.MethodPost {
   860  		w.WriteHeader(http.StatusMethodNotAllowed)
   861  		return
   862  	}
   863  
   864  	var spec struct {
   865  		internal.Association
   866  		TagIDs    []string                    `json:"tag_ids,omitempty"`
   867  		ObjectIDs []internal.AssociatedObject `json:"object_ids,omitempty"`
   868  	}
   869  	if !s.decode(r, w, &spec) {
   870  		return
   871  	}
   872  
   873  	switch s.action(r) {
   874  	case "list-attached-tags":
   875  		var ids []string
   876  		for id, objs := range s.Association {
   877  			if objs[*spec.ObjectID] {
   878  				ids = append(ids, id)
   879  			}
   880  		}
   881  		OK(w, ids)
   882  
   883  	case "list-attached-objects-on-tags":
   884  		var res []tags.AttachedObjects
   885  		for _, id := range spec.TagIDs {
   886  			o := tags.AttachedObjects{TagID: id}
   887  			for i := range s.Association[id] {
   888  				o.ObjectIDs = append(o.ObjectIDs, i)
   889  			}
   890  			res = append(res, o)
   891  		}
   892  		OK(w, res)
   893  
   894  	case "list-attached-tags-on-objects":
   895  		var res []tags.AttachedTags
   896  		for _, ref := range spec.ObjectIDs {
   897  			o := tags.AttachedTags{ObjectID: ref}
   898  			for id, objs := range s.Association {
   899  				if objs[ref] {
   900  					o.TagIDs = append(o.TagIDs, id)
   901  				}
   902  			}
   903  			res = append(res, o)
   904  		}
   905  		OK(w, res)
   906  
   907  	case "attach-multiple-tags-to-object":
   908  		// TODO: add check if target (moref) exist or return 403 as per API behavior
   909  
   910  		res := struct {
   911  			Success bool             `json:"success"`
   912  			Errors  tags.BatchErrors `json:"error_messages,omitempty"`
   913  		}{}
   914  
   915  		for _, id := range spec.TagIDs {
   916  			if _, exists := s.Association[id]; !exists {
   917  				log.Printf("association tag not found: %s", id)
   918  				res.Errors = append(res.Errors, tags.BatchError{
   919  					Type:    "cis.tagging.objectNotFound.error",
   920  					Message: fmt.Sprintf("Tagging object %s not found", id),
   921  				})
   922  			} else {
   923  				s.Association[id][*spec.ObjectID] = true
   924  			}
   925  		}
   926  
   927  		if len(res.Errors) == 0 {
   928  			res.Success = true
   929  		}
   930  		OK(w, res)
   931  
   932  	case "detach-multiple-tags-from-object":
   933  		// TODO: add check if target (moref) exist or return 403 as per API behavior
   934  
   935  		res := struct {
   936  			Success bool             `json:"success"`
   937  			Errors  tags.BatchErrors `json:"error_messages,omitempty"`
   938  		}{}
   939  
   940  		for _, id := range spec.TagIDs {
   941  			if _, exists := s.Association[id]; !exists {
   942  				log.Printf("association tag not found: %s", id)
   943  				res.Errors = append(res.Errors, tags.BatchError{
   944  					Type:    "cis.tagging.objectNotFound.error",
   945  					Message: fmt.Sprintf("Tagging object %s not found", id),
   946  				})
   947  			} else {
   948  				s.Association[id][*spec.ObjectID] = false
   949  			}
   950  		}
   951  
   952  		if len(res.Errors) == 0 {
   953  			res.Success = true
   954  		}
   955  		OK(w, res)
   956  	}
   957  }
   958  
   959  func (s *handler) associationID(w http.ResponseWriter, r *http.Request) {
   960  	if r.Method != http.MethodPost {
   961  		w.WriteHeader(http.StatusMethodNotAllowed)
   962  		return
   963  	}
   964  
   965  	id := s.id(r)
   966  	if _, exists := s.Association[id]; !exists {
   967  		log.Printf("association tag not found: %s", id)
   968  		http.NotFound(w, r)
   969  		return
   970  	}
   971  
   972  	var spec internal.Association
   973  	var specs struct {
   974  		ObjectIDs []internal.AssociatedObject `json:"object_ids"`
   975  	}
   976  	switch s.action(r) {
   977  	case "attach", "detach", "list-attached-objects":
   978  		if !s.decode(r, w, &spec) {
   979  			return
   980  		}
   981  	case "attach-tag-to-multiple-objects":
   982  		if !s.decode(r, w, &specs) {
   983  			return
   984  		}
   985  	}
   986  
   987  	switch s.action(r) {
   988  	case "attach":
   989  		s.Association[id][*spec.ObjectID] = true
   990  		OK(w)
   991  	case "detach":
   992  		delete(s.Association[id], *spec.ObjectID)
   993  		OK(w)
   994  	case "list-attached-objects":
   995  		var ids []internal.AssociatedObject
   996  		for id := range s.Association[id] {
   997  			ids = append(ids, id)
   998  		}
   999  		OK(w, ids)
  1000  	case "attach-tag-to-multiple-objects":
  1001  		for _, obj := range specs.ObjectIDs {
  1002  			s.Association[id][obj] = true
  1003  		}
  1004  		OK(w)
  1005  	}
  1006  }
  1007  
  1008  func (s *handler) library(w http.ResponseWriter, r *http.Request) {
  1009  	switch r.Method {
  1010  	case http.MethodPost:
  1011  		var spec struct {
  1012  			Library library.Library `json:"create_spec"`
  1013  			Find    library.Find    `json:"spec"`
  1014  		}
  1015  		if !s.decode(r, w, &spec) {
  1016  			return
  1017  		}
  1018  
  1019  		switch s.action(r) {
  1020  		case "find":
  1021  			var ids []string
  1022  			for _, l := range s.Library {
  1023  				if spec.Find.Type != "" {
  1024  					if spec.Find.Type != l.Library.Type {
  1025  						continue
  1026  					}
  1027  				}
  1028  				if spec.Find.Name != "" {
  1029  					if !strings.EqualFold(l.Library.Name, spec.Find.Name) {
  1030  						continue
  1031  					}
  1032  				}
  1033  				ids = append(ids, l.ID)
  1034  			}
  1035  			OK(w, ids)
  1036  		case "":
  1037  			if !s.isValidSecurityPolicy(spec.Library.SecurityPolicyID) {
  1038  				http.NotFound(w, r)
  1039  				return
  1040  			}
  1041  
  1042  			id := uuid.New().String()
  1043  			spec.Library.ID = id
  1044  			spec.Library.ServerGUID = uuid.New().String()
  1045  			spec.Library.CreationTime = types.NewTime(time.Now())
  1046  			spec.Library.LastModifiedTime = types.NewTime(time.Now())
  1047  			spec.Library.UnsetSecurityPolicyID = spec.Library.SecurityPolicyID == ""
  1048  			dir := s.libraryPath(&spec.Library, "")
  1049  			if err := os.Mkdir(dir, 0750); err != nil {
  1050  				s.error(w, err)
  1051  				return
  1052  			}
  1053  			s.Library[id] = &content{
  1054  				Library: &spec.Library,
  1055  				Item:    make(map[string]*item),
  1056  				Subs:    make(map[string]*library.Subscriber),
  1057  				VMTX:    make(map[string]*types.ManagedObjectReference),
  1058  			}
  1059  
  1060  			pub := spec.Library.Publication
  1061  			if pub != nil && pub.Published != nil && *pub.Published {
  1062  				// Generate PublishURL as real vCenter does
  1063  				pub.PublishURL = (&url.URL{
  1064  					Scheme: s.URL.Scheme,
  1065  					Host:   s.URL.Host,
  1066  					Path:   "/cls/vcsp/lib/" + id,
  1067  				}).String()
  1068  			}
  1069  
  1070  			s.syncSubLib(s.Library[id])
  1071  
  1072  			spec.Library.StateInfo = &library.StateInfo{State: "ACTIVE"}
  1073  
  1074  			OK(w, id)
  1075  		}
  1076  	case http.MethodGet:
  1077  		var ids []string
  1078  		for id := range s.Library {
  1079  			ids = append(ids, id)
  1080  		}
  1081  		OK(w, ids)
  1082  	}
  1083  }
  1084  
  1085  func (s *handler) syncSubLib(dstLib *content) error {
  1086  
  1087  	sub := dstLib.Subscription
  1088  	if sub == nil {
  1089  		return nil
  1090  	}
  1091  
  1092  	lastSyncTime := time.Now().UTC()
  1093  	dstLib.LastSyncTime = &lastSyncTime
  1094  
  1095  	var syncAll bool
  1096  	if sub.OnDemand != nil && !*sub.OnDemand {
  1097  		syncAll = true
  1098  	}
  1099  
  1100  	srcLib, ok := s.Library[path.Base(sub.SubscriptionURL)]
  1101  	if !ok {
  1102  		return nil
  1103  	}
  1104  
  1105  	if dstLib.Item == nil {
  1106  		dstLib.Item = map[string]*item{}
  1107  	}
  1108  
  1109  	// handledSrcItems tracks which items from the source library have been
  1110  	// seen when iterating over the existing, subscribed library. This enables
  1111  	// the addition of *new* items from the source library that do not yet exist
  1112  	// in the subscribed, destination library.
  1113  	handledSrcItems := map[string]struct{}{}
  1114  
  1115  	// Update any items that already exist in the subscribed library.
  1116  	for _, dstItem := range dstLib.Item {
  1117  
  1118  		// Indicate this source item has been seen.
  1119  		handledSrcItems[dstItem.SourceID] = struct{}{}
  1120  
  1121  		// Synchronize the item.
  1122  		if err := s.syncItem(
  1123  			dstItem,
  1124  			dstLib,
  1125  			srcLib,
  1126  			syncAll,
  1127  			srcLib.LastSyncTime); err != nil {
  1128  
  1129  			return err
  1130  		}
  1131  	}
  1132  
  1133  	// Add any new items from the published library.
  1134  	for _, srcItem := range srcLib.Item {
  1135  
  1136  		// Skip any source items that were handled above.
  1137  		if _, ok := handledSrcItems[srcItem.ID]; ok {
  1138  			continue
  1139  		}
  1140  
  1141  		now := time.Now().UTC()
  1142  
  1143  		// Create the destination item.
  1144  		dstItem := &item{
  1145  			Item: &library.Item{
  1146  				// Give the copy a unique ID.
  1147  				ID: uuid.NewString(),
  1148  
  1149  				// Track the source item's ID.
  1150  				SourceID: srcItem.ID,
  1151  
  1152  				// Track the library to which the new item belongs.
  1153  				LibraryID: dstLib.ID,
  1154  
  1155  				// Ensure the creation/modified times are set.
  1156  				CreationTime:     &now,
  1157  				LastModifiedTime: &now,
  1158  			},
  1159  		}
  1160  
  1161  		// Add the new item to the subscribed library.
  1162  		dstLib.Item[dstItem.ID] = dstItem
  1163  
  1164  		// Synchronize the item.
  1165  		if err := s.syncItem(
  1166  			dstItem,
  1167  			dstLib,
  1168  			srcLib,
  1169  			syncAll,
  1170  			dstLib.LastSyncTime); err != nil {
  1171  
  1172  			return err
  1173  		}
  1174  	}
  1175  
  1176  	return nil
  1177  }
  1178  
  1179  func (s *handler) evictLibrary(lib *content) {
  1180  	for i := range lib.Item {
  1181  		s.evictItem(lib.Item[i])
  1182  	}
  1183  }
  1184  
  1185  func (s *handler) evictItem(item *item) {
  1186  	item.Cached = false
  1187  	for i := range item.File {
  1188  		item.File[i].Cached = &item.Cached
  1189  	}
  1190  }
  1191  
  1192  var ovfOrManifestRx = regexp.MustCompile(`(?i)^.+\.(ovf|mf)$`)
  1193  
  1194  func (s *handler) syncItem(
  1195  	dstItem *item,
  1196  	dstLib,
  1197  	srcLib *content,
  1198  	syncAll bool,
  1199  	lastSyncTime *time.Time) error {
  1200  
  1201  	// dstLib is nil when this function is called by the workflow for deploying
  1202  	// a subscribed library item.
  1203  	if dstLib == nil {
  1204  		var ok bool
  1205  		if dstLib, ok = s.Library[dstItem.LibraryID]; !ok {
  1206  			return fmt.Errorf("cannot find sub library id %q", dstItem.LibraryID)
  1207  		}
  1208  	}
  1209  
  1210  	// srcLib is nil when this function is used to synchronize an individual
  1211  	// item versus synchronizing the entire library.
  1212  	if srcLib == nil {
  1213  		sub := dstLib.Subscription
  1214  		if sub == nil {
  1215  			return nil
  1216  		}
  1217  		var ok bool
  1218  		srcLibID := path.Base(sub.SubscriptionURL)
  1219  		if srcLib, ok = s.Library[srcLibID]; !ok {
  1220  			return fmt.Errorf("cannot find pub library id %q", srcLibID)
  1221  		}
  1222  	}
  1223  
  1224  	// Get the path to the destination library item on the local filesystem.
  1225  	dstItemPath := s.libraryPath(dstLib.Library, dstItem.ID)
  1226  
  1227  	// Get the source item.
  1228  	srcItem, ok := srcLib.Item[dstItem.SourceID]
  1229  	if !ok {
  1230  		// The source item is no more, so delete the destination item.
  1231  		delete(dstLib.Item, dstItem.ID)
  1232  
  1233  		// Clean up the destination item's files as well.
  1234  		os.RemoveAll(dstItemPath)
  1235  
  1236  		return nil
  1237  	}
  1238  
  1239  	// lastSyncTime is nil when this function is used to synchronize an
  1240  	// individual item versus synchronizing the entire library.
  1241  	if lastSyncTime == nil {
  1242  		now := time.Now().UTC()
  1243  		lastSyncTime = &now
  1244  	}
  1245  	dstItem.LastSyncTime = lastSyncTime
  1246  
  1247  	// There is nothing to sync if the metadata and content versions have not
  1248  	// changed, the item is already cached, and syncAll is false.
  1249  	if dstItem.MetadataVersion == srcItem.MetadataVersion &&
  1250  		dstItem.ContentVersion == srcItem.ContentVersion &&
  1251  		dstItem.Cached && !syncAll {
  1252  
  1253  		return nil
  1254  	}
  1255  
  1256  	// Since there was a modification, update the last mod time.
  1257  	dstItem.LastModifiedTime = lastSyncTime
  1258  
  1259  	// Copy information from the srcItem to dstItem.
  1260  	dstItem.Name = srcItem.Name
  1261  	dstItem.ContentVersion = srcItem.ContentVersion
  1262  	dstItem.MetadataVersion = srcItem.MetadataVersion
  1263  	dstItem.Type = srcItem.Type
  1264  	dstItem.Description = srcItem.Description
  1265  	dstItem.Version = srcItem.Version
  1266  
  1267  	// Update the destination item's files from the source.
  1268  	dstItem.File = make([]library.File, len(srcItem.File))
  1269  	copy(dstItem.File, srcItem.File)
  1270  
  1271  	// If the destination item was previously cached or syncAll was used, then
  1272  	// mark the destination item as cached.
  1273  	dstItem.Cached = dstItem.Cached || syncAll
  1274  	fileIsCached := true
  1275  	fileIsNotCached := false
  1276  	fileZeroSize := int64(0)
  1277  
  1278  	// Ensure a directory exists on the local filesystem for the destination
  1279  	// item.
  1280  	if err := os.MkdirAll(dstItemPath, 0750); err != nil {
  1281  		return fmt.Errorf(
  1282  			"failed to make directory for library %q item %q: %w",
  1283  			dstLib.ID,
  1284  			dstItem.ID,
  1285  			err)
  1286  	}
  1287  
  1288  	// Update the the destination item's files.
  1289  	srcItemPath := s.libraryPath(srcLib.Library, srcItem.ID)
  1290  	for i := range dstItem.File {
  1291  		var (
  1292  			dstFile = &dstItem.File[i]
  1293  			srcFile = srcItem.File[i]
  1294  		)
  1295  
  1296  		if !isValidFileName(dstFile.Name) || !isValidFileName(srcFile.Name) {
  1297  			return errors.New("invalid file name")
  1298  		}
  1299  
  1300  		var (
  1301  			dstFilePath = path.Join(dstItemPath, dstFile.Name)
  1302  			srcFilePath = path.Join(srcItemPath, srcFile.Name)
  1303  		)
  1304  
  1305  		// .ovf and .mf files are always cached.
  1306  		if ovfOrManifestRx.MatchString(dstFile.Name) {
  1307  			dstFile.Cached = &fileIsCached
  1308  			if err := copyFile(dstFilePath, srcFilePath); err != nil {
  1309  				return err
  1310  			}
  1311  			continue
  1312  		}
  1313  
  1314  		// For other file types, the behavior depends on syncAll:
  1315  		//
  1316  		// - false -- Create the destination file as a placeholder but do not
  1317  		//            mark it as cached.
  1318  		// - true  -- Copy the source file to the destination and mark it as
  1319  		//            cached.
  1320  		if !syncAll {
  1321  			if err := createFile(dstFilePath); err != nil {
  1322  				return err
  1323  			}
  1324  
  1325  			// Ensure the empty file does not indicate it is cached and does not
  1326  			// report a size.
  1327  			dstFile.Cached = &fileIsNotCached
  1328  			dstFile.Size = &fileZeroSize
  1329  		} else {
  1330  			if err := copyFile(dstFilePath, srcFilePath); err != nil {
  1331  				return err
  1332  			}
  1333  
  1334  			// Ensure the file reports that it is cached.
  1335  			dstFile.Cached = &fileIsCached
  1336  		}
  1337  	}
  1338  
  1339  	return nil
  1340  }
  1341  
  1342  const (
  1343  	createOrCopyFlags = os.O_RDWR | os.O_CREATE | os.O_TRUNC
  1344  	createOrCopyMode  = os.FileMode(0664)
  1345  )
  1346  
  1347  func createFile(dstPath string) error {
  1348  	f, err := os.OpenFile(dstPath, createOrCopyFlags, createOrCopyMode)
  1349  	if err != nil {
  1350  		return fmt.Errorf("failed to create %q: %w", dstPath, err)
  1351  	}
  1352  	return f.Close()
  1353  }
  1354  
  1355  // TODO: considering using object.DatastoreFileManager.Copy here instead
  1356  func openFile(src io.Reader, dstPath string, flag int, perm os.FileMode) (*os.File, error) {
  1357  	backing := simulator.VirtualDiskBackingFileName(dstPath)
  1358  	if backing == dstPath {
  1359  		// dstPath is not a .vmdk file
  1360  		return os.OpenFile(dstPath, flag, perm)
  1361  	}
  1362  
  1363  	var desc *vmdk.Descriptor
  1364  
  1365  	if _, ok := src.(*os.File); ok {
  1366  		// Local file copy
  1367  		var err error
  1368  		desc, err = vmdk.ParseDescriptor(src)
  1369  		if err != nil {
  1370  			return nil, err
  1371  		}
  1372  	} else {
  1373  		// Library import
  1374  		info, err := vmdk.Seek(src)
  1375  		if err != nil {
  1376  			return nil, err
  1377  		}
  1378  		desc = info.Descriptor
  1379  	}
  1380  
  1381  	desc.Extent[0].Info = filepath.Base(backing)
  1382  
  1383  	f, err := os.OpenFile(dstPath, flag, perm)
  1384  	if err != nil {
  1385  		return nil, err
  1386  	}
  1387  
  1388  	if err = desc.Write(f); err != nil {
  1389  		_ = f.Close()
  1390  		return nil, err
  1391  	}
  1392  
  1393  	if err = f.Close(); err != nil {
  1394  		return nil, err
  1395  	}
  1396  
  1397  	// Create ${name}-flat.vmdk to store contents
  1398  	return os.OpenFile(backing, flag, perm)
  1399  }
  1400  
  1401  func copyFile(dstPath, srcPath string) error {
  1402  	srcStat, err := os.Stat(srcPath)
  1403  	if err != nil {
  1404  		return fmt.Errorf("failed to stat %q: %w", srcPath, err)
  1405  	}
  1406  
  1407  	if !srcStat.Mode().IsRegular() {
  1408  		return fmt.Errorf("%q is not a regular file", srcPath)
  1409  	}
  1410  
  1411  	src, err := os.Open(srcPath)
  1412  	if err != nil {
  1413  		return fmt.Errorf("failed to open %q: %w", srcPath, err)
  1414  	}
  1415  	defer src.Close()
  1416  
  1417  	dst, err := openFile(src, dstPath, createOrCopyFlags, createOrCopyMode)
  1418  	if err != nil {
  1419  		return fmt.Errorf("failed to create %q: %w", dstPath, err)
  1420  	}
  1421  	defer dst.Close()
  1422  
  1423  	// Copy the file using a 1MiB buffer.
  1424  	if _, err = copyReaderToWriter(dst, dstPath, src, srcPath); err != nil {
  1425  		return err
  1426  	}
  1427  
  1428  	return nil
  1429  }
  1430  
  1431  // copyReaderToWriter copies the contents of src to dst using a 1MiB buffer.
  1432  func copyReaderToWriter(
  1433  	dst io.Writer, dstName string,
  1434  	src io.Reader, srcName string) (int64, error) {
  1435  
  1436  	buf := make([]byte, 1 /* byte */ *1024 /* kibibyte */ *1024 /* mebibyte */)
  1437  	n, err := io.CopyBuffer(dst, src, buf)
  1438  	if err != nil {
  1439  		return 0, fmt.Errorf("failed to copy %q to %q: %w", srcName, dstName, err)
  1440  	}
  1441  
  1442  	return n, nil
  1443  }
  1444  
  1445  func (s *handler) publish(w http.ResponseWriter, r *http.Request, sids []internal.SubscriptionDestination, l *content, vmtx *item) bool {
  1446  	var ids []string
  1447  	if len(sids) == 0 {
  1448  		for sid := range l.Subs {
  1449  			ids = append(ids, sid)
  1450  		}
  1451  	} else {
  1452  		for _, dst := range sids {
  1453  			ids = append(ids, dst.ID)
  1454  		}
  1455  	}
  1456  
  1457  	for _, sid := range ids {
  1458  		sub, ok := l.Subs[sid]
  1459  		if !ok {
  1460  			log.Printf("library subscription not found: %s", sid)
  1461  			http.NotFound(w, r)
  1462  			return false
  1463  		}
  1464  
  1465  		slib := s.Library[sub.LibraryID]
  1466  		if slib.VMTX[vmtx.ID] != nil {
  1467  			return true // already cloned
  1468  		}
  1469  
  1470  		ds := &vcenter.DiskStorage{Datastore: l.Library.Storage[0].DatastoreID}
  1471  		ref, err := s.cloneVM(vmtx.Template.Value, vmtx.Name, sub.Placement, ds)
  1472  		if err != nil {
  1473  			s.error(w, err)
  1474  			return false
  1475  		}
  1476  
  1477  		slib.VMTX[vmtx.ID] = ref
  1478  	}
  1479  
  1480  	return true
  1481  }
  1482  
  1483  func (s *handler) libraryID(w http.ResponseWriter, r *http.Request) {
  1484  	id := s.id(r)
  1485  	l, ok := s.Library[id]
  1486  	if !ok {
  1487  		log.Printf("library not found: %s", id)
  1488  		http.NotFound(w, r)
  1489  		return
  1490  	}
  1491  
  1492  	switch r.Method {
  1493  	case http.MethodDelete:
  1494  		p := s.libraryPath(l.Library, "")
  1495  		if err := os.RemoveAll(p); err != nil {
  1496  			s.error(w, err)
  1497  			return
  1498  		}
  1499  		for _, item := range l.Item {
  1500  			s.deleteVM(item.Template)
  1501  		}
  1502  		delete(s.Library, id)
  1503  		OK(w)
  1504  	case http.MethodPatch:
  1505  		var spec struct {
  1506  			Library library.Library `json:"update_spec"`
  1507  		}
  1508  		if s.decode(r, w, &spec) {
  1509  			l.Patch(&spec.Library)
  1510  			OK(w)
  1511  		}
  1512  	case http.MethodPost:
  1513  		switch s.action(r) {
  1514  		case "publish":
  1515  			var spec internal.SubscriptionDestinationSpec
  1516  			if !s.decode(r, w, &spec) {
  1517  				return
  1518  			}
  1519  			for _, item := range l.Item {
  1520  				if item.Type != library.ItemTypeVMTX {
  1521  					continue
  1522  				}
  1523  				if !s.publish(w, r, spec.Subscriptions, l, item) {
  1524  					return
  1525  				}
  1526  			}
  1527  			OK(w)
  1528  		case "sync":
  1529  			if l.Type == "SUBSCRIBED" {
  1530  				l.LastSyncTime = types.NewTime(time.Now())
  1531  				if err := s.syncSubLib(l); err != nil {
  1532  					BadRequest(w, err.Error())
  1533  				} else {
  1534  					OK(w)
  1535  				}
  1536  			} else {
  1537  				http.NotFound(w, r)
  1538  			}
  1539  		case "evict":
  1540  			s.evictLibrary(l)
  1541  			OK(w)
  1542  		}
  1543  	case http.MethodGet:
  1544  		OK(w, l)
  1545  	}
  1546  }
  1547  
  1548  func (s *handler) subscriptions(w http.ResponseWriter, r *http.Request) {
  1549  	if r.Method != http.MethodGet {
  1550  		w.WriteHeader(http.StatusMethodNotAllowed)
  1551  		return
  1552  	}
  1553  
  1554  	id := r.URL.Query().Get("library")
  1555  	l, ok := s.Library[id]
  1556  	if !ok {
  1557  		log.Printf("library not found: %s", id)
  1558  		http.NotFound(w, r)
  1559  		return
  1560  	}
  1561  
  1562  	var res []library.SubscriberSummary
  1563  	for sid, slib := range l.Subs {
  1564  		res = append(res, library.SubscriberSummary{
  1565  			LibraryID:              slib.LibraryID,
  1566  			LibraryName:            slib.LibraryName,
  1567  			SubscriptionID:         sid,
  1568  			LibraryVcenterHostname: "",
  1569  		})
  1570  	}
  1571  	OK(w, res)
  1572  }
  1573  
  1574  func (s *handler) subscriptionsID(w http.ResponseWriter, r *http.Request) {
  1575  	id := s.id(r)
  1576  	l, ok := s.Library[id]
  1577  	if !ok {
  1578  		log.Printf("library not found: %s", id)
  1579  		http.NotFound(w, r)
  1580  		return
  1581  	}
  1582  
  1583  	switch s.action(r) {
  1584  	case "get":
  1585  		var dst internal.SubscriptionDestination
  1586  		if !s.decode(r, w, &dst) {
  1587  			return
  1588  		}
  1589  
  1590  		sub, ok := l.Subs[dst.ID]
  1591  		if !ok {
  1592  			log.Printf("library subscription not found: %s", dst.ID)
  1593  			http.NotFound(w, r)
  1594  			return
  1595  		}
  1596  
  1597  		OK(w, sub)
  1598  	case "delete":
  1599  		var dst internal.SubscriptionDestination
  1600  		if !s.decode(r, w, &dst) {
  1601  			return
  1602  		}
  1603  
  1604  		delete(l.Subs, dst.ID)
  1605  
  1606  		OK(w)
  1607  	case "create", "":
  1608  		var spec struct {
  1609  			Sub struct {
  1610  				SubscriberLibrary library.SubscriberLibrary `json:"subscribed_library"`
  1611  			} `json:"spec"`
  1612  		}
  1613  		if !s.decode(r, w, &spec) {
  1614  			return
  1615  		}
  1616  
  1617  		sub := spec.Sub.SubscriberLibrary
  1618  		slib, ok := s.Library[sub.LibraryID]
  1619  		if !ok {
  1620  			log.Printf("library not found: %s", sub.LibraryID)
  1621  			http.NotFound(w, r)
  1622  			return
  1623  		}
  1624  
  1625  		id := uuid.New().String()
  1626  		l.Subs[id] = &library.Subscriber{
  1627  			LibraryID:       slib.ID,
  1628  			LibraryName:     slib.Name,
  1629  			LibraryLocation: sub.Target,
  1630  			Placement:       sub.Placement,
  1631  			Vcenter:         sub.Vcenter,
  1632  		}
  1633  
  1634  		OK(w, id)
  1635  	}
  1636  }
  1637  
  1638  func (s *handler) libraryItem(w http.ResponseWriter, r *http.Request) {
  1639  	switch r.Method {
  1640  	case http.MethodPost:
  1641  		var spec struct {
  1642  			Item library.Item     `json:"create_spec"`
  1643  			Find library.FindItem `json:"spec"`
  1644  		}
  1645  		if !s.decode(r, w, &spec) {
  1646  			return
  1647  		}
  1648  
  1649  		switch s.action(r) {
  1650  		case "find":
  1651  			var ids []string
  1652  			for _, l := range s.Library {
  1653  				if spec.Find.LibraryID != "" {
  1654  					if spec.Find.LibraryID != l.ID {
  1655  						continue
  1656  					}
  1657  				}
  1658  				for _, i := range l.Item {
  1659  					if spec.Find.Name != "" {
  1660  						if spec.Find.Name != i.Name {
  1661  							continue
  1662  						}
  1663  					}
  1664  					if spec.Find.Type != "" {
  1665  						if spec.Find.Type != i.Type {
  1666  							continue
  1667  						}
  1668  					}
  1669  					ids = append(ids, i.ID)
  1670  				}
  1671  			}
  1672  			OK(w, ids)
  1673  		case "create", "":
  1674  			id := spec.Item.LibraryID
  1675  			l, ok := s.Library[id]
  1676  			if !ok {
  1677  				log.Printf("library not found: %s", id)
  1678  				http.NotFound(w, r)
  1679  				return
  1680  			}
  1681  			if l.Type == "SUBSCRIBED" {
  1682  				BadRequest(w, "com.vmware.vapi.std.errors.invalid_element_type")
  1683  				return
  1684  			}
  1685  			for _, item := range l.Item {
  1686  				if item.Name == spec.Item.Name {
  1687  					BadRequest(w, "com.vmware.vapi.std.errors.already_exists")
  1688  					return
  1689  				}
  1690  			}
  1691  
  1692  			if !isValidFileName(spec.Item.Name) {
  1693  				ApiErrorInvalidArgument(w)
  1694  				return
  1695  			}
  1696  
  1697  			id = uuid.New().String()
  1698  			spec.Item.ID = id
  1699  			spec.Item.CreationTime = types.NewTime(time.Now())
  1700  			spec.Item.LastModifiedTime = types.NewTime(time.Now())
  1701  
  1702  			// Local items are always marked Cached=true
  1703  			spec.Item.Cached = true
  1704  
  1705  			// Local items start with a ContentVersion="1"
  1706  			spec.Item.ContentVersion = getVersionString("")
  1707  			spec.Item.MetadataVersion = getVersionString("")
  1708  
  1709  			if l.SecurityPolicyID != "" {
  1710  				// TODO: verify signed items
  1711  				spec.Item.SecurityCompliance = types.NewBool(false)
  1712  				spec.Item.CertificateVerification = &library.ItemCertificateVerification{
  1713  					Status: "NOT_AVAILABLE",
  1714  				}
  1715  			}
  1716  			l.Item[id] = &item{Item: &spec.Item}
  1717  			OK(w, id)
  1718  		}
  1719  	case http.MethodGet:
  1720  		id := r.URL.Query().Get("library_id")
  1721  		l, ok := s.Library[id]
  1722  		if !ok {
  1723  			log.Printf("library not found: %s", id)
  1724  			http.NotFound(w, r)
  1725  			return
  1726  		}
  1727  
  1728  		var ids []string
  1729  		for id := range l.Item {
  1730  			ids = append(ids, id)
  1731  		}
  1732  		OK(w, ids)
  1733  	}
  1734  }
  1735  
  1736  func (s *handler) libraryItemID(w http.ResponseWriter, r *http.Request) {
  1737  	id := s.id(r)
  1738  	lid := r.URL.Query().Get("library_id")
  1739  	if lid == "" {
  1740  		if l := s.itemLibrary(id); l != nil {
  1741  			lid = l.ID
  1742  		}
  1743  	}
  1744  	l, ok := s.Library[lid]
  1745  	if !ok {
  1746  		log.Printf("library not found: %q", lid)
  1747  		http.NotFound(w, r)
  1748  		return
  1749  	}
  1750  	item, ok := l.Item[id]
  1751  	if !ok {
  1752  		log.Printf("libraryItemID: library item not found: %q", id)
  1753  		http.NotFound(w, r)
  1754  		return
  1755  	}
  1756  
  1757  	switch r.Method {
  1758  	case http.MethodDelete:
  1759  		p := s.libraryPath(l.Library, id)
  1760  		if err := os.RemoveAll(p); err != nil {
  1761  			s.error(w, err)
  1762  			return
  1763  		}
  1764  		s.deleteVM(l.Item[item.ID].Template)
  1765  		delete(l.Item, item.ID)
  1766  		OK(w)
  1767  	case http.MethodPatch:
  1768  		var spec struct {
  1769  			library.Item `json:"update_spec"`
  1770  		}
  1771  		if s.decode(r, w, &spec) {
  1772  			item.Patch(&spec.Item)
  1773  			OK(w)
  1774  		}
  1775  	case http.MethodPost:
  1776  		switch s.action(r) {
  1777  		case "copy":
  1778  			var spec struct {
  1779  				library.Item `json:"destination_create_spec"`
  1780  			}
  1781  			if !s.decode(r, w, &spec) {
  1782  				return
  1783  			}
  1784  
  1785  			l, ok = s.Library[spec.LibraryID]
  1786  			if !ok {
  1787  				log.Printf("library not found: %q", spec.LibraryID)
  1788  				http.NotFound(w, r)
  1789  				return
  1790  			}
  1791  			if spec.Name == "" {
  1792  				BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  1793  			}
  1794  
  1795  			id := uuid.New().String()
  1796  			nitem := item.cp()
  1797  			nitem.ID = id
  1798  			nitem.LibraryID = spec.LibraryID
  1799  			l.Item[id] = nitem
  1800  
  1801  			OK(w, id)
  1802  		case "sync":
  1803  			if l.Type == "SUBSCRIBED" || l.Publication != nil {
  1804  				var spec internal.SubscriptionItemDestinationSpec
  1805  				if s.decode(r, w, &spec) {
  1806  					if l.Publication != nil {
  1807  						if s.publish(w, r, spec.Subscriptions, l, item) {
  1808  							OK(w)
  1809  						}
  1810  					}
  1811  					if l.Type == "SUBSCRIBED" {
  1812  						if err := s.syncItem(item, l, nil, spec.Force, nil); err != nil {
  1813  							BadRequest(w, err.Error())
  1814  						} else {
  1815  							OK(w)
  1816  						}
  1817  					}
  1818  				}
  1819  			} else {
  1820  				http.NotFound(w, r)
  1821  			}
  1822  		case "publish":
  1823  			var spec internal.SubscriptionDestinationSpec
  1824  			if s.decode(r, w, &spec) {
  1825  				if s.publish(w, r, spec.Subscriptions, l, item) {
  1826  					OK(w)
  1827  				}
  1828  			}
  1829  		case "evict":
  1830  			s.evictItem(item)
  1831  			OK(w, id)
  1832  		}
  1833  	case http.MethodGet:
  1834  		OK(w, item)
  1835  	}
  1836  }
  1837  
  1838  func (s *handler) libraryItemByID(id string) (*content, *item) {
  1839  	for _, l := range s.Library {
  1840  		if item, ok := l.Item[id]; ok {
  1841  			return l, item
  1842  		}
  1843  	}
  1844  
  1845  	log.Printf("library for item %q not found", id)
  1846  
  1847  	return nil, nil
  1848  }
  1849  
  1850  func (s *handler) libraryItemStorageByID(id string) ([]library.Storage, bool) {
  1851  	lib, item := s.libraryItemByID(id)
  1852  	if item == nil {
  1853  		return nil, false
  1854  	}
  1855  
  1856  	storage := make([]library.Storage, len(item.File))
  1857  
  1858  	for i, file := range item.File {
  1859  		storage[i] = library.Storage{
  1860  			StorageBacking: lib.Storage[0],
  1861  			StorageURIs: []string{
  1862  				path.Join(s.libraryPath(lib.Library, id), file.Name),
  1863  			},
  1864  			Name:    file.Name,
  1865  			Version: file.Version,
  1866  		}
  1867  		if file.Checksum != nil {
  1868  			storage[i].Checksum = *file.Checksum
  1869  		}
  1870  		if file.Size != nil {
  1871  			storage[i].Size = *file.Size
  1872  		}
  1873  		if file.Cached != nil {
  1874  			storage[i].Cached = *file.Cached
  1875  		}
  1876  	}
  1877  
  1878  	return storage, true
  1879  }
  1880  
  1881  func (s *handler) libraryItemStorage(w http.ResponseWriter, r *http.Request) {
  1882  	if r.Method != http.MethodGet {
  1883  		w.WriteHeader(http.StatusMethodNotAllowed)
  1884  		return
  1885  	}
  1886  
  1887  	id := r.URL.Query().Get("library_item_id")
  1888  	storage, ok := s.libraryItemStorageByID(id)
  1889  	if !ok {
  1890  		http.NotFound(w, r)
  1891  		return
  1892  	}
  1893  
  1894  	OK(w, storage)
  1895  }
  1896  
  1897  func (s *handler) libraryItemStorageID(w http.ResponseWriter, r *http.Request) {
  1898  	if r.Method != http.MethodPost {
  1899  		w.WriteHeader(http.StatusMethodNotAllowed)
  1900  		return
  1901  	}
  1902  
  1903  	id := s.id(r)
  1904  	storage, ok := s.libraryItemStorageByID(id)
  1905  	if !ok {
  1906  		http.NotFound(w, r)
  1907  		return
  1908  	}
  1909  
  1910  	var spec struct {
  1911  		Name string `json:"file_name"`
  1912  	}
  1913  
  1914  	if s.decode(r, w, &spec) {
  1915  		for _, file := range storage {
  1916  			if file.Name == spec.Name {
  1917  				OK(w, []library.Storage{file})
  1918  				return
  1919  			}
  1920  		}
  1921  		http.NotFound(w, r)
  1922  	}
  1923  }
  1924  
  1925  func (s *handler) libraryItemUpdateSession(w http.ResponseWriter, r *http.Request) {
  1926  	switch r.Method {
  1927  	case http.MethodGet:
  1928  		var ids []string
  1929  		for id := range s.Update {
  1930  			ids = append(ids, id)
  1931  		}
  1932  		OK(w, ids)
  1933  	case http.MethodPost:
  1934  		var spec struct {
  1935  			Session library.Session `json:"create_spec"`
  1936  		}
  1937  		if !s.decode(r, w, &spec) {
  1938  			return
  1939  		}
  1940  
  1941  		switch s.action(r) {
  1942  		case "create", "":
  1943  			lib, item := s.libraryItemByID(spec.Session.LibraryItemID)
  1944  			if lib == nil {
  1945  				log.Printf("library for item %q not found", item.ID)
  1946  				http.NotFound(w, r)
  1947  				return
  1948  			}
  1949  			session := &library.Session{
  1950  				ID:                        uuid.New().String(),
  1951  				LibraryItemID:             item.ID,
  1952  				LibraryItemContentVersion: item.ContentVersion,
  1953  				ClientProgress:            0,
  1954  				State:                     "ACTIVE",
  1955  				ExpirationTime:            types.NewTime(time.Now().Add(time.Hour)),
  1956  			}
  1957  			s.Update[session.ID] = update{
  1958  				WaitGroup: new(sync.WaitGroup),
  1959  				Session:   session,
  1960  				Library:   lib.Library,
  1961  				File:      make(map[string]*library.UpdateFile),
  1962  			}
  1963  			OK(w, session.ID)
  1964  		}
  1965  	}
  1966  }
  1967  
  1968  func (s *handler) libraryItemUpdateSessionID(w http.ResponseWriter, r *http.Request) {
  1969  	id := s.id(r)
  1970  	up, ok := s.Update[id]
  1971  	if !ok {
  1972  		log.Printf("update session not found: %s", id)
  1973  		http.NotFound(w, r)
  1974  		return
  1975  	}
  1976  
  1977  	session := up.Session
  1978  	done := func(state string) {
  1979  		if up.State != "ERROR" {
  1980  			up.State = state
  1981  		}
  1982  		go time.AfterFunc(session.ExpirationTime.Sub(time.Now()), func() {
  1983  			s.Lock()
  1984  			delete(s.Update, id)
  1985  			s.Unlock()
  1986  		})
  1987  	}
  1988  
  1989  	switch r.Method {
  1990  	case http.MethodGet:
  1991  		OK(w, session)
  1992  	case http.MethodPost:
  1993  		switch s.action(r) {
  1994  		case "cancel":
  1995  			done("CANCELED")
  1996  		case "complete":
  1997  			go func() {
  1998  				up.Wait() // wait for any PULL sources to complete
  1999  				done("DONE")
  2000  			}()
  2001  		case "fail":
  2002  			done("ERROR")
  2003  		case "keep-alive":
  2004  			session.ExpirationTime = types.NewTime(time.Now().Add(time.Hour))
  2005  		}
  2006  		OK(w)
  2007  	case http.MethodDelete:
  2008  		delete(s.Update, id)
  2009  		OK(w)
  2010  	}
  2011  }
  2012  
  2013  func (s *handler) libraryItemProbe(endpoint library.TransferEndpoint) *library.ProbeResult {
  2014  	p := &library.ProbeResult{
  2015  		Status: "SUCCESS",
  2016  	}
  2017  
  2018  	result := func() *library.ProbeResult {
  2019  		for i, m := range p.ErrorMessages {
  2020  			p.ErrorMessages[i].DefaultMessage = fmt.Sprintf(m.DefaultMessage, m.Args[0])
  2021  		}
  2022  		return p
  2023  	}
  2024  
  2025  	u, err := url.Parse(endpoint.URI)
  2026  	if err != nil {
  2027  		p.Status = "INVALID_URL"
  2028  		p.ErrorMessages = []rest.LocalizableMessage{{
  2029  			Args:           []string{endpoint.URI},
  2030  			ID:             "com.vmware.vdcs.cls-main.invalid_url_format",
  2031  			DefaultMessage: "Invalid URL format for %s",
  2032  		}}
  2033  		return result()
  2034  	}
  2035  
  2036  	if u.Scheme != "http" && u.Scheme != "https" {
  2037  		p.Status = "INVALID_URL"
  2038  		p.ErrorMessages = []rest.LocalizableMessage{{
  2039  			Args:           []string{endpoint.URI},
  2040  			ID:             "com.vmware.vdcs.cls-main.file_probe_unsupported_uri_scheme",
  2041  			DefaultMessage: "The specified URI %s is not supported",
  2042  		}}
  2043  		return result()
  2044  	}
  2045  
  2046  	res, err := http.Head(endpoint.URI)
  2047  	if err != nil {
  2048  		id := "com.vmware.vdcs.cls-main.http_request_error"
  2049  		p.Status = "INVALID_URL"
  2050  
  2051  		if soap.IsCertificateUntrusted(err) {
  2052  			var info object.HostCertificateInfo
  2053  			_ = info.FromURL(u, nil)
  2054  
  2055  			id = "com.vmware.vdcs.cls-main.http_request_error_peer_not_authenticated"
  2056  			p.Status = "CERTIFICATE_ERROR"
  2057  			p.SSLThumbprint = info.ThumbprintSHA1
  2058  		}
  2059  
  2060  		p.ErrorMessages = []rest.LocalizableMessage{{
  2061  			Args:           []string{err.Error()},
  2062  			ID:             id,
  2063  			DefaultMessage: "HTTP request error: %s",
  2064  		}}
  2065  
  2066  		return result()
  2067  	}
  2068  	_ = res.Body.Close()
  2069  
  2070  	if res.TLS != nil {
  2071  		p.SSLThumbprint = soap.ThumbprintSHA1(res.TLS.PeerCertificates[0])
  2072  	}
  2073  
  2074  	return result()
  2075  }
  2076  
  2077  func (s *handler) libraryItemUpdateSessionFile(w http.ResponseWriter, r *http.Request) {
  2078  	switch r.Method {
  2079  	case http.MethodPost:
  2080  		switch s.action(r) {
  2081  		case "probe":
  2082  			var spec struct {
  2083  				SourceEndpoint library.TransferEndpoint `json:"source_endpoint"`
  2084  			}
  2085  			if s.decode(r, w, &spec) {
  2086  				res := s.libraryItemProbe(spec.SourceEndpoint)
  2087  				OK(w, res)
  2088  			}
  2089  		default:
  2090  			http.NotFound(w, r)
  2091  		}
  2092  		return
  2093  	case http.MethodGet:
  2094  	default:
  2095  		w.WriteHeader(http.StatusMethodNotAllowed)
  2096  		return
  2097  	}
  2098  
  2099  	id := r.URL.Query().Get("update_session_id")
  2100  	up, ok := s.Update[id]
  2101  	if !ok {
  2102  		log.Printf("update session not found: %s", id)
  2103  		http.NotFound(w, r)
  2104  		return
  2105  	}
  2106  
  2107  	var files []*library.UpdateFile
  2108  	for _, f := range up.File {
  2109  		files = append(files, f)
  2110  	}
  2111  	OK(w, files)
  2112  }
  2113  
  2114  func (s *handler) pullSource(up update, info *library.UpdateFile) {
  2115  	defer up.Done()
  2116  	done := func(err error) {
  2117  		s.Lock()
  2118  		info.Status = "READY"
  2119  		if err != nil {
  2120  			log.Printf("PULL %s: %s", info.SourceEndpoint.URI, err)
  2121  			info.Status = "ERROR"
  2122  			info.ErrorMessage = &rest.LocalizableMessage{DefaultMessage: err.Error()}
  2123  			up.State = info.Status
  2124  			up.ErrorMessage = info.ErrorMessage
  2125  		}
  2126  		s.Unlock()
  2127  	}
  2128  
  2129  	c := &http.Client{
  2130  		Transport: &http.Transport{
  2131  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  2132  		},
  2133  	}
  2134  
  2135  	res, err := c.Get(info.SourceEndpoint.URI)
  2136  	if err != nil {
  2137  		done(err)
  2138  		return
  2139  	}
  2140  
  2141  	err = s.libraryItemFileCreate(&up, info.Name, res.Body, info.Checksum)
  2142  	done(err)
  2143  }
  2144  
  2145  func hasChecksum(c *library.Checksum) bool {
  2146  	return c != nil && c.Checksum != ""
  2147  }
  2148  
  2149  var checksum = map[string]func() hash.Hash{
  2150  	"MD5":    md5.New,
  2151  	"SHA1":   sha1.New,
  2152  	"SHA256": sha256.New,
  2153  	"SHA512": sha512.New,
  2154  }
  2155  
  2156  func (s *handler) libraryItemUpdateSessionFileID(w http.ResponseWriter, r *http.Request) {
  2157  	if r.Method != http.MethodPost {
  2158  		w.WriteHeader(http.StatusMethodNotAllowed)
  2159  		return
  2160  	}
  2161  
  2162  	id := s.id(r)
  2163  	up, ok := s.Update[id]
  2164  	if !ok {
  2165  		log.Printf("update session not found: %s", id)
  2166  		http.NotFound(w, r)
  2167  		return
  2168  	}
  2169  
  2170  	switch s.action(r) {
  2171  	case "add":
  2172  		var spec struct {
  2173  			File library.UpdateFile `json:"file_spec"`
  2174  		}
  2175  		if s.decode(r, w, &spec) {
  2176  			id = uuid.New().String()
  2177  			info := &library.UpdateFile{
  2178  				Name:             spec.File.Name,
  2179  				Checksum:         spec.File.Checksum,
  2180  				SourceType:       spec.File.SourceType,
  2181  				Status:           "WAITING_FOR_TRANSFER",
  2182  				BytesTransferred: 0,
  2183  			}
  2184  			switch info.SourceType {
  2185  			case "PUSH":
  2186  				u := url.URL{
  2187  					Scheme: s.URL.Scheme,
  2188  					Host:   s.URL.Host,
  2189  					Path:   path.Join(rest.Path, internal.LibraryItemFileData, id, info.Name),
  2190  				}
  2191  				info.UploadEndpoint = &library.TransferEndpoint{URI: u.String()}
  2192  			case "PULL":
  2193  				if hasChecksum(info.Checksum) && checksum[info.Checksum.Algorithm] == nil {
  2194  					BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  2195  					return
  2196  				}
  2197  				info.SourceEndpoint = spec.File.SourceEndpoint
  2198  				info.Status = "TRANSFERRING"
  2199  				up.Add(1)
  2200  				go s.pullSource(up, info)
  2201  			}
  2202  			up.File[id] = info
  2203  			OK(w, info)
  2204  		}
  2205  	case "get":
  2206  		var spec struct {
  2207  			File string `json:"file_name"`
  2208  		}
  2209  		if s.decode(r, w, &spec) {
  2210  			for _, f := range up.File {
  2211  				if f.Name == spec.File {
  2212  					OK(w, f)
  2213  					return
  2214  				}
  2215  			}
  2216  		}
  2217  	case "remove":
  2218  		if up.State != "ACTIVE" {
  2219  			s.error(w, fmt.Errorf("removeFile not allowed in state %s", up.State))
  2220  			return
  2221  		}
  2222  		delete(s.Update, id)
  2223  		OK(w)
  2224  	case "validate":
  2225  		if up.State != "ACTIVE" {
  2226  			BadRequest(w, "com.vmware.vapi.std.errors.not_allowed_in_current_state")
  2227  			return
  2228  		}
  2229  		var res library.UpdateFileValidation
  2230  		// TODO check missing_files, validate .ovf
  2231  		OK(w, res)
  2232  	}
  2233  }
  2234  
  2235  func (s *handler) libraryItemDownloadSession(w http.ResponseWriter, r *http.Request) {
  2236  	switch r.Method {
  2237  	case http.MethodGet:
  2238  		var ids []string
  2239  		for id := range s.Download {
  2240  			ids = append(ids, id)
  2241  		}
  2242  		OK(w, ids)
  2243  	case http.MethodPost:
  2244  		var spec struct {
  2245  			Session library.Session `json:"create_spec"`
  2246  		}
  2247  		if !s.decode(r, w, &spec) {
  2248  			return
  2249  		}
  2250  
  2251  		switch s.action(r) {
  2252  		case "create", "":
  2253  			lib, item := s.libraryItemByID(spec.Session.LibraryItemID)
  2254  			if item == nil {
  2255  				http.NotFound(w, r)
  2256  				return
  2257  			}
  2258  
  2259  			session := &library.Session{
  2260  				ID:                        uuid.New().String(),
  2261  				LibraryItemID:             spec.Session.LibraryItemID,
  2262  				LibraryItemContentVersion: item.ContentVersion,
  2263  				ClientProgress:            0,
  2264  				State:                     "ACTIVE",
  2265  				ExpirationTime:            types.NewTime(time.Now().Add(time.Hour)),
  2266  			}
  2267  			s.Download[session.ID] = download{
  2268  				Session: session,
  2269  				Library: lib.Library,
  2270  				File:    make(map[string]*library.DownloadFile),
  2271  			}
  2272  			for _, file := range item.File {
  2273  				s.Download[session.ID].File[file.Name] = &library.DownloadFile{
  2274  					Name:   file.Name,
  2275  					Status: "UNPREPARED",
  2276  				}
  2277  			}
  2278  			OK(w, session.ID)
  2279  		}
  2280  	}
  2281  }
  2282  
  2283  func (s *handler) libraryItemDownloadSessionID(w http.ResponseWriter, r *http.Request) {
  2284  	id := s.id(r)
  2285  	up, ok := s.Download[id]
  2286  	if !ok {
  2287  		log.Printf("download session not found: %s", id)
  2288  		http.NotFound(w, r)
  2289  		return
  2290  	}
  2291  
  2292  	session := up.Session
  2293  	switch r.Method {
  2294  	case http.MethodGet:
  2295  		OK(w, session)
  2296  	case http.MethodPost:
  2297  		switch s.action(r) {
  2298  		case "cancel", "complete", "fail":
  2299  			delete(s.Download, id) // TODO: fully mock VC's behavior
  2300  		case "keep-alive":
  2301  			session.ExpirationTime = types.NewTime(time.Now().Add(time.Hour))
  2302  		}
  2303  		OK(w)
  2304  	case http.MethodDelete:
  2305  		delete(s.Download, id)
  2306  		OK(w)
  2307  	}
  2308  }
  2309  
  2310  func (s *handler) libraryItemDownloadSessionFile(w http.ResponseWriter, r *http.Request) {
  2311  	if r.Method != http.MethodGet {
  2312  		w.WriteHeader(http.StatusMethodNotAllowed)
  2313  		return
  2314  	}
  2315  
  2316  	id := r.URL.Query().Get("download_session_id")
  2317  	dl, ok := s.Download[id]
  2318  	if !ok {
  2319  		log.Printf("download session not found: %s", id)
  2320  		http.NotFound(w, r)
  2321  		return
  2322  	}
  2323  
  2324  	var files []*library.DownloadFile
  2325  	for _, f := range dl.File {
  2326  		files = append(files, f)
  2327  	}
  2328  	OK(w, files)
  2329  }
  2330  
  2331  func (s *handler) libraryItemDownloadSessionFileID(w http.ResponseWriter, r *http.Request) {
  2332  	if r.Method != http.MethodPost {
  2333  		w.WriteHeader(http.StatusMethodNotAllowed)
  2334  		return
  2335  	}
  2336  
  2337  	id := s.id(r)
  2338  	dl, ok := s.Download[id]
  2339  	if !ok {
  2340  		log.Printf("download session not found: %s", id)
  2341  		http.NotFound(w, r)
  2342  		return
  2343  	}
  2344  
  2345  	var spec struct {
  2346  		File string `json:"file_name"`
  2347  	}
  2348  
  2349  	switch s.action(r) {
  2350  	case "prepare":
  2351  		if s.decode(r, w, &spec) {
  2352  			u := url.URL{
  2353  				Scheme: s.URL.Scheme,
  2354  				Host:   s.URL.Host,
  2355  				Path:   path.Join(rest.Path, internal.LibraryItemFileData, id, spec.File),
  2356  			}
  2357  			info := &library.DownloadFile{
  2358  				Name:             spec.File,
  2359  				Status:           "PREPARED",
  2360  				BytesTransferred: 0,
  2361  				DownloadEndpoint: &library.TransferEndpoint{
  2362  					URI: u.String(),
  2363  				},
  2364  			}
  2365  			dl.File[spec.File] = info
  2366  			OK(w, info)
  2367  		}
  2368  	case "get":
  2369  		if s.decode(r, w, &spec) {
  2370  			OK(w, dl.File[spec.File])
  2371  		}
  2372  	}
  2373  }
  2374  
  2375  func (s *handler) itemLibrary(id string) *library.Library {
  2376  	for _, l := range s.Library {
  2377  		if _, ok := l.Item[id]; ok {
  2378  			return l.Library
  2379  		}
  2380  	}
  2381  	return nil
  2382  }
  2383  
  2384  func (s *handler) updateFileInfo(id string) *update {
  2385  	for _, up := range s.Update {
  2386  		for i := range up.File {
  2387  			if i == id {
  2388  				return &up
  2389  			}
  2390  		}
  2391  	}
  2392  	return nil
  2393  }
  2394  
  2395  // libraryPath returns the local Datastore fs path for a Library or Item if id is specified.
  2396  func (s *handler) libraryPath(l *library.Library, id string) string {
  2397  	dsref := types.ManagedObjectReference{
  2398  		Type:  "Datastore",
  2399  		Value: l.Storage[0].DatastoreID,
  2400  	}
  2401  	ds := s.Map.Get(dsref).(*simulator.Datastore)
  2402  
  2403  	if !isValidFileName(l.ID) || !isValidFileName(id) {
  2404  		panic("invalid file name")
  2405  	}
  2406  
  2407  	return path.Join(append([]string{ds.Info.GetDatastoreInfo().Url, "contentlib-" + l.ID}, id)...)
  2408  }
  2409  
  2410  func (s *handler) libraryItemFileCreate(
  2411  	up *update,
  2412  	dstFileName string,
  2413  	body io.ReadCloser,
  2414  	cs *library.Checksum) error {
  2415  
  2416  	defer body.Close()
  2417  
  2418  	if !isValidFileName(dstFileName) {
  2419  		return errors.New("invalid file name")
  2420  	}
  2421  
  2422  	dstItemPath := s.libraryPath(up.Library, up.Session.LibraryItemID)
  2423  	if err := os.MkdirAll(dstItemPath, 0750); err != nil {
  2424  		return err
  2425  	}
  2426  
  2427  	// handleFile is used to process non-OVA files or files inside of an OVA.
  2428  	handleFile := func(
  2429  		fileName string,
  2430  		src io.Reader,
  2431  		doChecksum bool) (library.File, error) {
  2432  
  2433  		dstFilePath := path.Join(dstItemPath, fileName)
  2434  
  2435  		var h hash.Hash
  2436  
  2437  		if doChecksum {
  2438  			if hasChecksum(cs) {
  2439  				h = checksum[cs.Algorithm]()
  2440  				src = io.TeeReader(src, h)
  2441  			}
  2442  		}
  2443  
  2444  		dst, err := openFile(src, dstFilePath, createOrCopyFlags, createOrCopyMode)
  2445  		if err != nil {
  2446  			return library.File{}, err
  2447  		}
  2448  		defer dst.Close()
  2449  
  2450  		n, err := copyReaderToWriter(dst, dstFilePath, src, fileName)
  2451  		if err != nil {
  2452  			return library.File{}, err
  2453  		}
  2454  
  2455  		if h != nil {
  2456  			if sum := fmt.Sprintf("%x", h.Sum(nil)); sum != cs.Checksum {
  2457  				return library.File{}, fmt.Errorf(
  2458  					"checksum mismatch: file=%s, alg=%s, actual=%s, expected=%s",
  2459  					fileName, cs.Algorithm, sum, cs.Checksum)
  2460  			}
  2461  		}
  2462  
  2463  		return library.File{
  2464  			Cached:  types.NewBool(true),
  2465  			Name:    fileName,
  2466  			Size:    &n,
  2467  			Version: "1",
  2468  		}, nil
  2469  	}
  2470  
  2471  	// If the file being uploaded is not an OVA then it can be received
  2472  	// directly.
  2473  	if !strings.EqualFold(path.Ext(dstFileName), ".ova") {
  2474  
  2475  		// Handle the non-OVA file.
  2476  		f, err := handleFile(dstFileName, body, true)
  2477  		if err != nil {
  2478  			return err
  2479  		}
  2480  
  2481  		// Update the library item with the uploaded file.
  2482  		i := s.Library[up.Library.ID].Item[up.Session.LibraryItemID]
  2483  		i.File = append(i.File, f)
  2484  		return nil
  2485  	}
  2486  
  2487  	// If this is an OVA then the entire OVA is hashed.
  2488  	var (
  2489  		h   hash.Hash
  2490  		src io.Reader = body
  2491  	)
  2492  
  2493  	// See if the provided checksum is using a supported algorithm.
  2494  	if hasChecksum(cs) {
  2495  		h = checksum[cs.Algorithm]()
  2496  		src = io.TeeReader(src, h)
  2497  	}
  2498  
  2499  	// Otherwise the contents of the OVA should be uploaded.
  2500  	r := tar.NewReader(src)
  2501  
  2502  	// Collect the files from the OVA.
  2503  	var files []library.File
  2504  	for {
  2505  		h, err := r.Next()
  2506  		if err != nil {
  2507  			if err == io.EOF {
  2508  				break
  2509  			}
  2510  			return fmt.Errorf("failed to unwind ova: %w", err)
  2511  		}
  2512  		if isValidFileName(h.Name) {
  2513  
  2514  			// Tell the handleFile method *not* to do a checksum on the file
  2515  			// from the OVA. The checksum will occur on the entire OVA once its
  2516  			// contents have been read.
  2517  			f, err := handleFile(h.Name, io.LimitReader(r, h.Size), false)
  2518  			if err != nil {
  2519  				return err
  2520  			}
  2521  
  2522  			files = append(files, f)
  2523  		}
  2524  	}
  2525  
  2526  	// If there was a checksum provided then verify the entire OVA matches the
  2527  	// provided checksum.
  2528  	if h != nil {
  2529  		if sum := fmt.Sprintf("%x", h.Sum(nil)); sum != cs.Checksum {
  2530  			return fmt.Errorf(
  2531  				"checksum mismatch: file=%s, alg=%s, actual=%s, expected=%s",
  2532  				dstFileName, cs.Algorithm, sum, cs.Checksum)
  2533  		}
  2534  	}
  2535  
  2536  	// Update the library item with the uploaded files.
  2537  	i := s.Library[up.Library.ID].Item[up.Session.LibraryItemID]
  2538  	i.File = files
  2539  
  2540  	return nil
  2541  }
  2542  
  2543  func (s *handler) libraryItemFileData(w http.ResponseWriter, r *http.Request) {
  2544  	p := strings.Split(r.URL.Path, "/")
  2545  	id, name := p[len(p)-2], p[len(p)-1]
  2546  
  2547  	if r.Method == http.MethodGet {
  2548  		dl, ok := s.Download[id]
  2549  		if !ok {
  2550  			log.Printf("library download not found: %s", id)
  2551  			http.NotFound(w, r)
  2552  			return
  2553  		}
  2554  		p := path.Join(s.libraryPath(dl.Library, dl.Session.LibraryItemID), name)
  2555  		f, err := os.Open(p)
  2556  		if err != nil {
  2557  			s.error(w, err)
  2558  			return
  2559  		}
  2560  		_, err = io.Copy(w, f)
  2561  		if err != nil {
  2562  			log.Printf("copy %s: %s", p, err)
  2563  		}
  2564  		_ = f.Close()
  2565  		return
  2566  	}
  2567  
  2568  	if r.Method != http.MethodPut {
  2569  		w.WriteHeader(http.StatusMethodNotAllowed)
  2570  		return
  2571  	}
  2572  
  2573  	up := s.updateFileInfo(id)
  2574  	if up == nil {
  2575  		log.Printf("library update not found: %s", id)
  2576  		http.NotFound(w, r)
  2577  		return
  2578  	}
  2579  
  2580  	err := s.libraryItemFileCreate(up, name, r.Body, nil)
  2581  	if err != nil {
  2582  		s.error(w, err)
  2583  	}
  2584  }
  2585  
  2586  func (s *handler) libraryItemFile(w http.ResponseWriter, r *http.Request) {
  2587  	id := r.URL.Query().Get("library_item_id")
  2588  	for _, l := range s.Library {
  2589  		if i, ok := l.Item[id]; ok {
  2590  			OK(w, i.File)
  2591  			return
  2592  		}
  2593  	}
  2594  	http.NotFound(w, r)
  2595  }
  2596  
  2597  func (s *handler) libraryItemFileID(w http.ResponseWriter, r *http.Request) {
  2598  	if r.Method != http.MethodPost {
  2599  		w.WriteHeader(http.StatusMethodNotAllowed)
  2600  		return
  2601  	}
  2602  	id := s.id(r)
  2603  	var spec struct {
  2604  		Name string `json:"name"`
  2605  	}
  2606  	if !s.decode(r, w, &spec) {
  2607  		return
  2608  	}
  2609  	for _, l := range s.Library {
  2610  		if i, ok := l.Item[id]; ok {
  2611  			for _, f := range i.File {
  2612  				if f.Name == spec.Name {
  2613  					OK(w, f)
  2614  					return
  2615  				}
  2616  			}
  2617  		}
  2618  	}
  2619  	http.NotFound(w, r)
  2620  }
  2621  
  2622  func (i *item) cp() *item {
  2623  	nitem := *i.Item
  2624  
  2625  	nfile := make([]library.File, len(i.File))
  2626  	copy(nfile, i.File)
  2627  
  2628  	var nref *types.ManagedObjectReference
  2629  	if i.Template != nil {
  2630  		iref := *i.Template
  2631  		nref = &iref
  2632  	}
  2633  
  2634  	return &item{
  2635  		Item:     &nitem,
  2636  		File:     nfile,
  2637  		Template: nref,
  2638  	}
  2639  }
  2640  
  2641  func (i *item) ovf() string {
  2642  	for _, f := range i.File {
  2643  		if strings.HasSuffix(f.Name, ".ovf") {
  2644  			return f.Name
  2645  		}
  2646  	}
  2647  	return ""
  2648  }
  2649  
  2650  func vmConfigSpec(ctx context.Context, c *vim25.Client, deploy vcenter.Deploy) (*types.VirtualMachineConfigSpec, error) {
  2651  	if deploy.VmConfigSpec == nil {
  2652  		return nil, nil
  2653  	}
  2654  
  2655  	b, err := base64.StdEncoding.DecodeString(deploy.VmConfigSpec.XML)
  2656  	if err != nil {
  2657  		return nil, err
  2658  	}
  2659  
  2660  	var spec *types.VirtualMachineConfigSpec
  2661  
  2662  	dec := xml.NewDecoder(bytes.NewReader(b))
  2663  	dec.TypeFunc = c.Types
  2664  	err = dec.Decode(&spec)
  2665  	if err != nil {
  2666  		return nil, err
  2667  	}
  2668  
  2669  	return spec, nil
  2670  }
  2671  
  2672  func (s *handler) libraryDeploy(ctx context.Context, c *vim25.Client, lib *library.Library, item *item, deploy vcenter.Deploy) (*nfc.LeaseInfo, error) {
  2673  	config, err := vmConfigSpec(ctx, c, deploy)
  2674  	if err != nil {
  2675  		return nil, err
  2676  	}
  2677  
  2678  	name := item.ovf()
  2679  	desc, err := os.ReadFile(filepath.Join(s.libraryPath(lib, item.ID), name))
  2680  	if err != nil {
  2681  		return nil, err
  2682  	}
  2683  	ds := types.ManagedObjectReference{Type: "Datastore", Value: deploy.DeploymentSpec.DefaultDatastoreID}
  2684  	pool := types.ManagedObjectReference{Type: "ResourcePool", Value: deploy.Target.ResourcePoolID}
  2685  	var folder, host *types.ManagedObjectReference
  2686  	if deploy.Target.FolderID != "" {
  2687  		folder = &types.ManagedObjectReference{Type: "Folder", Value: deploy.Target.FolderID}
  2688  	}
  2689  	if deploy.Target.HostID != "" {
  2690  		host = &types.ManagedObjectReference{Type: "HostSystem", Value: deploy.Target.HostID}
  2691  	}
  2692  
  2693  	v, err := view.NewManager(c).CreateContainerView(ctx, c.ServiceContent.RootFolder, nil, true)
  2694  	if err != nil {
  2695  		return nil, err
  2696  	}
  2697  	defer func() {
  2698  		_ = v.Destroy(ctx)
  2699  	}()
  2700  	refs, err := v.Find(ctx, []string{"Network"}, nil)
  2701  	if err != nil {
  2702  		return nil, err
  2703  	}
  2704  
  2705  	var network []types.OvfNetworkMapping
  2706  	for _, net := range deploy.NetworkMappings {
  2707  		for i := range refs {
  2708  			if refs[i].Value == net.Value {
  2709  				network = append(network, types.OvfNetworkMapping{Name: net.Key, Network: refs[i]})
  2710  				break
  2711  			}
  2712  		}
  2713  	}
  2714  
  2715  	if ds.Value == "" {
  2716  		// Datastore is optional in the deploy spec, but not in OvfManager.CreateImportSpec
  2717  		refs, err = v.Find(ctx, []string{"Datastore"}, nil)
  2718  		if err != nil {
  2719  			return nil, err
  2720  		}
  2721  		// TODO: consider StorageProfileID
  2722  		ds = refs[0]
  2723  	}
  2724  
  2725  	cisp := types.OvfCreateImportSpecParams{
  2726  		DiskProvisioning: deploy.DeploymentSpec.StorageProvisioning,
  2727  		EntityName:       deploy.DeploymentSpec.Name,
  2728  		NetworkMapping:   network,
  2729  	}
  2730  
  2731  	for _, p := range deploy.AdditionalParams {
  2732  		switch p.Type {
  2733  		case vcenter.TypePropertyParams:
  2734  			for _, prop := range p.Properties {
  2735  				cisp.PropertyMapping = append(cisp.PropertyMapping, types.KeyValue{
  2736  					Key:   prop.ID,
  2737  					Value: prop.Value,
  2738  				})
  2739  			}
  2740  		case vcenter.TypeDeploymentOptionParams:
  2741  			cisp.OvfManagerCommonParams.DeploymentOption = p.SelectedKey
  2742  		}
  2743  	}
  2744  
  2745  	m := ovf.NewManager(c)
  2746  	spec, err := m.CreateImportSpec(ctx, string(desc), pool, ds, &cisp)
  2747  	if err != nil {
  2748  		return nil, err
  2749  	}
  2750  	if spec.Error != nil {
  2751  		return nil, errors.New(spec.Error[0].LocalizedMessage)
  2752  	}
  2753  
  2754  	if config != nil {
  2755  		if vmImportSpec, ok := spec.ImportSpec.(*types.VirtualMachineImportSpec); ok {
  2756  			var configSpecs []types.BaseVirtualDeviceConfigSpec
  2757  
  2758  			// Remove devices that we don't want to carry over from the import spec. Otherwise, since we
  2759  			// just reconfigure the VM with the provided ConfigSpec later these devices won't be removed.
  2760  			for _, d := range vmImportSpec.ConfigSpec.DeviceChange {
  2761  				switch d.GetVirtualDeviceConfigSpec().Device.(type) {
  2762  				case types.BaseVirtualEthernetCard:
  2763  				default:
  2764  					configSpecs = append(configSpecs, d)
  2765  				}
  2766  			}
  2767  			vmImportSpec.ConfigSpec.DeviceChange = configSpecs
  2768  		}
  2769  	}
  2770  
  2771  	req := types.ImportVApp{
  2772  		This:   pool,
  2773  		Spec:   spec.ImportSpec,
  2774  		Folder: folder,
  2775  		Host:   host,
  2776  	}
  2777  	res, err := methods.ImportVApp(ctx, c, &req)
  2778  	if err != nil {
  2779  		return nil, err
  2780  	}
  2781  
  2782  	lease := nfc.NewLease(c, res.Returnval)
  2783  	info, err := lease.Wait(ctx, spec.FileItem)
  2784  	if err != nil {
  2785  		return nil, err
  2786  	}
  2787  
  2788  	if err = lease.Complete(ctx); err != nil {
  2789  		return nil, err
  2790  	}
  2791  
  2792  	if config != nil {
  2793  		if err = s.reconfigVM(info.Entity, *config); err != nil {
  2794  			return nil, err
  2795  		}
  2796  	}
  2797  
  2798  	return info, nil
  2799  }
  2800  
  2801  func (s *handler) libraryItemOVF(w http.ResponseWriter, r *http.Request) {
  2802  	if r.Method != http.MethodPost {
  2803  		w.WriteHeader(http.StatusMethodNotAllowed)
  2804  		return
  2805  	}
  2806  
  2807  	var req vcenter.OVF
  2808  	if !s.decode(r, w, &req) {
  2809  		return
  2810  	}
  2811  
  2812  	switch {
  2813  	case req.Target.LibraryItemID != "":
  2814  	case req.Target.LibraryID != "":
  2815  		l, ok := s.Library[req.Target.LibraryID]
  2816  		if !ok {
  2817  			http.NotFound(w, r)
  2818  		}
  2819  
  2820  		id := uuid.New().String()
  2821  		l.Item[id] = &item{
  2822  			Item: &library.Item{
  2823  				ID:               id,
  2824  				LibraryID:        l.Library.ID,
  2825  				Name:             req.Spec.Name,
  2826  				Description:      &req.Spec.Description,
  2827  				Type:             library.ItemTypeOVF,
  2828  				CreationTime:     types.NewTime(time.Now()),
  2829  				LastModifiedTime: types.NewTime(time.Now()),
  2830  			},
  2831  		}
  2832  
  2833  		res := vcenter.CreateResult{
  2834  			Succeeded: true,
  2835  			ID:        id,
  2836  		}
  2837  		OK(w, res)
  2838  	default:
  2839  		BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  2840  		return
  2841  	}
  2842  }
  2843  
  2844  func (s *handler) libraryItemOVFID(w http.ResponseWriter, r *http.Request) {
  2845  	if r.Method != http.MethodPost {
  2846  		w.WriteHeader(http.StatusMethodNotAllowed)
  2847  		return
  2848  	}
  2849  
  2850  	id := s.id(r)
  2851  	ok := false
  2852  	var lib *library.Library
  2853  	var item *item
  2854  	for _, l := range s.Library {
  2855  		item, ok = l.Item[id]
  2856  		if ok {
  2857  			lib = l.Library
  2858  			break
  2859  		}
  2860  	}
  2861  	if !ok {
  2862  		log.Printf("libraryItemOVFID: library item not found: %q", id)
  2863  		http.NotFound(w, r)
  2864  		return
  2865  	}
  2866  
  2867  	var spec struct {
  2868  		vcenter.Deploy
  2869  	}
  2870  	if !s.decode(r, w, &spec) {
  2871  		return
  2872  	}
  2873  
  2874  	switch s.action(r) {
  2875  	case "deploy":
  2876  		var d vcenter.Deployment
  2877  		err := s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2878  			info, err := s.libraryDeploy(ctx, c, lib, item, spec.Deploy)
  2879  			if err != nil {
  2880  				return err
  2881  			}
  2882  			id := vcenter.ResourceID{
  2883  				Type:  info.Entity.Type,
  2884  				Value: info.Entity.Value,
  2885  			}
  2886  			d.Succeeded = true
  2887  			d.ResourceID = &id
  2888  			return nil
  2889  		})
  2890  		if err != nil {
  2891  			d.Error = &vcenter.DeploymentError{
  2892  				Errors: []vcenter.OVFError{{
  2893  					Category: "SERVER",
  2894  					Error: &vcenter.Error{
  2895  						Class: "com.vmware.vapi.std.errors.error",
  2896  						Messages: []rest.LocalizableMessage{
  2897  							{
  2898  								DefaultMessage: err.Error(),
  2899  							},
  2900  						},
  2901  					},
  2902  				}},
  2903  			}
  2904  		}
  2905  		OK(w, d)
  2906  	case "filter":
  2907  		res := vcenter.FilterResponse{
  2908  			Name: item.Name,
  2909  		}
  2910  		OK(w, res)
  2911  	default:
  2912  		http.NotFound(w, r)
  2913  	}
  2914  }
  2915  
  2916  func (s *handler) deleteVM(ref *types.ManagedObjectReference) {
  2917  	if ref == nil {
  2918  		return
  2919  	}
  2920  	_ = s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2921  		_, _ = object.NewVirtualMachine(c, *ref).Destroy(ctx)
  2922  		return nil
  2923  	})
  2924  }
  2925  
  2926  func (s *handler) reconfigVM(ref types.ManagedObjectReference, config types.VirtualMachineConfigSpec) error {
  2927  	return s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2928  		vm := object.NewVirtualMachine(c, ref)
  2929  		task, err := vm.Reconfigure(ctx, config)
  2930  		if err != nil {
  2931  			return err
  2932  		}
  2933  		return task.Wait(ctx)
  2934  	})
  2935  }
  2936  
  2937  func (s *handler) cloneVM(source string, name string, p *library.Placement, storage *vcenter.DiskStorage) (*types.ManagedObjectReference, error) {
  2938  	var folder, pool, host, ds *types.ManagedObjectReference
  2939  	if p.Folder != "" {
  2940  		folder = &types.ManagedObjectReference{Type: "Folder", Value: p.Folder}
  2941  	}
  2942  	if p.ResourcePool != "" {
  2943  		pool = &types.ManagedObjectReference{Type: "ResourcePool", Value: p.ResourcePool}
  2944  	}
  2945  	if p.Host != "" {
  2946  		host = &types.ManagedObjectReference{Type: "HostSystem", Value: p.Host}
  2947  	}
  2948  	if storage != nil {
  2949  		if storage.Datastore != "" {
  2950  			ds = &types.ManagedObjectReference{Type: "Datastore", Value: storage.Datastore}
  2951  		}
  2952  	}
  2953  
  2954  	spec := types.VirtualMachineCloneSpec{
  2955  		Template: true,
  2956  		Location: types.VirtualMachineRelocateSpec{
  2957  			Folder:    folder,
  2958  			Pool:      pool,
  2959  			Host:      host,
  2960  			Datastore: ds,
  2961  		},
  2962  	}
  2963  
  2964  	var ref *types.ManagedObjectReference
  2965  
  2966  	return ref, s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2967  		vm := object.NewVirtualMachine(c, types.ManagedObjectReference{Type: "VirtualMachine", Value: source})
  2968  
  2969  		task, err := vm.Clone(ctx, object.NewFolder(c, *folder), name, spec)
  2970  		if err != nil {
  2971  			return err
  2972  		}
  2973  		res, err := task.WaitForResult(ctx, nil)
  2974  		if err != nil {
  2975  			return err
  2976  		}
  2977  		ref = types.NewReference(res.Result.(types.ManagedObjectReference))
  2978  		return nil
  2979  	})
  2980  }
  2981  
  2982  func (s *handler) libraryItemCreateTemplate(w http.ResponseWriter, r *http.Request) {
  2983  	if r.Method != http.MethodPost {
  2984  		w.WriteHeader(http.StatusMethodNotAllowed)
  2985  		return
  2986  	}
  2987  
  2988  	var spec struct {
  2989  		vcenter.Template `json:"spec"`
  2990  	}
  2991  	if !s.decode(r, w, &spec) {
  2992  		return
  2993  	}
  2994  
  2995  	l, ok := s.Library[spec.Library]
  2996  	if !ok {
  2997  		http.NotFound(w, r)
  2998  		return
  2999  	}
  3000  
  3001  	ds := &vcenter.DiskStorage{Datastore: l.Library.Storage[0].DatastoreID}
  3002  	ref, err := s.cloneVM(spec.SourceVM, spec.Name, spec.Placement, ds)
  3003  	if err != nil {
  3004  		BadRequest(w, err.Error())
  3005  		return
  3006  	}
  3007  
  3008  	id := uuid.New().String()
  3009  	l.Item[id] = &item{
  3010  		Item: &library.Item{
  3011  			ID:               id,
  3012  			LibraryID:        l.Library.ID,
  3013  			Name:             spec.Name,
  3014  			Type:             library.ItemTypeVMTX,
  3015  			CreationTime:     types.NewTime(time.Now()),
  3016  			LastModifiedTime: types.NewTime(time.Now()),
  3017  		},
  3018  		Template: ref,
  3019  	}
  3020  
  3021  	OK(w, id)
  3022  }
  3023  
  3024  func (s *handler) libraryItemTemplateID(w http.ResponseWriter, r *http.Request) {
  3025  	// Go's ServeMux doesn't support wildcard matching, hacking around that for now to support
  3026  	// CheckOuts, e.g. "/vcenter/vm-template/library-items/{item}/check-outs/{vm}?action=check-in"
  3027  	p := strings.TrimPrefix(r.URL.Path, rest.Path+internal.VCenterVMTXLibraryItem+"/")
  3028  	route := strings.Split(p, "/")
  3029  	if len(route) == 0 {
  3030  		http.NotFound(w, r)
  3031  		return
  3032  	}
  3033  
  3034  	id := route[0]
  3035  	ok := false
  3036  
  3037  	var item *item
  3038  	for _, l := range s.Library {
  3039  		item, ok = l.Item[id]
  3040  		if ok {
  3041  			break
  3042  		}
  3043  	}
  3044  	if !ok {
  3045  		log.Printf("libraryItemTemplateID: library item not found: %q", id)
  3046  		http.NotFound(w, r)
  3047  		return
  3048  	}
  3049  
  3050  	if item.Type != library.ItemTypeVMTX {
  3051  		BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  3052  		return
  3053  	}
  3054  
  3055  	if len(route) > 1 {
  3056  		switch route[1] {
  3057  		case "check-outs":
  3058  			s.libraryItemCheckOuts(item, w, r)
  3059  			return
  3060  		default:
  3061  			http.NotFound(w, r)
  3062  			return
  3063  		}
  3064  	}
  3065  
  3066  	if r.Method == http.MethodGet {
  3067  		// TODO: add mock data
  3068  		t := &vcenter.TemplateInfo{}
  3069  		OK(w, t)
  3070  		return
  3071  	}
  3072  
  3073  	var spec struct {
  3074  		vcenter.DeployTemplate `json:"spec"`
  3075  	}
  3076  	if !s.decode(r, w, &spec) {
  3077  		return
  3078  	}
  3079  
  3080  	switch r.URL.Query().Get("action") {
  3081  	case "deploy":
  3082  		p := spec.Placement
  3083  		if p == nil {
  3084  			BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  3085  			return
  3086  		}
  3087  		if p.Cluster == "" && p.Host == "" && p.ResourcePool == "" {
  3088  			BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  3089  			return
  3090  		}
  3091  
  3092  		s.syncItem(item, nil, nil, true, nil)
  3093  		ref, err := s.cloneVM(item.Template.Value, spec.Name, p, spec.DiskStorage)
  3094  		if err != nil {
  3095  			BadRequest(w, err.Error())
  3096  			return
  3097  		}
  3098  		OK(w, ref.Value)
  3099  	default:
  3100  		http.NotFound(w, r)
  3101  	}
  3102  }
  3103  
  3104  func (s *handler) libraryItemCheckOuts(item *item, w http.ResponseWriter, r *http.Request) {
  3105  	switch r.URL.Query().Get("action") {
  3106  	case "check-out":
  3107  		var spec struct {
  3108  			*vcenter.CheckOut `json:"spec"`
  3109  		}
  3110  		if !s.decode(r, w, &spec) {
  3111  			return
  3112  		}
  3113  
  3114  		ref, err := s.cloneVM(item.Template.Value, spec.Name, spec.Placement, nil)
  3115  		if err != nil {
  3116  			BadRequest(w, err.Error())
  3117  			return
  3118  		}
  3119  		OK(w, ref.Value)
  3120  	case "check-in":
  3121  		// TODO: increment ContentVersion
  3122  		OK(w, "0")
  3123  	default:
  3124  		http.NotFound(w, r)
  3125  	}
  3126  }
  3127  
  3128  // defaultSecurityPolicies generates the initial set of security policies always present on vCenter.
  3129  func defaultSecurityPolicies() []library.ContentSecurityPoliciesInfo {
  3130  	policyID, _ := uuid.NewUUID()
  3131  	return []library.ContentSecurityPoliciesInfo{
  3132  		{
  3133  			ItemTypeRules: map[string]string{
  3134  				"ovf": "OVF_STRICT_VERIFICATION",
  3135  			},
  3136  			Name:   "OVF default policy",
  3137  			Policy: policyID.String(),
  3138  		},
  3139  	}
  3140  }
  3141  
  3142  func (s *handler) librarySecurityPolicies(w http.ResponseWriter, r *http.Request) {
  3143  	switch r.Method {
  3144  	case http.MethodGet:
  3145  		StatusOK(w, s.Policies)
  3146  	default:
  3147  		w.WriteHeader(http.StatusMethodNotAllowed)
  3148  	}
  3149  }
  3150  
  3151  func (s *handler) isValidSecurityPolicy(policy string) bool {
  3152  	if policy == "" {
  3153  		return true
  3154  	}
  3155  
  3156  	for _, p := range s.Policies {
  3157  		if p.Policy == policy {
  3158  			return true
  3159  		}
  3160  	}
  3161  	return false
  3162  }
  3163  
  3164  func (s *handler) libraryTrustedCertificates(w http.ResponseWriter, r *http.Request) {
  3165  	switch r.Method {
  3166  	case http.MethodGet:
  3167  		var res struct {
  3168  			Certificates []library.TrustedCertificateSummary `json:"certificates"`
  3169  		}
  3170  		for id, cert := range s.Trust {
  3171  			res.Certificates = append(res.Certificates, library.TrustedCertificateSummary{
  3172  				TrustedCertificate: cert,
  3173  				ID:                 id,
  3174  			})
  3175  		}
  3176  
  3177  		StatusOK(w, &res)
  3178  	case http.MethodPost:
  3179  		var info library.TrustedCertificate
  3180  		if s.decode(r, w, &info) {
  3181  			block, _ := pem.Decode([]byte(info.Text))
  3182  			if block == nil {
  3183  				s.error(w, errors.New("invalid certificate"))
  3184  				return
  3185  			}
  3186  			_, err := x509.ParseCertificate(block.Bytes)
  3187  			if err != nil {
  3188  				s.error(w, err)
  3189  				return
  3190  			}
  3191  
  3192  			id := uuid.New().String()
  3193  			for x, cert := range s.Trust {
  3194  				if info.Text == cert.Text {
  3195  					id = x // existing certificate
  3196  					break
  3197  				}
  3198  			}
  3199  			s.Trust[id] = info
  3200  
  3201  			w.WriteHeader(http.StatusCreated)
  3202  		}
  3203  	default:
  3204  		w.WriteHeader(http.StatusMethodNotAllowed)
  3205  	}
  3206  }
  3207  
  3208  func (s *handler) libraryTrustedCertificatesID(w http.ResponseWriter, r *http.Request) {
  3209  	id := path.Base(r.URL.Path)
  3210  	cert, ok := s.Trust[id]
  3211  	if !ok {
  3212  		http.NotFound(w, r)
  3213  		return
  3214  	}
  3215  
  3216  	switch r.Method {
  3217  	case http.MethodGet:
  3218  		StatusOK(w, &cert)
  3219  	case http.MethodDelete:
  3220  		delete(s.Trust, id)
  3221  	default:
  3222  		w.WriteHeader(http.StatusMethodNotAllowed)
  3223  	}
  3224  }
  3225  
  3226  func (s *handler) debugEcho(w http.ResponseWriter, r *http.Request) {
  3227  	r.Write(w)
  3228  }
  3229  
  3230  func isValidFileName(s string) bool {
  3231  	return !strings.Contains(s, "/") &&
  3232  		!strings.Contains(s, "\\") &&
  3233  		!strings.Contains(s, "..")
  3234  }
  3235  
  3236  func getVersionString(current string) string {
  3237  	if current == "" {
  3238  		return "1"
  3239  	}
  3240  	i, err := strconv.Atoi(current)
  3241  	if err != nil {
  3242  		panic(err)
  3243  	}
  3244  	i += 1
  3245  	return strconv.Itoa(i)
  3246  }