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  }