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 }