github.com/vmware/govmomi@v0.51.0/cli/task/recent.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 task 6 7 import ( 8 "context" 9 "flag" 10 "fmt" 11 "io" 12 "strings" 13 "time" 14 15 "github.com/vmware/govmomi/cli" 16 "github.com/vmware/govmomi/cli/flags" 17 "github.com/vmware/govmomi/property" 18 "github.com/vmware/govmomi/task" 19 "github.com/vmware/govmomi/view" 20 "github.com/vmware/govmomi/vim25" 21 "github.com/vmware/govmomi/vim25/methods" 22 "github.com/vmware/govmomi/vim25/mo" 23 "github.com/vmware/govmomi/vim25/types" 24 ) 25 26 type recent struct { 27 *flags.DatacenterFlag 28 29 max int 30 follow bool 31 long bool 32 33 state flags.StringList 34 begin time.Duration 35 end time.Duration 36 r bool 37 38 plain bool 39 } 40 41 func init() { 42 cli.Register("tasks", &recent{}) 43 } 44 45 func (cmd *recent) Register(ctx context.Context, f *flag.FlagSet) { 46 cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx) 47 cmd.DatacenterFlag.Register(ctx, f) 48 49 f.IntVar(&cmd.max, "n", 25, "Output the last N tasks") 50 f.BoolVar(&cmd.follow, "f", false, "Follow recent task updates") 51 f.BoolVar(&cmd.long, "l", false, "Use long task description") 52 f.Var(&cmd.state, "s", "Task states") 53 f.DurationVar(&cmd.begin, "b", 0, "Begin time of task history") 54 f.DurationVar(&cmd.end, "e", 0, "End time of task history") 55 f.BoolVar(&cmd.r, "r", false, "Include child entities when PATH is specified") 56 } 57 58 func (cmd *recent) Description() string { 59 return `Display info for recent tasks. 60 61 When a task has completed, the result column includes the task duration on success or 62 error message on failure. If a task is still in progress, the result column displays 63 the completion percentage and the task ID. The task ID can be used as an argument to 64 the 'task.cancel' command. 65 66 By default, all recent tasks are included (via TaskManager), but can be limited by PATH 67 to a specific inventory object. 68 69 Examples: 70 govc tasks # tasks completed within the past 10 minutes 71 govc tasks -b 24h # tasks completed within the past 24 hours 72 govc tasks -s queued -s running # incomplete tasks 73 govc tasks -s error -s success # completed tasks 74 govc tasks -r /dc1/vm/Namespaces # tasks for VMs in this Folder only 75 govc tasks -f 76 govc tasks -f /dc1/host/cluster1` 77 } 78 79 func (cmd *recent) Usage() string { 80 return "[PATH]" 81 } 82 83 func (cmd *recent) Process(ctx context.Context) error { 84 if err := cmd.DatacenterFlag.Process(ctx); err != nil { 85 return err 86 } 87 return nil 88 } 89 90 // chop middle of s if len(s) > n 91 func chop(s string, n int) string { 92 diff := len(s) - n 93 if diff <= 0 { 94 return s 95 } 96 diff /= 2 97 m := len(s) / 2 98 99 return s[:m-diff] + "*" + s[1+m+diff:] 100 } 101 102 // taskName describes the tasks similar to the ESX ui 103 func taskName(info *types.TaskInfo) string { 104 name := strings.TrimSuffix(info.Name, "_Task") 105 switch name { 106 case "": 107 return info.DescriptionId 108 case "Destroy", "Rename": 109 return info.Entity.Type + "." + name 110 default: 111 return name 112 } 113 } 114 115 type history struct { 116 *task.HistoryCollector 117 118 cmd *recent 119 } 120 121 func (h *history) Collect(ctx context.Context, f func([]types.TaskInfo)) error { 122 for { 123 tasks, err := h.ReadNextTasks(ctx, 10) 124 if err != nil { 125 return err 126 } 127 128 if len(tasks) == 0 { 129 if h.cmd.follow { 130 // TODO: this only follows new events. 131 // need to watch TaskHistoryCollector.LatestPage for updates to existing Tasks 132 time.Sleep(time.Second) 133 continue 134 } 135 break 136 } 137 138 f(tasks) 139 } 140 return nil 141 } 142 143 type collector interface { 144 Collect(context.Context, func([]types.TaskInfo)) error 145 Destroy(context.Context) error 146 } 147 148 // useRecent returns true if any options are specified that require use of TaskHistoryCollector 149 func (cmd *recent) useRecent() bool { 150 return cmd.begin == 0 && cmd.end == 0 && !cmd.r && len(cmd.state) == 0 151 } 152 153 func (cmd *recent) newCollector(ctx context.Context, c *vim25.Client, ref *types.ManagedObjectReference) (collector, error) { 154 if cmd.useRecent() { 155 // original flavor of this command that uses `RecentTask` instead of `TaskHistoryCollector` 156 if ref == nil { 157 ref = c.ServiceContent.TaskManager 158 } 159 160 v, err := view.NewManager(c).CreateTaskView(ctx, ref) 161 if err != nil { 162 return nil, err 163 } 164 165 v.Follow = cmd.follow && cmd.plain 166 return v, nil 167 } 168 169 m := task.NewManager(c) 170 r := types.TaskFilterSpecRecursionOptionSelf 171 if ref == nil { 172 ref = &c.ServiceContent.RootFolder 173 cmd.r = true 174 } 175 176 now, err := methods.GetCurrentTime(ctx, c) // vCenter server time (UTC) 177 if err != nil { 178 return nil, err 179 } 180 181 if cmd.r { 182 r = types.TaskFilterSpecRecursionOptionAll 183 } 184 185 if cmd.begin == 0 { 186 cmd.begin = 10 * time.Minute 187 } 188 189 filter := types.TaskFilterSpec{ 190 Entity: &types.TaskFilterSpecByEntity{ 191 Entity: *ref, 192 Recursion: r, 193 }, 194 Time: &types.TaskFilterSpecByTime{ 195 TimeType: types.TaskFilterSpecTimeOptionStartedTime, 196 BeginTime: types.NewTime(now.Add(-cmd.begin)), 197 }, 198 } 199 200 for _, state := range cmd.state { 201 filter.State = append(filter.State, types.TaskInfoState(state)) 202 } 203 204 if cmd.end != 0 { 205 filter.Time.EndTime = types.NewTime(now.Add(-cmd.end)) 206 } 207 208 collector, err := m.CreateCollectorForTasks(ctx, filter) 209 if err != nil { 210 return nil, err 211 } 212 213 return &history{collector, cmd}, nil 214 } 215 216 func (cmd *recent) Run(ctx context.Context, f *flag.FlagSet) error { 217 if f.NArg() > 1 { 218 return flag.ErrHelp 219 } 220 221 c, err := cmd.Client() 222 if err != nil { 223 return err 224 } 225 226 m := c.ServiceContent.TaskManager 227 228 tn := taskName 229 230 if cmd.long { 231 var o mo.TaskManager 232 err = property.DefaultCollector(c).RetrieveOne(ctx, *m, []string{"description.methodInfo"}, &o) 233 if err != nil { 234 return err 235 } 236 237 desc := make(map[string]string, len(o.Description.MethodInfo)) 238 239 for _, entry := range o.Description.MethodInfo { 240 info := entry.GetElementDescription() 241 desc[info.Key] = info.Label 242 } 243 244 tn = func(info *types.TaskInfo) string { 245 if name, ok := desc[info.DescriptionId]; ok { 246 return name 247 } 248 249 return taskName(info) 250 } 251 } 252 253 var watch *types.ManagedObjectReference 254 255 if f.NArg() == 1 { 256 refs, merr := cmd.ManagedObjects(ctx, f.Args()) 257 if merr != nil { 258 return merr 259 } 260 if len(refs) != 1 { 261 return fmt.Errorf("%s matches %d objects", f.Arg(0), len(refs)) 262 } 263 watch = &refs[0] 264 } 265 266 // writes dump/json/xml once even if follow is specified, otherwise syntax error occurs 267 cmd.plain = !(cmd.Dump || cmd.JSON || cmd.XML) 268 269 v, err := cmd.newCollector(ctx, c, watch) 270 if err != nil { 271 return err 272 } 273 274 defer func() { 275 _ = v.Destroy(context.Background()) 276 }() 277 278 res := &taskResult{name: tn} 279 if cmd.plain { 280 res.WriteHeader(cmd.Out) 281 } 282 283 updated := false 284 285 return v.Collect(ctx, func(tasks []types.TaskInfo) { 286 if !updated && len(tasks) > cmd.max { 287 tasks = tasks[len(tasks)-cmd.max:] 288 } 289 updated = true 290 291 res.Tasks = tasks 292 cmd.WriteResult(res) 293 }) 294 } 295 296 type taskResult struct { 297 Tasks []types.TaskInfo `json:"tasks"` 298 last string 299 name func(info *types.TaskInfo) string 300 } 301 302 func (t *taskResult) WriteHeader(w io.Writer) { 303 fmt.Fprintf(w, t.format("Task", "Target", "Initiator", "Queued", "Started", "Completed", "Result")) 304 } 305 306 func (t *taskResult) Write(w io.Writer) error { 307 stamp := "15:04:05" 308 309 for _, info := range t.Tasks { 310 var user string 311 312 switch x := info.Reason.(type) { 313 case *types.TaskReasonUser: 314 user = x.UserName 315 } 316 317 if info.EntityName == "" || user == "" { 318 continue 319 } 320 321 ruser := strings.SplitN(user, "\\", 2) 322 if len(ruser) == 2 { 323 user = ruser[1] // discard domain 324 } else { 325 user = strings.TrimPrefix(user, "com.vmware.") // e.g. com.vmware.vsan.health 326 } 327 328 queued := "-" 329 start := "-" 330 end := start 331 332 if info.StartTime != nil { 333 start = info.StartTime.Format(stamp) 334 queued = info.StartTime.Sub(info.QueueTime).Round(time.Millisecond).String() 335 } 336 337 msg := fmt.Sprintf("%2d%% %s", info.Progress, info.Task) 338 339 if info.CompleteTime != nil && info.StartTime != nil { 340 msg = info.CompleteTime.Sub(*info.StartTime).String() 341 342 if info.State == types.TaskInfoStateError { 343 msg = strings.TrimSuffix(info.Error.LocalizedMessage, ".") 344 } 345 346 end = info.CompleteTime.Format(stamp) 347 } 348 349 result := fmt.Sprintf("%-7s [%s]", info.State, msg) 350 351 item := t.format(chop(t.name(&info), 40), chop(info.EntityName, 30), chop(user, 30), queued, start, end, result) 352 353 if item == t.last { 354 continue // task info was updated, but the fields we display were not 355 } 356 t.last = item 357 358 fmt.Fprint(w, item) 359 } 360 361 return nil 362 } 363 364 func (t *taskResult) format(task, target, initiator, queued, started, completed, result string) string { 365 return fmt.Sprintf("%-40s %-30s %-30s %9s %9s %9s %s\n", 366 task, target, initiator, queued, started, completed, result) 367 }