Skip to content

Commit

Permalink
Add an option to use download/upload image clone method
Browse files Browse the repository at this point in the history
  • Loading branch information
kayrus committed May 6, 2020
1 parent 3fedb40 commit 1eb97c6
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 46 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Here comes cyclone (**C**loud **Clone** or cclone) to help you with this task. I

## Help

The tool uses a Glance V2 [web-download](https://docs.openstack.org/glance/latest/admin/interoperable-image-import.html#image-import-methods) method to clone images. This method allows Glance to download an image using a remote URL.
By default the tool uses a Glance V2 [web-download](https://docs.openstack.org/glance/latest/admin/interoperable-image-import.html#image-import-methods) method to clone images. This method allows Glance to download an image using a remote URL. Set the `--image-web-download` option to **false** to use the default download/upload method. In this case the whole image data will be streamed through cyclone.

A remote URL can be generated using a Swift [Temporary URL](https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html).

Expand Down Expand Up @@ -44,6 +44,7 @@ Available Commands:
Flags:
-d, --debug print out request and response objects
-h, --help help for cyclone
--image-web-download use Glance web-download image import method (default true)
--timeout-backup string timeout to wait for a backup status (default "1h")
--timeout-image string timeout to wait for an image status (default "1h")
--timeout-server string timeout to wait for a server status (default "1h")
Expand Down Expand Up @@ -71,6 +72,15 @@ $ source openrc-of-the-source-project
$ cyclone image 77c125f1-2c7b-473e-a56b-28a9a0bc4787 --to-region eu-de-2 --to-project destination-project-name --to-image-name image-from-source-project-name
```

### Clone an image between regions using download/upload method

Pay attention that the image data will be streamed through cyclone. It is recommended to use this method, when cyclone is executed directly on the VM, located in the source or destination region.

```sh
$ source openrc-of-the-source-project
$ cyclone image 77c125f1-2c7b-473e-a56b-28a9a0bc4787 --to-region eu-de-2 --to-project destination-project-name --to-image-name image-from-source-project-name --image-web-download=false
```

### Clone a bootable volume between regions

```sh
Expand Down
126 changes: 81 additions & 45 deletions pkg/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package pkg

import (
"fmt"
"io"
"math/rand"
"time"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imageimport"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/tasks"
Expand All @@ -18,8 +20,9 @@ import (
)

var (
waitForImageSec float64
swiftTempURLTTL int = 10 // 10 seconds is enough
waitForImageSec float64
swiftTempURLTTL int = 10 // 10 seconds is enough
imageWebDownload bool
)

var imageWaitStatuses = []string{
Expand Down Expand Up @@ -178,28 +181,31 @@ func generateTmpUrlKey(n int) string {
}

func migrateImage(srcImageClient, dstImageClient, srcObjectClient *gophercloud.ServiceClient, srcImg *images.Image, toImageName string) (*images.Image, error) {
var url string
containerName := "glance_" + srcImg.ID
objectName := srcImg.ID

tempUrlKey := containers.UpdateOpts{
TempURLKey: generateTmpUrlKey(20),
}
_, err := containers.Update(srcObjectClient, containerName, tempUrlKey).Extract()
if err != nil {
return nil, fmt.Errorf("unable to set container temporary url key: %s", err)
}
if imageWebDownload {
tempUrlKey := containers.UpdateOpts{
TempURLKey: generateTmpUrlKey(20),
}
_, err := containers.Update(srcObjectClient, containerName, tempUrlKey).Extract()
if err != nil {
return nil, fmt.Errorf("unable to set container temporary url key: %s", err)
}

tmpUrlOptions := objects.CreateTempURLOpts{
Method: "GET",
TTL: swiftTempURLTTL,
}
tmpUrlOptions := objects.CreateTempURLOpts{
Method: "GET",
TTL: swiftTempURLTTL,
}

url, err := objects.CreateTempURL(srcObjectClient, containerName, objectName, tmpUrlOptions)
if err != nil {
return nil, fmt.Errorf("unable to generate a temporary url for the %q container: %s", containerName, err)
}
url, err = objects.CreateTempURL(srcObjectClient, containerName, objectName, tmpUrlOptions)
if err != nil {
return nil, fmt.Errorf("unable to generate a temporary url for the %q container: %s", containerName, err)
}

log.Printf("Generated Swift Temp URL: %s", url)
log.Printf("Generated Swift Temp URL: %s", url)
}

imageName := srcImg.Name
if toImageName != "" {
Expand Down Expand Up @@ -232,31 +238,58 @@ func migrateImage(srcImageClient, dstImageClient, srcObjectClient *gophercloud.S
}
}()

var importInfo *imageimport.ImportInfo
importInfo, err = imageimport.Get(dstImageClient).Extract()
if err != nil {
return nil, fmt.Errorf("error while getting the supported import methods: %s", err)
}
if imageWebDownload {
var importInfo *imageimport.ImportInfo
importInfo, err = imageimport.Get(dstImageClient).Extract()
if err != nil {
return nil, fmt.Errorf("error while getting the supported import methods: %s", err)
}

if !isSliceContainsStr(importInfo.ImportMethods.Value, string(imageimport.WebDownloadMethod)) {
return nil, fmt.Errorf("the %q import method is not supported, supported import methods: %q", imageimport.WebDownloadMethod, importInfo.ImportMethods.Value)
}
if !isSliceContainsStr(importInfo.ImportMethods.Value, string(imageimport.WebDownloadMethod)) {
return nil, fmt.Errorf("the %q import method is not supported, supported import methods: %q", imageimport.WebDownloadMethod, importInfo.ImportMethods.Value)
}

// import
importOpts := &imageimport.CreateOpts{
Name: imageimport.WebDownloadMethod,
URI: url,
}
// import
importOpts := &imageimport.CreateOpts{
Name: imageimport.WebDownloadMethod,
URI: url,
}

err = imageimport.Create(dstImageClient, dstImg.ID, importOpts).ExtractErr()
if err != nil {
return nil, fmt.Errorf("error while importing url %q: %s", url, err)
err = imageimport.Create(dstImageClient, dstImg.ID, importOpts).ExtractErr()
if err != nil {
return nil, fmt.Errorf("error while importing url %q: %s", url, err)

}
}

dstImg, err = waitForImageTask(dstImageClient, dstImg.ID, waitForImageSec)
if err != nil {
return nil, fmt.Errorf("error while importing url %q: %s", url, err)
dstImg, err = waitForImageTask(dstImageClient, dstImg.ID, waitForImageSec)
if err != nil {
return nil, fmt.Errorf("error while importing url %q: %s", url, err)
}
} else {
// get the source reader
var imageReader io.Reader
imageReader, err = imagedata.Download(srcImageClient, srcImg.ID).Extract()
if err != nil {
return nil, fmt.Errorf("error getting the source image reader: %s", err)
}

dstImgID := dstImg.ID
errChan := make(chan error)
go func() {
dstImg, err = waitForImage(dstImageClient, dstImg.ID, waitForImageSec)
errChan <- err
}()

// write the source to the destination
err = imagedata.Upload(dstImageClient, dstImgID, imageReader).ExtractErr()
if err != nil {
return nil, fmt.Errorf("failed to upload an image: %s", err)
}

err = <-errChan
if err != nil {
return nil, fmt.Errorf("error while waiting for an image to be uploaded: %s", err)
}
}

createImageSpeed(dstImg)
Expand Down Expand Up @@ -292,6 +325,7 @@ var ImageCmd = &cobra.Command{
if err := parseTimeoutArgs(); err != nil {
return err
}
imageWebDownload = viper.GetBool("image-web-download")
return viper.BindPFlags(cmd.Flags())
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -340,13 +374,15 @@ var ImageCmd = &cobra.Command{
return fmt.Errorf("failed to wait for %q source image: %s", image, err)
}

// check whether current user scope belongs to the image owner
userProjectID, err := getAuthProjectID(srcImageClient.ProviderClient)
if err != nil {
return fmt.Errorf("failed to extract user project ID scope: %s", err)
}
if userProjectID != srcImg.Owner {
return fmt.Errorf("cannot clone an image, which belongs to another project: %s", srcImg.Owner)
if imageWebDownload {
// check whether current user scope belongs to the image owner
userProjectID, err := getAuthProjectID(srcImageClient.ProviderClient)
if err != nil {
return fmt.Errorf("failed to extract user project ID scope: %s", err)
}
if userProjectID != srcImg.Owner {
return fmt.Errorf("cannot clone an image using web download import method, when an image belongs to another project (%s), try to set --image-web-download=false", srcImg.Owner)
}
}

defer measureTime()
Expand Down
2 changes: 2 additions & 0 deletions pkg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func initRootCmdFlags() {
RootCmd.PersistentFlags().StringP("timeout-server", "", "1h", "timeout to wait for a server status")
RootCmd.PersistentFlags().StringP("timeout-snapshot", "", "1h", "timeout to wait for a snapshot status")
RootCmd.PersistentFlags().StringP("timeout-backup", "", "1h", "timeout to wait for a backup status")
RootCmd.PersistentFlags().BoolP("image-web-download", "", true, "use Glance web-download image import method")
viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug"))
viper.BindPFlag("to-auth-url", RootCmd.PersistentFlags().Lookup("to-auth-url"))
viper.BindPFlag("to-region", RootCmd.PersistentFlags().Lookup("to-region"))
Expand All @@ -128,6 +129,7 @@ func initRootCmdFlags() {
viper.BindPFlag("timeout-server", RootCmd.PersistentFlags().Lookup("timeout-server"))
viper.BindPFlag("timeout-snapshot", RootCmd.PersistentFlags().Lookup("timeout-snapshot"))
viper.BindPFlag("timeout-backup", RootCmd.PersistentFlags().Lookup("timeout-backup"))
viper.BindPFlag("image-web-download", RootCmd.PersistentFlags().Lookup("image-web-download"))
}

type Locations struct {
Expand Down
1 change: 1 addition & 0 deletions pkg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ var ServerCmd = &cobra.Command{
if err := parseTimeoutArgs(); err != nil {
return err
}
imageWebDownload = viper.GetBool("image-web-download")
return viper.BindPFlags(cmd.Flags())
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
1 change: 1 addition & 0 deletions pkg/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ var VolumeCmd = &cobra.Command{
if err := parseTimeoutArgs(); err != nil {
return err
}
imageWebDownload = viper.GetBool("image-web-download")
return viper.BindPFlags(cmd.Flags())
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1eb97c6

Please sign in to comment.