From ed95f3baab4b44a7aca70599fab7ad3d474d5604 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Wed, 27 Jan 2016 19:30:01 -0800 Subject: [PATCH 1/7] New Microsoft Azure docker-machine driver The new driver uses Azure Resource Manager APIs and offers a lot more functionality compared to the old Azure driver. It is also easier to authenticate and does not require user to create and place certificate files. It only has a single required argument. This is a breaking change: The new driver cannot work with machines created with the older Azure driver and vice versa (as the APIs are entirely different and resources are not shared between old/new azure APIs). The new driver addresses many issues about the azure driver reported so far. This resolves #2742, resolves #1368, resolves #1142, resolves #2236, resolves #2408, resolves #1126, resolves #774. Signed-off-by: Ahmet Alp Balkan --- cmd/machine.go | 3 + docs/drivers/azure.md | 120 ++- drivers/azure/azure.go | 464 +++++++++++ drivers/azure/azureutil/auth.go | 207 +++++ drivers/azure/azureutil/authorizer.go | 15 + drivers/azure/azureutil/azureutil.go | 749 ++++++++++++++++++ drivers/azure/azureutil/cleanup.go | 126 +++ drivers/azure/azureutil/clients.go | 125 +++ drivers/azure/azureutil/context.go | 20 + drivers/azure/azureutil/inspector.go | 36 + drivers/azure/azureutil/naming.go | 21 + drivers/azure/azureutil/powerstate.go | 93 +++ drivers/azure/azureutil/tenantid.go | 43 + drivers/azure/azureutil/util.go | 45 ++ drivers/azure/logutil/logfields.go | 16 + drivers/azure/util.go | 175 ++++ .../drivers/plugin/localbinary/plugin.go | 2 +- 17 files changed, 2226 insertions(+), 34 deletions(-) create mode 100644 drivers/azure/azure.go create mode 100644 drivers/azure/azureutil/auth.go create mode 100644 drivers/azure/azureutil/authorizer.go create mode 100644 drivers/azure/azureutil/azureutil.go create mode 100644 drivers/azure/azureutil/cleanup.go create mode 100644 drivers/azure/azureutil/clients.go create mode 100644 drivers/azure/azureutil/context.go create mode 100644 drivers/azure/azureutil/inspector.go create mode 100644 drivers/azure/azureutil/naming.go create mode 100644 drivers/azure/azureutil/powerstate.go create mode 100644 drivers/azure/azureutil/tenantid.go create mode 100644 drivers/azure/azureutil/util.go create mode 100644 drivers/azure/logutil/logfields.go create mode 100644 drivers/azure/util.go diff --git a/cmd/machine.go b/cmd/machine.go index 7b4fec2969..fe443a16a0 100644 --- a/cmd/machine.go +++ b/cmd/machine.go @@ -11,6 +11,7 @@ import ( "github.com/docker/machine/commands" "github.com/docker/machine/commands/mcndirs" "github.com/docker/machine/drivers/amazonec2" + "github.com/docker/machine/drivers/azure" "github.com/docker/machine/drivers/digitalocean" "github.com/docker/machine/drivers/exoscale" "github.com/docker/machine/drivers/generic" @@ -168,6 +169,8 @@ func runDriver(driverName string) { switch driverName { case "amazonec2": plugin.RegisterDriver(amazonec2.NewDriver("", "")) + case "azure": + plugin.RegisterDriver(azure.NewDriver("", "")) case "digitalocean": plugin.RegisterDriver(digitalocean.NewDriver("", "")) case "exoscale": diff --git a/docs/drivers/azure.md b/docs/drivers/azure.md index 8548ea234a..85ba965edc 100644 --- a/docs/drivers/azure.md +++ b/docs/drivers/azure.md @@ -10,51 +10,105 @@ parent="smn_machine_drivers" # 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: +> **NOTE:** This documentation is for the new version of Azure driver started +> shipping with v0.7.0 and it is not backwards-compatible with the old Azure +> driver. If you like to manage your existing Azure machines, please download +> and use machine versions prior to v0.7.0. - $ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem - $ openssl pkcs12 -export -out mycert.pfx -in mycert.pem -name "My Certificate" - $ openssl x509 -inform pem -in mycert.pem -outform der -out mycert.cer +[azure]: http://azure.microsoft.com/ +[trial]: https://azure.microsoft.com/free/ -Go to the Azure portal, go to the "Settings" page (you can find the link at the bottom of the -left sidebar - you need to scroll), then "Management Certificates" and upload `mycert.cer`. +## Authentication -Grab your subscription ID from the portal, then run `docker-machine create` with these details: +First time you try to create a machine, Azure driver will ask you to +authenticate: - $ docker-machine create -d azure --azure-subscription-id="SUB_ID" --azure-subscription-cert="mycert.pem" A-VERY-UNIQUE-NAME + $ docker-machine create --driver azure + Running pre-create checks... + Microsoft Azure: To sign in, use a web browser to open the page https://aka.ms/devicelogin. + Enter the code [...] to authenticate. -The Azure driver uses the `b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-15_10-amd64-server-20151116.1-en-us-30GB` -image by default. Note, this image is not available in the Chinese regions. In China you should - specify `b549f4301d0b4295b8e76ceb65df47d4__Ubuntu-15_10-amd64-server-20151116.1-en-us-30GB`. +After authenticating, the driver will remember your credentials up to two weeks. -You may need to `machine ssh` in to the virtual machine and reboot to ensure that the OS is updated. +## Options -Options: +Azure driver only has a single required argument to make things easier. Please +read the optional flags to configure machine details and placement further. -- `--azure-docker-port`: Port for Docker daemon. -- `--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) -- `--azure-location`: Machine instance location. -- `--azure-password`: Your Azure password. -- `--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-ssh-port`: Azure SSH port. -- `--azure-subscription-id`: **required** Your Azure subscription ID (A GUID like `d255d8d7-5af0-4f5c-8a3e-1545044b861e`). -- `--azure-subscription-cert`: **required** Your Azure subscription cert. -- `--azure-username`: Azure login user name. +Required: + +- `--azure-subscription-id`: **(required)** Your Azure Subscription ID. + +Optional: + +- `--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: | 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-username` | - | `ubuntu` | +| `--azure-environment` | `AZURE_ENVIRONMENT` | `AzurePublicCloud` | +| `--azure-image` | `AZURE_IMAGE` | `canonical:UbuntuServer:14.04.3-LTS: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` | `ubuntu` | +| `--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/ diff --git a/drivers/azure/azure.go b/drivers/azure/azure.go new file mode 100644 index 0000000000..9b675a7c4e --- /dev/null +++ b/drivers/azure/azure.go @@ -0,0 +1,464 @@ +package azure + +import ( + "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 = "ubuntu" // 'root' not allowed on Azure + defaultDockerPort = 2376 + defaultAzureImage = "canonical:UbuntuServer:14.04.3-LTS: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", d.naming().VM()) + } + + // 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 { + // 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 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) { + 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 { + 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 { + 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 { + // 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() +} diff --git a/drivers/azure/azureutil/auth.go b/drivers/azure/azureutil/auth.go new file mode 100644 index 0000000000..285e33cb94 --- /dev/null +++ b/drivers/azure/azureutil/auth.go @@ -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 +} diff --git a/drivers/azure/azureutil/authorizer.go b/drivers/azure/azureutil/authorizer.go new file mode 100644 index 0000000000..8ea1a354eb --- /dev/null +++ b/drivers/azure/azureutil/authorizer.go @@ -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))) +} diff --git a/drivers/azure/azureutil/azureutil.go b/drivers/azure/azureutil/azureutil.go new file mode 100644 index 0000000000..9c7e16e436 --- /dev/null +++ b/drivers/azure/azureutil/azureutil.go @@ -0,0 +1,749 @@ +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. +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[to.String(p.Namespace)] = to.String(p.RegistrationState) == "Registered" + } + + for _, ns := range namespaces { + registered, ok := m[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 +} diff --git a/drivers/azure/azureutil/cleanup.go b/drivers/azure/azureutil/cleanup.go new file mode 100644 index 0000000000..edb0d31e0c --- /dev/null +++ b/drivers/azure/azureutil/cleanup.go @@ -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 +} diff --git a/drivers/azure/azureutil/clients.go b/drivers/azure/azureutil/clients.go new file mode 100644 index 0000000000..69aeb7e1f1 --- /dev/null +++ b/drivers/azure/azureutil/clients.go @@ -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 +} diff --git a/drivers/azure/azureutil/context.go b/drivers/azure/azureutil/context.go new file mode 100644 index 0000000000..63d54af541 --- /dev/null +++ b/drivers/azure/azureutil/context.go @@ -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 +} diff --git a/drivers/azure/azureutil/inspector.go b/drivers/azure/azureutil/inspector.go new file mode 100644 index 0000000000..d992b69629 --- /dev/null +++ b/drivers/azure/azureutil/inspector.go @@ -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) + }) + } +} diff --git a/drivers/azure/azureutil/naming.go b/drivers/azure/azureutil/naming.go new file mode 100644 index 0000000000..9967fcf87e --- /dev/null +++ b/drivers/azure/azureutil/naming.go @@ -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) } diff --git a/drivers/azure/azureutil/powerstate.go b/drivers/azure/azureutil/powerstate.go new file mode 100644 index 0000000000..b9518e9f1b --- /dev/null +++ b/drivers/azure/azureutil/powerstate.go @@ -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 + } +} diff --git a/drivers/azure/azureutil/tenantid.go b/drivers/azure/azureutil/tenantid.go new file mode 100644 index 0000000000..2ae9722a5c --- /dev/null +++ b/drivers/azure/azureutil/tenantid.go @@ -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 +} diff --git a/drivers/azure/azureutil/util.go b/drivers/azure/azureutil/util.go new file mode 100644 index 0000000000..9d47b769ba --- /dev/null +++ b/drivers/azure/azureutil/util.go @@ -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())) } diff --git a/drivers/azure/logutil/logfields.go b/drivers/azure/logutil/logfields.go new file mode 100644 index 0000000000..b450594de8 --- /dev/null +++ b/drivers/azure/logutil/logfields.go @@ -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 +} diff --git a/drivers/azure/util.go b/drivers/azure/util.go new file mode 100644 index 0000000000..7de6497772 --- /dev/null +++ b/drivers/azure/util.go @@ -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 +} diff --git a/libmachine/drivers/plugin/localbinary/plugin.go b/libmachine/drivers/plugin/localbinary/plugin.go index 3222ab53fb..688633734c 100644 --- a/libmachine/drivers/plugin/localbinary/plugin.go +++ b/libmachine/drivers/plugin/localbinary/plugin.go @@ -17,7 +17,7 @@ var ( // plugin server. defaultTimeout = 10 * time.Second CurrentBinaryIsDockerMachine = false - CoreDrivers = [...]string{"amazonec2", "digitalocean", + CoreDrivers = [...]string{"amazonec2", "azure", "digitalocean", "exoscale", "generic", "google", "hyperv", "none", "openstack", "rackspace", "softlayer", "virtualbox", "vmwarefusion", "vmwarevcloudair", "vmwarevsphere"} From 7b961604543507fb35f76c5307cced4a5c1db9a9 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Wed, 9 Mar 2016 15:43:49 -0800 Subject: [PATCH 2/7] Friendly deprecation notice for old driver VMs Signed-off-by: Ahmet Alp Balkan --- drivers/azure/azure.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/drivers/azure/azure.go b/drivers/azure/azure.go index 9b675a7c4e..b7603b0a7e 100644 --- a/drivers/azure/azure.go +++ b/drivers/azure/azure.go @@ -1,6 +1,7 @@ package azure import ( + "errors" "fmt" "net" "net/url" @@ -329,6 +330,10 @@ func (d *Driver) Create() error { // 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. @@ -367,6 +372,10 @@ func (d *Driver) Remove() error { // 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 { @@ -407,6 +416,10 @@ func (d *Driver) GetURL() (string, error) { // 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 @@ -425,6 +438,10 @@ func (d *Driver) GetState() (state.State, error) { // 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 @@ -434,6 +451,10 @@ func (d *Driver) Start() error { // 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 @@ -445,6 +466,10 @@ func (d *Driver) Stop() error { // 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. @@ -462,3 +487,15 @@ func (d *Driver) Kill() error { 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 +} From ce3cb54482cae16961e979b2f15c807d840cfd3d Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Thu, 10 Mar 2016 15:38:05 -0800 Subject: [PATCH 3/7] Update default image to Ubuntu 15.10 - Default image now is ubuntu server 15.10. This ensures consistency with other cloud drivers. - Better error message when virtual machine already exists. - Change default SSH user from `ubuntu` to `docker-user`. Signed-off-by: Ahmet Alp Balkan --- docs/drivers/azure.md | 4 ++-- drivers/azure/azure.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/drivers/azure.md b/docs/drivers/azure.md index 85ba965edc..da19e36092 100644 --- a/docs/drivers/azure.md +++ b/docs/drivers/azure.md @@ -72,11 +72,11 @@ Environment variables and default values: | ------------------------------- | ----------------------------- | ------------------ | | **`--azure-subscription-id`** | `AZURE_SUBSCRIPTION_ID` | - | | `--azure-environment` | `AZURE_ENVIRONMENT` | `AzurePublicCloud` | -| `--azure-image` | `AZURE_IMAGE` | `canonical:UbuntuServer:14.04.3-LTS:latest` | +| `--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` | `ubuntu` | +| `--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` | diff --git a/drivers/azure/azure.go b/drivers/azure/azure.go index b7603b0a7e..0254684cce 100644 --- a/drivers/azure/azure.go +++ b/drivers/azure/azure.go @@ -20,9 +20,9 @@ const ( defaultAzureResourceGroup = "docker-machine" defaultAzureSize = "Standard_A2" defaultAzureLocation = "westus" - defaultSSHUser = "ubuntu" // 'root' not allowed on Azure + defaultSSHUser = "docker-user" // 'root' not allowed on Azure defaultDockerPort = 2376 - defaultAzureImage = "canonical:UbuntuServer:14.04.3-LTS:latest" + defaultAzureImage = "canonical:UbuntuServer:15.10:latest" defaultAzureVNet = "docker-machine-vnet" defaultAzureSubnet = "docker-machine" defaultAzureSubnetPrefix = "192.168.0.0/16" @@ -264,7 +264,7 @@ func (d *Driver) PreCreateCheck() (err error) { 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", d.naming().VM()) + 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 From ec33a7dfc3c6038b18ae586bde0176e2cd1f72a6 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Thu, 10 Mar 2016 16:59:28 -0800 Subject: [PATCH 4/7] Add RELEASE.md note about deprecation notice Signed-off-by: Ahmet Alp Balkan --- docs/RELEASE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/RELEASE.md b/docs/RELEASE.md index b05517e494..13ad2a650f 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -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 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 token needs only to have the `repo` scope. The token can be created on github in the settings > Personal Access Token menu. From 809104d7ea28678c49122936551a8c388b084862 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Fri, 11 Mar 2016 13:31:03 -0800 Subject: [PATCH 5/7] azure.md: Documentation clarification Signed-off-by: Ahmet Alp Balkan --- docs/drivers/azure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/drivers/azure.md b/docs/drivers/azure.md index da19e36092..6391c9fa14 100644 --- a/docs/drivers/azure.md +++ b/docs/drivers/azure.md @@ -26,7 +26,7 @@ You will need an Azure Subscription to use this Docker Machine driver. First time you try to create a machine, Azure driver will ask you to authenticate: - $ docker-machine create --driver azure + $ docker-machine create --driver azure --azure-subscription-id Running pre-create checks... Microsoft Azure: To sign in, use a web browser to open the page https://aka.ms/devicelogin. Enter the code [...] to authenticate. From 44200e7a04e143b91ee2c77c04f91da20051b104 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Sat, 12 Mar 2016 18:18:53 -0800 Subject: [PATCH 6/7] Case-insensitive Azure Resource Provider matching Signed-off-by: Ahmet Alp Balkan --- drivers/azure/azureutil/azureutil.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drivers/azure/azureutil/azureutil.go b/drivers/azure/azureutil/azureutil.go index 9c7e16e436..94f3e29d2b 100644 --- a/drivers/azure/azureutil/azureutil.go +++ b/drivers/azure/azureutil/azureutil.go @@ -53,7 +53,8 @@ func New(env azure.Environment, subsID string, auth autorest.Authorizer) *AzureC } // RegisterResourceProviders registers current subscription to the specified -// resource provider namespaces if they are not already registered. +// 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 { @@ -65,11 +66,11 @@ func (a AzureClient) RegisterResourceProviders(namespaces ...string) error { m := make(map[string]bool) for _, p := range *l.Value { - m[to.String(p.Namespace)] = to.String(p.RegistrationState) == "Registered" + m[strings.ToLower(to.String(p.Namespace))] = to.String(p.RegistrationState) == "Registered" } for _, ns := range namespaces { - registered, ok := m[ns] + registered, ok := m[strings.ToLower(ns)] if !ok { return fmt.Errorf("Unknown resource provider %q", ns) } From 9fd035ecba468e3befb2dac92fabb6b04cede152 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 14 Mar 2016 17:34:50 -0700 Subject: [PATCH 7/7] azure.md: Address docs comments Signed-off-by: Ahmet Alp Balkan --- docs/drivers/azure.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/drivers/azure.md b/docs/drivers/azure.md index 6391c9fa14..3b0e2a8d26 100644 --- a/docs/drivers/azure.md +++ b/docs/drivers/azure.md @@ -13,17 +13,18 @@ parent="smn_machine_drivers" You will need an Azure Subscription to use this Docker Machine driver. [Sign up for a free trial.][trial] -> **NOTE:** This documentation is for the new version of Azure driver started -> shipping with v0.7.0 and it is not backwards-compatible with the old Azure -> driver. If you like to manage your existing Azure machines, please download -> and use machine versions prior to v0.7.0. + +> **NOTE:** This documentation is for the new version of the Azure driver, which started +> shipping with v0.7.0. This driver is not backwards-compatible with the old +> Azure driver. If you want to continue managing your existing Azure machines, please +> download and use machine versions prior to v0.7.0. [azure]: http://azure.microsoft.com/ [trial]: https://azure.microsoft.com/free/ ## Authentication -First time you try to create a machine, Azure driver will ask you to +The first time you try to create a machine, Azure driver will ask you to authenticate: $ docker-machine create --driver azure --azure-subscription-id