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  }