
Sometimes openstackclient can hang if Keystone API fails to respond to requests. This patch adds retries to work around these situations. * Retry and timeouts for openstack command -'command_timeout=20': after this timeout openstack command is retried untill the request_timeout. (The value of command_timeout parameter was updated from 10 to 20 as it was observed that "10" value is not enough when sytem was overloaded.) -'request_timeout=60': timeout of the entire request. -'retry_sleep=3': interval between command retries * Does not retry non-idempotent actions. * Improve specs Change-Id: Ifd8ae1b00321366e3a54fd6fe4a68db46bb743c7
121 lines
3.6 KiB
Ruby
121 lines
3.6 KiB
Ruby
require 'csv'
|
|
require 'puppet'
|
|
require 'timeout'
|
|
|
|
class Puppet::Error::OpenstackAuthInputError < Puppet::Error
|
|
end
|
|
|
|
class Puppet::Error::OpenstackUnauthorizedError < Puppet::Error
|
|
end
|
|
|
|
class Puppet::Provider::Openstack < Puppet::Provider
|
|
|
|
initvars # so commands will work
|
|
commands :openstack_command => 'openstack'
|
|
|
|
# this actions are not idempotent and retries can cause
|
|
# duplications or endless loops
|
|
def self.no_retry_actions
|
|
%w(create remove delete)
|
|
end
|
|
|
|
# timeout the openstack command
|
|
# after this number of seconds
|
|
# retry the command until the request_timeout
|
|
def self.command_timeout
|
|
20
|
|
end
|
|
|
|
# timeout the entire request with error
|
|
# after this number of seconds
|
|
def self.request_timeout
|
|
60
|
|
end
|
|
|
|
# sleep for this number of seconds
|
|
# between command retries
|
|
def self.retry_sleep
|
|
3
|
|
end
|
|
|
|
# run the openstack command
|
|
# with command_timeout
|
|
def self.openstack(*args)
|
|
begin
|
|
Timeout.timeout(command_timeout) do
|
|
openstack_command *args
|
|
end
|
|
rescue Timeout::Error
|
|
raise Puppet::ExecutionFailure, "Command: 'openstack #{args.inspect}' has been running for more then #{command_timeout} seconds!"
|
|
end
|
|
end
|
|
|
|
# get the current timestamp
|
|
def self.current_time
|
|
Time.now.to_i
|
|
end
|
|
|
|
# Returns an array of hashes, where the keys are the downcased CSV headers
|
|
# with underscores instead of spaces
|
|
def self.request(service, action, properties, credentials=nil)
|
|
env = credentials ? credentials.to_env : {}
|
|
Puppet::Util.withenv(env) do
|
|
rv = nil
|
|
end_time = current_time + request_timeout
|
|
loop do
|
|
begin
|
|
if action == 'list'
|
|
# shell output is:
|
|
# ID,Name,Description,Enabled
|
|
response = openstack(service, action, '--quiet', '--format', 'csv', properties)
|
|
response = parse_csv(response)
|
|
keys = response.delete_at(0)
|
|
rv = response.collect do |line|
|
|
hash = {}
|
|
keys.each_index do |index|
|
|
key = keys[index].downcase.gsub(/ /, '_').to_sym
|
|
hash[key] = line[index]
|
|
end
|
|
hash
|
|
end
|
|
elsif action == 'show' or action == 'create'
|
|
rv = {}
|
|
# shell output is:
|
|
# name="value1"
|
|
# id="value2"
|
|
# description="value3"
|
|
openstack(service, action, '--format', 'shell', properties).split("\n").each do |line|
|
|
# key is everything before the first "="
|
|
key, val = line.split('=', 2)
|
|
next unless val # Ignore warnings
|
|
# value is everything after the first "=", with leading and trailing double quotes stripped
|
|
val = val.gsub(/\A"|"\Z/, '')
|
|
rv[key.downcase.to_sym] = val
|
|
end
|
|
else
|
|
rv = openstack(service, action, properties)
|
|
end
|
|
break
|
|
rescue Puppet::ExecutionFailure => exception
|
|
raise Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate' if exception.message =~ /HTTP 40[13]/
|
|
raise exception if current_time > end_time
|
|
debug "Non-fatal error: '#{exception.message}'. Retrying for #{end_time - current_time} more seconds"
|
|
raise exception if no_retry_actions.include? action
|
|
sleep retry_sleep
|
|
retry
|
|
end
|
|
end
|
|
return rv
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def self.parse_csv(text)
|
|
# Ignore warnings - assume legitimate output starts with a double quoted
|
|
# string. Errors will be caught and raised prior to this
|
|
text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
|
|
return CSV.parse(text + "\n")
|
|
end
|
|
end
|