#!/usr/bin/env ruby require 'pathname' require 'pp' require 'yaml' require 'json' require 'fileutils' require 'cmds' require 'qb' # constants # ========= ROOT = QB::ROOT ROLES_DIR = QB::ROLES_DIR ROLES = Pathname.glob(ROLES_DIR + 'qb.*').map {|path| path.basename.to_s} DEBUG_ARGS = ['-D', '--DEBUG'] TMP_DIR = ROOT + 'tmp' # globals # ======= # @api util # *pure* # # format a debug message with optional key / values to print # # @param msg [String] message to print. # @param dump [Hash] optional hash of keys and values to dump. def format msg, dump = {} unless dump.empty? msg += "\n" + dump.map {|k, v| " #{ k }: #{ v.inspect }" }.join("\n") end msg end def debug *args QB.debug *args end def set_debug! args if DEBUG_ARGS.any? {|arg| args.include? arg} QB.debug = true debug "ON" DEBUG_ARGS.each {|arg| args.delete arg} end end # needed for to clean the env if using bundler (like in dev). # # this is because the ansible gem module doesn't work right with the bundler # env vars set, so we need to remove them, but when running in dev we want # modules written in ruby like nrser.state_mate's `state` script to have # access to them so it can fire up bundler and get the right libraries. # # to accomplish this, we detect Bundler, and when it's present we copy the # bundler-related env vars (which i found by looking at # https://github.com/bundler/bundler/blob/master/lib/bundler.rb#L257) # into a hash to pass around the env sanitization, then copy them into # corresponding 'QB_DEV_ENV_' vars that modules can restore. # # we also set a 'QB_DEV_ENV=true' env var for modules to easily detect that # we're running in dev and restore the variables. # def with_clean_env &block if defined? Bundler # copy the Bundler env vars into a hash dev_env = ENV.select {|k, v| k.start_with?("BUNDLE_") || [ 'GEM_HOME', 'GEM_PATH', 'MANPATH', 'RUBYOPT', 'RUBYLIB', ].include?(k) } Bundler.with_clean_env do # now that we're in a clean env, copy the Bundler env vars into # 'QB_DEV_ENV_' vars. dev_env.each {|k, v| ENV["QB_DEV_ENV_#{ k }"] = v} # and set QB_DEV_ENV=true ENV['QB_DEV_ENV'] = 'true' # invoke the block block.call end else # bundler isn't loaded, so no env var silliness to deal with block.call end end def metadata if QB.gemspec.metadata && !QB.gemspec.metadata.empty? "metadata:\n" + QB.gemspec.metadata.map {|key, value| " #{ key }: #{ value }" }.join("\n") + "\n" end end def help puts <<-END version: #{ QB::VERSION } #{ metadata } syntax: qb ROLE [OPTIONS] DIRECTORY use `qb ROLE -h` for role options. available roles: END puts QB::Role.available puts exit 1 end def main args set_debug! args debug args: args QB.check_ansible_version role_arg = args.shift debug "role arg" => role_arg help if role_arg.nil? || ['-h', '--help', 'help'].include?(role_arg) begin role = QB::Role.require role_arg rescue QB::Role::NoMatchesError => e puts "ERROR - #{ e.message }\n\n" # exits with status code 1 help rescue QB::Role::MultipleMatchesError => e puts "ERROR - #{ e.message }\n\n" exit 1 end options, qb_options = QB::Options.parse! role, args debug "options set on cli", options.select {|k, o| !o.value.nil?} debug "qb options", qb_options cwd = Dir.getwd # get the target dir dir = case args.length when 0 # in this case, a dir has not been provided # # in some cases (like projects) the dir can be figured out in other ways: # QB.get_default_dir role, cwd, options when 1 # there is a single positional arg, which is used as dir args[0] else # there are multiple positional args, which is not allowed raise "can't supply more than one argument: #{ args.inspect }" end debug "input_dir", dir # normalize to expanded path (has no trailing slash) dir = File.expand_path dir debug "normalized_dir", dir # create the dir if it doesn't exist (so don't have to cover this in # every role) if role.mkdir FileUtils.mkdir_p dir unless File.exists? dir end saved_options_path = Pathname.new(dir) + '.qb-options.yml' saved_options = if saved_options_path.exist? # convert old _ separated names to - separated YAML.load(saved_options_path.read).map {|role_options_key, role_options| [ role_options_key, role_options.map {|name, value| [QB::Options.cli_ize_name(name), value] }.to_h ] }.to_h.tap {|saved_options| debug "found saved options", saved_options } else debug "no saved options" {} end if saved_options.key? role.options_key role_saved_options = saved_options[role.options_key] debug "found saved options for role", role_saved_options role_saved_options.each do |option_cli_name, value| option = options[option_cli_name] if option.value.nil? debug "setting from saved options", option: option, value: value option.value = value end end end # check that required options are present missing = options.values.select {|option| option.required? && option.value.nil? } unless missing.empty? puts "ERROR: options #{ missing.map {|o| o.cli_name } } are required." exit 1 end set_options = options.select {|k, o| !o.value.nil?} debug "set options", set_options playbook_role = {'role' => role.name} playbook_vars = { 'qb_dir' => dir, # depreciated due to mass potential for conflict 'dir' => dir, 'qb_cwd' => cwd, } set_options.values.each do |option| playbook_role[option.var_name] = option.value end play = { 'hosts' => qb_options['hosts'], 'vars' => playbook_vars, # 'gather_subset' => ['!all'], 'gather_facts' => qb_options['facts'], 'pre_tasks' => [ # need ansible 2.1.2.0 at least to run # but this is obviously not flexible enough # { # 'assert' => { # 'that' => "'ansible 2.1.2' in lookup('pipe', 'ansible --version')", # }, # }, { 'qb_facts' => { 'qb_dir' => dir, } }, ], 'roles' => [ 'nrser.blockinfile', playbook_role ], } if qb_options['user'] play['become'] = true play['become_user'] = qb_options['user'] end playbook = [play] debug "playbook", playbook playbook_path = Pathname.new(Dir.getwd) + '.qb-playbook.yml' debug playbook_path: playbook_path.to_s playbook_path.open('w') do |f| f.write YAML.dump(playbook) end # save the options back if ( # we set some options that we can save set_options.values.select {|o| o.save? }.length > 0 && # the role says to save options role.save_options ) saved_options[role.options_key] = set_options.select{|key, option| option.save? }.map {|key, option| [key, option.value] }.to_h unless saved_options_path.dirname.exist? FileUtils.mkdir_p saved_options_path.dirname end saved_options_path.open('w') do |f| f.write YAML.dump(saved_options) end end tmp_roles_path = QB::ROOT + 'tmp' + 'roles' ansible_roles_path = ( [ role.path.expand_path.dirname, tmp_roles_path ] + QB::Role.search_path ).join(':') ansible_library_path = [ ROOT + 'library', ].join(':') Dir.chdir QB::ROOT do with_clean_env do template = [] template << "ANSIBLE_ROLES_PATH=<%= roles_path %>" template << "ANSIBLE_LIBRARY=<%= library_path %>" template << "ansible-playbook" if play['hosts'] != ['localhost'] template << "-i <%= hosts %>" end if qb_options['tags'] template << "--tags=<%= tags %>" end if qb_options['verbose'] template << "-#{ 'v' * qb_options['verbose'] }" end template << "<%= playbook_path %>" cmd = Cmds.sub template.join(" "), [], { roles_path: ansible_roles_path, library_path: ansible_library_path, playbook_path: playbook_path.to_s, hosts: "#{ play['hosts'].join(',') },", tags: (qb_options['tags'] ? qb_options['tags'].join(',') : nil), } puts "COMMAND: #{ cmd }" status = Cmds.stream cmd if status != 0 puts "ERROR ansible-playbook failed." end exit status end end end main(ARGV) # if __FILE__ == $0 # doesn't work with gem stub or something?