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 }