Initial import of Microsoft contributed code

This commit is contained in:
Chris Bednarski 2016-03-04 02:14:55 -08:00
parent 507efda4f4
commit 5b5888b230
47 changed files with 3666 additions and 0 deletions

116
builder/azure/arm/README.md Normal file
View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}
}

249
builder/azure/arm/config.go Normal file
View file

@ -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"))
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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"
}
}
}
}
]
}`

View file

@ -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"`
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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"
)

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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),