module QB::CLI

Definitions

Definitions

Definitions

Definitions

Constants

DEBUG_ARGS

CLI args that common to all commands that enable debug output

@return [Array<String>]

DEFAULT_TERMINAL_WIDTH

Default terminal line width to use if we can't figure it out dynamically.

@return [Fixnum]

Public Class Methods

ask(name:, description: nil, type:, default: NRSER::NO_ARG) click to toggle source

@todo Document ask method.

@param [type] arg_name

@todo Add name param description.

@return [return_type]

@todo Document return value.
# File lib/qb/cli.rb, line 68
  def self.ask name:,
          description: nil,
          type:,
          default: NRSER::NO_ARG
    puts
      
    value = loop do
      
      puts "Enter value for #{ name }"
      
      if description
        puts description.indent
      end
      
      puts "TYPE #{ type.to_s }".indent
      
      if default
        puts "DEFAULT #{ default.to_s }".indent
      end
      
      $stdout.write '> '
      
      value = gets.chomp
      
      QB.debug "User input", value
      
      if value == '' && default != NRSER::NO_ARG
        puts <<-END.dedent
          
          Using default value #{ default.to_s }
          
        END
        
        return default
      end
      
      begin
        type.from_s value
      rescue TypeError => e
        puts <<-END.dedent
          Input value #{ value.inspect } failed to satisfy type
          
              #{ type.to_s }
          
        END
      else
        break value
      end
      
    end # loop
    
    puts "Using value #{ value.inspect }"
    
    return value
    
  end
ask_for_option(role:, option: default = if role.defaults.key?(option.var_name)) click to toggle source
# File lib/qb/cli.rb, line 126
def self.ask_for_option role:, option:
  default = if role.defaults.key?(option.var_name)
    role.defaults[option.var_name]
  elsif option.required?
    NRSER::NO_ARG
  else
    nil
  end
dev(cmd, *args) click to toggle source
# File lib/qb/cli/dev.rb, line 112
def self.dev cmd, *args
  case cmd
  when 'serve', 'server'
    Dev.serve *args.rest
  when 'req'
    Dev.req *args.rest
  else
    raise "bad .dev subcmd: #{ cmd }"
  end
end
help(args = []) click to toggle source

Show the help message.

@todo

We should have more types of help.

@return [1]

Error exit status - we don't want `qb ... && ...` to move on to the 
second command when we end up falling back to `help`.
# File lib/qb/cli/help.rb, line 27
  def self.help args = []
    metadata = if QB.gemspec.metadata && !QB.gemspec.metadata.empty?
      "metadata:\n" + QB.gemspec.metadata.map {|key, value|
        "  #{ key }: #{ value }"
      }.join("\n")
    end
    
    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
    
    return 1
  end
list(pattern = nil) click to toggle source

List available roles.

@example

qb list --user
qb list -u
qb list --local
qb list -l
qb list --system
qb list -s
qb list --path=:system
qb list --path=./roles
qb list -p ./roles
qb list gem

@todo

We should have more types of help.

@return [1]

Error exit status - we don't want `qb ... && ...` to move on to the
second command when we end up falling back to `help`.
# File lib/qb/cli/list.rb, line 34
def self.list pattern = nil
  roles = if pattern
    QB::Role.matches pattern
  else
    QB::Role.available
  end
  
  name_col_width = roles.map { |r| r.display_name.length }.max + 2
  
  roles.each { |role|
    summary = role.summary.truncate QB::CLI.terminal_width - name_col_width
    
    puts ("%-#{ name_col_width }s" % role.display_name) + summary
  }
  
  puts
  
  return 0
end
play(args) click to toggle source

Play an Ansible playbook (like `state.yml`) in the QB environment (sets up path env vars, IO streams, etc.).

@param [Array<String>] args

CLI arguments to use.

@return [Fixnum]

The `ansible-playbook` command exit code.
# File lib/qb/cli/play.rb, line 28
def self.play args
  if args.empty?
    raise "Need path to playbook in first arg."
  end
  
  playbook_path = QB::Util.resolve args[0]
  
  unless playbook_path.file?
    raise "Can't find Ansible playbook at `#{ playbook_path.to_s }`"
  end
  
  # By default, we won't change directories to run the command.
  chdir = nil
  
  # See if there is an Ansible config in the parent directories
  ansible_cfg_path = QB::Util.find_up \
    QB::Ansible::ConfigFile::FILE_NAME,
    playbook_path.dirname,
    raise_on_not_found: false
  
  # If we did find an Ansible config, we're going to want to run in that
  # directory and add it to the role search path so that we merge it's
  # values into our env vars (otherwise they would override the config
  # values).
  unless ansible_cfg_path.nil?
    QB::Role::PATH.unshift ansible_cfg_path.dirname
    chdir = ansible_cfg_path.dirname
  end
  
  cmd = QB::Ansible::Cmd::Playbook.new \
    chdir: chdir,
    playbook_path: playbook_path
  
  status = cmd.stream
  
  if status != 0
    $stderr.puts "ERROR ansible-playbook failed."
  end
  
  exit status
  
end
run(args) click to toggle source

Run a QB role.

@param [Array<String>] args

CLI args to work with.

@return [Fixnum]

