#!/usr/bin/env ruby require 'pathname' require 'pp' require 'yaml' require 'json' require 'fileutils' require 'cmds' require 'qb' # constants # ========= DEBUG_ARGS = ['-D', '--DEBUG'] # globals # ======= def set_debug! args if DEBUG_ARGS.any? {|arg| args.include? arg} ENV['QB_DEBUG'] = 'true' QB.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) } qb_env = ENV.select {|k, v| k.start_with? 'QB_'} 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} # set the flag that will be used by modules to know to restore the # variables ENV['QB_DEV_ENV'] = 'true' qb_env.each {|k, v| ENV[k] = v} # 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 QB.debug args: args QB.check_ansible_version role_arg = args.shift QB.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.new role, args QB.debug "role options set on cli", options.role_options.select {|k, o| !o.value.nil? } QB.debug "qb options", options.qb cwd = Dir.getwd dir = nil unless role.meta['default_dir'] == false # 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: # role.default_dir cwd, options.role_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 QB.debug "input_dir", dir # normalize to expanded path (has no trailing slash) dir = File.expand_path dir QB.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| QB.debug "found saved options", saved_options } else QB.debug "no saved options" {} end if saved_options.key? role.options_key role_saved_options = saved_options[role.options_key] QB.debug "found saved options for role", role_saved_options role_saved_options.each do |option_cli_name, value| option = options.role_options[option_cli_name] if option.value.nil? QB.debug "setting from saved options", option: option, value: value option.value = value end end end end # unless default_dir == false # check that required options are present missing = options.role_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.role_options.select {|k, o| !o.value.nil?} QB.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, 'qb_user_roles_dir' => QB::USER_ROLES_DIR.to_s, } set_options.values.each do |option| playbook_role[option.var_name] = option.value end play = { 'hosts' => options.qb['hosts'], 'vars' => playbook_vars, # 'gather_subset' => ['!all'], 'gather_facts' => options.qb['facts'], 'pre_tasks' => [ { 'qb_facts' => { 'qb_dir' => dir, } }, ], 'roles' => [ 'nrser.blockinfile', playbook_role ], } if options.qb['user'] play['become'] = true play['become_user'] = options.qb['user'] end playbook = [play] QB.debug "playbook", playbook playbook_path = Pathname.new(Dir.getwd) + '.qb-playbook.yml' QB.debug playbook_path: playbook_path.to_s env = { ANSIBLE_ROLES_PATH: [ # stick the role path in front to make sure we get **that** role role.path.expand_path.dirname, # then include the full role search path # NOTE this includes role paths pulled from a call-site local # ansible.cfg QB::Role.search_path, ]. flatten. # since QB::Role.search_path is an Array select(&:directory?). map(&:realpath). # so uniq works uniq, # drop dups (seems to keep first instance so preserves priority) ANSIBLE_LIBRARY: [ QB::ROOT.join('library'), ], ANSIBLE_FILTER_PLUGINS: [ QB::ROOT.join('plugins', 'filter_plugins'), ], ANSIBLE_LOOKUP_PLUGINS: [ QB::ROOT.join('plugins', 'lookup_plugins'), ], } cmd_options = options.ansible.clone if options.qb['inventory'] cmd_options['inventory-file'] = options.qb['inventory'] elsif play['hosts'] != ['localhost'] cmd_options['inventory-file'] = play['hosts'] end if options.qb['tags'] cmd_options['tags'] = options.qb['tags'] end cmd_template = <<-END ansible-playbook <%= cmd_options %> <% if verbose %> -<%= 'v' * verbose %> <% end %> <%= playbook_path %> END cmd = Cmds.new cmd_template, { env: env.map {|k, v| [k, v.is_a?(Array) ? v.join(':') : v]}.to_h, kwds: { cmd_options: cmd_options, verbose: options.qb['verbose'], playbook_path: playbook_path.to_s, }, format: :pretty, } # print # ===== # # print useful stuff for debugging / running outside of qb # if options.qb['print'].include? 'options' puts "SET OPTIONS:\n\n#{ YAML.dump set_options }\n\n" end if options.qb['print'].include? 'env' dump = YAML.dump env.map {|k, v| [k.to_s, v.map {|i| i.to_s}]}.to_h puts "ENV:\n\n#{ dump }\n\n" end if options.qb['print'].include? 'cmd' puts "COMMAND:\n\n#{ cmd.prepare }\n\n" end if options.qb['print'].include? 'playbook' puts "PLAYBOOK:\n\n#{ YAML.dump playbook }\n\n" end # stop here if we're not supposed to run exit 0 if !options.qb['run'] # run # === # # stuff below here does stuff # playbook_path.open('w') do |f| f.write YAML.dump(playbook) end # save the options back if ( dir && # 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 with_clean_env do # boot up stdio services so that ansible modules can stream to our # stdout and stderr to print stuff (including debug lines) in real-time stdio_services = {'out' => $stdout, 'err' => $stderr}.map do |name, dest| QB::Util::STDIO::Service.new(name, dest).tap {|s| s.open! } end status = cmd.stream # close the stdio services stdio_services.each {|s| s.close! } if status != 0 puts "ERROR ansible-playbook failed." end exit status end end main(ARGV) # if __FILE__ == $0 # doesn't work with gem stub or something?