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 }