collins-cli 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 087cbec35df74ca5deb78665d0d40547483968d3
4
+ data.tar.gz: dcc8cc251bc58b3e6ca6df9881de6eeffef00caf
5
+ SHA512:
6
+ metadata.gz: 5d3b4860eede427118996a3740233e494df10b0bc46c94a2394f01d78fb1d20b3796a528d8353900bb8774f655f7bf9e86d24594c331b7fa56ac22b71471b319
7
+ data.tar.gz: 19aa32592ffcf6cac34565ae87ca37bcd35c7ecc182138c2464bcceba65898258ac25fb11853f3d29fb627fe16c589fe5824bba081c7e60e6e70ff7857a6a4a8
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ collins-cli
2
+ ===========
3
+
4
+ CLI scripts for interacting with Collins API
5
+
6
+ [![Gem Version](https://badge.fury.io/rb/collins-cli.svg)](http://badge.fury.io/rb/collins-cli)
7
+
8
+ ## Overview
9
+
10
+ Main entry point is the ```collins``` binary.
11
+
12
+ Usage: collins <command> [options]
13
+ Available commands:
14
+ query, find: Search for assets in Collins
15
+ modify, set: Add and remove attributes, change statuses, and log to assets
16
+ log: Display log messages on assets
17
+ provision, action: Provision, control power status, allocate IPs, update IPMI info
18
+
19
+ ## Searching - collins-find
20
+
21
+ Usage: collins-find [options] [hostnamepattern]
22
+ Query options:
23
+ -t, --tag TAG[,...] Assets with tag[s] TAG
24
+ -T, --type TYPE Only show assets with type TYPE
25
+ -n, --nodeclass NODECLASS[,...] Assets in nodeclass NODECLASS
26
+ -p, --pool POOL[,...] Assets in pool POOL
27
+ -s, --size SIZE Number of assets to return (Default: 9999)
28
+ -r, --role ROLE[,...] Assets in primary role ROLE
29
+ -R, --secondary-role ROLE[,...] Assets in secondary role ROLE
30
+ -i, --ip-address IP[,...] Assets with IP address[es]
31
+ -S STATUS[:STATE][,...], Asset status (and optional state after :)
32
+ --status
33
+ -a attribute[:value[,...]], Arbitrary attributes and values to match in query. : between key and value
34
+ --attribute
35
+
36
+ Table formatting:
37
+ -H, --show-header Show header fields in output
38
+ -c, --columns ATTRIBUTES Attributes to output as columns, comma separated (Default: tag,hostname,nodeclass,status,pool,primary_role,secondary_role)
39
+ -x, --extra-columns ATTRIBUTES Show these columns in addition to the default columns, comma separated
40
+ -f, --field-separator SEPARATOR Separator between columns in output (Default: )
41
+
42
+ Robot formatting:
43
+ -l, --link Output link to assets found in web UI
44
+ -j, --json Output results in JSON (NOTE: This probably wont be what you expected)
45
+ -y, --yaml Output results in YAML
46
+
47
+ Extra options:
48
+ --expire SECONDS Timeout in seconds (0 == forever)
49
+ -C, --config CONFIG Use specific Collins config yaml for Collins::Client
50
+ -h, --help Help
51
+
52
+ Examples:
53
+ Query for devnodes in DEVEL pool that are VMs
54
+ cf -n develnode -p DEVEL
55
+ Query for asset 001234, and show its system_password
56
+ cf -t 001234 -x system_password
57
+ Query for all decommissioned VM assets
58
+ cf -a is_vm:true -S decommissioned
59
+ Query for hosts matching hostname '^web6-'
60
+ cf ^web6-
61
+ Query for all develnode6 nodes with a value for PUPPET_SERVER
62
+ cf -n develnode6 -a puppet_server -H
63
+
64
+ ## Logging - collins-log
65
+
66
+ Usage: collins-log [options]
67
+ -a, --all Show logs from ALL assets
68
+ -n, --number LINES Show the last LINES log entries. (Default: 20)
69
+ -t, --tags TAGS Tags to work on, comma separated
70
+ -f, --follow Poll for logs every 2 seconds
71
+ -s, --severity SEVERITY[,...] Log severities to return (Defaults to all). Use !SEVERITY to exclude one.
72
+ -C, --config CONFIG Use specific Collins config yaml for Collins::Client
73
+ -h, --help Help
74
+
75
+ Severities:
76
+ EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFORMATIONAL, DEBUG, NOTE
77
+
78
+ Examples:
79
+ Show last 20 logs for an asset
80
+ collins-log -t 001234
81
+ Show last 100 logs for an asset
82
+ collins-log -t 001234 -n100
83
+ Show last 10 logs for 2 assets that are ERROR severity
84
+ collins-log -t 001234,001235 -n10 -sERROR
85
+ Show last 10 logs all assets that are not note or informational severity
86
+ collins-log -a -n10 -s'!informational,!note'
87
+ Show last 10 logs for all web nodes that are provisioned having verification in the message
88
+ cf -S provisioned -n webnode$ | collins-log -n10 -s debug | grep -i verification
89
+
90
+ ## Modification - collins-modify
91
+
92
+ Usage: collins-modify [options]
93
+ -a attribute:value, Set attribute=value. : between key and value. attribute will be uppercased.
94
+ --set-attribute
95
+ -d, --delete-attribute attribute Delete attribute.
96
+ -S, --set-status status[:state] Set status (and optionally state) to status:state. Requires --reason
97
+ -r, --reason REASON Reason for changing status/state.
98
+ -l, --log MESSAGE Create a log entry.
99
+ -L, --level LEVEL Set log level. Default level is NOTE.
100
+ -t, --tags TAGS Tags to work on, comma separated
101
+ -C, --config CONFIG Use specific Collins config yaml for Collins::Client
102
+ -h, --help Help
103
+
104
+ Allowed values (uppercase or lowercase is accepted):
105
+ Status (-S,--set-status):
106
+ ALLOCATED, CANCELLED, DECOMMISSIONED, INCOMPLETE, MAINTENANCE, NEW, PROVISIONED, PROVISIONING, UNALLOCATED
107
+ States (-S,--set-status):
108
+ ALLOCATED ->
109
+ CLAIMED, SPARE, RUNNING_UNMONITORED, UNMONITORED
110
+ MAINTENANCE ->
111
+ AWAITING_REVIEW, HARDWARE_PROBLEM, HW_TESTING, HARDWARE_UPGRADE, IPMI_PROBLEM, MAINT_NOOP, NETWORK_PROBLEM, RELOCATION, PROVISIONING_PROBLEM
112
+ ANY ->
113
+ RUNNING, STARTING, STOPPING, TERMINATED
114
+ Log levels (-L,--level):
115
+ EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFORMATIONAL, DEBUG, NOTE
116
+
117
+ Examples:
118
+ Set an attribute on some hosts:
119
+ collins-modify -t 001234,004567 -a my_attribute:true
120
+ Delete an attribute on some hosts:
121
+ collins-modify -t 001234,004567 -d my_attribute
122
+ Delete and add attribute at same time:
123
+ collins-modify -t 001234,004567 -a new_attr:test -d old_attr
124
+ Set machine into maintenace noop:
125
+ collins-modify -t 001234 -S maintenance:maint_noop -r "I do what I want"
126
+ Set machine back to allocated:
127
+ collins-modify -t 001234 -S allocated:running -r "Back to allocated"
128
+ Set machine back to new without setting state:
129
+ collins-modify -t 001234 -S new -r "Dunno why you would want this"
130
+ Create a log entry:
131
+ collins-modify -t 001234 -l'computers are broken and everything is horrible' -Lwarning
132
+ Read from stdin:
133
+ cf -n develnode | collins-modify -d my_attribute
134
+ cf -n develnode -S allocated | collins-modify -a collectd_version:5.2.1-52
135
+ echo -e "001234\n001235\n001236"| collins-modify -a test_attribute:'hello world'
136
+
137
+ ## Actions - collins-action
138
+
139
+ Usage: collins-action [options]
140
+ Actions:
141
+ -P, --provision Provision assets (see Provisioning flags).
142
+ -S, --power-status Show IPMI power status.
143
+ -A, --power-action ACTION Perform IPMI power ACTION on assets
144
+
145
+ Provisioning Flags:
146
+ -n, --nodeclass NODECLASS Nodeclass to provision as. (Required)
147
+ -p, --pool POOL Provision with pool POOL.
148
+ -r, --role ROLE Provision with primary role ROLE.
149
+ -R, --secondary-role ROLE Provision with secondary role ROLE.
150
+ -s, --suffix SUFFIX Provision with suffix SUFFIX.
151
+ -a, --activate Activate server on provision (useful with SL plugin) (Default: ignored)
152
+ -b, --build-contact USER Build contact. (Default: gabe)
153
+
154
+ General:
155
+ -t, --tags TAG[,...] Tags to work on, comma separated
156
+ -C, --config CONFIG Use specific Collins config yaml for Collins::Client
157
+ -h, --help Help
158
+
159
+ Examples:
160
+ Provision some machines:
161
+ cf -Sunallocated -arack_position:716|collins-action -P -napiwebnode6 -RALL
162
+ Show power status:
163
+ cf ^dev6-gabe|collins-action -S
164
+ Power cycle a bunch of machines:
165
+ collins-action -t 001234,004567,007890 -A reboot
166
+
167
+ ## TODO
168
+
169
+ I know the architecture is BRUTAL. This was all organically created; I need to refactor stuff out into libraries to facilitate code sharing between the utilities
170
+
171
+ * Implement IP allocation in collins-action
172
+ * Implement IPMI stuff in collins-action
173
+ * Share code between binaries
data/bin/collins ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ALLOWED_ACTIONS = {
4
+ :find => ['query','find'],
5
+ :modify => ['modify','set'],
6
+ :log => ['log'],
7
+ :action => ['provision','action']
8
+ }
9
+ action = ARGV.shift
10
+ target,_ = ALLOWED_ACTIONS.select {|k,v| v.any? {|handle| ! %r|^#{action}|.match(handle).nil? } }.first
11
+ if action.nil? || target.nil?
12
+ abort <<_MESSAGE_
13
+ Usage: #{File.basename(File.realpath($0))} <command> [options]
14
+ Available commands:
15
+ query, find: Search for assets in Collins
16
+ modify, set: Add and remove attributes, change statuses, and log to assets
17
+ log: Display log messages on assets
18
+ provision, action: Provision, control power status, allocate IPs, update IPMI info
19
+ _MESSAGE_
20
+ end
21
+ exec File.join(__dir__,"collins-#{target}"), *ARGV
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+ # collins-action
3
+ # provision and manage assets in collins easily from the CLI
4
+
5
+ require 'collins_auth'
6
+ require 'yaml'
7
+ require 'optparse'
8
+ require 'etc'
9
+ require 'colorize'
10
+
11
+ SUCCESS = "SUCCESS".colorize(:color => :green)
12
+ ERROR = "ERROR".colorize(:color => :red)
13
+ ALLOWABLE_POWER_ACTIONS = ['reboot','rebootsoft','reboothard','on','off','poweron','poweroff','identify']
14
+ options = {
15
+ :timeout => 120,
16
+ :build_contact => Etc.getlogin,
17
+ :provision => { }
18
+ }
19
+
20
+ basename = File.basename(File.realpath($0))
21
+ parser = OptionParser.new do |opts|
22
+ opts.banner = "Usage: #{basename} [options]"
23
+ #TODO -s to show provisoining_profiles
24
+ #TODO update IPMI stuff with ipmi_update
25
+ #TODO create IPMI with ipmi_create
26
+ #TODO implement IP allocation
27
+ opts.separator "Actions:"
28
+ opts.on('-P','--provision',"Provision assets (see Provisioning flags).") {|v| options[:mode] = :provision }
29
+ opts.on('-S','--power-status',"Show IPMI power status.") {|v| options[:mode] = :power_status }
30
+ opts.on('-A','--power-action ACTION',String,"Perform IPMI power ACTION on assets"){|v| options[:mode] = :power ; options[:power_action] = v}
31
+
32
+ opts.separator ""
33
+ opts.separator "Provisioning Flags:"
34
+ opts.on('-n','--nodeclass NODECLASS',String,"Nodeclass to provision as. (Required)") {|v| options[:provision][:nodeclass] = v }
35
+ opts.on('-p','--pool POOL',String,"Provision with pool POOL.") {|v| options[:provision][:pool] = v }
36
+ opts.on('-r','--role ROLE',String,"Provision with primary role ROLE.") {|v| options[:provision][:primary_role] = v }
37
+ opts.on('-R','--secondary-role ROLE',String,"Provision with secondary role ROLE.") {|v| options[:provision][:secondary_role] = v }
38
+ opts.on('-s','--suffix SUFFIX',String,"Provision with suffix SUFFIX.") {|v| options[:provision][:suffix] = v }
39
+ opts.on('-a','--activate',"Activate server on provision (useful with SL plugin) (Default: ignored)") {|v| options[:provision][:activate] = true }
40
+ opts.on('-b','--build-contact USER',String,"Build contact. (Default: #{options[:build_contact]})") {|v| options[:build_contact] = v }
41
+
42
+ opts.separator ""
43
+ opts.separator "General:"
44
+ opts.on('-t','--tags TAG[,...]',Array,"Tags to work on, comma separated") {|v| options[:tags] = v.map(&:to_sym)}
45
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| options[:config] = v}
46
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
47
+
48
+ opts.separator ""
49
+ opts.separator "Examples:"
50
+ opts.separator <<_EXAMPLES_
51
+ Provision some machines:
52
+ cf -Sunallocated -arack_position:716|#{basename} -P -napiwebnode6 -RALL
53
+ Show power status:
54
+ cf ^dev6-gabe|#{basename} -S
55
+ Power cycle a bunch of machines:
56
+ #{basename} -t 001234,004567,007890 -A reboot
57
+ _EXAMPLES_
58
+ end.parse!
59
+
60
+ if ARGV.size > 0
61
+ # anything else left in ARGV is garbage
62
+ puts "Not sure what I am supposed to do with these arguments: #{ARGV.join(' ')}"
63
+ puts parser
64
+ exit 1
65
+ end
66
+
67
+
68
+ abort "See --help for #{basename} usage" unless [:provision, :power_status, :power].include? options[:mode]
69
+ abort "You need to specify at least a nodeclass when provisioning" if options[:mode] == :provision && options[:provision][:nodeclass].nil?
70
+ if options[:mode] == :power
71
+ # convert what we allow to be specified to what collins::power allows
72
+ options[:power_action] = 'rebootsoft' if options[:power_action] == 'reboot'
73
+ abort "Unknown power action #{options[:power_action]}, expecting one of #{ALLOWABLE_POWER_ACTIONS.join(',')}" unless ALLOWABLE_POWER_ACTIONS.include? options[:power_action]
74
+ begin
75
+ options[:power_action] = Collins::Power.normalize_action options[:power_action]
76
+ rescue => e
77
+ abort "Unknown power action #{options[:power_action]}! #{e.message}"
78
+ end
79
+ end
80
+
81
+ if options[:tags].nil? or options[:tags].empty?
82
+ # read tags from stdin. first field on the line is the tag
83
+ input = ARGF.readlines
84
+ options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
85
+ end
86
+
87
+ begin
88
+ @collins = Collins::Authenticator.setup_client timeout: options[:timeout], config_file: options[:config], prompt: true
89
+ rescue => e
90
+ abort "Unable to set up Collins client! #{e.message}"
91
+ end
92
+
93
+ def api_call desc, method, *varargs
94
+ success,message = begin
95
+ [@collins.send(method,*varargs),nil]
96
+ rescue => e
97
+ [false,e.message]
98
+ end
99
+ puts "#{success ? SUCCESS : ERROR}: #{desc}#{message.nil? ? nil : " (%s)" % e.message}"
100
+ success
101
+ end
102
+
103
+ action_successes = []
104
+ options[:tags].each do |t|
105
+ case options[:mode]
106
+ when :provision
107
+ action_string = "#{t} provisioning with #{options[:provision].map{|k,v| "#{k}:#{v}"}.join(" ")} by #{options[:build_contact]}... "
108
+ printf action_string
109
+ begin
110
+ res = @collins.provision(t, options[:provision][:nodeclass], options[:build_contact], options[:provision])
111
+ puts (res ? SUCCESS : ERROR )
112
+ action_successes << res
113
+ rescue => e
114
+ puts "#{ERROR} (#{e.message})"
115
+ action_successes << false
116
+ end
117
+ when :power_status
118
+ begin
119
+ s = @collins.power_status(t)
120
+ puts "#{SUCCESS}: #{t} power status is #{s}"
121
+ rescue => e
122
+ puts "#{ERROR}: Unable to query power status for #{t}#{e.message.nil? ? nil : " (%s)" % e.message}"
123
+ end
124
+ when :power
125
+ action_successes << api_call("#{t} performing #{options[:power_action]}", :power!, t, options[:power_action])
126
+ end
127
+ end
128
+
129
+ exit action_successes.all? ? 0 : 1
130
+
data/bin/collins-find ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby
2
+ # stands for collins-find
3
+ # look up hosts quickly from collins from the CLI
4
+
5
+ require 'collins_auth'
6
+ require 'yaml'
7
+ require 'json'
8
+ require 'optparse'
9
+
10
+ #TODO: querying for :status or :state with -a status:maintenance doesnt play nice with -Smaintenance,allocated
11
+ #TODO: we construct the query for states and statuses only from the parameters to --status (ignoring any -a attributes)
12
+ #TODO: add a formatting option to display the assets in a different way (not a table)
13
+
14
+ # this gets stuffed into the collins find call
15
+ query_opts = {
16
+ :operation => 'AND',
17
+ :size => 9999,
18
+ }
19
+ # these are all the attributes we search for
20
+ # may be converted into a CQL query
21
+ search_attrs = { }
22
+ options = {
23
+ :display => :table, # how to display the results
24
+ :separator => "\t",
25
+ :attributes => {}, # additional attributes to query for
26
+ :columns => [:tag, :hostname, :nodeclass, :status, :pool, :primary_role, :secondary_role],
27
+ :column_override => [], # if set, these are the columns to display
28
+ :timeout => 120,
29
+ :show_header => false, # if the header for columns should be displayed
30
+ :config => nil # collins config to give to setup_client
31
+ }
32
+
33
+
34
+ basename = File.basename(File.realpath($0))
35
+ abort "See --help for #{basename} usage" if ARGV.empty?
36
+ OptionParser.new do |opts|
37
+ opts.banner = "Usage: #{basename} [options] [hostnamepattern]"
38
+ opts.separator "Query options:"
39
+ opts.on('-t','--tag TAG[,...]',Array, "Assets with tag[s] TAG") {|v| search_attrs[:tag] = v}
40
+ opts.on('-T','--type TYPE',String, "Only show assets with type TYPE") {|v| search_attrs[:type] = v}
41
+ opts.on('-n','--nodeclass NODECLASS[,...]',Array, "Assets in nodeclass NODECLASS") {|v| search_attrs[:nodeclass] = v}
42
+ opts.on('-p','--pool POOL[,...]',Array, "Assets in pool POOL") {|v| search_attrs[:pool] = v}
43
+ opts.on('-s','--size SIZE',Integer, "Number of assets to return (Default: #{query_opts[:size]})") {|v| query_opts[:size] = v}
44
+ opts.on('-r','--role ROLE[,...]',Array,"Assets in primary role ROLE") {|v| search_attrs[:primary_role] = v}
45
+ opts.on('-R','--secondary-role ROLE[,...]',Array,"Assets in secondary role ROLE") {|v| search_attrs[:secondary_role] = v}
46
+ opts.on('-i','--ip-address IP[,...]',Array,"Assets with IP address[es]") {|v| search_attrs[:ip_address] = v}
47
+ opts.on('-S','--status STATUS[:STATE][,...]',Array,"Asset status (and optional state after :)") do |v|
48
+ # in order to know what state was paired with what status, lets store the original params
49
+ # so the query constructor can create the correct CQL query
50
+ options[:status_state] = v
51
+ search_attrs[:status], search_attrs[:state] = v.inject([[],[]]) do |memo,s|
52
+ status,state = s.split(':')
53
+ memo[0] << status.upcase if not status.nil? and not status.empty?
54
+ memo[1] << state.upcase if not state.nil? and not state.empty?
55
+ memo
56
+ end
57
+ end
58
+ opts.on('-a','--attribute attribute[:value[,...]]',String,"Arbitrary attributes and values to match in query. : between key and value") do |x|
59
+ x.split(',').each do |p|
60
+ a,v = p.split(':')
61
+ a = a.to_sym
62
+ if not search_attrs[a].nil? and not search_attrs[a].is_a? Array
63
+ # its a single value, turn it into an array
64
+ search_attrs[a] = [search_attrs[a]]
65
+ end
66
+ if search_attrs[a].is_a? Array
67
+ # already multivalue, append
68
+ search_attrs[a] << v
69
+ else
70
+ search_attrs[a] = v
71
+ end
72
+ end
73
+ end
74
+
75
+ opts.separator ""
76
+ opts.separator "Table formatting:"
77
+ opts.on('-H','--show-header',"Show header fields in output") {options[:show_header] = true}
78
+ opts.on('-c','--columns ATTRIBUTES',Array,"Attributes to output as columns, comma separated (Default: #{options[:columns].map(&:to_s).join(',')})") {|v| options[:column_override] = v.map(&:to_sym)}
79
+ opts.on('-x','--extra-columns ATTRIBUTES',Array,"Show these columns in addition to the default columns, comma separated") {|v| options[:columns].push(v.map(&:to_sym)).flatten! }
80
+ opts.on('-f','--field-separator SEPARATOR',String,"Separator between columns in output (Default: #{options[:separator]})") {|v| options[:separator] = v}
81
+
82
+ opts.separator ""
83
+ opts.separator "Robot formatting:"
84
+ opts.on('-l','--link',"Output link to assets found in web UI") {options[:display] = :link}
85
+ opts.on('-j','--json',"Output results in JSON (NOTE: This probably wont be what you expected)") {options[:display] = :json}
86
+ opts.on('-y','--yaml',"Output results in YAML") {options[:display] = :yaml}
87
+
88
+ opts.separator ""
89
+ opts.separator "Extra options:"
90
+ opts.on('--expire SECONDS',Integer,"Timeout in seconds (0 == forever)") {|v| options[:timeout] = v}
91
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| options[:config] = v}
92
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
93
+
94
+ opts.separator ""
95
+ opts.separator <<_EXAMPLES_
96
+ Examples:
97
+ Query for devnodes in DEVEL pool that are VMs
98
+ cf -n develnode -p DEVEL
99
+ Query for asset 001234, and show its system_password
100
+ cf -t 001234 -x system_password
101
+ Query for all decommissioned VM assets
102
+ cf -a is_vm:true -S decommissioned
103
+ Query for hosts matching hostname '^web6-'
104
+ cf ^web6-
105
+ Query for all develnode6 nodes with a value for PUPPET_SERVER
106
+ cf -n develnode6 -a puppet_server -H
107
+ _EXAMPLES_
108
+ end.parse!
109
+
110
+ # hostname is the final option, no flags
111
+ search_attrs[:hostname] = ARGV.shift
112
+ # fix bug where assets wont get found if they dont have that meta attribute
113
+ search_attrs.delete(:hostname) if search_attrs[:hostname].nil?
114
+
115
+ # if nothing passed to us, lets not search for EVERYTHING
116
+ #abort "You need to query for _something_, see --help" if
117
+ # selector_keys.all? {|key| options[key].nil?} and options[:attributes].empty?
118
+
119
+ # for any search attributes, lets not pass arrays of 1 element
120
+ # as that will confuse as_query?
121
+ search_attrs.each do |k,v|
122
+ if v.is_a? Array
123
+ search_attrs[k] = v.first if v.length == 1
124
+ search_attrs[k] = nil if v.empty?
125
+ end
126
+ end
127
+
128
+ def as_query?(attrs)
129
+ attrs.any?{|k,v| v.is_a? Array}
130
+ end
131
+
132
+ def convert_to_query(op, attrs, options)
133
+ # we want to support being able to query -Smaintenance:noop,:running,:provisioning_problem
134
+ # and not have the states ored together. Handle status/state pairs separately
135
+ basic_query = attrs.reject {|k,v| [:status,:state].include?(k)}.map do |k,v|
136
+ next if v.nil?
137
+ if v.is_a? Array
138
+ "(" + v.map{|x| "#{k} = #{x}"}.join(' OR ') + ")"
139
+ else
140
+ "#{k} = #{v}"
141
+ end
142
+ end.compact.join(" #{op} ")
143
+ # because they are provided in pairs, lets handle them together
144
+ # create the (( STATUS = maintenance AND STATE = noop) OR (STATE = provisioning_problem)) query
145
+ if options[:status_state]
146
+ status_query = options[:status_state].flat_map do |ss|
147
+ h = {}
148
+ h[:status], h[:state] = ss.split(':')
149
+ h[:status] = nil if h[:status].nil? or h[:status].empty?
150
+ h[:state] = nil if h[:state].nil? or h[:state].empty?
151
+ "( " + h.map {|k,v| v.nil? ? nil : "#{k.to_s.upcase} = #{v}"}.compact.join(" AND ") + " )"
152
+ end.compact.join(' OR ')
153
+ status_query = "( #{status_query} )"
154
+ end
155
+ [basic_query,status_query].reject {|q| q.nil? or q.empty?}.join(" #{op} ")
156
+ end
157
+
158
+ def display_as_robot_talk(assets, format = :json)
159
+ puts assets.send("to_#{format}".to_sym)
160
+ end
161
+ def display_as_table(assets, columns, separator, show_header = false)
162
+ # lets figure out how wide each column is, including header
163
+ column_width_pairs = columns.map do |column|
164
+ # grab all attributes == column and figure out max width
165
+ width = assets.map{|a| (column == :state) ? a.send(column).label.to_s.length : a.send(column).to_s.length}.max
166
+ width = [width, column.to_s.length].max if show_header
167
+ [column,width]
168
+ end
169
+ column_width_map = Hash[column_width_pairs]
170
+
171
+ if show_header
172
+ $stderr.puts column_width_map.map{|c,w| "%-#{w}s" % c}.join(separator)
173
+ end
174
+ assets.each do |a|
175
+ puts column_width_map.map {|c,w| v = (c == :state) ? a.send(c).label : a.send(c) ; "%-#{w}s" % v }.join(separator)
176
+ end
177
+ end
178
+ def display_as_link assets, client
179
+ assets.each do |a|
180
+ puts "#{client.host}/asset/#{a.tag}"
181
+ end
182
+ end
183
+
184
+ # merge search_attrs into query
185
+ if as_query?(search_attrs)
186
+ query_opts[:query] = convert_to_query(query_opts[:operation], search_attrs, options)
187
+ #puts "Query: #{query_opts[:query]}"
188
+ else
189
+ query_opts.merge!(search_attrs)
190
+ end
191
+
192
+ begin
193
+ collins = Collins::Authenticator.setup_client timeout: options[:timeout], config_file: options[:config], prompt: true
194
+ rescue => e
195
+ abort "Unable to set up Collins client! #{e.message}"
196
+ end
197
+
198
+ begin
199
+ assets = collins.find(query_opts)
200
+ if assets.length > 0
201
+ case options[:display]
202
+ when :table
203
+ # if the user passed :column_override, respect that absolutely. otherwise, the columns to display
204
+ # should be options[:columns] + any extra attributes queried for. this way ```cf -c hostname -a is_vm:true```
205
+ # wont return 2 columns; only the one you asked for
206
+ columns = if options[:column_override].empty?
207
+ options[:columns].concat(search_attrs.keys).compact.uniq
208
+ else
209
+ options[:column_override]
210
+ end
211
+ display_as_table(assets,columns,options[:separator],options[:show_header])
212
+ when :link
213
+ display_as_link assets, collins
214
+ when :json,:yaml
215
+ display_as_robot_talk(assets,options[:display])
216
+ else
217
+ abort "I don't know how to display assets in #{options[:display]} format!"
218
+ end
219
+ else
220
+ abort "No assets found"
221
+ end
222
+ rescue => e
223
+ abort "Error querying collins: #{e.message}"
224
+ end
225
+
data/bin/collins-log ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ # stands for collins-log
3
+ # looks at logs on assets
4
+
5
+ # TODO: make poll_wait tunable
6
+ # TODO: add options to sort ascending or descending on date
7
+ # TODO: implement searching logs (is this really useful?)
8
+ # TODO: add duplicate line detection and compression (...)
9
+
10
+ require 'collins_auth'
11
+ require 'yaml'
12
+ require 'optparse'
13
+ require 'colorize'
14
+ require 'set'
15
+
16
+ log_levels = Collins::Api::Logging::Severity.constants.map(&:to_s)
17
+
18
+ @options = {
19
+ :tags => [],
20
+ :show_all => false,
21
+ :poll_wait => 2,
22
+ :follow => false,
23
+ :severities => [],
24
+ :timeout => 20,
25
+ :sev_colors => {
26
+ 'EMERGENCY' => {:color => :red, :background => :light_blue},
27
+ 'ALERT' => {:color => :red},
28
+ 'CRITICAL' => {:color => :black, :background => :red},
29
+ 'ERROR' => {:color => :red},
30
+ 'WARNING' => {:color => :yellow},
31
+ 'NOTICE' => {},
32
+ 'INFORMATIONAL' => {:color => :green},
33
+ 'DEBUG' => {:color => :blue},
34
+ 'NOTE' => {:color => :light_cyan},
35
+ },
36
+ :config => nil
37
+ }
38
+ @search_opts = {
39
+ :size => 20,
40
+ :filter => nil,
41
+ }
42
+
43
+ basename = File.basename(File.realpath($0))
44
+ abort "See --help for #{basename} usage" if ARGV.empty?
45
+ OptionParser.new do |opts|
46
+ opts.banner = "Usage: #{basename} [options]"
47
+ opts.on('-a','--all',"Show logs from ALL assets") {|v| @options[:show_all] = true}
48
+ opts.on('-n','--number LINES',Integer,"Show the last LINES log entries. (Default: #{@search_opts[:size]})") {|v| @search_opts[:size] = v}
49
+ opts.on('-t','--tags TAGS',Array,"Tags to work on, comma separated") {|v| @options[:tags] = v}
50
+ opts.on('-f','--follow',"Poll for logs every #{@options[:poll_wait]} seconds") {|v| @options[:follow] = true}
51
+ opts.on('-s','--severity SEVERITY[,...]',Array,"Log severities to return (Defaults to all). Use !SEVERITY to exclude one.") {|v| @options[:severities] = v.map(&:upcase) }
52
+ #opts.on('-i','--interleave',"Interleave all log entries (Default: groups by asset)") {|v| options[:interleave] = true}
53
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| @options[:config] = v}
54
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
55
+ opts.separator ""
56
+ opts.separator <<_EOE_
57
+ Severities:
58
+ #{Collins::Api::Logging::Severity.to_a.map{|s| s.colorize(@options[:sev_colors][s])}.join(", ")}
59
+
60
+ Examples:
61
+ Show last 20 logs for an asset
62
+ #{basename} -t 001234
63
+ Show last 100 logs for an asset
64
+ #{basename} -t 001234 -n100
65
+ Show last 10 logs for 2 assets that are ERROR severity
66
+ #{basename} -t 001234,001235 -n10 -sERROR
67
+ Show last 10 logs all assets that are not note or informational severity
68
+ #{basename} -a -n10 -s'!informational,!note'
69
+ Show last 10 logs for all web nodes that are provisioned having verification in the message
70
+ cf -S provisioned -n webnode\$ | #{basename} -n10 -s debug | grep -i verification
71
+ _EOE_
72
+ end.parse!
73
+
74
+
75
+ abort "Log severities #{@options[:severities].join(',')} are invalid! Use one of #{log_levels.join(', ')}" unless @options[:severities].all? {|l| Collins::Api::Logging::Severity.valid?(l.tr('!','')) }
76
+ @search_opts[:filter] = @options[:severities].join(';')
77
+
78
+ if @options[:tags].empty? and not @options[:show_all]
79
+ # read tags from stdin. first field on the line is the tag
80
+ input = ARGF.readlines
81
+ @options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
82
+ end
83
+
84
+ abort "You need to give me some assets to display logs; see --help" if @options[:tags].empty? and not @options[:show_all]
85
+
86
+ begin
87
+ @collins = Collins::Authenticator.setup_client timeout: @options[:timeout], config_file: @options[:config], prompt: true
88
+ rescue => e
89
+ abort "Unable to set up Collins client! #{e.message}"
90
+ end
91
+
92
+ def output_logs(logs)
93
+ # colorize output before computing width of fields
94
+ logs.map! do |l|
95
+ l.TYPE = @options[:sev_colors].has_key?(l.TYPE) ? l.TYPE.colorize(@options[:sev_colors][l.TYPE]) : l.TYPE
96
+ l
97
+ end
98
+ # show newest last
99
+ sorted_logs = logs.sort_by {|l| l.CREATED }
100
+ tag_width = sorted_logs.map{|l| l.ASSET_TAG.length}.max
101
+ sev_width = sorted_logs.map{|l| l.TYPE.length}.max
102
+ time_width = sorted_logs.map{|l| l.CREATED.length}.max
103
+ sorted_logs.each do |l|
104
+ puts "%-#{time_width}s: %-#{sev_width}s %-#{tag_width}s %s" % [l.CREATED, l.TYPE, l.ASSET_TAG, l.MESSAGE]
105
+ end
106
+ end
107
+
108
+ def grab_logs
109
+ if @options[:tags].empty?
110
+ begin
111
+ @collins.all_logs(@search_opts)
112
+ rescue => e
113
+ $stderr.puts "Unable to fetch logs:".colorize(@options[:sev_colors]['WARNING']) + " #{e.message}"
114
+ []
115
+ end
116
+ else
117
+ @options[:tags].flat_map do |t|
118
+ begin
119
+ @collins.logs(t, @search_opts)
120
+ rescue => e
121
+ $stderr.puts "Unable to fetch logs for #{t}:".colorize(@options[:sev_colors]['WARNING']) + " #{e.message}"
122
+ []
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ begin
129
+ all_logs = grab_logs
130
+ logs_seen = all_logs.map(&:ID).to_set
131
+ output_logs(all_logs)
132
+ while @options[:follow]
133
+ sleep @options[:poll_wait]
134
+ logs = grab_logs
135
+ new_logs = logs.reject {|l| logs_seen.include?(l.ID)}
136
+ output_logs(new_logs)
137
+ logs_seen = logs_seen | new_logs.map(&:ID)
138
+ end
139
+ rescue Interrupt => e
140
+ exit 0
141
+ end
142
+
143
+
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env ruby
2
+ # stands for collins-modify
3
+ # perform actions on asset attribtues in collins easily
4
+
5
+ require 'collins_auth'
6
+ require 'yaml'
7
+ require 'optparse'
8
+
9
+ SUCCESS = "SUCCESS"
10
+ ERROR = "ERROR"
11
+ VALID_STATUSES = ["ALLOCATED","CANCELLED","DECOMMISSIONED","INCOMPLETE","MAINTENANCE","NEW","PROVISIONED","PROVISIONING","UNALLOCATED"]
12
+ #TODO: this shouldnt be hardcoded. we should pull this from the API instead?
13
+ # should elegantly support user-defined states without changing this script
14
+ VALID_STATES = {
15
+ "ALLOCATED" => ["CLAIMED","SPARE","RUNNING_UNMONITORED","UNMONITORED"],
16
+ "MAINTENANCE" => ["AWAITING_REVIEW","HARDWARE_PROBLEM","HW_TESTING","HARDWARE_UPGRADE","IPMI_PROBLEM","MAINT_NOOP","NETWORK_PROBLEM","RELOCATION",'PROVISIONING_PROBLEM'],
17
+ "ANY" => ["RUNNING","STARTING","STOPPING","TERMINATED"],
18
+ }
19
+ log_levels = Collins::Api::Logging::Severity.constants.map(&:to_s)
20
+
21
+ options = {
22
+ :query_size => 9999,
23
+ :attributes => {},
24
+ :delete_attributes => [],
25
+ :log_level => 'NOTE',
26
+ :timeout => 120,
27
+ :config => nil
28
+ }
29
+ basename = File.basename(File.realpath($0))
30
+ parser = OptionParser.new do |opts|
31
+ opts.banner = "Usage: #{basename} [options]"
32
+ opts.on('-a','--set-attribute attribute:value',String,"Set attribute=value. : between key and value. attribute will be uppercased.") do |x|
33
+ if not x.include? ':'
34
+ puts '--set-attribute requires attribute:value, missing :value'
35
+ puts opts.help
36
+ exit 1
37
+ end
38
+ a,v = x.split(':')
39
+ options[:attributes][a.upcase.to_sym] = v
40
+ end
41
+ opts.on('-d','--delete-attribute attribute',String,"Delete attribute.") {|v| options[:delete_attributes] << v.to_sym }
42
+ opts.on('-S','--set-status status[:state]',String,'Set status (and optionally state) to status:state. Requires --reason') do |v|
43
+ status,state = v.split(':')
44
+ options[:status] = status.upcase if not status.nil? and not status.empty?
45
+ options[:state] = state.upcase if not state.nil? and not state.empty?
46
+ end
47
+ opts.on('-r','--reason REASON',String,"Reason for changing status/state.") {|v| options[:reason] = v }
48
+ opts.on('-l','--log MESSAGE',String,"Create a log entry.") do |v|
49
+ options[:log_message] = v
50
+ end
51
+ opts.on('-L','--level LEVEL',String, log_levels + log_levels.map(&:downcase),"Set log level. Default level is #{options[:log_level]}.") do |v|
52
+ options[:log_level] = v.upcase
53
+ end
54
+ opts.on('-t','--tags TAGS',Array,"Tags to work on, comma separated") {|v| options[:tags] = v.map(&:to_sym)}
55
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| options[:config] = v}
56
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
57
+ opts.separator ""
58
+ opts.separator "Allowed values (uppercase or lowercase is accepted):"
59
+ opts.separator <<_EOF_
60
+ Status (-S,--set-status):
61
+ #{VALID_STATUSES.join(', ')}
62
+ States (-S,--set-status):
63
+ #{VALID_STATES.keys.map {|k| "#{k} ->\n #{VALID_STATES[k].join(', ')}"}.join "\n "}
64
+ Log levels (-L,--level):
65
+ #{log_levels.join(', ')}
66
+ _EOF_
67
+ opts.separator ""
68
+ opts.separator "Examples:"
69
+ opts.separator <<_EOF_
70
+ Set an attribute on some hosts:
71
+ #{basename} -t 001234,004567 -a my_attribute:true
72
+ Delete an attribute on some hosts:
73
+ #{basename} -t 001234,004567 -d my_attribute
74
+ Delete and add attribute at same time:
75
+ #{basename} -t 001234,004567 -a new_attr:test -d old_attr
76
+ Set machine into maintenace noop:
77
+ #{basename} -t 001234 -S maintenance:maint_noop -r "I do what I want"
78
+ Set machine back to allocated:
79
+ #{basename} -t 001234 -S allocated:running -r "Back to allocated"
80
+ Set machine back to new without setting state:
81
+ #{basename} -t 001234 -S new -r "Dunno why you would want this"
82
+ Create a log entry:
83
+ #{basename} -t 001234 -l'computers are broken and everything is horrible' -Lwarning
84
+ Read from stdin:
85
+ cf -n develnode | #{basename} -d my_attribute
86
+ cf -n develnode -S allocated | #{basename} -a collectd_version:5.2.1-52
87
+ echo -e "001234\\n001235\\n001236"| #{basename} -a test_attribute:'hello world'
88
+ _EOF_
89
+ end.parse!
90
+
91
+ if ARGV.size > 0
92
+ # anything else left in ARGV is garbage
93
+ puts "Not sure what I am supposed to do with these arguments: #{ARGV.join(' ')}"
94
+ puts parser
95
+ exit 1
96
+ end
97
+
98
+ abort "See --help for #{basename} usage" if options[:attributes].empty? and options[:delete_attributes].empty? and options[:status].nil? and options[:log_message].nil?
99
+ abort "You need to provide a --reason when changing asset states!" if not options[:status].nil? and options[:reason].nil?
100
+ #TODO this is never checked because we are making option parser vet our options for levels. Catch OptionParser::InvalidArgument?
101
+ abort "Log level #{options[:log_level]} is invalid! Use one of #{log_levels.join(', ')}" unless Collins::Api::Logging::Severity.valid?(options[:log_level])
102
+
103
+ # if any statuses or states, validate them against allowed values
104
+ unless options[:status].nil?
105
+ abort "Invalid status #{options[:status]} (Should be in #{VALID_STATUSES.join(', ')})" unless VALID_STATUSES.include? options[:status]
106
+ states_for_status = VALID_STATES["ANY"].concat((VALID_STATES[options[:status]].nil?) ? [] : VALID_STATES[options[:status]])
107
+ abort "State #{options[:state]} doesn't apply to status #{options[:status]} (Should be one of #{states_for_status.join(', ')})" unless options[:state].nil? or states_for_status.include?(options[:state])
108
+ end
109
+
110
+ if options[:tags].nil? or options[:tags].empty?
111
+ # read tags from stdin. first field on the line is the tag
112
+ input = ARGF.readlines
113
+ options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
114
+ end
115
+
116
+ begin
117
+ @collins = Collins::Authenticator.setup_client timeout: options[:timeout], config_file: options[:config], prompt: true
118
+ rescue => e
119
+ abort "Unable to set up Collins client! #{e.message}"
120
+ end
121
+
122
+ def api_call desc, method, *varargs
123
+ success,message = begin
124
+ [@collins.send(method,*varargs),nil]
125
+ rescue => e
126
+ [false,e.message]
127
+ end
128
+ puts "#{success ? SUCCESS : ERROR}: #{desc}#{message.nil? ? nil : " (%s)" % e.message}"
129
+ success
130
+ end
131
+
132
+ exit_clean = true
133
+ options[:tags].each do |t|
134
+ if options[:log_message]
135
+ exit_clean = api_call("#{t} create #{options[:log_level].downcase} log #{options[:log_message].inspect}", :log!, t, options[:log_message], options[:log_level]) && exit_clean
136
+ end
137
+ options[:attributes].each do |k,v|
138
+ exit_clean = api_call("#{t} set #{k}=#{v}", :set_attribute!, t, k, v) && exit_clean
139
+ end
140
+ options[:delete_attributes].each do |k|
141
+ exit_clean = api_call("#{t} delete #{k}", :delete_attribute!, t, k) && exit_clean
142
+ end
143
+ if options[:status]
144
+ exit_clean = api_call("#{t} set status to #{options[:status]}#{options[:state] ? ":#{options[:state]}" : ''}", :set_status!, t, :status => options[:status], :state => options[:state], :reason => options[:reason]) && exit_clean
145
+ end
146
+ end
147
+
148
+ exit exit_clean ? 0 : 1
149
+
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: collins-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Gabe Conradi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.7.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: collins_auth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: collins_client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.2.11
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.2.11
55
+ description: CLI utilities to interact with the Collins API
56
+ email:
57
+ - gabe@tumblr.com
58
+ - gummybearx@gmail.com
59
+ executables:
60
+ - collins
61
+ - collins-action
62
+ - collins-find
63
+ - collins-log
64
+ - collins-modify
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - bin/collins
69
+ - bin/collins-action
70
+ - bin/collins-find
71
+ - bin/collins-log
72
+ - bin/collins-modify
73
+ - README.md
74
+ homepage: http://github.com/byxorna/collins-cli
75
+ licenses:
76
+ - Apache License 2.0
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - '>='
85
+ - !ruby/object:Gem::Version
86
+ version: 1.9.2
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.0.14
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: CLI utilities to interact with the Collins API
98
+ test_files: []