diff --git a/builder/azure/arm/README.md b/builder/azure/arm/README.md new file mode 100644 index 000000000..32440f6d0 --- /dev/null +++ b/builder/azure/arm/README.md @@ -0,0 +1,116 @@ +# packer-azure-arm + +The ARM flavor of packer-azure utilizes the +[Azure Resource Manager APIs](https://msdn.microsoft.com/en-us/library/azure/dn790568.aspx). +Please see the +[overview](https://azure.microsoft.com/en-us/documentation/articles/resource-group-overview/) +for more information about ARM as well as the benefit of ARM. + +## Getting Started + +The ARM APIs use OAUTH to authenticate, so you must create a Service +Principal. The following articles are a good starting points. + + * [Automating Azure on your CI server using a Service Principal](http://blog.davidebbo.com/2014/12/azure-service-principal.html) + * [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) + +There are three pieces of configuration you will need as a result of +creating a Service Principal. + + 1. Client ID (aka Service Principal ID) + 1. Client Secret (aka Service Principal generated key) + 1. Client Tenant (aka Azure Active Directory tenant that owns the + Service Principal) + +You will also need the following. + + 1. Subscription ID + 1. Resource Group + 1. Storage Account + +Resource Group is where your storage account is located, and Storage +Account is where the created packer image will be stored. + +The Service Principal has been tested with the following [permissions](https://azure.microsoft.com/en-us/documentation/articles/role-based-access-control-configure/). +Please review the document for the [built in roles](https://azure.microsoft.com/en-gb/documentation/articles/role-based-access-built-in-roles/) +for more details. + + * Owner + +> NOTE: the Owner role is too powerful, and more explicit set of roles +> is TBD. Issue #183 is tracking this work. + +### Sample Ubuntu + +The following is a sample Packer template for use with the Packer +Azure for ARM builder. + +```json +{ + "variables": { + "cid": "your_client_id", + "cst": "your_client_secret", + "tid": "your_client_tenant", + "sid": "your_subscription_id", + + "rgn": "your_resource_group", + "sa": "your_storage_account" + }, + "builders": [ + { + "type": "azure-arm", + + "client_id": "{{user `cid`}}", + "client_secret": "{{user `cst`}}", + "subscription_id": "{{user `sid`}}", + "tenant_id": "{{user `tid`}}", + + "capture_container_name": "images", + "capture_name_prefix": "my_prefix", + + "image_publisher": "Canonical", + "image_offer": "UbuntuServer", + "image_sku": "14.04.3-LTS", + + "location": "South Central US", + + "resource_group_name": "{{user `rgn`}}", + "storage_account": "{{user `sa`}}", + + "vm_size": "Standard_A1" + } + ], + "provisioners": [ + { + "execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'", + "inline": [ + "sudo apt-get update", + ], + "inline_shebang": "/bin/sh -x", + "type": "shell" + } + ] +} +``` + +Using the above template, Packer would be invoked as follows. + +> NOTE: the following variables must be **changed** based on your +> subscription. These values are just dummy values, but they match +> format of expected, e.g. if the value is a GUID the sample is a +> GUID. + +```bat +packer build^ + -var cid="593c4dc4-9cd7-49af-9fe0-1ea5055ac1e4"^ + -var cst="GbzJfsfrVkqL/TLfZY8TXA=="^ + -var sid="ce323e74-56fc-4bd6-aa18-83b6dc262748"^ + -var tid="da3847b4-8e69-40bd-a2c2-41da6982c5e2"^ + -var rgn="My Resource Group"^ + -var sa="mystorageaccount"^ + c:\packer\ubuntu_14_LTS.json +``` + +Please see the +[config_sameples/arm](https://github.com/Azure/packer-azure/tree/master/config_examples) +directory for more examples of usage. diff --git a/builder/azure/arm/artifact.go b/builder/azure/arm/artifact.go new file mode 100644 index 000000000..e31310f76 --- /dev/null +++ b/builder/azure/arm/artifact.go @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +const ( + BuilderId = "Azure.ResourceManagement.VMImage" +) + +type artifact struct { + name string +} + +func (*artifact) BuilderId() string { + return BuilderId +} + +func (*artifact) Files() []string { + return []string{} +} + +func (*artifact) Id() string { + return "" +} + +func (*artifact) State(name string) interface{} { + return nil +} + +func (*artifact) String() string { + return "{}" +} + +func (*artifact) Destroy() error { + return nil +} diff --git a/builder/azure/arm/azure_client.go b/builder/azure/arm/azure_client.go new file mode 100644 index 000000000..38597a1e4 --- /dev/null +++ b/builder/azure/arm/azure_client.go @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/Azure/azure-sdk-for-go/arm/network" + "github.com/Azure/azure-sdk-for-go/arm/resources/resources" + armStorage "github.com/Azure/azure-sdk-for-go/arm/storage" + "github.com/Azure/azure-sdk-for-go/storage" + + "github.com/Azure/go-autorest/autorest/azure" +) + +type AzureClient struct { + compute.VirtualMachinesClient + network.PublicIPAddressesClient + resources.GroupsClient + resources.DeploymentsClient + storage.BlobStorageClient +} + +func NewAzureClient(subscriptionID string, resourceGroupName string, storageAccountName string, servicePrincipalToken *azure.ServicePrincipalToken) (*AzureClient, error) { + var azureClient = &AzureClient{} + + azureClient.DeploymentsClient = resources.NewDeploymentsClient(subscriptionID) + azureClient.DeploymentsClient.Authorizer = servicePrincipalToken + + azureClient.GroupsClient = resources.NewGroupsClient(subscriptionID) + azureClient.GroupsClient.Authorizer = servicePrincipalToken + + azureClient.PublicIPAddressesClient = network.NewPublicIPAddressesClient(subscriptionID) + azureClient.PublicIPAddressesClient.Authorizer = servicePrincipalToken + + azureClient.VirtualMachinesClient = compute.NewVirtualMachinesClient(subscriptionID) + azureClient.VirtualMachinesClient.Authorizer = servicePrincipalToken + + storageAccountsClient := armStorage.NewAccountsClient(subscriptionID) + storageAccountsClient.Authorizer = servicePrincipalToken + + accountKeys, err := storageAccountsClient.ListKeys(resourceGroupName, storageAccountName) + if err != nil { + return nil, err + } + + storageClient, err := storage.NewBasicClient(storageAccountName, *accountKeys.Key1) + if err != nil { + return nil, err + } + + azureClient.BlobStorageClient = storageClient.GetBlobService() + return azureClient, nil +} diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go new file mode 100644 index 000000000..a988563f2 --- /dev/null +++ b/builder/azure/arm/builder.go @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "errors" + "fmt" + "log" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/packer/builder/azure/common/lin" + + "github.com/Azure/go-autorest/autorest/azure" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/communicator" + "github.com/mitchellh/packer/packer" +) + +type Builder struct { + config *Config + stateBag multistep.StateBag + runner multistep.Runner +} + +const ( + DefaultPublicIPAddressName = "packerPublicIP" +) + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + c, warnings, errs := newConfig(raws...) + if errs != nil { + return warnings, errs + } + + b.config = c + + b.stateBag = new(multistep.BasicStateBag) + err := b.configureStateBag(b.stateBag) + if err != nil { + return nil, err + } + + return warnings, errs +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + ui.Say("Preparing builder ...") + + b.stateBag.Put("hook", hook) + b.stateBag.Put(constants.Ui, ui) + + servicePrincipalToken, err := b.createServicePrincipalToken() + if err != nil { + return nil, err + } + + ui.Message("Creating Azure Resource Manager (ARM) client ...") + azureClient, err := NewAzureClient(b.config.SubscriptionID, b.config.ResourceGroupName, b.config.StorageAccount, servicePrincipalToken) + if err != nil { + return nil, err + } + + steps := []multistep.Step{ + NewStepCreateResourceGroup(azureClient, ui), + NewStepValidateTemplate(azureClient, ui), + NewStepDeployTemplate(azureClient, ui), + NewStepGetIPAddress(azureClient, ui), + &communicator.StepConnectSSH{ + Config: &b.config.Comm, + Host: lin.SSHHost, + SSHConfig: lin.SSHConfig(b.config.UserName), + }, + &common.StepProvision{}, + NewStepGetOSDisk(azureClient, ui), + NewStepPowerOffCompute(azureClient, ui), + NewStepCaptureImage(azureClient, ui), + NewStepDeleteResourceGroup(azureClient, ui), + NewStepDeleteOSDisk(azureClient, ui), + } + + if b.config.PackerDebug { + ui.Message(fmt.Sprintf("temp admin user: '%s'", b.config.UserName)) + ui.Message(fmt.Sprintf("temp admin password: '%s'", b.config.Password)) + } + + b.runner = b.createRunner(&steps, ui) + b.runner.Run(b.stateBag) + + // Report any errors. + if rawErr, ok := b.stateBag.GetOk(constants.Error); ok { + return nil, rawErr.(error) + } + + // If we were interrupted or cancelled, then just exit. + if _, ok := b.stateBag.GetOk(multistep.StateCancelled); ok { + return nil, errors.New("Build was cancelled.") + } + + if _, ok := b.stateBag.GetOk(multistep.StateHalted); ok { + return nil, errors.New("Build was halted.") + } + + return &artifact{}, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} + +func (b *Builder) createRunner(steps *[]multistep.Step, ui packer.Ui) multistep.Runner { + if b.config.PackerDebug { + return &multistep.DebugRunner{ + Steps: *steps, + PauseFn: common.MultistepDebugFn(ui), + } + } + + return &multistep.BasicRunner{ + Steps: *steps, + } +} + +func (b *Builder) configureStateBag(stateBag multistep.StateBag) error { + stateBag.Put(constants.AuthorizedKey, b.config.sshAuthorizedKey) + stateBag.Put(constants.PrivateKey, b.config.sshPrivateKey) + + stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName) + stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName) + stateBag.Put(constants.ArmLocation, b.config.Location) + stateBag.Put(constants.ArmResourceGroupName, b.config.tmpResourceGroupName) + stateBag.Put(constants.ArmTemplateParameters, b.config.toTemplateParameters()) + stateBag.Put(constants.ArmVirtualMachineCaptureParameters, b.config.toVirtualMachineCaptureParameters()) + + stateBag.Put(constants.ArmPublicIPAddressName, DefaultPublicIPAddressName) + + return nil +} + +func (b *Builder) createServicePrincipalToken() (*azure.ServicePrincipalToken, error) { + oauthConfig, err := azure.PublicCloud.OAuthConfigForTenant(b.config.TenantID) + if err != nil { + return nil, err + } + + spt, err := azure.NewServicePrincipalToken( + *oauthConfig, + b.config.ClientID, + b.config.ClientSecret, + azure.PublicCloud.ResourceManagerEndpoint) + + return spt, err +} diff --git a/builder/azure/arm/builder_test.go b/builder/azure/arm/builder_test.go new file mode 100644 index 000000000..3d25962c1 --- /dev/null +++ b/builder/azure/arm/builder_test.go @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "github.com/mitchellh/packer/builder/azure/common/constants" + "testing" +) + +func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) { + var testSubject = &Builder{} + testSubject.Prepare(getArmBuilderConfiguration(), getPackerConfiguration()) + + var expectedStateBagKeys = []string{ + constants.AuthorizedKey, + constants.PrivateKey, + + constants.ArmComputeName, + constants.ArmDeploymentName, + constants.ArmLocation, + constants.ArmResourceGroupName, + constants.ArmTemplateParameters, + constants.ArmVirtualMachineCaptureParameters, + constants.ArmPublicIPAddressName, + } + + for _, v := range expectedStateBagKeys { + if _, ok := testSubject.stateBag.GetOk(v); ok == false { + t.Errorf("Expected the builder's state bag to contain '%s', but it did not.", v) + } + } +} diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go new file mode 100644 index 000000000..e8271966b --- /dev/null +++ b/builder/azure/arm/config.go @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "time" + + "golang.org/x/crypto/ssh" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/Azure/go-autorest/autorest/to" + + "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" +) + +const ( + DefaultUserName = "packer" + DefaultVMSize = "Standard_A1" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + // Authentication via OAUTH + ClientID string `mapstructure:"client_id"` + ClientSecret string `mapstructure:"client_secret"` + 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"` + Location string `mapstructure:"location"` + VMSize string `mapstructure:"vm_size"` + + // Deployment + ResourceGroupName string `mapstructure:"resource_group_name"` + StorageAccount string `mapstructure:"storage_account"` + + // Runtime Values + UserName string + Password string + tmpAdminPassword string + tmpResourceGroupName string + tmpComputeName string + tmpDeploymentName string + tmpOSDiskName string + + // Authentication with the VM via SSH + sshAuthorizedKey string + sshPrivateKey string + + Comm communicator.Config `mapstructure:",squash"` + ctx *interpolate.Context +} + +// If we ever feel the need to support more templates consider moving this +// method to its own factory class. +func (c *Config) toTemplateParameters() *TemplateParameters { + return &TemplateParameters{ + AdminUsername: &TemplateParameter{c.UserName}, + AdminPassword: &TemplateParameter{c.Password}, + DnsNameForPublicIP: &TemplateParameter{c.tmpComputeName}, + ImageOffer: &TemplateParameter{c.ImageOffer}, + ImagePublisher: &TemplateParameter{c.ImagePublisher}, + ImageSku: &TemplateParameter{c.ImageSku}, + OSDiskName: &TemplateParameter{c.tmpOSDiskName}, + SshAuthorizedKey: &TemplateParameter{c.sshAuthorizedKey}, + StorageAccountName: &TemplateParameter{c.StorageAccount}, + VMSize: &TemplateParameter{c.VMSize}, + VMName: &TemplateParameter{c.tmpComputeName}, + } +} + +func (c *Config) toVirtualMachineCaptureParameters() *compute.VirtualMachineCaptureParameters { + return &compute.VirtualMachineCaptureParameters{ + DestinationContainerName: &c.CaptureContainerName, + VhdPrefix: &c.CaptureNamePrefix, + OverwriteVhds: to.BoolPtr(false), + } +} + +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) + + err = setSshValues(&c) + if err != nil { + return nil, nil, err + } + + var errs *packer.MultiError + errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...) + + assertRequiredParametersSet(&c, errs) + 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 +} + +func setRuntimeValues(c *Config) { + var tempName = NewTempName() + + c.tmpAdminPassword = tempName.AdminPassword + c.tmpComputeName = tempName.ComputeName + c.tmpDeploymentName = tempName.DeploymentName + // c.tmpResourceGroupName = c.ResourceGroupName + c.tmpResourceGroupName = tempName.ResourceGroupName + c.tmpOSDiskName = tempName.OSDiskName +} + +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 + } +} + +func provideDefaultValues(c *Config) { + if c.VMSize == "" { + c.VMSize = DefaultVMSize + } +} + +func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { + ///////////////////////////////////////////// + // Authentication via OAUTH + + 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")) + } + + if c.TenantID == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A tenant_id must be specified")) + } + + if c.SubscriptionID == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified")) + } + + ///////////////////////////////////////////// + // Capture + if c.CaptureContainerName == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("An capture_container_name must be specified")) + } + + if c.CaptureNamePrefix == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("An capture_name_prefix must be specified")) + } + + ///////////////////////////////////////////// + // Compute + + if c.ImagePublisher == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A image_publisher must be specified")) + } + + if c.ImageOffer == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A image_offer must be specified")) + } + + if c.ImageSku == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A image_sku must be specified")) + } + + 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")) + } +} diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go new file mode 100644 index 000000000..c22429464 --- /dev/null +++ b/builder/azure/arm/config_test.go @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + "time" +) + +// List of configuration parameters that are required by the ARM builder. +var requiredConfigValues = []string{ + "capture_name_prefix", + "capture_container_name", + "client_id", + "client_secret", + "image_offer", + "image_publisher", + "image_sku", + "location", + "storage_account", + "subscription_id", + "tenant_id", +} + +func TestConfigShouldProvideReasonableDefaultValues(t *testing.T) { + c, _, err := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + + if err != nil { + t.Errorf("Expected configuration creation to succeed, but it failed!\n") + t.Fatalf(" errors: %s\n", err) + } + + if c.UserName == "" { + t.Errorf("Expected 'UserName' to be populated, but it was empty!") + } + + if c.VMSize == "" { + t.Errorf("Expected 'VMSize' to be populated, but it was empty!") + } +} + +func TestConfigShouldBeAbleToOverrideDefaultedValues(t *testing.T) { + builderValues := make(map[string]string) + + // Populate the dictionary with all of the required values. + for _, v := range requiredConfigValues { + builderValues[v] = "--some-value--" + } + + builderValues["ssh_password"] = "override_password" + builderValues["ssh_username"] = "override_username" + builderValues["vm_size"] = "override_vm_size" + + c, _, _ := newConfig(getArmBuilderConfigurationFromMap(builderValues), getPackerConfiguration()) + + if c.Password != "override_password" { + t.Errorf("Expected 'Password' to be set to 'override_password', but found '%s'!", c.Password) + } + + if c.Comm.SSHPassword != "override_password" { + t.Errorf("Expected 'c.Comm.SSHPassword' to be set to 'override_password', but found '%s'!", c.Comm.SSHPassword) + } + + if c.UserName != "override_username" { + t.Errorf("Expected 'UserName' to be set to 'override_username', but found '%s'!", c.UserName) + } + + if c.Comm.SSHUsername != "override_username" { + t.Errorf("Expected 'c.Comm.SSHUsername' to be set to 'override_username', but found '%s'!", c.Comm.SSHUsername) + } + + if c.VMSize != "override_vm_size" { + t.Errorf("Expected 'vm_size' to be set to 'override_username', but found '%s'!", c.VMSize) + } +} + +func TestConfigShouldDefaultVMSizeToStandardA1(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + + if c.VMSize != "Standard_A1" { + t.Errorf("Expected 'VMSize' to default to 'Standard_A1', but got '%s'.", c.VMSize) + } +} + +func TestUserShouldProvideRequiredValues(t *testing.T) { + builderValues := make(map[string]string) + + // Populate the dictionary with all of the required values. + for _, v := range requiredConfigValues { + builderValues[v] = "--some-value--" + } + + // Ensure we can successfully create a config. + _, _, err := newConfig(getArmBuilderConfigurationFromMap(builderValues), getPackerConfiguration()) + if err != nil { + t.Errorf("Expected configuration creation to succeed, but it failed!\n") + t.Fatalf(" -> %+v\n", builderValues) + } + + // Take away a required element, and ensure construction fails. + for _, v := range requiredConfigValues { + delete(builderValues, v) + + _, _, err := newConfig(getArmBuilderConfigurationFromMap(builderValues), getPackerConfiguration()) + if err == nil { + t.Errorf("Expected configuration creation to fail, but it succeeded!\n") + t.Fatalf(" -> %+v\n", builderValues) + } + + builderValues[v] = "--some-value--" + } +} + +func TestSystemShouldDefineRuntimeValues(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + + if c.Password == "" { + t.Errorf("Expected Password to not be empty, but it was '%s'!", c.Password) + } + + if c.tmpComputeName == "" { + t.Errorf("Expected tmpComputeName to not be empty, but it was '%s'!", c.tmpComputeName) + } + + if c.tmpDeploymentName == "" { + t.Errorf("Expected tmpDeploymentName to not be empty, but it was '%s'!", c.tmpDeploymentName) + } + + if c.tmpResourceGroupName == "" { + t.Errorf("Expected tmpResourceGroupName to not be empty, but it was '%s'!", c.tmpResourceGroupName) + } + + if c.tmpOSDiskName == "" { + t.Errorf("Expected tmpOSDiskName to not be empty, but it was '%s'!", c.tmpOSDiskName) + } +} + +func TestConfigShouldTransformToTemplateParameters(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + templateParameters := c.toTemplateParameters() + + if templateParameters.AdminUsername.Value != c.UserName { + t.Errorf("Expected AdminUsername to be equal to config's AdminUsername, but they were '%s' and '%s' respectively.", templateParameters.AdminUsername.Value, c.UserName) + } + + if templateParameters.DnsNameForPublicIP.Value != c.tmpComputeName { + t.Errorf("Expected DnsNameForPublicIP to be equal to config's DnsNameForPublicIP, but they were '%s' and '%s' respectively.", templateParameters.DnsNameForPublicIP.Value, c.tmpComputeName) + } + + if templateParameters.ImageOffer.Value != c.ImageOffer { + t.Errorf("Expected ImageOffer to be equal to config's ImageOffer, but they were '%s' and '%s' respectively.", templateParameters.ImageOffer.Value, c.ImageOffer) + } + + if templateParameters.ImagePublisher.Value != c.ImagePublisher { + t.Errorf("Expected ImagePublisher to be equal to config's ImagePublisher, but they were '%s' and '%s' respectively.", templateParameters.ImagePublisher.Value, c.ImagePublisher) + } + + if templateParameters.ImageSku.Value != c.ImageSku { + t.Errorf("Expected ImageSku to be equal to config's ImageSku, but they were '%s' and '%s' respectively.", templateParameters.ImageSku.Value, c.ImageSku) + } + + if templateParameters.OSDiskName.Value != c.tmpOSDiskName { + t.Errorf("Expected OSDiskName to be equal to config's OSDiskName, but they were '%s' and '%s' respectively.", templateParameters.OSDiskName.Value, c.tmpOSDiskName) + } + + if templateParameters.StorageAccountName.Value != c.StorageAccount { + t.Errorf("Expected StorageAccountName to be equal to config's StorageAccountName, but they were '%s' and '%s' respectively.", templateParameters.StorageAccountName.Value, c.StorageAccount) + } + + if templateParameters.VMName.Value != c.tmpComputeName { + t.Errorf("Expected VMName to be equal to config's VMName, but they were '%s' and '%s' respectively.", templateParameters.VMName.Value, c.tmpComputeName) + } + + if templateParameters.VMSize.Value != c.VMSize { + t.Errorf("Expected VMSize to be equal to config's VMSize, but they were '%s' and '%s' respectively.", templateParameters.VMSize.Value, c.VMSize) + } +} + +func TestConfigShouldTransformToVirtualMachineCaptureParameters(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + parameters := c.toVirtualMachineCaptureParameters() + + if *parameters.DestinationContainerName != c.CaptureContainerName { + t.Errorf("Expected DestinationContainerName to be equal to config's CaptureContainerName, but they were '%s' and '%s' respectively.", *parameters.DestinationContainerName, c.CaptureContainerName) + } + + if *parameters.VhdPrefix != c.CaptureNamePrefix { + t.Errorf("Expected DestinationContainerName to be equal to config's CaptureContainerName, but they were '%s' and '%s' respectively.", *parameters.VhdPrefix, c.CaptureNamePrefix) + } + + if *parameters.OverwriteVhds != false { + t.Error("Expected OverwriteVhds to be false, but it was not.") + } +} + +func TestConfigShouldSupportPackersConfigElements(t *testing.T) { + c, _, err := newConfig( + getArmBuilderConfiguration(), + getPackerConfiguration(), + getPackerCommunicatorConfiguration()) + + if err != nil { + t.Fatal(err) + } + + if c.Comm.SSHTimeout != 1*time.Hour { + t.Errorf("Expected Comm.SSHTimeout to be a duration of an hour, but got '%s' instead.", c.Comm.SSHTimeout) + } + + if c.Comm.WinRMTimeout != 2*time.Hour { + t.Errorf("Expected Comm.WinRMTimeout to be a durationof two hours, but got '%s' instead.", c.Comm.WinRMTimeout) + } +} + +func getArmBuilderConfiguration() interface{} { + m := make(map[string]string) + for _, v := range requiredConfigValues { + m[v] = fmt.Sprintf("%s00", v) + } + + return getArmBuilderConfigurationFromMap(m) +} + +func getArmBuilderConfigurationFromMap(kvp map[string]string) interface{} { + bs := bytes.NewBufferString("{") + + for k, v := range kvp { + bs.WriteString(fmt.Sprintf("\"%s\": \"%s\",\n", k, v)) + } + + // remove the trailing ",\n" because JSON + bs.Truncate(bs.Len() - 2) + bs.WriteString("}") + + var config interface{} + json.Unmarshal([]byte(bs.String()), &config) + + return config +} + +func getPackerConfiguration() interface{} { + var doc = `{ + "packer_user_variables": { + "sa": "my_storage_account" + }, + "packer_build_name": "azure-arm-vm", + "packer_builder_type": "azure-arm-vm", + "packer_debug": "false", + "packer_force": "false", + "packer_template_path": "/home/jenkins/azure-arm-vm/template.json" + }` + + var config interface{} + json.Unmarshal([]byte(doc), &config) + + return config +} + +func getPackerCommunicatorConfiguration() interface{} { + var doc = `{ + "ssh_timeout": "1h", + "winrm_timeout": "2h" + }` + + var config interface{} + json.Unmarshal([]byte(doc), &config) + + return config +} diff --git a/builder/azure/arm/deployment_factory.go b/builder/azure/arm/deployment_factory.go new file mode 100644 index 000000000..f563d8f4f --- /dev/null +++ b/builder/azure/arm/deployment_factory.go @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "encoding/json" + + "github.com/Azure/azure-sdk-for-go/arm/resources/resources" +) + +type DeploymentFactory struct { + template string +} + +func newDeploymentFactory(template string) DeploymentFactory { + return DeploymentFactory{ + template: template, + } +} + +func (f *DeploymentFactory) create(templateParameters TemplateParameters) (*resources.Deployment, error) { + template, err := f.getTemplate(templateParameters) + if err != nil { + return nil, err + } + + parameters, err := f.getTemplateParameters(templateParameters) + if err != nil { + return nil, err + } + + return &resources.Deployment{ + Properties: &resources.DeploymentProperties{ + Mode: resources.Incremental, + Template: template, + Parameters: parameters, + }, + }, nil +} + +func (f *DeploymentFactory) getTemplate(templateParameters TemplateParameters) (*map[string]interface{}, error) { + var t map[string]interface{} + err := json.Unmarshal([]byte(f.template), &t) + + if err != nil { + return nil, err + } + + return &t, nil +} + +func (f *DeploymentFactory) getTemplateParameters(templateParameters TemplateParameters) (*map[string]interface{}, error) { + b, err := json.Marshal(templateParameters) + if err != nil { + return nil, err + } + + var t map[string]interface{} + err = json.Unmarshal(b, &t) + if err != nil { + return nil, err + } + + return &t, nil +} diff --git a/builder/azure/arm/deployment_factory_test.go b/builder/azure/arm/deployment_factory_test.go new file mode 100644 index 000000000..06f68ee9e --- /dev/null +++ b/builder/azure/arm/deployment_factory_test.go @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "testing" +) + +func TestDeploymentFactoryShouldBeIncremental(t *testing.T) { + var testSubject = newDeploymentFactory(Linux) + + deployment, err := testSubject.create(getTemplateParameters()) + if err != nil { + t.Fatalf("ERROR: %s\n", err) + } + + if deployment.Properties.Mode != "Incremental" { + t.Fatalf("Expected the mode to be 'Incremental', but got '%s'.", deployment.Properties.Mode) + } +} + +// Either {Template,Parameter} are set or {Template,Parameter}Link values are +// set, but never both. +func TestDeploymentFactoryShouldNotSetLinks(t *testing.T) { + testSubject := newDeploymentFactory(Linux) + + deployment, err := testSubject.create(getTemplateParameters()) + if err != nil { + t.Fatalf("ERROR: %s\n", err) + } + + if deployment.Properties.ParametersLink != nil { + t.Fatalf("Expected the ParametersLink to be nil!") + } + + if deployment.Properties.TemplateLink != nil { + t.Fatalf("Expected the TemplateLink to be nil!") + } + + if deployment.Properties.Parameters == nil { + t.Fatalf("Expected the Parameters to not be nil!") + } + + if deployment.Properties.Template == nil { + t.Fatalf("Expected the Template to not be nil!") + } +} + +func TestFactoryShouldCreateDeploymentInstance(t *testing.T) { + testSubject := newDeploymentFactory(Linux) + + deployment, err := testSubject.create(getTemplateParameters()) + if err != nil { + t.Fatalf("ERROR: %s\n", err) + } + + // spot check well known values to ensure correct serialization. + + parametersMap := *deployment.Properties.Parameters + if _, ok := parametersMap["adminUsername"]; ok == false { + t.Fatalf("Expected the parameter value 'adminUsername' to be set, but it was not") + } + + templateMap := *deployment.Properties.Template + if _, ok := templateMap["contentVersion"]; ok == false { + t.Fatalf("Expected the parameter value 'contentVersion' to be set, but it was not") + } +} + +func TestMalformedTemplatesShouldReturnError(t *testing.T) { + testSubject := newDeploymentFactory("") + + _, err := testSubject.create(getTemplateParameters()) + if err == nil { + t.Fatalf("Expected an error, but did not receive one!\n") + } +} + +func getTemplateParameters() TemplateParameters { + templateParameters := TemplateParameters{ + AdminUsername: &TemplateParameter{"adminusername00"}, + DnsNameForPublicIP: &TemplateParameter{"dnsnameforpublicip00"}, + OSDiskName: &TemplateParameter{"osdiskname00"}, + SshAuthorizedKey: &TemplateParameter{"sshkeydata00"}, + StorageAccountName: &TemplateParameter{"storageaccountname00"}, + VMName: &TemplateParameter{"vmname00"}, + VMSize: &TemplateParameter{"vmsize00"}, + } + + return templateParameters +} diff --git a/builder/azure/arm/deployment_poller.go b/builder/azure/arm/deployment_poller.go new file mode 100644 index 000000000..1ce76aa2b --- /dev/null +++ b/builder/azure/arm/deployment_poller.go @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "time" +) + +const ( + DeployCanceled = "Canceled" + DeployFailed = "Failed" + DeployDeleted = "Deleted" + DeploySucceeded = "Succeeded" +) + +type DeploymentPoller struct { + getProvisioningState func() (string, error) + pause func() +} + +func NewDeploymentPoller(getProvisioningState func() (string, error)) *DeploymentPoller { + pollDuration := time.Second * 15 + + return &DeploymentPoller{ + getProvisioningState: getProvisioningState, + pause: func() { time.Sleep(pollDuration) }, + } +} + +func (t *DeploymentPoller) PollAsNeeded() (string, error) { + for { + res, err := t.getProvisioningState() + + if err != nil { + return res, err + } + + switch res { + case DeployCanceled, DeployDeleted, DeployFailed, DeploySucceeded: + return res, nil + default: + break + } + + t.pause() + } +} diff --git a/builder/azure/arm/deployment_poller_test.go b/builder/azure/arm/deployment_poller_test.go new file mode 100644 index 000000000..83c90bc49 --- /dev/null +++ b/builder/azure/arm/deployment_poller_test.go @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" +) + +func TestCanceledShouldImmediatelyStopPolling(t *testing.T) { + var testSubject = NewDeploymentPoller(func() (string, error) { return "Canceled", nil }) + testSubject.pause = func() { t.Fatal("Did not expect this to be called!") } + + res, err := testSubject.PollAsNeeded() + if err != nil { + t.Errorf("Expected PollAsNeeded to not return an error, but got '%s'.", err) + } + + if res != "Canceled" { + t.Fatalf("Expected PollAsNeeded to return a result of 'Canceled', but got '%s' instead.", res) + } +} + +func TestFailedShouldImmediatelyStopPolling(t *testing.T) { + var testSubject = NewDeploymentPoller(func() (string, error) { return "Failed", nil }) + testSubject.pause = func() { t.Fatal("Did not expect this to be called!") } + + res, err := testSubject.PollAsNeeded() + if err != nil { + t.Fatalf("Expected PollAsNeeded to not return an error, but got '%s'.", err) + } + + if res != "Failed" { + t.Fatalf("Expected PollAsNeeded to return a result of 'Failed', but got '%s' instead.", res) + } +} + +func TestDeletedShouldImmediatelyStopPolling(t *testing.T) { + var testSubject = NewDeploymentPoller(func() (string, error) { return "Deleted", nil }) + testSubject.pause = func() { t.Fatal("Did not expect this to be called!") } + + res, err := testSubject.PollAsNeeded() + if err != nil { + t.Fatalf("Expected PollAsNeeded to not return an error, but got '%s'.", err) + } + + if res != "Deleted" { + t.Fatalf("Expected PollAsNeeded to return a result of 'Deleted', but got '%s' instead.", res) + } +} + +func TestSucceededShouldImmediatelyStopPolling(t *testing.T) { + var testSubject = NewDeploymentPoller(func() (string, error) { return "Succeeded", nil }) + testSubject.pause = func() { t.Fatal("Did not expect this to be called!") } + + res, err := testSubject.PollAsNeeded() + if err != nil { + t.Fatalf("Expected PollAsNeeded to not return an error, but got '%s'.", err) + } + + if res != "Succeeded" { + t.Fatalf("Expected PollAsNeeded to return a result of 'Succeeded', but got '%s' instead.", res) + } +} + +func TestPollerShouldPollOnNonStoppingStatus(t *testing.T) { + count := 0 + + var testSubject = NewDeploymentPoller(func() (string, error) { return "Succeeded", nil }) + testSubject.pause = func() { count += 1 } + testSubject.getProvisioningState = func() (string, error) { + count += 1 + switch count { + case 0, 1: + return "Working", nil + default: + return "Succeeded", nil + } + } + + res, err := testSubject.PollAsNeeded() + if err != nil { + t.Fatalf("Expected PollAsNeeded to not return an error, but got '%s'.", err) + } + + if res != "Succeeded" { + t.Fatalf("Expected PollAsNeeded to return a result of 'Succeeded', but got '%s' instead.", res) + } + + if count != 3 { + t.Fatal("Expected DeploymentPoller to poll until 'Succeeded', but it did not.") + } +} + +func TestPollerShouldReturnErrorImmediately(t *testing.T) { + var testSubject = NewDeploymentPoller(func() (string, error) { return "bad-bad-bad", fmt.Errorf("BOOM") }) + testSubject.pause = func() { t.Fatal("Did not expect this to be called!") } + + res, err := testSubject.PollAsNeeded() + if err == nil { + t.Fatal("Expected PollAsNeeded to return an error, but it did not.") + } + + if res != "bad-bad-bad" { + t.Fatalf("Expected PollAsNeeded to return a result of 'bad-bad-bad', but got '%s' instead.", res) + } +} diff --git a/builder/azure/arm/openssh_key_pair.go b/builder/azure/arm/openssh_key_pair.go new file mode 100644 index 000000000..a775525ae --- /dev/null +++ b/builder/azure/arm/openssh_key_pair.go @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "golang.org/x/crypto/ssh" + "time" +) + +const ( + KeySize = 2048 +) + +type OpenSshKeyPair struct { + privateKey *rsa.PrivateKey + publicKey ssh.PublicKey +} + +func NewOpenSshKeyPair() (*OpenSshKeyPair, error) { + return NewOpenSshKeyPairWithSize(KeySize) +} + +func NewOpenSshKeyPairWithSize(keySize int) (*OpenSshKeyPair, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, keySize) + if err != nil { + return nil, err + } + + publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return nil, err + } + + return &OpenSshKeyPair{ + privateKey: privateKey, + publicKey: publicKey, + }, nil +} + +func (s *OpenSshKeyPair) AuthorizedKey() string { + return fmt.Sprintf("%s %s packer Azure Deployment%s", + s.publicKey.Type(), + base64.StdEncoding.EncodeToString(s.publicKey.Marshal()), + time.Now().Format(time.RFC3339)) +} + +func (s *OpenSshKeyPair) PrivateKey() string { + privateKey := string(pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(s.privateKey), + })) + + return privateKey +} diff --git a/builder/azure/arm/openssh_key_pair_test.go b/builder/azure/arm/openssh_key_pair_test.go new file mode 100644 index 000000000..41bbebbd3 --- /dev/null +++ b/builder/azure/arm/openssh_key_pair_test.go @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "golang.org/x/crypto/ssh" + "testing" +) + +func TestAuthorizedKeyShouldParse(t *testing.T) { + testSubject, err := NewOpenSshKeyPairWithSize(512) + if err != nil { + t.Fatalf("Failed to create a new OpenSSH key pair, err=%s.", err) + } + + authorizedKey := testSubject.AuthorizedKey() + + _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(authorizedKey)) + if err != nil { + t.Fatalf("Failed to parse the authorized key, err=%s", err) + } +} + +func TestPrivateKeyShouldParse(t *testing.T) { + testSubject, err := NewOpenSshKeyPairWithSize(512) + if err != nil { + t.Fatalf("Failed to create a new OpenSSH key pair, err=%s.", err) + } + + _, err = ssh.ParsePrivateKey([]byte(testSubject.PrivateKey())) + if err != nil { + t.Fatalf("Failed to parse the private key, err=%s\n", err) + } +} diff --git a/builder/azure/arm/step_capture_image.go b/builder/azure/arm/step_capture_image.go new file mode 100644 index 000000000..3e01b128b --- /dev/null +++ b/builder/azure/arm/step_capture_image.go @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepCaptureImage struct { + client *AzureClient + capture func(resourceGroupName string, computeName string, parameters *compute.VirtualMachineCaptureParameters) error + say func(message string) + error func(e error) +} + +func NewStepCaptureImage(client *AzureClient, ui packer.Ui) *StepCaptureImage { + var step = &StepCaptureImage{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.capture = step.captureImage + return step +} + +func (s *StepCaptureImage) captureImage(resourceGroupName string, computeName string, parameters *compute.VirtualMachineCaptureParameters) error { + generalizeResponse, err := s.client.Generalize(resourceGroupName, computeName) + if err != nil { + return err + } + + s.client.VirtualMachinesClient.PollAsNeeded(generalizeResponse.Response) + + captureResponse, err := s.client.Capture(resourceGroupName, computeName, *parameters) + if err != nil { + return err + } + + s.client.VirtualMachinesClient.PollAsNeeded(captureResponse.Response.Response) + return nil +} + +func (s *StepCaptureImage) Run(state multistep.StateBag) multistep.StepAction { + s.say("Capturing image ...") + + var computeName = state.Get(constants.ArmComputeName).(string) + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var parameters = state.Get(constants.ArmVirtualMachineCaptureParameters).(*compute.VirtualMachineCaptureParameters) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName)) + + err := s.capture(resourceGroupName, computeName, parameters) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepCaptureImage) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_capture_image_test.go b/builder/azure/arm/step_capture_image_test.go new file mode 100644 index 000000000..e3bcf3bd1 --- /dev/null +++ b/builder/azure/arm/step_capture_image_test.go @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepCaptureImageShouldFailIfCaptureFails(t *testing.T) { + + var testSubject = &StepCaptureImage{ + capture: func(string, string, *compute.VirtualMachineCaptureParameters) error { + return fmt.Errorf("!! Unit Test FAIL !!") + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCaptureImage() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepCaptureImageShouldPassIfCapturePasses(t *testing.T) { + var testSubject = &StepCaptureImage{ + capture: func(string, string, *compute.VirtualMachineCaptureParameters) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCaptureImage() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepCaptureImageShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualComputeName string + var actualVirtualMachineCaptureParameters *compute.VirtualMachineCaptureParameters + + var testSubject = &StepCaptureImage{ + capture: func(resourceGroupName string, computeName string, parameters *compute.VirtualMachineCaptureParameters) error { + actualResourceGroupName = resourceGroupName + actualComputeName = computeName + actualVirtualMachineCaptureParameters = parameters + + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCaptureImage() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string) + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + var expectedVirtualMachineCaptureParameters = stateBag.Get(constants.ArmVirtualMachineCaptureParameters).(*compute.VirtualMachineCaptureParameters) + + if actualComputeName != expectedComputeName { + t.Fatalf("Expected StepCaptureImage to source 'constants.ArmComputeName' from the state bag, but it did not.") + } + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected StepCaptureImage to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + if actualVirtualMachineCaptureParameters != expectedVirtualMachineCaptureParameters { + t.Fatalf("Expected StepCaptureImage to source 'constants.ArmVirtualMachineCaptureParameters' from the state bag, but it did not.") + } +} + +func createTestStateBagStepCaptureImage() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + stateBag.Put(constants.ArmVirtualMachineCaptureParameters, &compute.VirtualMachineCaptureParameters{}) + + return stateBag +} diff --git a/builder/azure/arm/step_create_resource_group.go b/builder/azure/arm/step_create_resource_group.go new file mode 100644 index 000000000..f6c3cab4c --- /dev/null +++ b/builder/azure/arm/step_create_resource_group.go @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/arm/resources/resources" + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepCreateResourceGroup struct { + client *AzureClient + create func(resourceGroupName string, location string) error + say func(message string) + error func(e error) +} + +func NewStepCreateResourceGroup(client *AzureClient, ui packer.Ui) *StepCreateResourceGroup { + var step = &StepCreateResourceGroup{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.create = step.createResourceGroup + return step +} + +func (s *StepCreateResourceGroup) createResourceGroup(resourceGroupName string, location string) error { + _, err := s.client.GroupsClient.CreateOrUpdate(resourceGroupName, resources.ResourceGroup{ + Location: &location, + }) + + return err +} + +func (s *StepCreateResourceGroup) Run(state multistep.StateBag) multistep.StepAction { + s.say("Creating resource group ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var location = state.Get(constants.ArmLocation).(string) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> Location : '%s'", location)) + + err := s.create(resourceGroupName, location) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepCreateResourceGroup) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_create_resource_group_test.go b/builder/azure/arm/step_create_resource_group_test.go new file mode 100644 index 000000000..88140fca8 --- /dev/null +++ b/builder/azure/arm/step_create_resource_group_test.go @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) { + var testSubject = &StepCreateResourceGroup{ + create: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCreateResourceGroup() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) { + var testSubject = &StepCreateResourceGroup{ + create: func(string, string) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCreateResourceGroup() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualLocation string + + var testSubject = &StepCreateResourceGroup{ + create: func(resourceGroupName string, location string) error { + actualResourceGroupName = resourceGroupName + actualLocation = location + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCreateResourceGroup() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedLocation = stateBag.Get(constants.ArmLocation).(string) + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + if actualLocation != expectedLocation { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } +} + +func createTestStateBagStepCreateResourceGroup() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmLocation, "Unit Test: Location") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + + return stateBag +} diff --git a/builder/azure/arm/step_delete_os_disk.go b/builder/azure/arm/step_delete_os_disk.go new file mode 100644 index 000000000..3bcde75bf --- /dev/null +++ b/builder/azure/arm/step_delete_os_disk.go @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "net/url" + "strings" + + "github.com/mitchellh/packer/builder/azure/common/constants" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepDeleteOSDisk struct { + client *AzureClient + delete func(string, string) error + say func(message string) + error func(e error) +} + +func NewStepDeleteOSDisk(client *AzureClient, ui packer.Ui) *StepDeleteOSDisk { + var step = &StepDeleteOSDisk{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.delete = step.deleteBlob + return step +} + +func (s *StepDeleteOSDisk) deleteBlob(storageContainerName string, blobName string) error { + return s.client.BlobStorageClient.DeleteBlob(storageContainerName, blobName) +} + +func (s *StepDeleteOSDisk) Run(state multistep.StateBag) multistep.StepAction { + s.say("Deleting the temporary OS disk ...") + + var osDisk = state.Get(constants.ArmOSDiskVhd).(string) + s.say(fmt.Sprintf(" -> OS Disk : '%s'", osDisk)) + + u, err := url.Parse(osDisk) + if err != nil { + s.say("Failed to parse the OS Disk's VHD URI!") + return multistep.ActionHalt + } + + xs := strings.Split(u.Path, "/") + + var storageAccountName = xs[1] + var blobName = strings.Join(xs[2:], "/") + + err = s.delete(storageAccountName, blobName) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + return multistep.ActionContinue +} + +func (*StepDeleteOSDisk) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_delete_os_disk_test.go b/builder/azure/arm/step_delete_os_disk_test.go new file mode 100644 index 000000000..214cfec74 --- /dev/null +++ b/builder/azure/arm/step_delete_os_disk_test.go @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepDeleteOSDiskShouldFailIfGetFails(t *testing.T) { + var testSubject = &StepDeleteOSDisk{ + delete: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/images/pkrvm_os.vhd") + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepDeleteOSDiskShouldPassIfGetPasses(t *testing.T) { + var testSubject = &StepDeleteOSDisk{ + delete: func(string, string) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/images/pkrvm_os.vhd") + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepDeleteOSDiskShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualStorageContainerName string + var actualBlobName string + + var testSubject = &StepDeleteOSDisk{ + delete: func(storageContainerName string, blobName string) error { + actualStorageContainerName = storageContainerName + actualBlobName = blobName + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/images/pkrvm_os.vhd") + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if actualStorageContainerName != "images" { + t.Fatalf("Expected the storage container name to be 'images', but found '%s'.", actualStorageContainerName) + } + + if actualBlobName != "pkrvm_os.vhd" { + t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName) + } +} + +func TestStepDeleteOSDiskShouldHandleComplexStorageContainerNames(t *testing.T) { + var actualStorageContainerName string + var actualBlobName string + + var testSubject = &StepDeleteOSDisk{ + delete: func(storageContainerName string, blobName string) error { + actualStorageContainerName = storageContainerName + actualBlobName = blobName + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteOSDisk("http://storage.blob.core.windows.net/abc/def/pkrvm_os.vhd") + testSubject.Run(stateBag) + + if actualStorageContainerName != "abc" { + t.Fatalf("Expected the storage container name to be 'abc/def', but found '%s'.", actualStorageContainerName) + } + + if actualBlobName != "def/pkrvm_os.vhd" { + t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName) + } +} + +func DeleteTestStateBagStepDeleteOSDisk(osDiskVhd string) multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + stateBag.Put(constants.ArmOSDiskVhd, osDiskVhd) + + return stateBag +} diff --git a/builder/azure/arm/step_delete_resource_group.go b/builder/azure/arm/step_delete_resource_group.go new file mode 100644 index 000000000..d56fee737 --- /dev/null +++ b/builder/azure/arm/step_delete_resource_group.go @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepDeleteResourceGroup struct { + client *AzureClient + delete func(resourceGroupName string) error + say func(message string) + error func(e error) +} + +func NewStepDeleteResourceGroup(client *AzureClient, ui packer.Ui) *StepDeleteResourceGroup { + var step = &StepDeleteResourceGroup{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.delete = step.deleteResourceGroup + return step +} + +func (s *StepDeleteResourceGroup) deleteResourceGroup(resourceGroupName string) error { + res, err := s.client.GroupsClient.Delete(resourceGroupName) + if err != nil { + return err + } + + s.client.GroupsClient.PollAsNeeded(res.Response) + return nil +} + +func (s *StepDeleteResourceGroup) Run(state multistep.StateBag) multistep.StepAction { + s.say("Deleting resource group ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + + err := s.delete(resourceGroupName) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepDeleteResourceGroup) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_delete_resource_group_test.go b/builder/azure/arm/step_delete_resource_group_test.go new file mode 100644 index 000000000..622136ecd --- /dev/null +++ b/builder/azure/arm/step_delete_resource_group_test.go @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepDeleteResourceGroupShouldFailIfDeleteFails(t *testing.T) { + var testSubject = &StepDeleteResourceGroup{ + delete: func(string) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteResourceGroup() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepDeleteResourceGroupShouldPassIfDeletePasses(t *testing.T) { + var testSubject = &StepDeleteResourceGroup{ + delete: func(string) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteResourceGroup() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepDeleteResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + + var testSubject = &StepDeleteResourceGroup{ + delete: func(resourceGroupName string) error { + actualResourceGroupName = resourceGroupName + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := DeleteTestStateBagStepDeleteResourceGroup() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } +} + +func DeleteTestStateBagStepDeleteResourceGroup() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + + return stateBag +} diff --git a/builder/azure/arm/step_deploy_template.go b/builder/azure/arm/step_deploy_template.go new file mode 100644 index 000000000..da866e67b --- /dev/null +++ b/builder/azure/arm/step_deploy_template.go @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepDeployTemplate struct { + client *AzureClient + deploy func(resourceGroupName string, deploymentName string, templateParameters *TemplateParameters) error + say func(message string) + error func(e error) +} + +func NewStepDeployTemplate(client *AzureClient, ui packer.Ui) *StepDeployTemplate { + var step = &StepDeployTemplate{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.deploy = step.deployTemplate + return step +} + +func (s *StepDeployTemplate) deployTemplate(resourceGroupName string, deploymentName string, templateParameters *TemplateParameters) error { + factory := newDeploymentFactory(Linux) + deployment, err := factory.create(*templateParameters) + if err != nil { + return err + } + + res, err := s.client.DeploymentsClient.CreateOrUpdate(resourceGroupName, deploymentName, *deployment) + if err != nil { + return err + } + + s.client.DeploymentsClient.PollAsNeeded(res.Response.Response) + poller := NewDeploymentPoller(func() (string, error) { + r, e := s.client.DeploymentsClient.Get(resourceGroupName, deploymentName) + if r.Properties != nil && r.Properties.ProvisioningState != nil { + return *r.Properties.ProvisioningState, e + } + + return "UNKNOWN", e + }) + + pollStatus, err := poller.PollAsNeeded() + if err != nil { + return err + } + + if pollStatus != DeploySucceeded { + return fmt.Errorf("Deployment failed with a status of '%s'.", pollStatus) + } + + return nil +} + +func (s *StepDeployTemplate) Run(state multistep.StateBag) multistep.StepAction { + s.say("Deploying deployment template ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var deploymentName = state.Get(constants.ArmDeploymentName).(string) + var templateParameters = state.Get(constants.ArmTemplateParameters).(*TemplateParameters) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> DeploymentName : '%s'", deploymentName)) + + err := s.deploy(resourceGroupName, deploymentName, templateParameters) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepDeployTemplate) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_deploy_template_test.go b/builder/azure/arm/step_deploy_template_test.go new file mode 100644 index 000000000..e51a355fa --- /dev/null +++ b/builder/azure/arm/step_deploy_template_test.go @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepDeployTemplateShouldFailIfDeployFails(t *testing.T) { + var testSubject = &StepDeployTemplate{ + deploy: func(string, string, *TemplateParameters) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepDeployTemplate() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepDeployTemplateShouldPassIfDeployPasses(t *testing.T) { + var testSubject = &StepDeployTemplate{ + deploy: func(string, string, *TemplateParameters) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepDeployTemplate() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepDeployTemplateShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualDeploymentName string + var actualTemplateParameters *TemplateParameters + + var testSubject = &StepDeployTemplate{ + deploy: func(resourceGroupName string, deploymentName string, templateParameter *TemplateParameters) error { + actualResourceGroupName = resourceGroupName + actualDeploymentName = deploymentName + actualTemplateParameters = templateParameter + + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepValidateTemplate() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedDeploymentName = stateBag.Get(constants.ArmDeploymentName).(string) + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + var expectedTemplateParameters = stateBag.Get(constants.ArmTemplateParameters).(*TemplateParameters) + + if actualDeploymentName != expectedDeploymentName { + t.Fatalf("Expected StepValidateTemplate to source 'constants.ArmDeploymentName' from the state bag, but it did not.") + } + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + if actualTemplateParameters != expectedTemplateParameters { + t.Fatalf("Expected the step to source 'constants.ArmTemplateParameters' from the state bag, but it did not.") + } +} + +func createTestStateBagStepDeployTemplate() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmDeploymentName, "Unit Test: DeploymentName") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + stateBag.Put(constants.ArmTemplateParameters, &TemplateParameters{}) + + return stateBag +} diff --git a/builder/azure/arm/step_get_ip_address.go b/builder/azure/arm/step_get_ip_address.go new file mode 100644 index 000000000..2dc5d6c1c --- /dev/null +++ b/builder/azure/arm/step_get_ip_address.go @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepGetIPAddress struct { + client *AzureClient + get func(resourceGroupName string, ipAddressName string) (string, error) + say func(message string) + error func(e error) +} + +func NewStepGetIPAddress(client *AzureClient, ui packer.Ui) *StepGetIPAddress { + var step = &StepGetIPAddress{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.get = step.getIPAddress + return step +} + +func (s *StepGetIPAddress) getIPAddress(resourceGroupName string, ipAddressName string) (string, error) { + res, err := s.client.PublicIPAddressesClient.Get(resourceGroupName, ipAddressName, "") + if err != nil { + return "", nil + } + + return *res.Properties.IPAddress, nil +} + +func (s *StepGetIPAddress) Run(state multistep.StateBag) multistep.StepAction { + s.say("Getting the public IP address ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var ipAddressName = state.Get(constants.ArmPublicIPAddressName).(string) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> PublicIPAddressName : '%s'", ipAddressName)) + + address, err := s.get(resourceGroupName, ipAddressName) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + s.say(fmt.Sprintf(" -> SSHHost : '%s'", address)) + state.Put(constants.SSHHost, address) + + return multistep.ActionContinue +} + +func (*StepGetIPAddress) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_get_ip_address_test.go b/builder/azure/arm/step_get_ip_address_test.go new file mode 100644 index 000000000..524c4e5ea --- /dev/null +++ b/builder/azure/arm/step_get_ip_address_test.go @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) { + var testSubject = &StepGetIPAddress{ + get: func(string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetIPAddress() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) { + var testSubject = &StepGetIPAddress{ + get: func(string, string) (string, error) { return "", nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetIPAddress() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualIPAddressName string + + var testSubject = &StepGetIPAddress{ + get: func(resourceGroupName string, ipAddressName string) (string, error) { + actualResourceGroupName = resourceGroupName + actualIPAddressName = ipAddressName + + return "127.0.0.1", nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetIPAddress() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + var expectedIPAddressName = stateBag.Get(constants.ArmPublicIPAddressName).(string) + + if actualIPAddressName != expectedIPAddressName { + t.Fatalf("Expected StepValidateTemplate to source 'constants.ArmIPAddressName' from the state bag, but it did not.") + } + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected StepValidateTemplate to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + expectedIPAddress, ok := stateBag.GetOk(constants.SSHHost) + if !ok { + t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.SSHHost) + } + + if expectedIPAddress != "127.0.0.1" { + t.Fatalf("Expected the value of stateBag[%s] to be '127.0.0.1', but got '%s'.", constants.SSHHost, expectedIPAddress) + } +} + +func createTestStateBagStepGetIPAddress() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmPublicIPAddressName, "Unit Test: PublicIPAddressName") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + + return stateBag +} diff --git a/builder/azure/arm/step_get_os_disk.go b/builder/azure/arm/step_get_os_disk.go new file mode 100644 index 000000000..4d67057db --- /dev/null +++ b/builder/azure/arm/step_get_os_disk.go @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + + "github.com/mitchellh/packer/builder/azure/common/constants" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepGetOSDisk struct { + client *AzureClient + query func(resourceGroupName string, computeName string) (compute.VirtualMachine, error) + say func(message string) + error func(e error) +} + +func NewStepGetOSDisk(client *AzureClient, ui packer.Ui) *StepGetOSDisk { + var step = &StepGetOSDisk{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.query = step.queryCompute + return step +} + +func (s *StepGetOSDisk) queryCompute(resourceGroupName string, computeName string) (compute.VirtualMachine, error) { + return s.client.VirtualMachinesClient.Get(resourceGroupName, computeName, "") +} + +func (s *StepGetOSDisk) Run(state multistep.StateBag) multistep.StepAction { + s.say("Querying the machine's properties ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var computeName = state.Get(constants.ArmComputeName).(string) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName)) + + vm, err := s.query(resourceGroupName, computeName) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + s.say(fmt.Sprintf(" -> OS Disk : '%s'", *vm.Properties.StorageProfile.OsDisk.Vhd.URI)) + state.Put(constants.ArmOSDiskVhd, *vm.Properties.StorageProfile.OsDisk.Vhd.URI) + + return multistep.ActionContinue +} + +func (*StepGetOSDisk) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_get_os_disk_test.go b/builder/azure/arm/step_get_os_disk_test.go new file mode 100644 index 000000000..fee1e8943 --- /dev/null +++ b/builder/azure/arm/step_get_os_disk_test.go @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + + "github.com/mitchellh/packer/builder/azure/common/constants" + + "github.com/mitchellh/multistep" +) + +func TestStepGetOSDiskShouldFailIfGetFails(t *testing.T) { + var testSubject = &StepGetOSDisk{ + query: func(string, string) (compute.VirtualMachine, error) { + return createVirtualMachineFromUri("test.vhd"), fmt.Errorf("!! Unit Test FAIL !!") + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetOSDisk() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepGetOSDiskShouldPassIfGetPasses(t *testing.T) { + var testSubject = &StepGetOSDisk{ + query: func(string, string) (compute.VirtualMachine, error) { + return createVirtualMachineFromUri("test.vhd"), nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetOSDisk() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepGetOSDiskShouldTakeValidateArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualComputeName string + + var testSubject = &StepGetOSDisk{ + query: func(resourceGroupName string, computeName string) (compute.VirtualMachine, error) { + actualResourceGroupName = resourceGroupName + actualComputeName = computeName + + return createVirtualMachineFromUri("test.vhd"), nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetOSDisk() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string) + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + + if actualComputeName != expectedComputeName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + expectedOSDiskVhd, ok := stateBag.GetOk(constants.ArmOSDiskVhd) + if !ok { + t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.ArmOSDiskVhd) + } + + if expectedOSDiskVhd != "test.vhd" { + t.Fatalf("Expected the value of stateBag[%s] to be '127.0.0.1', but got '%s'.", constants.ArmOSDiskVhd, expectedOSDiskVhd) + } +} + +func createTestStateBagStepGetOSDisk() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + + return stateBag +} + +func createVirtualMachineFromUri(vhdUri string) compute.VirtualMachine { + vm := compute.VirtualMachine{ + Properties: &compute.VirtualMachineProperties{ + StorageProfile: &compute.StorageProfile{ + OsDisk: &compute.OSDisk{ + Vhd: &compute.VirtualHardDisk{ + URI: &vhdUri, + }, + }, + }, + }, + } + + return vm +} diff --git a/builder/azure/arm/step_power_off_compute.go b/builder/azure/arm/step_power_off_compute.go new file mode 100644 index 000000000..aea15a61e --- /dev/null +++ b/builder/azure/arm/step_power_off_compute.go @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepPowerOffCompute struct { + client *AzureClient + powerOff func(resourceGroupName string, computeName string) error + say func(message string) + error func(e error) +} + +func NewStepPowerOffCompute(client *AzureClient, ui packer.Ui) *StepPowerOffCompute { + var step = &StepPowerOffCompute{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.powerOff = step.powerOffCompute + return step +} + +func (s *StepPowerOffCompute) powerOffCompute(resourceGroupName string, computeName string) error { + res, err := s.client.PowerOff(resourceGroupName, computeName) + if err != nil { + return err + } + + s.client.VirtualMachinesClient.PollAsNeeded(res.Response) + return nil +} + +func (s *StepPowerOffCompute) Run(state multistep.StateBag) multistep.StepAction { + s.say("Powering off machine ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var computeName = state.Get(constants.ArmComputeName).(string) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName)) + + err := s.powerOff(resourceGroupName, computeName) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepPowerOffCompute) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_power_off_compute_test.go b/builder/azure/arm/step_power_off_compute_test.go new file mode 100644 index 000000000..78a7114bf --- /dev/null +++ b/builder/azure/arm/step_power_off_compute_test.go @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepPowerOffComputeShouldFailIfPowerOffFails(t *testing.T) { + var testSubject = &StepPowerOffCompute{ + powerOff: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepPowerOffCompute() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepPowerOffComputeShouldPassIfPowerOffPasses(t *testing.T) { + var testSubject = &StepPowerOffCompute{ + powerOff: func(string, string) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepPowerOffCompute() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepPowerOffComputeShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualComputeName string + + var testSubject = &StepPowerOffCompute{ + powerOff: func(resourceGroupName string, computeName string) error { + actualResourceGroupName = resourceGroupName + actualComputeName = computeName + + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepPowerOffCompute() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string) + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + + if actualComputeName != expectedComputeName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } +} + +func createTestStateBagStepPowerOffCompute() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + + return stateBag +} diff --git a/builder/azure/arm/step_validate_template.go b/builder/azure/arm/step_validate_template.go new file mode 100644 index 000000000..4f08c79fb --- /dev/null +++ b/builder/azure/arm/step_validate_template.go @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepValidateTemplate struct { + client *AzureClient + validate func(resourceGroupName string, deploymentName string, templateParameters *TemplateParameters) error + say func(message string) + error func(e error) +} + +func NewStepValidateTemplate(client *AzureClient, ui packer.Ui) *StepValidateTemplate { + var step = &StepValidateTemplate{ + client: client, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + step.validate = step.validateTemplate + return step +} + +func (s *StepValidateTemplate) validateTemplate(resourceGroupName string, deploymentName string, templateParameters *TemplateParameters) error { + factory := newDeploymentFactory(Linux) + deployment, err := factory.create(*templateParameters) + + if err != nil { + return err + } + + _, err = s.client.Validate(resourceGroupName, deploymentName, *deployment) + return err +} + +func (s *StepValidateTemplate) Run(state multistep.StateBag) multistep.StepAction { + s.say("Validating deployment template ...") + + var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) + var deploymentName = state.Get(constants.ArmDeploymentName).(string) + var templateParameters = state.Get(constants.ArmTemplateParameters).(*TemplateParameters) + + s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) + s.say(fmt.Sprintf(" -> DeploymentName : '%s'", deploymentName)) + + err := s.validate(resourceGroupName, deploymentName, templateParameters) + if err != nil { + state.Put(constants.Error, err) + s.error(err) + + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepValidateTemplate) Cleanup(multistep.StateBag) { +} diff --git a/builder/azure/arm/step_validate_template_test.go b/builder/azure/arm/step_validate_template_test.go new file mode 100644 index 000000000..8acff7e21 --- /dev/null +++ b/builder/azure/arm/step_validate_template_test.go @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" +) + +func TestStepValidateTemplateShouldFailIfValidateFails(t *testing.T) { + + var testSubject = &StepValidateTemplate{ + validate: func(string, string, *TemplateParameters) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepValidateTemplate() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionHalt { + t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == false { + t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error) + } +} + +func TestStepValidateTemplateShouldPassIfValidatePasses(t *testing.T) { + var testSubject = &StepValidateTemplate{ + validate: func(string, string, *TemplateParameters) error { return nil }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepValidateTemplate() + + var result = testSubject.Run(stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } +} + +func TestStepValidateTemplateShouldTakeStepArgumentsFromStateBag(t *testing.T) { + var actualResourceGroupName string + var actualDeploymentName string + var actualTemplateParameters *TemplateParameters + + var testSubject = &StepValidateTemplate{ + validate: func(resourceGroupName string, deploymentName string, templateParameter *TemplateParameters) error { + actualResourceGroupName = resourceGroupName + actualDeploymentName = deploymentName + actualTemplateParameters = templateParameter + + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepValidateTemplate() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + var expectedDeploymentName = stateBag.Get(constants.ArmDeploymentName).(string) + var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + var expectedTemplateParameters = stateBag.Get(constants.ArmTemplateParameters).(*TemplateParameters) + + if actualDeploymentName != expectedDeploymentName { + t.Fatalf("Expected the step to source 'constants.ArmDeploymentName' from the state bag, but it did not.") + } + + if actualResourceGroupName != expectedResourceGroupName { + t.Fatalf("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") + } + + if actualTemplateParameters != expectedTemplateParameters { + t.Fatalf("Expected the step to source 'constants.ArmTemplateParameters' from the state bag, but it did not.") + } +} + +func createTestStateBagStepValidateTemplate() multistep.StateBag { + stateBag := new(multistep.BasicStateBag) + + stateBag.Put(constants.ArmDeploymentName, "Unit Test: DeploymentName") + stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + stateBag.Put(constants.ArmTemplateParameters, &TemplateParameters{}) + + return stateBag +} diff --git a/builder/azure/arm/template.go b/builder/azure/arm/template.go new file mode 100644 index 000000000..6350d9b82 --- /dev/null +++ b/builder/azure/arm/template.go @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +// See https://github.com/Azure/azure-quickstart-templates for a extensive list of templates. + +const Linux = `{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminUsername": { + "type": "string" + }, + "adminPassword": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "imagePublisher": { + "type": "string" + }, + "imageOffer": { + "type": "string" + }, + "imageSku": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "sshAuthorizedKey": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "vmSize": { + "type": "string" + }, + "vmName": { + "type": "string" + } + }, + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetName": "packerSubnet", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "packerNetwork", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[variables('publicIPAddressName')]", + "location": "[variables('location')]", + "properties": { + "publicIPAllocationMethod": "[variables('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[parameters('dnsNameForPublicIP')]" + } + } + }, + { + "apiVersion": "[variables('apiVersion')]", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetAddressPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "[variables('apiVersion')]", + "type": "Microsoft.Network/networkInterfaces", + "name": "[variables('nicName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + } + }, + { + "apiVersion": "[variables('apiVersion')]", + "type": "Microsoft.Compute/virtualMachines", + "name": "[parameters('vmName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "osProfile": { + "computerName": "[parameters('vmName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]", + "linuxConfiguration": { + "disablePasswordAuthentication": "false", + "ssh": { + "publicKeys": [ + { + "path": "[variables('sshKeyPath')]", + "keyData": "[parameters('sshAuthorizedKey')]" + } + ] + } + } + }, + "storageProfile": { + "imageReference": { + "publisher": "[parameters('imagePublisher')]", + "offer": "[parameters('imageOffer')]", + "sku": "[parameters('imageSku')]", + "version": "latest" + }, + "osDisk": { + "name": "osdisk", + "vhd": { + "uri": "[concat('http://',parameters('storageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": "false" + } + } + } + } + ] +}` diff --git a/builder/azure/arm/template_parameters.go b/builder/azure/arm/template_parameters.go new file mode 100644 index 000000000..1329b15bb --- /dev/null +++ b/builder/azure/arm/template_parameters.go @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +// The intent of these types to facilitate interchange with Azure in the +// appropriate JSON format. A sample format is below. Each parameter listed +// below corresponds to a parameter defined in the template. +// +// { +// "storageAccountName": { +// "value" : "my_storage_account_name" +// }, +// "adminUserName" : { +// "value": "admin" +// } +// } + +type TemplateParameter struct { + Value string `json:"value"` +} + +type TemplateParameters struct { + AdminUsername *TemplateParameter `json:"adminUsername,omitempty"` + AdminPassword *TemplateParameter `json:"adminPassword,omitempty"` + DnsNameForPublicIP *TemplateParameter `json:"dnsNameForPublicIP,omitempty"` + ImageOffer *TemplateParameter `json:"imageOffer,omitempty"` + ImagePublisher *TemplateParameter `json:"imagePublisher,omitempty"` + ImageSku *TemplateParameter `json:"imageSku,omitempty"` + OSDiskName *TemplateParameter `json:"osDiskName,omitempty"` + SshAuthorizedKey *TemplateParameter `json:"sshAuthorizedKey,omitempty"` + StorageAccountName *TemplateParameter `json:"storageAccountName,omitempty"` + VMSize *TemplateParameter `json:"vmSize,omitempty"` + VMName *TemplateParameter `json:"vmName,omitempty"` +} diff --git a/builder/azure/arm/template_parameters_test.go b/builder/azure/arm/template_parameters_test.go new file mode 100644 index 000000000..1daf8400a --- /dev/null +++ b/builder/azure/arm/template_parameters_test.go @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "encoding/json" + "fmt" + "strings" + "testing" +) + +func TestTemplateParametersShouldHaveExpectedKeys(t *testing.T) { + params := TemplateParameters{ + AdminUsername: &TemplateParameter{"sentinel"}, + AdminPassword: &TemplateParameter{"sentinel"}, + DnsNameForPublicIP: &TemplateParameter{"sentinel"}, + ImageOffer: &TemplateParameter{"sentinel"}, + ImagePublisher: &TemplateParameter{"sentinel"}, + ImageSku: &TemplateParameter{"sentinel"}, + OSDiskName: &TemplateParameter{"sentinel"}, + SshAuthorizedKey: &TemplateParameter{"sentinel"}, + StorageAccountName: &TemplateParameter{"sentinel"}, + VMName: &TemplateParameter{"sentinel"}, + VMSize: &TemplateParameter{"sentinel"}, + } + + bs, err := json.Marshal(params) + if err != nil { + t.Fail() + } + + var doc map[string]*json.RawMessage + err = json.Unmarshal(bs, &doc) + + if err != nil { + t.Fail() + } + + expectedKeys := []string{ + "adminUsername", + "adminPassword", + "dnsNameForPublicIP", + "imageOffer", + "imagePublisher", + "imageSku", + "osDiskName", + "sshAuthorizedKey", + "storageAccountName", + "vmSize", + "vmName", + } + + for _, expectedKey := range expectedKeys { + _, containsKey := doc[expectedKey] + if containsKey == false { + t.Fatalf("Expected template parameters to contain the key value '%s', but it did not!", expectedKey) + } + } +} + +func TestParameterValuesShouldBeSet(t *testing.T) { + params := TemplateParameters{ + AdminUsername: &TemplateParameter{"adminusername00"}, + AdminPassword: &TemplateParameter{"adminpassword00"}, + DnsNameForPublicIP: &TemplateParameter{"dnsnameforpublicip00"}, + ImageOffer: &TemplateParameter{"imageoffer00"}, + ImagePublisher: &TemplateParameter{"imagepublisher00"}, + ImageSku: &TemplateParameter{"imagesku00"}, + OSDiskName: &TemplateParameter{"osdiskname00"}, + SshAuthorizedKey: &TemplateParameter{"sshauthorizedkey00"}, + StorageAccountName: &TemplateParameter{"storageaccountname00"}, + VMName: &TemplateParameter{"vmname00"}, + VMSize: &TemplateParameter{"vmsize00"}, + } + + bs, err := json.Marshal(params) + if err != nil { + t.Fail() + } + + var doc map[string]map[string]interface{} + err = json.Unmarshal(bs, &doc) + + if err != nil { + t.Fail() + } + + for k, v := range doc { + var expectedValue = fmt.Sprintf("%s00", strings.ToLower(k)) + var actualValue, exists = v["value"] + if exists != true { + t.Errorf("Expected to find a 'value' key under '%s', but it was missing!", k) + } + + if expectedValue != actualValue { + t.Errorf("Expected '%s', but actual was '%s'!", expectedValue, actualValue) + } + } +} + +func TestEmptyValuesShouldBeOmitted(t *testing.T) { + params := TemplateParameters{ + AdminUsername: &TemplateParameter{"adminusername00"}, + } + + bs, err := json.Marshal(params) + if err != nil { + t.Fail() + } + + var doc map[string]map[string]interface{} + err = json.Unmarshal(bs, &doc) + + if err != nil { + t.Fail() + } + + if len(doc) != 1 { + t.Errorf("Failed to omit empty template parameters from the JSON document!") + t.Errorf("doc=%+v", doc) + t.Fail() + } +} diff --git a/builder/azure/arm/tempname.go b/builder/azure/arm/tempname.go new file mode 100644 index 000000000..8cdcbd1c9 --- /dev/null +++ b/builder/azure/arm/tempname.go @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "fmt" + + "github.com/mitchellh/packer/builder/azure/common" +) + +const ( + TempNameAlphabet = "0123456789bcdfghjklmnpqrstvwxyz" + TempPasswordAlphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +type TempName struct { + AdminPassword string + ComputeName string + DeploymentName string + ResourceGroupName string + OSDiskName string +} + +func NewTempName() *TempName { + tempName := &TempName{} + + suffix := common.RandomString(TempNameAlphabet, 10) + tempName.ComputeName = fmt.Sprintf("pkrvm%s", suffix) + tempName.DeploymentName = fmt.Sprintf("pkrdp%s", suffix) + tempName.OSDiskName = fmt.Sprintf("pkros%s", suffix) + tempName.ResourceGroupName = fmt.Sprintf("packer-Resource-Group-%s", suffix) + + tempName.AdminPassword = common.RandomString(TempPasswordAlphabet, 32) + + return tempName +} diff --git a/builder/azure/arm/tempname_test.go b/builder/azure/arm/tempname_test.go new file mode 100644 index 000000000..114ecedf3 --- /dev/null +++ b/builder/azure/arm/tempname_test.go @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package arm + +import ( + "strings" + "testing" +) + +func TestTempNameShouldCreatePrefixedRandomNames(t *testing.T) { + tempName := NewTempName() + + if strings.Index(tempName.ComputeName, "pkrvm") != 0 { + t.Errorf("Expected ComputeName to begin with 'pkrvm', but got '%s'!", tempName.ComputeName) + } + + if strings.Index(tempName.DeploymentName, "pkrdp") != 0 { + t.Errorf("Expected ComputeName to begin with 'pkrdp', but got '%s'!", tempName.ComputeName) + } + + if strings.Index(tempName.OSDiskName, "pkros") != 0 { + t.Errorf("Expected OSDiskName to begin with 'pkros', but got '%s'!", tempName.OSDiskName) + } + + if strings.Index(tempName.ResourceGroupName, "packer-Resource-Group-") != 0 { + t.Errorf("Expected ResourceGroupName to begin with 'packer-Resource-Group-', but got '%s'!", tempName.ResourceGroupName) + } +} + +func TestTempNameShouldHaveSameSuffix(t *testing.T) { + tempName := NewTempName() + suffix := tempName.ComputeName[5:] + + if strings.HasSuffix(tempName.DeploymentName, suffix) != true { + t.Errorf("Expected DeploymentName to end with '%s', but the value is '%s'!", suffix, tempName.DeploymentName) + } + + if strings.HasSuffix(tempName.OSDiskName, suffix) != true { + t.Errorf("Expected OSDiskName to end with '%s', but the value is '%s'!", suffix, tempName.OSDiskName) + } + + if strings.HasSuffix(tempName.ResourceGroupName, suffix) != true { + t.Errorf("Expected ResourceGroupName to end with '%s', but the value is '%s'!", suffix, tempName.ResourceGroupName) + } + +} diff --git a/builder/azure/common/constants/goos.go b/builder/azure/common/constants/goos.go new file mode 100644 index 000000000..b904bf9d2 --- /dev/null +++ b/builder/azure/common/constants/goos.go @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package constants + +const ( + GOOS_Linux string = "linux" + GOOS_Windows string = "windows" + GOOS_Darwin string = "darwin" +) diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go new file mode 100644 index 000000000..a27fe6da0 --- /dev/null +++ b/builder/azure/common/constants/stateBag.go @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package constants + +// complete flags +const ( + CertInstalled string = "certInstalled" + CertUploaded string = "certUploaded" + DiskExists string = "diskExists" + ImageCreated string = "imageCreated" + SrvExists string = "srvExists" + VmExists string = "vmExists" + VmRunning string = "vmRunning" +) +const ( + AuthorizedKey string = "authorizedKey" + Certificate string = "certificate" + Config string = "config" + Error string = "error" + HardDiskName string = "hardDiskName" + MediaLink string = "mediaLink" + OSImageName string = "osImageName" + PrivateKey string = "privateKey" + RequestManager string = "requestManager" + ServicePrincipalToken string = "servicePrincipalToken" + SSHHost string = "sshHost" + Thumbprint string = "thumbprint" + Ui string = "ui" +) +const ( + ArmComputeName string = "arm.ComputeName" + ArmDeploymentName string = "arm.DeploymentName" + ArmLocation string = "arm.Location" + ArmOSDiskVhd string = "arm.OSDiskVhd" + ArmPublicIPAddressName string = "arm.PublicIPAddressName" + ArmResourceGroupName string = "arm.ResourceGroupName" + ArmTemplateParameters string = "arm.TemplateParameters" + ArmVirtualMachineCaptureParameters string = "arm.VirtualMachineCaptureParameters" +) diff --git a/builder/azure/common/constants/targetplatforms.go b/builder/azure/common/constants/targetplatforms.go new file mode 100644 index 000000000..d95f7b66c --- /dev/null +++ b/builder/azure/common/constants/targetplatforms.go @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package constants + +// Target types +const ( + Target_Linux string = "Linux" + Target_Windows string = "Windows" +) diff --git a/builder/azure/common/gluestrings.go b/builder/azure/common/gluestrings.go new file mode 100644 index 000000000..2bab92e54 --- /dev/null +++ b/builder/azure/common/gluestrings.go @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package common + +// removes overlap between the end of a and the start of b and +// glues them together +func GlueStrings(a, b string) string { + shift := 0 + for shift < len(a) { + i := 0 + for (i+shift < len(a)) && (i < len(b)) && (a[i+shift] == b[i]) { + i++ + } + if i+shift == len(a) { + break + } + shift++ + } + + return string(a[:shift]) + b +} diff --git a/builder/azure/common/gluestrings_test.go b/builder/azure/common/gluestrings_test.go new file mode 100644 index 000000000..ac4c1528d --- /dev/null +++ b/builder/azure/common/gluestrings_test.go @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package common + +import ( + "testing" +) + +func TestGlueStrings(t *testing.T) { + cases := []struct{ a, b, expected string }{ + { + "Some log that starts in a", + "starts in a, but continues in b", + "Some log that starts in a, but continues in b", + }, + { + "", + "starts in b", + "starts in b", + }, + } + for _, testcase := range cases { + t.Logf("testcase: %+v\n", testcase) + + result := GlueStrings(testcase.a, testcase.b) + t.Logf("result: '%s'", result) + + if result != testcase.expected { + t.Errorf("expected %q, got %q", testcase.expected, result) + } + } +} diff --git a/builder/azure/common/lin/ssh.go b/builder/azure/common/lin/ssh.go new file mode 100644 index 000000000..b6d5a6efe --- /dev/null +++ b/builder/azure/common/lin/ssh.go @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package lin + +import ( + "fmt" + "github.com/mitchellh/packer/builder/azure/common/constants" + "github.com/mitchellh/multistep" + "golang.org/x/crypto/ssh" +) + +func SSHHost(state multistep.StateBag) (string, error) { + host := state.Get(constants.SSHHost).(string) + return host, nil +} + +// SSHConfig returns a function that can be used for the SSH communicator +// config for connecting to the instance created over SSH using the generated +// private key. +func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, error) { + return func(state multistep.StateBag) (*ssh.ClientConfig, error) { + privateKey := state.Get(constants.PrivateKey).(string) + + signer, err := ssh.ParsePrivateKey([]byte(privateKey)) + if err != nil { + return nil, fmt.Errorf("Error setting up SSH config: %s", err) + } + + return &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + }, nil + } +} diff --git a/builder/azure/common/lin/step_create_cert.go b/builder/azure/common/lin/step_create_cert.go new file mode 100644 index 000000000..cbac34cd4 --- /dev/null +++ b/builder/azure/common/lin/step_create_cert.go @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package lin + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "time" + + "github.com/mitchellh/packer/builder/azure/common/constants" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepCreateCert struct { + TmpServiceName string +} + +func (s *StepCreateCert) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + ui.Say("Creating Temporary Certificate...") + + err := s.createCert(state) + if err != nil { + err := fmt.Errorf("Error Creating Temporary Certificate: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepCreateCert) Cleanup(state multistep.StateBag) {} + +func (s *StepCreateCert) createCert(state multistep.StateBag) error { + + log.Printf("createCert: Generating RSA key pair...") + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + err := fmt.Errorf("Failed to Generate Private Key: %s", err) + return err + } + + // ASN.1 DER encoded form + privkey := string(pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + })) + + // Set the private key in the statebag for later + state.Put(constants.PrivateKey, privkey) + log.Printf("createCert: Private key:\n%s", privkey) + + log.Printf("createCert: Creating certificate...") + + host := fmt.Sprintf("%s.cloudapp.net", s.TmpServiceName) + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + err := fmt.Errorf("Failed to Generate Serial Number: %v", err) + 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, &priv.PublicKey, priv) + if err != nil { + err := fmt.Errorf("Failed to Create Certificate: %s", err) + return err + } + + cert := string(pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: derBytes, + })) + state.Put(constants.Certificate, cert) + log.Printf("createCert: Certificate:\n%s", cert) + + h := sha1.New() + h.Write(derBytes) + thumbprint := fmt.Sprintf("%X", h.Sum(nil)) + state.Put(constants.Thumbprint, thumbprint) + log.Printf("createCert: Thumbprint:\n%s", thumbprint) + + return nil +} diff --git a/builder/azure/common/lin/step_generalize_os.go b/builder/azure/common/lin/step_generalize_os.go new file mode 100644 index 000000000..508de67b4 --- /dev/null +++ b/builder/azure/common/lin/step_generalize_os.go @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package lin + +import ( + "bytes" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type StepGeneralizeOS struct { + Command string +} + +func (s *StepGeneralizeOS) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + comm := state.Get("communicator").(packer.Communicator) + + ui.Say("Executing OS generalization...") + + var stdout, stderr bytes.Buffer + cmd := &packer.RemoteCmd{ + Command: s.Command, + Stdout: &stdout, + Stderr: &stderr, + } + + if err := comm.Start(cmd); err != nil { + err := fmt.Errorf("Failed executing OS generalization command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Wait for the command to run + cmd.Wait() + + // If the command failed to run, notify the user in some way. + if cmd.ExitStatus != 0 { + state.Put("error", fmt.Errorf( + "OS generalization has non-zero exit status.\n\nStdout: %s\n\nStderr: %s", + stdout.String(), stderr.String())) + return multistep.ActionHalt + } + + log.Printf("OS generalization stdout: %s", stdout.String()) + log.Printf("OS generalization stderr: %s", stderr.String()) + + return multistep.ActionContinue +} + +func (s *StepGeneralizeOS) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/azure/common/randomstring.go b/builder/azure/common/randomstring.go new file mode 100644 index 000000000..eab3515dd --- /dev/null +++ b/builder/azure/common/randomstring.go @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package common + +import ( + "math/rand" + "os" + "time" +) + +var pwSymbols = []string{ + "abcdefghijklmnopqrstuvwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "0123456789", +} + +var rnd = rand.New(rand.NewSource(time.Now().UnixNano() + int64(os.Getpid()))) + +func RandomString(chooseFrom string, length int) (randomString string) { + cflen := len(chooseFrom) + for i := 0; i < length; i++ { + randomString += string(chooseFrom[rnd.Intn(cflen)]) + } + return +} + +func RandomPassword() (password string) { + pwlen := 15 + batchsize := pwlen / len(pwSymbols) + pw := make([]byte, 0, pwlen) + // choose character set + for c := 0; len(pw) < pwlen; c++ { + s := RandomString(pwSymbols[c%len(pwSymbols)], rnd.Intn(batchsize-1)+1) + pw = append(pw, []byte(s)...) + } + // truncate + pw = pw[:pwlen] + + // permute + for c := 0; c < pwlen-1; c++ { + i := rnd.Intn(pwlen-c) + c + x := pw[c] + pw[c] = pw[i] + pw[i] = x + } + return string(pw) +} diff --git a/builder/azure/common/randomstring_test.go b/builder/azure/common/randomstring_test.go new file mode 100644 index 000000000..6b0c3efc5 --- /dev/null +++ b/builder/azure/common/randomstring_test.go @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See the LICENSE file in the project root for license information. + +package common + +import ( + "testing" +) + +func TestRandomPassword_generates_15char_passwords(t *testing.T) { + for i := 0; i < 100; i++ { + pw := RandomPassword() + t.Logf("pw: %v", pw) + if len(pw) != 15 { + t.Fatalf("len(pw)!=15, but %v: %v (%v)", len(pw), pw, i) + } + } +} diff --git a/command/plugin.go b/command/plugin.go index 178358653..61a17de72 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -16,6 +16,7 @@ import ( amazonchrootbuilder "github.com/mitchellh/packer/builder/amazon/chroot" amazonebsbuilder "github.com/mitchellh/packer/builder/amazon/ebs" amazoninstancebuilder "github.com/mitchellh/packer/builder/amazon/instance" + azurearmbuilder "github.com/mitchellh/packer/builder/azure/arm" digitaloceanbuilder "github.com/mitchellh/packer/builder/digitalocean" dockerbuilder "github.com/mitchellh/packer/builder/docker" filebuilder "github.com/mitchellh/packer/builder/file" @@ -64,6 +65,7 @@ var Builders = map[string]packer.Builder{ "amazon-chroot": new(amazonchrootbuilder.Builder), "amazon-ebs": new(amazonebsbuilder.Builder), "amazon-instance": new(amazoninstancebuilder.Builder), + "azure-arm": new(azurearmbuilder.Builder), "digitalocean": new(digitaloceanbuilder.Builder), "docker": new(dockerbuilder.Builder), "file": new(filebuilder.Builder),