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 +7 -0
- data/README.md +173 -0
- data/bin/collins +21 -0
- data/bin/collins-action +130 -0
- data/bin/collins-find +225 -0
- data/bin/collins-log +143 -0
- data/bin/collins-modify +149 -0
- metadata +98 -0
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
|
+
[](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
|
data/bin/collins-action
ADDED
|
@@ -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
|
+
|
data/bin/collins-modify
ADDED
|
@@ -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: []
|