2016-03-04 05:14:55 -05:00
// Copyright (c) Microsoft Corporation. All rights reserved.
2016-05-06 23:32:18 -04:00
// Licensed under the MIT License. See the LICENSE file in builder/azure for license information.
2016-03-04 05:14:55 -05:00
package arm
import (
2016-04-21 19:50:03 -04:00
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
2016-03-04 05:14:55 -05:00
"encoding/base64"
2016-04-21 19:50:03 -04:00
"encoding/json"
2016-03-04 05:14:55 -05:00
"fmt"
"io/ioutil"
2016-04-21 19:50:03 -04:00
"math/big"
2016-05-18 20:25:57 -04:00
"regexp"
"strings"
2016-03-04 05:14:55 -05:00
"time"
"github.com/Azure/azure-sdk-for-go/arm/compute"
2016-05-18 20:25:57 -04:00
"github.com/Azure/go-autorest/autorest/azure"
2016-03-04 05:14:55 -05:00
"github.com/Azure/go-autorest/autorest/to"
2017-01-18 16:11:48 -05:00
"github.com/masterzen/winrm"
2016-05-18 20:25:57 -04:00
2016-04-21 19:50:03 -04:00
"github.com/mitchellh/packer/builder/azure/common/constants"
"github.com/mitchellh/packer/builder/azure/pkcs12"
2016-03-04 05:14:55 -05:00
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
2016-04-21 19:50:03 -04:00
"golang.org/x/crypto/ssh"
2016-03-04 05:14:55 -05:00
)
const (
2016-04-21 19:50:03 -04:00
DefaultCloudEnvironmentName = "Public"
DefaultImageVersion = "latest"
DefaultUserName = "packer"
DefaultVMSize = "Standard_A1"
2016-03-04 05:14:55 -05:00
)
2016-05-18 20:25:57 -04:00
var (
reCaptureContainerName = regexp . MustCompile ( "^[a-z0-9][a-z0-9\\-]{2,62}$" )
reCaptureNamePrefix = regexp . MustCompile ( "^[A-Za-z0-9][A-Za-z0-9_\\-\\.]{0,23}$" )
)
2016-03-04 05:14:55 -05:00
type Config struct {
common . PackerConfig ` mapstructure:",squash" `
// Authentication via OAUTH
ClientID string ` mapstructure:"client_id" `
ClientSecret string ` mapstructure:"client_secret" `
2016-04-21 19:50:03 -04:00
ObjectID string ` mapstructure:"object_id" `
2016-03-04 05:14:55 -05:00
TenantID string ` mapstructure:"tenant_id" `
SubscriptionID string ` mapstructure:"subscription_id" `
// Capture
CaptureNamePrefix string ` mapstructure:"capture_name_prefix" `
CaptureContainerName string ` mapstructure:"capture_container_name" `
// Compute
ImagePublisher string ` mapstructure:"image_publisher" `
ImageOffer string ` mapstructure:"image_offer" `
ImageSku string ` mapstructure:"image_sku" `
2016-04-21 19:50:03 -04:00
ImageVersion string ` mapstructure:"image_version" `
2016-05-21 02:01:16 -04:00
ImageUrl string ` mapstructure:"image_url" `
2016-03-04 05:14:55 -05:00
Location string ` mapstructure:"location" `
VMSize string ` mapstructure:"vm_size" `
// Deployment
2016-07-29 17:17:33 -04:00
AzureTags map [ string ] * string ` mapstructure:"azure_tags" `
ResourceGroupName string ` mapstructure:"resource_group_name" `
StorageAccount string ` mapstructure:"storage_account" `
2016-06-30 19:51:52 -04:00
storageAccountBlobEndpoint string
CloudEnvironmentName string ` mapstructure:"cloud_environment_name" `
cloudEnvironment * azure . Environment
VirtualNetworkName string ` mapstructure:"virtual_network_name" `
VirtualNetworkSubnetName string ` mapstructure:"virtual_network_subnet_name" `
VirtualNetworkResourceGroupName string ` mapstructure:"virtual_network_resource_group_name" `
2016-10-13 14:56:23 -04:00
CustomDataFile string ` mapstructure:"custom_data_file" `
customData string
2016-04-21 19:50:03 -04:00
// OS
2016-10-12 19:24:04 -04:00
OSType string ` mapstructure:"os_type" `
OSDiskSizeGB int32 ` mapstructure:"os_disk_size_gb" `
2016-03-04 05:14:55 -05:00
// Runtime Values
2016-04-21 19:50:03 -04:00
UserName string
Password string
tmpAdminPassword string
tmpCertificatePassword string
tmpResourceGroupName string
tmpComputeName string
tmpDeploymentName string
tmpKeyVaultName string
tmpOSDiskName string
tmpWinRMCertificateUrl string
useDeviceLogin bool
2016-03-04 05:14:55 -05:00
// Authentication with the VM via SSH
sshAuthorizedKey string
sshPrivateKey string
2016-04-21 19:50:03 -04:00
// Authentication with the VM via WinRM
winrmCertificate string
2016-03-04 05:14:55 -05:00
Comm communicator . Config ` mapstructure:",squash" `
ctx * interpolate . Context
}
2016-04-21 19:50:03 -04:00
type keyVaultCertificate struct {
Data string ` json:"data" `
DataType string ` json:"dataType" `
Password string ` json:"password,omitempty" `
}
2016-03-04 05:14:55 -05:00
func ( c * Config ) toVirtualMachineCaptureParameters ( ) * compute . VirtualMachineCaptureParameters {
return & compute . VirtualMachineCaptureParameters {
DestinationContainerName : & c . CaptureContainerName ,
VhdPrefix : & c . CaptureNamePrefix ,
OverwriteVhds : to . BoolPtr ( false ) ,
}
}
2016-04-21 19:50:03 -04:00
func ( c * Config ) createCertificate ( ) ( string , error ) {
privateKey , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
2016-07-16 01:23:53 -04:00
err = fmt . Errorf ( "Failed to Generate Private Key: %s" , err )
2016-04-21 19:50:03 -04:00
return "" , err
}
host := fmt . Sprintf ( "%s.cloudapp.net" , c . tmpComputeName )
notBefore := time . Now ( )
2016-05-17 17:15:24 -04:00
notAfter := notBefore . Add ( 24 * time . Hour )
2016-04-21 19:50:03 -04:00
serialNumber , err := rand . Int ( rand . Reader , new ( big . Int ) . Lsh ( big . NewInt ( 1 ) , 128 ) )
if err != nil {
2016-07-16 01:23:53 -04:00
err = fmt . Errorf ( "Failed to Generate Serial Number: %v" , err )
2016-04-21 19:50:03 -04:00
return "" , err
}
template := x509 . Certificate {
SerialNumber : serialNumber ,
Issuer : pkix . Name {
CommonName : host ,
} ,
Subject : pkix . Name {
CommonName : host ,
} ,
NotBefore : notBefore ,
NotAfter : notAfter ,
KeyUsage : x509 . KeyUsageKeyEncipherment | x509 . KeyUsageDigitalSignature ,
ExtKeyUsage : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageServerAuth } ,
BasicConstraintsValid : true ,
}
derBytes , err := x509 . CreateCertificate ( rand . Reader , & template , & template , & privateKey . PublicKey , privateKey )
if err != nil {
err = fmt . Errorf ( "Failed to Create Certificate: %s" , err )
return "" , err
}
pfxBytes , err := pkcs12 . Encode ( derBytes , privateKey , c . tmpCertificatePassword )
if err != nil {
err = fmt . Errorf ( "Failed to encode certificate as PFX: %s" , err )
return "" , err
}
keyVaultDescription := keyVaultCertificate {
Data : base64 . StdEncoding . EncodeToString ( pfxBytes ) ,
DataType : "pfx" ,
Password : c . tmpCertificatePassword ,
}
bytes , err := json . Marshal ( keyVaultDescription )
if err != nil {
err = fmt . Errorf ( "Failed to marshal key vault description: %s" , err )
return "" , err
}
return base64 . StdEncoding . EncodeToString ( bytes ) , nil
}
2016-03-04 05:14:55 -05:00
func newConfig ( raws ... interface { } ) ( * Config , [ ] string , error ) {
var c Config
err := config . Decode ( & c , & config . DecodeOpts {
Interpolate : true ,
InterpolateContext : c . ctx ,
} , raws ... )
if err != nil {
return nil , nil , err
}
provideDefaultValues ( & c )
setRuntimeValues ( & c )
setUserNamePassword ( & c )
2016-04-21 19:50:03 -04:00
err = setCloudEnvironment ( & c )
if err != nil {
return nil , nil , err
}
2016-03-04 05:14:55 -05:00
2016-10-13 14:56:23 -04:00
err = setCustomData ( & c )
if err != nil {
return nil , nil , err
}
2016-05-17 16:53:01 -04:00
// NOTE: if the user did not specify a communicator, then default to both
// SSH and WinRM. This is for backwards compatibility because the code did
2016-05-21 02:01:16 -04:00
// not specifically force the user to set a communicator.
2016-05-17 16:53:01 -04:00
if c . Comm . Type == "" || strings . EqualFold ( c . Comm . Type , "ssh" ) {
err = setSshValues ( & c )
if err != nil {
return nil , nil , err
}
2016-03-04 05:14:55 -05:00
}
2016-05-17 16:53:01 -04:00
if c . Comm . Type == "" || strings . EqualFold ( c . Comm . Type , "winrm" ) {
err = setWinRMCertificate ( & c )
if err != nil {
return nil , nil , err
}
2016-04-21 19:50:03 -04:00
}
2016-03-04 05:14:55 -05:00
var errs * packer . MultiError
errs = packer . MultiErrorAppend ( errs , c . Comm . Prepare ( c . ctx ) ... )
assertRequiredParametersSet ( & c , errs )
2016-07-29 17:17:33 -04:00
assertTagProperties ( & c , errs )
2016-03-04 05:14:55 -05:00
if errs != nil && len ( errs . Errors ) > 0 {
return nil , nil , errs
}
return & c , nil , nil
}
func setSshValues ( c * Config ) error {
if c . Comm . SSHTimeout == 0 {
c . Comm . SSHTimeout = 20 * time . Minute
}
if c . Comm . SSHPrivateKey != "" {
privateKeyBytes , err := ioutil . ReadFile ( c . Comm . SSHPrivateKey )
if err != nil {
panic ( err )
}
signer , err := ssh . ParsePrivateKey ( privateKeyBytes )
if err != nil {
panic ( err )
}
publicKey := signer . PublicKey ( )
c . sshAuthorizedKey = fmt . Sprintf ( "%s %s packer Azure Deployment%s" ,
publicKey . Type ( ) ,
base64 . StdEncoding . EncodeToString ( publicKey . Marshal ( ) ) ,
time . Now ( ) . Format ( time . RFC3339 ) )
c . sshPrivateKey = string ( privateKeyBytes )
} else {
sshKeyPair , err := NewOpenSshKeyPair ( )
if err != nil {
return err
}
c . sshAuthorizedKey = sshKeyPair . AuthorizedKey ( )
c . sshPrivateKey = sshKeyPair . PrivateKey ( )
}
return nil
}
2016-04-21 19:50:03 -04:00
func setWinRMCertificate ( c * Config ) error {
2017-01-18 16:11:48 -05:00
c . Comm . WinRMTransportDecorator =
func ( ) winrm . Transporter { return & winrm . ClientNTLM { } }
2016-05-17 16:53:01 -04:00
2016-04-21 19:50:03 -04:00
cert , err := c . createCertificate ( )
c . winrmCertificate = cert
return err
}
2016-03-04 05:14:55 -05:00
func setRuntimeValues ( c * Config ) {
var tempName = NewTempName ( )
c . tmpAdminPassword = tempName . AdminPassword
2016-04-21 19:50:03 -04:00
c . tmpCertificatePassword = tempName . CertificatePassword
2016-03-04 05:14:55 -05:00
c . tmpComputeName = tempName . ComputeName
c . tmpDeploymentName = tempName . DeploymentName
c . tmpResourceGroupName = tempName . ResourceGroupName
c . tmpOSDiskName = tempName . OSDiskName
2016-04-21 19:50:03 -04:00
c . tmpKeyVaultName = tempName . KeyVaultName
2016-03-04 05:14:55 -05:00
}
func setUserNamePassword ( c * Config ) {
if c . Comm . SSHUsername == "" {
c . Comm . SSHUsername = DefaultUserName
}
c . UserName = c . Comm . SSHUsername
if c . Comm . SSHPassword != "" {
c . Password = c . Comm . SSHPassword
} else {
c . Password = c . tmpAdminPassword
}
}
2016-04-21 19:50:03 -04:00
func setCloudEnvironment ( c * Config ) error {
2016-07-20 15:09:41 -04:00
lookup := map [ string ] string {
"CHINA" : "AzureChinaCloud" ,
"CHINACLOUD" : "AzureChinaCloud" ,
"AZURECHINACLOUD" : "AzureChinaCloud" ,
"GERMAN" : "AzureGermanCloud" ,
"GERMANCLOUD" : "AzureGermanCloud" ,
"AZUREGERMANCLOUD" : "AzureGermanCloud" ,
"GERMANY" : "AzureGermanCloud" ,
"GERMANYCLOUD" : "AzureGermanCloud" ,
"AZUREGERMANYCLOUD" : "AzureGermanCloud" ,
"PUBLIC" : "AzurePublicCloud" ,
"PUBLICCLOUD" : "AzurePublicCloud" ,
"AZUREPUBLICCLOUD" : "AzurePublicCloud" ,
"USGOVERNMENT" : "AzureUSGovernmentCloud" ,
"USGOVERNMENTCLOUD" : "AzureUSGovernmentCloud" ,
"AZUREUSGOVERNMENTCLOUD" : "AzureUSGovernmentCloud" ,
}
2016-04-21 19:50:03 -04:00
name := strings . ToUpper ( c . CloudEnvironmentName )
2016-07-20 15:09:41 -04:00
envName , ok := lookup [ name ]
if ! ok {
2016-04-21 19:50:03 -04:00
return fmt . Errorf ( "There is no cloud envionment matching the name '%s'!" , c . CloudEnvironmentName )
}
2016-07-20 15:09:41 -04:00
env , err := azure . EnvironmentFromName ( envName )
c . cloudEnvironment = & env
return err
2016-04-21 19:50:03 -04:00
}
2016-10-13 14:56:23 -04:00
func setCustomData ( c * Config ) error {
if c . CustomDataFile == "" {
return nil
}
b , err := ioutil . ReadFile ( c . CustomDataFile )
if err != nil {
return err
}
c . customData = base64 . StdEncoding . EncodeToString ( b )
return nil
}
2016-03-04 05:14:55 -05:00
func provideDefaultValues ( c * Config ) {
if c . VMSize == "" {
c . VMSize = DefaultVMSize
}
2016-04-21 19:50:03 -04:00
2016-05-21 02:01:16 -04:00
if c . ImageUrl == "" && c . ImageVersion == "" {
2016-04-21 19:50:03 -04:00
c . ImageVersion = DefaultImageVersion
}
if c . CloudEnvironmentName == "" {
c . CloudEnvironmentName = DefaultCloudEnvironmentName
}
2016-03-04 05:14:55 -05:00
}
2016-07-29 17:17:33 -04:00
func assertTagProperties ( c * Config , errs * packer . MultiError ) {
if len ( c . AzureTags ) > 15 {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "a max of 15 tags are supported, but %d were provided" , len ( c . AzureTags ) ) )
}
for k , v := range c . AzureTags {
if len ( k ) > 512 {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "the tag name %q exceeds (%d) the 512 character limit" , k , len ( k ) ) )
}
if len ( * v ) > 256 {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "the tag name %q exceeds (%d) the 256 character limit" , v , len ( * v ) ) )
}
}
}
2016-03-04 05:14:55 -05:00
func assertRequiredParametersSet ( c * Config , errs * packer . MultiError ) {
/////////////////////////////////////////////
// Authentication via OAUTH
2016-04-21 19:50:03 -04:00
// Check if device login is being asked for, and is allowed.
//
// Device login is enabled if the user only defines SubscriptionID and not
// ClientID, ClientSecret, and TenantID.
//
// Device login is not enabled for Windows because the WinRM certificate is
// readable by the ObjectID of the App. There may be another way to handle
// this case, but I am not currently aware of it - send feedback.
isUseDeviceLogin := func ( c * Config ) bool {
if c . OSType == constants . Target_Windows {
return false
}
2016-03-04 05:14:55 -05:00
2016-04-21 19:50:03 -04:00
return c . SubscriptionID != "" &&
c . ClientID == "" &&
c . ClientSecret == "" &&
c . TenantID == ""
2016-03-04 05:14:55 -05:00
}
2016-04-21 19:50:03 -04:00
if isUseDeviceLogin ( c ) {
c . useDeviceLogin = true
} else {
if c . ClientID == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A client_id must be specified" ) )
}
if c . ClientSecret == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A client_secret must be specified" ) )
}
2016-03-04 05:14:55 -05:00
2016-04-21 19:50:03 -04:00
if c . SubscriptionID == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A subscription_id must be specified" ) )
}
2016-03-04 05:14:55 -05:00
}
/////////////////////////////////////////////
// Capture
if c . CaptureContainerName == "" {
2016-05-18 20:25:57 -04:00
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_container_name must be specified" ) )
}
if ! reCaptureContainerName . MatchString ( c . CaptureContainerName ) {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_container_name must satisfy the regular expression %q." , reCaptureContainerName . String ( ) ) )
}
if strings . HasSuffix ( c . CaptureContainerName , "-" ) {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_container_name must not end with a hyphen, e.g. '-'." ) )
}
if strings . Contains ( c . CaptureContainerName , "--" ) {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_container_name must not contain consecutive hyphens, e.g. '--'." ) )
2016-03-04 05:14:55 -05:00
}
if c . CaptureNamePrefix == "" {
2016-05-18 20:25:57 -04:00
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_name_prefix must be specified" ) )
}
if ! reCaptureNamePrefix . MatchString ( c . CaptureNamePrefix ) {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_name_prefix must satisfy the regular expression %q." , reCaptureNamePrefix . String ( ) ) )
}
if strings . HasSuffix ( c . CaptureNamePrefix , "-" ) || strings . HasSuffix ( c . CaptureNamePrefix , "." ) {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A capture_name_prefix must not end with a hyphen or period." ) )
2016-03-04 05:14:55 -05:00
}
/////////////////////////////////////////////
// Compute
2016-05-21 02:01:16 -04:00
if c . ImageUrl == "" {
if c . ImagePublisher == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "An image_publisher must be specified" ) )
}
2016-03-04 05:14:55 -05:00
2016-05-21 02:01:16 -04:00
if c . ImageOffer == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "An image_offer must be specified" ) )
}
2016-03-04 05:14:55 -05:00
2016-05-21 02:01:16 -04:00
if c . ImageSku == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "An image_sku must be specified" ) )
}
} else {
if c . ImagePublisher != "" || c . ImageOffer != "" || c . ImageSku != "" || c . ImageVersion != "" {
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" ) )
}
2016-03-04 05:14:55 -05:00
}
if c . Location == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A location must be specified" ) )
}
/////////////////////////////////////////////
// Deployment
if c . StorageAccount == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A storage_account must be specified" ) )
}
2016-06-09 04:00:23 -04:00
if c . ResourceGroupName == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "A resource_group_name must be specified" ) )
}
2016-06-30 19:51:52 -04:00
if c . VirtualNetworkName == "" && c . VirtualNetworkResourceGroupName != "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "If virtual_network_resource_group_name is specified, so must virtual_network_name" ) )
}
if c . VirtualNetworkName == "" && c . VirtualNetworkSubnetName != "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "If virtual_network_subnet_name is specified, so must virtual_network_name" ) )
}
2016-04-21 19:50:03 -04:00
/////////////////////////////////////////////
// OS
2016-10-12 20:54:59 -04:00
if strings . EqualFold ( c . OSType , constants . Target_Linux ) {
c . OSType = constants . Target_Linux
} else if strings . EqualFold ( c . OSType , constants . Target_Windows ) {
c . OSType = constants . Target_Windows
} else if c . OSType == "" {
2016-04-21 19:50:03 -04:00
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "An os_type must be specified" ) )
2016-10-12 20:54:59 -04:00
} else {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "The os_type %q is invalid" , c . OSType ) )
2016-04-21 19:50:03 -04:00
}
2016-03-04 05:14:55 -05:00
}