mirror of
https://github.com/hashicorp/packer.git
synced 2026-05-28 04:35:38 -04:00
Initial import of Microsoft contributed code
This commit is contained in:
parent
507efda4f4
commit
5b5888b230
47 changed files with 3666 additions and 0 deletions
116
builder/azure/arm/README.md
Normal file
116
builder/azure/arm/README.md
Normal 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.
|
||||
36
builder/azure/arm/artifact.go
Normal file
36
builder/azure/arm/artifact.go
Normal 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
|
||||
}
|
||||
54
builder/azure/arm/azure_client.go
Normal file
54
builder/azure/arm/azure_client.go
Normal 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
|
||||
}
|
||||
158
builder/azure/arm/builder.go
Normal file
158
builder/azure/arm/builder.go
Normal 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
|
||||
}
|
||||
33
builder/azure/arm/builder_test.go
Normal file
33
builder/azure/arm/builder_test.go
Normal 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
249
builder/azure/arm/config.go
Normal 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"))
|
||||
}
|
||||
}
|
||||
273
builder/azure/arm/config_test.go
Normal file
273
builder/azure/arm/config_test.go
Normal 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
|
||||
}
|
||||
66
builder/azure/arm/deployment_factory.go
Normal file
66
builder/azure/arm/deployment_factory.go
Normal 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
|
||||
}
|
||||
92
builder/azure/arm/deployment_factory_test.go
Normal file
92
builder/azure/arm/deployment_factory_test.go
Normal 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
|
||||
}
|
||||
48
builder/azure/arm/deployment_poller.go
Normal file
48
builder/azure/arm/deployment_poller.go
Normal 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()
|
||||
}
|
||||
}
|
||||
108
builder/azure/arm/deployment_poller_test.go
Normal file
108
builder/azure/arm/deployment_poller_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
61
builder/azure/arm/openssh_key_pair.go
Normal file
61
builder/azure/arm/openssh_key_pair.go
Normal 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
|
||||
}
|
||||
35
builder/azure/arm/openssh_key_pair_test.go
Normal file
35
builder/azure/arm/openssh_key_pair_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
72
builder/azure/arm/step_capture_image.go
Normal file
72
builder/azure/arm/step_capture_image.go
Normal 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) {
|
||||
}
|
||||
105
builder/azure/arm/step_capture_image_test.go
Normal file
105
builder/azure/arm/step_capture_image_test.go
Normal 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
|
||||
}
|
||||
62
builder/azure/arm/step_create_resource_group.go
Normal file
62
builder/azure/arm/step_create_resource_group.go
Normal 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) {
|
||||
}
|
||||
92
builder/azure/arm/step_create_resource_group_test.go
Normal file
92
builder/azure/arm/step_create_resource_group_test.go
Normal 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
|
||||
}
|
||||
67
builder/azure/arm/step_delete_os_disk.go
Normal file
67
builder/azure/arm/step_delete_os_disk.go
Normal 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) {
|
||||
}
|
||||
113
builder/azure/arm/step_delete_os_disk_test.go
Normal file
113
builder/azure/arm/step_delete_os_disk_test.go
Normal 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
|
||||
}
|
||||
61
builder/azure/arm/step_delete_resource_group.go
Normal file
61
builder/azure/arm/step_delete_resource_group.go
Normal 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) {
|
||||
}
|
||||
83
builder/azure/arm/step_delete_resource_group_test.go
Normal file
83
builder/azure/arm/step_delete_resource_group_test.go
Normal 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
|
||||
}
|
||||
88
builder/azure/arm/step_deploy_template.go
Normal file
88
builder/azure/arm/step_deploy_template.go
Normal 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) {
|
||||
}
|
||||
101
builder/azure/arm/step_deploy_template_test.go
Normal file
101
builder/azure/arm/step_deploy_template_test.go
Normal 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
|
||||
}
|
||||
65
builder/azure/arm/step_get_ip_address.go
Normal file
65
builder/azure/arm/step_get_ip_address.go
Normal 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) {
|
||||
}
|
||||
102
builder/azure/arm/step_get_ip_address_test.go
Normal file
102
builder/azure/arm/step_get_ip_address_test.go
Normal 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
|
||||
}
|
||||
63
builder/azure/arm/step_get_os_disk.go
Normal file
63
builder/azure/arm/step_get_os_disk.go
Normal 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) {
|
||||
}
|
||||
125
builder/azure/arm/step_get_os_disk_test.go
Normal file
125
builder/azure/arm/step_get_os_disk_test.go
Normal 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
|
||||
}
|
||||
63
builder/azure/arm/step_power_off_compute.go
Normal file
63
builder/azure/arm/step_power_off_compute.go
Normal 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) {
|
||||
}
|
||||
93
builder/azure/arm/step_power_off_compute_test.go
Normal file
93
builder/azure/arm/step_power_off_compute_test.go
Normal 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
|
||||
}
|
||||
66
builder/azure/arm/step_validate_template.go
Normal file
66
builder/azure/arm/step_validate_template.go
Normal 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) {
|
||||
}
|
||||
102
builder/azure/arm/step_validate_template_test.go
Normal file
102
builder/azure/arm/step_validate_template_test.go
Normal 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
|
||||
}
|
||||
180
builder/azure/arm/template.go
Normal file
180
builder/azure/arm/template.go
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
35
builder/azure/arm/template_parameters.go
Normal file
35
builder/azure/arm/template_parameters.go
Normal 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"`
|
||||
}
|
||||
124
builder/azure/arm/template_parameters_test.go
Normal file
124
builder/azure/arm/template_parameters_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
37
builder/azure/arm/tempname.go
Normal file
37
builder/azure/arm/tempname.go
Normal 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
|
||||
}
|
||||
47
builder/azure/arm/tempname_test.go
Normal file
47
builder/azure/arm/tempname_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
10
builder/azure/common/constants/goos.go
Normal file
10
builder/azure/common/constants/goos.go
Normal 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"
|
||||
)
|
||||
40
builder/azure/common/constants/stateBag.go
Normal file
40
builder/azure/common/constants/stateBag.go
Normal 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"
|
||||
)
|
||||
10
builder/azure/common/constants/targetplatforms.go
Normal file
10
builder/azure/common/constants/targetplatforms.go
Normal 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"
|
||||
)
|
||||
22
builder/azure/common/gluestrings.go
Normal file
22
builder/azure/common/gluestrings.go
Normal 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
|
||||
}
|
||||
33
builder/azure/common/gluestrings_test.go
Normal file
33
builder/azure/common/gluestrings_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
builder/azure/common/lin/ssh.go
Normal file
37
builder/azure/common/lin/ssh.go
Normal 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
|
||||
}
|
||||
}
|
||||
114
builder/azure/common/lin/step_create_cert.go
Normal file
114
builder/azure/common/lin/step_create_cert.go
Normal 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
|
||||
}
|
||||
57
builder/azure/common/lin/step_generalize_os.go
Normal file
57
builder/azure/common/lin/step_generalize_os.go
Normal 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
|
||||
}
|
||||
48
builder/azure/common/randomstring.go
Normal file
48
builder/azure/common/randomstring.go
Normal 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)
|
||||
}
|
||||
18
builder/azure/common/randomstring_test.go
Normal file
18
builder/azure/common/randomstring_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue