github.com/yoctocloud/packer@v0.6.2-0.20160520224004-e11a0a18423f/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 Location string `mapstructure:"location"` 69 VMSize string `mapstructure:"vm_size"` 70 71 // Deployment 72 ResourceGroupName string `mapstructure:"resource_group_name"` 73 StorageAccount string `mapstructure:"storage_account"` 74 storageAccountBlobEndpoint string 75 CloudEnvironmentName string `mapstructure:"cloud_environment_name"` 76 cloudEnvironment *azure.Environment 77 78 // OS 79 OSType string `mapstructure:"os_type"` 80 81 // Runtime Values 82 UserName string 83 Password string 84 tmpAdminPassword string 85 tmpCertificatePassword string 86 tmpResourceGroupName string 87 tmpComputeName string 88 tmpDeploymentName string 89 tmpKeyVaultName string 90 tmpOSDiskName string 91 tmpWinRMCertificateUrl string 92 93 useDeviceLogin bool 94 95 // Authentication with the VM via SSH 96 sshAuthorizedKey string 97 sshPrivateKey string 98 99 // Authentication with the VM via WinRM 100 winrmCertificate string 101 102 Comm communicator.Config `mapstructure:",squash"` 103 ctx *interpolate.Context 104 } 105 106 type keyVaultCertificate struct { 107 Data string `json:"data"` 108 DataType string `json:"dataType"` 109 Password string `json:"password,omitempty"` 110 } 111 112 // If we ever feel the need to support more templates consider moving this 113 // method to its own factory class. 114 func (c *Config) toTemplateParameters() *TemplateParameters { 115 templateParameters := &TemplateParameters{ 116 AdminUsername: &TemplateParameter{c.UserName}, 117 AdminPassword: &TemplateParameter{c.Password}, 118 DnsNameForPublicIP: &TemplateParameter{c.tmpComputeName}, 119 ImageOffer: &TemplateParameter{c.ImageOffer}, 120 ImagePublisher: &TemplateParameter{c.ImagePublisher}, 121 ImageSku: &TemplateParameter{c.ImageSku}, 122 ImageVersion: &TemplateParameter{c.ImageVersion}, 123 OSDiskName: &TemplateParameter{c.tmpOSDiskName}, 124 StorageAccountBlobEndpoint: &TemplateParameter{c.storageAccountBlobEndpoint}, 125 VMSize: &TemplateParameter{c.VMSize}, 126 VMName: &TemplateParameter{c.tmpComputeName}, 127 } 128 129 switch c.OSType { 130 case constants.Target_Linux: 131 templateParameters.SshAuthorizedKey = &TemplateParameter{c.sshAuthorizedKey} 132 case constants.Target_Windows: 133 templateParameters.TenantId = &TemplateParameter{c.TenantID} 134 templateParameters.ObjectId = &TemplateParameter{c.ObjectID} 135 136 templateParameters.KeyVaultName = &TemplateParameter{c.tmpKeyVaultName} 137 templateParameters.KeyVaultSecretValue = &TemplateParameter{c.winrmCertificate} 138 templateParameters.WinRMCertificateUrl = &TemplateParameter{c.tmpWinRMCertificateUrl} 139 } 140 141 return templateParameters 142 } 143 144 func (c *Config) toVirtualMachineCaptureParameters() *compute.VirtualMachineCaptureParameters { 145 return &compute.VirtualMachineCaptureParameters{ 146 DestinationContainerName: &c.CaptureContainerName, 147 VhdPrefix: &c.CaptureNamePrefix, 148 OverwriteVhds: to.BoolPtr(false), 149 } 150 } 151 152 func (c *Config) createCertificate() (string, error) { 153 privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 154 if err != nil { 155 err := fmt.Errorf("Failed to Generate Private Key: %s", err) 156 return "", err 157 } 158 159 host := fmt.Sprintf("%s.cloudapp.net", c.tmpComputeName) 160 notBefore := time.Now() 161 notAfter := notBefore.Add(24 * time.Hour) 162 163 serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) 164 if err != nil { 165 err := fmt.Errorf("Failed to Generate Serial Number: %v", err) 166 return "", err 167 } 168 169 template := x509.Certificate{ 170 SerialNumber: serialNumber, 171 Issuer: pkix.Name{ 172 CommonName: host, 173 }, 174 Subject: pkix.Name{ 175 CommonName: host, 176 }, 177 NotBefore: notBefore, 178 NotAfter: notAfter, 179 180 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 181 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 182 BasicConstraintsValid: true, 183 } 184 185 derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 186 if err != nil { 187 err = fmt.Errorf("Failed to Create Certificate: %s", err) 188 return "", err 189 } 190 191 pfxBytes, err := pkcs12.Encode(derBytes, privateKey, c.tmpCertificatePassword) 192 if err != nil { 193 err = fmt.Errorf("Failed to encode certificate as PFX: %s", err) 194 return "", err 195 } 196 197 keyVaultDescription := keyVaultCertificate{ 198 Data: base64.StdEncoding.EncodeToString(pfxBytes), 199 DataType: "pfx", 200 Password: c.tmpCertificatePassword, 201 } 202 203 bytes, err := json.Marshal(keyVaultDescription) 204 if err != nil { 205 err = fmt.Errorf("Failed to marshal key vault description: %s", err) 206 return "", err 207 } 208 209 return base64.StdEncoding.EncodeToString(bytes), nil 210 } 211 212 func newConfig(raws ...interface{}) (*Config, []string, error) { 213 var c Config 214 215 err := config.Decode(&c, &config.DecodeOpts{ 216 Interpolate: true, 217 InterpolateContext: c.ctx, 218 }, raws...) 219 220 if err != nil { 221 return nil, nil, err 222 } 223 224 provideDefaultValues(&c) 225 setRuntimeValues(&c) 226 setUserNamePassword(&c) 227 err = setCloudEnvironment(&c) 228 if err != nil { 229 return nil, nil, err 230 } 231 232 // NOTE: if the user did not specify a communicator, then default to both 233 // SSH and WinRM. This is for backwards compatibility because the code did 234 // not specifically force the user to specify a value. 235 if c.Comm.Type == "" || strings.EqualFold(c.Comm.Type, "ssh") { 236 err = setSshValues(&c) 237 if err != nil { 238 return nil, nil, err 239 } 240 } 241 242 if c.Comm.Type == "" || strings.EqualFold(c.Comm.Type, "winrm") { 243 err = setWinRMCertificate(&c) 244 if err != nil { 245 return nil, nil, err 246 } 247 } 248 249 var errs *packer.MultiError 250 errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...) 251 252 assertRequiredParametersSet(&c, errs) 253 if errs != nil && len(errs.Errors) > 0 { 254 return nil, nil, errs 255 } 256 257 return &c, nil, nil 258 } 259 260 func setSshValues(c *Config) error { 261 if c.Comm.SSHTimeout == 0 { 262 c.Comm.SSHTimeout = 20 * time.Minute 263 } 264 265 if c.Comm.SSHPrivateKey != "" { 266 privateKeyBytes, err := ioutil.ReadFile(c.Comm.SSHPrivateKey) 267 if err != nil { 268 panic(err) 269 } 270 signer, err := ssh.ParsePrivateKey(privateKeyBytes) 271 if err != nil { 272 panic(err) 273 } 274 275 publicKey := signer.PublicKey() 276 c.sshAuthorizedKey = fmt.Sprintf("%s %s packer Azure Deployment%s", 277 publicKey.Type(), 278 base64.StdEncoding.EncodeToString(publicKey.Marshal()), 279 time.Now().Format(time.RFC3339)) 280 c.sshPrivateKey = string(privateKeyBytes) 281 282 } else { 283 sshKeyPair, err := NewOpenSshKeyPair() 284 if err != nil { 285 return err 286 } 287 288 c.sshAuthorizedKey = sshKeyPair.AuthorizedKey() 289 c.sshPrivateKey = sshKeyPair.PrivateKey() 290 } 291 292 return nil 293 } 294 295 func setWinRMCertificate(c *Config) error { 296 c.Comm.WinRMTransportDecorator = func(t *http.Transport) http.RoundTripper { 297 return &ntlmssp.Negotiator{RoundTripper: t} 298 } 299 300 cert, err := c.createCertificate() 301 c.winrmCertificate = cert 302 303 return err 304 } 305 306 func setRuntimeValues(c *Config) { 307 var tempName = NewTempName() 308 309 c.tmpAdminPassword = tempName.AdminPassword 310 c.tmpCertificatePassword = tempName.CertificatePassword 311 c.tmpComputeName = tempName.ComputeName 312 c.tmpDeploymentName = tempName.DeploymentName 313 c.tmpResourceGroupName = tempName.ResourceGroupName 314 c.tmpOSDiskName = tempName.OSDiskName 315 c.tmpKeyVaultName = tempName.KeyVaultName 316 } 317 318 func setUserNamePassword(c *Config) { 319 if c.Comm.SSHUsername == "" { 320 c.Comm.SSHUsername = DefaultUserName 321 } 322 323 c.UserName = c.Comm.SSHUsername 324 325 if c.Comm.SSHPassword != "" { 326 c.Password = c.Comm.SSHPassword 327 } else { 328 c.Password = c.tmpAdminPassword 329 } 330 } 331 332 func setCloudEnvironment(c *Config) error { 333 name := strings.ToUpper(c.CloudEnvironmentName) 334 switch name { 335 case "CHINA", "CHINACLOUD", "AZURECHINACLOUD": 336 c.cloudEnvironment = &azure.ChinaCloud 337 case "PUBLIC", "PUBLICCLOUD", "AZUREPUBLICCLOUD": 338 c.cloudEnvironment = &azure.PublicCloud 339 case "USGOVERNMENT", "USGOVERNMENTCLOUD", "AZUREUSGOVERNMENTCLOUD": 340 c.cloudEnvironment = &azure.USGovernmentCloud 341 default: 342 return fmt.Errorf("There is no cloud envionment matching the name '%s'!", c.CloudEnvironmentName) 343 } 344 345 return nil 346 } 347 348 func provideDefaultValues(c *Config) { 349 if c.VMSize == "" { 350 c.VMSize = DefaultVMSize 351 } 352 353 if c.ImageVersion == "" { 354 c.ImageVersion = DefaultImageVersion 355 } 356 357 if c.CloudEnvironmentName == "" { 358 c.CloudEnvironmentName = DefaultCloudEnvironmentName 359 } 360 } 361 362 func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { 363 ///////////////////////////////////////////// 364 // Authentication via OAUTH 365 366 // Check if device login is being asked for, and is allowed. 367 // 368 // Device login is enabled if the user only defines SubscriptionID and not 369 // ClientID, ClientSecret, and TenantID. 370 // 371 // Device login is not enabled for Windows because the WinRM certificate is 372 // readable by the ObjectID of the App. There may be another way to handle 373 // this case, but I am not currently aware of it - send feedback. 374 isUseDeviceLogin := func(c *Config) bool { 375 if c.OSType == constants.Target_Windows { 376 return false 377 } 378 379 return c.SubscriptionID != "" && 380 c.ClientID == "" && 381 c.ClientSecret == "" && 382 c.TenantID == "" 383 } 384 385 if isUseDeviceLogin(c) { 386 c.useDeviceLogin = true 387 } else { 388 if c.ClientID == "" { 389 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_id must be specified")) 390 } 391 392 if c.ClientSecret == "" { 393 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_secret must be specified")) 394 } 395 396 if c.TenantID == "" { 397 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A tenant_id must be specified")) 398 } 399 400 if c.SubscriptionID == "" { 401 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified")) 402 } 403 } 404 405 ///////////////////////////////////////////// 406 // Capture 407 if c.CaptureContainerName == "" { 408 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must be specified")) 409 } 410 411 if !reCaptureContainerName.MatchString(c.CaptureContainerName) { 412 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must satisfy the regular expression %q.", reCaptureContainerName.String())) 413 } 414 415 if strings.HasSuffix(c.CaptureContainerName, "-") { 416 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not end with a hyphen, e.g. '-'.")) 417 } 418 419 if strings.Contains(c.CaptureContainerName, "--") { 420 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not contain consecutive hyphens, e.g. '--'.")) 421 } 422 423 if c.CaptureNamePrefix == "" { 424 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must be specified")) 425 } 426 427 if !reCaptureNamePrefix.MatchString(c.CaptureNamePrefix) { 428 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must satisfy the regular expression %q.", reCaptureNamePrefix.String())) 429 } 430 431 if strings.HasSuffix(c.CaptureNamePrefix, "-") || strings.HasSuffix(c.CaptureNamePrefix, ".") { 432 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must not end with a hyphen or period.")) 433 } 434 435 ///////////////////////////////////////////// 436 // Compute 437 if c.ImagePublisher == "" { 438 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A image_publisher must be specified")) 439 } 440 441 if c.ImageOffer == "" { 442 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A image_offer must be specified")) 443 } 444 445 if c.ImageSku == "" { 446 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A image_sku must be specified")) 447 } 448 449 if c.Location == "" { 450 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A location must be specified")) 451 } 452 453 ///////////////////////////////////////////// 454 // Deployment 455 if c.StorageAccount == "" { 456 errs = packer.MultiErrorAppend(errs, fmt.Errorf("A storage_account must be specified")) 457 } 458 459 ///////////////////////////////////////////// 460 // OS 461 if c.OSType != constants.Target_Linux && c.OSType != constants.Target_Windows { 462 errs = packer.MultiErrorAppend(errs, fmt.Errorf("An os_type must be specified")) 463 } 464 }