lib/azure/armrest/virtual_machine_service.rb
# Azure namespace
module Azure
# Armrest namespace
module Armrest
# Base class for managing virtual machines
class VirtualMachineService < ResourceGroupBasedService
# Create and return a new VirtualMachineService instance. Most
# methods for a VirtualMachineService instance will return one or more
# VirtualMachine instances.
#
# This subclass accepts the additional :provider option as well. The
# default is 'Microsoft.Compute'. You may need to set this to
# 'Microsoft.ClassicCompute' for your purposes.
#
def initialize(configuration, options = {})
super(configuration, 'virtualMachines', 'Microsoft.Compute', options)
end
# Return a list of virtual machines for the given +location+.
#
def list_by_location(location, options = {})
url = url_with_api_version(api_version, base_url, 'providers', provider, 'locations', location, service_name)
response = rest_get(url)
get_all_results(response, options[:skip_accessors_definition])
end
# Return a list of available VM series (aka sizes, flavors, etc), such
# as "Basic_A1", though other information is included as well.
#
def series(location)
namespace = 'microsoft.compute'
version = configuration.provider_default_api_version(namespace, 'locations/vmsizes')
unless version
raise ArgumentError, "Unable to find resources for #{namespace}"
end
url = url_with_api_version(
version, base_url, 'providers', provider, 'locations', location, 'vmSizes'
)
JSON.parse(rest_get(url))['value'].map{ |hash| VirtualMachineSize.new(hash) }
end
alias sizes series
# Captures the +vmname+ and associated disks into a reusable CSM template.
# The 3rd argument is a hash of options that supports the following keys:
#
# * vhdPrefix - The prefix in the name of the blobs.
# * destinationContainerName - The name of the container inside which the image will reside.
# * overwriteVhds - Boolean that indicates whether or not to overwrite any VHD's
# with the same prefix. The default is false.
#
def capture(vmname, options, group = configuration.resource_group)
vm_operate('capture', vmname, group, options)
end
# Stop the VM +vmname+ in +group+ and deallocate the tenant in Fabric.
#
def deallocate(vmname, group = configuration.resource_group)
vm_operate('deallocate', vmname, group)
end
# Sets the OSState for the +vmname+ in +group+ to 'Generalized'.
#
def generalize(vmname, group = configuration.resource_group)
vm_operate('generalize', vmname, group)
end
# Retrieves the settings of the VM named +vmname+ in resource group
# +group+, which will default to the same as the name of the VM.
#
# You can also specify any query options. At this time only the
# :expand => 'instanceView' option is supported, but others could
# be added over time.
#
# For backwards compatibility, the third argument may also be a boolean
# which will retrieve the model view by default. Set to false if you only
# want the instance view.
#
# Examples:
#
# vms = VirtualMachineService.new(credentials)
#
# # Standard call, get just the model view
# vms.get('some_name', 'some_group')
# vms.get('some_name', 'some_group', true) # same
#
# # Get the instance view only
# vms.get('some_name', 'some_group', false)
#
# # Get the instance view merged with the model view
# vms.get('some_name', 'some_group', :expand => 'instanceView')
#
def get(vmname, group = configuration.resource_group, options = {})
if options.kind_of?(Hash)
url = build_url(group, vmname, options)
response = rest_get(url)
VirtualMachineInstance.new(response)
else
options ? super(vmname, group) : get_instance_view(vmname, group)
end
end
# Convenient wrapper around the get method that retrieves the model view
# for +vmname+ in resource_group +group+ without the instance view
# information.
#
def get_model_view(vmname, group = configuration.resource_group)
get(vmname, group)
end
# Convenient wrapper around the get method that retrieves only the
# instance view for +vmname+ in resource_group +group+.
#
def get_instance_view(vmname, group = configuration.resource_group)
raise ArgumentError, "must specify resource group" unless group
raise ArgumentError, "must specify name of the resource" unless vmname
url = build_url(group, vmname, 'instanceView')
response = rest_get(url)
VirtualMachineInstance.new(response)
end
# Restart the VM +vmname+ for the given +group+, which will default
# to the same as the vmname.
#
# This is an asynchronous operation that returns a response object
# which you can inspect, such as response.code or response.headers.
#
def restart(vmname, group = configuration.resource_group)
vm_operate('restart', vmname, group)
end
# Start the VM +vmname+ for the given +group+, which will default
# to the same as the vmname.
#
# This is an asynchronous operation that returns a response object
# which you can inspect, such as response.code or response.headers.
#
def start(vmname, group = configuration.resource_group)
vm_operate('start', vmname, group)
end
# Stop the VM +vmname+ for the given +group+ gracefully. However,
# a forced shutdown will occur after 15 minutes.
#
# This is an asynchronous operation that returns a response object
# which you can inspect, such as response.code or response.headers.
#
def stop(vmname, group = configuration.resource_group)
vm_operate('powerOff', vmname, group)
end
# Delete the VM and associated resources. By default, this will
# delete the VM, its NIC, the associated IP address, and the
# image files (.vhd and .status) for the VM.
#
# If you want to delete other associated resources, such as any
# attached disks, the VM's underlying storage account, or associated
# network security groups you must explicitly specify them as an option.
#
# An attempt to delete a resource that cannot be deleted because it's
# still associated with some other resource will be logged and skipped.
#
# If the :verbose option is set to true, then additional messages are
# sent to your configuration log, or stdout if no log was specified.
#
# Note that if all of your related resources are in a self-contained
# resource group, you do not necessarily need this method. You could
# just delete the resource group itself, which would automatically
# delete all of its resources.
#
def delete_associated_resources(vmname, vmgroup, options = {})
options = {
:network_interfaces => true,
:ip_addresses => true,
:os_disk => true,
:data_disks => false,
:network_security_groups => false,
:storage_account => false,
:verbose => false
}.merge(options)
Azure::Armrest::Configuration.log ||= STDOUT if options[:verbose]
vm = get(vmname, vmgroup)
delete_and_wait(self, vmname, vmgroup, options)
# Must delete network interfaces first if you want to delete
# IP addresses or network security groups.
if options[:network_interfaces] || options[:ip_addresses] || options[:network_security_groups]
delete_associated_nics(vm, options)
end
if options[:os_disk] || options[:storage_account]
delete_associated_disk(vm, options)
end
if options[:data_disks]
delete_associated_data_disks(vm, options)
end
end
def model_class
VirtualMachineModel
end
private
# Deletes any NIC's associated with the VM, and optionally any public IP addresses
# and network security groups.
#
def delete_associated_nics(vm, options)
nis = Azure::Armrest::Network::NetworkInterfaceService.new(configuration)
nics = vm.properties.network_profile.network_interfaces.map(&:id)
if options[:ip_addresses]
ips = Azure::Armrest::Network::IpAddressService.new(configuration)
end
if options[:network_security_groups]
nsgs = Azure::Armrest::Network::NetworkSecurityGroupService.new(configuration)
end
nics.each do |nic_id_string|
nic = get_by_id(nic_id_string)
delete_and_wait(nis, nic.name, nic.resource_group, options)
if options[:ip_addresses]
nic.properties.ip_configurations.each do |ipconfig|
address = ipconfig.properties.try(:public_ip_address)
if address
ip = get_by_id(address.id)
delete_and_wait(ips, ip.name, ip.resource_group, options)
end
end
end
if options[:network_security_groups]
if nic.properties.respond_to?(:network_security_group)
nsg = get_by_id(nic.properties.network_security_group.id)
delete_and_wait(nsgs, nsg.name, nsg.resource_group, options)
end
end
end
end
# This deletes the OS disk from the storage account that's backing the
# virtual machine, along with the .status file. This does NOT delete
# copies of the disk.
#
# If the option to delete the entire storage account was selected, then
# it will not bother with deleting invidual files from the storage
# account first.
#
def delete_associated_disk(vm, options)
if vm.managed_disk?
delete_managed_storage(vm, options)
else
delete_unmanaged_storage(vm, options)
end
end
# This deletes any attached data disks that are associated with the
# virtual machine. Note that this should only happen after the VM
# has been deleted.
#
def delete_associated_data_disks(vm, options)
sds = Azure::Armrest::Storage::DiskService.new(configuration)
data_disks = vm.properties.storage_profile.try(:data_disks)
data_disks&.each do |data_disk|
disk = sds.get_by_id(data_disk.managed_disk.id)
delete_and_wait(sds, disk.name, disk.resource_group, options)
end
end
def delete_managed_storage(vm, options)
sds = Azure::Armrest::Storage::DiskService.new(configuration)
disk = sds.get_by_id(vm.properties.storage_profile.os_disk.managed_disk.id)
delete_and_wait(sds, disk.name, disk.resource_group, options)
end
def delete_unmanaged_storage(vm, options)
sas = Azure::Armrest::StorageAccountService.new(configuration)
storage_account = sas.get_from_vm(vm)
# Deleting the storage account does not require deleting the disks
# first, so skip that if deletion of the storage account was requested.
if options[:storage_account]
delete_and_wait(sas, storage_account.name, storage_account.resource_group, options)
else
keys = sas.list_account_keys(storage_account.name, storage_account.resource_group)
key = keys['key1'] || keys['key2']
disk = sas.get_os_disk(vm)
# There's a short delay between deleting the VM and unlocking the underlying
# .vhd file by Azure. Therefore we sleep up to two minutes while checking.
if disk.x_ms_lease_status.casecmp('unlocked') != 0
sleep_time = 0
while sleep_time < 120
sleep 10
sleep_time += 10
disk = sas.get_os_disk(vm)
break if disk.x_ms_lease_status.casecmp('unlocked') != 0
end
# In the unlikely event it did not unlock, just log and skip.
if disk.x_ms_lease_status.casecmp('unlocked') != 0
log('warn', "Unable to delete disk #{disk.container}/#{disk.name}")
return
end
end
storage_account.delete_blob(disk.container, disk.name, key)
log("Deleted blob #{disk.container}/#{disk.name}") if options[:verbose]
begin
status_file = File.basename(disk.name, '.vhd') + '.status'
storage_account.delete_blob(disk.container, status_file, key)
rescue Azure::Armrest::NotFoundException
# Ignore, does not always exist.
else
log("Deleted blob #{disk.container}/#{status_file}") if options[:verbose]
end
end
end
# Delete a +service+ type resource using its name and resource group,
# and wait for the operation to complete before returning.
#
# If the operation fails because a dependent resource is still attached,
# then the error is logged (in verbose mode) and ignored.
#
def delete_and_wait(service, name, group, options)
resource_type = service.class.to_s.sub('Service', '').split('::').last
log("Deleting #{resource_type} #{name}/#{group}") if options[:verbose]
wait(service.delete(name, group), 0)
log("Deleted #{resource_type} #{name}/#{group}") if options[:verbose]
rescue Azure::Armrest::BadRequestException, Azure::Armrest::PreconditionFailedException => err
if options[:verbose]
msg = "Unable to delete #{resource_type} #{name}/#{group}, skipping. Message: #{err.message}"
log('warn', msg)
end
end
def vm_operate(action, vmname, group, options = {})
raise ArgumentError, "must specify resource group" unless group
raise ArgumentError, "must specify name of the vm" unless vmname
url = build_url(group, vmname, action)
response = rest_post(url, options.to_json)
Azure::Armrest::ResponseHeaders.new(response.headers).tap do |headers|
headers.response_code = response.code
end
end
end
end
end