github.com/vmware/govmomi@v0.37.2/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  }