github.com/coreos/mantle@v0.13.0/cmd/ore/aws/upload.go (about) 1 // Copyright 2017 CoreOS, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package aws 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "net/url" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/coreos/mantle/platform/api/aws" 26 "github.com/coreos/mantle/sdk" 27 "github.com/coreos/mantle/util" 28 "github.com/spf13/cobra" 29 ) 30 31 var ( 32 cmdUpload = &cobra.Command{ 33 Use: "upload", 34 Short: "Create AWS images", 35 Long: `Upload CoreOS image to S3 and create relevant AMIs (hvm and pv). 36 37 Supported source formats are VMDK (as created with ./image_to_vm --format=ami_vmdk) and RAW. 38 39 After a successful run, the final line of output will be a line of JSON describing the relevant resources. 40 `, 41 Example: ` ore aws upload --region=us-east-1 \ 42 --ami-name="CoreOS-stable-1234.5.6" \ 43 --ami-description="CoreOS stable 1234.5.6" \ 44 --file="/home/.../coreos_production_ami_vmdk_image.vmdk" \ 45 --tags="machine=production"`, 46 RunE: runUpload, 47 } 48 49 uploadSourceObject string 50 uploadBucket string 51 uploadImageName string 52 uploadBoard string 53 uploadFile string 54 uploadDiskSizeGiB uint 55 uploadDiskSizeInspect bool 56 uploadDeleteObject bool 57 uploadForce bool 58 uploadSourceSnapshot string 59 uploadObjectFormat aws.EC2ImageFormat 60 uploadAMIName string 61 uploadAMIDescription string 62 uploadGrantUsers []string 63 uploadCreatePV bool 64 uploadTags []string 65 ) 66 67 func init() { 68 AWS.AddCommand(cmdUpload) 69 cmdUpload.Flags().StringVar(&uploadSourceObject, "source-object", "", "'s3://' URI pointing to image data (default: same as upload)") 70 cmdUpload.Flags().StringVar(&uploadBucket, "bucket", "", "s3://bucket/prefix/ (defaults to a regional bucket and prefix defaults to $USER/board/name)") 71 cmdUpload.Flags().StringVar(&uploadImageName, "name", "", "name of uploaded image (default COREOS_VERSION)") 72 cmdUpload.Flags().StringVar(&uploadBoard, "board", "amd64-usr", "board used for naming with default prefix only") 73 cmdUpload.Flags().StringVar(&uploadFile, "file", 74 defaultUploadFile(), 75 "path to CoreOS image (build with: ./image_to_vm.sh --format=ami_vmdk ...)") 76 cmdUpload.Flags().UintVarP(&uploadDiskSizeGiB, "disk-size-gib", "", aws.ContainerLinuxDiskSizeGiB, "AMI disk size in GiB") 77 cmdUpload.Flags().BoolVar(&uploadDiskSizeInspect, "disk-size-inspect", false, "set AMI disk size to size of local file") 78 cmdUpload.Flags().BoolVar(&uploadDeleteObject, "delete-object", true, "delete uploaded S3 object after snapshot is created") 79 cmdUpload.Flags().BoolVar(&uploadForce, "force", false, "overwrite existing S3 object rather than reusing") 80 cmdUpload.Flags().StringVar(&uploadSourceSnapshot, "source-snapshot", "", "the snapshot ID to base this AMI on (default: create new snapshot)") 81 cmdUpload.Flags().Var(&uploadObjectFormat, "object-format", fmt.Sprintf("object format: %s or %s (default: %s)", aws.EC2ImageFormatVmdk, aws.EC2ImageFormatRaw, aws.EC2ImageFormatVmdk)) 82 cmdUpload.Flags().StringVar(&uploadAMIName, "ami-name", "", "name of the AMI to create (default: Container-Linux-$USER-$VERSION)") 83 cmdUpload.Flags().StringVar(&uploadAMIDescription, "ami-description", "", "description of the AMI to create (default: empty)") 84 cmdUpload.Flags().StringSliceVar(&uploadGrantUsers, "grant-user", []string{}, "grant launch permission to this AWS user ID") 85 cmdUpload.Flags().BoolVar(&uploadCreatePV, "create-pv", false, "create a PV AMI in addition to the HVM AMI") 86 cmdUpload.Flags().StringSliceVar(&uploadTags, "tags", []string{}, "list of key=value tags to attach to the AMI") 87 } 88 89 func defaultBucketNameForRegion(region string) string { 90 return fmt.Sprintf("coreos-dev-ami-import-%s", region) 91 } 92 93 func defaultUploadFile() string { 94 build := sdk.BuildRoot() 95 return build + "/images/amd64-usr/latest/coreos_production_ami_vmdk_image.vmdk" 96 } 97 98 // defaultBucketURL determines the location the tool should upload to. 99 // The 'urlPrefix' parameter, if it contains a path, will override all other 100 // arguments 101 func defaultBucketURL(urlPrefix, imageName, board, file, region string) (*url.URL, error) { 102 if urlPrefix == "" { 103 urlPrefix = fmt.Sprintf("s3://%s", defaultBucketNameForRegion(region)) 104 } 105 106 s3URL, err := url.Parse(urlPrefix) 107 if err != nil { 108 return nil, err 109 } 110 if s3URL.Scheme != "s3" { 111 return nil, fmt.Errorf("invalid s3 scheme; must be 's3://', not '%s://'", s3URL.Scheme) 112 } 113 if s3URL.Host == "" { 114 return nil, fmt.Errorf("URL missing bucket name %v\n", urlPrefix) 115 } 116 117 // if prefix not specified, default to /$USER/$BOARD/$VERSION 118 if s3URL.Path == "" { 119 s3URL.Path = fmt.Sprintf("/%s/%s/%s", os.Getenv("USER"), board, imageName) 120 } 121 122 if s3URL.Path[len(s3URL.Path)-1] != '/' { 123 s3URL.Path += "/" 124 } 125 s3URL.Path += filepath.Base(file) 126 127 return s3URL, nil 128 } 129 130 func runUpload(cmd *cobra.Command, args []string) error { 131 if len(args) != 0 { 132 fmt.Fprintf(os.Stderr, "Unrecognized args in aws upload cmd: %v\n", args) 133 os.Exit(2) 134 } 135 if uploadSourceObject != "" && uploadSourceSnapshot != "" { 136 fmt.Fprintf(os.Stderr, "At most one of --source-object and --source-snapshot may be specified.\n") 137 os.Exit(2) 138 } 139 if uploadDiskSizeInspect && (uploadSourceObject != "" || uploadSourceSnapshot != "") { 140 fmt.Fprintf(os.Stderr, "--disk-size-inspect cannot be used with --source-object or --source-snapshot.\n") 141 os.Exit(2) 142 } 143 144 // if an image name is unspecified try to use version.txt 145 imageName := uploadImageName 146 if imageName == "" { 147 ver, err := sdk.VersionsFromDir(filepath.Dir(uploadFile)) 148 if err != nil { 149 fmt.Fprintf(os.Stderr, "Unable to get version from image directory, provide a -name flag or include a version.txt in the image directory: %v\n", err) 150 os.Exit(1) 151 } 152 imageName = ver.Version 153 } 154 155 if uploadDiskSizeInspect { 156 imageInfo, err := util.GetImageInfo(uploadFile) 157 if err != nil { 158 fmt.Fprintf(os.Stderr, "Unable to query size of disk: %v\n", err) 159 os.Exit(1) 160 } 161 plog.Debugf("Image size: %v\n", imageInfo.VirtualSize) 162 const GiB = 1024 * 1024 * 1024 163 uploadDiskSizeGiB = uint(imageInfo.VirtualSize / GiB) 164 // Round up if there's leftover 165 if imageInfo.VirtualSize%GiB > 0 { 166 uploadDiskSizeGiB += 1 167 } 168 } 169 170 amiName := uploadAMIName 171 if amiName == "" { 172 ver, err := sdk.VersionsFromDir(filepath.Dir(uploadFile)) 173 if err != nil { 174 fmt.Fprintf(os.Stderr, "could not guess image name: %v\n", err) 175 os.Exit(1) 176 } 177 awsVersion := strings.Replace(ver.Version, "+", "-", -1) // '+' is invalid in an AMI name 178 amiName = fmt.Sprintf("Container-Linux-dev-%s-%s", os.Getenv("USER"), awsVersion) 179 } 180 181 var s3URL *url.URL 182 var err error 183 if uploadSourceObject != "" { 184 s3URL, err = url.Parse(uploadSourceObject) 185 if err != nil { 186 fmt.Fprintf(os.Stderr, "%v\n", err) 187 os.Exit(1) 188 } 189 } else { 190 s3URL, err = defaultBucketURL(uploadBucket, imageName, uploadBoard, uploadFile, region) 191 if err != nil { 192 fmt.Fprintf(os.Stderr, "%v\n", err) 193 os.Exit(1) 194 } 195 } 196 plog.Debugf("S3 object: %v\n", s3URL) 197 s3BucketName := s3URL.Host 198 s3ObjectPath := strings.TrimPrefix(s3URL.Path, "/") 199 200 // if no snapshot was specified, check for an existing one or a 201 // snapshot task in progress 202 sourceSnapshot := uploadSourceSnapshot 203 if sourceSnapshot == "" { 204 snapshot, err := API.FindSnapshot(imageName) 205 if err != nil { 206 fmt.Fprintf(os.Stderr, "failed finding snapshot: %v\n", err) 207 os.Exit(1) 208 } 209 if snapshot != nil { 210 sourceSnapshot = snapshot.SnapshotID 211 } 212 } 213 214 // if there's no existing snapshot and no provided S3 object to 215 // make one from, upload to S3 216 if uploadSourceObject == "" && sourceSnapshot == "" { 217 f, err := os.Open(uploadFile) 218 if err != nil { 219 fmt.Fprintf(os.Stderr, "Could not open image file %v: %v\n", uploadFile, err) 220 os.Exit(1) 221 } 222 defer f.Close() 223 224 err = API.UploadObject(f, s3BucketName, s3ObjectPath, uploadForce, "", "") 225 if err != nil { 226 fmt.Fprintf(os.Stderr, "Error uploading: %v\n", err) 227 os.Exit(1) 228 } 229 } 230 231 // if we don't already have a snapshot, make one 232 if sourceSnapshot == "" { 233 snapshot, err := API.CreateSnapshot(imageName, s3URL.String(), uploadObjectFormat) 234 if err != nil { 235 fmt.Fprintf(os.Stderr, "unable to create snapshot: %v\n", err) 236 os.Exit(1) 237 } 238 sourceSnapshot = snapshot.SnapshotID 239 } 240 241 // if delete is enabled and we created the snapshot from an S3 242 // object that we also created (perhaps in a previous run), delete 243 // the S3 object 244 if uploadSourceObject == "" && uploadSourceSnapshot == "" && uploadDeleteObject { 245 if err := API.DeleteObject(s3BucketName, s3ObjectPath); err != nil { 246 fmt.Fprintf(os.Stderr, "unable to delete object: %v\n", err) 247 os.Exit(1) 248 } 249 } 250 251 // create AMIs and grant permissions 252 hvmID, err := API.CreateHVMImage(sourceSnapshot, uploadDiskSizeGiB, amiName+"-hvm", uploadAMIDescription) 253 if err != nil { 254 fmt.Fprintf(os.Stderr, "unable to create HVM image: %v\n", err) 255 os.Exit(1) 256 } 257 258 if len(uploadGrantUsers) > 0 { 259 err = API.GrantLaunchPermission(hvmID, uploadGrantUsers) 260 if err != nil { 261 fmt.Fprintf(os.Stderr, "unable to grant launch permission: %v\n", err) 262 os.Exit(1) 263 } 264 } 265 266 tagMap := make(map[string]string) 267 for _, tag := range uploadTags { 268 splitTag := strings.SplitN(tag, "=", 2) 269 if len(splitTag) != 2 { 270 fmt.Fprintf(os.Stderr, "invalid tag format; should be key=value, not %v\n", tag) 271 os.Exit(1) 272 } 273 key, value := splitTag[0], splitTag[1] 274 tagMap[key] = value 275 } 276 277 if err := API.CreateTags([]string{hvmID, sourceSnapshot}, tagMap); err != nil { 278 fmt.Fprintf(os.Stderr, "unable to add tags: %v\n", err) 279 os.Exit(1) 280 } 281 282 var pvID string 283 if uploadCreatePV { 284 pvImageID, err := API.CreatePVImage(sourceSnapshot, uploadDiskSizeGiB, amiName, uploadAMIDescription) 285 if err != nil { 286 fmt.Fprintf(os.Stderr, "unable to create PV image: %v\n", err) 287 os.Exit(1) 288 } 289 pvID = pvImageID 290 291 if len(uploadGrantUsers) > 0 { 292 err = API.GrantLaunchPermission(pvID, uploadGrantUsers) 293 if err != nil { 294 fmt.Fprintf(os.Stderr, "unable to grant launch permission: %v\n", err) 295 os.Exit(1) 296 } 297 } 298 299 if err := API.CreateTags([]string{pvID}, tagMap); err != nil { 300 fmt.Fprintf(os.Stderr, "unable to add tags: %v\n", err) 301 os.Exit(1) 302 } 303 } 304 305 err = json.NewEncoder(os.Stdout).Encode(&struct { 306 HVM string 307 PV string `json:",omitempty"` 308 SnapshotID string 309 S3Object string 310 }{ 311 HVM: hvmID, 312 PV: pvID, 313 SnapshotID: sourceSnapshot, 314 S3Object: s3URL.String(), 315 }) 316 if err != nil { 317 fmt.Fprintf(os.Stderr, "Couldn't encode result: %v\n", err) 318 os.Exit(1) 319 } 320 return nil 321 }