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