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
|