
We recently patched the openstackclient code to increase the number of retries when running commands[1], but I think what we really wanted was to increase the amount of time to wait on each retry. The original approach did help somewhat, but it is doing so by just racing more times. Instead, it would be better to win the race more often. This is mostly a CI or developer environment issue where we are deploying with the minimal amount of computing resources to get a functioning cloud. [1] https://git.openstack.org/cgit/openstack/puppet-openstacklib/commit/?id=489099632446b1903986549e5feb80b509f9b77d Change-Id: I0707165a9e8054c1b3e0ad299604356cecc9f99f
154 lines
4.9 KiB
Ruby
154 lines
4.9 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'
|
|
|
|
@@no_retry_actions = %w(create remove delete)
|
|
@@command_timeout = 40
|
|
# Fails on the 5th retry for a max of 212s (~3.5min) before total
|
|
# failure.
|
|
@@request_timeout = 170
|
|
@@retry_sleep = 3
|
|
class << self
|
|
[:no_retry_actions, :request_timeout, :retry_sleep].each do |m|
|
|
define_method m do
|
|
self.class_variable_get("@@#{m}")
|
|
end
|
|
define_method :"#{m}=" do |value|
|
|
self.class_variable_set("@@#{m}", value)
|
|
end
|
|
end
|
|
end
|
|
|
|
# timeout the openstack command
|
|
# after this number of seconds
|
|
# retry the command until the request_timeout,
|
|
# unless it's a no_retry_actions call
|
|
def self.command_timeout(action=nil)
|
|
# give no_retry actions the full time limit to finish
|
|
return self.request_timeout() if no_retry_actions.include? action
|
|
self.class_variable_get("@@command_timeout")
|
|
end
|
|
|
|
# with command_timeout
|
|
def self.openstack(*args)
|
|
begin
|
|
action = args[1]
|
|
Timeout.timeout(command_timeout(action)) do
|
|
openstack_command *args
|
|
end
|
|
rescue Timeout::Error
|
|
raise Puppet::ExecutionFailure, "Command: 'openstack #{args.inspect}' has been running for more than #{command_timeout(action)} seconds"
|
|
end
|
|
end
|
|
|
|
# get the current timestamp
|
|
def self.current_time
|
|
Time.now.to_i
|
|
end
|
|
|
|
def self.request_without_retry(&block)
|
|
previous_timeout = self.request_timeout
|
|
rc = nil
|
|
if block_given?
|
|
self.request_timeout = 0
|
|
rc = yield
|
|
end
|
|
ensure
|
|
self.request_timeout = previous_timeout
|
|
rc
|
|
end
|
|
|
|
# Returns an array of hashes, where the keys are the downcased CSV headers
|
|
# with underscores instead of spaces
|
|
#
|
|
# @param options [Hash] Other options
|
|
# @options :no_retry_exception_msgs [Array<Regexp>,Regexp] exception without retries
|
|
def self.request(service, action, properties, credentials=nil, options={})
|
|
env = credentials ? credentials.to_env : {}
|
|
no_retry = options[:no_retry_exception_msgs]
|
|
|
|
Puppet::Util.withenv(env) do
|
|
rv = nil
|
|
end_time = current_time + request_timeout
|
|
start_time = current_time
|
|
retry_count = 0
|
|
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]/
|
|
if current_time > end_time
|
|
error_message = exception.message
|
|
error_message += " (tried #{retry_count}, for a total of #{end_time - start_time } seconds)"
|
|
raise(Puppet::ExecutionFailure, error_message)
|
|
end
|
|
|
|
raise exception if no_retry_actions.include? action
|
|
if no_retry
|
|
no_retry = [no_retry] unless no_retry.is_a?(Array)
|
|
no_retry.each do |nr|
|
|
raise exception if exception.message.match(nr)
|
|
end
|
|
end
|
|
debug "Non-fatal error: '#{exception.message}'. Retrying for #{end_time - current_time} more seconds"
|
|
sleep retry_sleep
|
|
retry_count += 1
|
|
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
|