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  }