github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/builder/azure/arm/config.go (about) 1 // Copyright (c) Microsoft Corporation. All rights reserved. 2 // Licensed under the MIT License. See the LICENSE file in builder/azure for license information. 3 4 package arm 5 6 import ( 7 "crypto/rand" 8 "crypto/rsa" 9 "crypto/x509" 10 "crypto/x509/pkix" 11 "encoding/base64" 12 "encoding/json" 13 "fmt" 14 "io/ioutil" 15 "math/big" 16 "net/http" 17 "regexp" 18 "strings" 19 "time" 20 21 "github.com/Azure/azure-sdk-for-go/arm/compute" 22 "github.com/Azure/go-autorest/autorest/azure" 23 "github.com/Azure/go-autorest/autorest/to" 24 "github.com/Azure/go-ntlmssp" 25 26 "github.com/mitchellh/packer/builder/azure/common/constants" 27 "github.com/mitchellh/packer/builder/azure/pkcs12" 28 "github.com/mitchellh/packer/common" 29 "github.com/mitchellh/packer/helper/communicator" 30 "github.com/mitchellh/packer/helper/config" 31 "github.com/mitchellh/packer/packer" 32 "github.com/mitchellh/packer/template/interpolate" 33 34 "golang.org/x/crypto/ssh" 35 ) 36 37 const ( 38 DefaultCloudEnvironmentName = "Public" 39 DefaultImageVersion = "latest" 40 DefaultUserName = "packer" 41 DefaultVMSize = "Standard_A1" 42 ) 43 44 var ( 45 reCaptureContainerName = regexp.MustCompile("^[a-z0-9][a-z0-9\\-]{2,62}$") 46 reCaptureNamePrefix = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9_\\-\\.]{0,23}$") 47 ) 48 49 type Config struct { 50 common.PackerConfig `mapstructure:",squash"` 51 52 // Authentication via OAUTH 53 ClientID string `mapstructure:"client_id"` 54 ClientSecret string `mapstructure:"client_secret"` 55 ObjectID string `mapstructure:"object_id"` 56 TenantID string `mapstructure:"tenant_id"` 57 SubscriptionID string `mapstructure:"subscription_id"` 58 59 // Capture 60 CaptureNamePrefix string `mapstructure:"capture_name_prefix"` 61 CaptureContainerName string `mapstructure:"capture_container_name"` 62 63 // Compute 64 ImagePublisher string `mapstructure:"image_publisher"` 65 ImageOffer string `mapstructure:"image_offer"` 66 ImageSku string `mapstructure:"image_sku"` 67 ImageVersion string `mapstructure:"image_version"` 68 ImageUrl string `mapstructure:"image_url"` 69 Location string `mapstructure:"location"` 70 VMSize string `mapstructure:"vm_size"` 71 72 // Deployment 73 AzureTags map[string]*string `mapstructure:"azure_tags"` 74 ResourceGroupName string `mapstructure:"resource_group_name"` 75 StorageAccount string `mapstructure:"storage_account"` 76 storageAccountBlobEndpoint string 77 CloudEnvironmentName string `mapstructure:"cloud_environment_name"` 78 cloudEnvironment *azure.Environment 79 VirtualNetworkName string `mapstructure:"virtual_network_name"` 80 VirtualNetworkSubnetName string `mapstructure:"virtual_network_subnet_name"` 81 VirtualNetworkResourceGroupName string `mapstructure:"virtual_network_resource_group_name"` 82 CustomDataFile string `mapstructure:"custom_data_file"` 83 customData string 84 85 // OS 86 OSType string `mapstructure:"os_type"` 87 OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb"` 88 89 // Runtime Values 90 UserName string 91 Password string 92 tmpAdminPassword string 93 tmpCertificatePassword string 94 tmpResourceGroupName string 95 tmpComputeName string 96 tmpDeploymentName string 97 tmpKeyVaultName string 98 tmpOSDiskName string 99 tmpWinRMCertificateUrl string 100 101 useDeviceLogin bool 102 103 // Authentication with the VM via SSH 104 sshAuthorizedKey string 105 sshPrivateKey string 106 107 // Authentication with the VM via WinRM 108 winrmCertificate string 109 110 Comm communicator.Config `mapstructure:",squash"` 111 ctx *interpolate.Context 112 } 113 114 type keyVaultCertificate struct { 115 Data string `json:"data"` 116 DataType string `json:"dataType"` 117 Password string `json:"password,omitempty"` 118 } 119 120 func (c *Config) toVirtualMachineCaptureParameters() *compute.VirtualMachineCaptureParameters { 121 return &compute.VirtualMachineCaptureParameters{ 122 DestinationContainerName: &c.CaptureContainerName, 123 VhdPrefix: &c.CaptureNamePrefix, 124 OverwriteVhds: to.BoolPtr(false), 125 } 126 } 127 128 func (c *Config) createCertificate() (string, error) { 129 privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 130 if err != nil { 131 err = fmt.Errorf("Failed to Generate Private Key: %s", err) 132 return "", err 133 } 134 135 host := fmt.Sprintf("%s.cloudapp.net", c.tmpComputeName) 136 notBefore := time.Now() 137 notAfter := notBefore.Add(24 * time.Hour) 138 139 serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) 140 if err != nil { 141 err = fmt.Errorf("Failed to Generate Serial Number: %v", err) 142 return "", err 143 } 144 145 template := x509.Certificate{ 146 SerialNumber: serialNumber, 147 Issuer: pkix.Name{ 148 CommonName: host, 149 }, 150 Subject: pkix.Name{ 151 CommonName: host, 152 }, 153 NotBefore: notBefore, 154 NotAfter: notAfter, 155 156 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 157 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 158 BasicConstraintsValid: true, 159 } 160 161 derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 162 if err != nil { 163 err = fmt.Errorf("Failed to Create Certificate: %s", err) 164 return "", err 165 } 166 167 pfxBytes, err := pkcs12.Encode(derBytes, privateKey, c.tmpCertificatePassword) 168 if err != nil { 169 err = fmt.Errorf("Failed to encode certificate as PFX: %s", err) 170 return "", err 171 } 172 173 keyVaultDescription := keyVaultCertificate{ 174 Data: base64.StdEncoding.EncodeToString(pfxBytes), 175 DataType: "pfx", 176 Password: c.tmpCertificatePassword, 177 } 178 179 bytes, err := json.Marshal(keyVaultDescription) 180 if err != nil { 181 err = fmt.Errorf("Failed to marshal key vault description: %s", err) 182 return "", err 183 } 184 185 return base64.StdEncoding.EncodeToString(bytes), nil 186 } 187 188 func newConfig(raws ...interface{}) (*Config, []string, error) { 189 var c Config 190 191 err := config.Decode(&c, &config.DecodeOpts{ 192 Interpolate: true, 193 InterpolateContext: c.ctx, 194 }, raws...) 195 196 if err != nil { 197 return nil, nil, err 198 } 199 200 provideDefaultValues(&c) 201 setRuntimeValues(&c) 202 setUserNamePassword(&c) 203 err = setCloudEnvironment(&c) 204 if err != nil { 205 return nil, nil, err 206 } 207 208 err = setCustomData(&c) 209 if err != nil { 210 return nil, nil, err 211 } 212 213 // NOTE: if the user did not specify a communicator, then default to both 214 // SSH and WinRM. This is for backwards compatibility because the code did 215 // not specifically force the user to set a communicator. 216 if c.Comm.Type == "" || strings.EqualFold(c.Comm.Type, "ssh") { 217 err = setSshValues(&c) 218 if err != nil { 219 return nil, nil, err 220 } 221 } 222 223 if c.Comm.Type == "" || strings.EqualFold(c.Comm.Type, "winrm") { 224 err = setWinRMCertificate(&c) 225 if err != nil { 226 return nil, nil, err 227 } 228 } 229 230 var errs *packer.MultiError 231 errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...) 232 233 assertRequiredParametersSet(&c, errs) 234 assertTagProperties(&c, errs) 235 if errs != nil && len(errs.Errors) > 0 { 236 return nil, nil, errs 237 } 238 239 return &c, nil, nil 240 } 241 242 func setSshValues(c *Config) error { 243 if c.Comm.SSHTimeout == 0 { 244 c.Comm.SSHTimeout = 20 * time.Minute 245 } 246 247 if c.Comm.SSHPrivateKey != "" { 248 privateKeyBytes, err := ioutil.ReadFile(c.Comm.SSHPrivateKey) 249 if err != nil { 250 panic(err) 251 } 252 signer, err := ssh.ParsePrivateKey(privateKeyBytes) 253 if err != nil { 254 panic(err) 255 } 256 257 publicKey := signer.PublicKey() 258 c.sshAuthorizedKey = fmt.Sprintf("%s %s packer Azure Deployment%s", 259 publicKey.Type(), 260 base64.StdEncoding.EncodeToString(publicKey.Marshal()), 261 time.Now().Format(time.RFC3339)) 262 c.sshPrivateKey = string(privateKeyBytes) 263 264 } else { 265 sshKeyPair, err := NewOpenSshKeyPair() 266 if err != nil { 267 return err 268 } 269 270 c.sshAuthorizedKey = sshKeyPair.AuthorizedKey() 271 c.sshPrivateKey = sshKeyPair.PrivateKey() 272 } 273 274 return nil 275 } 276 277 func setWinRMCertificate(c *Config) error { 278 c.Comm.WinRMTransportDecorator = func(t *http.Transport) http.RoundTripper { 279 return &ntlmssp.Negotiator{RoundTripper: t} 280 } 281 282 cert, err := c.createCertificate() 283 c.winrmCertificate = cert 284 285 return err 286 } 287 288 func setRuntimeValues(c *Config) { 289 var tempName = NewTempName() 290 291 c.tmpAdminPassword = tempName.AdminPassword 292 c.tmpCertificatePassword = tempName.CertificatePassword 293 c.tmpComputeName = tempName.ComputeName 294 c.tmpDeploymentName = tempName.DeploymentName 295 c.tmpResourceGroupName = tempName.ResourceGroupName 296 c.tmpOSDiskName = tempName.OSDiskName 297 c.tmpKeyVaultName = tempName.KeyVaultName 298 } 299 300 func setUserNamePassword(c *Config) { 301 if c.Comm.SSHUsername == "" { 302 c.Comm.SSHUsername = DefaultUserName 303 } 304 305 c.UserName = c.Comm.SSHUsername 306 307 if c.Comm.SSHPassword != "" { 308 c.Password = c.Comm.SSHPassword 309 } else { 310 c.Password = c.tmpAdminPassword 311 } 312 } 313 314 func setCloudEnvironment(c *Config) error { 315 lookup := map[string]string{ 316 "CHINA": "AzureChinaCloud", 317 "CHINACLOUD": "AzureChinaCloud", 318 "AZURECHINACLOUD": "AzureChinaCloud", 319 320 "GERMAN": "AzureGermanCloud", 321 "GERMANCLOUD": "AzureGermanCloud", 322 "AZUREGERMANCLOUD": "AzureGermanCloud", 323 324 "GERMANY": "AzureGermanCloud", 325 "GERMANYCLOUD": "AzureGermanCloud", 326 "AZUREGERMANYCLOUD": "AzureGermanCloud", 327 328 "PUBLIC": "AzurePublicCloud", 329 "PUBLICCLOUD": "AzurePublicCloud", 330 "AZUREPUBLICCLOUD": "AzurePublicCloud", 331 332 "USGOVERNMENT": "AzureUSGovernmentCloud", 333 "USGOVERNMENTCLOUD": "AzureUSGovernmentCloud", 334 "AZUREUSGOVERNMENTCLOUD": "AzureUSGovernmentCloud", 335 } 336 337 name := strings.ToUpper(c.CloudEnvironmentName) 338 envName, ok := lookup[name] 339 if !ok { 340 return fmt.Errorf("There is no cloud envionment matching the name '%s'!", c.CloudEnvironmentName) 341 } 342 343 env, err := azure.EnvironmentFromName(envName) 344 c.cloudEnvironment = &env 345 return err 346 } 347 348 func setCustomData(c *Config) error { 349 if c.CustomDataFile == "" { 350 return nil 351 } 352 353 b, err := ioutil.ReadFile(c.CustomDataFile) 354 if err != nil { 355 return err 356 } 357 358 c.customData = base64.StdEncoding.EncodeToString(b) 359 return nil 360 } 361 362 func provideDefaultValues(c *Config) { 363 if c.VMSize == "" { 364 c.VMSize = DefaultVMSize 365 } 366 367 if c.ImageUrl == "" && c.ImageVersion == "" { 368 c.ImageVersion = DefaultImageVersion 369 } 370 371 if c.CloudEnvironmentName == "" { 372 c.CloudEnvironmentName = DefaultCloudEnvironmentName 373 } 374 } 375 376 func assertTagProperties(c *Config, errs *packer.MultiError) { 377 if len(c.AzureTags) > 15 { 378 errs = packer.MultiErrorAppend(errs, fmt.Errorf("a max of 15 tags are supported, but %d were provided", len(c.AzureTags))) 379 } 380 381 for k, v := range c.AzureTags { 382 if len(k) > 512 { 383 errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 512 character limit", k, len(k))) 384 } 385 if len(*v) > 256 { 386 errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 256 character limit", v, len(*v))) 387 } 388 } 389 } 390 391 func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { 392 ///////////////////////////////////////////// 393 // Authentication via OAUTH 394 395 // Check if device login is being asked for, and is allowed. 396 // 397 // Device login is enabled if the user only defines SubscriptionID and not 398 // ClientID, ClientSecret, and TenantID. 399 // 400 // Device login is not enabled for Windows because the WinRM certificate is 401 // readable by the ObjectID of the App. There may be another way to handle 402 // this case, but I am not currently aware of it - send feedback. 403 isUseDeviceLogin := func(c *Config) bool { 404 if c.OSType == constants.Target_Windows { 405 return false 406 } 407 408 return c.SubscriptionID != "" && 409 c.ClientID == "" && 410 c.ClientSecret == "" && 411 c.TenantID == "" 412 } 413 414 if isUseDeviceLogin(c) { 415 c.useDeviceLogin = true 416 } else { 417 if c.ClientID == "" { 418 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_id must be specified")) 419 } 420 421 if c.ClientSecret == "" { 422 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_secret must be specified")) 423 } 424 425 if c.SubscriptionID == "" { 426 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified")) 427 } 428 } 429 430 ///////////////////////////////////////////// 431 // Capture 432 if c.CaptureContainerName == "" { 433 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must be specified")) 434 } 435 436 if !reCaptureContainerName.MatchString(c.CaptureContainerName) { 437 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must satisfy the regular expression %q.", reCaptureContainerName.String())) 438 } 439 440 if strings.HasSuffix(c.CaptureContainerName, "-") { 441 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not end with a hyphen, e.g. '-'.")) 442 } 443 444 if strings.Contains(c.CaptureContainerName, "--") { 445 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not contain consecutive hyphens, e.g. '--'.")) 446 } 447 448 if c.CaptureNamePrefix == "" { 449 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must be specified")) 450 } 451 452 if !reCaptureNamePrefix.MatchString(c.CaptureNamePrefix) { 453 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must satisfy the regular expression %q.", reCaptureNamePrefix.String())) 454 } 455 456 if strings.HasSuffix(c.CaptureNamePrefix, "-") || strings.HasSuffix(c.CaptureNamePrefix, ".") { 457 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must not end with a hyphen or period.")) 458 } 459 460 ///////////////////////////////////////////// 461 // Compute 462 if c.ImageUrl == "" { 463 if c.ImagePublisher == "" { 464 errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_publisher must be specified")) 465 } 466 467 if c.ImageOffer == "" { 468 errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_offer must be specified")) 469 } 470 471 if c.ImageSku == "" { 472 errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_sku must be specified")) 473 } 474 } else { 475 if c.ImagePublisher != "" || c.ImageOffer != "" || c.ImageSku != "" || c.ImageVersion != "" { 476 errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_url must not be specified if image_publisher, image_offer, image_sku, or image_version is specified")) 477 } 478 } 479 480 if c.Location == "" { 481 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A location must be specified")) 482 } 483 484 ///////////////////////////////////////////// 485 // Deployment 486 if c.StorageAccount == "" { 487 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A storage_account must be specified")) 488 } 489 if c.ResourceGroupName == "" { 490 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group_name must be specified")) 491 } 492 if c.VirtualNetworkName == "" && c.VirtualNetworkResourceGroupName != "" { 493 errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_resource_group_name is specified, so must virtual_network_name")) 494 } 495 if c.VirtualNetworkName == "" && c.VirtualNetworkSubnetName != "" { 496 errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_subnet_name is specified, so must virtual_network_name")) 497 } 498 499 ///////////////////////////////////////////// 500 // OS 501 if strings.EqualFold(c.OSType, constants.Target_Linux) { 502 c.OSType = constants.Target_Linux 503 } else if strings.EqualFold(c.OSType, constants.Target_Windows) { 504 c.OSType = constants.Target_Windows 505 } else if c.OSType == "" { 506 errs = packer.MultiErrorAppend(errs, fmt.Errorf("An os_type must be specified")) 507 } else { 508 errs = packer.MultiErrorAppend(errs, fmt.Errorf("The os_type %q is invalid", c.OSType)) 509 } 510 }