#!/usr/bin/env ruby # # William Stearns # Copyright (c) 2013, William Stearns # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the CloudPassage, Inc. nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL CLOUDPASSAGE, INC. BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED ANDON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Based on: # demo ruby cloudpassage API stuff # Tim Spencer # Thanks, Tim! # # you may need to install the oauth2, rest-client, and json gems with: # sudo gem install oauth2 rest-client json #Version 0.9 #======== User-modifiable values api_key_file = '/etc/halo-api-keys' default_host = 'api.cloudpassage.com' #Timeouts manually extended to handle long setup time for large numbers #of events. Set to -1 to wait forever (although nat, proxies, and load #balancers may cut you off externally. timeout=600 open_timeout=600 #Add the directory holding this script to the search path so we can find wlslib.rb $:.unshift File.dirname(__FILE__) #======== End of user-modifiable values #======== Functions def server_ids_of(group_matches,server_matches,tag_matches,api_hosts,one_client_id,timeout,open_timeout,token,server_names) #Note: server_names is modified and passed back as a parameter. server_id_list = [ ] if (group_matches.length > 0) or (tag_matches.length > 0) groups_json = api_get("https://#{api_hosts[one_client_id]}/v1/groups",timeout,open_timeout,token) groups_to_harvest = [ ] groups_json['groups'].each do |one_group| group_matches.each do |one_match| if one_group['name'].to_s.downcase.match(one_match.to_s.downcase) $stderr.puts "Adding all servers in group #{one_group['name']}" groups_to_harvest << one_group['id'] unless groups_to_harvest.include?(one_group['id']) end end tag_matches.each do |one_match| if one_group['tag'].to_s.downcase.match(one_match.to_s.downcase) $stderr.puts "Adding all servers in group #{one_group['name']}" groups_to_harvest << one_group['id'] unless groups_to_harvest.include?(one_group['id']) end end end groups_to_harvest.each do |one_group_id| servers_json = api_get("https://#{api_hosts[one_client_id]}/v1/groups/#{one_group_id}/servers",timeout,open_timeout,token) servers_json['servers'].each do |one_server| if ! server_id_list.include?(one_server['id']) server_id_list << one_server['id'] server_names[one_server['id']] = one_server['hostname'] end end end end if server_matches.length > 0 all_servers_json = api_get("https://#{api_hosts[one_client_id]}/v1/servers",timeout,open_timeout,token) all_servers_json['servers'].each do |one_server| server_matches.each do |one_match| if one_server['hostname'].to_s.downcase.match(one_match.to_s.downcase) if ! server_id_list.include?(one_server['id']) $stderr.puts "Adding server #{one_server['hostname']}" server_id_list << one_server['id'] server_names[one_server['id']] = one_server['hostname'] end end end end end return server_id_list end #======== End of functions #======== Loadable modules require 'rubygems' require 'optparse' require 'oauth2' require 'rest-client' require 'json' load 'wlslib.rb' #======== End of loadable modules #======== Initialization api_client_ids = [ ] api_secrets = { } api_hosts = { } my_proxy = nil partial_uri = "" pretty = false method = "GET" group_matches = [ ] server_matches = [ ] tag_matches = [ ] default_key = "" #======== End of initialization optparse = OptionParser.new do |opts| opts.banner = "Place a the same API request to multiple servers via the Halo API, printing the response in json format on stdout. Usage: call-servers.rb [options]" opts.on("-i keyid", "--api_client_id keyid", "API Key ID (can be read only or full access). If no key specified, use first key. If ALL , use all keys.") do |keyid| api_client_ids << keyid unless api_client_ids.include?(keyid) end opts.on("-g group_match", "Work with all groups with this text in the group name.") do |group_match| group_matches << group_match unless group_matches.include?(group_match) end opts.on("-t tag_match", "Work with all groups with this text in the server tag.") do |tag_match| tag_matches << tag_match unless tag_matches.include?(tag_match) end opts.on("-s server_match", "Work with all servers with this text in the server name.") do |server_match| server_matches << server_match unless server_matches.include?(server_match) end opts.on("-u partial_uri", "Partial URI to request, should be everything after /{server_id}/ . Good practice to put your partial URI in single quotes.") do |uri_param| partial_uri = uri_param end opts.on("-m method", "HTTP method to use (default is GET). Other options: PUT, POST, and DELETE. With PUT and POST, you must provide a payload on stdin.") do |req_method| method = req_method end opts.on("-p", "--pretty", "Show pretty-printed, indented nicely (default is unformatted on one line)") do pretty = true end opts.on_tail("-h", "--help", "Show help text") do $stderr.puts opts exit end end optparse.parse! default_key = load_api_keys(api_key_file,api_secrets,api_hosts,default_host) if default_key == "" $stderr.puts "Unable to load any keys from #{api_key_file}, exiting." exit 1 end #Validate all user params if (api_client_ids.length == 0) $stderr.puts "No key requested on command line; using the first valid key in #{api_key_file}, #{default_key}." api_client_ids << default_key elsif (api_client_ids.include?('ALL')) or (api_client_ids.include?('All')) or (api_client_ids.include?('all')) $stderr.puts "\"ALL\" requested; using all available keys in #{api_key_file}: #{api_secrets.keys.join(',')}" api_client_ids = api_secrets.keys.sort end if (group_matches.length == 0) and (server_matches.length == 0) $stderr.puts "Must have at least one group match or server match, exiting." exit 1 end #To accomodate a proxy, we need to handle both RestClient with the #following one-time statement, and also as a :proxy parameter to the #oauth2 call below. if ENV['https_proxy'].to_s.length > 0 my_proxy = ENV['https_proxy'] RestClient.proxy = my_proxy $stderr.puts "Using proxy: #{RestClient.proxy}" end payload = "" case method when "PUT", "POST" payload = $stdin.read #Do NOT convert payload to json format; RestClient needs to hand it as a string. end #puts JSON.pretty_generate(payload) #exit 1 api_client_ids.each do |one_client_id| if (api_secrets[one_client_id].to_s.length == 0) $stderr.puts "Invalid or missing api_client_secret for key id #{one_client_id}, skipping this key." $stderr.puts "The mode 600 file #{api_key_file} should contain one line per key ID/secret like:" $stderr.puts "myid1|mysecret1" $stderr.puts "myid2|mysecret2[|optional apihost:port]" else $stderr.puts "Making requests to #{api_hosts[one_client_id]} using key #{one_client_id}" ##Use a simple file lock to make sure that for a given API key only one ##copy of the script is running at a time. If you're working with 5 API ##keys that means you could have up to 5 copies running at once, one for ##each key. #lock_file = "/tmp/call-servers-#{one_client_id}.lock" #File.open(lock_file, "a") {} #unless File.new(lock_file).flock( File::LOCK_NB | File::LOCK_EX ) # $stderr.puts "It appears another copy of this script is running and holds the lock on #{lock_file}. Exiting." # exit #end #Acquire a session key from the Halo Portal for use by the rest of this script token = get_auth_token(one_client_id,api_secrets[one_client_id],my_proxy,api_hosts[one_client_id]) if token == "" $stderr.puts "Unable to retrieve a token, skipping account #{one_client_id}." else server_names = { } server_ids_of(group_matches,server_matches,tag_matches,api_hosts,one_client_id,timeout,open_timeout,token,server_names).each do |one_id| puts "#======== #{server_names[one_id]}" #Place an API request to the Halo grid. case method when "DELETE", "delete" data = api_delete("https://#{api_hosts[one_client_id]}/v1/servers/#{one_id}/#{partial_uri}",timeout,open_timeout,token) when "GET", "get" data = api_get("https://#{api_hosts[one_client_id]}/v1/servers/#{one_id}/#{partial_uri}",timeout,open_timeout,token) when "POST", "post" data = api_post("https://#{api_hosts[one_client_id]}/v1/servers/#{one_id}/#{partial_uri}",timeout,open_timeout,token,payload) when "PUT", "put" data = api_put("https://#{api_hosts[one_client_id]}/v1/servers/#{one_id}/#{partial_uri}",timeout,open_timeout,token,payload) else $stderr.puts "Unhandled method type: #{method}. Exiting." exit 1 end if pretty puts JSON.pretty_generate(data) else p data end end end end end $stderr.puts " Complete."