class Chef::Provisioning::AzureDriver::Driver

Provisions machines using the Azure SDK

Attributes

region[R]
subscription[R]

Public Class Methods

canonicalize_url(driver_url, config) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 35
def self.canonicalize_url(driver_url, config)
  scheme, account_id = driver_url.split(":", 2)
  if account_id.nil? || account_id.empty?
    subscription = Subscriptions.default_subscription(config)
    if !subscription
      raise "Driver #{driver_url} did not specify a subscription ID, and no default subscription was found.  Have you downloaded the Azure CLI and used `azure account download` and `azure account import` to set up Azure?  Alternately, you can set azure_subscriptions to [ { subscription_id: '...', management_credentials: ... }] in your Chef configuration."
    end
    config = Cheffish::MergedConfig.new({ azure_subscriptions: subscription }, config)
  end
  if subscription
    [ "#{scheme}:#{subscription[:subscription_id]}", config ]
  else
    [ driver_url, config]
  end
end
from_url(driver_url, config) click to toggle source

Construct an AzureDriver object from a URL - used to parse existing URL data to hydrate a driver object. URL scheme: azure:subscription_id @return [AzureDriver] A chef-provisioning Azure driver object for the given URL

# File lib/chef/provisioning/azure_driver/driver.rb, line 31
def self.from_url(driver_url, config)
  Driver.new(driver_url, config)
end
new(driver_url, config) click to toggle source
Calls superclass method
# File lib/chef/provisioning/azure_driver/driver.rb, line 51
def initialize(driver_url, config)
  super
  scheme, subscription_id = driver_url.split(":", 2)
  @subscription = Subscriptions.get_subscription(config, subscription_id)
  if !subscription
    raise "Driver #{driver_url} has a subscription ID, but the system has no credentials configured for it!  If you have access to this subscription, you can use `azure account download` and `azure account import` in the Azure CLI to get the credentials, or set azure_subscriptions to [ { subscription_id: '...', management_credentials: ... }] in your Chef configuration."
  end

  # TODO make this instantiable so we can have multiple drivers ......
  Azure.configure do |azure|
    # Configure these 3 properties to use Storage
    azure.management_certificate = subscription[:management_certificate]
    azure.subscription_id        = subscription[:subscription_id]
    azure.management_endpoint    = subscription[:management_endpoint]
  end
end

Public Instance Methods

allocate_machine(action_handler, machine_spec, machine_options) click to toggle source

