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