
There is cases when a command times out or when it fails that we and Puppet [1] will output the raw command that was executed. For a user create command that output contains the --password argument passed down to openstack CLI which causes sensitive passwords to be leaked into log files of the system executing Puppet, these can then be shipped of from the system into a remote syslog and still be in plain text. This tries to use Ruby gsub with a regular expression matching the two cases and instead output [redacted secret] the same way we do with config provider. [1] https://github.com/puppetlabs/puppet/blob/main/lib/puppet/util/execution.rb#L286 Change-Id: I4cad8f88fc7b67bb7aa4330832fc47bac41ae9df
164 lines
5.4 KiB
Ruby
164 lines
5.4 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
|
|
|
|
# redact sensitive information in exception and raise
|
|
def self.redact_and_raise(e)
|
|
new_message = e.message.gsub(/\-\-password\ [\w]+/, "--password [redacted secret]")
|
|
raise e.class, new_message
|
|
end
|
|
|
|
# with command_timeout
|
|
def self.openstack(*args)
|
|
begin
|
|
action = args[1]
|
|
Timeout.timeout(command_timeout(action)) do
|
|
execute([command(:openstack_command)] + args, override_locale: false, failonfail: true, combine: true)
|
|
end
|
|
rescue Timeout::Error
|
|
e = Puppet::ExecutionFailure.new "Command: 'openstack #{args.inspect}' has been running for more than #{command_timeout(action)} seconds"
|
|
redact_and_raise(e)
|
|
rescue Puppet::ExecutionFailure => e
|
|
redact_and_raise(e)
|
|
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]/
|
|
raise Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate' if exception.message.match(/Missing value \S* required for auth plugin/)
|
|
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
|