github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/catalog.go (about)

     1  /*
     2   * Copyright 2021 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     3   */
     4  
     5  package govcd
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/xml"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"math"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"path/filepath"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    24  	"github.com/vmware/go-vcloud-director/v2/util"
    25  )
    26  
    27  const (
    28  	defaultPieceSize int64 = 1024 * 1024
    29  )
    30  
    31  type Catalog struct {
    32  	Catalog *types.Catalog
    33  	client  *Client
    34  	parent  organization
    35  }
    36  
    37  func NewCatalog(client *Client) *Catalog {
    38  	return &Catalog{
    39  		Catalog: new(types.Catalog),
    40  		client:  client,
    41  	}
    42  }
    43  
    44  // Delete deletes the Catalog, returning an error if the vCD call fails.
    45  // Link to API call: https://code.vmware.com/apis/1046/vmware-cloud-director/doc/doc/operations/DELETE-Catalog.html
    46  func (catalog *Catalog) Delete(force, recursive bool) error {
    47  
    48  	adminCatalogHREF := catalog.client.VCDHREF
    49  	catalogID, err := getBareEntityUuid(catalog.Catalog.ID)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	if catalogID == "" {
    54  		return fmt.Errorf("empty ID returned for catalog %s", catalog.Catalog.Name)
    55  	}
    56  	adminCatalogHREF.Path += "/admin/catalog/" + catalogID
    57  
    58  	if force && recursive {
    59  		// A catalog cannot be removed if it has active tasks, or if any of its items have active tasks
    60  		err = catalog.consumeTasks()
    61  		if err != nil {
    62  			return fmt.Errorf("error while consuming tasks from catalog %s: %s", catalog.Catalog.Name, err)
    63  		}
    64  	}
    65  
    66  	req := catalog.client.NewRequest(map[string]string{
    67  		"force":     strconv.FormatBool(force),
    68  		"recursive": strconv.FormatBool(recursive),
    69  	}, http.MethodDelete, adminCatalogHREF, nil)
    70  
    71  	resp, err := checkResp(catalog.client.Http.Do(req))
    72  	if err != nil {
    73  		return fmt.Errorf("error deleting Catalog %s: %s", catalog.Catalog.Name, err)
    74  	}
    75  	task := NewTask(catalog.client)
    76  	if err = decodeBody(types.BodyTypeXML, resp, task.Task); err != nil {
    77  		return fmt.Errorf("error decoding task response: %s", err)
    78  	}
    79  	if task.Task.Status == "error" {
    80  		return fmt.Errorf(combinedTaskErrorMessage(task.Task, fmt.Errorf("catalog %s not properly destroyed", catalog.Catalog.Name)))
    81  	}
    82  	return task.WaitTaskCompletion()
    83  }
    84  
    85  // consumeTasks will cancel all catalog tasks and the ones related to its items
    86  // 1. cancel all tasks associated with the catalog and keep them in a list
    87  // 2. find a list of all catalog items
    88  // 3. find a list of all tasks associated with the organization, with name = "syncCatalogItem" or "createCatalogItem"
    89  // 4. loop through the tasks until we find the ones that belong to one of the items - add them to list in 1.
    90  // 5. cancel all the filtered tasks
    91  // 6. wait for the task list until all are finished
    92  func (catalog *Catalog) consumeTasks() error {
    93  	allTasks, err := catalog.client.QueryTaskList(map[string]string{
    94  		"status": "running,preRunning,queued",
    95  	})
    96  	if err != nil {
    97  		return fmt.Errorf("error getting task list from catalog %s: %s", catalog.Catalog.Name, err)
    98  	}
    99  	var taskList []string
   100  	addTask := func(status, href string) {
   101  		if status != "success" && status != "error" && status != "aborted" {
   102  			quickTask := Task{
   103  				client: catalog.client,
   104  				Task: &types.Task{
   105  					HREF: href,
   106  				},
   107  			}
   108  			err = quickTask.CancelTask()
   109  			if err != nil {
   110  				util.Logger.Printf("[consumeTasks] error canceling task: %s\n", err)
   111  			}
   112  			taskList = append(taskList, extractUuid(href))
   113  		}
   114  	}
   115  	if catalog.Catalog.Tasks != nil && len(catalog.Catalog.Tasks.Task) > 0 {
   116  		for _, task := range catalog.Catalog.Tasks.Task {
   117  			addTask(task.Status, task.HREF)
   118  		}
   119  	}
   120  	catalogItemRefs, err := catalog.QueryCatalogItemList()
   121  	if err != nil {
   122  		return fmt.Errorf("error getting catalog %s items list: %s", catalog.Catalog.Name, err)
   123  	}
   124  	for _, task := range allTasks {
   125  		for _, ref := range catalogItemRefs {
   126  			catalogItemId := extractUuid(ref.HREF)
   127  			if extractUuid(task.Object) == catalogItemId {
   128  				addTask(task.Status, task.HREF)
   129  				// No break here: the same object can have more than one task
   130  			}
   131  		}
   132  	}
   133  	_, err = catalog.client.WaitTaskListCompletion(taskList, true)
   134  	if err != nil {
   135  		return fmt.Errorf("error while waiting for task list completion for catalog %s: %s", catalog.Catalog.Name, err)
   136  	}
   137  	return nil
   138  }
   139  
   140  // Envelope is a ovf description root element. File contains information for vmdk files.
   141  // Namespace: http://schemas.dmtf.org/ovf/envelope/1
   142  // Description: Envelope is a ovf description root element. File contains information for vmdk files..
   143  type Envelope struct {
   144  	File []struct {
   145  		HREF      string `xml:"href,attr"`
   146  		ID        string `xml:"id,attr"`
   147  		Size      int    `xml:"size,attr"`
   148  		ChunkSize int    `xml:"chunkSize,attr"`
   149  	} `xml:"References>File"`
   150  }
   151  
   152  // If catalog item is a valid CatalogItem and the call succeeds,
   153  // then the function returns a CatalogItem. If the item does not
   154  // exist, then it returns an empty CatalogItem. If the call fails
   155  // at any point, it returns an error.
   156  // Deprecated: use GetCatalogItemByName instead
   157  func (cat *Catalog) FindCatalogItem(catalogItemName string) (CatalogItem, error) {
   158  	for _, catalogItems := range cat.Catalog.CatalogItems {
   159  		for _, catalogItem := range catalogItems.CatalogItem {
   160  			if catalogItem.Name == catalogItemName && catalogItem.Type == "application/vnd.vmware.vcloud.catalogItem+xml" {
   161  
   162  				cat := NewCatalogItem(cat.client)
   163  
   164  				_, err := cat.client.ExecuteRequest(catalogItem.HREF, http.MethodGet,
   165  					"", "error retrieving catalog: %s", nil, cat.CatalogItem)
   166  				return *cat, err
   167  			}
   168  		}
   169  	}
   170  
   171  	return CatalogItem{}, nil
   172  }
   173  
   174  // UploadOvf uploads an ova/ovf file to a catalog. This method only uploads bits to vCD spool area.
   175  // ovaFileName should be the path of OVA or OVF file(not ovf folder) itself. For OVF,
   176  // user need to make sure all the files that OVF depends on exist and locate under the same folder.
   177  // Returns errors if any occur during upload from vCD or upload process. On upload fail client may need to
   178  // remove vCD catalog item which waits for files to be uploaded. Files from ova are extracted to system
   179  // temp folder "govcd+random number" and left for inspection on error.
   180  func (cat *Catalog) UploadOvf(ovaFileName, itemName, description string, uploadPieceSize int64) (UploadTask, error) {
   181  
   182  	//	On a very high level the flow is as follows
   183  	//	1. Makes a POST call to vCD to create the catalog item (also creates a transfer folder in the spool area and as result will give a sparse catalog item resource XML).
   184  	//	2. Wait for the links to the transfer folder to appear in the resource representation of the catalog item.
   185  	//	3. Start uploading bits to the transfer folder
   186  	//	4. Wait on the import task to finish on vCD side -> task success = upload complete
   187  
   188  	if *cat == (Catalog{}) {
   189  		return UploadTask{}, errors.New("catalog can not be empty or nil")
   190  	}
   191  
   192  	ovaFileName, err := validateAndFixFilePath(ovaFileName)
   193  	if err != nil {
   194  		return UploadTask{}, err
   195  	}
   196  
   197  	for _, catalogItemName := range getExistingCatalogItems(cat) {
   198  		if catalogItemName == itemName {
   199  			return UploadTask{}, fmt.Errorf("catalog item '%s' already exists. Upload with different name", itemName)
   200  		}
   201  	}
   202  
   203  	isOvf := false
   204  	fileContentType, err := util.GetFileContentType(ovaFileName)
   205  	if err != nil {
   206  		return UploadTask{}, err
   207  	}
   208  	if strings.Contains(fileContentType, "text/xml") {
   209  		isOvf = true
   210  	}
   211  	ovfFilePath := ovaFileName
   212  	tmpDir := path.Dir(ovaFileName)
   213  	filesAbsPaths := []string{ovfFilePath}
   214  	if !isOvf {
   215  		filesAbsPaths, tmpDir, err = util.Unpack(ovaFileName)
   216  		if err != nil {
   217  			return UploadTask{}, fmt.Errorf("%s. Unpacked files for checking are accessible in: %s", err, tmpDir)
   218  		}
   219  		ovfFilePath, err = getOvfPath(filesAbsPaths)
   220  		if err != nil {
   221  			return UploadTask{}, fmt.Errorf("%s. Unpacked files for checking are accessible in: %s", err, tmpDir)
   222  		}
   223  	}
   224  
   225  	ovfFileDesc, err := getOvf(ovfFilePath)
   226  	if err != nil {
   227  		return UploadTask{}, fmt.Errorf("%s. OVF/Unpacked files for checking are accessible in: %s", err, tmpDir)
   228  	}
   229  
   230  	if !isOvf {
   231  		err = validateOvaContent(filesAbsPaths, &ovfFileDesc, tmpDir)
   232  		if err != nil {
   233  			return UploadTask{}, fmt.Errorf("%s. Unpacked files for checking are accessible in: %s", err, tmpDir)
   234  		}
   235  	} else {
   236  		dir := path.Dir(ovfFilePath)
   237  		for _, fileItem := range ovfFileDesc.File {
   238  			dependFile := path.Join(dir, fileItem.HREF)
   239  			dependFile, err := validateAndFixFilePath(dependFile)
   240  			if err != nil {
   241  				return UploadTask{}, err
   242  			}
   243  			filesAbsPaths = append(filesAbsPaths, dependFile)
   244  		}
   245  	}
   246  
   247  	catalogItemUploadURL, err := findCatalogItemUploadLink(cat, "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml")
   248  	if err != nil {
   249  		return UploadTask{}, err
   250  	}
   251  
   252  	vappTemplateUrl, err := createItemForUpload(cat.client, catalogItemUploadURL, itemName, description)
   253  	if err != nil {
   254  		return UploadTask{}, err
   255  	}
   256  
   257  	vappTemplate, err := queryVappTemplateAndVerifyTask(cat.client, vappTemplateUrl, itemName)
   258  	if err != nil {
   259  		return UploadTask{}, err
   260  	}
   261  
   262  	ovfUploadHref, err := getUploadLink(vappTemplate.Files)
   263  	if err != nil {
   264  		return UploadTask{}, err
   265  	}
   266  
   267  	err = uploadOvfDescription(cat.client, ovfFilePath, ovfUploadHref)
   268  	if err != nil {
   269  		removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName)
   270  		return UploadTask{}, err
   271  	}
   272  
   273  	vappTemplate, err = waitForTempUploadLinks(cat.client, vappTemplateUrl, itemName)
   274  	if err != nil {
   275  		removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName)
   276  		return UploadTask{}, err
   277  	}
   278  
   279  	progressCallBack, uploadProgress := getProgressCallBackFunction()
   280  
   281  	uploadError := *new(error)
   282  
   283  	// sending upload process to background, this allows not to lock and return task to client
   284  	// The error should be captured in uploadError, but just in case, we add a logging for the
   285  	// main error
   286  	go func() {
   287  		err = uploadFiles(cat.client, vappTemplate, &ovfFileDesc, tmpDir, filesAbsPaths, uploadPieceSize, progressCallBack, &uploadError, isOvf)
   288  		if err != nil {
   289  			util.Logger.Println(strings.Repeat("*", 80))
   290  			util.Logger.Printf("*** [DEBUG - UploadOvf] error calling uploadFiles: %s\n", err)
   291  			util.Logger.Println(strings.Repeat("*", 80))
   292  		}
   293  	}()
   294  
   295  	var task Task
   296  	for _, item := range vappTemplate.Tasks.Task {
   297  		task, err = createTaskForVcdImport(cat.client, item.HREF)
   298  		if err != nil {
   299  			removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName)
   300  			return UploadTask{}, err
   301  		}
   302  		if task.Task.Status == "error" {
   303  			removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName)
   304  			return UploadTask{}, fmt.Errorf("task did not complete succesfully: %s", task.Task.Description)
   305  		}
   306  	}
   307  
   308  	uploadTask := NewUploadTask(&task, uploadProgress, &uploadError)
   309  
   310  	util.Logger.Printf("[TRACE] Upload finished and task for vcd import created. \n")
   311  
   312  	return *uploadTask, nil
   313  }
   314  
   315  // UploadOvfByLink uploads an OVF file to a catalog from remote URL.
   316  // Returns errors if any occur during upload from VCD or upload process. On upload fail client may need to
   317  // remove VCD catalog item which is in failed state.
   318  func (cat *Catalog) UploadOvfByLink(ovfUrl, itemName, description string) (Task, error) {
   319  
   320  	if *cat == (Catalog{}) {
   321  		return Task{}, errors.New("catalog can not be empty or nil")
   322  	}
   323  
   324  	for _, catalogItemName := range getExistingCatalogItems(cat) {
   325  		if catalogItemName == itemName {
   326  			return Task{}, fmt.Errorf("catalog item '%s' already exists. Upload with different name", itemName)
   327  		}
   328  	}
   329  
   330  	catalogItemUploadURL, err := findCatalogItemUploadLink(cat, "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml")
   331  	if err != nil {
   332  		return Task{}, err
   333  	}
   334  
   335  	vappTemplateUrl, err := createItemWithLink(cat.client, catalogItemUploadURL, itemName, description, ovfUrl)
   336  	if err != nil {
   337  		return Task{}, err
   338  	}
   339  
   340  	vappTemplate, err := fetchVappTemplate(cat.client, vappTemplateUrl)
   341  	if err != nil {
   342  		return Task{}, err
   343  	}
   344  
   345  	var task Task
   346  	for _, item := range vappTemplate.Tasks.Task {
   347  		task, err = createTaskForVcdImport(cat.client, item.HREF)
   348  		if err != nil {
   349  			removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName)
   350  			return Task{}, err
   351  		}
   352  		if task.Task.Status == "error" {
   353  			removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName)
   354  			return Task{}, fmt.Errorf("task did not complete succesfully: %s", task.Task.Description)
   355  		}
   356  	}
   357  
   358  	util.Logger.Printf("[TRACE] task for vcd import created. \n")
   359  
   360  	return task, nil
   361  }
   362  
   363  // CaptureVappTemplate captures a vApp template from an existing vApp
   364  func (cat *Catalog) CaptureVappTemplate(captureParams *types.CaptureVAppParams) (*VAppTemplate, error) {
   365  	task, err := cat.CaptureVappTemplateAsync(captureParams)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	err = task.WaitTaskCompletion()
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  
   375  	if task.Task == nil || task.Task.Owner == nil || task.Task.Owner.HREF == "" {
   376  		return nil, fmt.Errorf("task does not have Owner HREF populated: %#v", task)
   377  	}
   378  
   379  	// After the task is finished, owner field contains the resulting vApp template
   380  	return cat.GetVappTemplateByHref(task.Task.Owner.HREF)
   381  }
   382  
   383  // CaptureVappTemplateAsync triggers vApp template capturing task and returns it
   384  //
   385  // Note. If 'CaptureVAppParams.CopyTpmOnInstantiate' is set, it will be unset for VCD versions
   386  // before 10.4.2 as it would break API call
   387  func (cat *Catalog) CaptureVappTemplateAsync(captureParams *types.CaptureVAppParams) (Task, error) {
   388  	util.Logger.Printf("[TRACE] Capturing vApp template to catalog %s", cat.Catalog.Name)
   389  	if captureParams == nil {
   390  		return Task{}, fmt.Errorf("input CaptureVAppParams cannot be nil")
   391  	}
   392  
   393  	captureTemplateHref := cat.client.VCDHREF
   394  	captureTemplateHref.Path += fmt.Sprintf("/catalog/%s/action/captureVApp", extractUuid(cat.Catalog.ID))
   395  
   396  	captureParams.Xmlns = types.XMLNamespaceVCloud
   397  	captureParams.XmlnsNs0 = types.XMLNamespaceOVF
   398  
   399  	util.Logger.Printf("[TRACE] Url for capturing vApp template: %s", captureTemplateHref.String())
   400  
   401  	if cat.client.APIVCDMaxVersionIs("< 37.2") {
   402  		captureParams.CopyTpmOnInstantiate = nil
   403  		util.Logger.Println("[TRACE] Explicitly unsetting 'CopyTpmOnInstantiate' because it was not supported before VCD 10.4.2")
   404  	}
   405  
   406  	return cat.client.ExecuteTaskRequest(captureTemplateHref.String(), http.MethodPost,
   407  		types.MimeCaptureVappTemplateParams, "error capturing vApp Template: %s", captureParams)
   408  }
   409  
   410  // Upload files for vCD created upload links. Different approach then vmdk file are
   411  // chunked (e.g. test.vmdk.000000000, test.vmdk.000000001 or test.vmdk). vmdk files are chunked if
   412  // in description file attribute ChunkSize is not zero.
   413  // params:
   414  // client - client for requests
   415  // vappTemplate - parsed from response vApp template
   416  // ovfFileDesc - parsed from xml part containing ova files definition
   417  // tempPath - path where extracted files are
   418  // filesAbsPaths - array of extracted files
   419  // uploadPieceSize - size of chunks in which the file will be uploaded to the catalog.
   420  // callBack a function with signature //function(bytesUpload, totalSize) to let the caller monitor progress of the upload operation.
   421  // uploadError - error to be ready be task
   422  func uploadFiles(client *Client, vappTemplate *types.VAppTemplate, ovfFileDesc *Envelope, tempPath string, filesAbsPaths []string, uploadPieceSize int64, progressCallBack func(bytesUpload, totalSize int64), uploadError *error, isOvf bool) error {
   423  	var uploadedBytes int64
   424  	for _, item := range vappTemplate.Files.File {
   425  		if item.BytesTransferred == 0 {
   426  			number, err := getFileFromDescription(item.Name, ovfFileDesc)
   427  			if err != nil {
   428  				util.Logger.Printf("[Error] Error uploading files: %#v", err)
   429  				*uploadError = err
   430  				return err
   431  			}
   432  			if ovfFileDesc.File[number].ChunkSize != 0 {
   433  				chunkFilePaths := getChunkedFilePaths(tempPath, ovfFileDesc.File[number].HREF, ovfFileDesc.File[number].Size, ovfFileDesc.File[number].ChunkSize)
   434  				details := uploadDetails{
   435  					uploadLink:               item.Link[0].HREF,
   436  					uploadedBytes:            uploadedBytes,
   437  					fileSizeToUpload:         int64(ovfFileDesc.File[number].Size),
   438  					uploadPieceSize:          uploadPieceSize,
   439  					uploadedBytesForCallback: uploadedBytes,
   440  					allFilesSize:             getAllFileSizeSum(ovfFileDesc),
   441  					callBack:                 progressCallBack,
   442  					uploadError:              uploadError,
   443  				}
   444  				tempVar, err := uploadMultiPartFile(client, chunkFilePaths, details)
   445  				if err != nil {
   446  					util.Logger.Printf("[Error] Error uploading files: %#v", err)
   447  					*uploadError = err
   448  					return err
   449  				}
   450  				uploadedBytes += tempVar
   451  			} else {
   452  				details := uploadDetails{
   453  					uploadLink:               item.Link[0].HREF,
   454  					uploadedBytes:            0,
   455  					fileSizeToUpload:         item.Size,
   456  					uploadPieceSize:          uploadPieceSize,
   457  					uploadedBytesForCallback: uploadedBytes,
   458  					allFilesSize:             getAllFileSizeSum(ovfFileDesc),
   459  					callBack:                 progressCallBack,
   460  					uploadError:              uploadError,
   461  				}
   462  				tempVar, err := uploadFile(client, findFilePath(filesAbsPaths, item.Name), details)
   463  				if err != nil {
   464  					util.Logger.Printf("[Error] Error uploading files: %#v", err)
   465  					*uploadError = err
   466  					return err
   467  				}
   468  				uploadedBytes += tempVar
   469  			}
   470  		}
   471  	}
   472  
   473  	//remove extracted files with temp dir
   474  	//If isOvf flag is true, means tempPath is origin OVF folder, not extracted, won't delete
   475  	if !isOvf {
   476  		err := os.RemoveAll(tempPath)
   477  		if err != nil {
   478  			util.Logger.Printf("[Error] Error removing temporary files: %#v", err)
   479  			*uploadError = err
   480  			return err
   481  		}
   482  	}
   483  	uploadError = nil
   484  	return nil
   485  }
   486  
   487  func getFileFromDescription(fileToFind string, ovfFileDesc *Envelope) (int, error) {
   488  	for fileInArray, item := range ovfFileDesc.File {
   489  		if item.HREF == fileToFind {
   490  			util.Logger.Printf("[TRACE] getFileFromDescription - found matching file: %s in array: %d\n", fileToFind, fileInArray)
   491  			return fileInArray, nil
   492  		}
   493  	}
   494  	return -1, errors.New("file expected from vcd didn't match any description file")
   495  }
   496  
   497  func getAllFileSizeSum(ovfFileDesc *Envelope) (sizeSum int64) {
   498  	sizeSum = 0
   499  	for _, item := range ovfFileDesc.File {
   500  		sizeSum += int64(item.Size)
   501  	}
   502  	return
   503  }
   504  
   505  // Uploads chunked ova file for vCD created upload link.
   506  // params:
   507  // client - client for requests
   508  // vappTemplate - parsed from response vApp template
   509  // filePaths - all chunked vmdk file paths
   510  // uploadDetails - file upload settings and data
   511  func uploadMultiPartFile(client *Client, filePaths []string, uDetails uploadDetails) (int64, error) {
   512  	util.Logger.Printf("[TRACE] Upload multi part file: %v\n, href: %s, size: %v", filePaths, uDetails.uploadLink, uDetails.fileSizeToUpload)
   513  
   514  	var uploadedBytes int64
   515  
   516  	for i, filePath := range filePaths {
   517  		util.Logger.Printf("[TRACE] Uploading file: %v\n", i+1)
   518  		uDetails.uploadedBytesForCallback += uploadedBytes // previous files uploaded size plus current upload size
   519  		uDetails.uploadedBytes = uploadedBytes
   520  		tempVar, err := uploadFile(client, filePath, uDetails)
   521  		if err != nil {
   522  			return uploadedBytes, err
   523  		}
   524  		uploadedBytes += tempVar
   525  	}
   526  	return uploadedBytes, nil
   527  }
   528  
   529  // Function waits until vCD provides temporary file upload links.
   530  func waitForTempUploadLinks(client *Client, vappTemplateUrl *url.URL, newItemName string) (*types.VAppTemplate, error) {
   531  	var vAppTemplate *types.VAppTemplate
   532  	var err error
   533  	for {
   534  		util.Logger.Printf("[TRACE] Sleep... for 5 seconds.\n")
   535  		time.Sleep(time.Second * 5)
   536  		vAppTemplate, err = queryVappTemplateAndVerifyTask(client, vappTemplateUrl, newItemName)
   537  		if err != nil {
   538  			return nil, err
   539  		}
   540  		if vAppTemplate.Files != nil && len(vAppTemplate.Files.File) > 1 {
   541  			util.Logger.Printf("[TRACE] upload link prepared.\n")
   542  			break
   543  		}
   544  	}
   545  	return vAppTemplate, nil
   546  }
   547  
   548  func queryVappTemplateAndVerifyTask(client *Client, vappTemplateUrl *url.URL, newItemName string) (*types.VAppTemplate, error) {
   549  	util.Logger.Printf("[TRACE] Querying vApp template: %s\n", vappTemplateUrl)
   550  
   551  	vappTemplateParsed, err := fetchVappTemplate(client, vappTemplateUrl)
   552  	if err != nil {
   553  		return nil, err
   554  	}
   555  
   556  	if vappTemplateParsed.Tasks == nil {
   557  		util.Logger.Printf("[ERROR] the vApp Template %s does not contain tasks, an error happened during upload: %v", vappTemplateUrl, vappTemplateParsed)
   558  		return vappTemplateParsed, fmt.Errorf("the vApp Template %s does not contain tasks, an error happened during upload", vappTemplateUrl)
   559  	}
   560  
   561  	for _, task := range vappTemplateParsed.Tasks.Task {
   562  		if task.Status == "error" && newItemName == task.Owner.Name {
   563  			util.Logger.Printf("[Error] %#v", task.Error)
   564  			return vappTemplateParsed, fmt.Errorf("error in vcd returned error code: %d, error: %s and message: %s ", task.Error.MajorErrorCode, task.Error.MinorErrorCode, task.Error.Message)
   565  		}
   566  	}
   567  
   568  	return vappTemplateParsed, nil
   569  }
   570  
   571  func fetchVappTemplate(client *Client, vappTemplateUrl *url.URL) (*types.VAppTemplate, error) {
   572  	util.Logger.Printf("[TRACE] Querying vApp template: %s\n", vappTemplateUrl)
   573  
   574  	vappTemplateParsed := &types.VAppTemplate{}
   575  
   576  	_, err := client.ExecuteRequest(vappTemplateUrl.String(), http.MethodGet,
   577  		"", "error fetching vApp template: %s", nil, vappTemplateParsed)
   578  	if err != nil {
   579  		return nil, err
   580  	}
   581  
   582  	return vappTemplateParsed, nil
   583  }
   584  
   585  // Uploads ovf description file from unarchived provided ova file. As a result vCD will generate temporary upload links which has to be queried later.
   586  // Function will return parsed part for upload files from description xml.
   587  func uploadOvfDescription(client *Client, ovfFile string, ovfUploadUrl *url.URL) error {
   588  	util.Logger.Printf("[TRACE] Uploding ovf description with file: %s and url: %s\n", ovfFile, ovfUploadUrl)
   589  	openedFile, err := os.Open(filepath.Clean(ovfFile))
   590  	if err != nil {
   591  		return err
   592  	}
   593  
   594  	var buf bytes.Buffer
   595  	ovfReader := io.TeeReader(openedFile, &buf)
   596  
   597  	request := client.NewRequest(map[string]string{}, http.MethodPut, *ovfUploadUrl, ovfReader)
   598  	request.Header.Add("Content-Type", "text/xml")
   599  
   600  	_, err = checkResp(client.Http.Do(request))
   601  	if err != nil {
   602  		return err
   603  	}
   604  
   605  	err = openedFile.Close()
   606  	if err != nil {
   607  		util.Logger.Printf("[Error] Error closing file: %#v", err)
   608  		return err
   609  	}
   610  
   611  	return nil
   612  }
   613  
   614  func parseOvfFileDesc(file *os.File, ovfFileDesc *Envelope) error {
   615  	ovfXml, err := io.ReadAll(file)
   616  	if err != nil {
   617  		return err
   618  	}
   619  
   620  	err = xml.Unmarshal(ovfXml, &ovfFileDesc)
   621  	if err != nil {
   622  		return err
   623  	}
   624  	return nil
   625  }
   626  
   627  func findCatalogItemUploadLink(catalog *Catalog, applicationType string) (*url.URL, error) {
   628  	for _, item := range catalog.Catalog.Link {
   629  		if item.Type == applicationType && item.Rel == "add" {
   630  			util.Logger.Printf("[TRACE] Found Catalong link for upload: %s\n", item.HREF)
   631  
   632  			uploadURL, err := url.ParseRequestURI(item.HREF)
   633  			if err != nil {
   634  				return nil, err
   635  			}
   636  
   637  			util.Logger.Printf("[TRACE] findCatalogItemUploadLink - catalog item upload url found: %s \n", uploadURL)
   638  			return uploadURL, nil
   639  		}
   640  	}
   641  	return nil, errors.New("catalog upload URL not found")
   642  }
   643  
   644  func getExistingCatalogItems(catalog *Catalog) (catalogItemNames []string) {
   645  	for _, catalogItems := range catalog.Catalog.CatalogItems {
   646  		for _, catalogItem := range catalogItems.CatalogItem {
   647  			catalogItemNames = append(catalogItemNames, catalogItem.Name)
   648  		}
   649  	}
   650  	return
   651  }
   652  
   653  func findFilePath(filesAbsPaths []string, fileName string) string {
   654  	for _, item := range filesAbsPaths {
   655  		_, file := filepath.Split(item)
   656  		if file == fileName {
   657  			return item
   658  		}
   659  	}
   660  	return ""
   661  }
   662  
   663  // Initiates creation of item and returns ovf upload url for created item.
   664  func createItemForUpload(client *Client, createHREF *url.URL, catalogItemName string, itemDescription string) (*url.URL, error) {
   665  	util.Logger.Printf("[TRACE] createItemForUpload: %s, item name: %s, description: %s \n", createHREF, catalogItemName, itemDescription)
   666  	reqBody := bytes.NewBufferString(
   667  		"<UploadVAppTemplateParams xmlns=\"" + types.XMLNamespaceVCloud + "\" name=\"" + catalogItemName + "\" >" +
   668  			"<Description>" + itemDescription + "</Description>" +
   669  			"</UploadVAppTemplateParams>")
   670  
   671  	request := client.NewRequest(map[string]string{}, http.MethodPost, *createHREF, reqBody)
   672  	request.Header.Add("Content-Type", "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml")
   673  
   674  	response, err := checkResp(client.Http.Do(request))
   675  	if err != nil {
   676  		return nil, err
   677  	}
   678  	defer func(Body io.ReadCloser) {
   679  		err := Body.Close()
   680  		if err != nil {
   681  			util.Logger.Printf("error closing response Body [createItemForUpload]: %s", err)
   682  		}
   683  	}(response.Body)
   684  
   685  	catalogItemParsed := &types.CatalogItem{}
   686  	if err = decodeBody(types.BodyTypeXML, response, catalogItemParsed); err != nil {
   687  		return nil, err
   688  	}
   689  
   690  	util.Logger.Printf("[TRACE] Catalog item parsed: %#v\n", catalogItemParsed)
   691  
   692  	ovfUploadUrl, err := url.ParseRequestURI(catalogItemParsed.Entity.HREF)
   693  	if err != nil {
   694  		return nil, err
   695  	}
   696  
   697  	return ovfUploadUrl, nil
   698  }
   699  
   700  // Initiates creation of item in catalog and returns vappTeamplate Url for created item.
   701  func createItemWithLink(client *Client, createHREF *url.URL, catalogItemName, itemDescription, vappTemplateRemoteUrl string) (*url.URL, error) {
   702  	util.Logger.Printf("[TRACE] createItemWithLink: %s, item name: %s, description: %s, vappTemplateRemoteUrl: %s \n",
   703  		createHREF, catalogItemName, itemDescription, vappTemplateRemoteUrl)
   704  
   705  	reqTemplate := `<UploadVAppTemplateParams xmlns="%s" name="%s" sourceHref="%s"><Description>%s</Description></UploadVAppTemplateParams>`
   706  	reqBody := bytes.NewBufferString(fmt.Sprintf(reqTemplate, types.XMLNamespaceVCloud, catalogItemName, vappTemplateRemoteUrl, itemDescription))
   707  	request := client.NewRequest(map[string]string{}, http.MethodPost, *createHREF, reqBody)
   708  	request.Header.Add("Content-Type", "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml")
   709  
   710  	response, err := checkResp(client.Http.Do(request))
   711  	if err != nil {
   712  		return nil, err
   713  	}
   714  	defer func(Body io.ReadCloser) {
   715  		err := Body.Close()
   716  		if err != nil {
   717  			util.Logger.Printf("error closing response Body [createItemWithLink]: %s", err)
   718  		}
   719  	}(response.Body)
   720  
   721  	catalogItemParsed := &types.CatalogItem{}
   722  	if err = decodeBody(types.BodyTypeXML, response, catalogItemParsed); err != nil {
   723  		return nil, err
   724  	}
   725  
   726  	util.Logger.Printf("[TRACE] Catalog item parsed: %#v\n", catalogItemParsed)
   727  
   728  	vappTemplateUrl, err := url.ParseRequestURI(catalogItemParsed.Entity.HREF)
   729  	if err != nil {
   730  		return nil, err
   731  	}
   732  
   733  	return vappTemplateUrl, nil
   734  }
   735  
   736  // Helper method to get path to multi-part files.
   737  // For example a file called test.vmdk with total_file_size = 100 bytes and part_size = 40 bytes, implies the file is made of *3* part files.
   738  //   - test.vmdk.000000000 = 40 bytes
   739  //   - test.vmdk.000000001 = 40 bytes
   740  //   - test.vmdk.000000002 = 20 bytes
   741  //
   742  // Say base_dir = /dummy_path/, and base_file_name = test.vmdk then
   743  // the output of this function will be [/dummy_path/test.vmdk.000000000,
   744  // /dummy_path/test.vmdk.000000001, /dummy_path/test.vmdk.000000002]
   745  func getChunkedFilePaths(baseDir, baseFileName string, totalFileSize, partSize int) []string {
   746  	var filePaths []string
   747  	numbParts := math.Ceil(float64(totalFileSize) / float64(partSize))
   748  	for i := 0; i < int(numbParts); i++ {
   749  		temp := "000000000" + strconv.Itoa(i)
   750  		postfix := temp[len(temp)-9:]
   751  		filePath := path.Join(baseDir, baseFileName+"."+postfix)
   752  		filePaths = append(filePaths, filePath)
   753  	}
   754  
   755  	util.Logger.Printf("[TRACE] Chunked files file paths: %s \n", filePaths)
   756  	return filePaths
   757  }
   758  
   759  func getOvfPath(filesAbsPaths []string) (string, error) {
   760  	for _, filePath := range filesAbsPaths {
   761  		if filepath.Ext(filePath) == ".ovf" {
   762  			return filePath, nil
   763  		}
   764  	}
   765  	return "", errors.New("ova is not correct - missing ovf file")
   766  }
   767  
   768  func getOvf(ovfFilePath string) (Envelope, error) {
   769  	openedFile, err := os.Open(filepath.Clean(ovfFilePath))
   770  	if err != nil {
   771  		return Envelope{}, err
   772  	}
   773  
   774  	var ovfFileDesc Envelope
   775  	err = parseOvfFileDesc(openedFile, &ovfFileDesc)
   776  	if err != nil {
   777  		return Envelope{}, err
   778  	}
   779  
   780  	err = openedFile.Close()
   781  	if err != nil {
   782  		util.Logger.Printf("[Error] Error closing file: %#v", err)
   783  		return Envelope{}, err
   784  	}
   785  
   786  	return ovfFileDesc, nil
   787  }
   788  
   789  func validateOvaContent(filesAbsPaths []string, ovfFileDesc *Envelope, tempPath string) error {
   790  	for _, fileDescription := range ovfFileDesc.File {
   791  		if fileDescription.ChunkSize == 0 {
   792  			err := checkIfFileMatchesDescription(filesAbsPaths, fileDescription)
   793  			if err != nil {
   794  				return err
   795  			}
   796  			// check chunked ova content
   797  		} else {
   798  			chunkFilePaths := getChunkedFilePaths(tempPath, fileDescription.HREF, fileDescription.Size, fileDescription.ChunkSize)
   799  			for part, chunkedFilePath := range chunkFilePaths {
   800  				_, fileName := filepath.Split(chunkedFilePath)
   801  				chunkedFileSize := fileDescription.Size - part*fileDescription.ChunkSize
   802  				if chunkedFileSize > fileDescription.ChunkSize {
   803  					chunkedFileSize = fileDescription.ChunkSize
   804  				}
   805  				chunkedFileDescription := struct {
   806  					HREF      string `xml:"href,attr"`
   807  					ID        string `xml:"id,attr"`
   808  					Size      int    `xml:"size,attr"`
   809  					ChunkSize int    `xml:"chunkSize,attr"`
   810  				}{fileName, "", chunkedFileSize, fileDescription.ChunkSize}
   811  				err := checkIfFileMatchesDescription(filesAbsPaths, chunkedFileDescription)
   812  				if err != nil {
   813  					return err
   814  				}
   815  			}
   816  		}
   817  	}
   818  	return nil
   819  }
   820  
   821  func checkIfFileMatchesDescription(filesAbsPaths []string, fileDescription struct {
   822  	HREF      string `xml:"href,attr"`
   823  	ID        string `xml:"id,attr"`
   824  	Size      int    `xml:"size,attr"`
   825  	ChunkSize int    `xml:"chunkSize,attr"`
   826  }) error {
   827  	filePath := findFilePath(filesAbsPaths, fileDescription.HREF)
   828  	if filePath == "" {
   829  		return fmt.Errorf("file '%s' described in ovf was not found in ova", fileDescription.HREF)
   830  	}
   831  	if fileInfo, err := os.Stat(filePath); err == nil {
   832  		if fileDescription.Size > 0 && (fileInfo.Size() != int64(fileDescription.Size)) {
   833  			return fmt.Errorf("file size didn't match described in ovf: %s", filePath)
   834  		}
   835  	} else {
   836  		return err
   837  	}
   838  	return nil
   839  }
   840  
   841  func removeCatalogItemOnError(client *Client, vappTemplateLink *url.URL, itemName string) {
   842  	if vappTemplateLink != nil {
   843  		util.Logger.Printf("[TRACE] Deleting Catalog item %v", vappTemplateLink)
   844  
   845  		// wait for task, cancel it and catalog item will be removed.
   846  		var vAppTemplate *types.VAppTemplate
   847  		var err error
   848  		for {
   849  			util.Logger.Printf("[TRACE] Sleep... for 5 seconds.\n")
   850  			time.Sleep(time.Second * 5)
   851  			vAppTemplate, err = queryVappTemplateAndVerifyTask(client, vappTemplateLink, itemName)
   852  			if err != nil {
   853  				util.Logger.Printf("[Error] Error deleting Catalog item %s: %s", vappTemplateLink, err)
   854  			}
   855  			if vAppTemplate.Tasks == nil {
   856  				util.Logger.Printf("[Error] Error deleting Catalog item %s: it doesn't contain any task", vappTemplateLink)
   857  				return
   858  			}
   859  			if vAppTemplate.Tasks != nil && len(vAppTemplate.Tasks.Task) > 0 {
   860  				util.Logger.Printf("[TRACE] Task found. Will try to cancel.\n")
   861  				break
   862  			}
   863  		}
   864  
   865  		for _, taskItem := range vAppTemplate.Tasks.Task {
   866  			if itemName == taskItem.Owner.Name {
   867  				task := NewTask(client)
   868  				task.Task = taskItem
   869  				err = task.CancelTask()
   870  				if err != nil {
   871  					util.Logger.Printf("[ERROR] Error canceling task for catalog item upload %#v", err)
   872  				}
   873  			}
   874  		}
   875  	} else {
   876  		util.Logger.Printf("[Error] Failed to delete catalog item created with error: %v", vappTemplateLink)
   877  	}
   878  }
   879  
   880  // UploadMediaImage uploads a media image to the catalog
   881  func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath string, uploadPieceSize int64) (UploadTask, error) {
   882  	return cat.UploadMediaFile(mediaName, mediaDescription, filePath, uploadPieceSize, true)
   883  }
   884  
   885  // UploadMediaFile uploads any file to the catalog.
   886  // However, if checkFileIsIso is true, only .ISO are allowed.
   887  func (cat *Catalog) UploadMediaFile(fileName, mediaDescription, filePath string, uploadPieceSize int64, checkFileIsIso bool) (UploadTask, error) {
   888  
   889  	if *cat == (Catalog{}) {
   890  		return UploadTask{}, errors.New("catalog can not be empty or nil")
   891  	}
   892  
   893  	mediaFilePath, err := validateAndFixFilePath(filePath)
   894  	if err != nil {
   895  		return UploadTask{}, err
   896  	}
   897  
   898  	if checkFileIsIso {
   899  		isISOGood, err := verifyIso(mediaFilePath)
   900  		if err != nil || !isISOGood {
   901  			return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err)
   902  		}
   903  	}
   904  
   905  	file, e := os.Stat(mediaFilePath)
   906  	if e != nil {
   907  		return UploadTask{}, fmt.Errorf("[ERROR] Issue finding file: %#v", e)
   908  	}
   909  	fileSize := file.Size()
   910  
   911  	for _, catalogItemName := range getExistingCatalogItems(cat) {
   912  		if catalogItemName == fileName {
   913  			return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", fileName)
   914  		}
   915  	}
   916  
   917  	catalogItemUploadURL, err := findCatalogItemUploadLink(cat, "application/vnd.vmware.vcloud.media+xml")
   918  	if err != nil {
   919  		return UploadTask{}, err
   920  	}
   921  
   922  	media, err := createMedia(cat.client, catalogItemUploadURL.String(), fileName, mediaDescription, fileSize)
   923  	if err != nil {
   924  		return UploadTask{}, fmt.Errorf("[ERROR] Issue creating media: %#v", err)
   925  	}
   926  
   927  	createdMedia, err := queryMedia(cat.client, media.Entity.HREF, fileName)
   928  	if err != nil {
   929  		return UploadTask{}, err
   930  	}
   931  
   932  	return executeUpload(cat.client, createdMedia, mediaFilePath, fileName, fileSize, uploadPieceSize)
   933  }
   934  
   935  // Refresh gets a fresh copy of the catalog from vCD
   936  func (cat *Catalog) Refresh() error {
   937  	if cat == nil || *cat == (Catalog{}) || cat.Catalog.HREF == "" {
   938  		return fmt.Errorf("cannot refresh, Object is empty or HREF is empty")
   939  	}
   940  
   941  	refreshedCatalog := &types.Catalog{}
   942  
   943  	_, err := cat.client.ExecuteRequest(cat.Catalog.HREF, http.MethodGet,
   944  		"", "error refreshing VDC: %s", nil, refreshedCatalog)
   945  	if err != nil {
   946  		return err
   947  	}
   948  	cat.Catalog = refreshedCatalog
   949  
   950  	return nil
   951  }
   952  
   953  // GetCatalogItemByHref finds a CatalogItem by HREF
   954  // On success, returns a pointer to the CatalogItem structure and a nil error
   955  // On failure, returns a nil pointer and an error
   956  func (cat *Catalog) GetCatalogItemByHref(catalogItemHref string) (*CatalogItem, error) {
   957  
   958  	catItem := NewCatalogItem(cat.client)
   959  
   960  	_, err := cat.client.ExecuteRequest(catalogItemHref, http.MethodGet,
   961  		"", "error retrieving catalog item: %s", nil, catItem.CatalogItem)
   962  	if err != nil {
   963  		return nil, err
   964  	}
   965  	return catItem, nil
   966  }
   967  
   968  // GetVappTemplateByHref finds a vApp template by HREF
   969  // On success, returns a pointer to the vApp template structure and a nil error
   970  // On failure, returns a nil pointer and an error
   971  func (cat *Catalog) GetVappTemplateByHref(href string) (*VAppTemplate, error) {
   972  	return getVAppTemplateByHref(cat.client, href)
   973  }
   974  
   975  // getVAppTemplateByHref finds a vApp template by HREF
   976  // On success, returns a pointer to the vApp template structure and a nil error
   977  // On failure, returns a nil pointer and an error
   978  func getVAppTemplateByHref(client *Client, href string) (*VAppTemplate, error) {
   979  	vappTemplate := NewVAppTemplate(client)
   980  
   981  	_, err := client.ExecuteRequest(href, http.MethodGet, "", "error retrieving vApp Template: %s", nil, vappTemplate.VAppTemplate)
   982  	if err != nil {
   983  		return nil, err
   984  	}
   985  	return vappTemplate, nil
   986  }
   987  
   988  // GetCatalogItemByName finds a CatalogItem by Name
   989  // On success, returns a pointer to the CatalogItem structure and a nil error
   990  // On failure, returns a nil pointer and an error
   991  func (cat *Catalog) GetCatalogItemByName(catalogItemName string, refresh bool) (*CatalogItem, error) {
   992  	if refresh {
   993  		err := cat.Refresh()
   994  		if err != nil {
   995  			return nil, err
   996  		}
   997  	}
   998  	for _, catalogItems := range cat.Catalog.CatalogItems {
   999  		for _, catalogItem := range catalogItems.CatalogItem {
  1000  			if catalogItem.Name == catalogItemName && catalogItem.Type == "application/vnd.vmware.vcloud.catalogItem+xml" {
  1001  				return cat.GetCatalogItemByHref(catalogItem.HREF)
  1002  			}
  1003  		}
  1004  	}
  1005  	return nil, ErrorEntityNotFound
  1006  }
  1007  
  1008  // GetVAppTemplateByName finds a VAppTemplate by Name
  1009  // On success, returns a pointer to the VAppTemplate structure and a nil error
  1010  // On failure, returns a nil pointer and an error
  1011  func (cat *Catalog) GetVAppTemplateByName(vAppTemplateName string) (*VAppTemplate, error) {
  1012  	vAppTemplateQueryResult, err := cat.QueryVappTemplateWithName(vAppTemplateName)
  1013  	if err != nil {
  1014  		return nil, err
  1015  	}
  1016  	return cat.GetVappTemplateByHref(vAppTemplateQueryResult.HREF)
  1017  }
  1018  
  1019  // GetCatalogItemById finds a Catalog Item by ID
  1020  // On success, returns a pointer to the CatalogItem structure and a nil error
  1021  // On failure, returns a nil pointer and an error
  1022  func (cat *Catalog) GetCatalogItemById(catalogItemId string, refresh bool) (*CatalogItem, error) {
  1023  	if refresh {
  1024  		err := cat.Refresh()
  1025  		if err != nil {
  1026  			return nil, err
  1027  		}
  1028  	}
  1029  	for _, catalogItems := range cat.Catalog.CatalogItems {
  1030  		for _, catalogItem := range catalogItems.CatalogItem {
  1031  			if equalIds(catalogItemId, catalogItem.ID, catalogItem.HREF) && catalogItem.Type == "application/vnd.vmware.vcloud.catalogItem+xml" {
  1032  				return cat.GetCatalogItemByHref(catalogItem.HREF)
  1033  			}
  1034  		}
  1035  	}
  1036  	return nil, ErrorEntityNotFound
  1037  }
  1038  
  1039  // GetVAppTemplateById finds a vApp Template by ID.
  1040  // On success, returns a pointer to the VAppTemplate structure and a nil error.
  1041  // On failure, returns a nil pointer and an error.
  1042  func (cat *Catalog) GetVAppTemplateById(vAppTemplateId string) (*VAppTemplate, error) {
  1043  	return getVAppTemplateById(cat.client, vAppTemplateId)
  1044  }
  1045  
  1046  // getVAppTemplateById finds a vApp Template by ID.
  1047  // On success, returns a pointer to the VAppTemplate structure and a nil error.
  1048  // On failure, returns a nil pointer and an error.
  1049  func getVAppTemplateById(client *Client, vAppTemplateId string) (*VAppTemplate, error) {
  1050  	vappTemplateHref := client.VCDHREF
  1051  	vappTemplateHref.Path += "/vAppTemplate/vappTemplate-" + extractUuid(vAppTemplateId)
  1052  
  1053  	vappTemplate, err := getVAppTemplateByHref(client, vappTemplateHref.String())
  1054  	if err != nil {
  1055  		return nil, fmt.Errorf("could not find vApp Template with ID %s: %s", vAppTemplateId, err)
  1056  	}
  1057  	return vappTemplate, nil
  1058  }
  1059  
  1060  // GetCatalogItemByNameOrId finds a Catalog Item by Name or ID.
  1061  // On success, returns a pointer to the CatalogItem structure and a nil error
  1062  // On failure, returns a nil pointer and an error
  1063  func (cat *Catalog) GetCatalogItemByNameOrId(identifier string, refresh bool) (*CatalogItem, error) {
  1064  	getByName := func(name string, refresh bool) (interface{}, error) { return cat.GetCatalogItemByName(name, refresh) }
  1065  	getById := func(id string, refresh bool) (interface{}, error) { return cat.GetCatalogItemById(id, refresh) }
  1066  	entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh)
  1067  	if entity == nil {
  1068  		return nil, err
  1069  	}
  1070  	return entity.(*CatalogItem), err
  1071  }
  1072  
  1073  // GetVAppTemplateByNameOrId finds a vApp Template by Name or ID.
  1074  // On success, returns a pointer to the VAppTemplate structure and a nil error
  1075  // On failure, returns a nil pointer and an error
  1076  func (cat *Catalog) GetVAppTemplateByNameOrId(identifier string, refresh bool) (*VAppTemplate, error) {
  1077  	getByName := func(name string, refresh bool) (interface{}, error) { return cat.GetVAppTemplateByName(name) }
  1078  	getById := func(id string, refresh bool) (interface{}, error) { return cat.GetVAppTemplateById(id) }
  1079  	entity, err := getEntityByNameOrIdSkipNonId(getByName, getById, identifier, refresh)
  1080  	if entity == nil {
  1081  		return nil, err
  1082  	}
  1083  	return entity.(*VAppTemplate), err
  1084  }
  1085  
  1086  // QueryMediaList retrieves a list of media items for the catalog
  1087  func (catalog *Catalog) QueryMediaList() ([]*types.MediaRecordType, error) {
  1088  	typeMedia := "media"
  1089  	if catalog.client.IsSysAdmin {
  1090  		typeMedia = "adminMedia"
  1091  	}
  1092  
  1093  	filter := fmt.Sprintf("catalog==%s", url.QueryEscape(catalog.Catalog.HREF))
  1094  	results, err := catalog.client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": filter, "filterEncoded": "true"})
  1095  	if err != nil {
  1096  		return nil, fmt.Errorf("error querying medias: %s", err)
  1097  	}
  1098  
  1099  	mediaResults := results.Results.MediaRecord
  1100  	if catalog.client.IsSysAdmin {
  1101  		mediaResults = results.Results.AdminMediaRecord
  1102  	}
  1103  	return mediaResults, nil
  1104  }
  1105  
  1106  // getOrgInfo finds the organization to which the catalog belongs, and returns its name and ID
  1107  func (catalog *Catalog) getOrgInfo() (*TenantContext, error) {
  1108  	org := catalog.parent
  1109  	if org == nil {
  1110  		return nil, fmt.Errorf("no parent found for catalog %s", catalog.Catalog.Name)
  1111  	}
  1112  
  1113  	return org.tenantContext()
  1114  }
  1115  
  1116  func publishToExternalOrganizations(client *Client, url string, tenantContext *TenantContext, publishExternalCatalog types.PublishExternalCatalogParams) error {
  1117  	url = url + "/action/publishToExternalOrganizations"
  1118  
  1119  	publishExternalCatalog.Xmlns = types.XMLNamespaceVCloud
  1120  
  1121  	if tenantContext != nil {
  1122  		client.SetCustomHeader(getTenantContextHeader(tenantContext))
  1123  	}
  1124  
  1125  	err := client.ExecuteRequestWithoutResponse(url, http.MethodPost,
  1126  		types.PublishExternalCatalog, "error publishing to external organization: %s", publishExternalCatalog)
  1127  
  1128  	if tenantContext != nil {
  1129  		client.RemoveProvidedCustomHeaders(getTenantContextHeader(tenantContext))
  1130  	}
  1131  
  1132  	return err
  1133  }
  1134  
  1135  // PublishToExternalOrganizations publishes a catalog to external organizations.
  1136  func (cat *Catalog) PublishToExternalOrganizations(publishExternalCatalog types.PublishExternalCatalogParams) error {
  1137  	if cat.Catalog == nil {
  1138  		return fmt.Errorf("cannot publish to external organization, Object is empty")
  1139  	}
  1140  
  1141  	catalogUrl := cat.Catalog.HREF
  1142  	if catalogUrl == "nil" || catalogUrl == "" {
  1143  		return fmt.Errorf("cannot publish to external organization, HREF is empty")
  1144  	}
  1145  
  1146  	err := publishToExternalOrganizations(cat.client, catalogUrl, nil, publishExternalCatalog)
  1147  	if err != nil {
  1148  		return err
  1149  	}
  1150  
  1151  	err = cat.Refresh()
  1152  	if err != nil {
  1153  		return err
  1154  	}
  1155  
  1156  	return err
  1157  }
  1158  
  1159  // elementSync is a low level function that synchronises a Catalog, AdminCatalog, CatalogItem, or Media item
  1160  func elementSync(client *Client, elementHref, label string) error {
  1161  	task, err := elementLaunchSync(client, elementHref, label)
  1162  	if err != nil {
  1163  		return err
  1164  	}
  1165  	return task.WaitTaskCompletion()
  1166  }
  1167  
  1168  // queryMediaList retrieves a list of media items for a given catalog or AdminCatalog
  1169  func queryMediaList(client *Client, catalogHref string) ([]*types.MediaRecordType, error) {
  1170  	typeMedia := "media"
  1171  	if client.IsSysAdmin {
  1172  		typeMedia = "adminMedia"
  1173  	}
  1174  
  1175  	filter := fmt.Sprintf("catalog==%s", url.QueryEscape(catalogHref))
  1176  	results, err := client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": filter, "filterEncoded": "true"})
  1177  	if err != nil {
  1178  		return nil, fmt.Errorf("error querying medias: %s", err)
  1179  	}
  1180  
  1181  	mediaResults := results.Results.MediaRecord
  1182  	if client.IsSysAdmin {
  1183  		mediaResults = results.Results.AdminMediaRecord
  1184  	}
  1185  	return mediaResults, nil
  1186  }
  1187  
  1188  // elementLaunchSync is a low level function that starts synchronisation for Catalog, AdminCatalog, CatalogItem, or Media item
  1189  func elementLaunchSync(client *Client, elementHref, label string) (*Task, error) {
  1190  	util.Logger.Printf("[TRACE] elementLaunchSync '%s' \n", label)
  1191  	href := elementHref + "/action/sync"
  1192  	syncTask, err := client.ExecuteTaskRequest(href, http.MethodPost,
  1193  		"", "error synchronizing "+label+": %s", nil)
  1194  
  1195  	if err != nil {
  1196  		// This process may fail due to a possible race condition: a synchronisation process may start in background
  1197  		// after we check for existing tasks (in the function that called this one)
  1198  		// and before we run the request in this function.
  1199  		// In a Terraform vcd_subscribed_catalog operation, the completeness of the synchronisation
  1200  		// will be ensured at the next refresh.
  1201  		if strings.Contains(err.Error(), "LIBRARY_ITEM_SYNC") {
  1202  			util.Logger.Printf("[SYNC FAILURE] error when launching synchronisation: %s\n", err)
  1203  			return nil, nil
  1204  		}
  1205  		return nil, err
  1206  	}
  1207  	return &syncTask, nil
  1208  }
  1209  
  1210  // QueryTaskList retrieves a list of tasks associated to the Catalog
  1211  func (catalog *Catalog) QueryTaskList(filter map[string]string) ([]*types.QueryResultTaskRecordType, error) {
  1212  	var newFilter = map[string]string{
  1213  		"object": catalog.Catalog.HREF,
  1214  	}
  1215  	for k, v := range filter {
  1216  		newFilter[k] = v
  1217  	}
  1218  	return catalog.client.QueryTaskList(newFilter)
  1219  }
  1220  
  1221  // GetCatalogByHref allows retrieving a catalog from HREF, without a fully qualified Org object
  1222  func (client *Client) GetCatalogByHref(catalogHref string) (*Catalog, error) {
  1223  	catalogHref = strings.Replace(catalogHref, "/api/admin/catalog", "/api/catalog", 1)
  1224  
  1225  	cat := NewCatalog(client)
  1226  
  1227  	_, err := client.ExecuteRequest(catalogHref, http.MethodGet,
  1228  		"", "error retrieving catalog: %s", nil, cat.Catalog)
  1229  
  1230  	if err != nil {
  1231  		return nil, err
  1232  	}
  1233  	// Setting the catalog parent, necessary to handle the tenant context
  1234  	org := NewOrg(client)
  1235  	for _, link := range cat.Catalog.Link {
  1236  		if link.Rel == "up" && link.Type == types.MimeOrg {
  1237  			_, err = client.ExecuteRequest(link.HREF, http.MethodGet,
  1238  				"", "error retrieving parent Org: %s", nil, org.Org)
  1239  			if err != nil {
  1240  				return nil, fmt.Errorf("error retrieving catalog parent: %s", err)
  1241  			}
  1242  			break
  1243  		}
  1244  	}
  1245  	cat.parent = org
  1246  	return cat, nil
  1247  }
  1248  
  1249  // GetCatalogById allows retrieving a catalog from ID, without a fully qualified Org object
  1250  func (client *Client) GetCatalogById(catalogId string) (*Catalog, error) {
  1251  	href, err := url.JoinPath(client.VCDHREF.String(), "catalog", extractUuid(catalogId))
  1252  	if err != nil {
  1253  		return nil, err
  1254  	}
  1255  	return client.GetCatalogByHref(href)
  1256  }
  1257  
  1258  // GetCatalogByName allows retrieving a catalog from name, without a fully qualified Org object
  1259  func (client *Client) GetCatalogByName(parentOrg, catalogName string) (*Catalog, error) {
  1260  	catalogs, err := queryCatalogList(client, nil)
  1261  	if err != nil {
  1262  		return nil, err
  1263  	}
  1264  	var parentOrgs []string
  1265  	for _, cat := range catalogs {
  1266  		if cat.Name == catalogName && cat.OrgName == parentOrg {
  1267  			return client.GetCatalogByHref(cat.HREF)
  1268  		}
  1269  		if cat.Name == catalogName {
  1270  			parentOrgs = append(parentOrgs, cat.OrgName)
  1271  		}
  1272  	}
  1273  	parents := ""
  1274  	if len(parentOrgs) > 0 {
  1275  		parents = fmt.Sprintf(" - Found catalog %s in Orgs %v", catalogName, parentOrgs)
  1276  	}
  1277  	return nil, fmt.Errorf("no catalog '%s' found in Org %s%s", catalogName, parentOrg, parents)
  1278  }
  1279  
  1280  // WaitForTasks waits for the catalog's tasks to complete
  1281  func (cat *Catalog) WaitForTasks() error {
  1282  	if ResourceInProgress(cat.Catalog.Tasks) {
  1283  		err := WaitResource(func() (*types.TasksInProgress, error) {
  1284  			err := cat.Refresh()
  1285  			if err != nil {
  1286  				return nil, err
  1287  			}
  1288  			return cat.Catalog.Tasks, nil
  1289  		})
  1290  		return err
  1291  	}
  1292  	return nil
  1293  }