github.com/vmware/govmomi@v0.37.2/vapi/simulator/simulator.go (about)

     1  /*
     2  Copyright (c) 2018-2023 VMware, Inc. All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package simulator
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"context"
    23  	"crypto/tls"
    24  	"crypto/x509"
    25  	"encoding/base64"
    26  	"encoding/json"
    27  	"encoding/pem"
    28  	"errors"
    29  	"fmt"
    30  	"io"
    31  	"log"
    32  	"net/http"
    33  	"net/url"
    34  	"os"
    35  	"path"
    36  	"path/filepath"
    37  	"reflect"
    38  	"strings"
    39  	"sync"
    40  	"time"
    41  
    42  	"github.com/google/uuid"
    43  
    44  	"github.com/vmware/govmomi"
    45  	"github.com/vmware/govmomi/nfc"
    46  	"github.com/vmware/govmomi/object"
    47  	"github.com/vmware/govmomi/ovf"
    48  	"github.com/vmware/govmomi/simulator"
    49  	"github.com/vmware/govmomi/vapi"
    50  	"github.com/vmware/govmomi/vapi/internal"
    51  	"github.com/vmware/govmomi/vapi/library"
    52  	"github.com/vmware/govmomi/vapi/rest"
    53  	"github.com/vmware/govmomi/vapi/tags"
    54  	"github.com/vmware/govmomi/vapi/vcenter"
    55  	"github.com/vmware/govmomi/view"
    56  	"github.com/vmware/govmomi/vim25"
    57  	"github.com/vmware/govmomi/vim25/methods"
    58  	"github.com/vmware/govmomi/vim25/soap"
    59  	"github.com/vmware/govmomi/vim25/types"
    60  	vim "github.com/vmware/govmomi/vim25/types"
    61  	"github.com/vmware/govmomi/vim25/xml"
    62  )
    63  
    64  type item struct {
    65  	*library.Item
    66  	File     []library.File
    67  	Template *types.ManagedObjectReference
    68  }
    69  
    70  type content struct {
    71  	*library.Library
    72  	Item map[string]*item
    73  	Subs map[string]*library.Subscriber
    74  	VMTX map[string]*types.ManagedObjectReference
    75  }
    76  
    77  type update struct {
    78  	*library.Session
    79  	Library *library.Library
    80  	File    map[string]*library.UpdateFile
    81  }
    82  
    83  type download struct {
    84  	*library.Session
    85  	Library *library.Library
    86  	File    map[string]*library.DownloadFile
    87  }
    88  
    89  type handler struct {
    90  	sync.Mutex
    91  	sm          *simulator.SessionManager
    92  	ServeMux    *http.ServeMux
    93  	URL         url.URL
    94  	Category    map[string]*tags.Category
    95  	Tag         map[string]*tags.Tag
    96  	Association map[string]map[internal.AssociatedObject]bool
    97  	Session     map[string]*rest.Session
    98  	Library     map[string]*content
    99  	Update      map[string]update
   100  	Download    map[string]download
   101  	Policies    []library.ContentSecurityPoliciesInfo
   102  	Trust       map[string]library.TrustedCertificate
   103  }
   104  
   105  func init() {
   106  	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
   107  		if r.IsVPX() {
   108  			patterns, h := New(s.Listen, r)
   109  			for _, p := range patterns {
   110  				s.Handle(p, h)
   111  			}
   112  		}
   113  	})
   114  }
   115  
   116  // New creates a vAPI simulator.
   117  func New(u *url.URL, r *simulator.Registry) ([]string, http.Handler) {
   118  	s := &handler{
   119  		sm:          r.SessionManager(),
   120  		ServeMux:    http.NewServeMux(),
   121  		URL:         *u,
   122  		Category:    make(map[string]*tags.Category),
   123  		Tag:         make(map[string]*tags.Tag),
   124  		Association: make(map[string]map[internal.AssociatedObject]bool),
   125  		Session:     make(map[string]*rest.Session),
   126  		Library:     make(map[string]*content),
   127  		Update:      make(map[string]update),
   128  		Download:    make(map[string]download),
   129  		Policies:    defaultSecurityPolicies(),
   130  		Trust:       make(map[string]library.TrustedCertificate),
   131  	}
   132  
   133  	handlers := []struct {
   134  		p string
   135  		m http.HandlerFunc
   136  	}{
   137  		// /rest/ patterns.
   138  		{internal.SessionPath, s.session},
   139  		{internal.CategoryPath, s.category},
   140  		{internal.CategoryPath + "/", s.categoryID},
   141  		{internal.TagPath, s.tag},
   142  		{internal.TagPath + "/", s.tagID},
   143  		{internal.AssociationPath, s.association},
   144  		{internal.AssociationPath + "/", s.associationID},
   145  		{internal.LibraryPath, s.library},
   146  		{internal.LocalLibraryPath, s.library},
   147  		{internal.SubscribedLibraryPath, s.library},
   148  		{internal.LibraryPath + "/", s.libraryID},
   149  		{internal.LocalLibraryPath + "/", s.libraryID},
   150  		{internal.SubscribedLibraryPath + "/", s.libraryID},
   151  		{internal.Subscriptions, s.subscriptions},
   152  		{internal.Subscriptions + "/", s.subscriptionsID},
   153  		{internal.LibraryItemPath, s.libraryItem},
   154  		{internal.LibraryItemPath + "/", s.libraryItemID},
   155  		{internal.SubscribedLibraryItem + "/", s.libraryItemID},
   156  		{internal.LibraryItemUpdateSession, s.libraryItemUpdateSession},
   157  		{internal.LibraryItemUpdateSession + "/", s.libraryItemUpdateSessionID},
   158  		{internal.LibraryItemUpdateSessionFile, s.libraryItemUpdateSessionFile},
   159  		{internal.LibraryItemUpdateSessionFile + "/", s.libraryItemUpdateSessionFileID},
   160  		{internal.LibraryItemDownloadSession, s.libraryItemDownloadSession},
   161  		{internal.LibraryItemDownloadSession + "/", s.libraryItemDownloadSessionID},
   162  		{internal.LibraryItemDownloadSessionFile, s.libraryItemDownloadSessionFile},
   163  		{internal.LibraryItemDownloadSessionFile + "/", s.libraryItemDownloadSessionFileID},
   164  		{internal.LibraryItemFileData + "/", s.libraryItemFileData},
   165  		{internal.LibraryItemFilePath, s.libraryItemFile},
   166  		{internal.LibraryItemFilePath + "/", s.libraryItemFileID},
   167  		{internal.VCenterOVFLibraryItem, s.libraryItemOVF},
   168  		{internal.VCenterOVFLibraryItem + "/", s.libraryItemOVFID},
   169  		{internal.VCenterVMTXLibraryItem, s.libraryItemCreateTemplate},
   170  		{internal.VCenterVMTXLibraryItem + "/", s.libraryItemTemplateID},
   171  		{internal.DebugEcho, s.debugEcho},
   172  		// /api/ patterns.
   173  		{internal.SecurityPoliciesPath, s.librarySecurityPolicies},
   174  		{internal.TrustedCertificatesPath, s.libraryTrustedCertificates},
   175  		{internal.TrustedCertificatesPath + "/", s.libraryTrustedCertificatesID},
   176  	}
   177  
   178  	for i := range handlers {
   179  		h := handlers[i]
   180  		s.HandleFunc(h.p, h.m)
   181  	}
   182  
   183  	return []string{rest.Path + "/", vapi.Path + "/"}, s
   184  }
   185  
   186  func (s *handler) withClient(f func(context.Context, *vim25.Client) error) error {
   187  	ctx := context.Background()
   188  	c, err := govmomi.NewClient(ctx, &s.URL, true)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	defer func() {
   193  		_ = c.Logout(ctx)
   194  	}()
   195  	return f(ctx, c.Client)
   196  }
   197  
   198  // HandleFunc wraps the given handler with authorization checks and passes to http.ServeMux.HandleFunc
   199  func (s *handler) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
   200  	// Rest paths have been moved from /rest/* to /api/*. Account for both the legacy and new cases here.
   201  	if !strings.HasPrefix(pattern, rest.Path) && !strings.HasPrefix(pattern, vapi.Path) {
   202  		pattern = rest.Path + pattern
   203  	}
   204  
   205  	s.ServeMux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
   206  		s.Lock()
   207  		defer s.Unlock()
   208  
   209  		if !s.isAuthorized(r) {
   210  			w.WriteHeader(http.StatusUnauthorized)
   211  			return
   212  		}
   213  
   214  		handler(w, r)
   215  	})
   216  }
   217  
   218  func (s *handler) isAuthorized(r *http.Request) bool {
   219  	if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, internal.SessionPath) && s.action(r) == "" {
   220  		return true
   221  	}
   222  	id := r.Header.Get(internal.SessionCookieName)
   223  	if id == "" {
   224  		if cookie, err := r.Cookie(internal.SessionCookieName); err == nil {
   225  			id = cookie.Value
   226  			r.Header.Set(internal.SessionCookieName, id)
   227  		}
   228  	}
   229  	info, ok := s.Session[id]
   230  	if ok {
   231  		info.LastAccessed = time.Now()
   232  	} else {
   233  		_, ok = s.Update[id]
   234  	}
   235  	return ok
   236  }
   237  
   238  func (s *handler) hasAuthorization(r *http.Request) (string, bool) {
   239  	u, p, ok := r.BasicAuth()
   240  	if ok { // user+pass auth
   241  		return u, s.sm.Authenticate(s.URL, &vim.Login{UserName: u, Password: p})
   242  	}
   243  	auth := r.Header.Get("Authorization")
   244  	return "TODO", strings.HasPrefix(auth, "SIGN ") // token auth
   245  }
   246  
   247  func (s *handler) findTag(e vim.VslmTagEntry) *tags.Tag {
   248  	for _, c := range s.Category {
   249  		if c.Name == e.ParentCategoryName {
   250  			for _, t := range s.Tag {
   251  				if t.Name == e.TagName && t.CategoryID == c.ID {
   252  					return t
   253  				}
   254  			}
   255  		}
   256  	}
   257  	return nil
   258  }
   259  
   260  // AttachedObjects is meant for internal use via simulator.Registry.tagManager
   261  func (s *handler) AttachedObjects(tag vim.VslmTagEntry) ([]vim.ManagedObjectReference, vim.BaseMethodFault) {
   262  	t := s.findTag(tag)
   263  	if t == nil {
   264  		return nil, new(vim.NotFound)
   265  	}
   266  	var ids []vim.ManagedObjectReference
   267  	for id := range s.Association[t.ID] {
   268  		ids = append(
   269  			ids,
   270  			vim.ManagedObjectReference{
   271  				Type:  id.Type,
   272  				Value: id.Value,
   273  			})
   274  	}
   275  	return ids, nil
   276  }
   277  
   278  // AttachedTags is meant for internal use via simulator.Registry.tagManager
   279  func (s *handler) AttachedTags(ref vim.ManagedObjectReference) ([]vim.VslmTagEntry, vim.BaseMethodFault) {
   280  	oid := internal.AssociatedObject{
   281  		Type:  ref.Type,
   282  		Value: ref.Value,
   283  	}
   284  	var tags []vim.VslmTagEntry
   285  	for id, objs := range s.Association {
   286  		if objs[oid] {
   287  			tag := s.Tag[id]
   288  			cat := s.Category[tag.CategoryID]
   289  			tags = append(tags, vim.VslmTagEntry{
   290  				TagName:            tag.Name,
   291  				ParentCategoryName: cat.Name,
   292  			})
   293  		}
   294  	}
   295  	return tags, nil
   296  }
   297  
   298  // AttachTag is meant for internal use via simulator.Registry.tagManager
   299  func (s *handler) AttachTag(ref vim.ManagedObjectReference, tag vim.VslmTagEntry) vim.BaseMethodFault {
   300  	t := s.findTag(tag)
   301  	if t == nil {
   302  		return new(vim.NotFound)
   303  	}
   304  	s.Association[t.ID][internal.AssociatedObject{
   305  		Type:  ref.Type,
   306  		Value: ref.Value,
   307  	}] = true
   308  	return nil
   309  }
   310  
   311  // DetachTag is meant for internal use via simulator.Registry.tagManager
   312  func (s *handler) DetachTag(id vim.ManagedObjectReference, tag vim.VslmTagEntry) vim.BaseMethodFault {
   313  	t := s.findTag(tag)
   314  	if t == nil {
   315  		return new(vim.NotFound)
   316  	}
   317  	delete(s.Association[t.ID], internal.AssociatedObject{
   318  		Type:  id.Type,
   319  		Value: id.Value,
   320  	})
   321  	return nil
   322  }
   323  
   324  // StatusOK responds with http.StatusOK and encodes val, if specified, to JSON
   325  // For use with "/api" endpoints.
   326  func StatusOK(w http.ResponseWriter, val ...interface{}) {
   327  	w.Header().Set("Content-Type", "application/json")
   328  	w.WriteHeader(http.StatusOK)
   329  	if len(val) == 0 {
   330  		return
   331  	}
   332  
   333  	err := json.NewEncoder(w).Encode(val[0])
   334  
   335  	if err != nil {
   336  		log.Panic(err)
   337  	}
   338  }
   339  
   340  // OK responds with http.StatusOK and encodes val, if specified, to JSON
   341  // For use with "/rest" endpoints where the response is a "value" wrapped structure.
   342  func OK(w http.ResponseWriter, val ...interface{}) {
   343  	if len(val) == 0 {
   344  		w.WriteHeader(http.StatusOK)
   345  		return
   346  	}
   347  
   348  	s := struct {
   349  		Value interface{} `json:"value,omitempty"`
   350  	}{
   351  		val[0],
   352  	}
   353  
   354  	StatusOK(w, s)
   355  }
   356  
   357  // BadRequest responds with http.StatusBadRequest and json encoded vAPI error of type kind.
   358  // For use with "/rest" endpoints where the response is a "value" wrapped structure.
   359  func BadRequest(w http.ResponseWriter, kind string) {
   360  	w.WriteHeader(http.StatusBadRequest)
   361  
   362  	err := json.NewEncoder(w).Encode(struct {
   363  		Type  string `json:"type"`
   364  		Value struct {
   365  			Messages []string `json:"messages,omitempty"`
   366  		} `json:"value,omitempty"`
   367  	}{
   368  		Type: kind,
   369  	})
   370  
   371  	if err != nil {
   372  		log.Panic(err)
   373  	}
   374  }
   375  
   376  // ApiErrorAlreadyExists responds with a REST error of type "ALREADY_EXISTS".
   377  // For use with "/api" endpoints.
   378  func ApiErrorAlreadyExists(w http.ResponseWriter) {
   379  	apiError(w, http.StatusBadRequest, "ALREADY_EXISTS")
   380  }
   381  
   382  // ApiErrorGeneral responds with a REST error of type "ERROR".
   383  // For use with "/api" endpoints.
   384  func ApiErrorGeneral(w http.ResponseWriter) {
   385  	apiError(w, http.StatusInternalServerError, "ERROR")
   386  }
   387  
   388  // ApiErrorInvalidArgument responds with a REST error of type "INVALID_ARGUMENT".
   389  // For use with "/api" endpoints.
   390  func ApiErrorInvalidArgument(w http.ResponseWriter) {
   391  	apiError(w, http.StatusBadRequest, "INVALID_ARGUMENT")
   392  }
   393  
   394  // ApiErrorNotAllowedInCurrentState responds with a REST error of type "NOT_ALLOWED_IN_CURRENT_STATE".
   395  // For use with "/api" endpoints.
   396  func ApiErrorNotAllowedInCurrentState(w http.ResponseWriter) {
   397  	apiError(w, http.StatusBadRequest, "NOT_ALLOWED_IN_CURRENT_STATE")
   398  }
   399  
   400  // ApiErrorNotFound responds with a REST error of type "NOT_FOUND".
   401  // For use with "/api" endpoints.
   402  func ApiErrorNotFound(w http.ResponseWriter) {
   403  	apiError(w, http.StatusNotFound, "NOT_FOUND")
   404  }
   405  
   406  // ApiErrorResourceInUse responds with a REST error of type "RESOURCE_IN_USE".
   407  // For use with "/api" endpoints.
   408  func ApiErrorResourceInUse(w http.ResponseWriter) {
   409  	apiError(w, http.StatusBadRequest, "RESOURCE_IN_USE")
   410  }
   411  
   412  // ApiErrorUnauthorized responds with a REST error of type "UNAUTHORIZED".
   413  // For use with "/api" endpoints.
   414  func ApiErrorUnauthorized(w http.ResponseWriter) {
   415  	apiError(w, http.StatusBadRequest, "UNAUTHORIZED")
   416  }
   417  
   418  // ApiErrorUnsupported responds with a REST error of type "UNSUPPORTED".
   419  // For use with "/api" endpoints.
   420  func ApiErrorUnsupported(w http.ResponseWriter) {
   421  	apiError(w, http.StatusBadRequest, "UNSUPPORTED")
   422  }
   423  
   424  func apiError(w http.ResponseWriter, statusCode int, errorType string) {
   425  	w.Header().Set("Content-Type", "application/json")
   426  	w.WriteHeader(statusCode)
   427  	w.Write([]byte(fmt.Sprintf(`{"error_type":"%s", "messages":[]}`, errorType)))
   428  }
   429  
   430  func (*handler) error(w http.ResponseWriter, err error) {
   431  	http.Error(w, err.Error(), http.StatusInternalServerError)
   432  	log.Print(err)
   433  }
   434  
   435  // ServeHTTP handles vAPI requests.
   436  func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   437  	switch r.Method {
   438  	case http.MethodPost, http.MethodDelete, http.MethodGet, http.MethodPatch, http.MethodPut:
   439  	default:
   440  		w.WriteHeader(http.StatusMethodNotAllowed)
   441  		return
   442  	}
   443  
   444  	h, _ := s.ServeMux.Handler(r)
   445  	h.ServeHTTP(w, r)
   446  }
   447  
   448  func (s *handler) decode(r *http.Request, w http.ResponseWriter, val interface{}) bool {
   449  	return Decode(r, w, val)
   450  }
   451  
   452  // Decode the request Body into val.
   453  // Returns true on success, otherwise false and sends the http.StatusBadRequest response.
   454  func Decode(r *http.Request, w http.ResponseWriter, val interface{}) bool {
   455  	defer r.Body.Close()
   456  	err := json.NewDecoder(r.Body).Decode(val)
   457  	if err != nil {
   458  		log.Printf("%s %s: %s", r.Method, r.RequestURI, err)
   459  		w.WriteHeader(http.StatusBadRequest)
   460  		return false
   461  	}
   462  	return true
   463  }
   464  
   465  func (s *handler) expiredSession(id string, now time.Time) bool {
   466  	expired := true
   467  	s.Lock()
   468  	session, ok := s.Session[id]
   469  	if ok {
   470  		expired = now.Sub(session.LastAccessed) > simulator.SessionIdleTimeout
   471  		if expired {
   472  			delete(s.Session, id)
   473  		}
   474  	}
   475  	s.Unlock()
   476  	return expired
   477  }
   478  
   479  func (s *handler) session(w http.ResponseWriter, r *http.Request) {
   480  	id := r.Header.Get(internal.SessionCookieName)
   481  	useHeaderAuthn := strings.ToLower(r.Header.Get(internal.UseHeaderAuthn))
   482  
   483  	switch r.Method {
   484  	case http.MethodPost:
   485  		if s.action(r) != "" {
   486  			if session, ok := s.Session[id]; ok {
   487  				OK(w, session)
   488  			} else {
   489  				w.WriteHeader(http.StatusUnauthorized)
   490  			}
   491  			return
   492  		}
   493  		user, ok := s.hasAuthorization(r)
   494  		if !ok {
   495  			w.WriteHeader(http.StatusUnauthorized)
   496  			return
   497  		}
   498  		id = uuid.New().String()
   499  		now := time.Now()
   500  		s.Session[id] = &rest.Session{User: user, Created: now, LastAccessed: now}
   501  		simulator.SessionIdleWatch(context.Background(), id, s.expiredSession)
   502  		if useHeaderAuthn != "true" {
   503  			http.SetCookie(w, &http.Cookie{
   504  				Name:  internal.SessionCookieName,
   505  				Value: id,
   506  				Path:  rest.Path,
   507  			})
   508  		}
   509  		OK(w, id)
   510  	case http.MethodDelete:
   511  		delete(s.Session, id)
   512  		OK(w)
   513  	case http.MethodGet:
   514  		OK(w, s.Session[id])
   515  	}
   516  }
   517  
   518  func (s *handler) action(r *http.Request) string {
   519  	return r.URL.Query().Get("~action")
   520  }
   521  
   522  func (s *handler) id(r *http.Request) string {
   523  	base := path.Base(r.URL.Path)
   524  	id := strings.TrimPrefix(base, "id:")
   525  	if id == base {
   526  		return "" // trigger 404 Not Found w/o id: prefix
   527  	}
   528  	return id
   529  }
   530  
   531  func newID(kind string) string {
   532  	return fmt.Sprintf("urn:vmomi:InventoryService%s:%s:GLOBAL", kind, uuid.New().String())
   533  }
   534  
   535  func (s *handler) category(w http.ResponseWriter, r *http.Request) {
   536  	switch r.Method {
   537  	case http.MethodPost:
   538  		var spec struct {
   539  			Category tags.Category `json:"create_spec"`
   540  		}
   541  		if s.decode(r, w, &spec) {
   542  			for _, category := range s.Category {
   543  				if category.Name == spec.Category.Name {
   544  					BadRequest(w, "com.vmware.vapi.std.errors.already_exists")
   545  					return
   546  				}
   547  			}
   548  			id := newID("Category")
   549  			spec.Category.ID = id
   550  			s.Category[id] = &spec.Category
   551  			OK(w, id)
   552  		}
   553  	case http.MethodGet:
   554  		var ids []string
   555  		for id := range s.Category {
   556  			ids = append(ids, id)
   557  		}
   558  
   559  		OK(w, ids)
   560  	}
   561  }
   562  
   563  func (s *handler) categoryID(w http.ResponseWriter, r *http.Request) {
   564  	id := s.id(r)
   565  
   566  	o, ok := s.Category[id]
   567  	if !ok {
   568  		http.NotFound(w, r)
   569  		return
   570  	}
   571  
   572  	switch r.Method {
   573  	case http.MethodDelete:
   574  		delete(s.Category, id)
   575  		for ix, tag := range s.Tag {
   576  			if tag.CategoryID == id {
   577  				delete(s.Tag, ix)
   578  				delete(s.Association, ix)
   579  			}
   580  		}
   581  		OK(w)
   582  	case http.MethodPatch:
   583  		var spec struct {
   584  			Category tags.Category `json:"update_spec"`
   585  		}
   586  		if s.decode(r, w, &spec) {
   587  			ntypes := len(spec.Category.AssociableTypes)
   588  			if ntypes != 0 {
   589  				// Validate that AssociableTypes is only appended to.
   590  				etypes := len(o.AssociableTypes)
   591  				fail := ntypes < etypes
   592  				if !fail {
   593  					fail = !reflect.DeepEqual(o.AssociableTypes, spec.Category.AssociableTypes[:etypes])
   594  				}
   595  				if fail {
   596  					BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
   597  					return
   598  				}
   599  			}
   600  			o.Patch(&spec.Category)
   601  			OK(w)
   602  		}
   603  	case http.MethodGet:
   604  		OK(w, o)
   605  	}
   606  }
   607  
   608  func (s *handler) tag(w http.ResponseWriter, r *http.Request) {
   609  	switch r.Method {
   610  	case http.MethodPost:
   611  		var spec struct {
   612  			Tag tags.Tag `json:"create_spec"`
   613  		}
   614  		if s.decode(r, w, &spec) {
   615  			for _, tag := range s.Tag {
   616  				if tag.Name == spec.Tag.Name && tag.CategoryID == spec.Tag.CategoryID {
   617  					BadRequest(w, "com.vmware.vapi.std.errors.already_exists")
   618  					return
   619  				}
   620  			}
   621  			id := newID("Tag")
   622  			spec.Tag.ID = id
   623  			s.Tag[id] = &spec.Tag
   624  			s.Association[id] = make(map[internal.AssociatedObject]bool)
   625  			OK(w, id)
   626  		}
   627  	case http.MethodGet:
   628  		var ids []string
   629  		for id := range s.Tag {
   630  			ids = append(ids, id)
   631  		}
   632  		OK(w, ids)
   633  	}
   634  }
   635  
   636  func (s *handler) tagID(w http.ResponseWriter, r *http.Request) {
   637  	id := s.id(r)
   638  
   639  	switch s.action(r) {
   640  	case "list-tags-for-category":
   641  		var ids []string
   642  		for _, tag := range s.Tag {
   643  			if tag.CategoryID == id {
   644  				ids = append(ids, tag.ID)
   645  			}
   646  		}
   647  		OK(w, ids)
   648  		return
   649  	}
   650  
   651  	o, ok := s.Tag[id]
   652  	if !ok {
   653  		log.Printf("tag not found: %s", id)
   654  		http.NotFound(w, r)
   655  		return
   656  	}
   657  
   658  	switch r.Method {
   659  	case http.MethodDelete:
   660  		delete(s.Tag, id)
   661  		delete(s.Association, id)
   662  		OK(w)
   663  	case http.MethodPatch:
   664  		var spec struct {
   665  			Tag tags.Tag `json:"update_spec"`
   666  		}
   667  		if s.decode(r, w, &spec) {
   668  			o.Patch(&spec.Tag)
   669  			OK(w)
   670  		}
   671  	case http.MethodGet:
   672  		OK(w, o)
   673  	}
   674  }
   675  
   676  // TODO: support cardinality checks
   677  func (s *handler) association(w http.ResponseWriter, r *http.Request) {
   678  	if r.Method != http.MethodPost {
   679  		w.WriteHeader(http.StatusMethodNotAllowed)
   680  		return
   681  	}
   682  
   683  	var spec struct {
   684  		internal.Association
   685  		TagIDs    []string                    `json:"tag_ids,omitempty"`
   686  		ObjectIDs []internal.AssociatedObject `json:"object_ids,omitempty"`
   687  	}
   688  	if !s.decode(r, w, &spec) {
   689  		return
   690  	}
   691  
   692  	switch s.action(r) {
   693  	case "list-attached-tags":
   694  		var ids []string
   695  		for id, objs := range s.Association {
   696  			if objs[*spec.ObjectID] {
   697  				ids = append(ids, id)
   698  			}
   699  		}
   700  		OK(w, ids)
   701  
   702  	case "list-attached-objects-on-tags":
   703  		var res []tags.AttachedObjects
   704  		for _, id := range spec.TagIDs {
   705  			o := tags.AttachedObjects{TagID: id}
   706  			for i := range s.Association[id] {
   707  				o.ObjectIDs = append(o.ObjectIDs, i)
   708  			}
   709  			res = append(res, o)
   710  		}
   711  		OK(w, res)
   712  
   713  	case "list-attached-tags-on-objects":
   714  		var res []tags.AttachedTags
   715  		for _, ref := range spec.ObjectIDs {
   716  			o := tags.AttachedTags{ObjectID: ref}
   717  			for id, objs := range s.Association {
   718  				if objs[ref] {
   719  					o.TagIDs = append(o.TagIDs, id)
   720  				}
   721  			}
   722  			res = append(res, o)
   723  		}
   724  		OK(w, res)
   725  
   726  	case "attach-multiple-tags-to-object":
   727  		// TODO: add check if target (moref) exist or return 403 as per API behavior
   728  
   729  		res := struct {
   730  			Success bool             `json:"success"`
   731  			Errors  tags.BatchErrors `json:"error_messages,omitempty"`
   732  		}{}
   733  
   734  		for _, id := range spec.TagIDs {
   735  			if _, exists := s.Association[id]; !exists {
   736  				log.Printf("association tag not found: %s", id)
   737  				res.Errors = append(res.Errors, tags.BatchError{
   738  					Type:    "cis.tagging.objectNotFound.error",
   739  					Message: fmt.Sprintf("Tagging object %s not found", id),
   740  				})
   741  			} else {
   742  				s.Association[id][*spec.ObjectID] = true
   743  			}
   744  		}
   745  
   746  		if len(res.Errors) == 0 {
   747  			res.Success = true
   748  		}
   749  		OK(w, res)
   750  
   751  	case "detach-multiple-tags-from-object":
   752  		// TODO: add check if target (moref) exist or return 403 as per API behavior
   753  
   754  		res := struct {
   755  			Success bool             `json:"success"`
   756  			Errors  tags.BatchErrors `json:"error_messages,omitempty"`
   757  		}{}
   758  
   759  		for _, id := range spec.TagIDs {
   760  			if _, exists := s.Association[id]; !exists {
   761  				log.Printf("association tag not found: %s", id)
   762  				res.Errors = append(res.Errors, tags.BatchError{
   763  					Type:    "cis.tagging.objectNotFound.error",
   764  					Message: fmt.Sprintf("Tagging object %s not found", id),
   765  				})
   766  			} else {
   767  				s.Association[id][*spec.ObjectID] = false
   768  			}
   769  		}
   770  
   771  		if len(res.Errors) == 0 {
   772  			res.Success = true
   773  		}
   774  		OK(w, res)
   775  	}
   776  }
   777  
   778  func (s *handler) associationID(w http.ResponseWriter, r *http.Request) {
   779  	if r.Method != http.MethodPost {
   780  		w.WriteHeader(http.StatusMethodNotAllowed)
   781  		return
   782  	}
   783  
   784  	id := s.id(r)
   785  	if _, exists := s.Association[id]; !exists {
   786  		log.Printf("association tag not found: %s", id)
   787  		http.NotFound(w, r)
   788  		return
   789  	}
   790  
   791  	var spec internal.Association
   792  	var specs struct {
   793  		ObjectIDs []internal.AssociatedObject `json:"object_ids"`
   794  	}
   795  	switch s.action(r) {
   796  	case "attach", "detach", "list-attached-objects":
   797  		if !s.decode(r, w, &spec) {
   798  			return
   799  		}
   800  	case "attach-tag-to-multiple-objects":
   801  		if !s.decode(r, w, &specs) {
   802  			return
   803  		}
   804  	}
   805  
   806  	switch s.action(r) {
   807  	case "attach":
   808  		s.Association[id][*spec.ObjectID] = true
   809  		OK(w)
   810  	case "detach":
   811  		delete(s.Association[id], *spec.ObjectID)
   812  		OK(w)
   813  	case "list-attached-objects":
   814  		var ids []internal.AssociatedObject
   815  		for id := range s.Association[id] {
   816  			ids = append(ids, id)
   817  		}
   818  		OK(w, ids)
   819  	case "attach-tag-to-multiple-objects":
   820  		for _, obj := range specs.ObjectIDs {
   821  			s.Association[id][obj] = true
   822  		}
   823  		OK(w)
   824  	}
   825  }
   826  
   827  func (s *handler) library(w http.ResponseWriter, r *http.Request) {
   828  	switch r.Method {
   829  	case http.MethodPost:
   830  		var spec struct {
   831  			Library library.Library `json:"create_spec"`
   832  			Find    library.Find    `json:"spec"`
   833  		}
   834  		if !s.decode(r, w, &spec) {
   835  			return
   836  		}
   837  
   838  		switch s.action(r) {
   839  		case "find":
   840  			var ids []string
   841  			for _, l := range s.Library {
   842  				if spec.Find.Type != "" {
   843  					if spec.Find.Type != l.Library.Type {
   844  						continue
   845  					}
   846  				}
   847  				if spec.Find.Name != "" {
   848  					if !strings.EqualFold(l.Library.Name, spec.Find.Name) {
   849  						continue
   850  					}
   851  				}
   852  				ids = append(ids, l.ID)
   853  			}
   854  			OK(w, ids)
   855  		case "":
   856  			if !s.isValidSecurityPolicy(spec.Library.SecurityPolicyID) {
   857  				http.NotFound(w, r)
   858  				return
   859  			}
   860  
   861  			id := uuid.New().String()
   862  			spec.Library.ID = id
   863  			spec.Library.CreationTime = types.NewTime(time.Now())
   864  			spec.Library.LastModifiedTime = types.NewTime(time.Now())
   865  			spec.Library.UnsetSecurityPolicyID = spec.Library.SecurityPolicyID == ""
   866  			dir := libraryPath(&spec.Library, "")
   867  			if err := os.Mkdir(dir, 0750); err != nil {
   868  				s.error(w, err)
   869  				return
   870  			}
   871  			s.Library[id] = &content{
   872  				Library: &spec.Library,
   873  				Item:    make(map[string]*item),
   874  				Subs:    make(map[string]*library.Subscriber),
   875  				VMTX:    make(map[string]*types.ManagedObjectReference),
   876  			}
   877  
   878  			pub := spec.Library.Publication
   879  			if pub != nil && pub.Published != nil && *pub.Published {
   880  				// Generate PublishURL as real vCenter does
   881  				pub.PublishURL = (&url.URL{
   882  					Scheme: s.URL.Scheme,
   883  					Host:   s.URL.Host,
   884  					Path:   "/cls/vcsp/lib/" + id,
   885  				}).String()
   886  			}
   887  
   888  			sub := spec.Library.Subscription
   889  			if sub != nil {
   890  				// Share the published Item map
   891  				pid := path.Base(sub.SubscriptionURL)
   892  				if p, ok := s.Library[pid]; ok {
   893  					s.Library[id].Item = p.Item
   894  				}
   895  			}
   896  
   897  			OK(w, id)
   898  		}
   899  	case http.MethodGet:
   900  		var ids []string
   901  		for id := range s.Library {
   902  			ids = append(ids, id)
   903  		}
   904  		OK(w, ids)
   905  	}
   906  }
   907  
   908  func (content *content) cached(val bool) {
   909  	for _, item := range content.Item {
   910  		item.cached(val)
   911  	}
   912  }
   913  
   914  func (item *item) cached(val bool) {
   915  	item.Cached = val
   916  	for _, file := range item.File {
   917  		file.Cached = types.NewBool(val)
   918  	}
   919  }
   920  
   921  func (s *handler) publish(w http.ResponseWriter, r *http.Request, sids []internal.SubscriptionDestination, l *content, vmtx *item) bool {
   922  	var ids []string
   923  	if len(sids) == 0 {
   924  		for sid := range l.Subs {
   925  			ids = append(ids, sid)
   926  		}
   927  	} else {
   928  		for _, dst := range sids {
   929  			ids = append(ids, dst.ID)
   930  		}
   931  	}
   932  
   933  	for _, sid := range ids {
   934  		sub, ok := l.Subs[sid]
   935  		if !ok {
   936  			log.Printf("library subscription not found: %s", sid)
   937  			http.NotFound(w, r)
   938  			return false
   939  		}
   940  
   941  		slib := s.Library[sub.LibraryID]
   942  		if slib.VMTX[vmtx.ID] != nil {
   943  			return true // already cloned
   944  		}
   945  
   946  		ds := &vcenter.DiskStorage{Datastore: l.Library.Storage[0].DatastoreID}
   947  		ref, err := s.cloneVM(vmtx.Template.Value, vmtx.Name, sub.Placement, ds)
   948  		if err != nil {
   949  			s.error(w, err)
   950  			return false
   951  		}
   952  
   953  		slib.VMTX[vmtx.ID] = ref
   954  	}
   955  
   956  	return true
   957  }
   958  
   959  func (s *handler) libraryID(w http.ResponseWriter, r *http.Request) {
   960  	id := s.id(r)
   961  	l, ok := s.Library[id]
   962  	if !ok {
   963  		log.Printf("library not found: %s", id)
   964  		http.NotFound(w, r)
   965  		return
   966  	}
   967  
   968  	switch r.Method {
   969  	case http.MethodDelete:
   970  		p := libraryPath(l.Library, "")
   971  		if err := os.RemoveAll(p); err != nil {
   972  			s.error(w, err)
   973  			return
   974  		}
   975  		for _, item := range l.Item {
   976  			s.deleteVM(item.Template)
   977  		}
   978  		delete(s.Library, id)
   979  		OK(w)
   980  	case http.MethodPatch:
   981  		var spec struct {
   982  			Library library.Library `json:"update_spec"`
   983  		}
   984  		if s.decode(r, w, &spec) {
   985  			l.Patch(&spec.Library)
   986  			OK(w)
   987  		}
   988  	case http.MethodPost:
   989  		switch s.action(r) {
   990  		case "publish":
   991  			var spec internal.SubscriptionDestinationSpec
   992  			if !s.decode(r, w, &spec) {
   993  				return
   994  			}
   995  			for _, item := range l.Item {
   996  				if item.Type != library.ItemTypeVMTX {
   997  					continue
   998  				}
   999  				if !s.publish(w, r, spec.Subscriptions, l, item) {
  1000  					return
  1001  				}
  1002  			}
  1003  			OK(w)
  1004  		case "sync":
  1005  			if l.Type == "SUBSCRIBED" {
  1006  				l.LastSyncTime = types.NewTime(time.Now())
  1007  				l.cached(true)
  1008  				OK(w)
  1009  			} else {
  1010  				http.NotFound(w, r)
  1011  			}
  1012  		case "evict":
  1013  			l.cached(false)
  1014  			OK(w)
  1015  		}
  1016  	case http.MethodGet:
  1017  		OK(w, l)
  1018  	}
  1019  }
  1020  
  1021  func (s *handler) subscriptions(w http.ResponseWriter, r *http.Request) {
  1022  	if r.Method != http.MethodGet {
  1023  		w.WriteHeader(http.StatusMethodNotAllowed)
  1024  		return
  1025  	}
  1026  
  1027  	id := r.URL.Query().Get("library")
  1028  	l, ok := s.Library[id]
  1029  	if !ok {
  1030  		log.Printf("library not found: %s", id)
  1031  		http.NotFound(w, r)
  1032  		return
  1033  	}
  1034  
  1035  	var res []library.SubscriberSummary
  1036  	for sid, slib := range l.Subs {
  1037  		res = append(res, library.SubscriberSummary{
  1038  			LibraryID:              slib.LibraryID,
  1039  			LibraryName:            slib.LibraryName,
  1040  			SubscriptionID:         sid,
  1041  			LibraryVcenterHostname: "",
  1042  		})
  1043  	}
  1044  	OK(w, res)
  1045  }
  1046  
  1047  func (s *handler) subscriptionsID(w http.ResponseWriter, r *http.Request) {
  1048  	id := s.id(r)
  1049  	l, ok := s.Library[id]
  1050  	if !ok {
  1051  		log.Printf("library not found: %s", id)
  1052  		http.NotFound(w, r)
  1053  		return
  1054  	}
  1055  
  1056  	switch s.action(r) {
  1057  	case "get":
  1058  		var dst internal.SubscriptionDestination
  1059  		if !s.decode(r, w, &dst) {
  1060  			return
  1061  		}
  1062  
  1063  		sub, ok := l.Subs[dst.ID]
  1064  		if !ok {
  1065  			log.Printf("library subscription not found: %s", dst.ID)
  1066  			http.NotFound(w, r)
  1067  			return
  1068  		}
  1069  
  1070  		OK(w, sub)
  1071  	case "delete":
  1072  		var dst internal.SubscriptionDestination
  1073  		if !s.decode(r, w, &dst) {
  1074  			return
  1075  		}
  1076  
  1077  		delete(l.Subs, dst.ID)
  1078  
  1079  		OK(w)
  1080  	case "create", "":
  1081  		var spec struct {
  1082  			Sub struct {
  1083  				SubscriberLibrary library.SubscriberLibrary `json:"subscribed_library"`
  1084  			} `json:"spec"`
  1085  		}
  1086  		if !s.decode(r, w, &spec) {
  1087  			return
  1088  		}
  1089  
  1090  		sub := spec.Sub.SubscriberLibrary
  1091  		slib, ok := s.Library[sub.LibraryID]
  1092  		if !ok {
  1093  			log.Printf("library not found: %s", sub.LibraryID)
  1094  			http.NotFound(w, r)
  1095  			return
  1096  		}
  1097  
  1098  		id := uuid.New().String()
  1099  		l.Subs[id] = &library.Subscriber{
  1100  			LibraryID:       slib.ID,
  1101  			LibraryName:     slib.Name,
  1102  			LibraryLocation: sub.Target,
  1103  			Placement:       sub.Placement,
  1104  			Vcenter:         sub.Vcenter,
  1105  		}
  1106  
  1107  		OK(w, id)
  1108  	}
  1109  }
  1110  
  1111  func (s *handler) libraryItem(w http.ResponseWriter, r *http.Request) {
  1112  	switch r.Method {
  1113  	case http.MethodPost:
  1114  		var spec struct {
  1115  			Item library.Item     `json:"create_spec"`
  1116  			Find library.FindItem `json:"spec"`
  1117  		}
  1118  		if !s.decode(r, w, &spec) {
  1119  			return
  1120  		}
  1121  
  1122  		switch s.action(r) {
  1123  		case "find":
  1124  			var ids []string
  1125  			for _, l := range s.Library {
  1126  				if spec.Find.LibraryID != "" {
  1127  					if spec.Find.LibraryID != l.ID {
  1128  						continue
  1129  					}
  1130  				}
  1131  				for _, i := range l.Item {
  1132  					if spec.Find.Name != "" {
  1133  						if spec.Find.Name != i.Name {
  1134  							continue
  1135  						}
  1136  					}
  1137  					if spec.Find.Type != "" {
  1138  						if spec.Find.Type != i.Type {
  1139  							continue
  1140  						}
  1141  					}
  1142  					ids = append(ids, i.ID)
  1143  				}
  1144  			}
  1145  			OK(w, ids)
  1146  		case "create", "":
  1147  			id := spec.Item.LibraryID
  1148  			l, ok := s.Library[id]
  1149  			if !ok {
  1150  				log.Printf("library not found: %s", id)
  1151  				http.NotFound(w, r)
  1152  				return
  1153  			}
  1154  			if l.Type == "SUBSCRIBED" {
  1155  				BadRequest(w, "com.vmware.vapi.std.errors.invalid_element_type")
  1156  				return
  1157  			}
  1158  			for _, item := range l.Item {
  1159  				if item.Name == spec.Item.Name {
  1160  					BadRequest(w, "com.vmware.vapi.std.errors.already_exists")
  1161  					return
  1162  				}
  1163  			}
  1164  			id = uuid.New().String()
  1165  			spec.Item.ID = id
  1166  			spec.Item.CreationTime = types.NewTime(time.Now())
  1167  			spec.Item.LastModifiedTime = types.NewTime(time.Now())
  1168  			if l.SecurityPolicyID != "" {
  1169  				// TODO: verify signed items
  1170  				spec.Item.SecurityCompliance = types.NewBool(false)
  1171  				spec.Item.CertificateVerification = &library.ItemCertificateVerification{
  1172  					Status: "NOT_AVAILABLE",
  1173  				}
  1174  			}
  1175  			l.Item[id] = &item{Item: &spec.Item}
  1176  			OK(w, id)
  1177  		}
  1178  	case http.MethodGet:
  1179  		id := r.URL.Query().Get("library_id")
  1180  		l, ok := s.Library[id]
  1181  		if !ok {
  1182  			log.Printf("library not found: %s", id)
  1183  			http.NotFound(w, r)
  1184  			return
  1185  		}
  1186  
  1187  		var ids []string
  1188  		for id := range l.Item {
  1189  			ids = append(ids, id)
  1190  		}
  1191  		OK(w, ids)
  1192  	}
  1193  }
  1194  
  1195  func (s *handler) libraryItemID(w http.ResponseWriter, r *http.Request) {
  1196  	id := s.id(r)
  1197  	lid := r.URL.Query().Get("library_id")
  1198  	if lid == "" {
  1199  		if l := s.itemLibrary(id); l != nil {
  1200  			lid = l.ID
  1201  		}
  1202  	}
  1203  	l, ok := s.Library[lid]
  1204  	if !ok {
  1205  		log.Printf("library not found: %q", lid)
  1206  		http.NotFound(w, r)
  1207  		return
  1208  	}
  1209  	item, ok := l.Item[id]
  1210  	if !ok {
  1211  		log.Printf("library item not found: %q", id)
  1212  		http.NotFound(w, r)
  1213  		return
  1214  	}
  1215  
  1216  	switch r.Method {
  1217  	case http.MethodDelete:
  1218  		p := libraryPath(l.Library, id)
  1219  		if err := os.RemoveAll(p); err != nil {
  1220  			s.error(w, err)
  1221  			return
  1222  		}
  1223  		s.deleteVM(l.Item[item.ID].Template)
  1224  		delete(l.Item, item.ID)
  1225  		OK(w)
  1226  	case http.MethodPatch:
  1227  		var spec struct {
  1228  			library.Item `json:"update_spec"`
  1229  		}
  1230  		if s.decode(r, w, &spec) {
  1231  			item.Patch(&spec.Item)
  1232  			OK(w)
  1233  		}
  1234  	case http.MethodPost:
  1235  		switch s.action(r) {
  1236  		case "copy":
  1237  			var spec struct {
  1238  				library.Item `json:"destination_create_spec"`
  1239  			}
  1240  			if !s.decode(r, w, &spec) {
  1241  				return
  1242  			}
  1243  
  1244  			l, ok = s.Library[spec.LibraryID]
  1245  			if !ok {
  1246  				log.Printf("library not found: %q", spec.LibraryID)
  1247  				http.NotFound(w, r)
  1248  				return
  1249  			}
  1250  			if spec.Name == "" {
  1251  				BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  1252  			}
  1253  
  1254  			id := uuid.New().String()
  1255  			nitem := item.cp()
  1256  			nitem.ID = id
  1257  			nitem.LibraryID = spec.LibraryID
  1258  			l.Item[id] = nitem
  1259  
  1260  			OK(w, id)
  1261  		case "sync":
  1262  			if l.Type == "SUBSCRIBED" || l.Publication != nil {
  1263  				item.LastSyncTime = types.NewTime(time.Now())
  1264  				item.cached(true)
  1265  				OK(w)
  1266  			} else {
  1267  				http.NotFound(w, r)
  1268  			}
  1269  		case "publish":
  1270  			var spec internal.SubscriptionDestinationSpec
  1271  			if s.decode(r, w, &spec) {
  1272  				if s.publish(w, r, spec.Subscriptions, l, item) {
  1273  					OK(w)
  1274  				}
  1275  			}
  1276  		case "evict":
  1277  			item.cached(false)
  1278  			OK(w, id)
  1279  		}
  1280  	case http.MethodGet:
  1281  		OK(w, item)
  1282  	}
  1283  }
  1284  
  1285  func (s *handler) libraryItemUpdateSession(w http.ResponseWriter, r *http.Request) {
  1286  	switch r.Method {
  1287  	case http.MethodGet:
  1288  		var ids []string
  1289  		for id := range s.Update {
  1290  			ids = append(ids, id)
  1291  		}
  1292  		OK(w, ids)
  1293  	case http.MethodPost:
  1294  		var spec struct {
  1295  			Session library.Session `json:"create_spec"`
  1296  		}
  1297  		if !s.decode(r, w, &spec) {
  1298  			return
  1299  		}
  1300  
  1301  		switch s.action(r) {
  1302  		case "create", "":
  1303  			lib := s.itemLibrary(spec.Session.LibraryItemID)
  1304  			if lib == nil {
  1305  				log.Printf("library for item %q not found", spec.Session.LibraryItemID)
  1306  				http.NotFound(w, r)
  1307  				return
  1308  			}
  1309  			session := &library.Session{
  1310  				ID:                        uuid.New().String(),
  1311  				LibraryItemID:             spec.Session.LibraryItemID,
  1312  				LibraryItemContentVersion: "1",
  1313  				ClientProgress:            0,
  1314  				State:                     "ACTIVE",
  1315  				ExpirationTime:            types.NewTime(time.Now().Add(time.Hour)),
  1316  			}
  1317  			s.Update[session.ID] = update{
  1318  				Session: session,
  1319  				Library: lib,
  1320  				File:    make(map[string]*library.UpdateFile),
  1321  			}
  1322  			OK(w, session.ID)
  1323  		}
  1324  	}
  1325  }
  1326  
  1327  func (s *handler) libraryItemUpdateSessionID(w http.ResponseWriter, r *http.Request) {
  1328  	id := s.id(r)
  1329  	up, ok := s.Update[id]
  1330  	if !ok {
  1331  		log.Printf("update session not found: %s", id)
  1332  		http.NotFound(w, r)
  1333  		return
  1334  	}
  1335  
  1336  	session := up.Session
  1337  	done := func(state string) {
  1338  		up.State = state
  1339  		go time.AfterFunc(session.ExpirationTime.Sub(time.Now()), func() {
  1340  			s.Lock()
  1341  			delete(s.Update, id)
  1342  			s.Unlock()
  1343  		})
  1344  	}
  1345  
  1346  	switch r.Method {
  1347  	case http.MethodGet:
  1348  		OK(w, session)
  1349  	case http.MethodPost:
  1350  		switch s.action(r) {
  1351  		case "cancel":
  1352  			done("CANCELED")
  1353  		case "complete":
  1354  			done("DONE")
  1355  		case "fail":
  1356  			done("ERROR")
  1357  		case "keep-alive":
  1358  			session.ExpirationTime = types.NewTime(time.Now().Add(time.Hour))
  1359  		}
  1360  		OK(w)
  1361  	case http.MethodDelete:
  1362  		delete(s.Update, id)
  1363  		OK(w)
  1364  	}
  1365  }
  1366  
  1367  func (s *handler) libraryItemProbe(endpoint library.TransferEndpoint) *library.ProbeResult {
  1368  	p := &library.ProbeResult{
  1369  		Status: "SUCCESS",
  1370  	}
  1371  
  1372  	result := func() *library.ProbeResult {
  1373  		for i, m := range p.ErrorMessages {
  1374  			p.ErrorMessages[i].DefaultMessage = fmt.Sprintf(m.DefaultMessage, m.Args[0])
  1375  		}
  1376  		return p
  1377  	}
  1378  
  1379  	u, err := url.Parse(endpoint.URI)
  1380  	if err != nil {
  1381  		p.Status = "INVALID_URL"
  1382  		p.ErrorMessages = []rest.LocalizableMessage{{
  1383  			Args:           []string{endpoint.URI},
  1384  			ID:             "com.vmware.vdcs.cls-main.invalid_url_format",
  1385  			DefaultMessage: "Invalid URL format for %s",
  1386  		}}
  1387  		return result()
  1388  	}
  1389  
  1390  	if u.Scheme != "http" && u.Scheme != "https" {
  1391  		p.Status = "INVALID_URL"
  1392  		p.ErrorMessages = []rest.LocalizableMessage{{
  1393  			Args:           []string{endpoint.URI},
  1394  			ID:             "com.vmware.vdcs.cls-main.file_probe_unsupported_uri_scheme",
  1395  			DefaultMessage: "The specified URI %s is not supported",
  1396  		}}
  1397  		return result()
  1398  	}
  1399  
  1400  	res, err := http.Head(endpoint.URI)
  1401  	if err != nil {
  1402  		id := "com.vmware.vdcs.cls-main.http_request_error"
  1403  		p.Status = "INVALID_URL"
  1404  
  1405  		if soap.IsCertificateUntrusted(err) {
  1406  			var info object.HostCertificateInfo
  1407  			_ = info.FromURL(u, nil)
  1408  
  1409  			id = "com.vmware.vdcs.cls-main.http_request_error_peer_not_authenticated"
  1410  			p.Status = "CERTIFICATE_ERROR"
  1411  			p.SSLThumbprint = info.ThumbprintSHA1
  1412  		}
  1413  
  1414  		p.ErrorMessages = []rest.LocalizableMessage{{
  1415  			Args:           []string{err.Error()},
  1416  			ID:             id,
  1417  			DefaultMessage: "HTTP request error: %s",
  1418  		}}
  1419  
  1420  		return result()
  1421  	}
  1422  	_ = res.Body.Close()
  1423  
  1424  	if res.TLS != nil {
  1425  		p.SSLThumbprint = soap.ThumbprintSHA1(res.TLS.PeerCertificates[0])
  1426  	}
  1427  
  1428  	return result()
  1429  }
  1430  
  1431  func (s *handler) libraryItemUpdateSessionFile(w http.ResponseWriter, r *http.Request) {
  1432  	switch r.Method {
  1433  	case http.MethodPost:
  1434  		switch s.action(r) {
  1435  		case "probe":
  1436  			var spec struct {
  1437  				SourceEndpoint library.TransferEndpoint `json:"source_endpoint"`
  1438  			}
  1439  			if s.decode(r, w, &spec) {
  1440  				res := s.libraryItemProbe(spec.SourceEndpoint)
  1441  				OK(w, res)
  1442  			}
  1443  		default:
  1444  			http.NotFound(w, r)
  1445  		}
  1446  		return
  1447  	case http.MethodGet:
  1448  	default:
  1449  		w.WriteHeader(http.StatusMethodNotAllowed)
  1450  		return
  1451  	}
  1452  
  1453  	id := r.URL.Query().Get("update_session_id")
  1454  	up, ok := s.Update[id]
  1455  	if !ok {
  1456  		log.Printf("update session not found: %s", id)
  1457  		http.NotFound(w, r)
  1458  		return
  1459  	}
  1460  
  1461  	var files []*library.UpdateFile
  1462  	for _, f := range up.File {
  1463  		files = append(files, f)
  1464  	}
  1465  	OK(w, files)
  1466  }
  1467  
  1468  func (s *handler) pullSource(up update, info *library.UpdateFile) {
  1469  	done := func(err error) {
  1470  		s.Lock()
  1471  		info.Status = "READY"
  1472  		if err != nil {
  1473  			log.Printf("PULL %s: %s", info.SourceEndpoint.URI, err)
  1474  			info.Status = "ERROR"
  1475  			up.State = "ERROR"
  1476  			up.ErrorMessage = &rest.LocalizableMessage{DefaultMessage: err.Error()}
  1477  		}
  1478  		s.Unlock()
  1479  	}
  1480  
  1481  	c := &http.Client{
  1482  		Transport: &http.Transport{
  1483  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  1484  		},
  1485  	}
  1486  
  1487  	res, err := c.Get(info.SourceEndpoint.URI)
  1488  	if err != nil {
  1489  		done(err)
  1490  		return
  1491  	}
  1492  
  1493  	err = s.libraryItemFileCreate(&up, info.Name, res.Body)
  1494  	done(err)
  1495  }
  1496  
  1497  func (s *handler) libraryItemUpdateSessionFileID(w http.ResponseWriter, r *http.Request) {
  1498  	if r.Method != http.MethodPost {
  1499  		w.WriteHeader(http.StatusMethodNotAllowed)
  1500  		return
  1501  	}
  1502  
  1503  	id := s.id(r)
  1504  	up, ok := s.Update[id]
  1505  	if !ok {
  1506  		log.Printf("update session not found: %s", id)
  1507  		http.NotFound(w, r)
  1508  		return
  1509  	}
  1510  
  1511  	switch s.action(r) {
  1512  	case "add":
  1513  		var spec struct {
  1514  			File library.UpdateFile `json:"file_spec"`
  1515  		}
  1516  		if s.decode(r, w, &spec) {
  1517  			id = uuid.New().String()
  1518  			info := &library.UpdateFile{
  1519  				Name:             spec.File.Name,
  1520  				SourceType:       spec.File.SourceType,
  1521  				Status:           "WAITING_FOR_TRANSFER",
  1522  				BytesTransferred: 0,
  1523  			}
  1524  			switch info.SourceType {
  1525  			case "PUSH":
  1526  				u := url.URL{
  1527  					Scheme: s.URL.Scheme,
  1528  					Host:   s.URL.Host,
  1529  					Path:   path.Join(rest.Path, internal.LibraryItemFileData, id, info.Name),
  1530  				}
  1531  				info.UploadEndpoint = &library.TransferEndpoint{URI: u.String()}
  1532  			case "PULL":
  1533  				info.SourceEndpoint = spec.File.SourceEndpoint
  1534  				go s.pullSource(up, info)
  1535  			}
  1536  			up.File[id] = info
  1537  			OK(w, info)
  1538  		}
  1539  	case "get":
  1540  		OK(w, up.Session)
  1541  	case "list":
  1542  		var ids []string
  1543  		for id := range up.File {
  1544  			ids = append(ids, id)
  1545  		}
  1546  		OK(w, ids)
  1547  	case "remove":
  1548  		if up.State != "ACTIVE" {
  1549  			s.error(w, fmt.Errorf("removeFile not allowed in state %s", up.State))
  1550  			return
  1551  		}
  1552  		delete(s.Update, id)
  1553  		OK(w)
  1554  	case "validate":
  1555  		if up.State != "ACTIVE" {
  1556  			BadRequest(w, "com.vmware.vapi.std.errors.not_allowed_in_current_state")
  1557  			return
  1558  		}
  1559  		var res library.UpdateFileValidation
  1560  		// TODO check missing_files, validate .ovf
  1561  		OK(w, res)
  1562  	}
  1563  }
  1564  
  1565  func (s *handler) libraryItemDownloadSession(w http.ResponseWriter, r *http.Request) {
  1566  	switch r.Method {
  1567  	case http.MethodGet:
  1568  		var ids []string
  1569  		for id := range s.Download {
  1570  			ids = append(ids, id)
  1571  		}
  1572  		OK(w, ids)
  1573  	case http.MethodPost:
  1574  		var spec struct {
  1575  			Session library.Session `json:"create_spec"`
  1576  		}
  1577  		if !s.decode(r, w, &spec) {
  1578  			return
  1579  		}
  1580  
  1581  		switch s.action(r) {
  1582  		case "create", "":
  1583  			var lib *library.Library
  1584  			var files []library.File
  1585  			for _, l := range s.Library {
  1586  				if item, ok := l.Item[spec.Session.LibraryItemID]; ok {
  1587  					lib = l.Library
  1588  					files = item.File
  1589  					break
  1590  				}
  1591  			}
  1592  			if lib == nil {
  1593  				log.Printf("library for item %q not found", spec.Session.LibraryItemID)
  1594  				http.NotFound(w, r)
  1595  				return
  1596  			}
  1597  			session := &library.Session{
  1598  				ID:                        uuid.New().String(),
  1599  				LibraryItemID:             spec.Session.LibraryItemID,
  1600  				LibraryItemContentVersion: "1",
  1601  				ClientProgress:            0,
  1602  				State:                     "ACTIVE",
  1603  				ExpirationTime:            types.NewTime(time.Now().Add(time.Hour)),
  1604  			}
  1605  			s.Download[session.ID] = download{
  1606  				Session: session,
  1607  				Library: lib,
  1608  				File:    make(map[string]*library.DownloadFile),
  1609  			}
  1610  			for _, file := range files {
  1611  				s.Download[session.ID].File[file.Name] = &library.DownloadFile{
  1612  					Name:   file.Name,
  1613  					Status: "UNPREPARED",
  1614  				}
  1615  			}
  1616  			OK(w, session.ID)
  1617  		}
  1618  	}
  1619  }
  1620  
  1621  func (s *handler) libraryItemDownloadSessionID(w http.ResponseWriter, r *http.Request) {
  1622  	id := s.id(r)
  1623  	up, ok := s.Download[id]
  1624  	if !ok {
  1625  		log.Printf("download session not found: %s", id)
  1626  		http.NotFound(w, r)
  1627  		return
  1628  	}
  1629  
  1630  	session := up.Session
  1631  	switch r.Method {
  1632  	case http.MethodGet:
  1633  		OK(w, session)
  1634  	case http.MethodPost:
  1635  		switch s.action(r) {
  1636  		case "cancel", "complete", "fail":
  1637  			delete(s.Download, id) // TODO: fully mock VC's behavior
  1638  		case "keep-alive":
  1639  			session.ExpirationTime = types.NewTime(time.Now().Add(time.Hour))
  1640  		}
  1641  		OK(w)
  1642  	case http.MethodDelete:
  1643  		delete(s.Download, id)
  1644  		OK(w)
  1645  	}
  1646  }
  1647  
  1648  func (s *handler) libraryItemDownloadSessionFile(w http.ResponseWriter, r *http.Request) {
  1649  	if r.Method != http.MethodGet {
  1650  		w.WriteHeader(http.StatusMethodNotAllowed)
  1651  		return
  1652  	}
  1653  
  1654  	id := r.URL.Query().Get("download_session_id")
  1655  	dl, ok := s.Download[id]
  1656  	if !ok {
  1657  		log.Printf("download session not found: %s", id)
  1658  		http.NotFound(w, r)
  1659  		return
  1660  	}
  1661  
  1662  	var files []*library.DownloadFile
  1663  	for _, f := range dl.File {
  1664  		files = append(files, f)
  1665  	}
  1666  	OK(w, files)
  1667  }
  1668  
  1669  func (s *handler) libraryItemDownloadSessionFileID(w http.ResponseWriter, r *http.Request) {
  1670  	if r.Method != http.MethodPost {
  1671  		w.WriteHeader(http.StatusMethodNotAllowed)
  1672  		return
  1673  	}
  1674  
  1675  	id := s.id(r)
  1676  	dl, ok := s.Download[id]
  1677  	if !ok {
  1678  		log.Printf("download session not found: %s", id)
  1679  		http.NotFound(w, r)
  1680  		return
  1681  	}
  1682  
  1683  	var spec struct {
  1684  		File string `json:"file_name"`
  1685  	}
  1686  
  1687  	switch s.action(r) {
  1688  	case "prepare":
  1689  		if s.decode(r, w, &spec) {
  1690  			u := url.URL{
  1691  				Scheme: s.URL.Scheme,
  1692  				Host:   s.URL.Host,
  1693  				Path:   path.Join(rest.Path, internal.LibraryItemFileData, id, spec.File),
  1694  			}
  1695  			info := &library.DownloadFile{
  1696  				Name:             spec.File,
  1697  				Status:           "PREPARED",
  1698  				BytesTransferred: 0,
  1699  				DownloadEndpoint: &library.TransferEndpoint{
  1700  					URI: u.String(),
  1701  				},
  1702  			}
  1703  			dl.File[spec.File] = info
  1704  			OK(w, info)
  1705  		}
  1706  	case "get":
  1707  		if s.decode(r, w, &spec) {
  1708  			OK(w, dl.File[spec.File])
  1709  		}
  1710  	}
  1711  }
  1712  
  1713  func (s *handler) itemLibrary(id string) *library.Library {
  1714  	for _, l := range s.Library {
  1715  		if _, ok := l.Item[id]; ok {
  1716  			return l.Library
  1717  		}
  1718  	}
  1719  	return nil
  1720  }
  1721  
  1722  func (s *handler) updateFileInfo(id string) *update {
  1723  	for _, up := range s.Update {
  1724  		for i := range up.File {
  1725  			if i == id {
  1726  				return &up
  1727  			}
  1728  		}
  1729  	}
  1730  	return nil
  1731  }
  1732  
  1733  // libraryPath returns the local Datastore fs path for a Library or Item if id is specified.
  1734  func libraryPath(l *library.Library, id string) string {
  1735  	dsref := types.ManagedObjectReference{
  1736  		Type:  "Datastore",
  1737  		Value: l.Storage[0].DatastoreID,
  1738  	}
  1739  	ds := simulator.Map.Get(dsref).(*simulator.Datastore)
  1740  
  1741  	return path.Join(append([]string{ds.Info.GetDatastoreInfo().Url, "contentlib-" + l.ID}, id)...)
  1742  }
  1743  
  1744  func (s *handler) libraryItemFileCreate(up *update, name string, body io.ReadCloser) error {
  1745  	var in io.Reader = body
  1746  	dir := libraryPath(up.Library, up.Session.LibraryItemID)
  1747  	if err := os.MkdirAll(dir, 0750); err != nil {
  1748  		return err
  1749  	}
  1750  
  1751  	if path.Ext(name) == ".ova" {
  1752  		// All we need is the .ovf, vcsim has no use for .vmdk or .mf
  1753  		r := tar.NewReader(body)
  1754  		for {
  1755  			h, err := r.Next()
  1756  			if err != nil {
  1757  				return err
  1758  			}
  1759  
  1760  			if path.Ext(h.Name) == ".ovf" {
  1761  				name = h.Name
  1762  				in = io.LimitReader(body, h.Size)
  1763  				break
  1764  			}
  1765  		}
  1766  	}
  1767  
  1768  	file, err := os.Create(path.Join(dir, name))
  1769  	if err != nil {
  1770  		return err
  1771  	}
  1772  
  1773  	n, err := io.Copy(file, in)
  1774  	_ = body.Close()
  1775  	if err != nil {
  1776  		return err
  1777  	}
  1778  	err = file.Close()
  1779  	if err != nil {
  1780  		return err
  1781  	}
  1782  
  1783  	i := s.Library[up.Library.ID].Item[up.Session.LibraryItemID]
  1784  	i.File = append(i.File, library.File{
  1785  		Cached:  types.NewBool(true),
  1786  		Name:    name,
  1787  		Size:    types.NewInt64(n),
  1788  		Version: "1",
  1789  	})
  1790  
  1791  	return nil
  1792  }
  1793  
  1794  func (s *handler) libraryItemFileData(w http.ResponseWriter, r *http.Request) {
  1795  	p := strings.Split(r.URL.Path, "/")
  1796  	id, name := p[len(p)-2], p[len(p)-1]
  1797  
  1798  	if r.Method == http.MethodGet {
  1799  		dl, ok := s.Download[id]
  1800  		if !ok {
  1801  			log.Printf("library download not found: %s", id)
  1802  			http.NotFound(w, r)
  1803  			return
  1804  		}
  1805  		p := path.Join(libraryPath(dl.Library, dl.Session.LibraryItemID), name)
  1806  		f, err := os.Open(p)
  1807  		if err != nil {
  1808  			s.error(w, err)
  1809  			return
  1810  		}
  1811  		_, err = io.Copy(w, f)
  1812  		if err != nil {
  1813  			log.Printf("copy %s: %s", p, err)
  1814  		}
  1815  		_ = f.Close()
  1816  		return
  1817  	}
  1818  
  1819  	if r.Method != http.MethodPut {
  1820  		w.WriteHeader(http.StatusMethodNotAllowed)
  1821  		return
  1822  	}
  1823  
  1824  	up := s.updateFileInfo(id)
  1825  	if up == nil {
  1826  		log.Printf("library update not found: %s", id)
  1827  		http.NotFound(w, r)
  1828  		return
  1829  	}
  1830  
  1831  	err := s.libraryItemFileCreate(up, name, r.Body)
  1832  	if err != nil {
  1833  		s.error(w, err)
  1834  	}
  1835  }
  1836  
  1837  func (s *handler) libraryItemFile(w http.ResponseWriter, r *http.Request) {
  1838  	id := r.URL.Query().Get("library_item_id")
  1839  	for _, l := range s.Library {
  1840  		if i, ok := l.Item[id]; ok {
  1841  			OK(w, i.File)
  1842  			return
  1843  		}
  1844  	}
  1845  	http.NotFound(w, r)
  1846  }
  1847  
  1848  func (s *handler) libraryItemFileID(w http.ResponseWriter, r *http.Request) {
  1849  	if r.Method != http.MethodPost {
  1850  		w.WriteHeader(http.StatusMethodNotAllowed)
  1851  		return
  1852  	}
  1853  	id := s.id(r)
  1854  	var spec struct {
  1855  		Name string `json:"name"`
  1856  	}
  1857  	if !s.decode(r, w, &spec) {
  1858  		return
  1859  	}
  1860  	for _, l := range s.Library {
  1861  		if i, ok := l.Item[id]; ok {
  1862  			for _, f := range i.File {
  1863  				if f.Name == spec.Name {
  1864  					OK(w, f)
  1865  					return
  1866  				}
  1867  			}
  1868  		}
  1869  	}
  1870  	http.NotFound(w, r)
  1871  }
  1872  
  1873  func (i *item) cp() *item {
  1874  	nitem := *i.Item
  1875  	return &item{&nitem, i.File, i.Template}
  1876  }
  1877  
  1878  func (i *item) ovf() string {
  1879  	for _, f := range i.File {
  1880  		if strings.HasSuffix(f.Name, ".ovf") {
  1881  			return f.Name
  1882  		}
  1883  	}
  1884  	return ""
  1885  }
  1886  
  1887  func vmConfigSpec(ctx context.Context, c *vim25.Client, deploy vcenter.Deploy) (*types.VirtualMachineConfigSpec, error) {
  1888  	if deploy.VmConfigSpec == nil {
  1889  		return nil, nil
  1890  	}
  1891  
  1892  	b, err := base64.StdEncoding.DecodeString(deploy.VmConfigSpec.XML)
  1893  	if err != nil {
  1894  		return nil, err
  1895  	}
  1896  
  1897  	var spec *types.VirtualMachineConfigSpec
  1898  
  1899  	dec := xml.NewDecoder(bytes.NewReader(b))
  1900  	dec.TypeFunc = c.Types
  1901  	err = dec.Decode(&spec)
  1902  	if err != nil {
  1903  		return nil, err
  1904  	}
  1905  
  1906  	return spec, nil
  1907  }
  1908  
  1909  func (s *handler) libraryDeploy(ctx context.Context, c *vim25.Client, lib *library.Library, item *item, deploy vcenter.Deploy) (*nfc.LeaseInfo, error) {
  1910  	config, err := vmConfigSpec(ctx, c, deploy)
  1911  	if err != nil {
  1912  		return nil, err
  1913  	}
  1914  
  1915  	name := item.ovf()
  1916  	desc, err := os.ReadFile(filepath.Join(libraryPath(lib, item.ID), name))
  1917  	if err != nil {
  1918  		return nil, err
  1919  	}
  1920  	ds := types.ManagedObjectReference{Type: "Datastore", Value: deploy.DeploymentSpec.DefaultDatastoreID}
  1921  	pool := types.ManagedObjectReference{Type: "ResourcePool", Value: deploy.Target.ResourcePoolID}
  1922  	var folder, host *types.ManagedObjectReference
  1923  	if deploy.Target.FolderID != "" {
  1924  		folder = &types.ManagedObjectReference{Type: "Folder", Value: deploy.Target.FolderID}
  1925  	}
  1926  	if deploy.Target.HostID != "" {
  1927  		host = &types.ManagedObjectReference{Type: "HostSystem", Value: deploy.Target.HostID}
  1928  	}
  1929  
  1930  	v, err := view.NewManager(c).CreateContainerView(ctx, c.ServiceContent.RootFolder, nil, true)
  1931  	if err != nil {
  1932  		return nil, err
  1933  	}
  1934  	defer func() {
  1935  		_ = v.Destroy(ctx)
  1936  	}()
  1937  	refs, err := v.Find(ctx, []string{"Network"}, nil)
  1938  	if err != nil {
  1939  		return nil, err
  1940  	}
  1941  
  1942  	var network []types.OvfNetworkMapping
  1943  	for _, net := range deploy.NetworkMappings {
  1944  		for i := range refs {
  1945  			if refs[i].Value == net.Value {
  1946  				network = append(network, types.OvfNetworkMapping{Name: net.Key, Network: refs[i]})
  1947  				break
  1948  			}
  1949  		}
  1950  	}
  1951  
  1952  	if ds.Value == "" {
  1953  		// Datastore is optional in the deploy spec, but not in OvfManager.CreateImportSpec
  1954  		refs, err = v.Find(ctx, []string{"Datastore"}, nil)
  1955  		if err != nil {
  1956  			return nil, err
  1957  		}
  1958  		// TODO: consider StorageProfileID
  1959  		ds = refs[0]
  1960  	}
  1961  
  1962  	cisp := types.OvfCreateImportSpecParams{
  1963  		DiskProvisioning: deploy.DeploymentSpec.StorageProvisioning,
  1964  		EntityName:       deploy.DeploymentSpec.Name,
  1965  		NetworkMapping:   network,
  1966  	}
  1967  
  1968  	for _, p := range deploy.AdditionalParams {
  1969  		switch p.Type {
  1970  		case vcenter.TypePropertyParams:
  1971  			for _, prop := range p.Properties {
  1972  				cisp.PropertyMapping = append(cisp.PropertyMapping, types.KeyValue{
  1973  					Key:   prop.ID,
  1974  					Value: prop.Value,
  1975  				})
  1976  			}
  1977  		case vcenter.TypeDeploymentOptionParams:
  1978  			cisp.OvfManagerCommonParams.DeploymentOption = p.SelectedKey
  1979  		}
  1980  	}
  1981  
  1982  	m := ovf.NewManager(c)
  1983  	spec, err := m.CreateImportSpec(ctx, string(desc), pool, ds, cisp)
  1984  	if err != nil {
  1985  		return nil, err
  1986  	}
  1987  	if spec.Error != nil {
  1988  		return nil, errors.New(spec.Error[0].LocalizedMessage)
  1989  	}
  1990  
  1991  	if config != nil {
  1992  		if vmImportSpec, ok := spec.ImportSpec.(*types.VirtualMachineImportSpec); ok {
  1993  			var configSpecs []types.BaseVirtualDeviceConfigSpec
  1994  
  1995  			// Remove devices that we don't want to carry over from the import spec. Otherwise, since we
  1996  			// just reconfigure the VM with the provided ConfigSpec later these devices won't be removed.
  1997  			for _, d := range vmImportSpec.ConfigSpec.DeviceChange {
  1998  				switch d.GetVirtualDeviceConfigSpec().Device.(type) {
  1999  				case types.BaseVirtualEthernetCard:
  2000  				default:
  2001  					configSpecs = append(configSpecs, d)
  2002  				}
  2003  			}
  2004  			vmImportSpec.ConfigSpec.DeviceChange = configSpecs
  2005  		}
  2006  	}
  2007  
  2008  	req := types.ImportVApp{
  2009  		This:   pool,
  2010  		Spec:   spec.ImportSpec,
  2011  		Folder: folder,
  2012  		Host:   host,
  2013  	}
  2014  	res, err := methods.ImportVApp(ctx, c, &req)
  2015  	if err != nil {
  2016  		return nil, err
  2017  	}
  2018  
  2019  	lease := nfc.NewLease(c, res.Returnval)
  2020  	info, err := lease.Wait(ctx, spec.FileItem)
  2021  	if err != nil {
  2022  		return nil, err
  2023  	}
  2024  
  2025  	if err = lease.Complete(ctx); err != nil {
  2026  		return nil, err
  2027  	}
  2028  
  2029  	if config != nil {
  2030  		if err = s.reconfigVM(info.Entity, *config); err != nil {
  2031  			return nil, err
  2032  		}
  2033  	}
  2034  
  2035  	return info, nil
  2036  }
  2037  
  2038  func (s *handler) libraryItemOVF(w http.ResponseWriter, r *http.Request) {
  2039  	if r.Method != http.MethodPost {
  2040  		w.WriteHeader(http.StatusMethodNotAllowed)
  2041  		return
  2042  	}
  2043  
  2044  	var req vcenter.OVF
  2045  	if !s.decode(r, w, &req) {
  2046  		return
  2047  	}
  2048  
  2049  	switch {
  2050  	case req.Target.LibraryItemID != "":
  2051  	case req.Target.LibraryID != "":
  2052  		l, ok := s.Library[req.Target.LibraryID]
  2053  		if !ok {
  2054  			http.NotFound(w, r)
  2055  		}
  2056  
  2057  		id := uuid.New().String()
  2058  		l.Item[id] = &item{
  2059  			Item: &library.Item{
  2060  				ID:               id,
  2061  				LibraryID:        l.Library.ID,
  2062  				Name:             req.Spec.Name,
  2063  				Description:      &req.Spec.Description,
  2064  				Type:             library.ItemTypeOVF,
  2065  				CreationTime:     types.NewTime(time.Now()),
  2066  				LastModifiedTime: types.NewTime(time.Now()),
  2067  			},
  2068  		}
  2069  
  2070  		res := vcenter.CreateResult{
  2071  			Succeeded: true,
  2072  			ID:        id,
  2073  		}
  2074  		OK(w, res)
  2075  	default:
  2076  		BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  2077  		return
  2078  	}
  2079  }
  2080  
  2081  func (s *handler) libraryItemOVFID(w http.ResponseWriter, r *http.Request) {
  2082  	if r.Method != http.MethodPost {
  2083  		w.WriteHeader(http.StatusMethodNotAllowed)
  2084  		return
  2085  	}
  2086  
  2087  	id := s.id(r)
  2088  	ok := false
  2089  	var lib *library.Library
  2090  	var item *item
  2091  	for _, l := range s.Library {
  2092  		if l.Library.Type == "SUBSCRIBED" {
  2093  			// Subscribers share the same Item map, we need the LOCAL library to find the .ovf on disk
  2094  			continue
  2095  		}
  2096  		item, ok = l.Item[id]
  2097  		if ok {
  2098  			lib = l.Library
  2099  			break
  2100  		}
  2101  	}
  2102  	if !ok {
  2103  		log.Printf("library item not found: %q", id)
  2104  		http.NotFound(w, r)
  2105  		return
  2106  	}
  2107  
  2108  	var spec struct {
  2109  		vcenter.Deploy
  2110  	}
  2111  	if !s.decode(r, w, &spec) {
  2112  		return
  2113  	}
  2114  
  2115  	switch s.action(r) {
  2116  	case "deploy":
  2117  		var d vcenter.Deployment
  2118  		err := s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2119  			info, err := s.libraryDeploy(ctx, c, lib, item, spec.Deploy)
  2120  			if err != nil {
  2121  				return err
  2122  			}
  2123  			id := vcenter.ResourceID{
  2124  				Type:  info.Entity.Type,
  2125  				Value: info.Entity.Value,
  2126  			}
  2127  			d.Succeeded = true
  2128  			d.ResourceID = &id
  2129  			return nil
  2130  		})
  2131  		if err != nil {
  2132  			d.Error = &vcenter.DeploymentError{
  2133  				Errors: []vcenter.OVFError{{
  2134  					Category: "SERVER",
  2135  					Error: &vcenter.Error{
  2136  						Class: "com.vmware.vapi.std.errors.error",
  2137  						Messages: []rest.LocalizableMessage{
  2138  							{
  2139  								DefaultMessage: err.Error(),
  2140  							},
  2141  						},
  2142  					},
  2143  				}},
  2144  			}
  2145  		}
  2146  		OK(w, d)
  2147  	case "filter":
  2148  		res := vcenter.FilterResponse{
  2149  			Name: item.Name,
  2150  		}
  2151  		OK(w, res)
  2152  	default:
  2153  		http.NotFound(w, r)
  2154  	}
  2155  }
  2156  
  2157  func (s *handler) deleteVM(ref *types.ManagedObjectReference) {
  2158  	if ref == nil {
  2159  		return
  2160  	}
  2161  	_ = s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2162  		_, _ = object.NewVirtualMachine(c, *ref).Destroy(ctx)
  2163  		return nil
  2164  	})
  2165  }
  2166  
  2167  func (s *handler) reconfigVM(ref types.ManagedObjectReference, config types.VirtualMachineConfigSpec) error {
  2168  	return s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2169  		vm := object.NewVirtualMachine(c, ref)
  2170  		task, err := vm.Reconfigure(ctx, config)
  2171  		if err != nil {
  2172  			return err
  2173  		}
  2174  		return task.Wait(ctx)
  2175  	})
  2176  }
  2177  
  2178  func (s *handler) cloneVM(source string, name string, p *library.Placement, storage *vcenter.DiskStorage) (*types.ManagedObjectReference, error) {
  2179  	var folder, pool, host, ds *types.ManagedObjectReference
  2180  	if p.Folder != "" {
  2181  		folder = &types.ManagedObjectReference{Type: "Folder", Value: p.Folder}
  2182  	}
  2183  	if p.ResourcePool != "" {
  2184  		pool = &types.ManagedObjectReference{Type: "ResourcePool", Value: p.ResourcePool}
  2185  	}
  2186  	if p.Host != "" {
  2187  		host = &types.ManagedObjectReference{Type: "HostSystem", Value: p.Host}
  2188  	}
  2189  	if storage != nil {
  2190  		if storage.Datastore != "" {
  2191  			ds = &types.ManagedObjectReference{Type: "Datastore", Value: storage.Datastore}
  2192  		}
  2193  	}
  2194  
  2195  	spec := types.VirtualMachineCloneSpec{
  2196  		Template: true,
  2197  		Location: types.VirtualMachineRelocateSpec{
  2198  			Folder:    folder,
  2199  			Pool:      pool,
  2200  			Host:      host,
  2201  			Datastore: ds,
  2202  		},
  2203  	}
  2204  
  2205  	var ref *types.ManagedObjectReference
  2206  
  2207  	return ref, s.withClient(func(ctx context.Context, c *vim25.Client) error {
  2208  		vm := object.NewVirtualMachine(c, types.ManagedObjectReference{Type: "VirtualMachine", Value: source})
  2209  
  2210  		task, err := vm.Clone(ctx, object.NewFolder(c, *folder), name, spec)
  2211  		if err != nil {
  2212  			return err
  2213  		}
  2214  		res, err := task.WaitForResult(ctx, nil)
  2215  		if err != nil {
  2216  			return err
  2217  		}
  2218  		ref = types.NewReference(res.Result.(types.ManagedObjectReference))
  2219  		return nil
  2220  	})
  2221  }
  2222  
  2223  func (s *handler) libraryItemCreateTemplate(w http.ResponseWriter, r *http.Request) {
  2224  	if r.Method != http.MethodPost {
  2225  		w.WriteHeader(http.StatusMethodNotAllowed)
  2226  		return
  2227  	}
  2228  
  2229  	var spec struct {
  2230  		vcenter.Template `json:"spec"`
  2231  	}
  2232  	if !s.decode(r, w, &spec) {
  2233  		return
  2234  	}
  2235  
  2236  	l, ok := s.Library[spec.Library]
  2237  	if !ok {
  2238  		http.NotFound(w, r)
  2239  		return
  2240  	}
  2241  
  2242  	ds := &vcenter.DiskStorage{Datastore: l.Library.Storage[0].DatastoreID}
  2243  	ref, err := s.cloneVM(spec.SourceVM, spec.Name, spec.Placement, ds)
  2244  	if err != nil {
  2245  		BadRequest(w, err.Error())
  2246  		return
  2247  	}
  2248  
  2249  	id := uuid.New().String()
  2250  	l.Item[id] = &item{
  2251  		Item: &library.Item{
  2252  			ID:               id,
  2253  			LibraryID:        l.Library.ID,
  2254  			Name:             spec.Name,
  2255  			Type:             library.ItemTypeVMTX,
  2256  			CreationTime:     types.NewTime(time.Now()),
  2257  			LastModifiedTime: types.NewTime(time.Now()),
  2258  		},
  2259  		Template: ref,
  2260  	}
  2261  
  2262  	OK(w, id)
  2263  }
  2264  
  2265  func (s *handler) libraryItemTemplateID(w http.ResponseWriter, r *http.Request) {
  2266  	// Go's ServeMux doesn't support wildcard matching, hacking around that for now to support
  2267  	// CheckOuts, e.g. "/vcenter/vm-template/library-items/{item}/check-outs/{vm}?action=check-in"
  2268  	p := strings.TrimPrefix(r.URL.Path, rest.Path+internal.VCenterVMTXLibraryItem+"/")
  2269  	route := strings.Split(p, "/")
  2270  	if len(route) == 0 {
  2271  		http.NotFound(w, r)
  2272  		return
  2273  	}
  2274  
  2275  	id := route[0]
  2276  	ok := false
  2277  
  2278  	var item *item
  2279  	for _, l := range s.Library {
  2280  		item, ok = l.Item[id]
  2281  		if ok {
  2282  			break
  2283  		}
  2284  	}
  2285  	if !ok {
  2286  		log.Printf("library item not found: %q", id)
  2287  		http.NotFound(w, r)
  2288  		return
  2289  	}
  2290  
  2291  	if item.Type != library.ItemTypeVMTX {
  2292  		BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  2293  		return
  2294  	}
  2295  
  2296  	if len(route) > 1 {
  2297  		switch route[1] {
  2298  		case "check-outs":
  2299  			s.libraryItemCheckOuts(item, w, r)
  2300  			return
  2301  		default:
  2302  			http.NotFound(w, r)
  2303  			return
  2304  		}
  2305  	}
  2306  
  2307  	if r.Method == http.MethodGet {
  2308  		// TODO: add mock data
  2309  		t := &vcenter.TemplateInfo{}
  2310  		OK(w, t)
  2311  		return
  2312  	}
  2313  
  2314  	var spec struct {
  2315  		vcenter.DeployTemplate `json:"spec"`
  2316  	}
  2317  	if !s.decode(r, w, &spec) {
  2318  		return
  2319  	}
  2320  
  2321  	switch r.URL.Query().Get("action") {
  2322  	case "deploy":
  2323  		p := spec.Placement
  2324  		if p == nil {
  2325  			BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  2326  			return
  2327  		}
  2328  		if p.Cluster == "" && p.Host == "" && p.ResourcePool == "" {
  2329  			BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
  2330  			return
  2331  		}
  2332  
  2333  		item.cached(true)
  2334  		ref, err := s.cloneVM(item.Template.Value, spec.Name, p, spec.DiskStorage)
  2335  		if err != nil {
  2336  			BadRequest(w, err.Error())
  2337  			return
  2338  		}
  2339  		OK(w, ref.Value)
  2340  	default:
  2341  		http.NotFound(w, r)
  2342  	}
  2343  }
  2344  
  2345  func (s *handler) libraryItemCheckOuts(item *item, w http.ResponseWriter, r *http.Request) {
  2346  	switch r.URL.Query().Get("action") {
  2347  	case "check-out":
  2348  		var spec struct {
  2349  			*vcenter.CheckOut `json:"spec"`
  2350  		}
  2351  		if !s.decode(r, w, &spec) {
  2352  			return
  2353  		}
  2354  
  2355  		ref, err := s.cloneVM(item.Template.Value, spec.Name, spec.Placement, nil)
  2356  		if err != nil {
  2357  			BadRequest(w, err.Error())
  2358  			return
  2359  		}
  2360  		OK(w, ref.Value)
  2361  	case "check-in":
  2362  		// TODO: increment ContentVersion
  2363  		OK(w, "0")
  2364  	default:
  2365  		http.NotFound(w, r)
  2366  	}
  2367  }
  2368  
  2369  // defaultSecurityPolicies generates the initial set of security policies always present on vCenter.
  2370  func defaultSecurityPolicies() []library.ContentSecurityPoliciesInfo {
  2371  	policyID, _ := uuid.NewUUID()
  2372  	return []library.ContentSecurityPoliciesInfo{
  2373  		{
  2374  			ItemTypeRules: map[string]string{
  2375  				"ovf": "OVF_STRICT_VERIFICATION",
  2376  			},
  2377  			Name:   "OVF default policy",
  2378  			Policy: policyID.String(),
  2379  		},
  2380  	}
  2381  }
  2382  
  2383  func (s *handler) librarySecurityPolicies(w http.ResponseWriter, r *http.Request) {
  2384  	switch r.Method {
  2385  	case http.MethodGet:
  2386  		StatusOK(w, s.Policies)
  2387  	default:
  2388  		w.WriteHeader(http.StatusMethodNotAllowed)
  2389  	}
  2390  }
  2391  
  2392  func (s *handler) isValidSecurityPolicy(policy string) bool {
  2393  	if policy == "" {
  2394  		return true
  2395  	}
  2396  
  2397  	for _, p := range s.Policies {
  2398  		if p.Policy == policy {
  2399  			return true
  2400  		}
  2401  	}
  2402  	return false
  2403  }
  2404  
  2405  func (s *handler) libraryTrustedCertificates(w http.ResponseWriter, r *http.Request) {
  2406  	switch r.Method {
  2407  	case http.MethodGet:
  2408  		var res struct {
  2409  			Certificates []library.TrustedCertificateSummary `json:"certificates"`
  2410  		}
  2411  		for id, cert := range s.Trust {
  2412  			res.Certificates = append(res.Certificates, library.TrustedCertificateSummary{
  2413  				TrustedCertificate: cert,
  2414  				ID:                 id,
  2415  			})
  2416  		}
  2417  
  2418  		StatusOK(w, &res)
  2419  	case http.MethodPost:
  2420  		var info library.TrustedCertificate
  2421  		if s.decode(r, w, &info) {
  2422  			block, _ := pem.Decode([]byte(info.Text))
  2423  			if block == nil {
  2424  				s.error(w, errors.New("invalid certificate"))
  2425  				return
  2426  			}
  2427  			_, err := x509.ParseCertificate(block.Bytes)
  2428  			if err != nil {
  2429  				s.error(w, err)
  2430  				return
  2431  			}
  2432  
  2433  			id := uuid.New().String()
  2434  			for x, cert := range s.Trust {
  2435  				if info.Text == cert.Text {
  2436  					id = x // existing certificate
  2437  					break
  2438  				}
  2439  			}
  2440  			s.Trust[id] = info
  2441  
  2442  			w.WriteHeader(http.StatusCreated)
  2443  		}
  2444  	default:
  2445  		w.WriteHeader(http.StatusMethodNotAllowed)
  2446  	}
  2447  }
  2448  
  2449  func (s *handler) libraryTrustedCertificatesID(w http.ResponseWriter, r *http.Request) {
  2450  	id := path.Base(r.URL.Path)
  2451  	cert, ok := s.Trust[id]
  2452  	if !ok {
  2453  		http.NotFound(w, r)
  2454  		return
  2455  	}
  2456  
  2457  	switch r.Method {
  2458  	case http.MethodGet:
  2459  		StatusOK(w, &cert)
  2460  	case http.MethodDelete:
  2461  		delete(s.Trust, id)
  2462  	default:
  2463  		w.WriteHeader(http.StatusMethodNotAllowed)
  2464  	}
  2465  }
  2466  
  2467  func (s *handler) debugEcho(w http.ResponseWriter, r *http.Request) {
  2468  	r.Write(w)
  2469  }