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