collins-cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +95 -36
- data/bin/collins +34 -15
- data/lib/collins/cli/find.rb +161 -0
- data/lib/collins/cli/formatter.rb +74 -0
- data/lib/collins/cli/ipam.rb +99 -0
- data/lib/collins/cli/log.rb +173 -0
- data/lib/collins/cli/mixins.rb +73 -0
- data/lib/collins/cli/modify.rb +144 -0
- data/lib/collins/cli/power.rb +77 -0
- data/lib/collins/cli/provision.rb +80 -0
- data/lib/collins-cli.rb +13 -0
- data/spec/collins__cli__find_spec.rb +48 -0
- data/spec/collins__cli__log_spec.rb +4 -0
- data/spec/collins__cli__modify_spec.rb +4 -0
- data/spec/collins__cli__power_spec.rb +4 -0
- data/spec/collins__cli__provision_spec.rb +4 -0
- data/spec/spec_helper.rb +2 -0
- metadata +52 -11
- data/bin/collins-action +0 -130
- data/bin/collins-find +0 -225
- data/bin/collins-log +0 -143
- data/bin/collins-modify +0 -149
@@ -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