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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 087cbec35df74ca5deb78665d0d40547483968d3
4
- data.tar.gz: dcc8cc251bc58b3e6ca6df9881de6eeffef00caf
3
+ metadata.gz: 9d8cb9285558a8adc49d7da521c0241e1331eb08
4
+ data.tar.gz: 27559afb065f378b3e60aa010eb3271e40e53222
5
5
  SHA512:
6
- metadata.gz: 5d3b4860eede427118996a3740233e494df10b0bc46c94a2394f01d78fb1d20b3796a528d8353900bb8774f655f7bf9e86d24594c331b7fa56ac22b71471b319
7
- data.tar.gz: 19aa32592ffcf6cac34565ae87ca37bcd35c7ecc182138c2464bcceba65898258ac25fb11853f3d29fb627fe16c589fe5824bba081c7e60e6e70ff7857a6a4a8
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
- Main entry point is the ```collins``` binary.
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, action: Provision, control power status, allocate IPs, update IPMI info
28
+ provision: Provision assets
29
+ power: Control and show power status
30
+ ip, address, ipmi: Allocate IPs, update IPMI info
18
31
 
19
- ## Searching - collins-find
32
+ ## Find Assets - collins find
20
33
 
21
- Usage: collins-find [options] [hostnamepattern]
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
- ## Logging - collins-log
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-modify
108
+ ## Modification - collins modify
91
109
 
92
- Usage: collins-modify [options]
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-modify -t 001234,004567 -a my_attribute:true
139
+ collins modify -t 001234,004567 -a my_attribute:true
120
140
  Delete an attribute on some hosts:
121
- collins-modify -t 001234,004567 -d my_attribute
141
+ collins modify -t 001234,004567 -d my_attribute
122
142
  Delete and add attribute at same time:
123
- collins-modify -t 001234,004567 -a new_attr:test -d old_attr
143
+ collins modify -t 001234,004567 -a new_attr:test -d old_attr
124
144
  Set machine into maintenace noop:
125
- collins-modify -t 001234 -S maintenance:maint_noop -r "I do what I want"
145
+ collins modify -t 001234 -S maintenance:maint_noop -r "I do what I want"
126
146
  Set machine back to allocated:
127
- collins-modify -t 001234 -S allocated:running -r "Back to allocated"
147
+ collins modify -t 001234 -S allocated:running -r "Back to allocated"
128
148
  Set machine back to new without setting state:
129
- collins-modify -t 001234 -S new -r "Dunno why you would want this"
149
+ collins modify -t 001234 -S new -r "Dunno why you would want this"
130
150
  Create a log entry:
131
- collins-modify -t 001234 -l'computers are broken and everything is horrible' -Lwarning
151
+ collins modify -t 001234 -l'computers are broken and everything is horrible' -Lwarning
132
152
  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'
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
- ## Actions - collins-action
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
- 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
197
+ Reset some machines:
198
+ collins power -t 001234,003456,007895 -p reboot
166
199
 
167
- ## TODO
200
+ ## IPAM - collins ip
201
+
202
+ Allocate and delete addresses, and show what address pools are configured in Collins.
168
203
 
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
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-action
172
- * Implement IPMI stuff in collins-action
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
- :find => ['query','find'],
5
- :modify => ['modify','set'],
6
- :log => ['log'],
7
- :action => ['provision','action']
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
- 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]
12
+
13
+ HELP_MESSAGE = "Usage: #{File.basename(File.realpath($0))} <command> [options]
14
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_
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
+