Merge pull request #3159 from ahmetalpbalkan/azure-arm

New Microsoft Azure docker-machine driver
This commit is contained in:
Nathan LeClaire 2016-03-14 18:28:51 -07:00
commit ef4823f2ac
18 changed files with 2269 additions and 34 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/machine/commands" "github.com/docker/machine/commands"
"github.com/docker/machine/commands/mcndirs" "github.com/docker/machine/commands/mcndirs"
"github.com/docker/machine/drivers/amazonec2" "github.com/docker/machine/drivers/amazonec2"
"github.com/docker/machine/drivers/azure"
"github.com/docker/machine/drivers/digitalocean" "github.com/docker/machine/drivers/digitalocean"
"github.com/docker/machine/drivers/exoscale" "github.com/docker/machine/drivers/exoscale"
"github.com/docker/machine/drivers/generic" "github.com/docker/machine/drivers/generic"
@ -168,6 +169,8 @@ func runDriver(driverName string) {
switch driverName { switch driverName {
case "amazonec2": case "amazonec2":
plugin.RegisterDriver(amazonec2.NewDriver("", "")) plugin.RegisterDriver(amazonec2.NewDriver("", ""))
case "azure":
plugin.RegisterDriver(azure.NewDriver("", ""))
case "digitalocean": case "digitalocean":
plugin.RegisterDriver(digitalocean.NewDriver("", "")) plugin.RegisterDriver(digitalocean.NewDriver("", ""))
case "exoscale": case "exoscale":

View File

@ -12,6 +12,10 @@ several "checklist items" which should be documented. This document is intended
to cover the current Docker Machine release process. It is written for Docker to cover the current Docker Machine release process. It is written for Docker
Machine core maintainers who might find themselves performing a release. Machine core maintainers who might find themselves performing a release.
0. The new version of `azure` driver released in 0.7.0 is not backwards compatible
and therefore errors out with a message saying the new driver is unsupported with
the new version. The commit 7b961604 should be undone prior to 0.8.0 release and
this notice must be removed from `docs/RELEASE.md`.
1. **Get a GITHUB_TOKEN** Check that you have a proper `GITHUB_TOKEN`. This 1. **Get a GITHUB_TOKEN** Check that you have a proper `GITHUB_TOKEN`. This
token needs only to have the `repo` scope. The token can be created on github token needs only to have the `repo` scope. The token can be created on github
in the settings > Personal Access Token menu. in the settings > Personal Access Token menu.

View File

@ -10,51 +10,106 @@ parent="smn_machine_drivers"
# Microsoft Azure # Microsoft Azure
Create machines on [Microsoft Azure](http://azure.microsoft.com/). You will need an Azure Subscription to use this Docker Machine driver.
[Sign up for a free trial.][trial]
You need to create a subscription with a cert. Run these commands and answer the questions:
$ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem > **NOTE:** This documentation is for the new version of the Azure driver, which started
$ openssl pkcs12 -export -out mycert.pfx -in mycert.pem -name "My Certificate" > shipping with v0.7.0. This driver is not backwards-compatible with the old
$ openssl x509 -inform pem -in mycert.pem -outform der -out mycert.cer > Azure driver. If you want to continue managing your existing Azure machines, please
> download and use machine versions prior to v0.7.0.
Go to the Azure portal, go to the "Settings" page (you can find the link at the bottom of the [azure]: http://azure.microsoft.com/
left sidebar - you need to scroll), then "Management Certificates" and upload `mycert.cer`. [trial]: https://azure.microsoft.com/free/
Grab your subscription ID from the portal, then run `docker-machine create` with these details: ## Authentication
$ docker-machine create -d azure --azure-subscription-id="SUB_ID" --azure-subscription-cert="mycert.pem" A-VERY-UNIQUE-NAME The first time you try to create a machine, Azure driver will ask you to
authenticate:
The Azure driver uses the `b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-15_10-amd64-server-20151116.1-en-us-30GB` $ docker-machine create --driver azure --azure-subscription-id <subs-id> <machine-name>
image by default. Note, this image is not available in the Chinese regions. In China you should Running pre-create checks...
specify `b549f4301d0b4295b8e76ceb65df47d4__Ubuntu-15_10-amd64-server-20151116.1-en-us-30GB`. Microsoft Azure: To sign in, use a web browser to open the page https://aka.ms/devicelogin.
Enter the code [...] to authenticate.
You may need to `machine ssh` in to the virtual machine and reboot to ensure that the OS is updated. After authenticating, the driver will remember your credentials up to two weeks.
Options: ## Options
- `--azure-docker-port`: Port for Docker daemon. Azure driver only has a single required argument to make things easier. Please
- `--azure-image`: Azure image name. See [How to: Get the Windows Azure Image Name](https://msdn.microsoft.com/en-us/library/dn135249%28v=nav.70%29.aspx) read the optional flags to configure machine details and placement further.
- `--azure-location`: Machine instance location.
- `--azure-password`: Your Azure password. Required:
- `--azure-publish-settings-file`: Azure setting file. See [How to: Download and Import Publish Settings and Subscription Information](https://msdn.microsoft.com/en-us/library/dn385850%28v=nav.70%29.aspx)
- `--azure-size`: Azure disk size. - `--azure-subscription-id`: **(required)** Your Azure Subscription ID.
- `--azure-ssh-port`: Azure SSH port.
- `--azure-subscription-id`: **required** Your Azure subscription ID (A GUID like `d255d8d7-5af0-4f5c-8a3e-1545044b861e`). Optional:
- `--azure-subscription-cert`: **required** Your Azure subscription cert.
- `--azure-username`: Azure login user name. - `--azure-image`: Azure virtual machine image. [[?][vm-image]]
- `--azure-location`: Azure region to create the virtual machine. [[?][location]]
- `--azure-resource-group`: Azure Resource Group name to create the resources in.
- `--azure-size`: Size for Azure Virtual Machine. [[?][vm-size]]
- `--azure-ssh-user`: Username for SSH login.
- `--azure-vnet`: Azure Virtual Network name to connect the virtual machine. [[?][vnet]]
- `--azure-subnet`: Azure Subnet Name to be used within the Virtual Network.
- `--azure-subnet-prefix`: Private CIDR block to be used for the new subnet.
- `--azure-availability-set`: Azure Availability Set to place the virtual machine into. [[?][av-set]]
- `--azure-open-port`: Make additional port number(s) accessible from the Internet [[?][nsg]]
- `--azure-private-ip-address`: Specify a static private IP address for the machine.
- `--azure-use-private-ip`: Use private IP address of the machine to connect.
- `--azure-no-public-ip`: Do not create a public IP address for the machine.
- `--azure-docker-port`: Port number for Docker engine [$AZURE_DOCKER_PORT]
- `--azure-environment`: Azure environment (e.g. `AzurePublicCloud`, `AzureChinaCloud`).
[vm-image]: https://azure.microsoft.com/en-us/documentation/articles/resource-groups-vm-searching/
[location]: https://azure.microsoft.com/en-us/regions/
[vm-size]: https://azure.microsoft.com/en-us/documentation/articles/virtual-machines-size-specs/
[vnet]: https://azure.microsoft.com/en-us/documentation/articles/virtual-networks-overview/
[av-set]: https://azure.microsoft.com/en-us/documentation/articles/virtual-machines-manage-availability/
Environment variables and default values: Environment variables and default values:
| CLI option | Environment variable | Default | | CLI option | Environment variable | Default |
| ------------------------------- | ----------------------------- | ------------------ | | ------------------------------- | ----------------------------- | ------------------ |
| `--azure-docker-port` | - | `2376` |
| `--azure-image` | `AZURE_IMAGE` | _Ubuntu 15.10 x64_ |
| `--azure-location` | `AZURE_LOCATION` | `West US` |
| `--azure-password` | - | - |
| `--azure-publish-settings-file` | `AZURE_PUBLISH_SETTINGS_FILE` | - |
| `--azure-size` | `AZURE_SIZE` | `Small` |
| `--azure-ssh-port` | - | `22` |
| **`--azure-subscription-cert`** | `AZURE_SUBSCRIPTION_CERT` | - |
| **`--azure-subscription-id`** | `AZURE_SUBSCRIPTION_ID` | - | | **`--azure-subscription-id`** | `AZURE_SUBSCRIPTION_ID` | - |
| `--azure-username` | - | `ubuntu` | | `--azure-environment` | `AZURE_ENVIRONMENT` | `AzurePublicCloud` |
| `--azure-image` | `AZURE_IMAGE` | `canonical:UbuntuServer:15.10:latest` |
| `--azure-location` | `AZURE_LOCATION` | `westus` |
| `--azure-resource-group` | `AZURE_RESOURCE_GROUP` | `docker-machine` |
| `--azure-size` | `AZURE_SIZE` | `Standard_A2` |
| `--azure-ssh-user` | `AZURE_SSH_USER` | `docker-user` |
| `--azure-vnet` | `AZURE_VNET` | `docker-machine` |
| `--azure-subnet` | `AZURE_SUBNET` | `docker-machine` |
| `--azure-subnet-prefix` | `AZURE_SUBNET_PREFIX` | `192.168.0.0/16` |
| `--azure-availability-set` | `AZURE_AVAILABILITY_SET` | `docker-machine` |
| `--azure-open-port` | - | - |
| `--azure-private-ip-address` | - | - |
| `--azure-use-private-ip` | - | - |
| `--azure-no-public-ip` | - | - |
| `--azure-docker-port` | `AZURE_DOCKER_PORT` | `2376` |
## Notes
Azure runs fully on the new [Azure Resource Manager (ARM)][arm] stack. Each
machine created comes with a few more Azure resources associated with it:
* A [Virtual Network][vnet] and a subnet under it is created to place your
machines into. This establishes a local network between your docker machines.
* An [Availability Set][av-set] is created to maximize availability of your
machines.
These are created once when the first machine is created and reused afterwards.
Although they are free resources, driver does a best effort to clean them up
after the last machine using these resources is removed.
Each machine is created with a public dynamic IP address for external
connectivity. All its ports (except Docker and SSH) are closed by default. You
can use `--azure-open-port` argument to specify multiple port numbers to be
accessible from Internet.
Once the machine is created, you can modify [Network Security Group][nsg]
rules and open ports of the machine from the [Azure Portal][portal].
[arm]: https://azure.microsoft.com/en-us/documentation/articles/resource-group-overview/
[nsg]: https://azure.microsoft.com/en-us/documentation/articles/virtual-networks-nsg/
[portal]: https://portal.azure.com/

501
drivers/azure/azure.go Normal file
View File

@ -0,0 +1,501 @@
package azure
import (
"errors"
"fmt"
"net"
"net/url"
"github.com/docker/machine/drivers/azure/azureutil"
"github.com/docker/machine/libmachine/drivers"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/mcnflag"
"github.com/docker/machine/libmachine/state"
"github.com/Azure/azure-sdk-for-go/arm/storage"
)
const (
defaultAzureEnvironment = "AzurePublicCloud"
defaultAzureResourceGroup = "docker-machine"
defaultAzureSize = "Standard_A2"
defaultAzureLocation = "westus"
defaultSSHUser = "docker-user" // 'root' not allowed on Azure
defaultDockerPort = 2376
defaultAzureImage = "canonical:UbuntuServer:15.10:latest"
defaultAzureVNet = "docker-machine-vnet"
defaultAzureSubnet = "docker-machine"
defaultAzureSubnetPrefix = "192.168.0.0/16"
defaultStorageType = storage.StandardLRS
defaultAzureAvailabilitySet = "docker-machine"
)
const (
flAzureEnvironment = "azure-environment"
flAzureSubscriptionID = "azure-subscription-id"
flAzureResourceGroup = "azure-resource-group"
flAzureSSHUser = "azure-ssh-user"
flAzureDockerPort = "azure-docker-port"
flAzureLocation = "azure-location"
flAzureSize = "azure-size"
flAzureImage = "azure-image"
flAzureVNet = "azure-vnet"
flAzureSubnet = "azure-subnet"
flAzureSubnetPrefix = "azure-subnet-prefix"
flAzureAvailabilitySet = "azure-availability-set"
flAzurePorts = "azure-open-port"
flAzurePrivateIPAddr = "azure-private-ip-address"
flAzureUsePrivateIP = "azure-use-private-ip"
flAzureNoPublicIP = "azure-no-public-ip"
)
const (
driverName = "azure"
sshPort = 22
)
// Driver represents Azure Docker Machine Driver.
type Driver struct {
*drivers.BaseDriver
Environment string
SubscriptionID string
ResourceGroup string
DockerPort int
Location string
Size string
Image string
VirtualNetwork string
SubnetName string
SubnetPrefix string
AvailabilitySet string
OpenPorts []string
PrivateIPAddr string
UsePrivateIP bool
NoPublicIP bool
// Ephemeral fields
ctx *azureutil.DeploymentContext
resolvedIP string // cache
}
// NewDriver returns a new driver instance.
func NewDriver(hostName, storePath string) drivers.Driver {
// NOTE(ahmetalpbalkan): any driver initialization I do here gets lost
// afterwards, especially for non-Create RPC calls. Therefore I am mostly
// making rest of the driver stateless by just relying on the following
// piece of info.
d := &Driver{
BaseDriver: &drivers.BaseDriver{
SSHUser: defaultSSHUser,
MachineName: hostName,
StorePath: storePath,
},
}
return d
}
// GetCreateFlags returns list of create flags driver accepts.
func (d *Driver) GetCreateFlags() []mcnflag.Flag {
return []mcnflag.Flag{
mcnflag.StringFlag{
Name: flAzureEnvironment,
Usage: "Azure environment (e.g. AzurePublicCloud, AzureChinaCloud)",
EnvVar: "AZURE_ENVIRONMENT",
Value: defaultAzureEnvironment,
},
mcnflag.StringFlag{
Name: flAzureSubscriptionID,
Usage: "Azure Subscription ID",
EnvVar: "AZURE_SUBSCRIPTION_ID",
},
mcnflag.StringFlag{
Name: flAzureResourceGroup,
Usage: "Azure Resource Group name (will be created if missing)",
EnvVar: "AZURE_RESOURCE_GROUP",
Value: defaultAzureResourceGroup,
},
mcnflag.StringFlag{
Name: flAzureSSHUser,
Usage: "Username for SSH login",
EnvVar: "AZURE_SSH_USER",
Value: defaultSSHUser,
},
mcnflag.IntFlag{
Name: flAzureDockerPort,
Usage: "Port number for Docker engine",
EnvVar: "AZURE_DOCKER_PORT",
Value: defaultDockerPort,
},
mcnflag.StringFlag{
Name: flAzureLocation,
Usage: "Azure region to create the virtual machine",
EnvVar: "AZURE_LOCATION",
Value: defaultAzureLocation,
},
mcnflag.StringFlag{
Name: flAzureSize,
Usage: "Size for Azure Virtual Machine",
EnvVar: "AZURE_SIZE",
Value: defaultAzureSize,
},
mcnflag.StringFlag{
Name: flAzureImage,
Usage: "Azure virtual machine OS image",
EnvVar: "AZURE_IMAGE",
Value: defaultAzureImage,
},
mcnflag.StringFlag{
Name: flAzureVNet,
Usage: "Azure Virtual Network name to connect the virtual machine",
EnvVar: "AZURE_VNET",
Value: defaultAzureVNet,
},
mcnflag.StringFlag{
Name: flAzureSubnet,
Usage: "Azure Subnet Name to be used within the Virtual Network",
EnvVar: "AZURE_SUBNET",
Value: defaultAzureSubnet,
},
mcnflag.StringFlag{
Name: flAzureSubnetPrefix,
Usage: "Private CIDR block to be used for the new subnet, should comply RFC 1918",
EnvVar: "AZURE_SUBNET_PREFIX",
Value: defaultAzureSubnetPrefix,
},
mcnflag.StringFlag{
Name: flAzureAvailabilitySet,
Usage: "Azure Availability Set to place the virtual machine into",
EnvVar: "AZURE_AVAILABILITY_SET",
Value: defaultAzureAvailabilitySet,
},
mcnflag.StringFlag{
Name: flAzurePrivateIPAddr,
Usage: "Specify a static private IP address for the machine",
},
mcnflag.BoolFlag{
Name: flAzureUsePrivateIP,
Usage: "Use private IP address of the machine to connect",
},
mcnflag.BoolFlag{
Name: flAzureNoPublicIP,
Usage: "Do not create a public IP address for the machine",
},
mcnflag.StringSliceFlag{
Name: flAzurePorts,
Usage: "Make the specified port number accessible from the Internet",
},
}
}
// SetConfigFromFlags initializes driver values from the command line values
// and checks if the arguments have values.
func (d *Driver) SetConfigFromFlags(fl drivers.DriverOptions) error {
// Initialize driver context for machine
d.ctx = &azureutil.DeploymentContext{}
// Required string flags
flags := []struct {
target *string
flag string
}{
{&d.BaseDriver.SSHUser, flAzureSSHUser},
{&d.SubscriptionID, flAzureSubscriptionID},
{&d.ResourceGroup, flAzureResourceGroup},
{&d.Location, flAzureLocation},
{&d.Size, flAzureSize},
{&d.Image, flAzureImage},
{&d.VirtualNetwork, flAzureVNet},
{&d.SubnetName, flAzureSubnet},
{&d.SubnetPrefix, flAzureSubnetPrefix},
{&d.AvailabilitySet, flAzureAvailabilitySet},
}
for _, f := range flags {
*f.target = fl.String(f.flag)
if *f.target == "" {
return requiredOptionError(f.flag)
}
}
// Optional flags or Flags of other types
d.Environment = fl.String(flAzureEnvironment)
d.OpenPorts = fl.StringSlice(flAzurePorts)
d.PrivateIPAddr = fl.String(flAzurePrivateIPAddr)
d.UsePrivateIP = fl.Bool(flAzureUsePrivateIP)
d.NoPublicIP = fl.Bool(flAzureNoPublicIP)
d.DockerPort = fl.Int(flAzureDockerPort)
// Set flags on the BaseDriver
d.BaseDriver.SSHPort = sshPort
d.SetSwarmConfigFromFlags(fl)
log.Debug("Set configuration from flags.")
return nil
}
// DriverName returns the name of the driver.
func (d *Driver) DriverName() string { return driverName }
// PreCreateCheck validates if driver values are valid to create the machine.
func (d *Driver) PreCreateCheck() (err error) {
c, err := d.newAzureClient()
if err != nil {
return err
}
// Register used resource providers with current Azure subscription.
if err := c.RegisterResourceProviders(
"Microsoft.Compute",
"Microsoft.Network",
"Microsoft.Storage"); err != nil {
return err
}
// Validate if firewall rules can be read correctly
d.ctx.FirewallRules, err = d.getSecurityRules(d.OpenPorts)
if err != nil {
return err
}
// Check if virtual machine exists. An existing virtual machine cannot be updated.
log.Debug("Checking if Virtual Machine already exists.")
if exists, err := c.VirtualMachineExists(d.ResourceGroup, d.naming().VM()); err != nil {
return err
} else if exists {
return fmt.Errorf("Virtual Machine with name %s already exists in resource group %q", d.naming().VM(), d.ResourceGroup)
}
// NOTE(ahmetalpbalkan) we could have done more checks here but Azure often
// returns meaningful error messages and it would be repeating the backend
// logic on the client side. Some examples:
// - Deployment of a machine to an existing Virtual Network fails if
// virtual network is in a different region.
// - Changing IP Address space of a subnet would fail if there are machines
// running in the Virtual Network.
log.Info("Completed machine pre-create checks.")
return nil
}
// Create creates the virtual machine.
func (d *Driver) Create() error {
// NOTE(ahmetalpbalkan): We can probably parallelize the sh*t out of this.
// However that would lead to a concurrency logic and while creation of a
// resource fails, other ones would be kicked off, which could lead to a
// resource leak. This is slower but safer.
c, err := d.newAzureClient()
if err != nil {
return err
}
if err := c.CreateResourceGroup(d.ResourceGroup, d.Location); err != nil {
return err
}
if err := c.CreateAvailabilitySetIfNotExists(d.ctx, d.ResourceGroup, d.AvailabilitySet, d.Location); err != nil {
return err
}
if err := c.CreateNetworkSecurityGroup(d.ctx, d.ResourceGroup, d.naming().NSG(), d.Location, d.ctx.FirewallRules); err != nil {
return err
}
if err := c.CreateVirtualNetworkIfNotExists(d.ResourceGroup, d.VirtualNetwork, d.Location); err != nil {
return err
}
if err := c.CreateSubnet(d.ctx, d.ResourceGroup, d.VirtualNetwork, d.SubnetName, d.SubnetPrefix); err != nil {
return err
}
if d.NoPublicIP {
log.Info("Not creating a public IP address.")
} else {
if err := c.CreatePublicIPAddress(d.ctx, d.ResourceGroup, d.naming().IP(), d.Location); err != nil {
return err
}
}
if err := c.CreateNetworkInterface(d.ctx, d.ResourceGroup, d.naming().NIC(), d.Location,
d.ctx.PublicIPAddressID, d.ctx.SubnetID, d.ctx.NetworkSecurityGroupID, d.PrivateIPAddr); err != nil {
return err
}
if err := c.CreateStorageAccount(d.ctx, d.ResourceGroup, d.Location, defaultStorageType); err != nil {
return err
}
if err := d.generateSSHKey(d.ctx); err != nil {
return err
}
if err := c.CreateVirtualMachine(d.ResourceGroup, d.naming().VM(), d.Location, d.Size, d.ctx.AvailabilitySetID,
d.ctx.NetworkInterfaceID, d.BaseDriver.SSHUser, d.ctx.SSHPublicKey, d.Image, d.ctx.StorageAccount); err != nil {
return err
}
return nil
}
// Remove deletes the virtual machine and resources associated to it.
func (d *Driver) Remove() error {
if err := d.checkLegacyDriver(false); err != nil {
return err
}
// NOTE(ahmetalpbalkan):
// - remove attemps are best effort and if a resource is already gone, we
// continue removing other resources instead of failing.
// - we can probably do a lot of parallelization here but a sequential
// logic works fine too. If we were to detach the NIC from the VM and
// then delete the VM, this could enable some parallelization.
log.Info("NOTICE: Please check Azure portal/CLI to make sure you have no leftover resources to avoid unexpected charges.")
c, err := d.newAzureClient()
if err != nil {
return err
}
if err := c.DeleteVirtualMachineIfExists(d.ResourceGroup, d.naming().VM()); err != nil {
return err
}
if err := c.DeleteNetworkInterfaceIfExists(d.ResourceGroup, d.naming().NIC()); err != nil {
return err
}
if err := c.DeletePublicIPAddressIfExists(d.ResourceGroup, d.naming().IP()); err != nil {
return err
}
if err := c.DeleteNetworkSecurityGroupIfExists(d.ResourceGroup, d.naming().NSG()); err != nil {
return err
}
if err := c.CleanupAvailabilitySetIfExists(d.ResourceGroup, d.AvailabilitySet); err != nil {
return err
}
if err := c.CleanupSubnetIfExists(d.ResourceGroup, d.VirtualNetwork, d.SubnetName); err != nil {
return err
}
if err := c.CleanupVirtualNetworkIfExists(d.ResourceGroup, d.VirtualNetwork); err != nil {
return err
}
return nil
}
// GetIP returns public IP address or hostname of the machine instance.
func (d *Driver) GetIP() (string, error) {
if err := d.checkLegacyDriver(true); err != nil {
return "", err
}
if d.resolvedIP == "" {
ip, err := d.ipAddress()
if err != nil {
return "", err
}
d.resolvedIP = ip
}
log.Debugf("Machine IP address resolved to: %s", d.resolvedIP)
return d.resolvedIP, nil
}
// GetSSHHostname returns an IP address or hostname for the machine instance.
func (d *Driver) GetSSHHostname() (string, error) {
return d.GetIP()
}
// GetURL returns a socket address to connect to Docker engine of the machine
// instance.
func (d *Driver) GetURL() (string, error) {
if err := drivers.MustBeRunning(d); err != nil {
return "", err
}
// NOTE (ahmetalpbalkan) I noticed that this is not used until machine is
// actually created and provisioned. By then GetIP() should be returning
// a non-empty IP address as the VM is already allocated and connected to.
ip, err := d.GetIP()
if err != nil {
return "", err
}
u := (&url.URL{
Scheme: "tcp",
Host: net.JoinHostPort(ip, fmt.Sprintf("%d", d.DockerPort)),
}).String()
log.Debugf("Machine URL is resolved to: %s", u)
return u, nil
}
// GetState returns the state of the virtual machine role instance.
func (d *Driver) GetState() (state.State, error) {
if err := d.checkLegacyDriver(true); err != nil {
return state.None, err
}
c, err := d.newAzureClient()
if err != nil {
return state.None, err
}
powerState, err := c.GetVirtualMachinePowerState(
d.ResourceGroup, d.naming().VM())
if err != nil {
return state.None, err
}
machineState := machineStateForVMPowerState(powerState)
log.Debugf("Determined Azure PowerState=%q, docker-machine state=%q",
powerState, machineState)
return machineState, nil
}
// Start issues a power on for the virtual machine instance.
func (d *Driver) Start() error {
if err := d.checkLegacyDriver(true); err != nil {
return err
}
c, err := d.newAzureClient()
if err != nil {
return err
}
return c.StartVirtualMachine(d.ResourceGroup, d.naming().VM())
}
// Stop issues a power off for the virtual machine instance.
func (d *Driver) Stop() error {
if err := d.checkLegacyDriver(true); err != nil {
return err
}
c, err := d.newAzureClient()
if err != nil {
return err
}
log.Info("NOTICE: Stopping an Azure Virtual Machine is just going to power it off, not deallocate.")
log.Info("NOTICE: You should remove the machine if you would like to avoid unexpected costs.")
return c.StopVirtualMachine(d.ResourceGroup, d.naming().VM())
}
// Restart reboots the virtual machine instance.
func (d *Driver) Restart() error {
if err := d.checkLegacyDriver(true); err != nil {
return err
}
// NOTE(ahmetalpbalkan) Azure will always keep the VM in Running state
// during the restart operation. Hence we rely on returned async operation
// polling to make sure the reboot is waited upon.
c, err := d.newAzureClient()
if err != nil {
return err
}
return c.RestartVirtualMachine(d.ResourceGroup, d.naming().VM())
}
// Kill stops the virtual machine role instance.
func (d *Driver) Kill() error {
// NOTE(ahmetalpbalkan) In Azure, there is no kill option for virtual
// machines, Stop() is the closest option.
log.Debug("Azure does not implement kill. Calling Stop instead.")
return d.Stop()
}
// checkLegacyDriver errors out if it encounters an Azure VM created with the
// legacy (<=0.6.0) docker-machine Azure driver.
func (d *Driver) checkLegacyDriver(short bool) error {
if d.ResourceGroup == "" {
if short {
return errors.New("New azure driver cannot manage old VMs, downgrade to v0.6.0")
}
return errors.New("New azure driver uses the new Azure Resource Manager APIs and therefore cannot manage this existing machine created with old azure driver. Please downgrade to docker-machine 0.6.0 to continue using these machines or to remove them.")
}
return nil
}

View File

@ -0,0 +1,207 @@
package azureutil
import (
"fmt"
"os"
"path/filepath"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/mcnutils"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/docker/machine/drivers/azure/logutil"
)
var (
// AD app id for docker-machine driver in various Azure realms
clientIDs = map[string]string{
azure.PublicCloud.Name: "637ddaba-219b-43b8-bf19-8cea500cf273",
azure.ChinaCloud.Name: "bb5eed6f-120b-4365-8fd9-ab1a3fba5698",
}
)
// NOTE(ahmetalpbalkan): Azure Active Directory implements OAuth 2.0 Device Flow
// described here: https://tools.ietf.org/html/draft-denniss-oauth-device-flow-00
// Although it has some gotchas, most of the authentication logic is in Azure SDK
// for Go helper packages.
//
// Device auth prints a message to the screen telling the user to click on URL
// and approve the app on the browser, meanwhile the client polls the auth API
// for a token. Once we have token, we save it locally to a file with proper
// permissions and when the token expires (in Azure case typically 1 hour) SDK
// will automatically refresh the specified token and will call the refresh
// callback function we implement here. This way we will always be storing a
// token with a refresh_token saved on the machine.
// Authenticate fetches a token from the local file cache or initiates a consent
// flow and waits for token to be obtained.
func Authenticate(env azure.Environment, subscriptionID string) (*azure.ServicePrincipalToken, error) {
clientID, ok := clientIDs[env.Name]
if !ok {
return nil, fmt.Errorf("docker-machine application not set up for Azure environment %q", env.Name)
}
// First we locate the tenant ID of the subscription as we store tokens per
// tenant (which could have multiple subscriptions)
log.Debug("Looking up AAD Tenant ID.", logutil.Fields{
"subs": subscriptionID})
tenantID, err := findTenantID(env, subscriptionID)
if err != nil {
return nil, err
}
log.Debug("Found AAD Tenant ID.", logutil.Fields{
"tenant": tenantID,
"subs": subscriptionID})
oauthCfg, err := env.OAuthConfigForTenant(tenantID)
if err != nil {
return nil, fmt.Errorf("Failed to obtain oauth config for azure environment: %v", err)
}
// for AzurePublicCloud (https://management.core.windows.net/), this old
// Service Management scope covers both ASM and ARM.
apiScope := env.ServiceManagementEndpoint
tokenPath := tokenCachePath(tenantID)
saveToken := mkTokenCallback(tokenPath)
saveTokenCallback := func(t azure.Token) error {
log.Debug("Azure token expired. Saving the refreshed token...")
return saveToken(t)
}
f := logutil.Fields{"path": tokenPath}
// Lookup the token cache file for an existing token.
spt, err := tokenFromFile(*oauthCfg, tokenPath, clientID, apiScope, saveTokenCallback)
if err != nil {
return nil, err
}
if spt != nil {
log.Debug("Auth token found in file.", f)
// NOTE(ahmetalpbalkan): The token file we found might be containng an
// expired access_token. In that case, the first call to Azure SDK will
// attempt to refresh the token using refresh_token which might have
// expired[1], in that case we will get an error and we shall remove the
// token file and initiate token flow again so that the user would not
// need removing the token cache file manually.
//
// [1]: expiration date of refresh_token is not returned in AAD /token
// response, we just know it is 14 days. Therefore users token
// will go stale every 14 days and we will delete the token file,
// re-initiate the device flow.
log.Debug("Validating the token.")
if err := validateToken(env, spt); err != nil {
log.Debug(fmt.Sprintf("Error: %v", err))
log.Info("Stored Azure credentials expired. Please reauthenticate.")
log.Debug(fmt.Sprintf("Deleting %s", tokenPath))
if err := os.RemoveAll(tokenPath); err != nil {
return nil, fmt.Errorf("Error deleting stale token file: %v", err)
}
} else {
log.Debug("Token works.")
return spt, nil
}
}
// Start an OAuth 2.0 device flow
log.Debug("Initiating device flow.", f)
spt, err = tokenFromDeviceFlow(*oauthCfg, tokenPath, clientID, apiScope)
if err != nil {
return nil, err
}
log.Debug("Obtained service principal token.")
if err := saveToken(spt.Token); err != nil {
log.Error("Error occurred saving token to cache file.")
return nil, err
}
return spt, nil
}
// tokenFromFile returns a token from the specified file if it is found, otherwise
// returns nil. Any error retrieving or creating the token is returned as an error.
func tokenFromFile(oauthCfg azure.OAuthConfig, tokenPath, clientID, resource string,
callback azure.TokenRefreshCallback) (*azure.ServicePrincipalToken, error) {
log.Debug("Loading auth token from file", logutil.Fields{"path": tokenPath})
if _, err := os.Stat(tokenPath); err != nil {
if os.IsNotExist(err) { // file not found
return nil, nil
}
return nil, err
}
token, err := azure.LoadToken(tokenPath)
if err != nil {
return nil, fmt.Errorf("Failed to load token from file: %v", err)
}
spt, err := azure.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token, callback)
if err != nil {
return nil, fmt.Errorf("Error constructing service principal token: %v", err)
}
return spt, nil
}
// tokenFromDeviceFlow prints a message to the screen for user to take action to
// consent application on a browser and in the meanwhile the authentication
// endpoint is polled until user gives consent, denies or the flow times out.
// Returned token must be saved.
func tokenFromDeviceFlow(oauthCfg azure.OAuthConfig, tokenPath, clientID, resource string) (*azure.ServicePrincipalToken, error) {
cl := oauthClient()
deviceCode, err := azure.InitiateDeviceAuth(&cl, oauthCfg, clientID, resource)
if err != nil {
return nil, fmt.Errorf("Failed to start device auth: %v", err)
}
log.Debug("Retrieved device code.", logutil.Fields{
"expires_in": to.Int64(deviceCode.ExpiresIn),
"interval": to.Int64(deviceCode.Interval),
})
// Example message: “To sign in, open https://aka.ms/devicelogin and enter
// the code 0000000 to authenticate.”
log.Infof("Microsoft Azure: %s", to.String(deviceCode.Message))
token, err := azure.WaitForUserCompletion(&cl, deviceCode)
if err != nil {
return nil, fmt.Errorf("Failed to complete device auth: %v", err)
}
spt, err := azure.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token)
if err != nil {
return nil, fmt.Errorf("Error constructing service principal token: %v", err)
}
return spt, nil
}
// tokenCachePath returns the full path the OAuth 2.0 token should be saved at
// for given tenant ID.
func tokenCachePath(tenantID string) string {
return filepath.Join(mcnutils.GetHomeDir(), ".docker", "machine", "credentials", "azure", fmt.Sprintf("%s.json", tenantID))
}
// mkTokenCallback returns a callback function that can be used to save the
// token initially or register to the Azure SDK to be called when the token is
// refreshed.
func mkTokenCallback(path string) azure.TokenRefreshCallback {
return func(t azure.Token) error {
if err := azure.SaveToken(path, 0600, t); err != nil {
return err
}
log.Debug("Saved token to file.")
return nil
}
}
// validateToken makes a call to Azure SDK with given token, essentially making
// sure if the access_token valid, if not it uses SDKs functionality to
// automatically refresh the token using refresh_token (which might have
// expired). This check is essentially to make sure refresh_token is good.
func validateToken(env azure.Environment, token *azure.ServicePrincipalToken) error {
c := subscriptionsClient(env.ResourceManagerEndpoint)
c.Authorizer = token
_, err := c.List()
if err != nil {
return fmt.Errorf("Token validity check failed: %v", err)
}
return nil
}

View File

@ -0,0 +1,15 @@
package azureutil
import (
"fmt"
"github.com/Azure/go-autorest/autorest"
)
// accessToken is interim autorest.Authorizer until we figure out oauth token
// handling. It holds the access token.
type accessToken string
func (a accessToken) WithAuthorization() autorest.PrepareDecorator {
return autorest.WithHeader("Authorization", fmt.Sprintf("Bearer %s", string(a)))
}

View File

@ -0,0 +1,750 @@
package azureutil
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/docker/machine/drivers/azure/logutil"
"github.com/docker/machine/libmachine/log"
"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"
"github.com/Azure/azure-sdk-for-go/arm/storage"
blobstorage "github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
)
const (
storageAccountPrefix = "vhds" // do not contaminate to user's existing storage accounts
fmtOSDiskContainer = "vhd-%s" // place vhds of VMs in separate containers for ease of cleanup
fmtOSDiskBlobName = "%s-os-disk.vhd"
fmtOSDiskResourceName = "%s-os-disk"
defaultStorageAPIVersion = blobstorage.DefaultAPIVersion
)
var (
// Private IPv4 address space per RFC 1918.
defaultVnetAddressPrefixes = []string{
"192.168.0.0/16",
"10.0.0.0/6",
"172.16.0.0/12"}
// Polling interval for VM power state check.
powerStatePollingInterval = time.Second * 5
waitStartTimeout = time.Minute * 10
waitPowerOffTimeout = time.Minute * 5
)
type AzureClient struct {
env azure.Environment
subscriptionID string
auth autorest.Authorizer
}
func New(env azure.Environment, subsID string, auth autorest.Authorizer) *AzureClient {
return &AzureClient{env, subsID, auth}
}
// RegisterResourceProviders registers current subscription to the specified
// resource provider namespaces if they are not already registered. Namespaces
// are case-insensitive.
func (a AzureClient) RegisterResourceProviders(namespaces ...string) error {
l, err := a.providersClient().List(nil)
if err != nil {
return err
}
if l.Value == nil {
return errors.New("Resource Providers list is returned as nil.")
}
m := make(map[string]bool)
for _, p := range *l.Value {
m[strings.ToLower(to.String(p.Namespace))] = to.String(p.RegistrationState) == "Registered"
}
for _, ns := range namespaces {
registered, ok := m[strings.ToLower(ns)]
if !ok {
return fmt.Errorf("Unknown resource provider %q", ns)
}
if registered {
log.Debugf("Already registered for %q", ns)
} else {
log.Info("Registering subscription to resource provider.", logutil.Fields{
"ns": ns,
"subs": a.subscriptionID,
})
if _, err := a.providersClient().Register(ns); err != nil {
return err
}
}
}
return nil
}
// CreateResourceGroup creates a Resource Group if not exists
func (a AzureClient) CreateResourceGroup(name, location string) error {
if ok, err := a.resourceGroupExists(name); err != nil {
return err
} else if ok {
log.Infof("Resource group %q already exists.", name)
return nil
}
log.Info("Creating resource group...", logutil.Fields{
"name": name,
"location": location})
_, err := a.resourceGroupsClient().CreateOrUpdate(name,
resources.ResourceGroup{
Location: to.StringPtr(location),
})
return err
}
func (a AzureClient) resourceGroupExists(name string) (bool, error) {
log.Info("Querying existing resource group...", logutil.Fields{"name": name})
_, err := a.resourceGroupsClient().Get(name)
return checkResourceExistsFromError(err)
}
func (a AzureClient) CreateNetworkSecurityGroup(ctx *DeploymentContext, resourceGroup, name, location string, rules *[]network.SecurityRule) error {
log.Info("Creating network security group...", logutil.Fields{
"name": name,
"location": location})
_, err := a.securityGroupsClient().CreateOrUpdate(resourceGroup, name,
network.SecurityGroup{
Location: to.StringPtr(location),
Properties: &network.SecurityGroupPropertiesFormat{
SecurityRules: rules,
},
})
if err != nil {
return err
}
nsg, err := a.securityGroupsClient().Get(resourceGroup, name, "")
ctx.NetworkSecurityGroupID = to.String(nsg.ID)
return err
}
func (a AzureClient) DeleteNetworkSecurityGroupIfExists(resourceGroup, name string) error {
return deleteResourceIfExists("Network Security Group", name,
func() error {
_, err := a.securityGroupsClient().Get(resourceGroup, name, "")
return err
},
func() (autorest.Response, error) { return a.securityGroupsClient().Delete(resourceGroup, name) })
}
func (a AzureClient) CreatePublicIPAddress(ctx *DeploymentContext, resourceGroup, name, location string) error {
log.Info("Creating public IP address...", logutil.Fields{"name": name})
_, err := a.publicIPAddressClient().CreateOrUpdate(resourceGroup, name,
network.PublicIPAddress{
Location: to.StringPtr(location),
Properties: &network.PublicIPAddressPropertiesFormat{
PublicIPAllocationMethod: network.Dynamic,
},
})
if err != nil {
return err
}
ip, err := a.publicIPAddressClient().Get(resourceGroup, name, "")
ctx.PublicIPAddressID = to.String(ip.ID)
return err
}
func (a AzureClient) DeletePublicIPAddressIfExists(resourceGroup, name string) error {
return deleteResourceIfExists("Public IP", name,
func() error {
_, err := a.publicIPAddressClient().Get(resourceGroup, name, "")
return err
},
func() (autorest.Response, error) { return a.publicIPAddressClient().Delete(resourceGroup, name) })
}
func (a AzureClient) CreateVirtualNetworkIfNotExists(resourceGroup, name, location string) error {
f := logutil.Fields{
"name": name,
"location": location}
log.Info("Querying if virtual network already exists...", f)
if exists, err := a.virtualNetworkExists(resourceGroup, name); err != nil {
return err
} else if exists {
log.Info("Virtual network already exists.", f)
return nil
}
log.Debug("Virtual network does not exist, creating...", f)
_, err := a.virtualNetworksClient().CreateOrUpdate(resourceGroup, name,
network.VirtualNetwork{
Location: to.StringPtr(location),
Properties: &network.VirtualNetworkPropertiesFormat{
AddressSpace: &network.AddressSpace{
AddressPrefixes: to.StringSlicePtr(defaultVnetAddressPrefixes),
},
},
})
return err
}
func (a AzureClient) virtualNetworkExists(resourceGroup, name string) (bool, error) {
_, err := a.virtualNetworksClient().Get(resourceGroup, name, "")
return checkResourceExistsFromError(err)
}
// CleanupVirtualNetworkIfExists removes a subnet if there are no subnets
// attached to it. Note that this method is not safe for multiple concurrent
// writers, in case of races, deployment of a machine could fail or resource
// might not be cleaned up.
func (a AzureClient) CleanupVirtualNetworkIfExists(resourceGroup, name string) error {
return a.cleanupResourceIfExists(&vnetCleanup{rg: resourceGroup, name: name})
}
func (a AzureClient) GetSubnet(resourceGroup, virtualNetwork, name string) (network.Subnet, error) {
return a.subnetsClient().Get(resourceGroup, virtualNetwork, name, "")
}
func (a AzureClient) CreateSubnet(ctx *DeploymentContext, resourceGroup, virtualNetwork, name, subnetPrefix string) error {
log.Info("Creating subnet...", logutil.Fields{
"name": name,
"vnet": virtualNetwork,
"cidr": subnetPrefix})
_, err := a.subnetsClient().CreateOrUpdate(resourceGroup, virtualNetwork, name,
network.Subnet{
Properties: &network.SubnetPropertiesFormat{
AddressPrefix: to.StringPtr(subnetPrefix),
},
})
if err != nil {
return err
}
subnet, err := a.subnetsClient().Get(resourceGroup, virtualNetwork, name, "")
ctx.SubnetID = to.String(subnet.ID)
return err
}
// CleanupSubnetIfExists removes a subnet if there are no IP configurations
// (through NICs) are attached to it. Note that this method is not safe for
// multiple concurrent writers, in case of races, deployment of a machine could
// fail or resource might not be cleaned up.
func (a AzureClient) CleanupSubnetIfExists(resourceGroup, virtualNetwork, name string) error {
return a.cleanupResourceIfExists(&subnetCleanup{
rg: resourceGroup, vnet: virtualNetwork, name: name,
})
}
func (a AzureClient) CreateNetworkInterface(ctx *DeploymentContext, resourceGroup, name, location, publicIPAddressID, subnetID, nsgID, privateIPAddress string) error {
// NOTE(ahmetalpbalkan) This method is expected to fail if the user
// specified Azure location is different than location of the virtual
// network as Azure does not support cross-region virtual networks. In this
// situation, user will get an explanatory API error from Azure.
log.Info("Creating network interface...", logutil.Fields{"name": name})
var publicIP *network.PublicIPAddress
if publicIPAddressID != "" {
publicIP = &network.PublicIPAddress{ID: to.StringPtr(publicIPAddressID)}
}
var privateIPAllocMethod = network.Dynamic
if privateIPAddress != "" {
privateIPAllocMethod = network.Static
}
_, err := a.networkInterfacesClient().CreateOrUpdate(resourceGroup, name, network.Interface{
Location: to.StringPtr(location),
Properties: &network.InterfacePropertiesFormat{
NetworkSecurityGroup: &network.SecurityGroup{
ID: to.StringPtr(nsgID),
},
IPConfigurations: &[]network.InterfaceIPConfiguration{
{
Name: to.StringPtr("ip"),
Properties: &network.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.StringPtr(privateIPAddress),
PrivateIPAllocationMethod: privateIPAllocMethod,
PublicIPAddress: publicIP,
Subnet: &network.Subnet{
ID: to.StringPtr(subnetID),
},
},
},
},
},
})
if err != nil {
return err
}
nic, err := a.networkInterfacesClient().Get(resourceGroup, name, "")
ctx.NetworkInterfaceID = to.String(nic.ID)
return err
}
func (a AzureClient) DeleteNetworkInterfaceIfExists(resourceGroup, name string) error {
return deleteResourceIfExists("Network Interface", name,
func() error {
_, err := a.networkInterfacesClient().Get(resourceGroup, name, "")
return err
},
func() (autorest.Response, error) { return a.networkInterfacesClient().Delete(resourceGroup, name) })
}
func (a AzureClient) CreateStorageAccount(ctx *DeploymentContext, resourceGroup, location string, storageType storage.AccountType) error {
s, err := a.findOrCreateStorageAccount(resourceGroup, location, storageType)
ctx.StorageAccount = s
return err
}
func (a AzureClient) findOrCreateStorageAccount(resourceGroup, location string, storageType storage.AccountType) (*storage.AccountProperties, error) {
prefix := storageAccountPrefix
if s, err := a.findStorageAccount(resourceGroup, location, prefix, storageType); err != nil {
return nil, err
} else if s != nil {
return s, nil
}
log.Debug("No eligible storage account found.", logutil.Fields{
"location": location,
"type": storageType})
return a.createStorageAccount(resourceGroup, location, storageType)
}
func (a AzureClient) findStorageAccount(resourceGroup, location, prefix string, storageType storage.AccountType) (*storage.AccountProperties, error) {
f := logutil.Fields{
"type": storageType,
"prefix": prefix,
"location": location}
log.Debug("Querying existing storage accounts...", f)
l, err := a.storageAccountsClient().ListByResourceGroup(resourceGroup)
if err != nil {
return nil, err
}
if l.Value != nil {
for _, v := range *l.Value {
log.Debug("Iterating...", logutil.Fields{
"name": to.String(v.Name),
"type": storageType,
"location": to.String(v.Location),
})
if to.String(v.Location) == location && v.Properties.AccountType == storageType && strings.HasPrefix(to.String(v.Name), prefix) {
log.Debug("Found eligible storage account.", logutil.Fields{"name": to.String(v.Name)})
return v.Properties, nil
}
}
}
log.Debug("No account matching the pattern is found.", f)
return nil, err
}
func (a AzureClient) createStorageAccount(resourceGroup, location string, storageType storage.AccountType) (*storage.AccountProperties, error) {
name := randomAzureStorageAccountName() // if it's not random enough, then you're unlucky
f := logutil.Fields{
"name": name,
"location": location}
log.Info("Creating storage account...", f)
_, err := a.storageAccountsClient().Create(resourceGroup, name,
storage.AccountCreateParameters{
Location: to.StringPtr(location),
Properties: &storage.AccountPropertiesCreateParameters{
AccountType: storageType,
},
})
if err != nil {
return nil, err
}
// NOTE(ahmetalpbalkan) The following loop should eventually be deleted.
// Azure Storage Provider has a different polling logic than other Core RPs
// and that is not currently implemented in go-autorest. In this loop we are
// polling until the property we need is present.
for {
// Issue a GET call because polling endpoint (?monitor=true) does not respond with
// full storage object (has all .Properties)
log.Debug("Waiting for storage account to be ready.", f)
s, err := a.storageAccountsClient().GetProperties(resourceGroup, name)
if err != nil {
return nil, err
}
if s.Properties != nil && s.Properties.PrimaryEndpoints != nil &&
s.Properties.PrimaryEndpoints.Blob != nil {
return s.Properties, err
}
log.Debug("Storage account is not yet ready.", f)
time.Sleep(time.Second * 10)
}
}
func (a AzureClient) VirtualMachineExists(resourceGroup, name string) (bool, error) {
_, err := a.virtualMachinesClient().Get(resourceGroup, name, "")
return checkResourceExistsFromError(err)
}
func (a AzureClient) DeleteVirtualMachineIfExists(resourceGroup, name string) error {
var vmRef compute.VirtualMachine
err := deleteResourceIfExists("Virtual Machine", name,
func() error {
vm, err := a.virtualMachinesClient().Get(resourceGroup, name, "")
vmRef = vm
return err
},
func() (autorest.Response, error) { return a.virtualMachinesClient().Delete(resourceGroup, name) })
if err != nil {
return err
}
// Remove disk
if vmRef.Properties != nil {
vhdURL := to.String(vmRef.Properties.StorageProfile.OsDisk.Vhd.URI)
return a.removeOSDiskBlob(resourceGroup, name, vhdURL)
}
return nil
}
func (a AzureClient) removeOSDiskBlob(resourceGroup, vmName, vhdURL string) error {
// NOTE(ahmetalpbalkan) Currently Azure APIs do not offer a Delete Virtual
// Machine functionality which deletes the attached disks along with the VM
// as well. Therefore we find out the storage account from OS disk URL and
// fetch storage account keys to delete the container containing the disk.
log.Debug("Attempting to remove OS disk...", logutil.Fields{"vm": vmName})
log.Debugf("OS Disk vhd URL: %q", vhdURL)
vhdContainer := osDiskStorageContainerName(vmName)
storageAccount, blobServiceBaseURL := extractStorageAccountFromVHDURL(vhdURL)
if storageAccount == "" {
log.Warn("Could not extract the storage account name from URL. Please clean up the disk yourself.")
return nil
}
log.Debug("Fetching storage account keys.", logutil.Fields{
"account": storageAccount,
"storageBase": blobServiceBaseURL,
})
keys, err := a.storageAccountsClient().ListKeys(resourceGroup, storageAccount)
if err != nil {
return err
}
storageAccountKey := to.String(keys.Key1)
bs, err := blobstorage.NewClient(storageAccount, storageAccountKey, blobServiceBaseURL, defaultStorageAPIVersion, true)
if err != nil {
return fmt.Errorf("Error constructing blob storage client :%v", err)
}
f := logutil.Fields{
"account": storageAccount,
"container": vhdContainer}
log.Debug("Removing container of disk blobs.", f)
ok, err := bs.GetBlobService().DeleteContainerIfExists(vhdContainer) // HTTP round-trip will not be inspected
if err != nil {
log.Debugf("Container remove happened: %v", ok)
}
return err
}
func (a AzureClient) CreateVirtualMachine(resourceGroup, name, location, size, availabilitySetID, networkInterfaceID,
username, sshPublicKey, imageName string, storageAccount *storage.AccountProperties) error {
log.Info("Creating Virtual Machine...", logutil.Fields{
"name": name,
"location": location,
"size": size,
"username": username,
"osImage": imageName,
})
img, err := parseImageName(imageName)
if err != nil {
return err
}
var (
osDiskBlobURL = osDiskStorageBlobURL(storageAccount, name)
sshKeyPath = fmt.Sprintf("/home/%s/.ssh/authorized_keys", username)
)
log.Debugf("OS disk blob will be placed at: %s", osDiskBlobURL)
log.Debugf("SSH key will be placed at: %s", sshKeyPath)
_, err = a.virtualMachinesClient().CreateOrUpdate(resourceGroup, name,
compute.VirtualMachine{
Location: to.StringPtr(location),
Properties: &compute.VirtualMachineProperties{
AvailabilitySet: &compute.SubResource{
ID: to.StringPtr(availabilitySetID),
},
HardwareProfile: &compute.HardwareProfile{
VMSize: compute.VirtualMachineSizeTypes(size),
},
NetworkProfile: &compute.NetworkProfile{
NetworkInterfaces: &[]compute.NetworkInterfaceReference{
{
ID: to.StringPtr(networkInterfaceID),
},
},
},
OsProfile: &compute.OSProfile{
ComputerName: to.StringPtr(name),
AdminUsername: to.StringPtr(username),
LinuxConfiguration: &compute.LinuxConfiguration{
DisablePasswordAuthentication: to.BoolPtr(true),
SSH: &compute.SSHConfiguration{
PublicKeys: &[]compute.SSHPublicKey{
{
Path: to.StringPtr(sshKeyPath),
KeyData: to.StringPtr(sshPublicKey),
},
},
},
},
},
StorageProfile: &compute.StorageProfile{
ImageReference: &compute.ImageReference{
Publisher: to.StringPtr(img.publisher),
Offer: to.StringPtr(img.offer),
Sku: to.StringPtr(img.sku),
Version: to.StringPtr(img.version),
},
OsDisk: &compute.OSDisk{
Name: to.StringPtr(fmt.Sprintf(fmtOSDiskResourceName, name)),
Caching: compute.ReadWrite,
CreateOption: compute.FromImage,
Vhd: &compute.VirtualHardDisk{
URI: to.StringPtr(osDiskBlobURL),
},
},
},
},
})
return err
}
func (a AzureClient) GetVirtualMachinePowerState(resourceGroup, name string) (VMPowerState, error) {
log.Debug("Querying instance view for power state.")
vm, err := a.virtualMachinesClient().Get(resourceGroup, name, "instanceView")
if err != nil {
log.Errorf("Error querying instance view: %v", err)
return Unknown, err
}
return powerStateFromInstanceView(vm.Properties.InstanceView), nil
}
func (a AzureClient) GetAvailabilitySet(resourceGroup, name string) (compute.AvailabilitySet, error) {
return a.availabilitySetsClient().Get(resourceGroup, name)
}
func (a AzureClient) CreateAvailabilitySetIfNotExists(ctx *DeploymentContext, resourceGroup, name, location string) error {
f := logutil.Fields{"name": name}
if ctx.AvailabilitySetID != "" {
log.Info("Availability Set already exists.", f)
return nil
}
log.Debug("Could not find existing availability set.", f)
log.Info("Creating availability set...", f)
as, err := a.availabilitySetsClient().CreateOrUpdate(resourceGroup, name,
compute.AvailabilitySet{
Location: to.StringPtr(location),
})
ctx.AvailabilitySetID = to.String(as.ID)
return err
}
// CleanupAvailabilitySetIfExists removes an availability set if there are no
// virtual machines attached to it. Note that this method is not safe for
// multiple concurrent writers, in case of races, deployment of a machine could
// fail or resource might not be cleaned up.
func (a AzureClient) CleanupAvailabilitySetIfExists(resourceGroup, name string) error {
return a.cleanupResourceIfExists(&avSetCleanup{rg: resourceGroup, name: name})
}
// GetPublicIPAddress attempts to get public IP address from the Public IP
// resource. If IP address is not allocated yet, returns empty string.
func (a AzureClient) GetPublicIPAddress(resourceGroup, name string) (string, error) {
f := logutil.Fields{"name": name}
log.Debug("Querying public IP address.", f)
ip, err := a.publicIPAddressClient().Get(resourceGroup, name, "")
if err != nil {
return "", err
}
if ip.Properties == nil {
log.Debug("publicIP.Properties is nil. Could not determine IP address", f)
return "", nil
}
return to.String(ip.Properties.IPAddress), nil
}
// GetPrivateIPAddress attempts to retrieve private IP address of the specified
// network interface name. If IP address is not allocated yet, returns empty
// string.
func (a AzureClient) GetPrivateIPAddress(resourceGroup, name string) (string, error) {
f := logutil.Fields{"name": name}
log.Debug("Querying network interface.", f)
nic, err := a.networkInterfacesClient().Get(resourceGroup, name, "")
if err != nil {
return "", err
}
if nic.Properties == nil || nic.Properties.IPConfigurations == nil ||
len(*nic.Properties.IPConfigurations) == 0 {
log.Debug("No IPConfigurations found on NIC", f)
return "", nil
}
return to.String((*nic.Properties.IPConfigurations)[0].Properties.PrivateIPAddress), nil
}
// StartVirtualMachine starts the virtual machine and waits until it reaches
// the goal state (running) or times out.
func (a AzureClient) StartVirtualMachine(resourceGroup, name string) error {
log.Info("Starting virtual machine.", logutil.Fields{"vm": name})
if _, err := a.virtualMachinesClient().Start(resourceGroup, name); err != nil {
return err
}
return a.waitVMPowerState(resourceGroup, name, Running, waitStartTimeout)
}
// StopVirtualMachine power offs the virtual machine and waits until it reaches
// the goal state (stopped) or times out.
func (a AzureClient) StopVirtualMachine(resourceGroup, name string) error {
log.Info("Stopping virtual machine.", logutil.Fields{"vm": name})
if _, err := a.virtualMachinesClient().PowerOff(resourceGroup, name); err != nil {
return err
}
return a.waitVMPowerState(resourceGroup, name, Stopped, waitPowerOffTimeout)
}
// RestartVirtualMachine restarts the virtual machine and waits until it reaches
// the goal state (stopped) or times out.
func (a AzureClient) RestartVirtualMachine(resourceGroup, name string) error {
log.Info("Restarting virtual machine.", logutil.Fields{"vm": name})
if _, err := a.virtualMachinesClient().Restart(resourceGroup, name); err != nil {
return err
}
return a.waitVMPowerState(resourceGroup, name, Running, waitStartTimeout)
}
// deleteResourceIfExists is an utility method to determine if a resource exists
// from the error returned from its Get response. If so, deletes it. name is
// used only for logging purposes.
func deleteResourceIfExists(resourceType, name string, getFunc func() error, deleteFunc func() (autorest.Response, error)) error {
f := logutil.Fields{"name": name}
log.Debug(fmt.Sprintf("Querying if %s exists.", resourceType), f)
if exists, err := checkResourceExistsFromError(getFunc()); err != nil {
return err
} else if !exists {
log.Info(fmt.Sprintf("%s does not exist. Skipping.", resourceType), f)
return nil
}
log.Info(fmt.Sprintf("Removing %s resource...", resourceType), f)
_, err := deleteFunc()
return err
}
// waitVMPowerState polls the Virtual Machine instance view until it reaches the
// specified goal power state or times out. If checking for virtual machine
// state fails or waiting times out, an error is returned.
func (a AzureClient) waitVMPowerState(resourceGroup, name string, goalState VMPowerState, timeout time.Duration) error {
// NOTE(ahmetalpbalkan): Azure APIs for Start and Stop are actually async
// operations on which our SDK blocks and does polling until the operation
// is complete.
//
// By the time the issued power cycle operation is complete, the VM will be
// already in the goal PowerState. Hence, this method will return in the
// first check, however there is no harm in being defensive.
log.Debug("Waiting until VM reaches goal power state.", logutil.Fields{
"vm": name,
"goalState": goalState,
"timeout": timeout,
})
chErr := make(chan error)
go func(ch chan error) {
for {
select {
case <-ch:
// channel closed
return
default:
state, err := a.GetVirtualMachinePowerState(resourceGroup, name)
if err != nil {
ch <- err
return
}
if state != goalState {
log.Debug(fmt.Sprintf("Waiting %v...", powerStatePollingInterval),
logutil.Fields{
"goalState": goalState,
"state": state,
})
time.Sleep(powerStatePollingInterval)
} else {
log.Debug("Reached goal power state.",
logutil.Fields{"state": state})
ch <- nil
return
}
}
}
}(chErr)
select {
case <-time.After(timeout):
close(chErr)
return fmt.Errorf("Waiting for goal state %q timed out after %v", goalState, timeout)
case err := <-chErr:
return err
}
}
// checkExistsFromError inspects an error and returns a true if err is nil,
// false if error is an autorest.Error with StatusCode=404 and will return the
// error back if error is another status code or another type of error.
func checkResourceExistsFromError(err error) (bool, error) {
if err == nil {
return true, nil
}
v, ok := err.(autorest.DetailedError)
if ok && v.StatusCode == http.StatusNotFound {
return false, nil
}
return false, v
}
// osDiskStorageBlobURL gives the full url of the VHD blob where the OS disk for
// the given VM should be stored.
func osDiskStorageBlobURL(account *storage.AccountProperties, vmName string) string {
containerURL := osDiskStorageContainerURL(account, vmName) // has trailing slash
blobName := fmt.Sprintf(fmtOSDiskBlobName, vmName)
return containerURL + blobName
}
// osDiskStorageContainerName returns the container name the OS disk for the VM
// should be saved.
func osDiskStorageContainerName(vm string) string { return fmt.Sprintf(fmtOSDiskContainer, vm) }
// osDiskStorageContainerURL crafts a URL with a trailing slash pointing
// to the full Azure Blob Container URL for given VM name.
func osDiskStorageContainerURL(account *storage.AccountProperties, vmName string) string {
return fmt.Sprintf("%s%s/", to.String(account.PrimaryEndpoints.Blob), osDiskStorageContainerName(vmName))
}
// extractStorageAccountFromVHDURL parses a blob URL and extracts the Azure
// Storage account name from the URL, namely first subdomain of the hostname and
// the Azure Storage service base URL (e.g. core.windows.net). If it could not
// be parsed, returns empty string.
func extractStorageAccountFromVHDURL(vhdURL string) (string, string) {
u, err := url.Parse(vhdURL)
if err != nil {
log.Warn(fmt.Sprintf("URL parse error: %v", err), logutil.Fields{"url": vhdURL})
return "", ""
}
parts := strings.SplitN(u.Host, ".", 2)
if len(parts) != 2 {
log.Warnf("Could not split account name and storage base URL: %s", vhdURL)
return "", ""
}
return parts[0], strings.TrimPrefix(parts[1], "blob.") // "blob." prefix will added by azure storage sdk
}

View File

@ -0,0 +1,126 @@
package azureutil
import (
"fmt"
"github.com/Azure/azure-sdk-for-go/arm/compute"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/docker/machine/drivers/azure/logutil"
"github.com/docker/machine/libmachine/log"
)
type cleanupResource interface {
// Get retrieves if the resource and saves its reference to the instance
// for further using, returned error is used to determine if the resource
// exists
Get(a AzureClient) error
// Delete deletes the resource
Delete(a AzureClient) error
// HasAttachedResources checks the resource reference if it has dependent
// resources attached to it preventing it from being deleted.
HasAttachedResources() bool
// ResourceType returns human-readable name of the type of the resource.
ResourceType() string
// LogFields returns the logging fields used during cleanup logging.
LogFields() logutil.Fields
}
// cleanupResourceIfExists checks if the resource exists, if it does and it
// does not have any attached resources, then deletes the resource. If the
// resource does not exist or is not eligible for cleanup, returns nil. If an
// error is encountered, returns the error.
func (a AzureClient) cleanupResourceIfExists(r cleanupResource) error {
f := r.LogFields()
log.Info(fmt.Sprintf("Attempting to clean up %s resource...", r.ResourceType()), f)
err := r.Get(a)
if exists, err := checkResourceExistsFromError(err); err != nil {
return err
} else if !exists {
log.Debug(fmt.Sprintf("%s resource does not exist. Skipping.", r.ResourceType()), f)
return nil
}
if !r.HasAttachedResources() {
log.Debug(fmt.Sprintf("%s does not have any attached dependent resource.", r.ResourceType()), f)
log.Info(fmt.Sprintf("Removing %s resource...", r.ResourceType()), f)
return r.Delete(a)
}
log.Info(fmt.Sprintf("%s is still in use by other resources, skipping removal.", r.ResourceType()), f)
return nil
}
// subnetCleanup manages cleanup of Subnet resources
type subnetCleanup struct {
rg, vnet, name string
ref network.Subnet
}
func (c *subnetCleanup) Get(a AzureClient) (err error) {
c.ref, err = a.subnetsClient().Get(c.rg, c.vnet, c.name, "")
return err
}
func (c *subnetCleanup) Delete(a AzureClient) error {
_, err := a.subnetsClient().Delete(c.rg, c.vnet, c.name)
return err
}
func (c *subnetCleanup) ResourceType() string { return "Subnet" }
func (c *subnetCleanup) LogFields() logutil.Fields { return logutil.Fields{"name": c.name} }
func (c *subnetCleanup) HasAttachedResources() bool {
return c.ref.Properties.IPConfigurations != nil && len(*c.ref.Properties.IPConfigurations) > 0
}
// vnetCleanup manages cleanup of Virtual Network resources.
type vnetCleanup struct {
rg, name string
ref network.VirtualNetwork
}
func (c *vnetCleanup) Get(a AzureClient) (err error) {
c.ref, err = a.virtualNetworksClient().Get(c.rg, c.name, "")
return err
}
func (c *vnetCleanup) Delete(a AzureClient) error {
_, err := a.virtualNetworksClient().Delete(c.rg, c.name)
return err
}
func (c *vnetCleanup) ResourceType() string { return "Virtual Network" }
func (c *vnetCleanup) LogFields() logutil.Fields { return logutil.Fields{"name": c.name} }
func (c *vnetCleanup) HasAttachedResources() bool {
return c.ref.Properties.Subnets != nil && len(*c.ref.Properties.Subnets) > 0
}
// avSetCleanup manages cleanup of Availability Set resources.
type avSetCleanup struct {
rg, name string
ref compute.AvailabilitySet
}
func (c *avSetCleanup) Get(a AzureClient) (err error) {
c.ref, err = a.availabilitySetsClient().Get(c.rg, c.name)
return err
}
func (c *avSetCleanup) Delete(a AzureClient) error {
_, err := a.availabilitySetsClient().Delete(c.rg, c.name)
return err
}
func (c *avSetCleanup) ResourceType() string { return "Availability Set" }
func (c *avSetCleanup) LogFields() logutil.Fields { return logutil.Fields{"name": c.name} }
func (c *avSetCleanup) HasAttachedResources() bool {
return c.ref.Properties.VirtualMachines != nil && len(*c.ref.Properties.VirtualMachines) > 0
}

View File

@ -0,0 +1,125 @@
package azureutil
import (
"fmt"
"github.com/docker/machine/version"
"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"
"github.com/Azure/azure-sdk-for-go/arm/resources/subscriptions"
"github.com/Azure/azure-sdk-for-go/arm/storage"
"github.com/Azure/go-autorest/autorest"
)
// TODO(ahmetalpbalkan) Remove duplication around client creation. This is
// happening because we auto-generate our SDK and we don't have generics in Go.
// We are hoping to come up with a factory or some defaults instance to set
// these client configuration in a central place in azure-sdk-for-go.
func oauthClient() autorest.Client {
c := autorest.NewClientWithUserAgent(fmt.Sprintf("docker-machine/%s", version.Version))
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
// TODO set user agent
return c
}
func subscriptionsClient(baseURI string) subscriptions.Client {
c := subscriptions.NewClientWithBaseURI(baseURI, "") // used only for unauthenticated requests for generic subs IDs
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) providersClient() resources.ProvidersClient {
c := resources.NewProvidersClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) resourceGroupsClient() resources.GroupsClient {
c := resources.NewGroupsClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) securityGroupsClient() network.SecurityGroupsClient {
c := network.NewSecurityGroupsClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) virtualNetworksClient() network.VirtualNetworksClient {
c := network.NewVirtualNetworksClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) subnetsClient() network.SubnetsClient {
c := network.NewSubnetsClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) networkInterfacesClient() network.InterfacesClient {
c := network.NewInterfacesClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) publicIPAddressClient() network.PublicIPAddressesClient {
c := network.NewPublicIPAddressesClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) storageAccountsClient() storage.AccountsClient {
c := storage.NewAccountsClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) virtualMachinesClient() compute.VirtualMachinesClient {
c := compute.NewVirtualMachinesClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}
func (a AzureClient) availabilitySetsClient() compute.AvailabilitySetsClient {
c := compute.NewAvailabilitySetsClientWithBaseURI(a.env.ResourceManagerEndpoint, a.subscriptionID)
c.Authorizer = a.auth
c.Client.UserAgent += fmt.Sprintf(";docker-machine/%s", version.Version)
c.RequestInspector = withInspection()
c.ResponseInspector = byInspecting()
return c
}

View File

@ -0,0 +1,20 @@
package azureutil
import (
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/Azure/azure-sdk-for-go/arm/storage"
)
// DeploymentContext contains references to various sources created and then
// used in creating other resources.
type DeploymentContext struct {
VirtualNetworkExists bool
StorageAccount *storage.AccountProperties
PublicIPAddressID string
NetworkSecurityGroupID string
SubnetID string
NetworkInterfaceID string
SSHPublicKey string
AvailabilitySetID string
FirewallRules *[]network.SecurityRule
}

View File

@ -0,0 +1,36 @@
package azureutil
import (
"net/http"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/docker/machine/drivers/azure/logutil"
"github.com/docker/machine/libmachine/log"
)
func withInspection() autorest.PrepareDecorator {
return func(p autorest.Preparer) autorest.Preparer {
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
log.Debug("Azure request", logutil.Fields{
"method": r.Method,
"request": r.URL.String(),
})
return p.Prepare(r)
})
}
}
func byInspecting() autorest.RespondDecorator {
return func(r autorest.Responder) autorest.Responder {
return autorest.ResponderFunc(func(resp *http.Response) error {
log.Debug("Azure response", logutil.Fields{
"status": resp.Status,
"method": resp.Request.Method,
"request": resp.Request.URL.String(),
"x-ms-request-id": azure.ExtractRequestID(resp),
})
return r.Respond(resp)
})
}
}

