github.com/vmware/govmomi@v0.51.0/simulator/task.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package simulator
     6  
     7  import (
     8  	"fmt"
     9  	"reflect"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/vmware/govmomi/vim25"
    14  	"github.com/vmware/govmomi/vim25/methods"
    15  	"github.com/vmware/govmomi/vim25/mo"
    16  	"github.com/vmware/govmomi/vim25/soap"
    17  	"github.com/vmware/govmomi/vim25/types"
    18  )
    19  
    20  const vTaskSuffix = "_Task" // vmomi suffix
    21  const sTaskSuffix = "Task"  // simulator suffix (avoiding golint warning)
    22  
    23  // TaskDelay applies to all tasks.
    24  // Names for DelayConfig.MethodDelay will differ for task and api delays. API
    25  // level names often look like PowerOff_Task, whereas the task name is simply
    26  // PowerOff.
    27  var TaskDelay = DelayConfig{}
    28  
    29  type Task struct {
    30  	mo.Task
    31  
    32  	ctx     *Context
    33  	Execute func(*Task) (types.AnyType, types.BaseMethodFault)
    34  }
    35  
    36  func NewTask(runner TaskRunner) *Task {
    37  	ref := runner.Reference()
    38  	name := reflect.TypeOf(runner).Elem().Name()
    39  	name = strings.Replace(name, "VM", "Vm", 1) // "VM" for the type to make go-lint happy, but "Vm" for the vmodl ID
    40  	return CreateTask(ref, name, runner.Run)
    41  }
    42  
    43  func CreateTask(e mo.Reference, name string, run func(*Task) (types.AnyType, types.BaseMethodFault)) *Task {
    44  	ref := e.Reference()
    45  	id := name
    46  
    47  	if strings.HasSuffix(id, sTaskSuffix) {
    48  		id = id[:len(id)-len(sTaskSuffix)]
    49  		name = id + vTaskSuffix
    50  	}
    51  
    52  	task := &Task{
    53  		Execute: run,
    54  	}
    55  
    56  	task.Info.Name = ucFirst(name)
    57  	task.Info.DescriptionId = fmt.Sprintf("%s.%s", ref.Type, id)
    58  	task.Info.Entity = &ref
    59  	task.Info.EntityName = ref.Value
    60  	task.Info.Reason = &types.TaskReasonUser{UserName: "vcsim"} // TODO: Context.Session.User
    61  	task.Info.QueueTime = time.Now()
    62  	task.Info.State = types.TaskInfoStateQueued
    63  
    64  	return task
    65  }
    66  
    67  type TaskRunner interface {
    68  	mo.Reference
    69  
    70  	Run(*Task) (types.AnyType, types.BaseMethodFault)
    71  }
    72  
    73  // taskReference is a helper struct so we can call AcquireLock in Run()
    74  type taskReference struct {
    75  	Self types.ManagedObjectReference
    76  }
    77  
    78  func (tr *taskReference) Reference() types.ManagedObjectReference {
    79  	return tr.Self
    80  }
    81  
    82  func (t *Task) Run(ctx *Context) types.ManagedObjectReference {
    83  	if ctx.Map.Path != vim25.Path {
    84  		// All Tasks live in the vim25 namespace
    85  		ctx = ctx.For(vim25.Path)
    86  	}
    87  
    88  	t.ctx = ctx
    89  
    90  	t.Self = ctx.Map.newReference(t)
    91  	t.Info.Key = t.Self.Value
    92  	t.Info.Task = t.Self
    93  	ctx.Map.Put(t)
    94  
    95  	ctx.Map.AtomicUpdate(t.ctx, t, []types.PropertyChange{
    96  		{Name: "info.startTime", Val: time.Now()},
    97  		{Name: "info.state", Val: types.TaskInfoStateRunning},
    98  	})
    99  
   100  	tr := &taskReference{
   101  		Self: *t.Info.Entity,
   102  	}
   103  
   104  	// in most cases, the caller already holds this lock, and we would like
   105  	// the lock to be held across the "hand off" to the async goroutine.
   106  	// however, with a TaskDelay, PropertyCollector (for example) cannot read
   107  	// any object properties while the lock is held.
   108  	handoff := true
   109  	if v, ok := TaskDelay.MethodDelay["LockHandoff"]; ok {
   110  		handoff = v != 0
   111  	}
   112  	var unlock func()
   113  	if handoff {
   114  		unlock = ctx.Map.AcquireLock(ctx, tr)
   115  	}
   116  	go func() {
   117  		TaskDelay.delay(t.Info.Name)
   118  		if !handoff {
   119  			unlock = ctx.Map.AcquireLock(ctx, tr)
   120  		}
   121  		res, err := t.Execute(t)
   122  		unlock()
   123  
   124  		state := types.TaskInfoStateSuccess
   125  		var fault any
   126  		if err != nil {
   127  			state = types.TaskInfoStateError
   128  			fault = types.LocalizedMethodFault{
   129  				Fault:            err,
   130  				LocalizedMessage: fmt.Sprintf("%T", err),
   131  			}
   132  		}
   133  
   134  		ctx.Map.AtomicUpdate(t.ctx, t, []types.PropertyChange{
   135  			{Name: "info.completeTime", Val: time.Now()},
   136  			{Name: "info.state", Val: state},
   137  			{Name: "info.result", Val: res},
   138  			{Name: "info.error", Val: fault},
   139  		})
   140  	}()
   141  
   142  	return t.Self
   143  }
   144  
   145  // RunBlocking() should only be used when an async simulator task needs to wait
   146  // on another async simulator task.
   147  // It polls for task completion to avoid the need to set up a PropertyCollector.
   148  func (t *Task) RunBlocking(ctx *Context) {
   149  	_ = t.Run(ctx)
   150  	t.Wait()
   151  }
   152  
   153  // Wait blocks until the task is complete.
   154  func (t *Task) Wait() {
   155  	// we do NOT want to share our lock with the tasks's context, because
   156  	// the goroutine that executes the task will use ctx to update the
   157  	// state (among other things).
   158  	isolatedLockingContext := &Context{}
   159  
   160  	isRunning := func() bool {
   161  		var running bool
   162  		t.ctx.Map.WithLock(isolatedLockingContext, t, func() {
   163  			switch t.Info.State {
   164  			case types.TaskInfoStateSuccess, types.TaskInfoStateError:
   165  				running = false
   166  			default:
   167  				running = true
   168  			}
   169  		})
   170  		return running
   171  	}
   172  
   173  	for isRunning() {
   174  		time.Sleep(10 * time.Millisecond)
   175  	}
   176  }
   177  
   178  func (t *Task) isDone() bool {
   179  	return t.Info.State == types.TaskInfoStateError || t.Info.State == types.TaskInfoStateSuccess
   180  }
   181  
   182  func (t *Task) SetTaskState(ctx *Context, req *types.SetTaskState) soap.HasFault {
   183  	body := new(methods.SetTaskStateBody)
   184  
   185  	if t.isDone() {
   186  		body.Fault_ = Fault("", new(types.InvalidState))
   187  		return body
   188  	}
   189  
   190  	changes := []types.PropertyChange{
   191  		{Name: "info.state", Val: req.State},
   192  	}
   193  
   194  	switch req.State {
   195  	case types.TaskInfoStateRunning:
   196  		changes = append(changes, types.PropertyChange{Name: "info.startTime", Val: time.Now()})
   197  	case types.TaskInfoStateError, types.TaskInfoStateSuccess:
   198  		changes = append(changes, types.PropertyChange{Name: "info.completeTime", Val: time.Now()})
   199  
   200  		if req.Fault != nil {
   201  			changes = append(changes, types.PropertyChange{Name: "info.error", Val: req.Fault})
   202  		}
   203  		if req.Result != nil {
   204  			changes = append(changes, types.PropertyChange{Name: "info.result", Val: req.Result})
   205  		}
   206  	}
   207  
   208  	ctx.Update(t, changes)
   209  
   210  	body.Res = new(types.SetTaskStateResponse)
   211  	return body
   212  }
   213  
   214  func (t *Task) SetTaskDescription(ctx *Context, req *types.SetTaskDescription) soap.HasFault {
   215  	body := new(methods.SetTaskDescriptionBody)
   216  
   217  	if t.isDone() {
   218  		body.Fault_ = Fault("", new(types.InvalidState))
   219  		return body
   220  	}
   221  
   222  	ctx.Update(t, []types.PropertyChange{{Name: "info.description", Val: req.Description}})
   223  
   224  	body.Res = new(types.SetTaskDescriptionResponse)
   225  	return body
   226  }
   227  
   228  func (t *Task) UpdateProgress(ctx *Context, req *types.UpdateProgress) soap.HasFault {
   229  	body := new(methods.UpdateProgressBody)
   230  
   231  	if t.Info.State != types.TaskInfoStateRunning {
   232  		body.Fault_ = Fault("", new(types.InvalidState))
   233  		return body
   234  	}
   235  
   236  	ctx.Update(t, []types.PropertyChange{{Name: "info.progress", Val: req.PercentDone}})
   237  
   238  	body.Res = new(types.UpdateProgressResponse)
   239  	return body
   240  }
   241  
   242  func (t *Task) CancelTask(ctx *Context, req *types.CancelTask) soap.HasFault {
   243  	body := new(methods.CancelTaskBody)
   244  
   245  	if t.isDone() {
   246  		body.Fault_ = Fault("", new(types.InvalidState))
   247  		return body
   248  	}
   249  
   250  	changes := []types.PropertyChange{
   251  		{Name: "info.cancelled", Val: true},
   252  		{Name: "info.completeTime", Val: time.Now()},
   253  		{Name: "info.state", Val: types.TaskInfoStateError},
   254  		{Name: "info.error", Val: &types.LocalizedMethodFault{
   255  			Fault:            &types.RequestCanceled{},
   256  			LocalizedMessage: "The task was canceled by a user",
   257  		}},
   258  	}
   259  
   260  	ctx.Update(t, changes)
   261  
   262  	body.Res = new(types.CancelTaskResponse)
   263  	return body
   264  }