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.
@@ -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
+