lib/azure/armrest/armrest_service.rb
require 'time'
require 'active_support'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/module/delegation'
require_relative 'model/base_model'
module Azure
module Armrest
# Abstract base class for the other service classes.
class ArmrestService
extend Gem::Deprecate
# Configuration to access azure APIs
attr_accessor :armrest_configuration
alias configuration armrest_configuration
# Base url with subscription information used for most REST calls.
attr_accessor :base_url
# Provider for service specific API calls
attr_accessor :provider
# The service name for the Service class
attr_accessor :service_name
# The api-version string for this particular service
attr_accessor :api_version
# Returns a new Armrest::Configuration object.
#
# This method is deprecated, but is provided for backwards compatibility.
#
def self.configure(options)
Azure::Armrest::Configuration.new(options)
end
# Do not instantiate directly. This is an abstract base class from which
# all other service classes should subclass, and call super within their
# own constructors.
#
def initialize(armrest_configuration, service_name, default_provider, options)
@armrest_configuration = armrest_configuration
@service_name = service_name
@provider = options[:provider] || default_provider
if configuration.subscription_id.nil?
raise ArgumentError, 'subscription_id must be specified for this Service class'
end
# Base URL used for REST calls. Modify within method calls as needed.
@base_url = File.join(
configuration.environment.resource_url,
'subscriptions',
configuration.subscription_id
)
set_service_api_version(options, service_name)
end
# Returns a list of the available resource providers. This is really
# just a wrapper for Azure::Armrest::Configuration#providers.
#
delegate :providers, :to => :configuration, :prefix => :list
alias providers list_providers
deprecate :providers, :list_providers, 2018, 1
# Returns information about the specific provider +namespace+.
#
def get_provider(provider)
configuration.providers.find { |rp| rp.namespace.casecmp(provider) == 0 }
end
alias geo_locations get_provider
alias provider_info get_provider
deprecate :provider_info, :get_provider, 2018, 1
# Returns a list of all locations for all resource types of the given
# +provider+. If you do not specify a provider, then the locations for
# all providers will be returned.
#
# If you need individual details on a per-provider basis, use the methods
# of the ResourceProviderService instead.
#
# Deprecated.
#
def locations(provider = nil)
list = configuration.providers
list = list.select { |rp| rp.namespace.casecmp(provider) == 0 } if provider
list.collect { |rp| rp.resource_types.map(&:locations) }.flatten.uniq.sort
end
deprecate :locations, :list_locations, 2019, 1
# Returns a list of Location objects for the current subscription.
#
def list_locations
url = url_with_api_version(configuration.api_version, base_url, 'locations')
response = rest_get(url)
Azure::Armrest::ArmrestCollection.create_from_response(response, Location)
end
# Returns a list of subscriptions for the current tenant.
def list_subscriptions
Azure::Armrest::SubscriptionService.new(configuration).list
end
alias subscriptions list_subscriptions
deprecate :subscriptions, :list_subscriptions, 2018, 1
# Return information for the specified subscription ID, or the
# subscription ID that was provided in the constructor if none is
# specified.
#
def get_subscription(subscription_id = configuration.subscription_id)
subs = Azure::Armrest::SubscriptionService.new(configuration)
subs.get(subscription_id)
end
alias subscription_info get_subscription
deprecate :subscription_info, :get_subscription, 2018, 1
# Returns an array of Resource objects for the current subscription. If a
# +resource_group+ is provided, only list resources for that
# resource group.
#
def list_resources(resource_group = nil)
if resource_group
Azure::Armrest::ResourceService.new(configuration).list(resource_group)
else
Azure::Armrest::ResourceService.new(configuration).list_all
end
end
alias resources list_resources
deprecate :resources, :list_resources, 2018, 1
# Returns an array of ResourceGroup objects for the current subscription.
#
def list_resource_groups
Azure::Armrest::ResourceGroupService.new(configuration).list
end
alias resource_groups list_resource_groups
deprecate :resource_groups, :list_resource_groups, 2018, 1
# Returns a list of tags for the current subscription.
#
def tags
url = url_with_api_version(configuration.api_version, base_url, 'tagNames')
resp = rest_get(url)
JSON.parse(resp.body)["value"].map{ |hash| Azure::Armrest::Tag.new(hash) }
end
# Returns a list of tenants that can be accessed.
#
def tenants
url = url_with_api_version(configuration.api_version, configuration.environment.resource_url, 'tenants')
resp = rest_get(url)
JSON.parse(resp.body)['value'].map{ |hash| Azure::Armrest::Tenant.new(hash) }
end
# Poll a resource and return its current operations status. The
# +response+ argument should be a ResponseHeaders object that
# contains the :azure_asyncoperation header. It may optionally
# be an object that returns a URL from a .to_s method.
#
# This is meant to check the status of asynchronous operations,
# such as create or delete.
#
def poll(response)
return 'Succeeded' if [200, 201].include?(response.response_code)
url = response.try(:azure_asyncoperation) || response.try(:location)
response = rest_get(url).body
unless response.blank?
status = JSON.parse(response)['status']
end
status || 'Succeeded' # assume succeeded otherwise the wait method may hang
end
# Wait for the given +response+ to return a status of 'Succeeded', up
# to a maximum of +max_time+ seconds, and return the operations status.
# The first argument must be a ResponseHeaders object that contains
# the azure_asyncoperation header.
#
# Internally this will poll the response header every :retry_after
# seconds (or 10 seconds if that header isn't found), up to a maximum of
# 60 seconds by default. There is no timeout limit if +max_time+ is 0.
#
# For most resources the +max_time+ argument should be more than sufficient.
# Certain resources, such as virtual machines, could take longer.
#
def wait(response, max_time = 60, default_interval = 10)
sleep_time = response.respond_to?(:retry_after) ? response.retry_after.to_i : default_interval
total_time = 0
until (status = poll(response)) =~ /^succe/i # success or succeeded
total_time += sleep_time
break if max_time > 0 && total_time >= max_time
sleep sleep_time
end
status
end
class << self
private
def rest_execute(options, http_method = :get, encode = true, max_retries = 3)
tries ||= 0
url = encode ? Addressable::URI.encode(options[:url]) : options[:url]
options = options.merge(:method => http_method, :url => url)
RestClient::Request.execute(options)
rescue RestClient::Exception => err
if [409, 429, 500, 502, 503, 504].include?(err.http_code)
tries += 1
if tries <= max_retries
msg = "A rate limit or server side issue has occurred [#{err.http_code}]. Retry number #{tries}."
Azure::Armrest::Configuration.log.try(:log, Logger::WARN, msg)
sleep_time = (err.response.headers[:retry_after] || 30).to_i
sleep_time = 5 if sleep_time < 5 # 5 second minimum
sleep_time = 120 if sleep_time > 120 # 2 minute maximum
sleep(sleep_time)
retry
end
end
raise_api_exception(err)
end
def rest_get(options)
rest_execute(options, :get)
end
def rest_post(options)
rest_execute(options, :post)
end
def rest_patch(options)
rest_execute(options, :patch)
end
def rest_delete(options)
rest_execute(options, :delete)
end
def rest_put(options)
rest_execute(options, :put)
end
def rest_head(options)
rest_execute(options, :head)
end
def raise_api_exception(err)
begin
content_type_header = err.response.headers[:content_type]
response = case content_type_header.match(%r{(application/\w+)})[1]
when "application/json"
JSON.parse(err.http_body)
when "application/xml"
# The XML document that is returned has Error, Code, and Message
# so we need to downcase the keys to stay consistent with the
# parsed-json hash.
Hash.from_xml(err.http_body).deep_transform_keys(&:downcase)
end
code = response['error']['code']
message = response['error']['message']
rescue
code = err.try(:http_code) || err.try(:code)
message = err.try(:http_body) || err.try(:message)
end
exception_type = Azure::Armrest::EXCEPTION_MAP[err.http_code]
# If this is an exception that doesn't map directly to an HTTP code
# then parse it the exception class name and re-raise it as our own.
if exception_type.nil?
begin
klass = "Azure::Armrest::" + err.class.to_s.split("::").last + "Exception"
exception_type = const_get(klass)
rescue NameError
exception_type = Azure::Armrest::ApiException
end
end
raise exception_type.new(code, message, err)
end
end
private
# REST verb methods
def rest_execute(url, body = nil, http_method = :get, encode = true, max_retries = 3)
options = {
:url => url,
:proxy => configuration.proxy,
:ssl_version => configuration.ssl_version,
:ssl_verify => configuration.ssl_verify,
:timeout => configuration.timeout,
:headers => {
:accept => configuration.accept,
:content_type => configuration.content_type,
:authorization => configuration.token
}
}
options[:payload] = body if body
self.class.send(:rest_execute, options, http_method, encode, max_retries)
end
def rest_get(url)
rest_execute(url, nil, :get, true, configuration.max_retries)
end
def rest_get_without_encoding(url)
rest_execute(url, nil, :get, false, configuration.max_retries)
end
def rest_put(url, body = '')
rest_execute(url, body, :put, true, configuration.max_retries)
end
def rest_post(url, body = '')
rest_execute(url, body, :post, true, configuration.max_retries)
end
def rest_patch(url, body = '')
rest_execute(url, body, :patch, true, configuration.max_retries)
end
def rest_delete(url)
rest_execute(url, nil, :delete, true, configuration.max_retries)
end
def rest_head(url)
rest_execute(url, nil, :head, true, configuration.max_retries)
end
# Take an array of URI elements and join the together with the API version.
def url_with_api_version(api_version, *paths)
File.join(*paths) << "?api-version=#{api_version}"
end
# Each Azure API call may require different api_version.
# The api_version in armrest_configuration is used for common methods provided
# by ArmrestService
#
# The options hash for each service's constructor can contain key-value pair
# api_version => version
# This version will be used for the service specific API calls
#
# Otherwise the service specific api_version is looked up from configuration.providers
#
# Finally api_version in armrest_configuration is used if service specific version
# cannot be determined
def set_service_api_version(options, service)
@api_version =
options['api_version'] ||
configuration.provider_default_api_version(provider, service) ||
configuration.api_version
end
# Parse the skip token value out of the nextLink attribute from a response.
def parse_skip_token(json)
return nil unless json['nextLink']
json['nextLink'][/.*?skipToken=(.*?)$/i, 1]
end
# Make additional calls and concatenate the results if a continuation URL is found.
def get_all_results(response, skip_accessors_definition = false)
results = Azure::Armrest::ArmrestCollection.create_from_response(response, model_class, skip_accessors_definition)
nextlink = results.next_link
while nextlink
response = rest_get_without_encoding(nextlink)
more = Azure::Armrest::ArmrestCollection.create_from_response(response, model_class, skip_accessors_definition)
results.concat(more)
nextlink = more.next_link
end
results
end
def model_class
@model_class ||= Object.const_get(self.class.to_s.sub(/Service$/, ''))
end
def log(level = "info", msg)
RestClient.log.try(level, msg)
end
end # ArmrestService
end # Armrest
end # Azure