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

     1  /*
     2   * Copyright 2022 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     3   */
     4  
     5  package govcd
     6  
     7  import (
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    17  	"github.com/vmware/go-vcloud-director/v2/util"
    18  )
    19  
    20  type Task struct {
    21  	Task   *types.Task
    22  	client *Client
    23  }
    24  
    25  func NewTask(cli *Client) *Task {
    26  	return &Task{
    27  		Task:   new(types.Task),
    28  		client: cli,
    29  	}
    30  }
    31  
    32  const errorRetrievingTask = "error retrieving task"
    33  
    34  // getErrorMessage composes a new error message, if the error is not nil.
    35  // The message is made of the error itself + the information from the task's Error component.
    36  // See:
    37  //
    38  //	https://code.vmware.com/apis/220/vcloud#/doc/doc/types/TaskType.html
    39  //	https://code.vmware.com/apis/220/vcloud#/doc/doc/types/ErrorType.html
    40  func (task *Task) getErrorMessage(err error) string {
    41  	errorMessage := ""
    42  	if err != nil {
    43  		errorMessage = err.Error()
    44  	}
    45  	if task.Task.Error != nil {
    46  		errorMessage += " [" +
    47  			fmt.Sprintf("%d:%s",
    48  				task.Task.Error.MajorErrorCode,   // The MajorError is a numeric code
    49  				task.Task.Error.MinorErrorCode) + // The MinorError is a string with a generic definition of the error
    50  			"] - " + task.Task.Error.Message
    51  	}
    52  	return errorMessage
    53  }
    54  
    55  // Refresh retrieves a fresh copy of the task
    56  func (task *Task) Refresh() error {
    57  
    58  	if task.Task == nil {
    59  		return fmt.Errorf("cannot refresh, Object is empty")
    60  	}
    61  
    62  	refreshUrl := urlParseRequestURI(task.Task.HREF)
    63  
    64  	req := task.client.NewRequest(map[string]string{}, http.MethodGet, *refreshUrl, nil)
    65  
    66  	resp, err := checkResp(task.client.Http.Do(req))
    67  	if err != nil {
    68  		return fmt.Errorf("%s: %s", errorRetrievingTask, err)
    69  	}
    70  
    71  	// Empty struct before a new unmarshal, otherwise we end up with duplicate
    72  	// elements in slices.
    73  	task.Task = &types.Task{}
    74  
    75  	if err = decodeBody(types.BodyTypeXML, resp, task.Task); err != nil {
    76  		return fmt.Errorf("error decoding task response: %s", task.getErrorMessage(err))
    77  	}
    78  
    79  	// The request was successful
    80  	return nil
    81  }
    82  
    83  // InspectionFunc is a callback function that can be passed to task.WaitInspectTaskCompletion
    84  // to perform user defined operations
    85  // * task is the task object being processed
    86  // * howManyTimes is the number of times the task has been refreshed
    87  // * elapsed is how much time since the task was initially processed
    88  // * first is true if this is the first refresh of the task
    89  // * last is true if the function is being called for the last time.
    90  type InspectionFunc func(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool)
    91  
    92  // TaskMonitoringFunc can run monitoring operations on a task
    93  type TaskMonitoringFunc func(*types.Task)
    94  
    95  // WaitInspectTaskCompletion is a customizable version of WaitTaskCompletion.
    96  // Users can define the sleeping duration and an optional callback function for
    97  // extra monitoring.
    98  func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay time.Duration) error {
    99  
   100  	if task.Task == nil {
   101  		return fmt.Errorf("cannot refresh, Object is empty")
   102  	}
   103  
   104  	taskMonitor := os.Getenv("GOVCD_TASK_MONITOR")
   105  	howManyTimesRefreshed := 0
   106  	startTime := time.Now()
   107  	for {
   108  		howManyTimesRefreshed++
   109  		elapsed := time.Since(startTime)
   110  		err := task.Refresh()
   111  		if err != nil {
   112  			return fmt.Errorf("%s : %s", errorRetrievingTask, err)
   113  		}
   114  
   115  		// If an inspection function is provided, we pass information about the task processing:
   116  		// * the task itself
   117  		// * the number of iterations
   118  		// * how much time we have spent querying the task so far
   119  		// * whether this is the first iteration
   120  		// * whether this is the last iteration
   121  		// It's up to the inspection function to render this information fittingly.
   122  
   123  		// If task is not in a waiting status we're done, check if there's an error and return it.
   124  		if !isTaskRunning(task.Task.Status) {
   125  			if inspectionFunc != nil {
   126  				inspectionFunc(task.Task,
   127  					howManyTimesRefreshed,
   128  					elapsed,
   129  					howManyTimesRefreshed == 1,              // first
   130  					isTaskCompleteOrError(task.Task.Status), // last
   131  				)
   132  			}
   133  			if task.Task.Status == "error" {
   134  				return fmt.Errorf("task did not complete successfully: %s", task.getErrorMessage(err))
   135  			}
   136  			return nil
   137  		}
   138  
   139  		// If the environment variable "GOVCD_TASK_MONITOR" is set, its value
   140  		// will be used to choose among pre-defined InspectionFunc
   141  		if inspectionFunc == nil {
   142  			if taskMonitor != "" {
   143  				switch taskMonitor {
   144  				case "log":
   145  					inspectionFunc = LogTask // writes full task details to the log
   146  				case "show":
   147  					inspectionFunc = ShowTask // writes full task details to the screen
   148  				case "simple_log":
   149  					inspectionFunc = SimpleLogTask // writes a summary line for the task to the log
   150  				case "simple_show":
   151  					inspectionFunc = SimpleShowTask // writes a summary line for the task to the screen
   152  				case "minimal_show":
   153  					inspectionFunc = MinimalShowTask // writes a dot for each iteration, or "+" for success, "-" for failure
   154  				}
   155  			}
   156  		}
   157  		if inspectionFunc != nil {
   158  			inspectionFunc(task.Task,
   159  				howManyTimesRefreshed,
   160  				elapsed,
   161  				howManyTimesRefreshed == 1, // first
   162  				false,                      // last
   163  			)
   164  		}
   165  
   166  		// Sleep for a given period and try again.
   167  		time.Sleep(delay)
   168  	}
   169  }
   170  
   171  // WaitTaskCompletion checks the status of the task every 3 seconds and returns when the
   172  // task is either completed or failed
   173  func (task *Task) WaitTaskCompletion() error {
   174  	return task.WaitInspectTaskCompletion(nil, 3*time.Second)
   175  }
   176  
   177  // GetTaskProgress retrieves the task progress as a string
   178  func (task *Task) GetTaskProgress() (string, error) {
   179  	if task.Task == nil {
   180  		return "", fmt.Errorf("cannot refresh, Object is empty")
   181  	}
   182  
   183  	err := task.Refresh()
   184  	if err != nil {
   185  		return "", fmt.Errorf("error retrieving task: %s", err)
   186  	}
   187  
   188  	if task.Task.Status == "error" {
   189  		return "", fmt.Errorf("task did not complete successfully: %s", task.getErrorMessage(err))
   190  	}
   191  
   192  	return strconv.Itoa(task.Task.Progress), nil
   193  }
   194  
   195  // CancelTask attempts a task cancellation, returning an error if cancellation fails
   196  func (task *Task) CancelTask() error {
   197  	cancelTaskURL, err := url.ParseRequestURI(task.Task.HREF + "/action/cancel")
   198  	if err != nil {
   199  		util.Logger.Printf("[CancelTask] Error parsing task request URI %v: %s", cancelTaskURL.String(), err)
   200  		return err
   201  	}
   202  
   203  	request := task.client.NewRequest(map[string]string{}, http.MethodPost, *cancelTaskURL, nil)
   204  	_, err = checkResp(task.client.Http.Do(request))
   205  	if err != nil {
   206  		util.Logger.Printf("[CancelTask] Error cancelling task  %v: %s", cancelTaskURL.String(), err)
   207  		return err
   208  	}
   209  	util.Logger.Printf("[CancelTask] task %s CANCELED\n", task.Task.ID)
   210  	return nil
   211  }
   212  
   213  // ResourceInProgress returns true if any of the provided tasks is still running
   214  func ResourceInProgress(tasksInProgress *types.TasksInProgress) bool {
   215  	util.Logger.Printf("[TRACE] ResourceInProgress - has tasks %v\n", tasksInProgress != nil)
   216  	if tasksInProgress == nil {
   217  		return false
   218  	}
   219  	tasks := tasksInProgress.Task
   220  	for _, task := range tasks {
   221  		if isTaskCompleteOrError(task.Status) {
   222  			continue
   223  		}
   224  		if isTaskRunning(task.Status) {
   225  			return true
   226  		}
   227  	}
   228  	return false
   229  }
   230  
   231  // ResourceComplete return true is none of its tasks are running
   232  func ResourceComplete(tasksInProgress *types.TasksInProgress) bool {
   233  	util.Logger.Printf("[TRACE] ResourceComplete - has tasks %v\n", tasksInProgress != nil)
   234  	return !ResourceInProgress(tasksInProgress)
   235  }
   236  
   237  // WaitResource waits for the tasks associated to a given resource to complete
   238  func WaitResource(refresh func() (*types.TasksInProgress, error)) error {
   239  	util.Logger.Printf("[TRACE] WaitResource \n")
   240  	tasks, err := refresh()
   241  	if tasks == nil {
   242  		return nil
   243  	}
   244  	for err == nil {
   245  		time.Sleep(time.Second)
   246  		tasks, err = refresh()
   247  		if err != nil {
   248  			return err
   249  		}
   250  		if tasks == nil || ResourceComplete(tasks) {
   251  			return nil
   252  		}
   253  	}
   254  	return nil
   255  }
   256  
   257  // SkimTasksList checks a list of tasks and returns a list of tasks still in progress and a list of failed ones
   258  func SkimTasksList(taskList []*Task) ([]*Task, []*Task, error) {
   259  	return SkimTasksListMonitor(taskList, nil)
   260  }
   261  
   262  // SkimTasksListMonitor checks a list of tasks and returns a list of tasks in progress and a list of failed ones
   263  // It can optionally do something with each task by means of a monitoring function
   264  func SkimTasksListMonitor(taskList []*Task, monitoringFunc TaskMonitoringFunc) ([]*Task, []*Task, error) {
   265  	var newTaskList []*Task
   266  	var errorList []*Task
   267  	for _, task := range taskList {
   268  		if task == nil {
   269  			continue
   270  		}
   271  		err := task.Refresh()
   272  		if err != nil {
   273  			if strings.Contains(err.Error(), errorRetrievingTask) {
   274  				// Task was not found. Probably expired. We don't need it anymore
   275  				continue
   276  			}
   277  			return newTaskList, errorList, err
   278  		}
   279  		if monitoringFunc != nil {
   280  			monitoringFunc(task.Task)
   281  		}
   282  		// if a cancellation was requested, we can ignore the task
   283  		if task.Task.CancelRequested {
   284  			continue
   285  		}
   286  		// If the task was completed successfully, or it was abandoned, we don't need further processing
   287  		if isTaskComplete(task.Task.Status) {
   288  			continue
   289  		}
   290  		// if the task failed, we add it to the special list
   291  		if task.Task.Status == "error" && !task.Task.CancelRequested {
   292  			errorList = append(errorList, task)
   293  			continue
   294  		}
   295  		// If the task is running, we add it to the list that will continue to be monitored
   296  		if isTaskRunning(task.Task.Status) {
   297  			newTaskList = append(newTaskList, task)
   298  		}
   299  	}
   300  	return newTaskList, errorList, nil
   301  }
   302  
   303  // isTaskRunning returns true if the task has started or is about to start
   304  func isTaskRunning(status string) bool {
   305  	return status == "running" || status == "preRunning" || status == "queued"
   306  }
   307  
   308  // isTaskComplete returns true if the task has finished successfully or was interrupted, but not if it finished with error
   309  func isTaskComplete(status string) bool {
   310  	return status == "success" || status == "aborted"
   311  }
   312  
   313  // isTaskCompleteOrError returns true if the status has finished, regardless of the outcome
   314  func isTaskCompleteOrError(status string) bool {
   315  	return isTaskComplete(status) || status == "error"
   316  }
   317  
   318  // WaitTaskListCompletion continuously skims the task list until no tasks in progress are left
   319  func WaitTaskListCompletion(taskList []*Task) ([]*Task, error) {
   320  	return WaitTaskListCompletionMonitor(taskList, nil)
   321  }
   322  
   323  // WaitTaskListCompletionMonitor continuously skims the task list until no tasks in progress are left
   324  // Using a TaskMonitoringFunc, it can display or log information as the list reduction happens
   325  func WaitTaskListCompletionMonitor(taskList []*Task, f TaskMonitoringFunc) ([]*Task, error) {
   326  	var failedTaskList []*Task
   327  	var err error
   328  	for len(taskList) > 0 {
   329  		taskList, failedTaskList, err = SkimTasksListMonitor(taskList, f)
   330  		if err != nil {
   331  			return failedTaskList, err
   332  		}
   333  		time.Sleep(3 * time.Second)
   334  	}
   335  	if len(failedTaskList) == 0 {
   336  		return nil, nil
   337  	}
   338  	return failedTaskList, fmt.Errorf("%d tasks have failed", len(failedTaskList))
   339  }
   340  
   341  // GetTaskByHREF retrieves a task by its HREF
   342  func (client *Client) GetTaskByHREF(taskHref string) (*Task, error) {
   343  	task := NewTask(client)
   344  
   345  	_, err := client.ExecuteRequest(taskHref, http.MethodGet,
   346  		"", "error retrieving task: %s", nil, task.Task)
   347  	if err != nil {
   348  		return nil, fmt.Errorf("%s : %s", ErrorEntityNotFound, err)
   349  	}
   350  
   351  	return task, nil
   352  }
   353  
   354  // GetTaskById retrieves a task by ID
   355  func (client *Client) GetTaskById(taskId string) (*Task, error) {
   356  	// Builds the task HREF using the VCD HREF + /task/{ID} suffix
   357  	taskHref, err := url.JoinPath(client.VCDHREF.String(), "task", extractUuid(taskId))
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	return client.GetTaskByHREF(taskHref)
   362  }
   363  
   364  // SkimTasksList checks a list of task IDs and returns a list of IDs for tasks in progress and a list of IDs for failed ones
   365  func (client Client) SkimTasksList(taskIdList []string) ([]string, []string, error) {
   366  	var seenTasks = make(map[string]bool)
   367  	var newTaskList []string
   368  	var errorList []string
   369  	for i, taskId := range taskIdList {
   370  		_, seen := seenTasks[taskId]
   371  		if seen {
   372  			continue
   373  		}
   374  		seenTasks[taskId] = true
   375  		task, err := client.GetTaskById(taskId)
   376  		if err != nil {
   377  			if strings.Contains(err.Error(), errorRetrievingTask) {
   378  				// Task was not found. Probably expired. We don't need it anymore
   379  				continue
   380  			}
   381  			return newTaskList, errorList, err
   382  		}
   383  		util.Logger.Printf("[SkimTasksList] {%d} task %s %s (status %s - cancel requested: %v)\n", i, task.Task.Name, task.Task.ID, task.Task.Status, task.Task.CancelRequested)
   384  		if isTaskComplete(task.Task.Status) {
   385  			continue
   386  		}
   387  		if isTaskRunning(task.Task.Status) {
   388  			newTaskList = append(newTaskList, taskId)
   389  		}
   390  		if task.Task.Status == "error" && !task.Task.CancelRequested {
   391  			errorList = append(errorList, taskId)
   392  		}
   393  	}
   394  	return newTaskList, errorList, nil
   395  }
   396  
   397  // WaitTaskListCompletion waits until all tasks in the list are completed, removed, or failed
   398  // Returns a list of failed tasks and an error
   399  func (client Client) WaitTaskListCompletion(taskIdList []string, ignoreFailed bool) ([]string, error) {
   400  	var failedTaskList []string
   401  	var err error
   402  	for len(taskIdList) > 0 {
   403  		taskIdList, failedTaskList, err = client.SkimTasksList(taskIdList)
   404  		if err != nil {
   405  			return failedTaskList, err
   406  		}
   407  		time.Sleep(time.Second)
   408  	}
   409  	if len(failedTaskList) == 0 || ignoreFailed {
   410  		return nil, nil
   411  	}
   412  	return failedTaskList, fmt.Errorf("%d tasks have failed", len(failedTaskList))
   413  }
   414  
   415  // QueryTaskList performs a query for tasks according to a specific filter
   416  func (client *Client) QueryTaskList(filter map[string]string) ([]*types.QueryResultTaskRecordType, error) {
   417  	taskType := types.QtTask
   418  	if client.IsSysAdmin {
   419  		taskType = types.QtAdminTask
   420  	}
   421  
   422  	filterText := buildFilterTextWithLogicalOr(filter)
   423  
   424  	notEncodedParams := map[string]string{
   425  		"type": taskType,
   426  	}
   427  	if filterText != "" {
   428  		notEncodedParams["filter"] = filterText
   429  	}
   430  	results, err := client.cumulativeQuery(taskType, nil, notEncodedParams)
   431  	if err != nil {
   432  		return nil, fmt.Errorf("error querying task %s", err)
   433  	}
   434  
   435  	if client.IsSysAdmin {
   436  		return results.Results.AdminTaskRecord, nil
   437  	} else {
   438  		return results.Results.TaskRecord, nil
   439  	}
   440  }
   441  
   442  // buildFilterTextWithLogicalOr creates a filter with multiple values for a single column
   443  // Given a map entry "key": "value1,value2"
   444  // it creates a filter with a logical OR:  "key==value1,key==value2"
   445  func buildFilterTextWithLogicalOr(filter map[string]string) string {
   446  	filterText := ""
   447  	for k, v := range filter {
   448  		if filterText != "" {
   449  			filterText += ";" // logical AND
   450  		}
   451  		if strings.Contains(v, ",") {
   452  			valueText := ""
   453  			values := strings.Split(v, ",")
   454  			for _, value := range values {
   455  				if valueText != "" {
   456  					valueText += "," // logical OR
   457  				}
   458  				valueText += fmt.Sprintf("%s==%s", k, url.QueryEscape(value))
   459  			}
   460  			filterText += valueText
   461  		} else {
   462  			filterText += fmt.Sprintf("%s==%s", k, url.QueryEscape(v))
   463  		}
   464  	}
   465  	return filterText
   466  }
   467  
   468  // WaitForRouteAdvertisementTasks is a convenience function to query for unfinished Route
   469  // Advertisement tasks. An exact case for it was that updating some IP Space related objects (IP
   470  // Spaces, IP Space Uplinks). Updating such an object sometimes results in a separate task for Route
   471  // Advertisement being spun up (name="ipSpaceUplinkRouteAdvertisementSync"). When such task is
   472  // running - other operations may fail so it is best to wait for completion of such task before
   473  // triggering any other jobs.
   474  func (client *Client) WaitForRouteAdvertisementTasks() error {
   475  	name := "ipSpaceUplinkRouteAdvertisementSync"
   476  
   477  	util.Logger.Printf("[TRACE] WaitForRouteAdvertisementTasks attempting to search for unfinished tasks with name='%s'", name)
   478  	allTasks, err := client.QueryTaskList(map[string]string{
   479  		"status": "running,preRunning,queued",
   480  		"name":   name,
   481  	})
   482  	if err != nil {
   483  		return fmt.Errorf("error retrieving all running '%s' tasks: %s", name, err)
   484  	}
   485  
   486  	util.Logger.Printf("[TRACE] WaitForRouteAdvertisementTasks got %d unifinished tasks with name='%s'", len(allTasks), name)
   487  	for _, singleQueryTask := range allTasks {
   488  		task := NewTask(client)
   489  		task.Task.HREF = singleQueryTask.HREF
   490  
   491  		err = task.WaitTaskCompletion()
   492  		if err != nil {
   493  			return fmt.Errorf("error waiting for task '%s' of type '%s' to finish: %s", singleQueryTask.HREF, name, err)
   494  		}
   495  	}
   496  
   497  	return nil
   498  }