collins-cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []