collins-cli 0.1.1 → 0.2.0
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 +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
|
+
|