From 2b0fb2b56e0fb96d5bde03fcf864d83a27e365eb Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Mon, 20 Feb 2017 14:54:03 -0800 Subject: [PATCH] Add vsan and disk commands / helpers * Add DatastoreFileManager API wrapper * Add HostVsanInternalSystem API wrappers * Add govc datastore.disk.create command * Add govc datastore.vsan.ls and datastore.vsan.rm commands * Use DatastoreFileManager in govc datastore.rm command --- govc/datastore/cp.go | 8 ++ govc/datastore/disk/create.go | 97 ++++++++++++++ govc/datastore/ls.go | 15 ++- govc/datastore/mv.go | 8 ++ govc/datastore/rm.go | 37 ++++-- govc/datastore/vsan/ls.go | 156 +++++++++++++++++++++++ govc/datastore/vsan/rm.go | 102 +++++++++++++++ govc/emacs/govc.el | 19 ++- govc/main.go | 2 + govc/test/clean.sh | 1 + govc/test/datastore.bats | 45 +++++++ govc/test/datastore_file_manager_test.sh | 87 +++++++++++++ govc/test/test_helper.bash | 21 +-- govc/test/vm.bats | 2 +- govc/vm/disk/create.go | 7 + object/datastore_file_manager.go | 132 +++++++++++++++++++ object/host_config_manager.go | 16 +++ object/host_vsan_internal_system.go | 117 +++++++++++++++++ 18 files changed, 838 insertions(+), 34 deletions(-) create mode 100644 govc/datastore/disk/create.go create mode 100644 govc/datastore/vsan/ls.go create mode 100644 govc/datastore/vsan/rm.go create mode 100755 govc/test/datastore_file_manager_test.sh create mode 100644 object/datastore_file_manager.go create mode 100644 object/host_vsan_internal_system.go diff --git a/govc/datastore/cp.go b/govc/datastore/cp.go index 44925c54d..83c495535 100644 --- a/govc/datastore/cp.go +++ b/govc/datastore/cp.go @@ -61,6 +61,14 @@ func (cmd *cp) Usage() string { return "SRC DST" } +func (cmd *cp) Description() string { + return `Copy SRC to DST on DATASTORE. + +Examples: + govc datastore.cp foo/foo.vmx foo/foo.vmx.old + govc datastore.cp -f my.vmx foo/foo.vmx` +} + func (cmd *cp) Run(ctx context.Context, f *flag.FlagSet) error { args := f.Args() if len(args) != 2 { diff --git a/govc/datastore/disk/create.go b/govc/datastore/disk/create.go new file mode 100644 index 000000000..87defd12a --- /dev/null +++ b/govc/datastore/disk/create.go @@ -0,0 +1,97 @@ +/* +Copyright (c) 2017 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package disk + +import ( + "context" + "flag" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/units" + "github.com/vmware/govmomi/vim25/types" +) + +type create struct { + *flags.DatastoreFlag + + Bytes units.ByteSize +} + +func init() { + cli.Register("datastore.disk.create", &create{}) +} + +func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) { + cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx) + cmd.DatastoreFlag.Register(ctx, f) + + _ = cmd.Bytes.Set("10G") + f.Var(&cmd.Bytes, "size", "Size of new disk") +} + +func (cmd *create) Process(ctx context.Context) error { + if err := cmd.DatastoreFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *create) Usage() string { + return "VMDK" +} + +func (cmd *create) Description() string { + return `Create VMDK on DS. + +Examples: + govc datastore.disk.create -size 24G disks/disk1.vmdk` +} + +func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error { + if f.NArg() == 0 { + return flag.ErrHelp + } + + dc, err := cmd.Datacenter() + if err != nil { + return err + } + + ds, err := cmd.Datastore() + if err != nil { + return err + } + + m := object.NewVirtualDiskManager(ds.Client()) + + spec := &types.FileBackedVirtualDiskSpec{ + VirtualDiskSpec: types.VirtualDiskSpec{ + AdapterType: string(types.VirtualDiskAdapterTypeLsiLogic), + DiskType: string(types.VirtualDiskTypeThin), + }, + CapacityKb: int64(cmd.Bytes) / 1024, + } + + task, err := m.CreateVirtualDisk(ctx, ds.Path(f.Arg(0)), dc, spec) + if err != nil { + return err + } + + return task.Wait(ctx) +} diff --git a/govc/datastore/ls.go b/govc/datastore/ls.go index f0486c9cf..6c33a27f0 100644 --- a/govc/datastore/ls.go +++ b/govc/datastore/ls.go @@ -74,6 +74,17 @@ func (cmd *ls) Usage() string { return "[FILE]..." } +func isInvalid(err error) bool { + if f, ok := err.(types.HasFault); ok { + switch f.Fault().(type) { + case *types.InvalidArgument: + return true + } + } + + return false +} + func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error { ds, err := cmd.Datastore() if err != nil { @@ -113,7 +124,7 @@ func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error { r, err := cmd.ListPath(b, arg, spec) if err != nil { // Treat the argument as a match pattern if not found as directory - if i == 0 && types.IsFileNotFound(err) { + if i == 0 && types.IsFileNotFound(err) || isInvalid(err) { spec.MatchPattern[0] = path.Base(arg) arg = path.Dir(arg) continue @@ -187,7 +198,7 @@ func (o *listOutput) add(r types.HostDatastoreBrowserSearchResults) { } for _, p := range path { - if p[0] == '.' { + if len(p) != 0 && p[0] == '.' { return } } diff --git a/govc/datastore/mv.go b/govc/datastore/mv.go index 7c99550e3..ee11c3766 100644 --- a/govc/datastore/mv.go +++ b/govc/datastore/mv.go @@ -54,6 +54,14 @@ func (cmd *mv) Usage() string { return "SRC DST" } +func (cmd *mv) Description() string { + return `Move SRC to DST on DATASTORE. + +Examples: + govc datastore.mv foo/foo.vmx foo/foo.vmx.old + govc datastore.mv -f my.vmx foo/foo.vmx` +} + func (cmd *mv) Run(ctx context.Context, f *flag.FlagSet) error { args := f.Args() if len(args) != 2 { diff --git a/govc/datastore/rm.go b/govc/datastore/rm.go index 58676ba5f..2fadc013e 100644 --- a/govc/datastore/rm.go +++ b/govc/datastore/rm.go @@ -18,7 +18,6 @@ package datastore import ( "context" - "errors" "flag" "github.com/vmware/govmomi/govc/cli" @@ -30,6 +29,7 @@ import ( type rm struct { *flags.DatastoreFlag + kind bool force bool isNamespace bool } @@ -43,6 +43,7 @@ func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) { cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx) cmd.DatastoreFlag.Register(ctx, f) + f.BoolVar(&cmd.kind, "t", true, "Use file type to choose disk or file manager") f.BoolVar(&cmd.force, "f", false, "Force; ignore nonexistent files and arguments") f.BoolVar(&cmd.isNamespace, "namespace", false, "Path is uuid of namespace on vsan datastore") } @@ -58,10 +59,19 @@ func (cmd *rm) Usage() string { return "FILE" } +func (cmd *rm) Description() string { + return `Remove FILE from DATASTORE. + +Examples: + govc datastore.rm vm/vmware.log + govc datastore.rm vm + govc datastore.rm -f images/base.vmdk` +} + func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error { args := f.Args() if len(args) == 0 { - return errors.New("missing operand") + return flag.ErrHelp } c, err := cmd.Client() @@ -75,28 +85,27 @@ func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error { return err } + ds, err := cmd.Datastore() + if err != nil { + return err + } + if cmd.isNamespace { path := args[0] nm := object.NewDatastoreNamespaceManager(c) err = nm.DeleteDirectory(ctx, dc, path) } else { - var path string - var task *object.Task + fm := ds.NewFileManager(ctx, dc) - // TODO(PN): Accept multiple args - path, err = cmd.DatastorePath(args[0]) - if err != nil { - return err - } + fm.Force = cmd.force - m := object.NewFileManager(c) - task, err = m.DeleteDatastoreFile(ctx, path, dc) - if err != nil { - return err + remove := fm.DeleteFile // File delete + if cmd.kind { + remove = fm.Delete // VirtualDisk or File delete } - err = task.Wait(ctx) + err = remove(ctx, args[0]) } if err != nil { diff --git a/govc/datastore/vsan/ls.go b/govc/datastore/vsan/ls.go new file mode 100644 index 000000000..26b6288e9 --- /dev/null +++ b/govc/datastore/vsan/ls.go @@ -0,0 +1,156 @@ +/* +Copyright (c) 2017 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vsan + +import ( + "context" + "flag" + "fmt" + "net/url" + "text/tabwriter" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" +) + +type ls struct { + *flags.DatastoreFlag + + long bool + orphan bool +} + +func init() { + cli.Register("datastore.vsan.ls", &ls{}) +} + +func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) { + cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx) + cmd.DatastoreFlag.Register(ctx, f) + + f.BoolVar(&cmd.long, "l", false, "Long listing") + f.BoolVar(&cmd.orphan, "o", false, "List orphan objects") +} + +func (cmd *ls) Process(ctx context.Context) error { + if err := cmd.DatastoreFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *ls) Usage() string { + return "[UUID]..." +} + +func (cmd *ls) Description() string { + return `List vSAN DOM objects in DS. + +Examples: + govc datastore.vsan.ls + govc datastore.vsan.ls -ds vsanDatastore -l + govc datastore.vsan.ls -l d85aa758-63f5-500a-3150-0200308e589c` +} + +func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error { + ds, err := cmd.Datastore() + if err != nil { + return err + } + + var mds mo.Datastore + err = ds.Properties(ctx, ds.Reference(), []string{"summary"}, &mds) + if err != nil { + return err + } + + if mds.Summary.Type != "vsan" { + return flag.ErrHelp + } + + hosts, err := ds.AttachedHosts(ctx) + if err != nil { + return err + } + + if len(hosts) == 0 { + return flag.ErrHelp + } + + m, err := hosts[0].ConfigManager().VsanInternalSystem(ctx) + if err != nil { + return err + } + + ids, err := m.QueryVsanObjectUuidsByFilter(ctx, f.Args(), 0, 0) + if err != nil { + return err + } + + if len(ids) == 0 { + return nil + } + + if !cmd.long && !cmd.orphan { + for _, id := range ids { + fmt.Fprintln(cmd.Out, id) + } + + return nil + } + + objs, err := m.GetVsanObjExtAttrs(ctx, ids) + if err != nil { + return err + } + + u, err := url.Parse(mds.Summary.Url) + if err != nil { + return err + } + + tw := tabwriter.NewWriter(cmd.Out, 2, 0, 2, ' ', 0) + cmd.Out = tw + + for id, obj := range objs { + path := obj.DatastorePath(u.Path) + + if cmd.orphan { + _, err = ds.Stat(ctx, path) + if err == nil { + continue + } + + switch err.(type) { + case object.DatastoreNoSuchDirectoryError, object.DatastoreNoSuchFileError: + default: + return err + } + + if !cmd.long { + fmt.Fprintln(cmd.Out, id) + continue + } + } + + fmt.Fprintf(cmd.Out, "%s\t%s\t%s\n", id, obj.Class, path) + } + + return tw.Flush() +} diff --git a/govc/datastore/vsan/rm.go b/govc/datastore/vsan/rm.go new file mode 100644 index 000000000..8e3a66efc --- /dev/null +++ b/govc/datastore/vsan/rm.go @@ -0,0 +1,102 @@ +/* +Copyright (c) 2017 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vsan + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" +) + +type rm struct { + *flags.DatastoreFlag + + force bool +} + +func init() { + cli.Register("datastore.vsan.rm", &rm{}) +} + +func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) { + cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx) + cmd.DatastoreFlag.Register(ctx, f) + + f.BoolVar(&cmd.force, "f", false, "Force delete") +} + +func (cmd *rm) Process(ctx context.Context) error { + if err := cmd.DatastoreFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *rm) Usage() string { + return "UUID..." +} + +func (cmd *rm) Description() string { + return `Remove vSAN DOM objects in DS. + +Examples: + govc datastore.vsan.rm d85aa758-63f5-500a-3150-0200308e589c + govc datastore.vsan.rm -f d85aa758-63f5-500a-3150-0200308e589c + govc datastore.vsan.ls -o | xargs govc datastore.vsan.rm` +} + +func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error { + if f.NArg() == 0 { + return flag.ErrHelp + } + + ds, err := cmd.Datastore() + if err != nil { + return err + } + + hosts, err := ds.AttachedHosts(ctx) + if err != nil { + return err + } + + if len(hosts) == 0 { + return flag.ErrHelp + } + + m, err := hosts[0].ConfigManager().VsanInternalSystem(ctx) + if err != nil { + return err + } + + res, err := m.DeleteVsanObjects(ctx, f.Args(), &cmd.force) + if err != nil { + return err + } + + for _, r := range res { + if !r.Success { + fmt.Fprintf(os.Stderr, "%s: %s\n", r.Uuid, r.FailureReason[0].Message) + } + } + + return nil +} diff --git a/govc/emacs/govc.el b/govc/emacs/govc.el index 5b97afc60..350c692b2 100644 --- a/govc/emacs/govc.el +++ b/govc/emacs/govc.el @@ -164,6 +164,7 @@ The default prefix is `C-c ;' and can be changed by setting `govc-keymap-prefix' (list (list dired-re-mark '(0 dired-mark-face)) (list "types.ManagedObjectReference\\(.*\\)" '(1 dired-directory-face)) + (list "[^ ]*/$" '(0 dired-directory-face)) (list "\\.\\.\\.$" '(0 dired-symlink-face)))) (defvar govc-tabulated-list-mode-map @@ -711,7 +712,7 @@ Optionally specify JSON encoding." (cond ((s-blank? val)) - ((s-ends-with? "types.ManagedObjectReference" type) + ((and (not json) (s-ends-with? "types.ManagedObjectReference" type)) (let ((ids (govc "ls" "-L" (split-string val ",")))) (setq govc-args (list (govc-object-prompt "moid: " ids))))) ((string= val "...") @@ -1002,10 +1003,12 @@ Optionally filter by FILTER and inherit SESSION." "Open datastore folder or file." (interactive) (let ((id (tabulated-list-get-id))) - (if (s-ends-with? "/" id) - (progn (setq govc-filter id) - (tabulated-list-revert)) - (govc-datastore-open)))) + (if current-prefix-arg + (govc-shell-command (govc-format-command "datastore.ls" "-l" "-p" "-R" id)) + (if (s-ends-with? "/" id) + (progn (setq govc-filter id) + (tabulated-list-revert)) + (govc-datastore-open))))) (defun govc-datastore-open () "Open datastore file." @@ -1053,7 +1056,7 @@ Optionally filter by FILTER and inherit SESSION." (defun govc-datastore-rm (paths) "Delete datastore PATHS." - (--each paths (govc "datastore.rm" it))) + (--each paths (govc "datastore.rm" (if current-prefix-arg "-f") it))) (defun govc-datastore-rm-selection () "Delete selected datastore paths." @@ -1143,7 +1146,9 @@ Optionally filter by FILTER and inherit SESSION." (govc-session-clone session) (call-interactively 'govc-session)) (setq govc-filter filter) - (tabulated-list-print))) + (tabulated-list-print) + (if (and govc-session-datastore (search-forward govc-session-datastore nil t)) + (beginning-of-line)))) (define-derived-mode govc-datastore-mode tabulated-list-mode "Datastore" "Major mode for govc datastore.info." diff --git a/govc/main.go b/govc/main.go index 474b5c4c6..8d762dd0e 100644 --- a/govc/main.go +++ b/govc/main.go @@ -25,6 +25,8 @@ import ( _ "github.com/vmware/govmomi/govc/cluster" _ "github.com/vmware/govmomi/govc/datacenter" _ "github.com/vmware/govmomi/govc/datastore" + _ "github.com/vmware/govmomi/govc/datastore/disk" + _ "github.com/vmware/govmomi/govc/datastore/vsan" _ "github.com/vmware/govmomi/govc/device" _ "github.com/vmware/govmomi/govc/device/cdrom" _ "github.com/vmware/govmomi/govc/device/floppy" diff --git a/govc/test/clean.sh b/govc/test/clean.sh index c7700852c..f910e5dab 100755 --- a/govc/test/clean.sh +++ b/govc/test/clean.sh @@ -16,6 +16,7 @@ datastore_rm $GOVC_TEST_IMG datastore_rm $GOVC_TEST_ISO datastore_rm $GOVC_TEST_VMDK datastore_rm $(echo $GOVC_TEST_VMDK | sed 's/.vmdk/-flat.vmdk/') +datastore_rm $(dirname $GOVC_TEST_VMDK) # Recursively destroy all resource pools created by the test suite govc ls host/*/Resources/govc-test-* | \ diff --git a/govc/test/datastore.bats b/govc/test/datastore.bats index 1928d2996..2d37179d0 100755 --- a/govc/test/datastore.bats +++ b/govc/test/datastore.bats @@ -191,3 +191,48 @@ upload_file() { done done } + +@test "datastore.disk" { + id=$(new_id) + vmdk="$id/$id.vmdk" + + run govc datastore.mkdir "$id" + assert_success + + run govc datastore.disk.create "$vmdk" + assert_success + + run govc datastore.rm "$vmdk" + assert_success + + run govc datastore.mkdir -p "$id" + assert_success + + run govc datastore.disk.create "$vmdk" + assert_success + + id=$(new_id) + run govc vm.create -on=false -link -disk "$vmdk" "$id" + assert_success + + # should fail due to: ddb.deletable=false + run govc datastore.rm "$vmdk" + assert_failure + + run govc datastore.rm -f "$vmdk" + assert_success + + # one more time, but rm the directory w/o -f + run govc datastore.mkdir -p "$id" + assert_success + + run govc datastore.disk.create "$vmdk" + assert_success + + id=$(new_id) + run govc vm.create -on=false -link -disk "$vmdk" "$id" + assert_success + + run govc datastore.rm "$(dirname "$vmdk")" + assert_success +} diff --git a/govc/test/datastore_file_manager_test.sh b/govc/test/datastore_file_manager_test.sh new file mode 100755 index 000000000..9cb3f554c --- /dev/null +++ b/govc/test/datastore_file_manager_test.sh @@ -0,0 +1,87 @@ +#!/bin/bash -e + +# This test is not run via bats + +# See also: datastore.bats@test "datastore.disk" + +export GOVC_TEST_URL=$GOVC_URL + +. "$(dirname "$0")"/test_helper.bash + +echo -n "checking datastore type..." +type=$(govc object.collect -s "datastore/$GOVC_DATASTORE" summary.type) +echo "$type" + +if [ "$type" = "vsan" ] ; then + echo -n "checking for orphan objects..." + objs=($(govc datastore.vsan.ls -o)) + echo "${#objs[@]}" + + if [ "${#objs[@]}" -ne "0" ] ; then + govc datastore.vsan.rm "${objs[@]}" + fi +fi + +dir=govc-test-dfm + +echo "uploading plain file..." +cal | govc datastore.upload - "$dir/cal.txt" +echo "removing plain file..." +govc datastore.rm "$dir/cal.txt" + +scratch=$dir/govc-test-scratch/govc-test-scratch.vmdk + +govc datastore.mkdir -p "$(dirname $scratch)" + +echo "creating disk $scratch..." +govc datastore.disk.create -size 1M $scratch + +id=$(new_id) + +echo "creating $id VM with disk linked to $scratch..." +govc vm.create -on=false -link -disk $scratch "$id" +info=$(govc device.info -vm "$id" disk-*) +echo "$info" + +disk="$(grep Name: <<<"$info" | awk '{print $2}')" +vmdk="$id/$id.vmdk" + +echo "removing $disk device but keeping the .vmdk backing file..." +govc device.remove -vm "$id" -keep "$disk" + +echo -n "checking delta disk ddb.deletable..." +govc datastore.download "$vmdk" - | grep -q -v ddb.deletable +echo "yes" + +echo -n "checking scratch disk ddb.deletable..." +govc datastore.download "$scratch" - | grep ddb.deletable | grep -q false +echo "no" + +echo "removing $vmdk" +govc datastore.rm "$vmdk" + +echo -n "checking that rm $scratch fails..." +govc datastore.rm "$scratch" 2>/dev/null || echo "yes" + +echo -n "checking that rm -f $scratch deletes..." +govc datastore.rm -f "$scratch" && echo "yes" + +echo -n "checking for remaining files..." +govc datastore.ls -p -R "$dir" + +teardown + +status=0 + +if [ "$type" = "vsan" ] ; then + echo -n "checking for leaked objects..." + objs=($(govc datastore.vsan.ls -l -o | awk '{print $3}')) + echo "${#objs[@]}" + + if [ "${#objs[@]}" -ne "0" ] ; then + printf "%s\n" "${objs[@]}" + status=1 + fi +fi + +exit $status diff --git a/govc/test/test_helper.bash b/govc/test/test_helper.bash index a44fa22b4..f3831aa47 100644 --- a/govc/test/test_helper.bash +++ b/govc/test/test_helper.bash @@ -28,13 +28,13 @@ GOVC_IMAGES=$BATS_TEST_DIRNAME/images TTYLINUX_NAME=ttylinux-pc_i486-16.1 GOVC_TEST_VMDK_SRC=$GOVC_IMAGES/${TTYLINUX_NAME}-disk1.vmdk -GOVC_TEST_VMDK=$(basename $GOVC_TEST_VMDK_SRC) +GOVC_TEST_VMDK=govc-images/$(basename $GOVC_TEST_VMDK_SRC) GOVC_TEST_ISO_SRC=$GOVC_IMAGES/${TTYLINUX_NAME}.iso -GOVC_TEST_ISO=$(basename $GOVC_TEST_ISO_SRC) +GOVC_TEST_ISO=govc-images/$(basename $GOVC_TEST_ISO_SRC) GOVC_TEST_IMG_SRC=$GOVC_IMAGES/floppybird.img -GOVC_TEST_IMG=$(basename $GOVC_TEST_IMG_SRC) +GOVC_TEST_IMG=govc-images/$(basename $GOVC_TEST_IMG_SRC) PATH="$(dirname $BATS_TEST_DIRNAME):$PATH" @@ -49,17 +49,18 @@ new_id() { } import_ttylinux_vmdk() { - # TODO: fix datastore.ls do we don't need grep - govc datastore.ls | grep -q $GOVC_TEST_VMDK || \ - govc import.vmdk $GOVC_TEST_VMDK_SRC > /dev/null + govc datastore.mkdir -p govc-images + govc datastore.ls "$GOVC_TEST_VMDK" >/dev/null 2>&1 || \ + govc import.vmdk "$GOVC_TEST_VMDK_SRC" govc-images > /dev/null } datastore_upload() { src=$1 - dst=$(basename $src) - # TODO: fix datastore.ls do we don't need grep - govc datastore.ls | grep -q $dst || \ - govc datastore.upload $src $dst > /dev/null + dst=govc-images/$(basename $src) + + govc datastore.mkdir -p govc-images + govc datastore.ls "$dst" >/dev/null 2>&1 || \ + govc datastore.upload "$src" "$dst" > /dev/null } upload_img() { diff --git a/govc/test/vm.bats b/govc/test/vm.bats index a5235ad2e..bc9313ea4 100755 --- a/govc/test/vm.bats +++ b/govc/test/vm.bats @@ -407,7 +407,7 @@ load test_helper run govc vm.disk.attach -vm $vm -disk enoent.vmdk assert_failure "govc: Invalid configuration for device '0'." - run govc vm.disk.attach -vm $vm -disk $vm/$GOVC_TEST_VMDK -controller lsilogic-1000 + run govc vm.disk.attach -vm $vm -disk $vm/$(basename $GOVC_TEST_VMDK) -controller lsilogic-1000 assert_success result=$(govc device.ls -vm $vm | grep disk- | wc -l) [ $result -eq 2 ] diff --git a/govc/vm/disk/create.go b/govc/vm/disk/create.go index d297a3cde..4c0aac4eb 100644 --- a/govc/vm/disk/create.go +++ b/govc/vm/disk/create.go @@ -89,6 +89,13 @@ func (cmd *create) Process(ctx context.Context) error { return nil } +func (cmd *create) Description() string { + return `Create disk and attach to VM. + +Examples: + govc vm.disk.create -vm $name -name $name/disk1 -size 10G` +} + func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error { if len(cmd.Name) == 0 { return errors.New("please specify a disk name") diff --git a/object/datastore_file_manager.go b/object/datastore_file_manager.go new file mode 100644 index 000000000..10fd2e193 --- /dev/null +++ b/object/datastore_file_manager.go @@ -0,0 +1,132 @@ +/* +Copyright (c) 2017 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package object + +import ( + "bufio" + "bytes" + "context" + "fmt" + "path" + "strings" + + "github.com/vmware/govmomi/vim25/soap" +) + +// DatastoreFileManager combines FileManager and VirtualDiskManager to manage files on a Datastore +type DatastoreFileManager struct { + *Datastore + + Force bool + + dc *Datacenter + fm *FileManager + dm *VirtualDiskManager +} + +// NewFileManager creates a new instance of DatastoreFileManager +func (d Datastore) NewFileManager(ctx context.Context, dc *Datacenter) *DatastoreFileManager { + c := d.Client() + + m := &DatastoreFileManager{ + Datastore: &d, + dc: dc, + dm: NewVirtualDiskManager(c), + fm: NewFileManager(c), + } + + return m +} + +// Delete dispatches to the appropriate Delete method based on file name extension +func (m *DatastoreFileManager) Delete(ctx context.Context, name string) error { + switch path.Ext(name) { + case ".vmdk": + return m.DeleteVirtualDisk(ctx, name) + default: + return m.DeleteFile(ctx, name) + } +} + +// DeleteFile calls FileManager.DeleteDatastoreFile +func (m *DatastoreFileManager) DeleteFile(ctx context.Context, path string) error { + name := m.Path(path) + + task, err := m.fm.DeleteDatastoreFile(ctx, name, m.dc) + if err != nil { + return err + } + + return task.Wait(ctx) +} + +// DeleteVirtualDisk calls VirtualDiskManager.DeleteVirtualDisk +// Regardless of the Datastore type, DeleteVirtualDisk will fail if 'ddb.deletable=false', +// so if Force=true this method attempts to set 'ddb.deletable=true' before starting the delete task. +func (m *DatastoreFileManager) DeleteVirtualDisk(ctx context.Context, path string) error { + name := m.Path(path) + + if m.Force { + m.markDiskAsDeletable(ctx, path) + } + + task, err := m.dm.DeleteVirtualDisk(ctx, name, m.dc) + if err != nil { + return err + } + + return task.Wait(ctx) +} + +func (m *DatastoreFileManager) markDiskAsDeletable(ctx context.Context, path string) { + r, n, err := m.Download(ctx, path, &soap.DefaultDownload) + if err != nil { + return + } + + defer r.Close() + + if n > 2048 { + return // sanity check before reading; should be only a few hundred bytes + } + + hasFlag := false + buf := new(bytes.Buffer) + + s := bufio.NewScanner(r) + + for s.Scan() { + line := s.Text() + if strings.HasPrefix(line, "ddb.deletable") { + hasFlag = true + continue + } + + fmt.Fprintln(buf, line) + } + + if err := s.Err(); err != nil { + return // any error other than EOF + } + + if !hasFlag { + return // already deletable, so leave as-is + } + + // rewrite the .vmdk with ddb.deletable flag removed (the default is true) + _ = m.Upload(ctx, buf, path, &soap.DefaultUpload) +} diff --git a/object/host_config_manager.go b/object/host_config_manager.go index 6f061a6d1..123227ecc 100644 --- a/object/host_config_manager.go +++ b/object/host_config_manager.go @@ -105,6 +105,22 @@ func (m HostConfigManager) VsanSystem(ctx context.Context) (*HostVsanSystem, err return NewHostVsanSystem(m.c, *h.ConfigManager.VsanSystem), nil } +func (m HostConfigManager) VsanInternalSystem(ctx context.Context) (*HostVsanInternalSystem, error) { + var h mo.HostSystem + + err := m.Properties(ctx, m.Reference(), []string{"configManager.vsanInternalSystem"}, &h) + if err != nil { + return nil, err + } + + // Added in 5.5 + if h.ConfigManager.VsanInternalSystem == nil { + return nil, ErrNotSupported + } + + return NewHostVsanInternalSystem(m.c, *h.ConfigManager.VsanInternalSystem), nil +} + func (m HostConfigManager) AccountManager(ctx context.Context) (*HostAccountManager, error) { var h mo.HostSystem diff --git a/object/host_vsan_internal_system.go b/object/host_vsan_internal_system.go new file mode 100644 index 000000000..65e4587f6 --- /dev/null +++ b/object/host_vsan_internal_system.go @@ -0,0 +1,117 @@ +/* +Copyright (c) 2017 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package object + +import ( + "context" + "encoding/json" + + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/methods" + "github.com/vmware/govmomi/vim25/types" +) + +type HostVsanInternalSystem struct { + Common +} + +func NewHostVsanInternalSystem(c *vim25.Client, ref types.ManagedObjectReference) *HostVsanInternalSystem { + m := HostVsanInternalSystem{ + Common: NewCommon(c, ref), + } + + return &m +} + +// QueryVsanObjectUuidsByFilter returns vSAN DOM object uuids by filter. +func (m HostVsanInternalSystem) QueryVsanObjectUuidsByFilter(ctx context.Context, uuids []string, limit int32, version int32) ([]string, error) { + req := types.QueryVsanObjectUuidsByFilter{ + This: m.Reference(), + Uuids: uuids, + Limit: limit, + Version: version, + } + + res, err := methods.QueryVsanObjectUuidsByFilter(ctx, m.Client(), &req) + if err != nil { + return nil, err + } + + return res.Returnval, nil +} + +type VsanObjExtAttrs struct { + Type string `json:"Object type"` + Class string `json:"Object class"` + Size string `json:"Object size"` + Path string `json:"Object path"` + Name string `json:"User friendly name"` +} + +func (a *VsanObjExtAttrs) DatastorePath(dir string) string { + l := len(dir) + path := a.Path + + if len(path) >= l { + path = a.Path[l:] + } + + if path != "" { + return path + } + + return a.Name // vmnamespace +} + +// GetVsanObjExtAttrs is internal and intended for troubleshooting/debugging situations in the field. +// WARNING: This API can be slow because we do IOs (reads) to all the objects. +func (m HostVsanInternalSystem) GetVsanObjExtAttrs(ctx context.Context, uuids []string) (map[string]VsanObjExtAttrs, error) { + req := types.GetVsanObjExtAttrs{ + This: m.Reference(), + Uuids: uuids, + } + + res, err := methods.GetVsanObjExtAttrs(ctx, m.Client(), &req) + if err != nil { + return nil, err + } + + var attrs map[string]VsanObjExtAttrs + + err = json.Unmarshal([]byte(res.Returnval), &attrs) + + return attrs, err +} + +// DeleteVsanObjects is internal and intended for troubleshooting/debugging only. +// WARNING: This API can be slow because we do IOs to all the objects. +// DOM won't allow access to objects which have lost quorum. Such objects can be deleted with the optional "force" flag. +// These objects may however re-appear with quorum if the absent components come back (network partition gets resolved, etc.) +func (m HostVsanInternalSystem) DeleteVsanObjects(ctx context.Context, uuids []string, force *bool) ([]types.HostVsanInternalSystemDeleteVsanObjectsResult, error) { + req := types.DeleteVsanObjects{ + This: m.Reference(), + Uuids: uuids, + Force: force, + } + + res, err := methods.DeleteVsanObjects(ctx, m.Client(), &req) + if err != nil { + return nil, err + } + + return res.Returnval, nil +}