mirror of https://github.com/docker/docs.git
Merge pull request #3159 from ahmetalpbalkan/azure-arm
New Microsoft Azure docker-machine driver
This commit is contained in:
commit
ef4823f2ac
|
@ -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":
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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/
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 user’s 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 SDK’s 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
|
||||||
|
}
|
|
@ -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)))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) }
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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())) }
|
|
@ -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
|
||||||
|
}
|
|
@ -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 machine’s 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
|
||||||
|
}
|
|
@ -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"}
|
||||||
|
|
Loading…
Reference in New Issue