View File

@ -0,0 +1,21 @@
package azureutil
import (
"fmt"
)
const (
fmtNIC = "%s-nic"
fmtIP = "%s-ip"
fmtNSG = "%s-firewall"
fmtVM = "%s"
)
// ResourceNaming provides methods to construct Azure resource names for a given
// machine name.
type ResourceNaming string
func (r ResourceNaming) IP() string { return fmt.Sprintf(fmtIP, r) }
func (r ResourceNaming) NIC() string { return fmt.Sprintf(fmtNIC, r) }
func (r ResourceNaming) NSG() string { return fmt.Sprintf(fmtNSG, r) }
func (r ResourceNaming) VM() string { return fmt.Sprintf(fmtVM, r) }

View File

@ -0,0 +1,93 @@
package azureutil
import (
"strings"
"github.com/docker/machine/libmachine/log"
"github.com/Azure/azure-sdk-for-go/arm/compute"
"github.com/Azure/go-autorest/autorest/to"
)
type VMPowerState string
const (
// Unknown is returned when Azure does not provide a PowerState (happens
// when VM is just deployed or started transitioning to another state) or
// obtained PowerState is not one of the following.
Unknown VMPowerState = ""
// Stopped indicates that VM is allocated and in powered off state or the VM
// has been just deployed for the first time. In this state, VM can be powered
// on or
Stopped VMPowerState = "stopped"
// Stopping indicates that VM is about to go into powered off state.
Stopping VMPowerState = "stopping"
// Starting indicates that VM is being created or powered on.
Starting VMPowerState = "starting"
// Running indicates that VM is either powered on or being rebooted. VM
// stays in this state during the reboot operation. In this state VM can be
// stopped, restarted or deallocated.
Running VMPowerState = "running"
// Deallocating indicates that the VM is being terminated.
Deallocating VMPowerState = "deallocating"
// Deallocated indicates that the VM is being terminated. In this state, VM
// can be powered on or powered off.
Deallocated VMPowerState = "deallocated"
)
const (
powerStateCodePrefix = "PowerState/"
)
// powerStateFromInstanceView reads the instance view response and extracts the
// power state status (if exists) from there. If no status is found or an
// unknown status has occured, returns Unknown.
func powerStateFromInstanceView(instanceView *compute.VirtualMachineInstanceView) VMPowerState {
if instanceView == nil {
log.Debug("Retrieved nil instance view.")
return Unknown
} else if instanceView.Statuses == nil || len(*instanceView.Statuses) == 0 {
log.Debug("Retrieved nil or empty instanceView.statuses.")
return Unknown
}
statuses := *instanceView.Statuses
// Filter statuses whose "code" starts with "PowerState/"
var s *compute.InstanceViewStatus
for _, v := range statuses {
log.Debugf("Matching pattern for code=%q", to.String(v.Code))
if strings.HasPrefix(to.String(v.Code), powerStateCodePrefix) {
log.Debug("Power state found.")
s = &v
break
}
}
if s == nil {
log.Debug("No PowerState found in the instance view statuses.")
return Unknown
}
code := strings.TrimPrefix(to.String(s.Code), powerStateCodePrefix)
switch code {
case "stopped":
return Stopped
case "stopping":
return Stopping
case "starting":
return Starting
case "running":
return Running
case "deallocated":
return Deallocated
case "deallocating":
return Deallocating
default:
log.Warn("Encountered unknown PowerState for virtual machine: %q", code)
return Unknown
}
}

