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.
@@ -0,0 +1,173 @@
1
+ # TODO: make poll_wait tunable
2
+ # TODO: add options to sort ascending or descending on date
3
+ # TODO: implement searching logs (is this really useful?)
4
+ # TODO: add duplicate line detection and compression (...)
5
+
6
+ require 'collins-cli'
7
+ require 'set'
8
+
9
+ module Collins::CLI
10
+ class Log
11
+
12
+ include Mixins
13
+
14
+ LOG_LEVELS = Collins::Api::Logging::Severity.constants.map(&:to_s)
15
+ OPTIONS_DEFAULTS = {
16
+ :tags => [],
17
+ :show_all => false,
18
+ :poll_wait => 2,
19
+ :follow => false,
20
+ :severities => [],
21
+ :timeout => 20,
22
+ :sev_colors => {
23
+ 'EMERGENCY' => {:color => :red, :background => :light_blue},
24
+ 'ALERT' => {:color => :red},
25
+ 'CRITICAL' => {:color => :black, :background => :red},
26
+ 'ERROR' => {:color => :red},
27
+ 'WARNING' => {:color => :yellow},
28
+ 'NOTICE' => {},
29
+ 'INFORMATIONAL' => {:color => :green},
30
+ 'DEBUG' => {:color => :blue},
31
+ 'NOTE' => {:color => :light_cyan},
32
+ },
33
+ :config => nil
34
+ }
35
+ SEARCH_DEFAULTS = {
36
+ :size => 20,
37
+ :filter => nil,
38
+ }
39
+ PROG_NAME = 'collins log'
40
+
41
+ def initialize
42
+ @parsed = false
43
+ @validated = false
44
+ @running = false
45
+ @collins = nil
46
+ @logs_seen = []
47
+ @options = OPTIONS_DEFAULTS.clone
48
+ @search_opts = SEARCH_DEFAULTS.clone
49
+ end
50
+
51
+ def parse!(argv = ARGV)
52
+ raise "No flags given! See --help for #{PROG_NAME} usage" if argv.empty?
53
+ OptionParser.new do |opts|
54
+ opts.banner = "Usage: #{PROG_NAME} [options]"
55
+ opts.on('-a','--all',"Show logs from ALL assets") {|v| @options[:show_all] = true}
56
+ opts.on('-n','--number LINES',Integer,"Show the last LINES log entries. (Default: #{@search_opts[:size]})") {|v| @search_opts[:size] = v}
57
+ opts.on('-t','--tags TAGS',Array,"Tags to work on, comma separated") {|v| @options[:tags] = v}
58
+ opts.on('-f','--follow',"Poll for logs every #{@options[:poll_wait]} seconds") {|v| @options[:follow] = true}
59
+ opts.on('-s','--severity SEVERITY[,...]',Array,"Log severities to return (Defaults to all). Use !SEVERITY to exclude one.") {|v| @options[:severities] = v.map(&:upcase) }
60
+ #opts.on('-i','--interleave',"Interleave all log entries (Default: groups by asset)") {|v| options[:interleave] = true}
61
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| @options[:config] = v}
62
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
63
+ opts.separator ""
64
+ opts.separator <<_EOE_
65
+ Severities:
66
+ #{Collins::Api::Logging::Severity.to_a.map{|s| s.colorize(@options[:sev_colors][s])}.join(", ")}
67
+
68
+ Examples:
69
+ Show last 20 logs for an asset
70
+ #{PROG_NAME} -t 001234
71
+ Show last 100 logs for an asset
72
+ #{PROG_NAME} -t 001234 -n100
73
+ Show last 10 logs for 2 assets that are ERROR severity
74
+ #{PROG_NAME} -t 001234,001235 -n10 -sERROR
75
+ Show last 10 logs all assets that are not note or informational severity
76
+ #{PROG_NAME} -a -n10 -s'!informational,!note'
77
+ Show last 10 logs for all web nodes that are provisioned having verification in the message
78
+ collins find -S provisioned -n webnode\$ | #{PROG_NAME} -n10 -s debug | grep -i verification
79
+ Tail logs for all assets that are provisioning
80
+ collins find -Sprovisioning,provisioned | #{PROG_NAME} -f
81
+ _EOE_
82
+ end.parse!(argv)
83
+ @parsed = true
84
+ self
85
+ end
86
+
87
+ def validate!
88
+ raise "Options not yet parsed with #parse!" unless @parsed
89
+ unless @options[:severities].all? {|l| Collins::Api::Logging::Severity.valid?(l.tr('!','')) }
90
+ raise "Log severities #{@options[:severities].join(',')} are invalid! Use one of #{LOG_LEVELS.join(', ')}"
91
+ end
92
+ @search_opts[:filter] = @options[:severities].join(';')
93
+ if @options[:tags].empty? and not @options[:show_all]
94
+ # read tags from stdin. first field on the line is the tag
95
+ begin
96
+ input = ARGF.readlines
97
+ rescue Interrupt
98
+ raise "Interrupt reading tags from ARGF"
99
+ end
100
+ @options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
101
+ end
102
+ raise "You need to give me some assets to display logs; see --help" if @options[:tags].empty? and not @options[:show_all]
103
+ @validated = true
104
+ self
105
+ end
106
+
107
+ def run!
108
+ raise "Options not yet validated with #validate!" unless @validated
109
+ raise "Already running" if @running
110
+
111
+ begin
112
+ @running = true
113
+ all_logs = grab_logs
114
+ @logs_seen = all_logs.map(&:ID).to_set
115
+ output_logs(all_logs)
116
+ while @options[:follow]
117
+ sleep @options[:poll_wait]
118
+ logs = grab_logs
119
+ new_logs = logs.reject {|l| @logs_seen.include?(l.ID)}
120
+ output_logs(new_logs)
121
+ @logs_seen = @logs_seen | new_logs.map(&:ID)
122
+ end
123
+ return true
124
+ rescue Interrupt
125
+ return true
126
+ rescue
127
+ return false
128
+ ensure
129
+ @running = false
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def output_logs(logs)
136
+ # colorize output before computing width of fields
137
+ logs.map! do |l|
138
+ l.TYPE = @options[:sev_colors].has_key?(l.TYPE) ? l.TYPE.colorize(@options[:sev_colors][l.TYPE]) : l.TYPE
139
+ l
140
+ end
141
+ # show newest last
142
+ sorted_logs = logs.sort_by {|l| l.CREATED }
143
+ tag_width = sorted_logs.map{|l| l.ASSET_TAG.length}.max
144
+ sev_width = sorted_logs.map{|l| l.TYPE.length}.max
145
+ time_width = sorted_logs.map{|l| l.CREATED.length}.max
146
+ sorted_logs.each do |l|
147
+ puts "%-#{time_width}s: %-#{sev_width}s %-#{tag_width}s %s" % [l.CREATED, l.TYPE, l.ASSET_TAG, l.MESSAGE]
148
+ end
149
+ end
150
+
151
+ def grab_logs
152
+ if @options[:tags].empty?
153
+ begin
154
+ collins.all_logs(@search_opts)
155
+ rescue => e
156
+ $stderr.puts "Unable to fetch logs:".colorize(@options[:sev_colors]['WARNING']) + " #{e.message}"
157
+ []
158
+ end
159
+ else
160
+ @options[:tags].flat_map do |t|
161
+ begin
162
+ collins.logs(t, @search_opts)
163
+ rescue => e
164
+ $stderr.puts "Unable to fetch logs for #{t}:".colorize(@options[:sev_colors]['WARNING']) + " #{e.message}"
165
+ []
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ end
172
+ end
173
+
@@ -0,0 +1,73 @@
1
+ require 'collins-cli'
2
+
3
+ module Collins ; module CLI ; module Mixins
4
+ COLORS = {
5
+ 'EMERGENCY' => {:color => :red, :background => :light_blue},
6
+ 'ALERT' => {:color => :red},
7
+ 'CRITICAL' => {:color => :black, :background => :red},
8
+ 'ERROR' => {:color => :red},
9
+ 'WARNING' => {:color => :yellow},
10
+ 'NOTICE' => {},
11
+ 'INFORMATIONAL' => {:color => :green},
12
+ 'SUCCESS' => {:color => :green},
13
+ 'DEBUG' => {:color => :blue},
14
+ 'NOTE' => {:color => :light_cyan},
15
+ }
16
+ SUCCESS = "SUCCESS".colorize(COLORS['SUCCESS'])
17
+ ERROR = "ERROR".colorize(COLORS['ERROR'])
18
+
19
+ def collins
20
+ begin
21
+ @collins ||= Collins::Authenticator.setup_client timeout: @options[:timeout], config_file: @options[:config], prompt: true
22
+ rescue => e
23
+ raise "Unable to set up Collins client! #{e.message}"
24
+ end
25
+ end
26
+
27
+ def api_call desc, method, tag, *varargs, &block
28
+ printf "%s %s... " % [tag, desc]
29
+ result,message = begin
30
+ [collins.send(method,tag,*varargs),nil]
31
+ rescue => e
32
+ [false,e.message]
33
+ end
34
+ if result && block_given?
35
+ # if the call was a success, let the caller format the response
36
+ formatted_result = yield result
37
+ end
38
+ str = "#{result ? SUCCESS : ERROR}#{formatted_result.nil? ? '' : " (#{formatted_result})"}#{message.nil? ? nil : " (%s)" % e.message}"
39
+ puts str
40
+ result
41
+ end
42
+
43
+ def as_query?(attrs)
44
+ attrs.any?{|k,v| v.is_a? Array}
45
+ end
46
+
47
+ def convert_to_query(op, attrs, options)
48
+ # we want to support being able to query -Smaintenance:noop,:running,:provisioning_problem
49
+ # and not have the states ored together. Handle status/state pairs separately
50
+ basic_query = attrs.reject {|k,v| [:status,:state].include?(k)}.map do |k,v|
51
+ next if v.nil?
52
+ if v.is_a? Array
53
+ "(" + v.map{|x| "#{k} = #{x}"}.join(' OR ') + ")"
54
+ else
55
+ "#{k} = #{v}"
56
+ end
57
+ end.compact.join(" #{op} ")
58
+ # because they are provided in pairs, lets handle them together
59
+ # create the (( STATUS = maintenance AND STATE = noop) OR (STATE = provisioning_problem)) query
60
+ if options[:status_state]
61
+ status_query = options[:status_state].flat_map do |ss|
62
+ h = {}
63
+ h[:status], h[:state] = ss.split(':')
64
+ h[:status] = nil if h[:status].nil? or h[:status].empty?
65
+ h[:state] = nil if h[:state].nil? or h[:state].empty?
66
+ "( " + h.map {|k,v| v.nil? ? nil : "#{k.to_s.upcase} = #{v}"}.compact.join(" AND ") + " )"
67
+ end.compact.join(' OR ')
68
+ status_query = "( #{status_query} )"
69
+ end
70
+ [basic_query,status_query].reject {|q| q.nil? or q.empty?}.join(" #{op} ")
71
+ end
72
+
73
+ end ; end ; end
@@ -0,0 +1,144 @@
1
+ require 'collins-cli'
2
+
3
+ module Collins::CLI
4
+ class Modify
5
+
6
+ include Mixins
7
+
8
+ VALID_STATUSES = ["ALLOCATED","CANCELLED","DECOMMISSIONED","INCOMPLETE","MAINTENANCE","NEW","PROVISIONED","PROVISIONING","UNALLOCATED"]
9
+ #TODO: this shouldnt be hardcoded. we should pull this from the API instead?
10
+ # should elegantly support user-defined states without changing this script
11
+ VALID_STATES = {
12
+ "ALLOCATED" => ["CLAIMED","SPARE","RUNNING_UNMONITORED","UNMONITORED"],
13
+ "MAINTENANCE" => ["AWAITING_REVIEW","HARDWARE_PROBLEM","HW_TESTING","HARDWARE_UPGRADE","IPMI_PROBLEM","MAINT_NOOP","NETWORK_PROBLEM","RELOCATION",'PROVISIONING_PROBLEM'],
14
+ "ANY" => ["RUNNING","STARTING","STOPPING","TERMINATED"],
15
+ }
16
+ LOG_LEVELS = Collins::Api::Logging::Severity.constants.map(&:to_s)
17
+ OPTIONS_DEFAULTS = {
18
+ :query_size => 9999,
19
+ :attributes => {},
20
+ :delete_attributes => [],
21
+ :log_level => 'NOTE',
22
+ :timeout => 120,
23
+ :config => nil
24
+ }
25
+ PROG_NAME = 'collins modify'
26
+
27
+ attr_reader :options
28
+
29
+ def initialize
30
+ @options = OPTIONS_DEFAULTS.clone
31
+ @validated = false
32
+ @parsed = false
33
+ end
34
+
35
+ def parse!(argv = ARGV)
36
+ OptionParser.new do |opts|
37
+ opts.banner = "Usage: #{PROG_NAME} [options]"
38
+ opts.on('-a','--set-attribute attribute:value',String,"Set attribute=value. : between key and value. attribute will be uppercased.") do |x|
39
+ if not x.include? ':'
40
+ puts '--set-attribute requires attribute:value, missing :value'
41
+ puts opts.help
42
+ exit 1
43
+ end
44
+ a,v = x.split(':')
45
+ @options[:attributes][a.upcase.to_sym] = v
46
+ end
47
+ opts.on('-d','--delete-attribute attribute',String,"Delete attribute.") {|v| @options[:delete_attributes] << v.to_sym }
48
+ opts.on('-S','--set-status status[:state]',String,'Set status (and optionally state) to status:state. Requires --reason') do |v|
49
+ status,state = v.split(':')
50
+ @options[:status] = status.upcase if not status.nil? and not status.empty?
51
+ @options[:state] = state.upcase if not state.nil? and not state.empty?
52
+ end
53
+ opts.on('-r','--reason REASON',String,"Reason for changing status/state.") {|v| @options[:reason] = v }
54
+ opts.on('-l','--log MESSAGE',String,"Create a log entry.") do |v|
55
+ @options[:log_message] = v
56
+ end
57
+ opts.on('-L','--level LEVEL',String, LOG_LEVELS + LOG_LEVELS.map(&:downcase),"Set log level. Default level is #{@options[:log_level]}.") do |v|
58
+ @options[:log_level] = v.upcase
59
+ end
60
+ opts.on('-t','--tags TAGS',Array,"Tags to work on, comma separated") {|v| @options[:tags] = v.map(&:to_sym)}
61
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| @options[:config] = v}
62
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
63
+ opts.separator ""
64
+ opts.separator "Allowed values (uppercase or lowercase is accepted):"
65
+ opts.separator <<_EOF_
66
+ Status (-S,--set-status):
67
+ #{VALID_STATUSES.join(', ')}
68
+ States (-S,--set-status):
69
+ #{VALID_STATES.keys.map {|k| "#{k} ->\n #{VALID_STATES[k].join(', ')}"}.join "\n "}
70
+ Log levels (-L,--level):
71
+ #{LOG_LEVELS.join(', ')}
72
+ _EOF_
73
+ opts.separator ""
74
+ opts.separator "Examples:"
75
+ opts.separator <<_EOF_
76
+ Set an attribute on some hosts:
77
+ #{PROG_NAME} -t 001234,004567 -a my_attribute:true
78
+ Delete an attribute on some hosts:
79
+ #{PROG_NAME} -t 001234,004567 -d my_attribute
80
+ Delete and add attribute at same time:
81
+ #{PROG_NAME} -t 001234,004567 -a new_attr:test -d old_attr
82
+ Set machine into maintenace noop:
83
+ #{PROG_NAME} -t 001234 -S maintenance:maint_noop -r "I do what I want"
84
+ Set machine back to allocated:
85
+ #{PROG_NAME} -t 001234 -S allocated:running -r "Back to allocated"
86
+ Set machine back to new without setting state:
87
+ #{PROG_NAME} -t 001234 -S new -r "Dunno why you would want this"
88
+ Create a log entry:
89
+ #{PROG_NAME} -t 001234 -l'computers are broken and everything is horrible' -Lwarning
90
+ Read from stdin:
91
+ cf -n develnode | #{PROG_NAME} -d my_attribute
92
+ cf -n develnode -S allocated | #{PROG_NAME} -a collectd_version:5.2.1-52
93
+ echo -e "001234\\n001235\\n001236"| #{PROG_NAME} -a test_attribute:'hello world'
94
+ _EOF_
95
+ end.parse!(argv)
96
+ if options[:tags].nil? or options[:tags].empty?
97
+ # read tags from stdin. first field on the line is the tag
98
+ input = ARGF.readlines
99
+ @options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
100
+ end
101
+ @parsed = true
102
+ self
103
+ end
104
+
105
+ def validate!
106
+ raise "See --help for #{PROG_NAME} usage" if options[:attributes].empty? and options[:delete_attributes].empty? and options[:status].nil? and options[:log_message].nil?
107
+ raise "You need to provide a --reason when changing asset states!" if not options[:status].nil? and options[:reason].nil?
108
+ #TODO this is never checked because we are making option parser vet our options for levels. Catch OptionParser::InvalidArgument?
109
+ raise "Log level #{options[:log_level]} is invalid! Use one of #{LOG_LEVELS.join(', ')}" unless Collins::Api::Logging::Severity.valid?(options[:log_level])
110
+
111
+ # if any statuses or states, validate them against allowed values
112
+ unless options[:status].nil?
113
+ raise "Invalid status #{options[:status]} (Should be in #{VALID_STATUSES.join(', ')})" unless VALID_STATUSES.include? options[:status]
114
+ states_for_status = VALID_STATES["ANY"].concat((VALID_STATES[options[:status]].nil?) ? [] : VALID_STATES[options[:status]])
115
+ raise "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])
116
+ end
117
+
118
+
119
+ @validated = true
120
+ self
121
+ end
122
+
123
+ def run!
124
+ exit_clean = true
125
+ options[:tags].each do |t|
126
+ if options[:log_message]
127
+ exit_clean = api_call("logging #{options[:log_level].downcase} #{options[:log_message].inspect}", :log!, t, options[:log_message], options[:log_level]) && exit_clean
128
+ end
129
+ options[:attributes].each do |k,v|
130
+ exit_clean = api_call("setting #{k}=#{v}", :set_attribute!, t, k, v) && exit_clean
131
+ end
132
+ options[:delete_attributes].each do |k|
133
+ exit_clean = api_call("deleting #{k}", :delete_attribute!, t, k) && exit_clean
134
+ end
135
+ if options[:status]
136
+ exit_clean = api_call("changing status to #{options[:status]}#{options[:state] ? ":#{options[:state]}" : ''}", :set_status!, t, :status => options[:status], :state => options[:state], :reason => options[:reason]) && exit_clean
137
+ end
138
+ end
139
+ exit_clean
140
+ end
141
+
142
+ end
143
+ end
144
+
@@ -0,0 +1,77 @@
1
+ require 'collins-cli'
2
+
3
+ module Collins::CLI
4
+ class Power
5
+ include Mixins
6
+ PROG_NAME = 'collins power'
7
+ ALLOWABLE_POWER_ACTIONS = ['reboot','rebootsoft','reboothard','on','off','poweron','poweroff','identify']
8
+ DEFAULT_OPTIONS = {
9
+ :timeout => 120,
10
+ }
11
+
12
+ attr_reader :options
13
+
14
+ def initialize
15
+ @options = DEFAULT_OPTIONS.clone
16
+ @parsed, @validated = false, false
17
+ @parser = nil
18
+ end
19
+
20
+ def parse!(argv = ARGV)
21
+ @parser = OptionParser.new do |opts|
22
+ opts.banner = "Usage: #{PROG_NAME} [options]"
23
+ opts.separator ""
24
+ opts.on('-s','--status',"Show IPMI power status") {|v| @options[:mode] = :status }
25
+ opts.on('-p','--power ACTION',String,"Perform IPMI power ACTION") {|v| @options[:mode] = :power ; @options[:power] = v.downcase }
26
+
27
+ opts.separator ""
28
+ opts.separator "General:"
29
+ opts.on('-t','--tags TAG[,...]',Array,"Tags to work on, comma separated") {|v| @options[:tags] = v.map(&:to_sym)}
30
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| @options[:config] = v}
31
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
32
+
33
+ opts.separator ""
34
+ opts.separator "Examples:"
35
+ opts.separator " Reset some machines:"
36
+ opts.separator " #{PROG_NAME} -t 001234,003456,007895 -p reboot"
37
+ end.parse!(argv)
38
+
39
+ # convert what we allow to be specified to what collins::power allows
40
+ @options[:power] = 'rebootsoft' if @options[:power] == 'reboot'
41
+
42
+ if options[:tags].nil? or options[:tags].empty?
43
+ # read tags from stdin. first field on the line is the tag
44
+ input = ARGF.readlines
45
+ @options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
46
+ end
47
+ @parsed = true
48
+ self
49
+ end
50
+
51
+ def validate!
52
+ raise "You need to tell me to do something!" if @options[:mode].nil?
53
+ if options[:mode] == :power
54
+ abort "Unknown power action #{options[:power]}, expecting one of #{ALLOWABLE_POWER_ACTIONS.join(',')}" unless ALLOWABLE_POWER_ACTIONS.include? options[:power]
55
+ # TODO this arguably shouldnt be in validate. Maybe #parse!?
56
+ @options[:power] = Collins::Power.normalize_action @options[:power]
57
+ end
58
+ self
59
+ end
60
+
61
+ def run!
62
+ success = true
63
+ options[:tags].each do |t|
64
+ case options[:mode]
65
+ when :status
66
+ res = api_call("checking power status",:power_status,t) {|status| status}
67
+ success = false if !res
68
+ when :power
69
+ success &&= api_call("performing #{options[:power]}", :power!, t, options[:power])
70
+ end
71
+ end
72
+ success
73
+ end
74
+
75
+ end
76
+ end
77
+
@@ -0,0 +1,80 @@
1
+ require 'collins-cli'
2
+ require 'etc'
3
+
4
+ #TODO help should be an action done in run
5
+
6
+ module Collins::CLI
7
+ class Provision
8
+ include Mixins
9
+ PROG_NAME = 'collins provision'
10
+ DEFAULT_OPTIONS = {
11
+ :timeout => 120,
12
+ :provision => { }
13
+ }
14
+
15
+ attr_reader :options
16
+
17
+ def initialize
18
+ @options = DEFAULT_OPTIONS.clone
19
+ @parsed, @validated = false, false
20
+ @options[:build_contact] = Etc.getlogin
21
+ @parser = nil
22
+ end
23
+
24
+ def parse!(argv = ARGV)
25
+ @parser = OptionParser.new do |opts|
26
+ opts.banner = "Usage: #{PROG_NAME} [options]"
27
+ #TODO -s to show provisoining_profiles
28
+ opts.separator ""
29
+ opts.on('-n','--nodeclass NODECLASS',String,"Nodeclass to provision as. (Required)") {|v| @options[:provision][:nodeclass] = v }
30
+ opts.on('-p','--pool POOL',String,"Provision with pool POOL.") {|v| @options[:provision][:pool] = v }
31
+ opts.on('-r','--role ROLE',String,"Provision with primary role ROLE.") {|v| @options[:provision][:primary_role] = v }
32
+ opts.on('-R','--secondary-role ROLE',String,"Provision with secondary role ROLE.") {|v| @options[:provision][:secondary_role] = v }
33
+ opts.on('-s','--suffix SUFFIX',String,"Provision with suffix SUFFIX.") {|v| @options[:provision][:suffix] = v }
34
+ opts.on('-a','--activate',"Activate server on provision (useful with SL plugin) (Default: ignored)") {|v| @options[:provision][:activate] = true }
35
+ opts.on('-b','--build-contact USER',String,"Build contact. (Default: #{@options[:build_contact]})") {|v| @options[:build_contact] = v }
36
+
37
+ opts.separator ""
38
+ opts.separator "General:"
39
+ opts.on('-t','--tags TAG[,...]',Array,"Tags to work on, comma separated") {|v| @options[:tags] = v.map(&:to_sym)}
40
+ opts.on('-C','--config CONFIG',String,'Use specific Collins config yaml for Collins::Client') {|v| @options[:config] = v}
41
+ opts.on('-h','--help',"Help") {puts opts ; exit 0}
42
+
43
+ opts.separator ""
44
+ opts.separator "Examples:\n Provision some machines:\n collins find -Sunallocated -arack_position:716|#{PROG_NAME} -P -napiwebnode6 -RALL"
45
+ end.parse!(argv)
46
+
47
+ if @options[:tags].nil? or @options[:tags].empty?
48
+ # read tags from stdin. first field on the line is the tag
49
+ input = ARGF.readlines
50
+ @options[:tags] = input.map{|l| l.split(/\s+/)[0] rescue nil}.compact.uniq
51
+ end
52
+ @parsed = true
53
+ self
54
+ end
55
+
56
+ def validate!
57
+ raise "You need to specify at least a nodeclass when provisioning" if options[:provision][:nodeclass].nil?
58
+ self
59
+ end
60
+
61
+ def run!
62
+ action_successes = []
63
+ options[:tags].each do |t|
64
+ action_string = "#{t} provisioning with #{options[:provision].map{|k,v| "#{k}:#{v}"}.join(" ")} by #{options[:build_contact]}... "
65
+ printf action_string
66
+ begin
67
+ res = collins.provision(t, options[:provision][:nodeclass], options[:build_contact], options[:provision])
68
+ puts (res ? SUCCESS : ERROR )
69
+ action_successes << res
70
+ rescue => e
71
+ puts "#{ERROR} (#{e.message})"
72
+ action_successes << false
73
+ end
74
+ end
75
+ action_successes.all?
76
+ end
77
+
78
+ end
79
+ end
80
+
@@ -0,0 +1,13 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'collins_auth'
4
+ require 'yaml'
5
+ require 'json'
6
+ require 'optparse'
7
+ require 'colorize'
8
+
9
+ ['mixins','formatter','log','modify','find','power','provision','ipam'].
10
+ each {|r| require File.join('collins/cli',r) }
11
+
12
+ module Collins ; module CLI ; end ; end
13
+
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Collins::CLI::Find do
4
+ before(:each) do
5
+ Collins::CLI::Find.stub(:format_assets).and_return(true)
6
+ Collins::CLI::Find.stub_chain(:collins,:find).and_return([])
7
+ subject { Collins::CLI::Find.new }
8
+ end
9
+ context "#parse!" do
10
+ [
11
+ %w|-h|,
12
+ %w|-S allocated,maintenance|,
13
+ %w|-n testnode -ais_vm:true|,
14
+ %w|hostname|,
15
+ %w|hostname -c tag|,
16
+ ].each do |args|
17
+ it "Should parse #{args.inspect} successfully" do
18
+ expect{subject.parse!(args)}.to_not raise_error
19
+ end
20
+ end
21
+ [
22
+ %w|-ZZZZZZZ|,
23
+ %w|-K -Z_ LJIFJ?=I)|,
24
+ ].each do |args|
25
+ it "Should fail to parse unknown flags #{args.inspect}" do
26
+ expect{subject.parse!(args)}.to raise_error
27
+ end
28
+ end
29
+ it "requires arguments" do
30
+ expect{subject.parse!([])}.to raise_error(/See --help/)
31
+ end
32
+ end
33
+
34
+ context "#validate!" do
35
+ it "raises if not yet parsed" do
36
+ expect{subject.validate!}.to raise_error(/not yet parsed/)
37
+ end
38
+ end
39
+
40
+ context "#run!" do
41
+ it "raises if not yet parsed" do
42
+ expect{subject.run!}.to raise_error(/not yet parsed/)
43
+ end
44
+ it "raises if not yet validated" do
45
+ expect{subject.parse!(%w|-n nodeclass|).run!}.to raise_error(/not yet validated/)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Collins::CLI::Log do
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Collins::CLI::Modify do
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Collins::CLI::Power do
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Collins::CLI::Provision do
4
+ end
@@ -0,0 +1,2 @@
1
+ require 'collins-cli'
2
+