hammer_cli 0.0.4
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.
- data/LICENSE +5 -0
- data/README.md +105 -0
- data/bin/hammer +53 -0
- data/config/cli_config.template.yml +14 -0
- data/doc/design.png +0 -0
- data/doc/design.uml +24 -0
- data/hammer_cli_complete +13 -0
- data/lib/hammer_cli/abstract.rb +95 -0
- data/lib/hammer_cli/apipie/command.rb +75 -0
- data/lib/hammer_cli/apipie/options.rb +97 -0
- data/lib/hammer_cli/apipie/read_command.rb +58 -0
- data/lib/hammer_cli/apipie/resource.rb +54 -0
- data/lib/hammer_cli/apipie/write_command.rb +35 -0
- data/lib/hammer_cli/apipie.rb +3 -0
- data/lib/hammer_cli/autocompletion.rb +46 -0
- data/lib/hammer_cli/exception_handler.rb +69 -0
- data/lib/hammer_cli/exit_codes.rb +23 -0
- data/lib/hammer_cli/logger.rb +50 -0
- data/lib/hammer_cli/logger_watch.rb +14 -0
- data/lib/hammer_cli/main.rb +53 -0
- data/lib/hammer_cli/messages.rb +55 -0
- data/lib/hammer_cli/option_formatters.rb +13 -0
- data/lib/hammer_cli/output/adapter/abstract.rb +27 -0
- data/lib/hammer_cli/output/adapter/base.rb +143 -0
- data/lib/hammer_cli/output/adapter/silent.rb +17 -0
- data/lib/hammer_cli/output/adapter/table.rb +53 -0
- data/lib/hammer_cli/output/adapter.rb +6 -0
- data/lib/hammer_cli/output/definition.rb +17 -0
- data/lib/hammer_cli/output/dsl.rb +56 -0
- data/lib/hammer_cli/output/fields.rb +128 -0
- data/lib/hammer_cli/output/output.rb +27 -0
- data/lib/hammer_cli/output.rb +6 -0
- data/lib/hammer_cli/settings.rb +48 -0
- data/lib/hammer_cli/shell.rb +49 -0
- data/lib/hammer_cli/validator.rb +114 -0
- data/lib/hammer_cli/version.rb +5 -0
- data/lib/hammer_cli.rb +13 -0
- metadata +189 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'logging'
|
3
|
+
|
4
|
+
module HammerCLI
|
5
|
+
class ExceptionHandler
|
6
|
+
|
7
|
+
def initialize options={}
|
8
|
+
@output = options[:output] or raise "Missing option output"
|
9
|
+
@logger = Logging.logger['Exception']
|
10
|
+
end
|
11
|
+
|
12
|
+
def mappings
|
13
|
+
[
|
14
|
+
[Exception, :handle_general_exception], # catch all
|
15
|
+
[RestClient::ResourceNotFound, :handle_not_found],
|
16
|
+
[RestClient::Unauthorized, :handle_unauthorized],
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_exception e, options={}
|
21
|
+
@options = options
|
22
|
+
handler = mappings.reverse.find { |m| e.class.respond_to?(:"<=") ? e.class <= m[0] : false }
|
23
|
+
return send(handler[1], e) if handler
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def print_error error
|
30
|
+
error = error.join("\n") if error.kind_of? Array
|
31
|
+
@logger.error error
|
32
|
+
|
33
|
+
if @options[:heading]
|
34
|
+
@output.print_error @options[:heading], error
|
35
|
+
else
|
36
|
+
@output.print_error error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_full_error e
|
41
|
+
backtrace = e.backtrace || []
|
42
|
+
@logger.error "\n\n#{e.class} (#{e.message}):\n " +
|
43
|
+
backtrace.join("\n ")
|
44
|
+
"\n\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_general_exception e
|
48
|
+
print_error "Error: " + e.message
|
49
|
+
log_full_error e
|
50
|
+
HammerCLI::EX_SOFTWARE
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_not_found e
|
54
|
+
print_error e.message
|
55
|
+
log_full_error e
|
56
|
+
HammerCLI::EX_NOT_FOUND
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_unauthorized e
|
60
|
+
print_error "Invalid username or password"
|
61
|
+
log_full_error e
|
62
|
+
HammerCLI::EX_UNAUTHORIZED
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HammerCLI
|
2
|
+
# taken from sysexits.h
|
3
|
+
EX_OK = 0 # successful termination
|
4
|
+
EX_USAGE = 64 # command line usage error
|
5
|
+
EX_DATAERR = 65 # data format error
|
6
|
+
EX_NOINPUT = 66 # cannot open input
|
7
|
+
EX_NOUSER = 67 # addressee unknown
|
8
|
+
EX_NOHOST = 68 # host name unknown
|
9
|
+
EX_UNAVAILABLE = 69 # service unavailable
|
10
|
+
EX_SOFTWARE = 70 # internal software error
|
11
|
+
EX_OSERR = 71 # system error (e.g., can't fork)
|
12
|
+
EX_OSFILE = 72 # critical OS file missing
|
13
|
+
EX_CANTCREAT = 73 # can't create (user) output file
|
14
|
+
EX_IOERR = 74 # input/output error
|
15
|
+
EX_TEMPFAIL = 75 # temp failure; user is invited to retry
|
16
|
+
EX_PROTOCOL = 76 # remote error in protocol
|
17
|
+
EX_NOPERM = 77 # permission denied
|
18
|
+
EX_CONFIG = 78 # configuration error
|
19
|
+
|
20
|
+
# non POSIX codes
|
21
|
+
EX_NOT_FOUND = 128 # resource was not found
|
22
|
+
EX_UNAUTHORIZED = 129 # authorization failed
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'logging'
|
3
|
+
|
4
|
+
module HammerCLI
|
5
|
+
module Logger
|
6
|
+
|
7
|
+
Logging.color_scheme('bright',
|
8
|
+
:levels => {
|
9
|
+
:info => :green,
|
10
|
+
:warn => :yellow,
|
11
|
+
:error => :red,
|
12
|
+
:fatal => [:white, :on_red]},
|
13
|
+
:date => :blue,
|
14
|
+
:logger => :cyan,
|
15
|
+
:line => :yellow,
|
16
|
+
:file => :yellow,
|
17
|
+
:method => :yellow)
|
18
|
+
|
19
|
+
pattern = "[%5l %d %c] %m\n"
|
20
|
+
COLOR_LAYOUT = Logging::Layouts::Pattern.new(:pattern => pattern, :color_scheme => 'bright')
|
21
|
+
NOCOLOR_LAYOUT = Logging::Layouts::Pattern.new(:pattern => pattern, :color_scheme => nil)
|
22
|
+
DEFAULT_LOG_DIR = '/var/log/foreman'
|
23
|
+
|
24
|
+
log_dir = File.expand_path(HammerCLI::Settings[:log_dir] || DEFAULT_LOG_DIR)
|
25
|
+
begin
|
26
|
+
FileUtils.mkdir_p(log_dir, :mode => 0750)
|
27
|
+
rescue Errno::EACCES => e
|
28
|
+
puts "No permissions to create log dir #{log_dir}"
|
29
|
+
end
|
30
|
+
|
31
|
+
logger = Logging.logger.root
|
32
|
+
filename = "#{log_dir}/hammer.log"
|
33
|
+
begin
|
34
|
+
logger.appenders = ::Logging.appenders.rolling_file('configure',
|
35
|
+
:filename => filename,
|
36
|
+
:layout => NOCOLOR_LAYOUT,
|
37
|
+
:truncate => false,
|
38
|
+
:keep => 5,
|
39
|
+
:size => HammerCLI::Settings[:log_size] || 1024*1024) # 1MB
|
40
|
+
# set owner and group (it's ignored if attribute is nil)
|
41
|
+
FileUtils.chown HammerCLI::Settings[:log_owner], HammerCLI::Settings[:log_group], filename
|
42
|
+
rescue ArgumentError => e
|
43
|
+
puts "File #{filename} not writeable, won't log anything to file!"
|
44
|
+
end
|
45
|
+
|
46
|
+
logger.level = HammerCLI::Settings[:log_level]
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'awesome_print'
|
2
|
+
|
3
|
+
module HammerCLI
|
4
|
+
module Logger
|
5
|
+
module Watch
|
6
|
+
def watch(label, obj, options={})
|
7
|
+
if debug?
|
8
|
+
options = { :plain => HammerCLI::Settings[:watch_plain], :indent => -2 }.merge(options)
|
9
|
+
debug label + "\n" + obj.ai(options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
|
3
|
+
module HammerCLI
|
4
|
+
|
5
|
+
class MainCommand < AbstractCommand
|
6
|
+
|
7
|
+
option ["-v", "--verbose"], :flag, "be verbose"
|
8
|
+
option ["-c", "--config"], "CFG_FILE", "path to custom config file"
|
9
|
+
|
10
|
+
option ["-u", "--username"], "USERNAME", "username to access the remote system"
|
11
|
+
option ["-p", "--password"], "PASSWORD", "password to access the remote system"
|
12
|
+
|
13
|
+
option "--version", :flag, "show version" do
|
14
|
+
puts "hammer-%s" % HammerCLI.version
|
15
|
+
exit(HammerCLI::EX_OK)
|
16
|
+
end
|
17
|
+
|
18
|
+
option ["-P", "--ask-pass"], :flag, "Ask for password" do
|
19
|
+
context[:password] = get_password()
|
20
|
+
''
|
21
|
+
end
|
22
|
+
|
23
|
+
option "--autocomplete", "LINE", "Get list of possible endings" do |line|
|
24
|
+
line = line.split
|
25
|
+
line.shift
|
26
|
+
endings = self.class.autocomplete(line).map { |l| l[0] }
|
27
|
+
puts endings.join(' ')
|
28
|
+
exit(HammerCLI::EX_OK)
|
29
|
+
end
|
30
|
+
|
31
|
+
def password=(password)
|
32
|
+
context[:password] = password.nil? ? ENV['FOREMAN_PASSWORD'] : password
|
33
|
+
end
|
34
|
+
|
35
|
+
def username=(username)
|
36
|
+
context[:username] = username.nil? ? ENV['FOREMAN_USERNAME'] : username
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def get_password(prompt="Enter Password ")
|
42
|
+
ask(prompt) {|q| q.echo = false}
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# extend MainCommand
|
52
|
+
require 'hammer_cli/shell'
|
53
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
module HammerCLI
|
3
|
+
module Messages
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def success_message_for action
|
10
|
+
self.class.success_message_for action
|
11
|
+
end
|
12
|
+
|
13
|
+
def success_message
|
14
|
+
self.class.success_message
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure_message_for action
|
18
|
+
self.class.failure_message_for action
|
19
|
+
end
|
20
|
+
|
21
|
+
def failure_message
|
22
|
+
self.class.failure_message
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_exception e
|
26
|
+
exception_handler.handle_exception e, :heading => failure_message
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def success_message_for action, msg=nil
|
31
|
+
@success_message ||= {}
|
32
|
+
@success_message[action] = msg unless msg.nil?
|
33
|
+
@success_message[action]
|
34
|
+
end
|
35
|
+
|
36
|
+
def success_message msg=nil
|
37
|
+
success_message_for :default, msg
|
38
|
+
end
|
39
|
+
|
40
|
+
def failure_message_for action, msg=nil
|
41
|
+
@failure_message ||= {}
|
42
|
+
@failure_message[action] = msg unless msg.nil?
|
43
|
+
@failure_message[action]
|
44
|
+
end
|
45
|
+
|
46
|
+
def failure_message msg=nil
|
47
|
+
failure_message_for :default, msg
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
module HammerCLI::Output::Adapter
|
3
|
+
|
4
|
+
class Abstract
|
5
|
+
|
6
|
+
def print_message msg
|
7
|
+
puts msg
|
8
|
+
end
|
9
|
+
|
10
|
+
def print_error msg, details=nil
|
11
|
+
details = details.split("\n") if details.kind_of? String
|
12
|
+
|
13
|
+
if details
|
14
|
+
indent = " "
|
15
|
+
$stderr.puts msg+":"
|
16
|
+
$stderr.puts indent + details.join("\n"+indent)
|
17
|
+
else
|
18
|
+
$stderr.puts msg
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def print_records fields, data, heading=nil
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module HammerCLI::Output::Adapter
|
2
|
+
class Base < Abstract
|
3
|
+
|
4
|
+
HEADING_LINE_WIDTH = 80
|
5
|
+
GROUP_INDENT = " "*2
|
6
|
+
LABEL_DIVIDER = ": "
|
7
|
+
|
8
|
+
def print_records fields, data, heading=nil
|
9
|
+
self.fields = fields
|
10
|
+
|
11
|
+
puts "-"*HEADING_LINE_WIDTH
|
12
|
+
puts " " + heading.to_s
|
13
|
+
puts "-"*HEADING_LINE_WIDTH
|
14
|
+
|
15
|
+
data.each do |d|
|
16
|
+
fields.collect do |f|
|
17
|
+
render_field(f, d)
|
18
|
+
end
|
19
|
+
puts
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def render_Field(field, data, indent="")
|
26
|
+
puts indent.to_s+" "+format_value(field, data).to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_Label(field, data, indent="")
|
30
|
+
render_label(field, indent)
|
31
|
+
puts
|
32
|
+
indent = indent.to_s + GROUP_INDENT
|
33
|
+
|
34
|
+
field.output_definition.fields.collect do |f|
|
35
|
+
render_field(f, data, indent)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def render_LabeledField(field, data, indent="")
|
40
|
+
render_label(field, indent)
|
41
|
+
puts format_value(field, data).to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def render_KeyValue(field, data, indent="")
|
45
|
+
render_label(field, indent)
|
46
|
+
params = field.get_value(data) || []
|
47
|
+
print "%{name} => %{value}" % params
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_Collection(field, data, indent="")
|
51
|
+
render_label(field, indent)
|
52
|
+
puts
|
53
|
+
indent = indent.to_s + " "*(label_width_for_list(self.fields)+LABEL_DIVIDER.size)
|
54
|
+
|
55
|
+
field.get_value(data).each do |d|
|
56
|
+
field.output_definition.fields.collect do |f|
|
57
|
+
render_field(f, d, indent)
|
58
|
+
end
|
59
|
+
puts
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def render_Joint(field, data, indent="")
|
64
|
+
render_label(field, indent)
|
65
|
+
data = field.get_value(data)
|
66
|
+
puts field.attributes.collect{|attr| data[attr] }.join(" ")
|
67
|
+
end
|
68
|
+
|
69
|
+
def format_Date(string_date)
|
70
|
+
t = DateTime.parse(string_date.to_s)
|
71
|
+
t.strftime("%Y/%m/%d %H:%M:%S")
|
72
|
+
rescue ArgumentError
|
73
|
+
""
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_List(list)
|
77
|
+
list.join(", ") if list
|
78
|
+
end
|
79
|
+
|
80
|
+
def format_OSName(os)
|
81
|
+
"%{name} %{major}.%{minor}" % os
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_Server(server)
|
85
|
+
"%{name} (%{url})" % server
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def render_field(field, data, indent="")
|
90
|
+
renderer = find_renderer(field)
|
91
|
+
renderer.call(field, data, indent)
|
92
|
+
end
|
93
|
+
|
94
|
+
def render_label(field, indent="")
|
95
|
+
if field.label
|
96
|
+
w = label_width_for_list(self.fields) - indent.size + 1
|
97
|
+
print indent.to_s+" %#{-w}s" % (field.label.to_s+LABEL_DIVIDER)
|
98
|
+
else
|
99
|
+
print indent.to_s+" "
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def format_value(field, data)
|
104
|
+
format_method = "format_"+field_type(field.class)
|
105
|
+
|
106
|
+
value = field.get_value(data)
|
107
|
+
value = send(format_method, value) if respond_to?(format_method, true)
|
108
|
+
value
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_renderer(field)
|
112
|
+
field.class.ancestors.each do |cls|
|
113
|
+
render_method = "render_"+field_type(cls)
|
114
|
+
return method(render_method) if respond_to?(render_method, true)
|
115
|
+
end
|
116
|
+
raise "No renderer found for field %s" % field.class
|
117
|
+
end
|
118
|
+
|
119
|
+
def field_type(field_class)
|
120
|
+
field_class.name.split("::")[-1]
|
121
|
+
end
|
122
|
+
|
123
|
+
def label_width_for(field)
|
124
|
+
if field.respond_to?(:output_definition)
|
125
|
+
label_width_for_list(field.output_definition.fields)+GROUP_INDENT.size
|
126
|
+
elsif field.respond_to?(:label)
|
127
|
+
field.label.size+LABEL_DIVIDER.size rescue 0
|
128
|
+
else
|
129
|
+
0
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def label_width_for_list(fields)
|
134
|
+
fields.inject(0) do |result, f|
|
135
|
+
width = label_width_for(f)
|
136
|
+
(width > result) ? width : result
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
attr_accessor :fields
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'table_print'
|
2
|
+
|
3
|
+
module HammerCLI::Output::Adapter
|
4
|
+
|
5
|
+
class Table < Base
|
6
|
+
|
7
|
+
def print_records fields, data, heading=nil
|
8
|
+
|
9
|
+
rows = data.collect do |d|
|
10
|
+
row = {}
|
11
|
+
fields.each do |f|
|
12
|
+
row[f.label.to_sym] = f.get_value(d)
|
13
|
+
end
|
14
|
+
row
|
15
|
+
end
|
16
|
+
|
17
|
+
options = fields.collect do |f|
|
18
|
+
{ f.label.to_sym => {
|
19
|
+
:formatters => [ Formatter.new(self, "format_"+field_type(f.class)) ] } }
|
20
|
+
end
|
21
|
+
|
22
|
+
printer = TablePrint::Printer.new(rows, options)
|
23
|
+
output = printer.table_print
|
24
|
+
dashes = /\n(-+)\n/.match output
|
25
|
+
puts dashes[1]
|
26
|
+
puts output
|
27
|
+
puts dashes[1]
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_heading heading, size
|
31
|
+
size = heading.size if heading.size > size
|
32
|
+
puts '-' * size
|
33
|
+
puts ' ' * ((size-heading.size)/2) + heading
|
34
|
+
puts '-' * size
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class Formatter
|
40
|
+
def initialize adapter, method
|
41
|
+
@adapter = adapter
|
42
|
+
@method = method
|
43
|
+
end
|
44
|
+
|
45
|
+
def format value
|
46
|
+
if @adapter.respond_to?(@method, true)
|
47
|
+
value = @adapter.send(@method, value)
|
48
|
+
end
|
49
|
+
value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module HammerCLI::Output
|
2
|
+
|
3
|
+
class Dsl
|
4
|
+
|
5
|
+
def initialize options={}
|
6
|
+
@current_path = options[:path] || []
|
7
|
+
end
|
8
|
+
|
9
|
+
def fields
|
10
|
+
@fields ||= []
|
11
|
+
@fields
|
12
|
+
end
|
13
|
+
|
14
|
+
def build &block
|
15
|
+
self.instance_eval &block
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def field key, label, type=nil, options={}, &block
|
21
|
+
options[:path] = current_path.clone << key
|
22
|
+
options[:label] = label
|
23
|
+
type ||= HammerCLI::Output::DataField
|
24
|
+
custom_field type, options, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def custom_field type, options={}, &block
|
28
|
+
self.fields << type.new(options, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def label label, &block
|
32
|
+
options = {}
|
33
|
+
options[:path] = current_path.clone
|
34
|
+
options[:label] = label
|
35
|
+
custom_field HammerCLI::Output::Fields::Label, options, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
def from key
|
39
|
+
self.current_path.push key
|
40
|
+
yield if block_given?
|
41
|
+
self.current_path.pop
|
42
|
+
end
|
43
|
+
|
44
|
+
def collection key, label, options={}, &block
|
45
|
+
field key, label, HammerCLI::Output::Fields::Collection, options, &block
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def current_path
|
50
|
+
@current_path ||= []
|
51
|
+
@current_path
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|