From 63421b061b13449efa328002d02ea388da65cbb6 Mon Sep 17 00:00:00 2001 From: Muhammad Hewedy Date: Fri, 12 Jun 2020 13:04:41 +0300 Subject: [PATCH] implement commit function to commit VM into an image --- README.md | 1 + TODO.md | 1 - cmd/commit.go | 67 +++++++++++++++++++++++++++ images/commit.go | 51 +++++++++++++++++++++ images/download.go | 70 +++++++++++++++++++++++++++++ images/images.go | 110 --------------------------------------------- images/remote.go | 6 +-- images/utils.go | 22 +++++++++ images/writer.go | 43 ++++++++++++++++++ vms/vms.go | 13 ++++++ 10 files changed, 270 insertions(+), 114 deletions(-) create mode 100644 cmd/commit.go create mode 100644 images/commit.go create mode 100644 images/download.go create mode 100644 images/utils.go create mode 100644 images/writer.go diff --git a/README.md b/README.md index bff91df..d2288b2 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ $ vermin create Available Commands: + commit Commit a VM into a new Image completion Generates shell completion scripts cp Copy files/folders between a VM and the local filesystem or between two VMs create Create a new VM diff --git a/TODO.md b/TODO.md index 9f4a1cc..224d211 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,5 @@ * consider move ova images from dropbox (for ratelimit reasons) - consider use google drive (https://drive.google.com/uc?export=download&confirm=htAy&id=) - https://wasabi.com/ -* Work on Clone to clone a VM (export: [vboxmanage export vm_01 --ovf20 -o ~/Documents/temp.ova] then import) * Implement docker container names & ids (https://github.com/moby/moby/blob/634a848b8e3bdd8aed834559f3b2e0dfc7f5ae3a/pkg/stringid/stringid.go, https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go) diff --git a/cmd/commit.go b/cmd/commit.go new file mode 100644 index 0000000..b31407d --- /dev/null +++ b/cmd/commit.go @@ -0,0 +1,67 @@ +/* +Copyright © 2020 NAME HERE + +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 cmd + +import ( + "errors" + "fmt" + "github.com/mhewedy/vermin/vms" + "github.com/spf13/cobra" + "os" +) + +// commitCmd represents the commit command +var commitCmd = &cobra.Command{ + Use: "commit", + Short: "Commit a VM into a new Image", + Long: `Commit a VM into a new Image to be used later as a template to create VMs from.`, + Run: func(cmd *cobra.Command, args []string) { + + vmName := args[0] + imageName := args[1] + override, _ := cmd.Flags().GetBool("override") + + err := vms.Commit(vmName, imageName, override) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("vm required") + } + if len(args) < 2 { + return errors.New("new image name required, and should be in format base/name, example k8s/worker") + } + return nil + }, + ValidArgsFunction: listStoppedVms, +} + +func init() { + rootCmd.AddCommand(commitCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // commitCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + commitCmd.Flags().BoolP("override", "", false, "override any existing image") +} diff --git a/images/commit.go b/images/commit.go new file mode 100644 index 0000000..d6370d6 --- /dev/null +++ b/images/commit.go @@ -0,0 +1,51 @@ +package images + +import ( + "errors" + "fmt" + "github.com/mhewedy/vermin/command" + "io/ioutil" + "os" + "strings" +) + +func Commit(vmName, imageName string, override bool) error { + + if err := validateName(imageName); err != nil { + return err + } + + if !override { + existingImgs, _ := List() + if contains(existingImgs, imageName) { + return errors.New("Image with same name already exists, either choose a new name or use the --override flag") + } + } + + tmpDir, err := ioutil.TempDir("", "vermin_commit") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + ovaFile := tmpDir + strings.ReplaceAll(imageName, "/", "_") + ova + + export := command.VBoxManage("export", vmName, "--ovf20", "-o", ovaFile) + _, err = export.CallWithProgress(fmt.Sprintf("Committing %s into image %s", vmName, imageName)) + if err != nil { + return err + } + + tmpFile, err := os.Open(ovaFile) + if err != nil { + return err + } + defer tmpFile.Close() + + if err := writeNewImage(tmpFile, imageName); err != nil { + return err + } + + fmt.Printf("\nImage is ready, to create a VM from it use:\n$ vermin create %s\n", imageName) + return nil +} diff --git a/images/download.go b/images/download.go new file mode 100644 index 0000000..7907055 --- /dev/null +++ b/images/download.go @@ -0,0 +1,70 @@ +package images + +import ( + "fmt" + "github.com/schollz/progressbar/v3" + "io" + "io/ioutil" + "net/http" + "os" + "strings" +) + +func Download(image string) error { + // check image against cached + cached, err := listCachedImages() + if err != nil { + return err + } + + if contains(cached, image) { + return nil + } + + remote, err := listRemoteImages(false) + if err != nil { + return err + } + + dbImage, err := remote.findByName(image) + if err != nil { + return err + } + + return download(dbImage) +} + +func download(r *dbImage) error { + fmt.Printf("Image '%s' could not be found. Attempting to find and install \n", r.Name) + + // download to a temp file + tmpFile, err := ioutil.TempFile("", strings.ReplaceAll(r.Name, "/", "_")) + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + + resp, err := http.Get(r.URL) + if err != nil { + return err + } + defer resp.Body.Close() + + bar := buildDownloadBar(resp) + + if _, err = io.Copy(io.MultiWriter(tmpFile, bar), resp.Body); err != nil { + return err + } + + return writeNewImage(tmpFile, r.Name) +} + +func buildDownloadBar(resp *http.Response) *progressbar.ProgressBar { + bar := progressbar.DefaultBytes( + resp.ContentLength, + "Downloading", + ) + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "#", SaucerPadding: ".", BarStart: "[", BarEnd: "]"})(bar) + return bar +} diff --git a/images/images.go b/images/images.go index 21cf556..9d2d347 100644 --- a/images/images.go +++ b/images/images.go @@ -1,40 +1,5 @@ package images -import ( - "fmt" - "github.com/mhewedy/vermin/db" - "github.com/schollz/progressbar/v3" - "io" - "io/ioutil" - "net/http" - "os" - "strings" -) - -func Download(image string) error { - // check image against cached - cached, err := listCachedImages() - if err != nil { - return err - } - - if contains(cached, image) { - return nil - } - - remote, err := listRemoteImages(false) - if err != nil { - return err - } - - dbImage, err := remote.findByName(image) - if err != nil { - return err - } - - return download(dbImage) -} - func List() ([]string, error) { list, err := list(false) if err != nil { @@ -94,78 +59,3 @@ func list(purgeCache bool) ([]image, error) { } return result, nil } - -func download(r *dbImage) error { - fmt.Printf("Image '%s' could not be found. Attempting to find and install \n", r.Name) - - // download to a temp file - tmpFile, err := ioutil.TempFile("", strings.ReplaceAll(r.Name, "/", "_")) - if err != nil { - return err - } - defer os.Remove(tmpFile.Name()) - - resp, err := http.Get(r.URL) - if err != nil { - return err - } - defer resp.Body.Close() - - bar := buildDownloadBar(resp) - - if _, err = io.Copy(io.MultiWriter(tmpFile, bar), resp.Body); err != nil { - return err - } - - // copy the downloaded file to images directory - if err := os.MkdirAll(db.ImagesDir+"/"+strings.Split(r.Name, "/")[0], 0755); err != nil { - return err - } - - if err := copyFile(tmpFile.Name(), db.ImagesDir+"/"+r.Name+".ova"); err != nil { - return err - } - - return nil -} - -func buildDownloadBar(resp *http.Response) *progressbar.ProgressBar { - bar := progressbar.DefaultBytes( - resp.ContentLength, - "Downloading", - ) - progressbar.OptionSetTheme(progressbar.Theme{ - Saucer: "#", SaucerPadding: ".", BarStart: "[", BarEnd: "]"})(bar) - return bar -} - -func contains(a []string, s string) bool { - for i := range a { - if a[i] == s { - return true - } - } - return false -} - -// 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/images/remote.go b/images/remote.go index f877738..69499e6 100644 --- a/images/remote.go +++ b/images/remote.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "path/filepath" - "strings" ) const imagesCSVURL = "https://raw.githubusercontent.com/mhewedy/vermin/master/images/images.csv" @@ -77,8 +76,9 @@ func validate(vms []dbImage) error { for i := range vms { // check name - if len(strings.Split(vms[i].Name, "/")) != 2 { - return errors.New("Name doesn't follow pattern /") + + if err := validateName(vms[i].Name); err != nil { + return err } // check duplicate _, found := names[vms[i].Name] diff --git a/images/utils.go b/images/utils.go new file mode 100644 index 0000000..25846fa --- /dev/null +++ b/images/utils.go @@ -0,0 +1,22 @@ +package images + +import ( + "errors" + "strings" +) + +func contains(a []string, s string) bool { + for i := range a { + if a[i] == s { + return true + } + } + return false +} + +func validateName(imageName string) error { + if len(strings.Split(imageName, "/")) != 2 { + return errors.New("Name doesn't follow pattern /") + } + return nil +} diff --git a/images/writer.go b/images/writer.go new file mode 100644 index 0000000..b712805 --- /dev/null +++ b/images/writer.go @@ -0,0 +1,43 @@ +package images + +import ( + "github.com/mhewedy/vermin/db" + "io" + "os" + "strings" +) + +func writeNewImage(tmpFile *os.File, imageName string) error { + // copy the downloaded file to images directory + if err := os.MkdirAll(db.ImagesDir+"/"+strings.Split(imageName, "/")[0], 0755); err != nil { + return err + } + + if err := copyFile(tmpFile.Name(), db.ImagesDir+"/"+imageName+ova); err != nil { + return err + } + + return nil +} + +// 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/vms.go b/vms/vms.go index ae69a48..cac1051 100644 --- a/vms/vms.go +++ b/vms/vms.go @@ -7,6 +7,7 @@ import ( "github.com/mhewedy/vermin/command/scp" "github.com/mhewedy/vermin/command/ssh" "github.com/mhewedy/vermin/db" + "github.com/mhewedy/vermin/images" "github.com/mhewedy/vermin/ip" "github.com/mhewedy/vermin/progress" "os" @@ -173,3 +174,15 @@ func GUI(vmName string) error { return command.VBoxManage("startvm", "--type", "separate", vmName).Run() } + +func Commit(vmName, imageName string, override bool) error { + if err := checkVM(vmName); err != nil { + return err + } + + if isRunningVM(vmName) { + return fmt.Errorf(`VM is running, use "vermin stop %s" to stop the VM before commiting image from it.`, vmName) + } + + return images.Commit(vmName, imageName, override) +}