collins-cli 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +95 -36
- data/bin/collins +34 -15
- data/lib/collins/cli/find.rb +161 -0
- data/lib/collins/cli/formatter.rb +74 -0
- data/lib/collins/cli/ipam.rb +99 -0
- data/lib/collins/cli/log.rb +173 -0
- data/lib/collins/cli/mixins.rb +73 -0
- data/lib/collins/cli/modify.rb +144 -0
- data/lib/collins/cli/power.rb +77 -0
- data/lib/collins/cli/provision.rb +80 -0
- data/lib/collins-cli.rb +13 -0
- data/spec/collins__cli__find_spec.rb +48 -0
- data/spec/collins__cli__log_spec.rb +4 -0
- data/spec/collins__cli__modify_spec.rb +4 -0
- data/spec/collins__cli__power_spec.rb +4 -0
- data/spec/collins__cli__provision_spec.rb +4 -0
- data/spec/spec_helper.rb +2 -0
- metadata +52 -11
- data/bin/collins-action +0 -130
- data/bin/collins-find +0 -225
- data/bin/collins-log +0 -143
- data/bin/collins-modify +0 -149
@@ -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
|
+
|
data/lib/collins-cli.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED