#!/usr/bin/env ruby require 'pathname' require 'pp' require 'yaml' require 'optparse' 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 vaues to dump. def format msg, dump = {} unless dump.empty? msg += "\n" + dump.map {|k, v| " #{ k }: #{ v.inspect }" }.join("\n") end msg end def role? pathname pathname.directory? && ['qb.yml', 'qb'].any? {|fn| pathname.join('meta', fn).file?} end def role_matches 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 def parse! role, var_prefix, vars, defaults, args positional = vars.select do |var| var['positional'] == true end positional_banner = if positional.empty? '' else ' ' + positional.map {|var| var['name'].upcase }.join(' ') end options = {} opt_parser = OptionParser.new do |opts| # opts.banner = "qb #{ role.name } [OPTIONS]#{ positional_banner }" opts.banner = "qb #{ role.name } [OPTIONS] DIRECTORY" vars.each do |var| arg_name = var.fetch 'name' var_name = "#{ var_prefix }_#{ arg_name }" required = var['required'] || false arg_style = required ? :REQUIRED : :OPTIONAL # on_args = [arg_style] on_args = [] if var['type'] == 'boolean' if var['short'] on_args << "-#{ var['short'] }" end on_args << "--[no-]#{ var['name'] }" else ruby_type = case var['type'] when 'string' String when Hash if var['type'].key? 'one_of' klass = Class.new opts.accept(klass) {|value| if var['type']['one_of'].include? value value else raise ArgumentError, "argument '#{ var['name'] }' must be " + "one of: #{ var['type']['one_of'].join(', ') }" end } klass else raise ArgumentError, "bad type: #{ var['type'].inspect }" end else raise ArgumentError, "bad type: #{ var['type'].inspect }" end if var['short'] on_args << "-#{ var['short'] } #{ arg_name.upcase }" end on_args << "--#{ var['name'] }=#{ arg_name.upcase }" on_args << ruby_type end # description description = if var.key? 'description' var['description'] else "set the #{ var_name } variable" end if var['type'].is_a?(Hash) && var['type'].key?('one_of') lb = "\n" + "\t" * 5 description += " options:" + "#{ lb }#{ var['type']['one_of'].join(lb) }" end on_args << description if defaults.key? var_name on_args << if var['type'] == 'boolean' if defaults[var_name] "default --#{ var['name'] }" else "default --no-#{ var['name'] }" end else "default = #{ defaults[var_name] }" end end debug "adding option", name: arg_name, on_args: on_args opts.on(*on_args) do |value| options[var['name']] = value end end # No argument, shows at tail. This will print an options summary. # Try it and see! opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end opt_parser.parse! args # args.each_with_index do |value, index| # var_name = positional[index]['name'] # if options.key? var_name # raise ArgumentError, "don't supply #{ var_name } as option and positionaly" # else # options[var_name] = value # end # end options end # needed for to clean the env if using bundler (like in dev) def with_clean_env &block if defined? Bundler Bundler.with_clean_env &block else block.call end end def metadata if QB.gemspec.metadata "metadata:\n" + QB.gemspec.metadata.map {|key, value| " #{ key }: #{ value }" }.join("\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.available_roles puts exit 1 end def main args set_debug! args debug args: args role_arg = args.shift debug "role arg" => role_arg help if role_arg.nil? || ['-h', '--help', 'help'].include?(role_arg) matches = QB.role_matches role_arg debug "role matches" => matches role = case matches.length when 0 puts "ERROR - no roles match arg #{ role_arg.inspect }\n\n" help when 1 matches[0] else puts "ERROR - multiple role matches:\n\n" puts matches puts exit 1 end debug role: role defaults_path = role.path + 'defaults' + 'main.yml' defaults = if defaults_path.file? YAML.load(defaults_path.read) || {} else {} end qb_meta = if (role.path + 'meta' + 'qb').exist? YAML.load Cmds.out! (role.path + 'meta' + 'qb').realpath.to_s elsif (role.path + 'meta' + 'qb.yml').exist? YAML.load (role.path + 'meta' + 'qb.yml').read else {} end vars = qb_meta['vars'] || [] var_prefix = qb_meta['var_prefix'] || role.namespaceless options = parse! role, var_prefix, vars, defaults, args debug options: 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, qb_meta, 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) FileUtils.mkdir_p dir unless File.exists? dir saved_options_path = Pathname.new(dir) + '.qb-options.yml' saved_options = if saved_options_path.exist? YAML.load saved_options_path.read else {} end if saved_options.key? role.options_key options = saved_options[role.options_key].merge options end playbook_role = {'role' => role.name} options.each do |arg_name, arg_value| playbook_role["#{ var_prefix }_#{ arg_name }"] = arg_value end playbook_role['dir'] = dir playbook_role['qb_dir'] = dir playbook_role['qb_cwd'] = cwd playbook = [ { 'hosts' => 'localhost', 'pre_tasks' => [ {'qb_facts' => nil}, ], 'roles' => [ 'nrser.blockinfile', playbook_role ], } ] debug playbook: playbook playbook_path = ROOT + '.qb-playbook.yml' debug playbook_path: playbook_path.to_s playbook_path.open('w') do |f| f.write YAML.dump(playbook) end unless ( options.empty? || (qb_meta.key?('save_options') && qb_meta['save_options'] == false) ) saved_options[role.options_key] = options FileUtils.mkdir_p saved_options_path.dirname unless saved_options_path.dirname.exist? 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 = ( [tmp_roles_path] + QB.role_dirs ).join(':') Dir.chdir ROOT do # install requirements # unless (TMP_DIR + 'roles').directory? # with_clean_env do # Cmds.stream! "ANSIBLE_ROLES_PATH=<%= roles_path %> ansible-galaxy install --ignore-errors -r <%= req_path%>", # req_path: (ROOT + 'requirements.yml'), # roles_path: tmp_roles_path.to_s # end # end with_clean_env do Cmds.stream! "ANSIBLE_ROLES_PATH=<%= roles_path %> ansible-playbook <%= playbook_path %>", roles_path: ansible_roles_path, playbook_path: playbook_path.to_s end end end main(ARGV) # if __FILE__ == $0 # doesn't work with gem stub or something?