github.com/Microsoft/azure-vhd-utils@v0.0.0-20230613175315-7c30a3748a1b/vhdUploadCmdHandler.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/base64"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"runtime"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/Azure/azure-sdk-for-go/storage"
    13  	"github.com/Microsoft/azure-vhd-utils/upload"
    14  	"github.com/Microsoft/azure-vhd-utils/upload/metadata"
    15  	"github.com/Microsoft/azure-vhd-utils/vhdcore/common"
    16  	"github.com/Microsoft/azure-vhd-utils/vhdcore/diskstream"
    17  	"github.com/Microsoft/azure-vhd-utils/vhdcore/validator"
    18  	"gopkg.in/urfave/cli.v1"
    19  )
    20  
    21  func vhdUploadCmdHandler() cli.Command {
    22  	return cli.Command{
    23  		Name:  "upload",
    24  		Usage: "Upload a local VHD to Azure storage as page blob",
    25  		Flags: []cli.Flag{
    26  			cli.StringFlag{
    27  				Name:  "localvhdpath",
    28  				Usage: "Path to source VHD in the local machine.",
    29  			},
    30  			cli.StringFlag{
    31  				Name:  "stgaccountname",
    32  				Usage: "Azure storage account name.",
    33  			},
    34  			cli.StringFlag{
    35  				Name:  "stgaccountkey",
    36  				Usage: "Azure storage account key.",
    37  			},
    38  			cli.StringFlag{
    39  				Name:  "containername",
    40  				Usage: "Name of the container holding destination page blob. (Default: vhds)",
    41  			},
    42  			cli.StringFlag{
    43  				Name:  "blobname",
    44  				Usage: "Name of the destination page blob.",
    45  			},
    46  			cli.StringFlag{
    47  				Name:  "parallelism",
    48  				Usage: "Number of concurrent goroutines to be used for upload",
    49  			},
    50  			cli.BoolFlag{
    51  				Name:  "overwrite",
    52  				Usage: "Overwrite the blob if already exists.",
    53  			},
    54  		},
    55  		Action: func(c *cli.Context) error {
    56  			const PageBlobPageSize int64 = 2 * 1024 * 1024
    57  
    58  			localVHDPath := c.String("localvhdpath")
    59  			if localVHDPath == "" {
    60  				return errors.New("Missing required argument --localvhdpath")
    61  			}
    62  
    63  			stgAccountName := c.String("stgaccountname")
    64  			if stgAccountName == "" {
    65  				return errors.New("Missing required argument --stgaccountname")
    66  			}
    67  
    68  			stgAccountKey := c.String("stgaccountkey")
    69  			if stgAccountKey == "" {
    70  				return errors.New("Missing required argument --stgaccountkey")
    71  			}
    72  
    73  			containerName := c.String("containername")
    74  			if containerName == "" {
    75  				containerName = "vhds"
    76  				log.Println("Using default container 'vhds'")
    77  			}
    78  
    79  			blobName := c.String("blobname")
    80  			if blobName == "" {
    81  				return errors.New("Missing required argument --blobname")
    82  			}
    83  
    84  			if !strings.HasSuffix(strings.ToLower(blobName), ".vhd") {
    85  				blobName = blobName + ".vhd"
    86  			}
    87  
    88  			parallelism := int(0)
    89  			if c.IsSet("parallelism") {
    90  				p, err := strconv.ParseUint(c.String("parallelism"), 10, 32)
    91  				if err != nil {
    92  					return fmt.Errorf("invalid index value --parallelism: %s", err)
    93  				}
    94  				parallelism = int(p)
    95  			} else {
    96  				parallelism = 8 * runtime.NumCPU()
    97  				log.Printf("Using default parallelism [8*NumCPU] : %d\n", parallelism)
    98  			}
    99  
   100  			overwrite := c.IsSet("overwrite")
   101  
   102  			ensureVHDSanity(localVHDPath)
   103  			diskStream, err := diskstream.CreateNewDiskStream(localVHDPath)
   104  			if err != nil {
   105  				return err
   106  			}
   107  			defer diskStream.Close()
   108  
   109  			storageClient, err := storage.NewBasicClient(stgAccountName, stgAccountKey)
   110  			if err != nil {
   111  				return err
   112  			}
   113  			blobServiceClient := storageClient.GetBlobService()
   114  			if _, err = blobServiceClient.CreateContainerIfNotExists(containerName, storage.ContainerAccessTypePrivate); err != nil {
   115  				return err
   116  			}
   117  
   118  			blobExists, err := blobServiceClient.BlobExists(containerName, blobName)
   119  			if err != nil {
   120  				return err
   121  			}
   122  
   123  			resume := false
   124  			var blobMetaData *metadata.MetaData
   125  			if blobExists {
   126  				if !overwrite {
   127  					blobMetaData = getBlobMetaData(blobServiceClient, containerName, blobName)
   128  					resume = true
   129  					log.Printf("Blob with name '%s' already exists, checking upload can be resumed\n", blobName)
   130  				}
   131  			}
   132  
   133  			localMetaData := getLocalVHDMetaData(localVHDPath)
   134  			var rangesToSkip []*common.IndexRange
   135  			if resume {
   136  				if errs := metadata.CompareMetaData(blobMetaData, localMetaData); len(errs) != 0 {
   137  					printErrorsAndFatal(errs)
   138  				}
   139  				rangesToSkip = getAlreadyUploadedBlobRanges(blobServiceClient, containerName, blobName)
   140  			} else {
   141  				createBlob(blobServiceClient, containerName, blobName, diskStream.GetSize(), localMetaData)
   142  			}
   143  
   144  			uploadableRanges, err := upload.LocateUploadableRanges(diskStream, rangesToSkip, PageBlobPageSize)
   145  			if err != nil {
   146  				return err
   147  			}
   148  
   149  			uploadableRanges, err = upload.DetectEmptyRanges(diskStream, uploadableRanges)
   150  			if err != nil {
   151  				return err
   152  			}
   153  
   154  			cxt := &upload.DiskUploadContext{
   155  				VhdStream:             diskStream,
   156  				UploadableRanges:      uploadableRanges,
   157  				AlreadyProcessedBytes: common.TotalRangeLength(rangesToSkip),
   158  				BlobServiceClient:     blobServiceClient,
   159  				ContainerName:         containerName,
   160  				BlobName:              blobName,
   161  				Parallelism:           parallelism,
   162  				Resume:                resume,
   163  				MD5Hash:               localMetaData.FileMetaData.MD5Hash,
   164  			}
   165  
   166  			err = upload.Upload(cxt)
   167  			if err != nil {
   168  				return err
   169  			}
   170  
   171  			setBlobMD5Hash(blobServiceClient, containerName, blobName, localMetaData)
   172  			fmt.Println("\nUpload completed")
   173  			return nil
   174  		},
   175  	}
   176  }
   177  
   178  // printErrorsAndFatal prints the errors in a slice one by one and then exit
   179  //
   180  func printErrorsAndFatal(errs []error) {
   181  	fmt.Println()
   182  	for _, e := range errs {
   183  		fmt.Println(e)
   184  	}
   185  	log.Fatal("Cannot continue due to above errors.")
   186  }
   187  
   188  // ensureVHDSanity ensure is VHD is valid for Azure.
   189  //
   190  func ensureVHDSanity(localVHDPath string) {
   191  	if err := validator.ValidateVhd(localVHDPath); err != nil {
   192  		log.Fatal(err)
   193  	}
   194  
   195  	if err := validator.ValidateVhdSize(localVHDPath); err != nil {
   196  		log.Fatal(err)
   197  	}
   198  }
   199  
   200  // getBlobMetaData returns the custom metadata associated with a page blob which is set by createBlob method.
   201  // The parameter client is the Azure blob service client, parameter containerName is the name of an existing container
   202  // in which the page blob resides, parameter blobName is name for the page blob
   203  // This method attempt to fetch the metadata only if MD5Hash is not set for the page blob, this method panic if the
   204  // MD5Hash is already set or if the custom metadata is absent.
   205  //
   206  func getBlobMetaData(client storage.BlobStorageClient, containerName, blobName string) *metadata.MetaData {
   207  	md5Hash := getBlobMD5Hash(client, containerName, blobName)
   208  	if md5Hash != "" {
   209  		log.Fatalf("VHD exists in blob storage with name '%s'. If you want to upload again, use the --overwrite option.", blobName)
   210  	}
   211  
   212  	blobMetaData, err := metadata.NewMetadataFromBlob(client, containerName, blobName)
   213  	if err != nil {
   214  		log.Fatal(err)
   215  	}
   216  
   217  	if blobMetaData == nil {
   218  		log.Fatalf("There is no upload metadata associated with the existing blob '%s', so upload operation cannot be resumed, use --overwrite option.", blobName)
   219  	}
   220  	return blobMetaData
   221  }
   222  
   223  // getLocalVHDMetaData returns the metadata of a local VHD
   224  //
   225  func getLocalVHDMetaData(localVHDPath string) *metadata.MetaData {
   226  	localMetaData, err := metadata.NewMetaDataFromLocalVHD(localVHDPath)
   227  	if err != nil {
   228  		log.Fatal(err)
   229  	}
   230  	return localMetaData
   231  }
   232  
   233  // createBlob creates a page blob of specific size and sets custom metadata
   234  // The parameter client is the Azure blob service client, parameter containerName is the name of an existing container
   235  // in which the page blob needs to be created, parameter blobName is name for the new page blob, size is the size of
   236  // the new page blob in bytes and parameter vhdMetaData is the custom metadata to be associacted with the page blob
   237  //
   238  func createBlob(client storage.BlobStorageClient, containerName, blobName string, size int64, vhdMetaData *metadata.MetaData) {
   239  	if err := client.PutPageBlob(containerName, blobName, size, nil); err != nil {
   240  		log.Fatal(err)
   241  	}
   242  	m, _ := vhdMetaData.ToMap()
   243  	if err := client.SetBlobMetadata(containerName, blobName, m, make(map[string]string)); err != nil {
   244  		log.Fatal(err)
   245  	}
   246  }
   247  
   248  // setBlobMD5Hash sets MD5 hash of the blob in it's properties
   249  //
   250  func setBlobMD5Hash(client storage.BlobStorageClient, containerName, blobName string, vhdMetaData *metadata.MetaData) {
   251  	if vhdMetaData.FileMetaData.MD5Hash != nil {
   252  		blobHeaders := storage.BlobHeaders{
   253  			ContentMD5: base64.StdEncoding.EncodeToString(vhdMetaData.FileMetaData.MD5Hash),
   254  		}
   255  		if err := client.SetBlobProperties(containerName, blobName, blobHeaders); err != nil {
   256  			log.Fatal(err)
   257  		}
   258  	}
   259  }
   260  
   261  // getAlreadyUploadedBlobRanges returns the range slice containing ranges of a page blob those are already uploaded.
   262  // The parameter client is the Azure blob service client, parameter containerName is the name of an existing container
   263  // in which the page blob resides, parameter blobName is name for the page blob
   264  //
   265  func getAlreadyUploadedBlobRanges(client storage.BlobStorageClient, containerName, blobName string) []*common.IndexRange {
   266  	existingRanges, err := client.GetPageRanges(containerName, blobName)
   267  	if err != nil {
   268  		log.Fatal(err)
   269  	}
   270  	var rangesToSkip = make([]*common.IndexRange, len(existingRanges.PageList))
   271  	for i, r := range existingRanges.PageList {
   272  		rangesToSkip[i] = common.NewIndexRange(r.Start, r.End)
   273  	}
   274  	return rangesToSkip
   275  }
   276  
   277  // getBlobMD5Hash returns the MD5Hash associated with a blob
   278  // The parameter client is the Azure blob service client, parameter containerName is the name of an existing container
   279  // in which the page blob resides, parameter blobName is name for the page blob
   280  //
   281  func getBlobMD5Hash(client storage.BlobStorageClient, containerName, blobName string) string {
   282  	properties, err := client.GetBlobProperties(containerName, blobName)
   283  	if err != nil {
   284  		log.Fatal(err)
   285  	}
   286  	return properties.ContentMD5
   287  }