diff --git a/README.md b/README.md index 83e94cc..54e246e 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,12 @@ If you are running on macOS and use Homebrew's `bash-completion` formula, you ca ln -s $(floaty completion --shell bash) /usr/local/etc/bash_completion.d/floaty ``` +There is also tab completion for zsh: + +```zsh +source $(floaty completion --shell zsh) +``` + ## vmpooler API This cli tool uses the [vmpooler API](https://github.com/puppetlabs/vmpooler/blob/master/API.md). diff --git a/extras/completions/floaty.bash b/extras/completions/floaty.bash index 5adee75..0c18c49 100644 --- a/extras/completions/floaty.bash +++ b/extras/completions/floaty.bash @@ -21,7 +21,7 @@ _vmfloaty() COMPREPLY=( $(compgen -W "${_vmfloaty_avail_templates}" -- "${cur}") ) elif [[ $hostname_subcommands =~ (^| )$prev($| ) ]] ; then - _vmfloaty_active_hostnames=$(floaty list --active 2>/dev/null | grep '^-' | cut -d' ' -f2) + _vmfloaty_active_hostnames=$(floaty list --active --hostnameonly 2>/dev/null) COMPREPLY=( $(compgen -W "${_vmfloaty_active_hostnames}" -- "${cur}") ) else COMPREPLY=( $(compgen -W "${subcommands}" -- "${cur}") ) diff --git a/extras/completions/floaty.zsh b/extras/completions/floaty.zsh new file mode 100644 index 0000000..77edf17 --- /dev/null +++ b/extras/completions/floaty.zsh @@ -0,0 +1,37 @@ +_floaty() +{ + local line subcommands template_subcommands hostname_subcommands + + subcommands="delete get help list modify query revert snapshot ssh status summary token" + + template_subcommands=("get" "ssh") + hostname_subcommands=("delete" "modify" "query" "revert" "snapshot") + + _arguments -C \ + "1: :(${subcommands})" \ + "*::arg:->args" + + if ((template_subcommands[(Ie)$line[1]])); then + _floaty_template_sub + elif ((hostname_subcommands[(Ie)$line[1]])); then + _floaty_hostname_sub + fi +} + +_floaty_template_sub() +{ + if [[ -z "$_vmfloaty_avail_templates" ]] ; then + _vmfloaty_avail_templates=$(floaty list 2>/dev/null) + fi + + _arguments "1: :(${_vmfloaty_avail_templates})" +} + +_floaty_hostname_sub() +{ + _vmfloaty_active_hostnames=$(floaty list --active --hostnameonly 2>/dev/null) + + _arguments "1: :(${_vmfloaty_active_hostnames})" +} + +compdef _floaty floaty diff --git a/lib/vmfloaty.rb b/lib/vmfloaty.rb index e1cbb20..7d015f3 100644 --- a/lib/vmfloaty.rb +++ b/lib/vmfloaty.rb @@ -88,6 +88,7 @@ class Vmfloaty c.option '--service STRING', String, 'Configured pooler service name' c.option '--active', 'Prints information about active vms for a given token' c.option '--json', 'Prints information as JSON' + c.option '--hostnameonly', 'When listing active vms, prints only hostnames, one per line' c.option '--token STRING', String, 'Token for pooler service' c.option '--url STRING', String, 'URL of pooler service' c.option '--user STRING', String, 'User to authenticate with' @@ -115,6 +116,10 @@ class Vmfloaty else if options.json puts Utils.get_host_data(verbose, service, running_vms).to_json + elsif options.hostnameonly + Utils.get_host_data(verbose, service, running_vms).each do |hostname, host_data| + Utils.print_fqdn_for_host(service, hostname, host_data) + end else puts "Your VMs on #{host}:" Utils.pretty_print_hosts(verbose, service, running_vms) diff --git a/lib/vmfloaty/utils.rb b/lib/vmfloaty/utils.rb index 9478d04..a569415 100644 --- a/lib/vmfloaty/utils.rb +++ b/lib/vmfloaty/utils.rb @@ -84,7 +84,28 @@ class Utils os_types end + def self.print_fqdn_for_host(service, hostname, host_data) + case service.type + when 'ABS' + abs_hostnames = [] + + host_data['allocated_resources'].each do |vm_name, _i| + abs_hostnames << vm_name['hostname'] + end + + puts abs_hostnames.join("\n") + when 'Pooler' + puts "#{hostname}.#{host_data['domain']}" + when 'NonstandardPooler' + puts host_data['fqdn'] + else + raise "Invalid service type #{service.type}" + end + end + def self.pretty_print_hosts(verbose, service, hostnames = [], print_to_stderr = false, indent = 0) + output_target = print_to_stderr ? $stderr : $stdout + fetched_data = self.get_host_data(verbose, service, hostnames) fetched_data.each do |hostname, host_data| case service.type @@ -93,25 +114,30 @@ class Utils # # Create a vmpooler service to query each hostname there so as to get the metadata too - vmpooler_service = service.clone - vmpooler_service.silent = true - vmpooler_service.maybe_use_vmpooler - puts "- [JobID:#{host_data['request']['job']['id']}] <#{host_data['state']}>" - host_data['allocated_resources'].each do |vm_name, _i| - self.pretty_print_hosts(verbose, vmpooler_service, vm_name['hostname'].split('.')[0], print_to_stderr, indent+2) + output_target.puts "- [JobID:#{host_data['request']['job']['id']}] <#{host_data['state']}>" + host_data['allocated_resources'].each do |allocated_resources, _i| + if allocated_resources['engine'] == "vmpooler" + vmpooler_service = service.clone + vmpooler_service.silent = true + vmpooler_service.maybe_use_vmpooler + self.pretty_print_hosts(verbose, vmpooler_service, allocated_resources['hostname'].split('.')[0], print_to_stderr, indent+2) + else + #TODO we could add more specific metadata for the other services, nspooler and aws + output_target.puts " - #{allocated_resources['hostname']} (#{allocated_resources['type']})" + end end when 'Pooler' tag_pairs = [] tag_pairs = host_data['tags'].map { |key, value| "#{key}: #{value}" } unless host_data['tags'].nil? duration = "#{host_data['running']}/#{host_data['lifetime']} hours" metadata = [host_data['template'], duration, *tag_pairs] - puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(', ')})".gsub(/^/, ' ' * indent) + output_target.puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(', ')})".gsub(/^/, ' ' * indent) when 'NonstandardPooler' line = "- #{host_data['fqdn']} (#{host_data['os_triple']}" line += ", #{host_data['hours_left_on_reservation']}h remaining" line += ", reason: #{host_data['reserved_for_reason']}" unless host_data['reserved_for_reason'].empty? line += ')' - puts line + output_target.puts line else raise "Invalid service type #{service.type}" end diff --git a/spec/vmfloaty/utils_spec.rb b/spec/vmfloaty/utils_spec.rb index 3941eb2..e26e0f1 100644 --- a/spec/vmfloaty/utils_spec.rb +++ b/spec/vmfloaty/utils_spec.rb @@ -162,97 +162,395 @@ describe Utils do end end - describe '#pretty_print_hosts' do + describe '#print_fqdn_for_host' do let(:url) { 'http://pooler.example.com' } - it 'prints a vmpooler output with host fqdn, template and duration info' do - hostname = 'mcpy42eqjxli9g2' - response_body = { hostname => { - 'template' => 'ubuntu-1604-x86_64', - 'lifetime' => 12, - 'running' => 9.66, - 'state' => 'running', - 'ip' => '127.0.0.1', - 'domain' => 'delivery.mycompany.net', - } } - output = '- mcpy42eqjxli9g2.delivery.mycompany.net (ubuntu-1604-x86_64, 9.66/12 hours)' + subject { Utils.print_fqdn_for_host(service, hostname, host_data) } - expect(STDOUT).to receive(:puts).with(output) + describe 'with vmpooler host' do + let(:service) { Service.new(MockOptions.new, 'url' => url) } + let(:hostname) { 'mcpy42eqjxli9g2' } + let(:domain) { 'delivery.mycompany.net' } + let(:fqdn) { [hostname, domain].join('.') } - service = Service.new(MockOptions.new, 'url' => url) - allow(service).to receive(:query) - .with(nil, hostname) - .and_return(response_body) + let(:host_data) do + { + 'template' => 'ubuntu-1604-x86_64', + 'lifetime' => 12, + 'running' => 9.66, + 'state' => 'running', + 'ip' => '127.0.0.1', + 'domain' => domain, + } + end - Utils.pretty_print_hosts(nil, service, hostname) + it 'outputs fqdn for host' do + expect(STDOUT).to receive(:puts).with(fqdn) + + subject + end end - it 'prints a vmpooler output with host fqdn, template, duration info, and tags when supplied' do - hostname = 'aiydvzpg23r415q' - response_body = { hostname => { - 'template' => 'redhat-7-x86_64', - 'lifetime' => 48, - 'running' => 7.67, - 'state' => 'running', - 'tags' => { - 'user' => 'bob', - 'role' => 'agent', - }, - 'ip' => '127.0.0.1', - 'domain' => 'delivery.mycompany.net', - } } - output = '- aiydvzpg23r415q.delivery.mycompany.net (redhat-7-x86_64, 7.67/48 hours, user: bob, role: agent)' + describe 'with nonstandard pooler host' do + let(:service) { Service.new(MockOptions.new, 'url' => url, 'type' => 'ns') } + let(:hostname) { 'sol11-9.delivery.mycompany.net' } + let(:host_data) do + { + 'fqdn' => hostname, + 'os_triple' => 'solaris-11-sparc', + 'reserved_by_user' => 'first.last', + 'reserved_for_reason' => '', + 'hours_left_on_reservation' => 35.89, + } + end + let(:fqdn) { hostname } # for nspooler these are the same - expect(STDOUT).to receive(:puts).with(output) + it 'outputs fqdn for host' do + expect(STDOUT).to receive(:puts).with(fqdn) - service = Service.new(MockOptions.new, 'url' => url) - allow(service).to receive(:query) - .with(nil, hostname) - .and_return(response_body) - - Utils.pretty_print_hosts(nil, service, hostname) + subject + end end - it 'prints a nonstandard pooler output with host, template, and time remaining' do - hostname = 'sol11-9.delivery.mycompany.net' - response_body = { hostname => { - 'fqdn' => hostname, - 'os_triple' => 'solaris-11-sparc', - 'reserved_by_user' => 'first.last', - 'reserved_for_reason' => '', - 'hours_left_on_reservation' => 35.89, - } } - output = '- sol11-9.delivery.mycompany.net (solaris-11-sparc, 35.89h remaining)' + describe 'with ABS host' do + let(:service) { Service.new(MockOptions.new, 'url' => url, 'type' => 'abs') } + let(:hostname) { '1597952189390' } + let(:fqdn) { 'example-noun.delivery.puppetlabs.net' } + let(:template) { 'ubuntu-1604-x86_64' } - expect(STDOUT).to receive(:puts).with(output) + # This seems to be the miminal stub response from ABS for the current output + let(:host_data) do + { + 'state' => 'allocated', + 'allocated_resources' => [ + { + 'hostname' => fqdn, + 'type' => template, + 'enging' => 'vmpooler', + }, + ], + 'request' => { + 'job' => { + 'id' => hostname, + } + }, + } + end - service = Service.new(MockOptions.new, 'url' => url, 'type' => 'ns') + it 'outputs fqdn for host' do + expect(STDOUT).to receive(:puts).with(fqdn) + + subject + end + end + end + + describe '#pretty_print_hosts' do + let(:url) { 'http://pooler.example.com' } + let(:verbose) { nil } + let(:print_to_stderr) { false } + + before(:each) do allow(service).to receive(:query) - .with(nil, hostname) + .with(anything, hostname) .and_return(response_body) - - Utils.pretty_print_hosts(nil, service, hostname) end - it 'prints a nonstandard pooler output with host, template, time remaining, and reason' do - hostname = 'sol11-9.delivery.mycompany.net' - response_body = { hostname => { - 'fqdn' => hostname, - 'os_triple' => 'solaris-11-sparc', - 'reserved_by_user' => 'first.last', - 'reserved_for_reason' => 'testing', - 'hours_left_on_reservation' => 35.89, - } } - output = '- sol11-9.delivery.mycompany.net (solaris-11-sparc, 35.89h remaining, reason: testing)' + subject { Utils.pretty_print_hosts(verbose, service, hostname, print_to_stderr) } - expect(STDOUT).to receive(:puts).with(output) + describe 'with vmpooler service' do + let(:service) { Service.new(MockOptions.new, 'url' => url) } - service = Service.new(MockOptions.new, 'url' => url, 'type' => 'ns') - allow(service).to receive(:query) - .with(nil, hostname) - .and_return(response_body) + let(:hostname) { 'mcpy42eqjxli9g2' } + let(:domain) { 'delivery.mycompany.net' } + let(:fqdn) { [hostname, domain].join('.') } - Utils.pretty_print_hosts(nil, service, hostname) + let(:response_body) do + { + hostname => { + 'template' => 'ubuntu-1604-x86_64', + 'lifetime' => 12, + 'running' => 9.66, + 'state' => 'running', + 'ip' => '127.0.0.1', + 'domain' => domain, + } + } + end + + let(:default_output) { "- #{fqdn} (ubuntu-1604-x86_64, 9.66/12 hours)" } + + it 'prints output with host fqdn, template and duration info' do + expect(STDOUT).to receive(:puts).with(default_output) + + subject + end + + context 'when tags are supplied' do + let(:hostname) { 'aiydvzpg23r415q' } + let(:response_body) do + { + hostname => { + 'template' => 'redhat-7-x86_64', + 'lifetime' => 48, + 'running' => 7.67, + 'state' => 'running', + 'tags' => { + 'user' => 'bob', + 'role' => 'agent', + }, + 'ip' => '127.0.0.1', + 'domain' => domain, + } + } + end + + it 'prints output with host fqdn, template, duration info, and tags' do + output = "- #{fqdn} (redhat-7-x86_64, 7.67/48 hours, user: bob, role: agent)" + + expect(STDOUT).to receive(:puts).with(output) + + subject + end + end + + context 'when print_to_stderr option is true' do + let(:print_to_stderr) { true } + + it 'outputs to stderr instead of stdout' do + expect(STDERR).to receive(:puts).with(default_output) + + subject + end + end + end + + describe 'with nonstandard pooler service' do + let(:service) { Service.new(MockOptions.new, 'url' => url, 'type' => 'ns') } + + let(:hostname) { 'sol11-9.delivery.mycompany.net' } + let(:response_body) do + { + hostname => { + 'fqdn' => hostname, + 'os_triple' => 'solaris-11-sparc', + 'reserved_by_user' => 'first.last', + 'reserved_for_reason' => '', + 'hours_left_on_reservation' => 35.89, + } + } + end + + let(:default_output) { "- #{hostname} (solaris-11-sparc, 35.89h remaining)" } + + it 'prints output with host, template, and time remaining' do + expect(STDOUT).to receive(:puts).with(default_output) + + subject + end + + context 'when reason is supplied' do + let(:response_body) do + { + hostname => { + 'fqdn' => hostname, + 'os_triple' => 'solaris-11-sparc', + 'reserved_by_user' => 'first.last', + 'reserved_for_reason' => 'testing', + 'hours_left_on_reservation' => 35.89, + } + } + end + + it 'prints output with host, template, time remaining, and reason' do + output = '- sol11-9.delivery.mycompany.net (solaris-11-sparc, 35.89h remaining, reason: testing)' + + expect(STDOUT).to receive(:puts).with(output) + + subject + end + end + + context 'when print_to_stderr option is true' do + let(:print_to_stderr) { true } + + it 'outputs to stderr instead of stdout' do + expect(STDERR).to receive(:puts).with(default_output) + + subject + end + end + end + + describe 'with ABS service' do + let(:service) { Service.new(MockOptions.new, 'url' => url, 'type' => 'abs') } + + let(:hostname) { '1597952189390' } + let(:fqdn) { 'example-noun.delivery.mycompany.net' } + let(:fqdn_hostname) {'example-noun'} + let(:template) { 'ubuntu-1604-x86_64' } + + # This seems to be the miminal stub response from ABS for the current output + let(:response_body) do + { + hostname => { + 'state' => 'allocated', + 'allocated_resources' => [ + { + 'hostname' => fqdn, + 'type' => template, + 'engine' => 'vmpooler', + }, + ], + 'request' => { + 'job' => { + 'id' => hostname, + } + }, + } + } + end + + # The vmpooler response contains metadata that is printed + let(:domain) { 'delivery.mycompany.net' } + let(:response_body_vmpooler) do + { + fqdn_hostname => { + 'template' => template, + 'lifetime' => 48, + 'running' => 7.67, + 'state' => 'running', + 'tags' => { + 'user' => 'bob', + 'role' => 'agent', + }, + 'ip' => '127.0.0.1', + 'domain' => domain, + } + } + end + + before(:each) do + allow(Utils).to receive(:get_vmpooler_service_config).and_return({ + 'url' => 'http://vmpooler.example.com', + 'token' => 'krypto-knight' + }) + allow(service).to receive(:query) + .with(anything, fqdn_hostname) + .and_return(response_body_vmpooler) + end + + let(:default_output_first_line) { "- [JobID:#{hostname}] " } + let(:default_output_second_line) { " - #{fqdn} (#{template}, 7.67/48 hours, user: bob, role: agent)" } + + it 'prints output with job id, host, and template' do + expect(STDOUT).to receive(:puts).with(default_output_first_line) + expect(STDOUT).to receive(:puts).with(default_output_second_line) + + subject + end + + context 'when print_to_stderr option is true' do + let(:print_to_stderr) { true } + + it 'outputs to stderr instead of stdout' do + expect(STDERR).to receive(:puts).with(default_output_first_line) + expect(STDERR).to receive(:puts).with(default_output_second_line) + + subject + end + end + end + + describe 'with ABS service returning vmpooler and nspooler resources' do + let(:service) { Service.new(MockOptions.new, 'url' => url, 'type' => 'abs') } + + let(:hostname) { '1597952189390' } + let(:fqdn) { 'this-noun.delivery.mycompany.net' } + let(:fqdn_ns) { 'that-noun.delivery.mycompany.net' } + let(:fqdn_hostname) {'this-noun'} + let(:fqdn_ns_hostname) {'that-noun'} + let(:template) { 'ubuntu-1604-x86_64' } + let(:template_ns) { 'solaris-10-sparc' } + + # This seems to be the miminal stub response from ABS for the current output + let(:response_body) do + { + hostname => { + 'state' => 'allocated', + 'allocated_resources' => [ + { + 'hostname' => fqdn, + 'type' => template, + 'engine' => 'vmpooler', + }, + { + 'hostname' => fqdn_ns, + 'type' => template_ns, + 'engine' => 'nspooler', + }, + ], + 'request' => { + 'job' => { + 'id' => hostname, + } + }, + } + } + end + + # The vmpooler response contains metadata that is printed + let(:domain) { 'delivery.mycompany.net' } + let(:response_body_vmpooler) do + { + fqdn_hostname => { + 'template' => template, + 'lifetime' => 48, + 'running' => 7.67, + 'state' => 'running', + 'tags' => { + 'user' => 'bob', + 'role' => 'agent', + }, + 'ip' => '127.0.0.1', + 'domain' => domain, + } + } + end + + before(:each) do + allow(Utils).to receive(:get_vmpooler_service_config).and_return({ + 'url' => 'http://vmpooler.example.com', + 'token' => 'krypto-knight' + }) + allow(service).to receive(:query) + .with(anything, fqdn_hostname) + .and_return(response_body_vmpooler) + end + + let(:default_output_first_line) { "- [JobID:#{hostname}] " } + let(:default_output_second_line) { " - #{fqdn} (#{template}, 7.67/48 hours, user: bob, role: agent)" } + let(:default_output_third_line) { " - #{fqdn_ns} (#{template_ns})" } + + it 'prints output with job id, host, and template' do + expect(STDOUT).to receive(:puts).with(default_output_first_line) + expect(STDOUT).to receive(:puts).with(default_output_second_line) + expect(STDOUT).to receive(:puts).with(default_output_third_line) + + subject + end + + context 'when print_to_stderr option is true' do + let(:print_to_stderr) { true } + + it 'outputs to stderr instead of stdout' do + expect(STDERR).to receive(:puts).with(default_output_first_line) + expect(STDERR).to receive(:puts).with(default_output_second_line) + expect(STDERR).to receive(:puts).with(default_output_third_line) + + subject + end + end end end