f6ea0881c9
This change checks the return code status of librarian puppet and stops the script execution if there is anerror. Without this change if librarian-puppet encounters an error, the script would continue and not all the modules may be available. This is problematic for the build systems that are using this script to pull down the modules. Change-Id: Ic7277deb957072abcae9a8639196938b24288cfa Closes-Bug: #1588895
587 lines
17 KiB
Ruby
Executable File
587 lines
17 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
###############################################################################
|
|
#
|
|
# Copyright 2016 Mirantis, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
###############################################################################
|
|
|
|
require 'pathname'
|
|
require 'optparse'
|
|
require 'timeout'
|
|
|
|
module PuppetModules
|
|
# all actions that can be entered as the first argument of the command
|
|
ALLOWED_ACTIONS = %w(console list restore compress status update reset install remove reinstall)
|
|
# this action will be used if no action is provided
|
|
DEFAULT_ACTION = 'install'
|
|
# the maximum allowed time for the command to run
|
|
TIMEOUT = 600
|
|
|
|
# parse the command line options
|
|
# @return [Hash]
|
|
def self.options
|
|
return @options if @options
|
|
@options = {}
|
|
@options[:actions] = []
|
|
parser = OptionParser.new do |opts|
|
|
opts.banner = "Usage: puppet_modules [options] [#{ALLOWED_ACTIONS.join '|'}]"
|
|
opts.separator 'Main options:'
|
|
opts.on('-f', '--file FILE', 'Use this Puppetfile') do |value|
|
|
@options[:puppetfile] = value
|
|
end
|
|
opts.on('-d', '--puppet_dir DIR', 'Install puppet modules into this directory') do |value|
|
|
@options[:puppet_dir] = value
|
|
end
|
|
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |value|
|
|
@options[:verbose] = value
|
|
end
|
|
opts.on('-t', '--[no-]test', 'Perform self-tests') do |value|
|
|
@options[:test] = value
|
|
end
|
|
opts.on('-i', '--install', 'Install all external Puppet modules') do
|
|
@options[:actions] << :install
|
|
end
|
|
opts.on('-R', '--reinstall', 'Remove modules and install them again') do
|
|
@options[:actions] << :reinstall
|
|
end
|
|
opts.on('-r', '--reset', 'Reset Git of all external Puppet modules') do
|
|
@options[:actions] << :reset
|
|
end
|
|
opts.on('-x', '--remove', 'Remove external Puppet modules') do
|
|
@options[:actions] << :remove
|
|
end
|
|
opts.on('-u', '--update', 'Update external Puppet modules') do
|
|
@options[:actions] << :update
|
|
end
|
|
opts.on('-s', '--status', 'Show git status of the external Puppet modules') do
|
|
@options[:actions] << :status
|
|
end
|
|
opts.on('-c', '--compress', 'Compress external Puppet modules to the archive file') do
|
|
@options[:actions] << :compress
|
|
end
|
|
opts.on('-e', '--restore', 'Restore external Puppet modules from the archive file') do
|
|
@options[:actions] << :restore
|
|
end
|
|
opts.on('-l', '--list', 'List external puppet modules') do
|
|
@options[:actions] << :list
|
|
end
|
|
opts.on('-C', '--console', 'Run pry console') do
|
|
@options[:actions] << :console
|
|
end
|
|
|
|
opts.separator 'Gem options:'
|
|
opts.on('-b', '--[no-]bundler', 'Setup and use "bundler"') do |value|
|
|
@options[:bundler] = value
|
|
end
|
|
opts.on('-g', '--gem_home DIR', 'Use this folder as a GEM_HOME') do |value|
|
|
ENV['GEM_HOME'] = value
|
|
end
|
|
opts.on('-p', '--puppet_gem VERSION', 'Use this version of Puppet gem') do |value|
|
|
ENV['PUPPET_GEM_VERSION'] = value
|
|
end
|
|
|
|
end
|
|
parser.separator "Default action: #{DEFAULT_ACTION}" if DEFAULT_ACTION
|
|
parser.parse!
|
|
@options
|
|
end
|
|
|
|
# Output a line of text
|
|
# @param message [String]
|
|
def self.output(message)
|
|
puts message
|
|
end
|
|
|
|
# Output an error message and exit
|
|
# @param message [String]
|
|
def self.error(message)
|
|
fail 'ERROR: ' + message
|
|
end
|
|
|
|
# check if any version of puppet librarian is installed
|
|
# @return [true,false]
|
|
def self.librarian_puppet_installed?
|
|
cmd = 'librarian-puppet -h 2>&1 >/dev/null'
|
|
system cmd
|
|
$?.exitstatus == 0
|
|
end
|
|
|
|
# check if this version of librarian is puppet-librarian-simple
|
|
# @return [true,false]
|
|
def self.librarian_puppet_simple?
|
|
cmd = 'librarian-puppet help generate_puppetfile 2>&1 >/dev/null'
|
|
system cmd
|
|
$?.exitstatus == 0
|
|
end
|
|
|
|
# check if timeout command is installed
|
|
# @return [true,false]
|
|
def self.timeout_installed?
|
|
return @timeout_installed unless @timeout_installed.nil?
|
|
cmd = 'which timeout 2>&1 >/dev/null'
|
|
system cmd
|
|
@timeout_installed = ($?.exitstatus == 0)
|
|
end
|
|
|
|
# check if the puppetfile contains any modules records
|
|
# @return [true,false]
|
|
def self.puppetfile_modules_present?
|
|
module_names.any?
|
|
end
|
|
|
|
# check if the puppet modules directory exists
|
|
# @return [true,false]
|
|
def self.dir_present_puppet?
|
|
dir_path_puppet.directory?
|
|
end
|
|
|
|
# check if the provided directory has a git repository inside
|
|
# @return [true, false]
|
|
def self.git_present?(directory)
|
|
directory = Pathname.new directory unless directory.is_a? Pathname
|
|
git = directory + dir_name_git
|
|
git.directory?
|
|
end
|
|
|
|
# the root path of this script
|
|
# should be the 'deployment' folder
|
|
# @return [Pathname]
|
|
def self.dir_path_root
|
|
Pathname.new(__FILE__).dirname.realpath
|
|
end
|
|
|
|
# puppetfile file name list
|
|
# @return [Array<Pathname>]
|
|
def self.file_name_list_puppetfile
|
|
[
|
|
Pathname.new('Puppetfile'),
|
|
Pathname.new('puppet/openstack_tasks/Puppetfile'),
|
|
]
|
|
end
|
|
|
|
# list of full paths to puppetfiles
|
|
# @return [Array<Pathname>]
|
|
def self.file_path_list_puppetfile
|
|
# return [Pathname.new options[:puppetfile]] if options[:puppetfile]
|
|
file_name_list_puppetfile.map do |puppetfile|
|
|
dir_path_root + puppetfile
|
|
end
|
|
end
|
|
|
|
# the name of the directory with puppet modules
|
|
# @return [Pathname]
|
|
def self.dir_name_puppet
|
|
Pathname.new 'puppet'
|
|
end
|
|
|
|
# full path to the directory with puppet modules
|
|
# @return [Pathname]
|
|
def self.dir_path_puppet
|
|
return Pathname.new options[:puppet_dir] if options[:puppet_dir]
|
|
dir_path_root + dir_name_puppet
|
|
end
|
|
|
|
# the name of the file used as puppet modules archive
|
|
# @return [Pathname]
|
|
def self.file_name_archive
|
|
Pathname.new 'puppet_modules.tgz'
|
|
end
|
|
|
|
# full path to the puppet modules archive file
|
|
# @return [Pathname]
|
|
def self.file_path_archive
|
|
dir_path_root + file_name_archive
|
|
end
|
|
|
|
# the name of gemfile lock file
|
|
# @return [Pathname]
|
|
def self.file_name_gemfile_lock
|
|
Pathname.new 'Gemfile.lock'
|
|
end
|
|
|
|
# full path to the gemfile lock file
|
|
# @return [Pathname]
|
|
def self.file_path_gemfile_lock
|
|
dir_path_root + file_name_gemfile_lock
|
|
end
|
|
|
|
# the name of the git repository folder
|
|
# @return [Pathname]
|
|
def self.dir_name_git
|
|
Pathname.new '.git'
|
|
end
|
|
|
|
# Run a command inside the provided directory
|
|
# and then return back. Returns true on success.
|
|
# @param directory [String, Pathname]
|
|
# @param command [String]
|
|
# @return [true,false]
|
|
def self.run_inside_directory(directory, command)
|
|
directory = directory.to_s
|
|
error "Cannot run command inside: '#{directory}'! Directory does not exist!" unless File.directory? directory
|
|
Dir.chdir(directory) do
|
|
command = "timeout #{TIMEOUT} " + command if timeout_installed?
|
|
output "Run: #{command} (dir: #{directory})"
|
|
system command
|
|
end
|
|
end
|
|
|
|
module Evaluator
|
|
# read the list of modules
|
|
# @return [Array<String>]
|
|
def self.modules
|
|
@modules || []
|
|
end
|
|
|
|
# set the list of modules
|
|
# @param value [Array<String>]
|
|
def self.modules=(value)
|
|
@modules = value
|
|
end
|
|
|
|
# a helper function that adds the module name to the list
|
|
# when executed from, the Puppetfile
|
|
# @param args [Array]
|
|
def self.mod(*args)
|
|
return unless args.first
|
|
@modules = [] unless @modules
|
|
module_name = args.first.to_s
|
|
@modules << module_name unless @modules.include? module_name
|
|
end
|
|
|
|
# evaluate the content of a puppetfile
|
|
# and record modules to the list
|
|
# @return [Array<String>]
|
|
def self.eval_puppetfile(content)
|
|
eval content
|
|
self.modules
|
|
end
|
|
end
|
|
|
|
# read a single Puppetfile and evaluate it
|
|
# add all module names to the list of modules
|
|
# @param file_path [Pathname]
|
|
# @return [Array<String>]
|
|
def self.read_puppetfile(file_path)
|
|
content = file_read file_path
|
|
return unless content
|
|
PuppetModules::Evaluator.modules = []
|
|
PuppetModules::Evaluator.eval_puppetfile content
|
|
end
|
|
|
|
# read the contents of this file
|
|
# @param file_path [Pathname]
|
|
# @return [String]
|
|
def self.file_read(file_path)
|
|
return nil unless file_exists? file_path
|
|
begin
|
|
file_path.read
|
|
rescue
|
|
nil
|
|
end
|
|
end
|
|
|
|
# check if this file exists
|
|
# @param file_path [Pathname]
|
|
# @return [true,false]
|
|
def self.file_exists?(file_path)
|
|
file_path.exist?
|
|
end
|
|
|
|
# remove a file ar directory structure
|
|
# @param file_path [Pathname]
|
|
def self.file_remove(file_path)
|
|
file_path.rmtree
|
|
end
|
|
|
|
# extract the list of external puppet module names
|
|
# from the puppetfile
|
|
# @return [Array<String>]
|
|
def self.module_names
|
|
return @module_names if @module_names
|
|
@module_names = []
|
|
file_path_list_puppetfile.each do |file_path|
|
|
modules = read_puppetfile file_path
|
|
next unless modules.is_a? Array
|
|
@module_names += modules.flatten
|
|
end
|
|
@module_names.uniq!
|
|
@module_names.sort!
|
|
@module_names
|
|
end
|
|
|
|
# get the array of full paths to external Puppet modules
|
|
# @return [Array<Pathname>]
|
|
def self.module_full_paths
|
|
module_names.map do |module_name|
|
|
dir_path_puppet + Pathname.new(module_name)
|
|
end
|
|
end
|
|
|
|
# remove all external puppet modules
|
|
def self.modules_remove
|
|
module_full_paths.each do |module_path|
|
|
if file_exists? module_path
|
|
output "Remove: '#{module_path}'"
|
|
file_remove module_path
|
|
end
|
|
end
|
|
end
|
|
|
|
# use tar to compress all external puppet modules
|
|
def self.modules_compress
|
|
modules = module_names.join ' '
|
|
command = "tar -czpvf #{file_path_archive} #{modules}"
|
|
success = run_inside_directory dir_path_puppet, command
|
|
if success
|
|
output "Archive of modules from: '#{dir_path_puppet}' written to: '#{file_path_archive}'"
|
|
else
|
|
error "Error writing modules archive to: '#{file_path_archive}'"
|
|
end
|
|
end
|
|
|
|
# first remove all modules, then restore the saved puppet modules
|
|
def self.modules_restore
|
|
error "The archive of external Puppet modules '#{file_path_archive}' doesn't exist!" unless file_exists? file_path_archive
|
|
modules_remove
|
|
command = "tar -xpvf #{file_path_archive}"
|
|
success = run_inside_directory dir_path_puppet, command
|
|
if success
|
|
output "Archive restored from: '#{file_path_archive}' to: '#{dir_path_puppet}'"
|
|
else
|
|
error "Error restoring modules archive from: '#{file_path_archive}'"
|
|
end
|
|
end
|
|
|
|
# prepare the command line and run the librarian command
|
|
# @param command [String]
|
|
# @return [true,false]
|
|
def self.librarian_puppet_command(command)
|
|
file_path_list_puppetfile.each do |file_path|
|
|
output "Running librarian command '#{command}' for Puppetfile: '#{file_path}'"
|
|
cmd = "librarian-puppet #{command}"
|
|
cmd += " --path=#{dir_path_puppet}"
|
|
cmd += " --puppetfile=#{file_path}"
|
|
cmd += ' --verbose' if options[:verbose]
|
|
cmd = 'bundle exec ' + cmd if options[:bundler]
|
|
success = run_inside_directory dir_path_root, cmd
|
|
error "librarian-puppet command failed" unless success
|
|
end
|
|
end
|
|
|
|
# use librarian to install all external Puppet modules
|
|
def self.modules_install
|
|
success = librarian_puppet_command 'install'
|
|
error 'Modules installation failed!' unless success
|
|
write_module_versions_file
|
|
end
|
|
|
|
# use librarian to install and then update Puppet modules
|
|
def self.modules_update
|
|
modules_install
|
|
success = librarian_puppet_command 'update'
|
|
error 'Modules update failed!' unless success
|
|
end
|
|
|
|
# use librarian to query git status of external Puppet modules
|
|
def self.modules_status
|
|
librarian_puppet_command 'git_status'
|
|
end
|
|
|
|
# run hard git reset inside this directory
|
|
# if there is a git repository present
|
|
# @param directory [String, Pathname]
|
|
# @return [true,false]
|
|
def self.git_reset_hard(directory)
|
|
if git_present? directory
|
|
success = run_inside_directory directory, 'git reset --hard HEAD'
|
|
return false unless success
|
|
run_inside_directory directory, 'git clean -f -d -x'
|
|
else
|
|
output "There is not Git in: '#{directory}'!"
|
|
false
|
|
end
|
|
end
|
|
|
|
# try to reset git repository inside
|
|
# every external Puppet modules
|
|
def self.modules_reset
|
|
module_full_paths.each do |module_path|
|
|
unless file_exists? module_path
|
|
output "Directory '#{module_path}' doesn't exist. Noting to reset."
|
|
next
|
|
end
|
|
success = git_reset_hard module_path
|
|
next if success
|
|
output "Failed to reset Git in: '#{module_path}'. Removing this directory!"
|
|
file_remove module_path
|
|
end
|
|
modules_install
|
|
end
|
|
|
|
# first, remove all modules then install them again
|
|
def self.modules_reinstall
|
|
modules_remove
|
|
modules_install
|
|
end
|
|
|
|
# run a bunch of self check operations
|
|
def self.perform_tests
|
|
output 'Running self tests...'
|
|
error "You have no 'librarian-puppet-simple' installed! Try to use '-b' option." unless librarian_puppet_installed?
|
|
error "You have installed 'librarian-puppet' instead of 'librarian-puppet-simple'!" unless librarian_puppet_simple?
|
|
error 'Could not find any modules in Puppetfiles!' unless puppetfile_modules_present?
|
|
error "There is no Puppet modules directory: '#{dir_path_puppet}'!" unless dir_present_puppet?
|
|
end
|
|
|
|
# try to decode the actions from the script's command line
|
|
# or take the default action
|
|
def self.actions
|
|
return options[:actions] if options[:actions].any?
|
|
ARGV.each do |action|
|
|
error "There is no action: '#{action}'!" unless ALLOWED_ACTIONS.include? action
|
|
options[:actions] << action.to_sym
|
|
end
|
|
options[:actions] << DEFAULT_ACTION.to_sym unless options[:actions].any?
|
|
options[:actions]
|
|
end
|
|
|
|
# run the pry console inside this module
|
|
def self.console
|
|
require 'pry'
|
|
binding.pry
|
|
end
|
|
|
|
# remove the gemfile lock file if it's present
|
|
def self.remove_gemfile_lock
|
|
if file_exists? file_path_gemfile_lock
|
|
output "Remove file: '#{file_path_gemfile_lock}'"
|
|
file_remove file_path_gemfile_lock
|
|
end
|
|
end
|
|
|
|
# prepare and update the bundler environment
|
|
def self.prepare_bundler
|
|
output 'Preparing bundler...'
|
|
remove_gemfile_lock
|
|
success = run_inside_directory dir_path_root, 'bundle install'
|
|
error 'Bundler install command failed!' unless success
|
|
success = run_inside_directory dir_path_root, 'bundle update'
|
|
error 'Bundler update command failed!' unless success
|
|
end
|
|
|
|
# output the list of all external Puppet modules
|
|
def self.modules_list
|
|
output module_names.join("\n") + "\n"
|
|
end
|
|
|
|
# @return [String]
|
|
def self.git_head(directory)
|
|
return unless git_present? directory
|
|
cmd = "git --git-dir #{directory}/.git rev-parse HEAD"
|
|
head = `#{cmd}`
|
|
return unless $?.exitstatus == 0
|
|
head = head.chomp.strip
|
|
head
|
|
end
|
|
|
|
# @return []Hash<String => String>]
|
|
def self.module_versions
|
|
module_versions = {}
|
|
module_names.each do |module_name|
|
|
module_path = dir_path_puppet + Pathname.new(module_name)
|
|
head = git_head module_path
|
|
error "#{module_name} is not currently checked out" unless head
|
|
module_versions.store module_name, head
|
|
end
|
|
module_versions
|
|
end
|
|
|
|
# @return [Pathname]
|
|
def self.file_path_module_versions
|
|
dir_path_puppet + Pathname.new('module_versions')
|
|
end
|
|
|
|
def self.write_module_versions_file
|
|
versions = "MODULE LIST\n"
|
|
module_versions.each do |module_name, version|
|
|
versions += "#{module_name}: #{version}\n"
|
|
end
|
|
puts versions
|
|
File.open(file_path_module_versions.to_s, 'w') do |file|
|
|
file.puts versions
|
|
end
|
|
end
|
|
|
|
# run all preparation functions if they are enabled by options
|
|
def self.preparations
|
|
prepare_bundler if options[:bundler]
|
|
perform_tests if options[:test]
|
|
end
|
|
|
|
# run a block of code with timeout
|
|
def self.with_timeout
|
|
begin
|
|
Timeout.timeout(TIMEOUT) do
|
|
yield
|
|
end
|
|
rescue Timeout::Error
|
|
error "Timeout of '#{TIMEOUT}' seconds is expired! The action was: '#{options[:action]}'"
|
|
end
|
|
end
|
|
|
|
# the main procedure
|
|
def self.main
|
|
options
|
|
preparations
|
|
|
|
# TODO: make it possible to run several actions at once
|
|
with_timeout do
|
|
actions.each do |action|
|
|
case action
|
|
when :console;
|
|
console
|
|
when :list;
|
|
modules_list
|
|
when :restore;
|
|
modules_restore
|
|
when :compress;
|
|
modules_compress
|
|
when :status;
|
|
modules_status
|
|
when :update;
|
|
modules_update
|
|
when :reset;
|
|
modules_reset
|
|
when :install;
|
|
modules_install
|
|
when :remove;
|
|
modules_remove
|
|
when :reinstall;
|
|
modules_reinstall
|
|
else
|
|
error 'There is no action specified!'
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
if __FILE__ == $0
|
|
PuppetModules.main
|
|
end
|