collins-cli 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +95 -36
- data/bin/collins +34 -15
- data/lib/collins/cli/find.rb +161 -0
- data/lib/collins/cli/formatter.rb +74 -0
- data/lib/collins/cli/ipam.rb +99 -0
- data/lib/collins/cli/log.rb +173 -0
- data/lib/collins/cli/mixins.rb +73 -0
- data/lib/collins/cli/modify.rb +144 -0
- data/lib/collins/cli/power.rb +77 -0
- data/lib/collins/cli/provision.rb +80 -0
- data/lib/collins-cli.rb +13 -0
- data/spec/collins__cli__find_spec.rb +48 -0
- data/spec/collins__cli__log_spec.rb +4 -0
- data/spec/collins__cli__modify_spec.rb +4 -0
- data/spec/collins__cli__power_spec.rb +4 -0
- data/spec/collins__cli__provision_spec.rb +4 -0
- data/spec/spec_helper.rb +2 -0
- metadata +52 -11
- data/bin/collins-action +0 -130
- data/bin/collins-find +0 -225
- data/bin/collins-log +0 -143
- data/bin/collins-modify +0 -149
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d8cb9285558a8adc49d7da521c0241e1331eb08
|
4
|
+
data.tar.gz: 27559afb065f378b3e60aa010eb3271e40e53222
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b96eda4857aaed7c4a92a704306ff7158f322f3708e24cb66fd15a9665977827d6620fec9244fff7496bf8217445144695d1058e935522504de95fc00f8d50f1
|
7
|
+
data.tar.gz: 050ca2a0dfddb04dd02c798bcd58fad07078e5445e37f0fb8337360928fb95ea2c7f3518fd293acbeec5da10011296b119b1b4516992adad21f8ec0e8e5b5b1a
|
data/README.md
CHANGED
@@ -7,18 +7,34 @@ CLI scripts for interacting with Collins API
|
|
7
7
|
|
8
8
|
## Overview
|
9
9
|
|
10
|
-
|
10
|
+
```collins-cli``` uses the ```collins_auth``` gem for authentication, so it relies on you either typing in your credentials every time, or setting up a ~/.collins.yml file. The base format for the config file is as follows:
|
11
11
|
|
12
|
+
---
|
13
|
+
host: https://collins.iata.company.com
|
14
|
+
username: myuser
|
15
|
+
# omit password to have collins auth prompt you
|
16
|
+
password: mypass
|
17
|
+
|
18
|
+
(see https://github.com/tumblr/collins/tree/master/support/ruby/collins-auth for more details)
|
19
|
+
|
20
|
+
Main entry point is the ```collins``` binary:
|
21
|
+
|
22
|
+
$ collins -h
|
12
23
|
Usage: collins <command> [options]
|
13
24
|
Available commands:
|
14
25
|
query, find: Search for assets in Collins
|
15
26
|
modify, set: Add and remove attributes, change statuses, and log to assets
|
16
27
|
log: Display log messages on assets
|
17
|
-
provision
|
28
|
+
provision: Provision assets
|
29
|
+
power: Control and show power status
|
30
|
+
ip, address, ipmi: Allocate IPs, update IPMI info
|
18
31
|
|
19
|
-
##
|
32
|
+
## Find Assets - collins find
|
20
33
|
|
21
|
-
|
34
|
+
Use ```collins find``` to quickly construct complex queries of your assets in Collins. Bonus points for piping the output of ```collins find``` into another program.
|
35
|
+
|
36
|
+
$ collins find -h
|
37
|
+
Usage: collins find [options] [hostnamepattern]
|
22
38
|
Query options:
|
23
39
|
-t, --tag TAG[,...] Assets with tag[s] TAG
|
24
40
|
-T, --type TYPE Only show assets with type TYPE
|
@@ -61,7 +77,9 @@ Main entry point is the ```collins``` binary.
|
|
61
77
|
Query for all develnode6 nodes with a value for PUPPET_SERVER
|
62
78
|
cf -n develnode6 -a puppet_server -H
|
63
79
|
|
64
|
-
##
|
80
|
+
## View Logs - collins log
|
81
|
+
|
82
|
+
Pipe the output of ```collins find``` into ```collins log``` to pull recent logs, or tail logs. Very useful while watching provisioning. Reads asset tags from ARGF if ```--tags``` aren't provided.
|
65
83
|
|
66
84
|
Usage: collins-log [options]
|
67
85
|
-a, --all Show logs from ALL assets
|
@@ -87,9 +105,11 @@ Main entry point is the ```collins``` binary.
|
|
87
105
|
Show last 10 logs for all web nodes that are provisioned having verification in the message
|
88
106
|
cf -S provisioned -n webnode$ | collins-log -n10 -s debug | grep -i verification
|
89
107
|
|
90
|
-
## Modification - collins
|
108
|
+
## Modification - collins modify
|
91
109
|
|
92
|
-
|
110
|
+
Pipe the output of ```collins find``` into ```collins modify``` to change statuses, create and delete attributes, write log messages, etc. Reads asset tags from ARGF if ```--tags``` aren't provided.
|
111
|
+
|
112
|
+
Usage: collins modify [options]
|
93
113
|
-a attribute:value, Set attribute=value. : between key and value. attribute will be uppercased.
|
94
114
|
--set-attribute
|
95
115
|
-d, --delete-attribute attribute Delete attribute.
|
@@ -116,33 +136,31 @@ Main entry point is the ```collins``` binary.
|
|
116
136
|
|
117
137
|
Examples:
|
118
138
|
Set an attribute on some hosts:
|
119
|
-
collins
|
139
|
+
collins modify -t 001234,004567 -a my_attribute:true
|
120
140
|
Delete an attribute on some hosts:
|
121
|
-
collins
|
141
|
+
collins modify -t 001234,004567 -d my_attribute
|
122
142
|
Delete and add attribute at same time:
|
123
|
-
collins
|
143
|
+
collins modify -t 001234,004567 -a new_attr:test -d old_attr
|
124
144
|
Set machine into maintenace noop:
|
125
|
-
collins
|
145
|
+
collins modify -t 001234 -S maintenance:maint_noop -r "I do what I want"
|
126
146
|
Set machine back to allocated:
|
127
|
-
collins
|
147
|
+
collins modify -t 001234 -S allocated:running -r "Back to allocated"
|
128
148
|
Set machine back to new without setting state:
|
129
|
-
collins
|
149
|
+
collins modify -t 001234 -S new -r "Dunno why you would want this"
|
130
150
|
Create a log entry:
|
131
|
-
collins
|
151
|
+
collins modify -t 001234 -l'computers are broken and everything is horrible' -Lwarning
|
132
152
|
Read from stdin:
|
133
|
-
cf -n develnode | collins
|
134
|
-
cf -n develnode -S allocated | collins
|
135
|
-
echo -e "001234\n001235\n001236"| collins
|
153
|
+
cf -n develnode | collins modify -d my_attribute
|
154
|
+
cf -n develnode -S allocated | collins modify -a collectd_version:5.2.1-52
|
155
|
+
echo -e "001234\n001235\n001236"| collins modify -a test_attribute:'hello world'
|
136
156
|
|
137
|
-
##
|
157
|
+
## Provision - collins provision
|
158
|
+
|
159
|
+
Pipe the output of ```collins find``` into ```collins provision``` to provision assets. Reads asset tags from ARGF if ```--tags``` aren't provided.
|
160
|
+
|
161
|
+
$ collins provision -h
|
162
|
+
Usage: collins provision [options]
|
138
163
|
|
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
164
|
-n, --nodeclass NODECLASS Nodeclass to provision as. (Required)
|
147
165
|
-p, --pool POOL Provision with pool POOL.
|
148
166
|
-r, --role ROLE Provision with primary role ROLE.
|
@@ -150,6 +168,25 @@ Main entry point is the ```collins``` binary.
|
|
150
168
|
-s, --suffix SUFFIX Provision with suffix SUFFIX.
|
151
169
|
-a, --activate Activate server on provision (useful with SL plugin) (Default: ignored)
|
152
170
|
-b, --build-contact USER Build contact. (Default: gabe)
|
171
|
+
|
172
|
+
General:
|
173
|
+
-t, --tags TAG[,...] Tags to work on, comma separated
|
174
|
+
-C, --config CONFIG Use specific Collins config yaml for Collins::Client
|
175
|
+
-h, --help Help
|
176
|
+
|
177
|
+
Examples:
|
178
|
+
Provision some machines:
|
179
|
+
collins find -Sunallocated -arack_position:716|collins provision -P -napiwebnode6 -RALL
|
180
|
+
|
181
|
+
## Power Management - collins power
|
182
|
+
|
183
|
+
Manage and show power states with ```collins power```
|
184
|
+
|
185
|
+
$ collins power -h
|
186
|
+
Usage: collins power [options]
|
187
|
+
|
188
|
+
-s, --status Show IPMI power status
|
189
|
+
-p, --power ACTION Perform IPMI power ACTION
|
153
190
|
|
154
191
|
General:
|
155
192
|
-t, --tags TAG[,...] Tags to work on, comma separated
|
@@ -157,17 +194,39 @@ Main entry point is the ```collins``` binary.
|
|
157
194
|
-h, --help Help
|
158
195
|
|
159
196
|
Examples:
|
160
|
-
|
161
|
-
|
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
|
197
|
+
Reset some machines:
|
198
|
+
collins power -t 001234,003456,007895 -p reboot
|
166
199
|
|
167
|
-
##
|
200
|
+
## IPAM - collins ip
|
201
|
+
|
202
|
+
Allocate and delete addresses, and show what address pools are configured in Collins.
|
168
203
|
|
169
|
-
|
204
|
+
Usage: collins ipam [options]
|
205
|
+
|
206
|
+
-s, --show-pools Show IP pools
|
207
|
+
-H, --show-header Show header fields in --show-pools output
|
208
|
+
-a, --allocate POOL Allocate addresses in POOL
|
209
|
+
-n, --number [NUM] Allocate NUM addresses (Defaults to 1 if omitted)
|
210
|
+
-d, --delete [POOL] Delete addresses in POOL. Deletes ALL addresses if POOL is omitted
|
211
|
+
|
212
|
+
General:
|
213
|
+
-t, --tags TAG[,...] Tags to work on, comma separated
|
214
|
+
-C, --config CONFIG Use specific Collins config yaml for Collins::Client
|
215
|
+
-h, --help Help
|
216
|
+
|
217
|
+
Examples:
|
218
|
+
Show configured IP address pools:
|
219
|
+
collins ipam --show-pools -H
|
220
|
+
Allocate 2 IPs on each asset
|
221
|
+
collins ipam -t 001234,003456,007895 -a DEV_POOL -n2
|
222
|
+
Deallocate IPs in DEV_POOL pool on assets:
|
223
|
+
collins ipam -t 001234,003456,007895 -d DEV_POOL
|
224
|
+
Deallocate ALL IPs on assets:
|
225
|
+
collins ipam -t 001234,003456,007895 -d
|
226
|
+
|
227
|
+
## TODO
|
170
228
|
|
171
|
-
* Implement IP allocation in collins-
|
172
|
-
* Implement IPMI stuff in collins-
|
173
|
-
* Share code between binaries
|
229
|
+
* Implement IP allocation in collins-ipam
|
230
|
+
* Implement IPMI stuff in collins-ipmi
|
231
|
+
* Share code between binaries more
|
232
|
+
* Write some tests
|
data/bin/collins
CHANGED
@@ -1,21 +1,40 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
require 'collins-cli'
|
2
3
|
|
3
4
|
ALLOWED_ACTIONS = {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
Collins::CLI::Find => ['query','find'],
|
6
|
+
Collins::CLI::Modify => ['modify','set'],
|
7
|
+
Collins::CLI::Log => ['log'],
|
8
|
+
Collins::CLI::Provision => ['provision'],
|
9
|
+
Collins::CLI::Power => ['power'],
|
10
|
+
Collins::CLI::IPAM => ['ipam','address','ipaddress'],
|
8
11
|
}
|
9
|
-
|
10
|
-
|
11
|
-
if action.nil? || target.nil?
|
12
|
-
abort <<_MESSAGE_
|
13
|
-
Usage: #{File.basename(File.realpath($0))} <command> [options]
|
12
|
+
|
13
|
+
HELP_MESSAGE = "Usage: #{File.basename(File.realpath($0))} <command> [options]
|
14
14
|
Available commands:
|
15
|
-
query, find:
|
16
|
-
modify, set:
|
17
|
-
log:
|
18
|
-
provision
|
19
|
-
|
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: Provision assets
|
19
|
+
power: Control and show power status
|
20
|
+
ipam, address, ipaddress: Allocate and delete IPs, show IP pools"
|
21
|
+
|
22
|
+
abort HELP_MESSAGE if ARGV.empty?
|
23
|
+
action = ARGV.shift
|
24
|
+
targets = ALLOWED_ACTIONS.select {|k,v| v.any? {|handle| ! %r|^#{action}|.match(handle).nil? } }
|
25
|
+
target,_ = targets.first
|
26
|
+
if ['-h','--help'].include? action
|
27
|
+
puts HELP_MESSAGE
|
28
|
+
exit 0
|
29
|
+
elsif targets.empty? or target.nil?
|
30
|
+
abort ["Unknown action #{action}!","",HELP_MESSAGE].join("\n")
|
31
|
+
elsif targets.length > 1
|
32
|
+
abort ["Action #{action} was ambiguous! Please be more specific (i.e. type the whole action out)","",HELP_MESSAGE].join("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
exit target.new.parse!.validate!.run!
|
37
|
+
rescue => e
|
38
|
+
abort e.message
|
39
|
+
#raise e
|
20
40
|
end
|
21
|
-
exec File.join(__dir__,"collins-#{target}"), *ARGV
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'collins-cli'
|
2
|
+
|
3
|
+
#TODO: querying for :status or :state with -a status:maintenance doesnt play nice with -Smaintenance,allocated
|
4
|
+
#TODO: we construct the query for states and statuses only from the parameters to --status (ignoring any -a attributes)
|
5
|
+
|
6
|
+
module Collins::CLI
|
7
|
+
class Find
|
8
|
+
include Mixins
|
9
|
+
include Formatter # how to display assets
|
10
|
+
|
11
|
+
PROG_NAME = 'collins find'
|
12
|
+
QUERY_DEFAULTS = {
|
13
|
+
:operation => 'AND',
|
14
|
+
:size => 9999,
|
15
|
+
}
|
16
|
+
OPTION_DEFAULTS = {
|
17
|
+
:format => :table, # how to display the results
|
18
|
+
:separator => "\t",
|
19
|
+
:attributes => {}, # additional attributes to query for
|
20
|
+
:columns => [:tag, :hostname, :nodeclass, :status, :pool, :primary_role, :secondary_role],
|
21
|
+
:column_override => [], # if set, these are the columns to display
|
22
|
+
:timeout => 120,
|
23
|
+
:show_header => false, # if the header for columns should be displayed
|
24
|
+
:config => nil # collins config to give to setup_client
|
25
|
+
}
|
26
|
+
|
27
|
+
attr_reader :options, :query_opts, :search_attrs, :parser
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@parsed, @validated = false, false
|
31
|
+
@query_opts = QUERY_DEFAULTS.clone
|
32
|
+
@search_attrs = {}
|
33
|
+
@options = OPTION_DEFAULTS.clone
|
34
|
+
@parser = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse!(argv = ARGV)
|
38
|
+
raise "See --help for #{PROG_NAME} usage" if argv.empty?
|
39
|
+
@parser = OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: #{PROG_NAME} [options] [hostnamepattern]"
|
41
|
+
opts.separator "Query options:"
|
42
|
+
opts.on('-t','--tag TAG[,...]',Array, "Assets with tag[s] TAG") {|v| search_attrs[:tag] = v}
|
43
|
+
opts.on('-T','--type TYPE',String, "Only show assets with type TYPE") {|v| search_attrs[:type] = v}
|
44
|
+
opts.on('-n','--nodeclass NODECLASS[,...]',Array, "Assets in nodeclass NODECLASS") {|v| search_attrs[:nodeclass] = v}
|
45
|
+
opts.on('-p','--pool POOL[,...]',Array, "Assets in pool POOL") {|v| search_attrs[:pool] = v}
|
46
|
+
opts.on('-s','--size SIZE',Integer, "Number of assets to return (Default: #{query_opts[:size]})") {|v| query_opts[:size] = v}
|
47
|
+
opts.on('-r','--role ROLE[,...]',Array,"Assets in primary role ROLE") {|v| search_attrs[:primary_role] = v}
|
48
|
+
opts.on('-R','--secondary-role ROLE[,...]',Array,"Assets in secondary role ROLE") {|v| search_attrs[:secondary_role] = v}
|
49
|
+
opts.on('-i','--ip-address IP[,...]',Array,"Assets with IP address[es]") {|v| search_attrs[:ip_address] = v}
|
50
|
+
opts.on('-S','--status STATUS[:STATE][,...]',Array,"Asset status (and optional state after :)") do |v|
|
51
|
+
# in order to know what state was paired with what status, lets store the original params
|
52
|
+
# so the query constructor can create the correct CQL query
|
53
|
+
options[:status_state] = v
|
54
|
+
search_attrs[:status], search_attrs[:state] = v.inject([[],[]]) do |memo,s|
|
55
|
+
status,state = s.split(':')
|
56
|
+
memo[0] << status.upcase if not status.nil? and not status.empty?
|
57
|
+
memo[1] << state.upcase if not state.nil? and not state.empty?
|
58
|
+
memo
|
59
|
+
end
|
60
|
+
end
|
61
|
+
opts.on('-a','--attribute attribute[:value[,...]]',String,"Arbitrary attributes and values to match in query. : between key and value") do |x|
|
62
|
+
x.split(',').each do |p|
|
63
|
+
a,v = p.split(':')
|
64
|
+
a = a.to_sym
|
65
|
+
if not search_attrs[a].nil? and not search_attrs[a].is_a? Array
|
66
|
+
# its a single value, turn it into an array
|
67
|
+
search_attrs[a] = [search_attrs[a]]
|
68
|
+
end
|
69
|
+
if search_attrs[a].is_a? Array
|
70
|
+
# already multivalue, append
|
71
|
+
search_attrs[a] << v
|
72
|
+
else
|
73
|
+
search_attrs[a] = v
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.separator ""
|
79
|
+
opts.separator "Table formatting:"
|
80
|
+
opts.on('-H','--show-header',"Show header fields in output") {options[:show_header] = true}
|
81
|
+
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)}
|
82
|
+
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! }
|
83
|
+
opts.on('-f','--field-separator SEPARATOR',String,"Separator between columns in output (Default: #{options[:separator]})") {|v| options[:separator] = v}
|
84
|
+
|
85
|
+
opts.separator ""
|
86
|
+
opts.separator "Robot formatting:"
|
87
|
+
opts.on('-l','--link',"Output link to assets found in web UI") {options[:format] = :link}
|
88
|
+
opts.on('-j','--json',"Output results in JSON (NOTE: This probably wont be what you expected)") {options[:format] = :json}
|
89
|
+
opts.on('-y','--yaml',"Output results in YAML") {options[:format] = :yaml}
|
90
|
+
|
91
|
+
opts.separator ""
|
92
|
+
opts.separator "Extra options:"
|
93
|
+
opts.on('--expire SECONDS',Integer,"Timeout in seconds (0 == forever)") {|v| options[:timeout] = v}
|
94
|
+
opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| options[:config] = v}
|
95
|
+
opts.on('-h','--help',"Help") {options[:mode] = :help}
|
96
|
+
|
97
|
+
opts.separator ""
|
98
|
+
opts.separator <<_EXAMPLES_
|
99
|
+
Examples:
|
100
|
+
Query for devnodes in DEVEL pool that are VMs
|
101
|
+
cf -n develnode -p DEVEL
|
102
|
+
Query for asset 001234, and show its system_password
|
103
|
+
cf -t 001234 -x system_password
|
104
|
+
Query for all decommissioned VM assets
|
105
|
+
cf -a is_vm:true -S decommissioned
|
106
|
+
Query for hosts matching hostname '^web6-'
|
107
|
+
cf ^web6-
|
108
|
+
Query for all develnode6 nodes with a value for PUPPET_SERVER
|
109
|
+
cf -n develnode6 -a puppet_server -H
|
110
|
+
_EXAMPLES_
|
111
|
+
end
|
112
|
+
@parser.parse!(argv)
|
113
|
+
# hostname is the final option, no flags
|
114
|
+
search_attrs[:hostname] = argv.shift
|
115
|
+
@parsed = true
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate!
|
120
|
+
raise "Options not yet parsed with #parse!" unless @parsed
|
121
|
+
# fix bug where assets wont get found if they dont have that meta attribute
|
122
|
+
search_attrs.delete(:hostname) if search_attrs[:hostname].nil?
|
123
|
+
# for any search attributes, lets not pass arrays of 1 element
|
124
|
+
# as that will confuse as_query?
|
125
|
+
search_attrs.each do |k,v|
|
126
|
+
if v.is_a? Array
|
127
|
+
search_attrs[k] = v.first if v.length == 1
|
128
|
+
search_attrs[k] = nil if v.empty?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# merge search_attrs into query
|
133
|
+
if as_query?(search_attrs)
|
134
|
+
query_opts[:query] = convert_to_query(query_opts[:operation], search_attrs, options)
|
135
|
+
else
|
136
|
+
query_opts.merge!(search_attrs)
|
137
|
+
end
|
138
|
+
@validated = true
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
def run!
|
143
|
+
raise "Options not yet parsed with #parse!" unless @parsed
|
144
|
+
raise "Options not yet validated with #validate!" unless @validated
|
145
|
+
if options[:mode] == :help
|
146
|
+
puts parser
|
147
|
+
else
|
148
|
+
begin
|
149
|
+
assets = collins.find(query_opts)
|
150
|
+
rescue => e
|
151
|
+
raise "Error querying collins: #{e.message}"
|
152
|
+
end
|
153
|
+
format_assets(assets, options)
|
154
|
+
end
|
155
|
+
true
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'collins-cli'
|
2
|
+
|
3
|
+
module Collins::CLI::Formatter
|
4
|
+
FORMATTING_DEFAULTS = {
|
5
|
+
:format => :table, # how to display the results
|
6
|
+
:separator => "\t",
|
7
|
+
:columns => [:tag, :hostname, :nodeclass, :status, :pool, :primary_role, :secondary_role],
|
8
|
+
:column_override => [], # if set, these are the columns to display
|
9
|
+
:show_header => false, # if the header for columns should be displayed
|
10
|
+
}
|
11
|
+
ADDRESS_POOL_COLUMNS = [:name, :network, :start_address, :specified_gateway, :gateway, :broadcast, :possible_addresses]
|
12
|
+
|
13
|
+
def format_pools(pools, opts = {})
|
14
|
+
if pools.length > 0
|
15
|
+
opts = FORMATTING_DEFAULTS.merge(opts)
|
16
|
+
# map the hashes into openstructs that will respond to #send(:name)
|
17
|
+
ostructs = pools.map { |p| OpenStruct.new(Hash[p.map {|k,v| [k.downcase,v]}]) }
|
18
|
+
display_as_table(ostructs, ADDRESS_POOL_COLUMNS, opts[:separator], opts[:show_header])
|
19
|
+
else
|
20
|
+
raise "No pools found"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_assets(assets, opts = {})
|
25
|
+
opts = FORMATTING_DEFAULTS.merge(opts)
|
26
|
+
if assets.length > 0
|
27
|
+
case opts[:format]
|
28
|
+
when :table
|
29
|
+
# if the user passed :column_override, respect that absolutely. otherwise, the columns to display
|
30
|
+
# should be opts[:columns] + any extra attributes queried for. this way ```cf -c hostname -a is_vm:true```
|
31
|
+
# wont return 2 columns; only the one you asked for
|
32
|
+
columns = if opts[:column_override].empty?
|
33
|
+
opts[:columns].concat(search_attrs.keys).compact.uniq
|
34
|
+
else
|
35
|
+
opts[:column_override]
|
36
|
+
end
|
37
|
+
display_as_table(assets,columns,opts[:separator],opts[:show_header])
|
38
|
+
when :link
|
39
|
+
display_as_link assets, collins
|
40
|
+
when :json,:yaml
|
41
|
+
display_as_robot_talk(assets,opts[:format])
|
42
|
+
else
|
43
|
+
raise "I don't know how to display assets in #{opts[:format]} format!"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
raise "No assets found"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def display_as_robot_talk(assets, format = :json)
|
51
|
+
puts assets.send("to_#{format}".to_sym)
|
52
|
+
end
|
53
|
+
def display_as_table(assets, columns, separator, show_header = false)
|
54
|
+
# lets figure out how wide each column is, including header
|
55
|
+
column_width_pairs = columns.map do |column|
|
56
|
+
# grab all attributes == column and figure out max width
|
57
|
+
width = assets.map{|a| (column == :state) ? a.send(column).label.to_s.length : a.send(column).to_s.length}.max
|
58
|
+
width = [width, column.to_s.length].max if show_header
|
59
|
+
[column,width]
|
60
|
+
end
|
61
|
+
column_width_map = Hash[column_width_pairs]
|
62
|
+
if show_header
|
63
|
+
$stderr.puts column_width_map.map{|c,w| "%-#{w}s" % c}.join(separator)
|
64
|
+
end
|
65
|
+
assets.each do |a|
|
66
|
+
puts column_width_map.map {|c,w| v = (c == :state) ? a.send(c).label : a.send(c) ; "%-#{w}s" % v }.join(separator)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
def display_as_link assets, client
|
70
|
+
assets.each do |a|
|
71
|
+
puts "#{client.host}/asset/#{a.tag}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'collins-cli'
|
2
|
+
|
3
|
+
module Collins::CLI
|
4
|
+
class IPAM
|
5
|
+
include Mixins
|
6
|
+
include Formatter
|
7
|
+
PROG_NAME = 'collins ipam'
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
:timeout => 120,
|
11
|
+
:mode => nil,
|
12
|
+
:show_header => false,
|
13
|
+
:num => 1,
|
14
|
+
:tags => [],
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_reader :options, :parser
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@options = DEFAULT_OPTIONS.clone
|
21
|
+
@parsed, @validated = false, false
|
22
|
+
@parser = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse!(argv = ARGV)
|
26
|
+
@parser = OptionParser.new do |opts|
|
27
|
+
opts.banner = "Usage: #{PROG_NAME} [options]"
|
28
|
+
opts.separator ""
|
29
|
+
opts.on('-s','--show-pools',"Show IP pools") {|v| @options[:mode] = :show }
|
30
|
+
opts.on('-H','--show-header',"Show header fields in --show-pools output") {|v| @options[:show_header] = true }
|
31
|
+
opts.on('-a','--allocate POOL',String,"Allocate addresses in POOL") {|v| @options[:mode] = :allocate ; @options[:pool] = v }
|
32
|
+
opts.on('-n','--number [NUM]',Integer,"Allocate NUM addresses (Defaults to 1 if omitted)") {|v| @options[:num] = v || 1 }
|
33
|
+
opts.on('-d','--delete [POOL]',String,"Delete addresses in POOL. Deletes ALL addresses if POOL is omitted") {|v| @options[:mode] = :delete ; @options[:pool] = v }
|
34
|
+
|
35
|
+
opts.separator ""
|
36
|
+
opts.separator "General:"
|
37
|
+
opts.on('-t','--tags TAG[,...]',Array,"Tags to work on, comma separated") {|v| @options[:tags] = v.map(&:to_sym)}
|
38
|
+
opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| @options[:config] = v}
|
39
|
+
opts.on('-h','--help',"Help") {@options[:mode] = :help}
|
40
|
+
|
41
|
+
opts.separator ""
|
42
|
+
opts.separator "Examples:"
|
43
|
+
opts.separator " Show configured IP address pools:"
|
44
|
+
opts.separator " #{PROG_NAME} --show-pools -H"
|
45
|
+
opts.separator " Allocate 2 IPs on each asset"
|
46
|
+
opts.separator " #{PROG_NAME} -t 001234,003456,007895 -a DEV_POOL -n2"
|
47
|
+
opts.separator " Deallocate IPs in DEV_POOL pool on assets:"
|
48
|
+
opts.separator " #{PROG_NAME} -t 001234,003456,007895 -d DEV_POOL"
|
49
|
+
opts.separator " Deallocate ALL IPs on assets:"
|
50
|
+
opts.separator " #{PROG_NAME} -t 001234,003456,007895 -d"
|
51
|
+
end
|
52
|
+
@parser.parse!(argv)
|
53
|
+
|
54
|
+
# only read tags from ARGF if we are going to do something with the tags
|
55
|
+
if [:allocate,:delete].include? options[:mode] && (options[:tags].nil? or options[:tags].empty?)
|
56
|
+
# read tags from stdin. first field on the line is the tag
|
57
|
+
input = ARGF.readlines
|
58
|
+
@options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
|
59
|
+
end
|
60
|
+
@parsed = true
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate!
|
65
|
+
raise "You need to tell me to do something!" if @options[:mode].nil?
|
66
|
+
raise "No asset tags found via ARGF" if [:allocate,:delete].include?(options[:mode]) && (options[:tags].nil? or options[:tags].empty?)
|
67
|
+
@validated = true
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def run!
|
72
|
+
raise "Options not yet parsed with #parse!" unless @parsed
|
73
|
+
raise "Options not yet validated with #validate!" unless @validated
|
74
|
+
success = true
|
75
|
+
case options[:mode]
|
76
|
+
when :help
|
77
|
+
puts parser
|
78
|
+
when :show
|
79
|
+
pools = collins.ipaddress_pools
|
80
|
+
format_pools(pools, :show_header => options[:show_header])
|
81
|
+
when :allocate
|
82
|
+
options[:tags].each do |t|
|
83
|
+
res = api_call("allocating #{options[:num]} IP in #{options[:pool]}",:ipaddress_allocate!,t,options[:pool],options[:num]) do |addresses|
|
84
|
+
"Allocated #{addresses.map(&:address).join(' ')}"
|
85
|
+
end
|
86
|
+
success = false unless res
|
87
|
+
end
|
88
|
+
when :delete
|
89
|
+
options[:tags].each do |t|
|
90
|
+
res = api_call("deleting all IPs#{" in #{options[:pool]}" unless options[:pool].nil?}",:ipaddress_delete!,t,options[:pool]) { |count| "Deleted #{count} IPs" }
|
91
|
+
success = false unless res
|
92
|
+
end
|
93
|
+
end
|
94
|
+
success
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|