github.com/vmware/govmomi@v0.37.1/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 }