src/script/aeolus-upgrade
#!/usr/bin/ruby
require 'optparse'
AEOLUS_UPGRADE_DIR = ENV['AEOLUS_UPGRADE_DIR'] || '/usr/share/aeolus-conductor/upgrade-scripts/'
HISTORY_FILE_PATH = ENV['AEOLUS_UPGRADE_HISTORY'] || '/var/lib/aeolus-conductor/upgrade-history'
DEFAULT_LOG_FILE = '/var/log/aeolus-conductor/aeolus_upgrade.log'
DEFAULT_SCRIPTS_DIR = 'default/'
UPGRADE_SCRIPTS_DIR = 'upgrade/'
# error codes for exit_with function
ERROR_CODES = {
:success => 0,
:general_error => 1,
:option_parser_error => 2,
:not_root => 3,
:io_error => 4,
:unknown => 127,
}
BACKEND_SERVICES = ["mongod",
"postgresql",
"httpd",
"deltacloud-core",
"libvirtd",
"aeolus-conductor",
"conductor-dbomatic",
"imagefactory",
"ntpd"]
STDOUT_NAME = "stdout"
# Terminate script with error code from ERROR_CODES hash
def exit_with(code = :unknown)
code = ERROR_CODES[code.to_sym] || ERROR_CODES[:unknown]
exit code
end
# Indent each line of text with spacing
def indent_text(text, spacing=" ")
text.gsub(/^/, spacing)
end
# Prints message and waits for Y/n answer.
# Returns true if the answer is 'Y', false if it is 'n'.
def confirm(message)
yes = 'y'
no = 'n'
begin
print message + " (y/n): "
answer = gets.strip
end while !(answer == yes or answer == no)
return answer == yes
end
# Stores history of executed scripts
class UpgradeHistory
def initialize(path_to_history_file)
@history_file = path_to_history_file
end
# Returns last inserted executed script
def get_last_script
last_line = nil
f = open_history_file 'r'
f.each_line do |line|
last_line = line
end
f.close
last_line
end
# Ddds a script to history
def add_script(script)
f = open_history_file 'a'
f.puts File.basename(script)
f.close
end
private
def ensure_file(path)
if not File.exists? path
f = File.new(path, "w")
f.close
end
end
def open_history_file(mode)
ensure_file @history_file
File.open(@history_file, mode)
rescue => e
$stderr.puts e.message
exit_with :io_error
end
end
# Dummy history with /dev/null-like behaviour
class DevNullHistory
def get_last_script
nil
end
def add_script(script)
end
end
# Queue of upgrade scripts.
class UpgradeQueue
# Load scripts form a directory
# Takes path to a directory with upgrade scripts, an upgrade history instance
# and an instance of script parser.
# The queue finds all scripts applicable to current deployment that have not been
# executed yet.
def load(script_dir, history, script_parser)
@history = history
@script_dir = script_dir
@script_parser = script_parser
@scripts = nil
end
# Returns all scripts in the queue that have not been executed yet
def get_scripts
return [] if not @history or not @script_dir
@scripts ||= upgrade_scripts(@script_dir, @history.get_last_script)
@scripts
end
# Process all scripts in the queue. Execute a block for each of them
def process &block
while not self.get_scripts.empty?
yield self.get_scripts.first, @history
self.get_scripts.shift
end
end
def empty?
self.get_scripts.empty?
end
def length
self.get_scripts.length
end
protected
def upgrade_scripts(dir_path, starting_from = nil)
script_infos = find_upgrade_scripts(dir_path, starting_from).map do |path|
@script_parser.get_info(path)
end
return filter_scripts_for("aeolus-conductor", script_infos)
end
def filter_scripts_for(deployment, script_infos)
script_infos.delete_if do |script|
not script[:apply].include? deployment
end
end
def find_upgrade_scripts(path, starting_from = nil)
scripts = Dir.glob(path+'*').select{|f| File.executable?(f)}.sort
if not starting_from.nil?
scripts = scripts.delete_if {|script| (script <= path+starting_from)}
end
return scripts
end
end
# Parses upgrade scripts headers
# The files can contain:
# '#name: NAME' - on a single line, pretty name of the script
# '#description: DESC' - can be multiline, longer description of the script
class ScriptParser
# Returns info available from the file header
def get_info(script_path)
@info = {}
parse_script_file script_path
@info[:name] ||= ""
@info[:apply] ||= []
@info[:filename] = File.basename(script_path)
@info[:path] = script_path
@info
end
private
def parse_script_file(script_path)
f = File.open(script_path, 'r')
f.each_line do |line|
process_line line
end
f.close
end
def process_line(line)
@in_desc ||= false
if line =~ /#\s*name:\s*(.*)\s*$/
@info[:name] = $1
@in_desc = false
elsif line =~ /#\s*apply:\s*(.*)$/
@info[:apply] = $1.strip.split(/\s/)
@in_desc = false
elsif /#\s*description:\s*(.*)\s*$/.match line
@info[:description] = $1
@in_desc = true
elsif ( line =~ /^\s*#.*/ ) and @in_desc
@info[:description] ||= ""
@info[:description] += "\n" + line.gsub(/\s*#/, "").strip
@info[:description] = @info[:description].strip
else
@in_desc = false
end
end
end
# The upgrade process itself.
# It creates two upgrade queues:
# - one for default scripts that are executed everytime
# - one for one-time scripts that get executed only once
# Tries to execute the scripts one by one. If any of them fails, the process
# is stopped.
# Prints a result status at the end.
class UpgradeProcess
LINE_LEN = 80
def run(options)
@options = options
# check if backend services are stopped
ensure_services_are_off
fake_history = DevNullHistory.new
upgrade_history = UpgradeHistory.new(HISTORY_FILE_PATH)
@default_queue = UpgradeQueue.new
@default_queue.load(AEOLUS_UPGRADE_DIR+DEFAULT_SCRIPTS_DIR, fake_history, ScriptParser.new) if not @options[:skip_default]
@upgrade_queue = UpgradeQueue.new
@upgrade_queue.load(AEOLUS_UPGRADE_DIR+UPGRADE_SCRIPTS_DIR, upgrade_history, ScriptParser.new)
print_header
@step_count = @upgrade_queue.length
@step_count += @default_queue.length
if @step_count == 0
print_nothing_to_do
exit_with :success
end
process_queue(@default_queue)
process_queue(@upgrade_queue) if @default_queue.empty?
print_result
start_services
if finished_step_count < @step_count
exit_with :general_error
else
exit_with :success
end
end
protected
def ensure_services_are_off
stopped = true
BACKEND_SERVICES.each do |service|
if !is_stopped? service
puts "Service '%s' can not be running while aeolus-upgrade is in progress" % service
stopped = false
end
end
if !stopped
print_line
if @options[:auto_stop]
stop_services
else
puts "This script can be run in auto-stop mode with the -a / --auto-stop option to"
puts "disable services automatically."
puts "You may always use `aeolus-services stop` to stop all the services"
puts ""
exit_service_stop
end
end
end
def stop_services
service = 'aeolus-services'
puts "We will stop all aeolus services using #{service} stop."
if @options[:assume_yes] || confirm("PROCEED?")
if !stop(service)
puts "There was an issue stopping your services. Please resolve this manually"
puts "and try again"
exit_with :general_error
end
else
exit_service_stop
end
end
def start_services
service = 'aeolus-services'
puts "Aeolus services were stopped before executing the upgrade."
puts "We will start all aeolus services now using '#{service} start'"
if @options[:assume_yes] || confirm("START SERVICES?")
if !start(service)
puts "There was an issue starting your services. Please resolve this manually"
puts "and try again."
exit_with :general_error
end
else
puts "Aeolus services not started."
puts "Remember to start aeolus services using '#{service} start'."
end
end
def exit_service_stop
puts "Exiting. Please stop your services and try again"
exit_with :general_error
end
def print_line(char="=", repeats=LINE_LEN)
puts char*repeats
end
def print_nothing_to_do
print_line
puts "Nothing to do"
end
def print_header
print_line
puts " Aeolus-conductor upgrade"
end
def print_step_info info
print_line
puts
puts "%s/%s: %s (%s)" % [@current_step, @step_count, info[:name], info[:filename]]
puts indent_text(info[:description]) if info[:description]
puts
end
def print_result
print_line
puts "Upgrade successful" if finished_step_count == @step_count
puts "Finished %i of %i upgrade steps" % [finished_step_count, @step_count]
end
def finished_step_count
@step_count - @upgrade_queue.length - @default_queue.length
end
def process_queue(queue)
@current_step ||= 1
queue.process do |script_info, history|
print_step_info(script_info)
if not @options[:dry_run]
break if not @options[:assume_yes] and not confirm("Do you want to proceed?")
break if not process_script(script_info)
history.add_script script_info[:path]
end
@current_step+=1
end
end
def process_script(script_info)
if execute_script(script_info[:path])
puts
puts script_info[:name]+" OK."
puts
return true
else
puts
puts script_info[:name]+" FAILED."
puts
return false
end
end
def execute_script script
command = script
command += ' &>>'+ log_file if log_file != STDOUT_NAME
log_start(script) if log_file != STDOUT_NAME
result = system(command)
log_result(result) if log_file != STDOUT_NAME
return result
end
def log_start script
system('printf "\n[$(date)] '+ script +'\n" >> '+ log_file)
end
def log_result success
system('printf "SUCCEEDED\n" >> '+ log_file) if success
system('printf "FAILED\n" >> '+ log_file) if not success
end
def log_file
return @options[:log_file] || DEFAULT_LOG_FILE
end
end
# test if given service is stopped
def is_stopped? (service)
return !system('/sbin/service %s status 2>/dev/null >/dev/null' % service)
end
# system appears to return TRUE in some cases even when the
# system call fails. non catastrophic errors, such as the service
# already being off
def stop(service)
return system('%s stop' % service)
end
# start a given service
def start(service)
return system('%s start' % service)
end
# Parse and return script options
def parse_options
opts = {}
opts[:assume_yes] = false
begin
option_parser = OptionParser.new
option_parser.banner = "Usage: #{$0} [options]"
option_parser.on_tail.on('-y', '--assumeyes', 'Assume yes on confirmations') do
opts[:assume_yes] = true
end
option_parser.on_tail.on('-a', '--autostop', 'Automatically stop services') do
opts[:auto_stop] = true
end
option_parser.on_tail.on('-d', '--dry-run', 'Prints the upgrade steps without modifying anything') do
opts[:dry_run] = true
end
option_parser.on_tail.on('-s', '--skip-default', 'Skips the default upgrade steps') do
opts[:skip_default] = true
end
option_parser.on_tail.on('--log=LOG_FILE', 'Log file, can also be set to stdout') do |value|
opts[:log_file] = value
end
option_parser.on_tail.on('-h', '--help', 'Show this short summary') do
puts option_parser
exit_with :success
end
option_parser.parse!
rescue => e
$stderr.puts e.message
$stderr.puts option_parser
exit_with :option_parser_error
end
opts
end
# check if running as root
unless Process.uid == 0
$stderr.puts "You must run aeolus-upgrade as root"
exit_with :not_root
end
# start the upgrade process
upgrade = UpgradeProcess.new
upgrade.run(parse_options)