Merge pull request #90 from scotje/add_zsh_completions_new

Add tab completion script for zsh and fix bash completion for ABS services
This commit is contained in:
Gene Liverman 2020-09-22 11:56:31 -04:00 committed by GitHub
commit bc1ea2e2d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 452 additions and 80 deletions

View file

@ -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).

View file

@ -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}") )

View file

@ -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

View file

@ -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)

View file

@ -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
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
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)
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

View file

@ -162,34 +162,135 @@ 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 => {
subject { Utils.print_fqdn_for_host(service, hostname, host_data) }
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('.') }
let(:host_data) do
{
'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)'
expect(STDOUT).to receive(:puts).with(output)
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)
'domain' => domain,
}
end
it 'prints a vmpooler output with host fqdn, template, duration info, and tags when supplied' do
hostname = 'aiydvzpg23r415q'
response_body = { hostname => {
it 'outputs fqdn for host' do
expect(STDOUT).to receive(:puts).with(fqdn)
subject
end
end
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
it 'outputs fqdn for host' do
expect(STDOUT).to receive(:puts).with(fqdn)
subject
end
end
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' }
# 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
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(anything, hostname)
.and_return(response_body)
end
subject { Utils.pretty_print_hosts(verbose, service, hostname, print_to_stderr) }
describe 'with vmpooler service' do
let(:service) { Service.new(MockOptions.new, 'url' => url) }
let(:hostname) { 'mcpy42eqjxli9g2' }
let(:domain) { 'delivery.mycompany.net' }
let(:fqdn) { [hostname, domain].join('.') }
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,
@ -199,60 +300,257 @@ describe Utils do
'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)'
'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)
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 => {
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,
} }
output = '- sol11-9.delivery.mycompany.net (solaris-11-sparc, 35.89h remaining)'
expect(STDOUT).to receive(:puts).with(output)
service = Service.new(MockOptions.new, 'url' => url, 'type' => 'ns')
allow(service).to receive(:query)
.with(nil, 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 => {
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)
service = Service.new(MockOptions.new, 'url' => url, 'type' => 'ns')
allow(service).to receive(:query)
.with(nil, hostname)
.and_return(response_body)
subject
end
end
Utils.pretty_print_hosts(nil, service, hostname)
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}] <allocated>" }
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}] <allocated>" }
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