View File

@ -0,0 +1,43 @@
package azureutil
import (
"fmt"
"net/http"
"regexp"
"github.com/Azure/go-autorest/autorest/azure"
)
// findTenantID figures out the AAD tenant ID of the subscription by making an
// unauthenticated request to the Get Subscription Details endpoint and parses
// the value from WWW-Authenticate header.
func findTenantID(env azure.Environment, subscriptionID string) (string, error) {
const hdrKey = "WWW-Authenticate"
c := subscriptionsClient(env.ResourceManagerEndpoint)
// we expect this request to fail (err != nil), but we are only interested
// in headers, so surface the error if the Response is not present (i.e.
// network error etc)
subs, err := c.Get(subscriptionID)
if subs.Response.Response == nil {
return "", fmt.Errorf("Request failed: %v", err)
}
// Expecting 401 StatusUnauthorized here, just read the header
if subs.StatusCode != http.StatusUnauthorized {
return "", fmt.Errorf("Unexpected response from Get Subscription: %v", err)
}
hdr := subs.Header.Get(hdrKey)
if hdr == "" {
return "", fmt.Errorf("Header %v not found in Get Subscription response", hdrKey)
}
// Example value for hdr:
// Bearer authorization_uri="https://login.windows.net/996fe9d1-6171-40aa-945b-4c64b63bf655", error="invalid_token", error_description="The authentication failed because of missing 'Authorization' header."
r := regexp.MustCompile(`authorization_uri=".*/([0-9a-f\-]+)"`)
m := r.FindStringSubmatch(hdr)
if m == nil {
return "", fmt.Errorf("Could not find the tenant ID in header: %s %q", hdrKey, hdr)
}
return m[1], nil
}

