diff --git a/Gemfile b/Gemfile index 6c10f3a..faec794 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' -gem 'thor' +gem 'commander' +gem 'faraday' gemspec diff --git a/README.md b/README.md index 77d10a9..c60aaba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ vmfloaty ======== -A CLI helper tool for Puppet Labs vmpooler to help you stay afloat. +A CLI helper tool for [Puppet Labs vmpooler](https://github.com/puppetlabs/vmpooler) to help you stay afloat. + + ## Install @@ -13,14 +15,43 @@ gem install vmfloaty ## Usage -_note:_ subject to change +``` + delete Schedules the deletion of a host or hosts + get Gets a vm or vms based on the os flag + help Display global or [command] help documentation + list Shows a list of available vms from the pooler + modify Modify a vms tags and TTL + query Get information about a given vm + revert Reverts a vm to a specified snapshot + snapshot Takes a snapshot of a given vm + status Prints the status of vmpooler + summary Prints the summary of vmpooler + token Retrieves or deletes a token + GLOBAL OPTIONS: + + -h, --help + Display help documentation + + -v, --version + Display version information + + -t, --trace + Display backtrace when an error occurs ``` -Commands: - floaty get # Gets a VM - floaty help [COMMAND] # Describe available commands or one specific command - floaty list [PATTERN] # List all open VMs - floaty modify # (TODO STILL) Modify a VM - floaty release [--all] # Schedules a VM for deletion - floaty status # (TODO STILL) List status of all active VMs + +### vmfloaty dotfile + +If you do not wish to continuely specify various config options with the cli, you can have a dotfile in your home directory for some defaults. For example: + +```yaml +#file at /Users/me/.vmpooler.yml +url: 'http://vmpooler.mycompany.net' +user: 'brian' ``` + +Now vmfloaty will use those config files if no flag was specified. + +## vmpooler API + +This cli tool uses the [vmpooler API](https://github.com/puppetlabs/vmpooler/blob/master/API.md). diff --git a/bin/floaty b/bin/floaty index 5c5a3f6..6fe5b10 100755 --- a/bin/floaty +++ b/bin/floaty @@ -1,6 +1,7 @@ #!/usr/bin/env ruby + $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) require 'vmfloaty' -Vmfloaty.new(ENV).start +Vmfloaty.new.run diff --git a/lib/vmfloaty.rb b/lib/vmfloaty.rb index da12900..b7e104c 100644 --- a/lib/vmfloaty.rb +++ b/lib/vmfloaty.rb @@ -1,17 +1,252 @@ -require 'vmfloaty/cli' -require 'vmfloaty/hosts' +#!/usr/bin/env ruby + +require 'rubygems' +require 'commander' +require 'yaml' +require 'vmfloaty/auth' +require 'vmfloaty/pooler' class Vmfloaty + include Commander::Methods - def initialize(env) - $vmpooler_url = env['VMPOOLER_URL'] + def run + program :version, '0.2.0' + program :description, 'A CLI helper tool for Puppet Labs vmpooler to help you stay afloat' - unless $vmpooler_url - $vmpooler_url = 'http://vcloud.delivery.puppetlabs.net' + config = read_config + + command :get do |c| + c.syntax = 'floaty get [options]' + c.summary = 'Gets a vm or vms based on the os flag' + c.description = '' + c.example 'Gets 3 vms', 'floaty get --user brian --url http://vmpooler.example.com --os centos,centos,debian' + c.option '--verbose', 'Enables verbose output' + c.option '--user STRING', String, 'User to authenticate with' + c.option '--url STRING', String, 'URL of vmpooler' + c.option '--token STRING', String, 'Token for vmpooler' + c.option '--os STRING', String, 'Operating systems to retrieve' + c.option '--notoken', 'Makes a request without a token' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + token = options.token || config['token'] + user = options.user ||= config['user'] + url = options.url ||= config['url'] + os_types = options.os + no_token = options.notoken + + unless no_token.nil? + response = Pooler.retrieve(verbose, os_types, token, url) + puts response + return + end + + unless options.token + pass = password "Enter your password please:", '*' + token = Auth.get_token(verbose, url, user, pass) + end + + unless os_types.nil? + response = Pooler.retrieve(verbose, os_types, token, url) + puts response + else + puts 'You did not provide an OS to get' + end + end end + + command :list do |c| + c.syntax = 'floaty list [options]' + c.summary = 'Shows a list of available vms from the pooler' + c.description = '' + c.example 'Filter the list on centos', 'floaty list --filter centos --url http://vmpooler.example.com' + c.option '--verbose', 'Enables verbose output' + c.option '--filter STRING', String, 'A filter to apply to the list' + c.option '--url STRING', String, 'URL of vmpooler' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + filter = options.filter + url = options.url ||= config['url'] + + os_list = Pooler.list(verbose, url, filter) + puts os_list + end + end + + command :query do |c| + c.syntax = 'floaty query [options]' + c.summary = 'Get information about a given vm' + c.description = '' + c.example 'Get information about a sample host', 'floaty query --url http://vmpooler.example.com --host myvmhost.example.com' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.option '--host STRING', String, 'Hostname to query' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + url = options.url ||= config['url'] + hostname = options.hostname + + query = Pooler.query(verbose, url, hostname) + puts query + end + end + + command :modify do |c| + c.syntax = 'floaty modify [options]' + c.summary = 'Modify a vms tags and TTL' + c.description = '' + c.example 'description', 'command example' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.option '--token STRING', String, 'Token for vmpooler' + c.option '--host STRING', String, 'Hostname to modify' + c.option '--lifetime INT', Integer, 'VM TTL (Integer, in hours)' + c.option '--tags HASH', Hash, 'free-form VM tagging' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + url = options.url ||= config['url'] + hostname = options.hostname + lifetime = options.lifetime + tags = options.tags + token = options.token + + res_body = Pooler.modify(verbose, url, hostname, token, lifetime, tags) + puts res_body + end + end + + command :delete do |c| + c.syntax = 'floaty delete [options]' + c.summary = 'Schedules the deletion of a host or hosts' + c.description = '' + c.example 'Schedules the deletion of a host or hosts', 'floaty delete --hosts myhost1,myhost2 --url http://vmpooler.example.com' + c.option '--verbose', 'Enables verbose output' + c.option '--hosts STRING', String, 'Hostname(s) to delete' + c.option '--url STRING', String, 'URL of vmpooler' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + hosts = options.hosts + url = options.url ||= config['url'] + + Pool.delete(verbose, url, hosts) + end + end + + command :snapshot do |c| + c.syntax = 'floaty snapshot [options]' + c.summary = 'Takes a snapshot of a given vm' + c.description = '' + c.example 'Takes a snapshot for a given host', 'floaty snapshot --url http://vmpooler.example.com --host myvm.example.com --token a9znth9dn01t416hrguu56ze37t790bl' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.option '--host STRING', String, 'Hostname to modify' + c.option '--token STRING', String, 'Token for vmpooler' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + url = options.url ||= config['url'] + hostname = options.hostname + token = options.token + + res_body = Pooler.snapshot(verbose, url, hostname, token) + puts res_body + end + end + + command :revert do |c| + c.syntax = 'floaty revert [options]' + c.summary = 'Reverts a vm to a specified snapshot' + c.description = '' + c.example 'Reverts to a snapshot for a given host', 'floaty revert --url http://vmpooler.example.com --host myvm.example.com --token a9znth9dn01t416hrguu56ze37t790bl --snapshot n4eb4kdtp7rwv4x158366vd9jhac8btq' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.option '--host STRING', String, 'Hostname to modify' + c.option '--token STRING', String, 'Token for vmpooler' + c.option '--snapshot STRING', String, 'SHA of snapshot' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + url = options.url ||= config['url'] + hostname = options.hostname + token = options.token + snapshot_sha = options.snapshot + + res_body = Pooler.revert(verbose, url, hostname, token, snapshot_sha) + puts res_body + end + end + + command :status do |c| + c.syntax = 'floaty status [options]' + c.summary = 'Prints the status of vmpooler' + c.description = '' + c.example 'Gets the current vmpooler status', 'floaty status --url http://vmpooler.example.com' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + url = options.url ||= config['url'] + + status = Pooler.status(verbose, url) + puts status + end + end + + command :summary do |c| + c.syntax = 'floaty summary [options]' + c.summary = 'Prints the summary of vmpooler' + c.description = '' + c.example 'Gets the current day summary of vmpooler', 'floaty summary --url http://vmpooler.example.com' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + url = options.url ||= config['url'] + + summary = Pooler.summary(verbose, url) + puts summary + end + end + + command :token do |c| + c.syntax = 'floaty token [get | delete | status]' + c.summary = 'Retrieves or deletes a token' + c.description = '' + c.example '', '' + c.option '--verbose', 'Enables verbose output' + c.option '--url STRING', String, 'URL of vmpooler' + c.option '--user STRING', String, 'User to authenticate with' + c.option '--token STRING', String, 'Token for vmpooler' + c.action do |args, options| + verbose = options.verbose || config['verbose'] + action = args.first + url = options.url ||= config['url'] + token = options.token + user = options.user ||= config['user'] + pass = password "Enter your password please:", '*' + + case action + when "get" + puts Auth.get_token(verbose, url, user, pass) + when "delete" + Auth.delete_token(verbose, url, user, pass, token) + when "status" + Auth.token_status(verbose, url, user, pass, token) + when nil + STDERR.puts "No action provided" + else + STDERR.puts "Unknown action: #{action}" + end + end + end + + run! end - def start - CLI.start(ARGV) + def read_config + conf = {} + begin + conf = YAML.load_file("#{Dir.home}/.vmfloaty.yml") + rescue + STDERR.puts "There was no config file at #{Dir.home}/.vmfloaty.yml" + end + conf end end diff --git a/lib/vmfloaty/auth.rb b/lib/vmfloaty/auth.rb new file mode 100644 index 0000000..4816e04 --- /dev/null +++ b/lib/vmfloaty/auth.rb @@ -0,0 +1,59 @@ +require 'faraday' +require 'json' +require 'vmfloaty/http' + +class Auth + def self.get_token(verbose, url, user, password) + conn = Http.get_conn_with_auth(verbose, url, user, password) + + resp = conn.post "/token" + + res_body = JSON.parse(resp.body) + if res_body["ok"] + return res_body["token"] + else + STDERR.puts "There was a problem with your request:" + puts res_body + exit 1 + end + end + + def self.delete_token(verbose, url, user, password, token) + if token.nil? + STDERR.puts 'You did not provide a token' + exit 1 + end + + conn = Http.get_conn_with_auth(verbose, url, user, password) + + response = conn.delete "/token/#{token}" + res_body = JSON.parse(response.body) + if res_body["ok"] + puts res_body + else + STDERR.puts "There was a problem with your request:" + puts res_body + exit 1 + end + end + + def self.token_status(verbose, url, user, password, token) + if token.nil? + STDERR.puts 'You did not provide a token' + exit 1 + end + + conn = Http.get_conn_with_auth(verbose, url, user, password) + + response = conn.get "/token/#{token}" + res_body = JSON.parse(response.body) + + if res_body["ok"] + puts res_body + else + STDERR.puts "There was a problem with your request:" + puts res_body + exit 1 + end + end +end diff --git a/lib/vmfloaty/cli.rb b/lib/vmfloaty/cli.rb deleted file mode 100644 index 5b41181..0000000 --- a/lib/vmfloaty/cli.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'thor' -require 'net/http' -require 'uri' -require 'json' - -class CLI < Thor - desc "get ", "Gets a VM" - def get(os_list) - # HTTP POST -d os_list vmpooler.company.com/vm - - uri = URI.parse("#{$vmpooler_url}/vm/#{os_list.gsub(",","+")}") - http = Net::HTTP.new(uri.host, uri.port) - request = Net::HTTP::Post.new(uri.request_uri) - response = http.request(request) - - if response.code.to_i == 200 - hosts = JSON.parse(response.body) - - # puts hosts - - save_hosts = {} - hosts.each do |k,v| - unless k == 'ok' || k == 'domain' - save_hosts[k] = v['hostname'] - end - end - - puts 'New Hosts:' - puts save_hosts - - #hosts.add_host save_hosts - end - - # parse host names/os's and save - end - - desc "modify ", "Modify a VM" - def modify(hostname) - say 'Modify a vm' - end - - desc "status", "List status of all active VMs" - def status - #$hosts.print_host_list - end - - desc "list [PATTERN]", "List all open VMs" - def list(pattern=nil) - # HTTP GET vmpooler.company.com/vm - uri = URI.parse("#{$vmpooler_url}/vm") - response = Net::HTTP.get_response(uri) - host_res = JSON.parse(response.body) - - if pattern - # Filtering VMs based on pattern - hosts = host_res.select { |i| i[/#{pattern}/] } - else - hosts = host_res - end - - puts hosts - end - - desc "release [--all]", "Schedules a VM for deletion" - option :all - def release(hostname_list=nil) - # HTTP DELETE vmpooler.company.com/vm/#{hostname} - # { "ok": true } - - if options[:all] - # release all hosts managed by vmfloaty - else - hostname_arr = hostname_list.split(',') - - hostname_arr.each do |hostname| - say "Releasing host #{hostname}..." - uri = URI.parse("#{$vmpooler_url}/vm/#{hostname}") - http = Net::HTTP.new(uri.host, uri.port) - request = Net::HTTP::Delete.new(uri.request_uri) - response = http.request(request) - res = JSON.parse(response.body) - - puts res - end - end - end -end diff --git a/lib/vmfloaty/hosts.rb b/lib/vmfloaty/hosts.rb deleted file mode 100644 index 5ca90a5..0000000 --- a/lib/vmfloaty/hosts.rb +++ /dev/null @@ -1,29 +0,0 @@ -# manage hosts used from vm pooler -class Hosts - def initialize - @host_list = {} - end - - def add_host(host_hash) - host_hash.each do |k,v| - if @host_list[k] - @host_list[k].push(v) - else - if v.is_a?(Array) - @host_list[k] = v - else - @host_list[k] = [v] - end - end - end - - puts @host_list - end - - def remove_host(host_id) - end - - def print_host_list - puts @host_list - end -end diff --git a/lib/vmfloaty/http.rb b/lib/vmfloaty/http.rb new file mode 100644 index 0000000..ecc6b2f --- /dev/null +++ b/lib/vmfloaty/http.rb @@ -0,0 +1,60 @@ +require 'faraday' + +class Http + def self.get_conn(verbose, url) + if url.nil? + STDERR.puts "The url you provided was empty" + exit 1 + end + + conn = Faraday.new(:url => url, :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.response :logger if verbose + faraday.adapter Faraday.default_adapter + end + + return conn + end + + def self.get_conn_with_auth(verbose, url, user, password) + if url.nil? + STDERR.puts "The url you provided was empty" + exit 1 + end + + if user.nil? + STDERR.puts "You did not provide a user to authenticate with" + exit 1 + end + + conn = Faraday.new(:url => url, :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.request :basic_auth, user, password + faraday.response :logger if verbose + faraday.adapter Faraday.default_adapter + end + + return conn + end + + def self.get_conn_with_token(verbose, url, token) + if url.nil? + STDERR.puts "The url you provided was empty" + exit 1 + end + + if token.nil? + STDERR.puts "The token you provided was empty" + exit 1 + end + + conn = Faraday.new(:url => url, :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.request :token_auth, token + faraday.response :logger if verbose + faraday.adapter Faraday.default_adapter + end + + return conn + end +end diff --git a/lib/vmfloaty/pooler.rb b/lib/vmfloaty/pooler.rb new file mode 100644 index 0000000..25b79c7 --- /dev/null +++ b/lib/vmfloaty/pooler.rb @@ -0,0 +1,100 @@ +require 'faraday' +require 'vmfloaty/http' +require 'json' + +class Pooler + def self.list(verbose, url, os_filter=nil) + conn = Http.get_conn(verbose, url) + + response = conn.get '/vm' + response_body = JSON.parse(response.body) + + if os_filter + hosts = response_body.select { |i| i[/#{os_filter}/] } + else + hosts = response_body + end + + hosts + end + + def self.retrieve(verbose, os_type, token, url) + os = os_type.gsub(',','+') + if token.nil? + conn = Http.get_conn(verbose, url) + else + conn = Http.get_conn_with_token(verbose, url, token) + conn.headers['X-AUTH-TOKEN'] + end + + response = conn.post "/vm/#{os}" + + res_body = JSON.parse(response.body) + res_body + end + + def self.modify(verbose, url, hostname, token, lifetime, tags) + modify_body = {'lifetime'=>lifetime, 'tags'=>tags} + conn = Http.get_conn_with_token(verbose, url, token) + + response = conn.put do |req| + req.url "/vm/#{hostname}" + end + res_body = JSON.parse(response.body) + + res_body + end + + def self.delete(verbose, url, hostname) + hosts = hostnames.split(',') + conn = Http.get_conn(verbose, url) + + hosts.each do |host| + puts "Scheduling host #{host} for deletion" + response = conn.delete "/vm/#{host}" + res_body = JSON.parse(response.body) + puts res_body + end + end + + def self.status(verbose, url) + conn = Http.get_conn(verbose, url) + + response = conn.get '/status' + res_body = JSON.parse(response.body) + res_body + end + + def self.summary(verbose, url) + conn = Http.get_conn(verbose, url) + + response = conn.get '/summary' + res_body = JSON.parse(response.body) + res_body + end + + def self.query(verbose, url, hostname) + conn = Http.get_conn(verbose, url) + + response = conn.get "/vm/#{hostname}" + res_body = JSON.parse(response.body) + + res_body + end + + def self.snapshot(verbose, url, hostname, token) + conn = Http.get_conn_with_token(verbose, url, token) + + response = conn.post "/vm/#{hostname}/snapshot" + res_body = JSON.parse(response.body) + res_body + end + + def self.revert(verbose, url, hostname, token, snapshot_sha) + conn = Http.get_conn_with_token(verbose, url, token) + + response = conn.post "/vm/#{hostname}/snapshot/#{snapshot}" + res_body = JSON.parse(response.body) + res_body + end +end diff --git a/lib/vmfloaty/version.rb b/lib/vmfloaty/version.rb deleted file mode 100644 index 7da0994..0000000 --- a/lib/vmfloaty/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Vmfloaty - module CLI - VERSION = '0.1.0' - end -end diff --git a/vmfloaty.gemspec b/vmfloaty.gemspec index f20500b..c00e709 100644 --- a/vmfloaty.gemspec +++ b/vmfloaty.gemspec @@ -1,8 +1,6 @@ -require File.expand_path '../lib/vmfloaty/version', __FILE__ - Gem::Specification.new do |s| s.name = 'vmfloaty' - s.version = Vmfloaty::CLI::VERSION.dup + s.version = '0.2.0' s.authors = ['Brian Cain'] s.email = ['brian.cain@puppetlabs.com'] s.license = 'Apache' @@ -13,5 +11,4 @@ Gem::Specification.new do |s| s.files = Dir['LICENSE', 'README.md', 'lib/**/*'] s.test_files = Dir['spec/**/*'] s.require_path = 'lib' - s.add_dependency 'thor', '~> 0.19' end diff --git a/vmfloaty.yml.example b/vmfloaty.yml.example new file mode 100644 index 0000000..7728537 --- /dev/null +++ b/vmfloaty.yml.example @@ -0,0 +1,2 @@ +url: 'http://vmpooler.example.com' +user: 'brian'