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 }