View File

@ -0,0 +1,45 @@
package azureutil
import (
"fmt"
"math/rand"
"strings"
"time"
)
/* Utilities */
// randomAzureStorageAccountName generates a valid storage account name prefixed
// with a predefined string. Availability of the name is not checked. Uses maximum
// length to maximise randomness.
func randomAzureStorageAccountName() string {
const (
maxLen = 24
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
)
return storageAccountPrefix + randomString(maxLen-len(storageAccountPrefix), chars)
}
// randomString generates a random string of given length using specified alphabet.
func randomString(n int, alphabet string) string {
r := timeSeed()
b := make([]byte, n)
for i := range b {
b[i] = alphabet[r.Intn(len(alphabet))]
}
return string(b)
}
// imageName holds various components of an OS image name identifier
type imageName struct{ publisher, offer, sku, version string }
// parseImageName parses a publisher:offer:sku:version into those parts.
func parseImageName(image string) (imageName, error) {
l := strings.Split(image, ":")
if len(l) != 4 {
return imageName{}, fmt.Errorf("Image name %q not a valid format.", image)
}
return imageName{l[0], l[1], l[2], l[3]}, nil
}
func timeSeed() *rand.Rand { return rand.New(rand.NewSource(time.Now().UTC().UnixNano())) }

View File

