ManageIQ/azure-armrest

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

Summary

Maintainability
B
4 hrs
Test Coverage
C
78%
require 'active_support'
require 'active_support/core_ext/hash/conversions'

module Azure
  module Armrest
    # Base class for services that need to run in a resource group
    class ResourceGroupBasedService < ArmrestService
      # Used to map service name strings to internal classes
      SERVICE_NAME_MAP = {
        'availabilitysets'      => Azure::Armrest::AvailabilitySet,
        'loadbalancers'         => Azure::Armrest::Network::LoadBalancer,
        'networkinterfaces'     => Azure::Armrest::Network::NetworkInterface,
        'networksecuritygroups' => Azure::Armrest::Network::NetworkSecurityGroup,
        'publicipaddresses'     => Azure::Armrest::Network::IpAddress,
        'storageaccounts'       => Azure::Armrest::StorageAccount,
        'virtualnetworks'       => Azure::Armrest::Network::VirtualNetwork,
        'subnets'               => Azure::Armrest::Network::Subnet,
        'inboundnatrules'       => Azure::Armrest::Network::InboundNat,
        'securityrules'         => Azure::Armrest::Network::NetworkSecurityRule,
        'routes'                => Azure::Armrest::Network::Route,
        'databases'             => Azure::Armrest::Sql::SqlDatabase,
        'extensions'            => Azure::Armrest::VirtualMachineExtension,
        'disks'                 => Azure::Armrest::Storage::Disk,
        'snapshots'             => Azure::Armrest::Storage::Snapshot,
        'images'                => Azure::Armrest::Storage::Image,
        'deployments'           => Azure::Armrest::TemplateDeployment,
        'operations'            => Azure::Armrest::TemplateDeploymentOperation
      }.freeze

      # Create a resource +name+ within the resource group +rgroup+, or the
      # resource group that was specified in the configuration, along with
      # a hash of appropriate +options+.
      #
      # Returns an instance of the object that was created if possible,
      # otherwise nil is returned.
      #
      # Note that this is an asynchronous operation. You can check the current
      # status of the resource by inspecting the :response_headers instance and
      # polling either the :azure_asyncoperation or :location URL.
      #
      # The +options+ hash keys are automatically converted to camelCase for
      # flexibility, so :createOption and :create_option will both work
      # when creating a virtual machine, for example.
      #
      def create(name, rgroup = configuration.resource_group, options = {})
        validate_resource_group(rgroup)
        validate_resource(name)

        url = build_url(rgroup, name)
        url = yield(url) || url if block_given?

        body = transform_create_options(options).to_json

        response = rest_put(url, body)

        headers = Azure::Armrest::ResponseHeaders.new(response.headers)
        headers.response_code = response.code

        if response.body.empty?
          obj = get(name, rgroup)
        else
          obj = model_class.new(response.body)
        end

        obj.response_headers = headers
        obj.response_code = headers.response_code

        obj
      end

      alias update create

      # List all resources within the resource group +rgroup+, or the
      # resource group that was specified in the configuration.
      #
      # Returns an ArmrestCollection, with the response headers set
      # for the operation as a whole.
      #
      def list(rgroup = configuration.resource_group, skip_accessors_definition = false)
        validate_resource_group(rgroup)

        url = build_url(rgroup)
        url = yield(url) || url if block_given?
        response = rest_get(url)

        get_all_results(response, skip_accessors_definition)
      end

      # Use a single call to get all resources for the service. You may
      # optionally provide a filter on various properties to limit the
      # result set.
      #
      # Example:
      #
      #   vms = Azure::Armrest::VirtualMachineService.new(conf)
      #   vms.list_all(:location => "eastus", :resource_group => "rg1")
      #
      # Note that comparisons against string values are caseless.
      #
      def list_all(filter = {})
        url = build_url
        url = yield(url) || url if block_given?

        skip_accessors_definition = filter.delete(:skip_accessors_definition) || false
        response = rest_get(url)
        results  = get_all_results(response, skip_accessors_definition)

        if filter.empty?
          results
        else
          results.select do |obj|
            filter.all? do |method_name, value|
              if value.kind_of?(String)
                if skip_accessors_definition
                  obj[method_name.to_s].casecmp(value).zero?
                else
                  obj.public_send(method_name).casecmp(value).zero?
                end
              else
                obj.public_send(method_name) == value
              end
            end
          end
        end
      end

      # This method returns a model object based on an ID string for a resource.
      #
      # Example:
      #
      #   vms = Azure::Armrest::VirtualMachineService.new(conf)
      #
      #   vm = vms.get('your_vm', 'your_group')
      #   nic_id = vm.properties.network_profile.network_interfaces[0].id
      #   nic = vm.get_by_id(nic_id)
      #
      def get_by_id(id_string)
        info = parse_id_string(id_string)
        url = convert_id_string_to_url(id_string, info)

        service_name = info['subservice_name'] || info['service_name'] || 'resourceGroups'

        model_class = SERVICE_NAME_MAP.fetch(service_name.downcase) do
          raise ArgumentError, "unable to map service name #{service_name} to model"
        end

        model_class.new(rest_get(url))
      end

      alias get_associated_resource get_by_id

      def delete_by_id(id_string)
        url = convert_id_string_to_url(id_string)
        delete_by_url(url, id_string)
      end

      # Get information about a single resource +name+ within resource group
      # +rgroup+, or the resource group that was set in the configuration.
      #
      def get(name, rgroup = configuration.resource_group)
        validate_resource_group(rgroup)
        validate_resource(name)

        url = build_url(rgroup, name)
        url = yield(url) || url if block_given?
        response = rest_get(url)

        obj = model_class.new(response.body)
        obj.response_headers = Azure::Armrest::ResponseHeaders.new(response.headers)
        obj.response_code = response.code

        obj
      end

      # Delete the resource with the given +name+ for the provided +resource_group+,
      # or the resource group specified in your original configuration object. If
      # successful, returns a ResponseHeaders object.
      #
      # If the delete operation returns a 204 (no body), which is what the Azure
      # REST API typically returns if the resource is not found, it is treated
      # as an error and a ResourceNotFoundException is raised.
      #
      def delete(name, rgroup = configuration.resource_group)
        validate_resource_group(rgroup)
        validate_resource(name)

        url = build_url(rgroup, name)
        url = yield(url) || url if block_given?

        delete_by_url(url, "#{rgroup}/#{name}")
      end

      private

      # By default, camelize all hash options for the create method.
      # Subclasses should override this behavior as needed.
      #
      def transform_create_options(hash)
        hash.deep_transform_keys{ |k| k.to_s.camelize(:lower) }
      end

      def convert_id_string_to_url(id_string, info = nil)
        if id_string.include?('api-version')
          File.join(configuration.environment.resource_url, id_string)
        else
          info ||= parse_id_string(id_string)
          api_version = api_version_lookup(info['provider'], info['service_name'], info['subservice_name'])
          File.join(configuration.environment.resource_url, id_string) + "?api-version=#{api_version}"
        end
      end

      # Parse the provider and service name out of an ID string.
      def parse_id_string(id_string)
        regex = %r{
          subscriptions/(?<subscription_id>[^\/]+)?
          (/resourceGroups/(?<resource_group>[^\/]+)?)?
          (/providers/(?<provider>[^\/]+)?)?
          (/(?<service_name>[^\/]+)?/(?<resource_name>[^\/]+))?
          (/(?<subservice_name>[^\/]+)?/(?<subservice_resource_name>[^\/]+))?
          \z
        }xi

        match = regex.match(id_string)
        raise ArgumentError, "Invalid ID string: #{id_string}" unless match
        Hash[match.names.zip(match.captures)]
      end

      def api_version_lookup(provider_name, service_name, subservice_name)
        provider_name ||= 'Microsoft.Resources'
        service_name  ||= 'resourceGroups'
        if subservice_name
          full_service_name = "#{service_name}/#{subservice_name}"
          api_version = configuration.provider_default_api_version(provider_name, full_service_name)
        end
        api_version ||= configuration.provider_default_api_version(provider_name, service_name)
        api_version || configuration.api_version
      end

      def delete_by_url(url, resource_name = '')
        response = rest_delete(url)

        if response.code == 204
          msg = "resource #{resource_name} not found"
          raise Azure::Armrest::ResourceNotFoundException.new(response.code, msg, response)
        end

        Azure::Armrest::ResponseHeaders.new(response.headers).tap do |headers|
          headers.response_code = response.code
        end
      end

      def validate_resource_group(name)
        raise ArgumentError, "must specify resource group" unless name
      end

      def validate_resource(name)
        raise ArgumentError, "must specify #{@service_name.singularize.underscore.humanize}" unless name
      end

      # Builds a URL based on subscription_id an resource_group and any other
      # arguments provided, and appends it with the api_version.
      #
      def build_url(resource_group = nil, *args)
        File.join(configuration.environment.resource_url, build_id_string(resource_group, *args))
      end

      def build_id_string(resource_group = nil, *args)
        id_string = File.join('', 'subscriptions', configuration.subscription_id)
        id_string = File.join(id_string, 'resourceGroups', resource_group) if resource_group
        id_string = File.join(id_string, 'providers', @provider, @service_name)

        query = "?api-version=#{@api_version}"

        args.each do |arg|
          if arg.kind_of?(Hash)
            arg.each do |key, value|
              key = key.to_s.camelize(:lower)

              if key.casecmp('top').zero?
                query << "&$top=#{value}"
              elsif key.casecmp('filter').zero?
                query << "&$filter=#{value}" # Allow raw filter
              elsif key.casecmp('expand').zero?
                query << "&$expand=#{value}"
              else
                if query.include?("$filter")
                  query << " and #{key} eq '#{value}'"
                else
                  query << "&$filter=#{key} eq '#{value}'"
                end
              end
            end
          else
            id_string = File.join(id_string, arg)
          end
        end

        id_string + query
      end

      # Aggregate resources from all resource groups.
      #
      # To be used in the cases where the API does not support list_all with
      # one call. Note that this does not set the skip token because we're
      # actually collating the results of multiple calls internally.
      #
      def list_in_all_groups(options = {})
        array   = []
        mutex   = Mutex.new
        headers = nil
        code    = nil

        Parallel.each(list_resource_groups, :in_threads => configuration.max_threads) do |rg|
          url = build_url(rg.name, options)
          response = rest_get(url)
          json_response = JSON.parse(response.body)['value']
          headers = Azure::Armrest::ResponseHeaders.new(response.headers)
          code = response.code
          results = json_response.map { |hash| model_class.new(hash) }
          mutex.synchronize { array << results } unless results.blank?
        end

        array = ArmrestCollection.new(array.flatten)

        # Use the last set of headers and response code for the overall result.
        array.response_headers = headers
        array.response_code = code

        array
      end
    end
  end
end