diff --git a/command/update.go b/command/update.go index a741cc6..7a15f6e 100644 --- a/command/update.go +++ b/command/update.go @@ -31,22 +31,32 @@ var updateCmd = &cobra.Command{ Long: "Update configuration of a VM", Example: ` +You can either change the cpu and/or mem or you can use the update command to shrink the disk. + To change the VM to use 2 cores and 512MB memory $ vermin update vm_01 --cpus 2 --mem 512 + +To shrink the disk size on the host machine +$ vermin update vm_01 --shrink-disk `, Run: func(cmd *cobra.Command, args []string) { vmName := normalizeVmName(args[0]) - var script string - if len(args) > 1 { - script = args[1] - checkFilePath(script) - } - cpus, _ := cmd.Flags().GetInt("cpus") - mem, _ := cmd.Flags().GetInt("mem") - if err := vms.Modify(vmName, cpus, mem); err != nil { - fmt.Println(err) - os.Exit(1) + shrink, _ := cmd.Flags().GetBool("shrink-disk") + + if shrink { + if err := vms.Shrink(vmName); err != nil { + fmt.Println(err) + os.Exit(1) + } + } else { + cpus, _ := cmd.Flags().GetInt("cpus") + mem, _ := cmd.Flags().GetInt("mem") + + if err := vms.Modify(vmName, cpus, mem); err != nil { + fmt.Println(err) + os.Exit(1) + } } }, Args: func(cmd *cobra.Command, args []string) error { @@ -54,11 +64,14 @@ $ vermin update vm_01 --cpus 2 --mem 512 return errors.New("vm required") } - cpus, _ := cmd.Flags().GetInt("cpus") - mem, _ := cmd.Flags().GetInt("mem") + shrink, _ := cmd.Flags().GetBool("shrink-disk") + if !shrink { + cpus, _ := cmd.Flags().GetInt("cpus") + mem, _ := cmd.Flags().GetInt("mem") - if cpus == 0 && mem == 0 { - return errors.New("should specify cpus and/or mem specs") + if cpus == 0 && mem == 0 { + return errors.New("should specify cpus and/or mem specs") + } } return nil }, @@ -78,4 +91,5 @@ func init() { // is called directly, e.g.: updateCmd.Flags().IntP("cpus", "c", 0, "Number of cpu cores") updateCmd.Flags().IntP("mem", "m", 0, "Memory size in mega bytes") + updateCmd.Flags().BoolP("shrink-disk", "", false, "Shrink the disk to reduce the size on the host machine") } diff --git a/hypervisor/base/types.go b/hypervisor/base/types.go index e5c94dd..c3176a7 100644 --- a/hypervisor/base/types.go +++ b/hypervisor/base/types.go @@ -35,6 +35,8 @@ type Hypervisor interface { GetBoxInfo(vmName string) (*Box, error) GetSubnet() (*Subnet, error) + + ShrinkDisk(vmName string) error } type MountPath struct { @@ -43,10 +45,16 @@ type MountPath struct { } type Box struct { - CPU string - Mem string - DiskSize string - MACAddr string + CPU string + Mem string + Disk *Disk + MACAddr string +} + +type Disk struct { + Size string + UUID string + Location string } type Subnet struct { diff --git a/hypervisor/hypervisor.go b/hypervisor/hypervisor.go index 9093dd1..89ba497 100644 --- a/hypervisor/hypervisor.go +++ b/hypervisor/hypervisor.go @@ -159,3 +159,13 @@ func GetSubnet() (*base.Subnet, error) { return h.GetSubnet() } + +func ShrinkDisk(vmName string) error { + + h, err := detect() + if err != nil { + return err + } + + return h.ShrinkDisk(vmName) +} diff --git a/hypervisor/virtualbox/box.go b/hypervisor/virtualbox/box.go index 7ce0b6c..98034c5 100644 --- a/hypervisor/virtualbox/box.go +++ b/hypervisor/virtualbox/box.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" ) type vbox struct { @@ -16,6 +17,7 @@ type vbox struct { MediaRegistry struct { HardDisks struct { HardDisk []struct { + UUID string `xml:"uuid,attr"` Location string `xml:"location,attr"` } `xml:"HardDisk"` } `xml:"HardDisks"` @@ -55,11 +57,22 @@ func getBoxInfo(vm string) (*base.Box, error) { diskLocation = vb.Machine.MediaRegistry.HardDisks.HardDisk[0].Location } + diskUUID := "" + if len(vb.Machine.MediaRegistry.HardDisks.HardDisk) > 0 { + diskUUID = strings.TrimFunc(vb.Machine.MediaRegistry.HardDisks.HardDisk[0].UUID, func(r rune) bool { + return r == '{' || r == '}' + }) + } + return &base.Box{ - CPU: cpuCount, - Mem: vb.Machine.Hardware.Memory.RAMSize, - DiskSize: getDiskSizeInGB(vm, diskLocation), - MACAddr: vb.Machine.Hardware.Network.Adapter.MACAddress, + CPU: cpuCount, + Mem: vb.Machine.Hardware.Memory.RAMSize, + Disk: &base.Disk{ + Size: getDiskSizeInGB(vm, diskLocation), + Location: diskLocation, + UUID: diskUUID, + }, + MACAddr: vb.Machine.Hardware.Network.Adapter.MACAddress, }, nil } diff --git a/hypervisor/virtualbox/disk.go b/hypervisor/virtualbox/disk.go new file mode 100644 index 0000000..c319b68 --- /dev/null +++ b/hypervisor/virtualbox/disk.go @@ -0,0 +1,93 @@ +package virtualbox + +import ( + "fmt" + "github.com/mhewedy/vermin/db" + "github.com/mhewedy/vermin/hypervisor/base" + "github.com/mhewedy/vermin/log" + "github.com/mhewedy/vermin/progress" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func (*virtualbox) ShrinkDisk(vmName string) error { + + stop := progress.Show("Shrinking disk", false) + defer stop() + + box, err := getBoxInfo(vmName) + if err != nil { + return err + } + + origDiskPath := filepath.Join(db.GetVMPath(vmName), box.Disk.Location) + + if isVMDK(box.Disk) { + + tmpDir, err := ioutil.TempDir("", "vermin_disk_shrink_"+vmName) + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + vdiPath := filepath.Join(tmpDir, box.Disk.Location+".vdi") + newVMDKPath := filepath.Join(tmpDir, box.Disk.Location) + + log.Debug("clone vmdk disk into vdi") + if err := vboxManage("clonehd", origDiskPath, vdiPath, "--format", "vdi").Run(); err != nil { + return err + } + log.Debug("shrink the vdi") + if err := vboxManage("modifyhd", vdiPath, "--compact").Run(); err != nil { + return err + } + log.Debug("clone the vdi back into a new vmdk") + if err := vboxManage("clonehd", vdiPath, newVMDKPath, "--format", "vmdk").Run(); err != nil { + return err + } + log.Debug("set the uuid of the new vmdk with the same uuid of the old vmdk") + if err := vboxManage("internalcommands", "sethduuid", newVMDKPath, box.Disk.UUID).Run(); err != nil { + return err + } + log.Debug("copy the new vmdk into the same location of the old vmdk") + return copyFile(newVMDKPath, origDiskPath) + + } else if isVDI(box.Disk) { + return vboxManage("modifyhd", origDiskPath, "--compact").Run() + } else { + return fmt.Errorf("unsupported disk format %s", box.Disk.Location) + } +} + +func isVMDK(disk *base.Disk) bool { + return strings.HasSuffix(strings.ToLower(disk.Location), ".vmdk") +} + +func isVDI(disk *base.Disk) bool { + return strings.HasSuffix(strings.ToLower(disk.Location), ".vdi") +} + +// Copy the src file to dst. Any existing file will be overwritten and will not +// copy file attributes. +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} diff --git a/vms/list.go b/vms/list.go index 9bec171..8dffa0d 100644 --- a/vms/list.go +++ b/vms/list.go @@ -27,7 +27,7 @@ type vmInfo struct { } func (v vmInfo) String() string { - return fmt.Sprintf(format, v.name, v.image, v.box.CPU, v.box.Mem, v.box.DiskSize, v.tags) + return fmt.Sprintf(format, v.name, v.image, v.box.CPU, v.box.Mem, v.box.Disk.Size, v.tags) } type vmInfoList []vmInfo diff --git a/vms/shrink_disk.go b/vms/shrink_disk.go new file mode 100644 index 0000000..74f5766 --- /dev/null +++ b/vms/shrink_disk.go @@ -0,0 +1,40 @@ +package vms + +import ( + "fmt" + "github.com/mhewedy/vermin/cmd/ssh" + "github.com/mhewedy/vermin/hypervisor" + "github.com/mhewedy/vermin/progress" +) + +func Shrink(vmName string) error { + + fmt.Println("The VM will restarted as part of the disk shrinking process.\n" + + "Please note that, this is a time-consuming process and requires a free disk space on your disk.") + + if err := ssh.EstablishConn(vmName); err != nil { + return err + } + + if err := zerofyDisk(vmName); err != nil { + return err + } + + if err := Stop(vmName); err != nil { + return err + } + + if err := hypervisor.ShrinkDisk(vmName); err != nil { + return err + } + + return Start(vmName) +} + +func zerofyDisk(vmName string) error { + stop := progress.Show("Filling free disk space with zeros", false) + defer stop() + // sometimes the an error returned, however the command succeed + _, _ = ssh.Execute(vmName, "sh -c 'cat /dev/zero > zero.fill; sync; sleep 1; sync; rm -f zero.fill'") + return nil +}