@ -0,0 +1,16 @@
package logutil
import "fmt"
type Fields map[string]interface{}
func (f Fields) String() string {
var s string
for k, v := range f {
if sv, ok := v.(string); ok {
v = fmt.Sprintf("%q", sv)
}
s += fmt.Sprintf(" %s=%v", k, v)
}
return s
}

175
drivers/azure/util.go Normal file
View File

@ -0,0 +1,175 @@
package azure
import (
"fmt"
"io/ioutil"
"net"
"net/url"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/docker/machine/drivers/azure/azureutil"
"github.com/docker/machine/drivers/azure/logutil"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/ssh"
"github.com/docker/machine/libmachine/state"
)
var (
environments = map[string]azure.Environment{
azure.PublicCloud.Name: azure.PublicCloud,
azure.USGovernmentCloud.Name: azure.USGovernmentCloud,
azure.ChinaCloud.Name: azure.ChinaCloud,
}
)
// requiredOptionError forms an error from the error indicating the option has
// to be provided with a value for this driver.
type requiredOptionError string
func (r requiredOptionError) Error() string {
return fmt.Sprintf("%s driver requires the %q option.", driverName, string(r))
}
// newAzureClient creates an AzureClient helper from the Driver context and
// initiates authentication if required.
func (d *Driver) newAzureClient() (*azureutil.AzureClient, error) {
env, ok := environments[d.Environment]
if !ok {
return nil, fmt.Errorf("Invalid Azure environment: %q", d.Environment)
}
servicePrincipalToken, err := azureutil.Authenticate(env, d.SubscriptionID)
if err != nil {
return nil, fmt.Errorf("Error creating Azure client: %v", err)
}
return azureutil.New(env, d.SubscriptionID, servicePrincipalToken), nil
}
// generateSSHKey creates a ssh key pair locally and saves the public key file
// contents in OpenSSH format to the DeploymentContext.
func (d *Driver) generateSSHKey(ctx *azureutil.DeploymentContext) error {
privPath := d.GetSSHKeyPath()
pubPath := privPath + ".pub"
log.Debug("Creating SSH key...", logutil.Fields{
"pub": pubPath,
"priv": privPath,
})
if err := ssh.GenerateSSHKey(privPath); err != nil {
return err
}
log.Debug("SSH key pair generated.")
publicKey, err := ioutil.ReadFile(pubPath)
ctx.SSHPublicKey = string(publicKey)
return err
}
// getSecurityRules creates network security group rules based on driver
// configuration such as SSH port, docker port and swarm port.
func (d *Driver) getSecurityRules(extraPorts []string) (*[]network.SecurityRule, error) {
mkRule := func(priority int, name, description, srcPort, dstPort string) network.SecurityRule {
return network.SecurityRule{
Name: to.StringPtr(name),
Properties: &network.SecurityRulePropertiesFormat{
Description: to.StringPtr(description),
SourceAddressPrefix: to.StringPtr("*"),
DestinationAddressPrefix: to.StringPtr("*"),
SourcePortRange: to.StringPtr(srcPort),
DestinationPortRange: to.StringPtr(dstPort),
Access: network.Allow,
Direction: network.Inbound,
Protocol: network.TCP,
Priority: to.Int32Ptr(int32(priority)),
},
}
}
log.Debugf("Docker port is configured as %d", d.DockerPort)
// Base ports to be opened for any machine
rl := []network.SecurityRule{
mkRule(100, "SSHAllowAny", "Allow ssh from public Internet", "*", fmt.Sprintf("%d", d.BaseDriver.SSHPort)),
mkRule(300, "DockerAllowAny", "Allow docker engine access (TLS-protected)", "*", fmt.Sprintf("%d", d.DockerPort)),
}
// Open swarm port if configured
if d.BaseDriver.SwarmMaster {
swarmHost := d.BaseDriver.SwarmHost
log.Debugf("Swarm host is configured as %q", swarmHost)
u, err := url.Parse(swarmHost)
if err != nil {
return nil, fmt.Errorf("Cannot parse URL %q: %v", swarmHost, err)
}
_, swarmPort, err := net.SplitHostPort(u.Host)
if err != nil {
return nil, fmt.Errorf("Could not parse swarm port in %q: %v", u.Host, err)
}
rl = append(rl, mkRule(500, "DockerSwarmAllowAny", "Allow swarm manager access (TLS-protected)", "*", swarmPort))
} else {
log.Debug("Swarm host is not configured.")
}
// extra port numbers requested by user
basePri := 1000
for i, port := range extraPorts {
log.Debugf("User-requested port number to be opened on NSG: %v", port)
r := mkRule(basePri+i, fmt.Sprintf("Port%sAllowAny", port), "User requested port to be accessible from Internet via docker-machine", "*", port)
rl = append(rl, r)
}
log.Debugf("Total NSG rules: %d", len(rl))
return &rl, nil
}
func (d *Driver) naming() azureutil.ResourceNaming {
return azureutil.ResourceNaming(d.BaseDriver.MachineName)
}
// ipAddress returns machines private or public IP address according to the
// configuration. If no IP address is found it returns empty string.
func (d *Driver) ipAddress() (ip string, err error) {
c, err := d.newAzureClient()
if err != nil {
return "", err
}
var ipType string
if d.UsePrivateIP || d.NoPublicIP {
ipType = "Private"
ip, err = c.GetPrivateIPAddress(d.ResourceGroup, d.naming().NIC())
} else {
ipType = "Public"
ip, err = c.GetPublicIPAddress(d.ResourceGroup, d.naming().IP())
}
log.Debugf("Retrieving %s IP address...", ipType)
if err != nil {
return "", fmt.Errorf("Error querying %s IP: %v", ipType, err)
}
if ip == "" {
log.Debugf("%s IP address is not yet allocated.", ipType)
}
return ip, nil
}
func machineStateForVMPowerState(ps azureutil.VMPowerState) state.State {
m := map[azureutil.VMPowerState]state.State{
azureutil.Running: state.Running,
azureutil.Starting: state.Starting,
azureutil.Stopping: state.Stopping,
azureutil.Stopped: state.Stopped,
azureutil.Deallocating: state.Stopping,
azureutil.Deallocated: state.Stopped,
azureutil.Unknown: state.None,
}
if v, ok := m[ps]; ok {
return v
}
log.Warnf("Azure PowerState %q does not map to a docker-machine state.", ps)
return state.None
}

View File

@ -17,7 +17,7 @@ var (
// plugin server. // plugin server.
defaultTimeout = 10 * time.Second defaultTimeout = 10 * time.Second
CurrentBinaryIsDockerMachine = false CurrentBinaryIsDockerMachine = false
CoreDrivers = [...]string{"amazonec2", "digitalocean", CoreDrivers = [...]string{"amazonec2", "azure", "digitalocean",
"exoscale", "generic", "google", "hyperv", "none", "openstack", "exoscale", "generic", "google", "hyperv", "none", "openstack",
"rackspace", "softlayer", "virtualbox", "vmwarefusion", "rackspace", "softlayer", "virtualbox", "vmwarefusion",
"vmwarevcloudair", "vmwarevsphere"} "vmwarevcloudair", "vmwarevsphere"}