Exit status code from `ansible-playbook` command, unless we invoked
help or error'd out in another way before the run (in which case `1`
is returned).
# File lib/qb/cli/run.rb, line 28
def self.run args
  role_arg = args.shift
  QB.debug "role arg" => 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
    return help
  rescue QB::Role::MultipleMatchesError => e
    puts "ERROR - #{ e.message }\n\n"
    return 1
  end
  
  role.check_requirements
  
  options = QB::Options.new role, args
  
  QB.debug "Role options set on cli",
    role: options.role_options.reject { |k, o| o.value.nil? }
  
  QB.debug "QB options", options.qb.dup
  QB.debug "Ansible options", options.ansible.dup
  
  cwd = Dir.getwd
  
  dir = nil
  
  if role.has_dir_arg?
    # 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:
      #
      
      if options.ask?
        default = begin
          role.default_dir cwd, options.role_options
        rescue QB::UserInputError => e
          NRSER::NO_ARG
        end
        
        QB::CLI.ask name: "target directory (`qb_dir`)",
                    type: t.non_empty_str,
                    default: default
        
      else
        role.default_dir cwd, options.role_options
      end
      
    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
  
  
  # Interactive Input
  # =====================================================================
  
  if options.ask?
    # Incomplete
    raise "COMING SOON!!!...?"
    QB::CLI.ask_for_options role: role, options: options
  end
  
  
  # Validation
  # =====================================================================
  #
  # Should have already been taken care of if we used interactive input.
  #
  
  # 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."
    return 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_data
  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',
    ],
  }
  
  if role.meta['call_role']
    logger.debug "Calling role through qb/call..."
    
    play['tasks'] = [
      {
        'include_role' => {
          'name' => 'qb/call',
        },
        'vars' => {
          'role' => role.name,
          'args' => set_options.map { |option|
            [option.var_name, option.value_data]
          }.to_h,
        }
      }
    ]
    
    env = QB::Ansible::Env::Devel.new
    exe = [
      QB::Python.bin,
      (QB::Ansible::Env::Devel::ANSIBLE_HOME / 'bin' / 'ansible-playbook')
    ].join " "
    
  else
    play['roles'] << playbook_role
    env = QB::Ansible::Env.new
    exe = "ansible-playbook"
    
  end
  
  if options.qb['user']
    play['become'] = true
    play['become_user'] = options.qb['user']
  end
  
  playbook = [play]
  
  logger.debug "playbook", playbook
  
  # stick the role path in front to make sure we get **that** role
  env.roles_path.unshift role.path.expand_path.dirname
  
  cmd = QB::Ansible::Cmd::Playbook.new \
    env: env,
    playbook: playbook,
    role_options: options,
    chdir: (File.exists?('./ansible/ansible.cfg') ? './ansible' : nil),
    exe: exe
  
  # 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'
    puts "ENV:\n\n#{ YAML.dump cmd.env.to_h }\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
  #
  
  # 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
  
  logger.debug "Command prepared, running...",
    command: cmd,
    prepared: cmd.prepare
  
  status = cmd.stream
  
  if status != 0
    $stderr.puts "ERROR ansible-playbook failed."
  end
  
  # exit status
  status
end
set_debug!(args) click to toggle source

Module (Static) Methods

# File lib/qb/cli.rb, line 52
def self.set_debug! args
  if DEBUG_ARGS.any? {|arg| args.include? arg}
    ENV['QB_DEBUG'] = 'true'
    DEBUG_ARGS.each {|arg| args.delete arg}
  end
end
setup(args = []) click to toggle source

Run a setup playbook.

The path to the setup playbook can be given as the first of `args`, or `setup.qb.{yaml,yml}` will be searched in `$REPO_ROOT/dev/` and `$REPO_ROOT/`, where `$REPO_ROOT` is the Git root for the current directory.

@todo

1.  While it works, this system of finding the setup files feels kind-of 
    wonky.
2.  Any additional entries in `args` after the first seem to be silently
    ignored. Seems like we should do something with them (run all of them?)
    or error.

@param [Array<String>] args

Either:

1.  Empty, in which case we search for the setup playbook as detailed above.
2.  Contains a single path to the setup playbook.

@return [Fixnum]

The `ansible-playbook` command exit code.
# File lib/qb/cli/setup.rb, line 49
def self.setup args = []
  # Figure out project root and setup playbook path
  case args[0]
  when String, Pathname
    # The playbook path has been provided, use that to find the project root
    playbook_path = QB::Util.resolve args[0]
    project_root = NRSER.git_root playbook_path
    
  when nil
    # Figure the project root out from the current directory, then
    # form the playbook path from that
    project_root = NRSER.git_root '.'
    
    playbook_path = Util.find_yaml_file! \
      dirs: [
        project_root.join( 'dev' ),
        project_root,
      ],
      basename: 'setup.qb'
  
  else
    raise TypeError.new binding.erb <<-END
      First entry of `args` must be nil, String or Pathname, found:
      
          <%= args[0].pretty_inspect %>
      
      args:
      
          <%= args.pretty_inspect %>
      
    END
  end
  
  unless playbook_path.file?
    raise "Can't find QB setup playbook at `#{ playbook_path.to_s }`"
  end
  
  cmd = QB::Ansible::Cmd::Playbook.new \
    chdir: project_root,
    extra_vars: {
      project_root: project_root,
      qb_dir: project_root,
      qb_cwd: Pathname.getwd,
      qb_user_roles_dir: QB::USER_ROLES_DIR,
    },
    playbook_path: playbook_path
  
  puts cmd.prepare
  
  status = cmd.stream
  
  if status != 0
    $stderr.puts "ERROR QB setup failed."
  end
  
  exit status
  
end