Allocate a new machine with the Azure API and start it up, without blocking to wait for it. Creates any needed resources to get a machine up and running. @param (see Chef::Provisioning::Driver#allocate_machine)

# File lib/chef/provisioning/azure_driver/driver.rb, line 76
def allocate_machine(action_handler, machine_spec, machine_options)
  existing_vm = vm_for(machine_spec)

  # We don't need to do anything if the existing VM is found
  return if existing_vm

  bootstrap_options = machine_options[:bootstrap_options] || {}
  bootstrap_options[:vm_size] ||= "Small"
  bootstrap_options[:cloud_service_name] ||= "chefprovisioning"
  bootstrap_options[:storage_account_name] ||= "chefprovisioning"
  bootstrap_options[:location] ||= "West US"

  location = bootstrap_options[:location]

  machine_spec.location = {
    "driver_url" => driver_url,
    "driver_version" => Chef::Provisioning::AzureDriver::VERSION,
    "allocated_at" => Time.now.utc.to_s,
    "host_node" => action_handler.host_node,
    "image_id" => machine_options[:image_id],
    "location" => location,
    "cloud_service" => bootstrap_options[:cloud_service_name],
  }

  image_id = machine_options[:image_id] || default_image_for_location(location)

  Chef::Log.debug "Azure bootstrap options: #{bootstrap_options.inspect}"

  params = {
      vm_name: machine_spec.name,
      vm_user: bootstrap_options[:vm_user] || default_ssh_username,
      image: image_id,
      # This is only until SSH keys are added
      password: machine_options[:password],
      location: location,
      cloud_service_name: bootstrap_options[:cloud_service_name],
  }

  # If the cloud service exists already, need to add a role to it - otherwise create virtual machine (including cloud service)
  cloud_service = azure_cloud_service_service.get_cloud_service(bootstrap_options[:cloud_service_name])
  existing_deployment = azure_vm_service.list_virtual_machines(bootstrap_options[:cloud_service_name]).any?

  if cloud_service && existing_deployment
    action_handler.report_progress "Cloud Service #{bootstrap_options[:cloud_service_name]} already exists, adding role."
    action_handler.report_progress "Creating #{machine_spec.name} with image #{image_id} in #{bootstrap_options[:cloud_service_name]}..."
    vm = azure_vm_service.add_role(params, bootstrap_options)
  else
    action_handler.report_progress "Creating #{machine_spec.name} with image #{image_id} in #{location}..."
    vm = azure_vm_service.create_virtual_machine(params, bootstrap_options)
  end

  machine_spec.location["vm_name"] = vm.vm_name
  machine_spec.location["is_windows"] = (true if vm.os_type == "Windows") || false
  action_handler.report_progress "Created #{vm.vm_name} in #{location}..."
end
destroy_machine(action_handler, machine_spec, machine_options) click to toggle source

(see Chef::Provisioning::Driver#destroy_machine)

# File lib/chef/provisioning/azure_driver/driver.rb, line 154
def destroy_machine(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  vm_name = machine_spec.name
  cloud_service = machine_spec.location["cloud_service"]

  # Check if we need to proceed
  return if vm.nil? || vm_name.nil? || cloud_service.nil?

  # Skip if we don't actually need to do anything
  return unless action_handler.should_perform_actions

  # TODO: action_handler.do |block| ?
  action_handler.report_progress "Destroying VM #{machine_spec.name}!"
  azure_vm_service.delete_virtual_machine(vm_name, cloud_service)
  action_handler.report_progress "Destroyed VM #{machine_spec.name}!"
end
ready_machine(action_handler, machine_spec, machine_options) click to toggle source

(see Chef::Provisioning::Driver#ready_machine)

# File lib/chef/provisioning/azure_driver/driver.rb, line 133
def ready_machine(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  location = machine_spec.location["location"]

  if vm.nil?
    raise "Machine #{machine_spec.name} does not have a VM associated with it, or the VM does not exist."
  end

  # TODO: Not sure if this is the right thing to check
  if vm.status != "ReadyRole"
    action_handler.report_progress "Readying #{machine_spec.name} in #{location}..."
    wait_until_ready(action_handler, machine_spec)
    wait_for_transport(action_handler, machine_spec, machine_options)
  else
    action_handler.report_progress "#{machine_spec.name} already ready in #{location}!"
  end

  machine_for(machine_spec, machine_options, vm)
end

Private Instance Methods

azure_cloud_service_service() click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 192
def azure_cloud_service_service
  @cloud_service_service ||= Azure::CloudServiceManagementService.new
end
azure_vm_service() click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 188
def azure_vm_service
  @vm_service ||= Azure::VirtualMachineManagementService.new
end
convergence_strategy_for(machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 297
def convergence_strategy_for(machine_spec, machine_options)
  convergence_options = machine_options[:convergence_options]
  # Defaults
  unless machine_spec.location
    return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
  end

  if machine_spec.location["is_windows"]
    Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(convergence_options, config)
  elsif machine_options[:cached_installer]
    Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
  else
    Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
  end
end
create_ssh_transport(machine_spec, machine_options, vm) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 230
def create_ssh_transport(machine_spec, machine_options, vm)
  bootstrap_options = machine_options[:bootstrap_options] || {}
  username = bootstrap_options[:vm_user] || default_ssh_username
  tcp_endpoint = vm.tcp_endpoints.select { |tcp| tcp[:name] == "SSH" }.first
  remote_host = tcp_endpoint[:vip]

  # TODO: not this... replace with SSH key ASAP, only for getting this thing going...
  ssh_options = {
    password: machine_options[:password],
    port: tcp_endpoint[:public_port] # use public port from Cloud Service endpoint
  }

  options = {}
  options[:prefix] = "sudo " if machine_spec.location[:sudo] || username != "root"

  # Enable pty by default
  # TODO: why?
  options[:ssh_pty_enable] = true
  options[:ssh_gateway] ||= machine_spec.location["ssh_gateway"]

  Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
end
create_winrm_transport(machine_spec, machine_options, instance) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 253
def create_winrm_transport(machine_spec, machine_options, instance)
  winrm_transport_options = machine_options[:bootstrap_options][:winrm_transport]
  shared_winrm_options = {
      :user => machine_options[:bootstrap_options][:vm_user] || default_ssh_username,
      :pass => machine_options[:password] # TODO: Replace with encryption
  }

  if winrm_transport_options["https"]
    tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == "PowerShell" }.first
    remote_host = tcp_endpoint[:vip]
    port = tcp_endpoint[:public_port] || default_winrm_https_port
    endpoint = "https://#{remote_host}:#{port}/wsman"
    type = :ssl
    winrm_options = {
      :disable_sspi => winrm_transport_options["https"][:disable_sspi] || false, # default to Negotiate
      :basic_auth_only => winrm_transport_options["https"][:basic_auth_only] || false, # disallow Basic auth by default
      :no_ssl_peer_verification => winrm_transport_options["https"][:no_ssl_peer_verification] || false #disallow MITM potential by default
    }
  end

  if winrm_transport_options["http"]
    tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == "WinRm-Http" }.first
    remote_host = tcp_endpoint[:vip]
    port = tcp_endpoint[:public_port] || default_winrm_http_port
    endpoint = "http://#{remote_host}:#{port}/wsman"
    type = :plaintext
    winrm_options = {
      :disable_sspi => winrm_transport_options["http"]["disable_sspi"] || false, # default to Negotiate
      :basic_auth_only => winrm_transport_options["http"]["basic_auth_only"] || false # disallow Basic auth by default
    }
  end

  merged_winrm_options = winrm_options.merge(shared_winrm_options)
  Chef::Provisioning::Transport::WinRM.new("#{endpoint}", type, merged_winrm_options, {})
end
default_image_for_location(location) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 217
def default_image_for_location(location)
  Chef::Log.debug("Choosing default image for region '#{location}'")

  case location
  when "East US"
  when "Southeast Asia"
  when "West US"
    "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04_1-LTS-amd64-server-20140927-en-us-30GB"
  else
    raise "Unsupported location!"
  end
end
default_ssh_username() click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 196
def default_ssh_username
  "ubuntu"
end
default_winrm_http_port() click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 289
def default_winrm_http_port
  5985
end
default_winrm_https_port() click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 293
def default_winrm_https_port
  5986
end
machine_for(machine_spec, machine_options, vm = nil) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 173
def machine_for(machine_spec, machine_options, vm = nil)
  vm ||= vm_for(machine_spec)

  raise "VM for node #{machine_spec.name} has not been created!" unless vm

  transport =  transport_for(machine_spec, machine_options, vm)
  convergence_strategy = convergence_strategy_for(machine_spec, machine_options)

  if machine_spec.location["is_windows"]
    Chef::Provisioning::Machine::WindowsMachine.new(machine_spec, transport, convergence_strategy)
  else
    Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport, convergence_strategy)
  end
end
transport_for(machine_spec, machine_options, vm) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 209
def transport_for(machine_spec, machine_options, vm)
  if machine_spec.location["is_windows"]
    create_winrm_transport(machine_spec, machine_options, vm)
  else
    create_ssh_transport(machine_spec, machine_options, vm)
  end
end
vm_for(machine_spec) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 200
def vm_for(machine_spec)
  if machine_spec.location && machine_spec.name
    existing_vms = azure_vm_service.list_virtual_machines
    existing_vms.select { |vm| vm.vm_name == machine_spec.name }.first
  else
    nil
  end
end
wait_for_transport(action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 337
def wait_for_transport(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  transport = transport_for(machine_spec, machine_options, vm)

  return if transport.available?
  return unless action_handler.should_perform_actions

  time_elapsed = 0
  sleep_time = 10
  max_wait_time = 120

  action_handler.report_progress "Waiting for transport on #{machine_spec.name} ..."
  while time_elapsed < 120 && !transport.available?
    action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
    sleep(sleep_time)
    time_elapsed += sleep_time
  end
  action_handler.report_progress "Transport to #{machine_spec.name} is now up!"
end
wait_until_ready(action_handler, machine_spec) click to toggle source
# File lib/chef/provisioning/azure_driver/driver.rb, line 313
def wait_until_ready(action_handler, machine_spec)
  vm = vm_for(machine_spec)

  # If the machine is ready, nothing to do
  return if vm.status == "ReadyRole"

  # Skip if we don't actually need to do anything
  return unless action_handler.should_perform_actions

  time_elapsed = 0
  sleep_time = 10
  max_wait_time = 120

  action_handler.report_progress "waiting for #{machine_spec.name} to be ready ..."
  while time_elapsed < 120 && vm.status != "ReadyRole"
    action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
    sleep(sleep_time)
    time_elapsed += sleep_time
    # Azure caches results
    vm = vm_for(machine_spec)
  end
  action_handler.report_progress "#{machine_spec.name} is now ready"
end