ManageIQ/azure-armrest

View on GitHub
lib/azure/armrest/virtual_machine_service.rb

Summary

Maintainability
C
1 day
Test Coverage
F
28%
# 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