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  }