morpheus-cli 2.10.3 → 2.11.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/bin/morpheus +5 -96
- data/lib/morpheus/api/api_client.rb +23 -1
- data/lib/morpheus/api/checks_interface.rb +106 -0
- data/lib/morpheus/api/incidents_interface.rb +102 -0
- data/lib/morpheus/api/monitoring_apps_interface.rb +47 -0
- data/lib/morpheus/api/monitoring_contacts_interface.rb +47 -0
- data/lib/morpheus/api/monitoring_groups_interface.rb +47 -0
- data/lib/morpheus/api/monitoring_interface.rb +36 -0
- data/lib/morpheus/api/option_type_lists_interface.rb +47 -0
- data/lib/morpheus/api/option_types_interface.rb +47 -0
- data/lib/morpheus/api/roles_interface.rb +0 -1
- data/lib/morpheus/api/setup_interface.rb +19 -9
- data/lib/morpheus/cli.rb +20 -1
- data/lib/morpheus/cli/accounts.rb +8 -3
- data/lib/morpheus/cli/app_templates.rb +19 -11
- data/lib/morpheus/cli/apps.rb +52 -37
- data/lib/morpheus/cli/cli_command.rb +229 -53
- data/lib/morpheus/cli/cli_registry.rb +48 -40
- data/lib/morpheus/cli/clouds.rb +55 -26
- data/lib/morpheus/cli/command_error.rb +12 -0
- data/lib/morpheus/cli/credentials.rb +68 -26
- data/lib/morpheus/cli/curl_command.rb +98 -0
- data/lib/morpheus/cli/dashboard_command.rb +2 -7
- data/lib/morpheus/cli/deployments.rb +4 -4
- data/lib/morpheus/cli/deploys.rb +1 -2
- data/lib/morpheus/cli/dot_file.rb +5 -8
- data/lib/morpheus/cli/error_handler.rb +179 -15
- data/lib/morpheus/cli/groups.rb +21 -13
- data/lib/morpheus/cli/hosts.rb +220 -110
- data/lib/morpheus/cli/instance_types.rb +2 -2
- data/lib/morpheus/cli/instances.rb +257 -167
- data/lib/morpheus/cli/key_pairs.rb +15 -9
- data/lib/morpheus/cli/library.rb +673 -27
- data/lib/morpheus/cli/license.rb +2 -2
- data/lib/morpheus/cli/load_balancers.rb +4 -4
- data/lib/morpheus/cli/log_level_command.rb +6 -4
- data/lib/morpheus/cli/login.rb +17 -3
- data/lib/morpheus/cli/logout.rb +25 -11
- data/lib/morpheus/cli/man_command.rb +388 -0
- data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -1
- data/lib/morpheus/cli/mixins/monitoring_helper.rb +434 -0
- data/lib/morpheus/cli/mixins/print_helper.rb +620 -112
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +1 -1
- data/lib/morpheus/cli/monitoring_apps_command.rb +29 -0
- data/lib/morpheus/cli/monitoring_checks_command.rb +427 -0
- data/lib/morpheus/cli/monitoring_contacts_command.rb +373 -0
- data/lib/morpheus/cli/monitoring_groups_command.rb +29 -0
- data/lib/morpheus/cli/monitoring_incidents_command.rb +711 -0
- data/lib/morpheus/cli/option_types.rb +10 -1
- data/lib/morpheus/cli/recent_activity_command.rb +2 -5
- data/lib/morpheus/cli/remote.rb +874 -134
- data/lib/morpheus/cli/roles.rb +54 -27
- data/lib/morpheus/cli/security_group_rules.rb +2 -2
- data/lib/morpheus/cli/security_groups.rb +23 -19
- data/lib/morpheus/cli/set_prompt_command.rb +50 -0
- data/lib/morpheus/cli/shell.rb +222 -157
- data/lib/morpheus/cli/tasks.rb +19 -15
- data/lib/morpheus/cli/users.rb +27 -17
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/virtual_images.rb +28 -13
- data/lib/morpheus/cli/whoami.rb +131 -52
- data/lib/morpheus/cli/workflows.rb +24 -9
- data/lib/morpheus/formatters.rb +195 -3
- data/lib/morpheus/logging.rb +86 -0
- data/lib/morpheus/terminal.rb +371 -0
- data/scripts/generate_morpheus_commands_help.morpheus +60 -0
- metadata +21 -2
@@ -49,9 +49,14 @@ class Morpheus::Cli::Workflows
|
|
49
49
|
print JSON.pretty_generate(json_response)
|
50
50
|
else
|
51
51
|
task_sets = json_response['taskSets']
|
52
|
-
|
52
|
+
title = "Morpheus Workflows"
|
53
|
+
subtitles = []
|
54
|
+
if params[:phrase]
|
55
|
+
subtitles << "Search: #{params[:phrase]}".strip
|
56
|
+
end
|
57
|
+
print_h1 title, subtitles
|
53
58
|
if task_sets.empty?
|
54
|
-
|
59
|
+
print cyan,"No workflows found.",reset,"\n"
|
55
60
|
else
|
56
61
|
print cyan
|
57
62
|
print_workflows_table(task_sets)
|
@@ -142,15 +147,25 @@ class Morpheus::Cli::Workflows
|
|
142
147
|
# tasks << find_task_by_name_or_id(task_name)['id']
|
143
148
|
# end
|
144
149
|
tasks = workflow['taskSetTasks'].sort { |x,y| x['taskOrder'].to_i <=> y['taskOrder'].to_i }
|
145
|
-
|
150
|
+
print_h1 "Workflow Details"
|
151
|
+
|
146
152
|
print cyan
|
147
|
-
|
148
|
-
|
149
|
-
|
153
|
+
description_cols = {
|
154
|
+
"ID" => 'id',
|
155
|
+
"Name" => 'name',
|
156
|
+
#"Description" => 'description',
|
157
|
+
}
|
158
|
+
print_description_list(description_cols, workflow)
|
159
|
+
|
150
160
|
#task_names = tasks.collect {|it| it['name'] }
|
151
|
-
|
152
|
-
tasks.
|
153
|
-
|
161
|
+
print_h2 "Tasks"
|
162
|
+
if tasks.empty?
|
163
|
+
print yellow,"No tasks in this workflow.",reset,"\n"
|
164
|
+
else
|
165
|
+
print cyan
|
166
|
+
tasks.each_with_index do |taskSetTask, index|
|
167
|
+
puts "#{(index+1).to_s.rjust(3, ' ')}. #{taskSetTask['task']['name']}"
|
168
|
+
end
|
154
169
|
end
|
155
170
|
print reset,"\n"
|
156
171
|
end
|
data/lib/morpheus/formatters.rb
CHANGED
@@ -1,13 +1,37 @@
|
|
1
1
|
require 'time'
|
2
2
|
|
3
|
+
DEFAULT_TIME_FORMAT = "%x %I:%M %p %Z"
|
4
|
+
|
3
5
|
# returns an instance of Time
|
4
|
-
def parse_time(dt)
|
6
|
+
def parse_time(dt, format=nil)
|
5
7
|
if dt.nil? || dt == '' || dt.to_i == 0
|
6
8
|
return nil
|
7
9
|
elsif dt.is_a?(Time)
|
8
10
|
return dt
|
9
11
|
elsif dt.is_a?(String)
|
10
|
-
|
12
|
+
result = nil
|
13
|
+
err = nil
|
14
|
+
begin
|
15
|
+
result = Time.parse(dt)
|
16
|
+
rescue => e
|
17
|
+
err = e
|
18
|
+
end
|
19
|
+
if !result
|
20
|
+
format ||= DEFAULT_TIME_FORMAT
|
21
|
+
if format
|
22
|
+
begin
|
23
|
+
result = Time.strptime(dt, format)
|
24
|
+
rescue => e
|
25
|
+
err = e
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
if result
|
30
|
+
return result
|
31
|
+
else
|
32
|
+
raise "unable to parse time '#{dt}'. #{err}"
|
33
|
+
end
|
34
|
+
|
11
35
|
elsif dt.is_a?(Numeric)
|
12
36
|
return Time.at(dt)
|
13
37
|
else
|
@@ -21,7 +45,7 @@ def format_dt(dt, options={})
|
|
21
45
|
if options[:local]
|
22
46
|
dt = dt.getlocal
|
23
47
|
end
|
24
|
-
format = options[:format] ||
|
48
|
+
format = options[:format] || DEFAULT_TIME_FORMAT
|
25
49
|
return dt.strftime(format)
|
26
50
|
end
|
27
51
|
|
@@ -42,6 +66,77 @@ def format_dt_as_param(dt)
|
|
42
66
|
format_dt(dt, {format: "%Y-%m-%d %X"})
|
43
67
|
end
|
44
68
|
|
69
|
+
def format_duration(start_time, end_time=nil, format="human")
|
70
|
+
if !start_time
|
71
|
+
return ""
|
72
|
+
end
|
73
|
+
start_time = parse_time(start_time)
|
74
|
+
if end_time
|
75
|
+
end_time = parse_time(end_time)
|
76
|
+
else
|
77
|
+
end_time = Time.now
|
78
|
+
end
|
79
|
+
seconds = (end_time - start_time).abs
|
80
|
+
format_duration_seconds(seconds, format)
|
81
|
+
end
|
82
|
+
|
83
|
+
def format_duration_seconds(seconds, format="human")
|
84
|
+
seconds = seconds.abs
|
85
|
+
out = ""
|
86
|
+
# interval = Math.abs(interval)
|
87
|
+
if format == "human"
|
88
|
+
out = format_human_duration(seconds)
|
89
|
+
elsif format
|
90
|
+
interval_time = Time.mktime(0) + seconds
|
91
|
+
out = interval_time.strftime(format)
|
92
|
+
else
|
93
|
+
interval_time = Time.mktime(0) + seconds
|
94
|
+
out = interval_time.strftime("%H:%M:%S")
|
95
|
+
end
|
96
|
+
out
|
97
|
+
end
|
98
|
+
|
99
|
+
# returns a human readable time duration
|
100
|
+
# @param seconds - duration in seconds
|
101
|
+
def format_human_duration(seconds)
|
102
|
+
out = ""
|
103
|
+
#seconds = seconds.round
|
104
|
+
days, hours, minutes = (seconds / (60*60*24)).floor, (seconds / (60*60)).floor, (seconds / (60)).floor
|
105
|
+
if days > 365
|
106
|
+
out << "#{days.floor} days (more than a year!!)"
|
107
|
+
elsif days > 61
|
108
|
+
out << "#{days.floor} days (months!)"
|
109
|
+
elsif days > 31
|
110
|
+
out << "#{days.floor} days (over a month)"
|
111
|
+
elsif days > 0
|
112
|
+
if days.floor == 1
|
113
|
+
out << "1 day"
|
114
|
+
else
|
115
|
+
out << "#{days.floor} days"
|
116
|
+
end
|
117
|
+
elsif hours > 1
|
118
|
+
if hours == 1
|
119
|
+
out << "1 hour"
|
120
|
+
else
|
121
|
+
out << "#{hours.floor} hours"
|
122
|
+
end
|
123
|
+
elsif minutes > 1
|
124
|
+
if minutes == 1
|
125
|
+
out << "1 minute"
|
126
|
+
else
|
127
|
+
out << "#{minutes.floor} minutes"
|
128
|
+
end
|
129
|
+
else
|
130
|
+
seconds = seconds.floor
|
131
|
+
if seconds == 1
|
132
|
+
out << "1 second"
|
133
|
+
else
|
134
|
+
out << "#{seconds} seconds"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
out
|
138
|
+
end
|
139
|
+
|
45
140
|
def display_appliance(name, url)
|
46
141
|
"#{name} - #{url}"
|
47
142
|
end
|
@@ -49,3 +144,100 @@ end
|
|
49
144
|
def iso8601(dt)
|
50
145
|
dt.instance_of(Time) ? dt.iso8601 : "#{dt}"
|
51
146
|
end
|
147
|
+
|
148
|
+
# get_object_value returns a value within a Hash like object
|
149
|
+
# Usage: get_object_value(host, "plan.name")
|
150
|
+
def get_object_value(data, key)
|
151
|
+
value = nil
|
152
|
+
if key.is_a?(Proc)
|
153
|
+
return key.call(data)
|
154
|
+
end
|
155
|
+
key = key.to_s
|
156
|
+
if key.include?(".")
|
157
|
+
namespaces = key.split(".")
|
158
|
+
value = data
|
159
|
+
namespaces.each do |ns|
|
160
|
+
if value.respond_to?("key?")
|
161
|
+
if value.key?(ns.to_s)
|
162
|
+
value = value[ns]
|
163
|
+
elsif value.key?(ns.to_sym)
|
164
|
+
value = value[ns.to_sym]
|
165
|
+
else
|
166
|
+
value = nil
|
167
|
+
end
|
168
|
+
else
|
169
|
+
value = nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
else
|
173
|
+
value = data[key] || data[key.to_sym]
|
174
|
+
end
|
175
|
+
return value
|
176
|
+
end
|
177
|
+
|
178
|
+
# filter_data filters Hash-like data to only the specified fields
|
179
|
+
# To specify fields of child objects, use a "."
|
180
|
+
# Usage: filter_data(instance, ["id", "name", "plan.name"])
|
181
|
+
def filter_data(data, include_fields=nil, exclude_fields=nil)
|
182
|
+
if !data
|
183
|
+
return data
|
184
|
+
elsif data.is_a?(Array)
|
185
|
+
new_data = data.collect { |it| filter_data(it, include_fields, exclude_fields) }
|
186
|
+
return new_data
|
187
|
+
elsif data.is_a?(Hash)
|
188
|
+
if include_fields
|
189
|
+
#new_data = data.select {|k, v| include_fields.include?(k.to_s) || include_fields.include?(k.to_sym) }
|
190
|
+
# allow extracting dot pathed fields, just like get_object_value
|
191
|
+
my_data = {}
|
192
|
+
include_fields.each do |field|
|
193
|
+
if field.nil?
|
194
|
+
next
|
195
|
+
end
|
196
|
+
field = field.to_s
|
197
|
+
if field.empty?
|
198
|
+
next
|
199
|
+
end
|
200
|
+
if field.include?(".")
|
201
|
+
# could do this instead...
|
202
|
+
# namespaces = field.split(".")
|
203
|
+
# cur_data = data
|
204
|
+
# namespaces.each
|
205
|
+
# if index != namespaces.length - 1
|
206
|
+
# cur_data[ns] ||= {}
|
207
|
+
# else
|
208
|
+
# cur_data[ns] = get_object_value(new_data, field)
|
209
|
+
# end
|
210
|
+
# end
|
211
|
+
namespaces = field.split(".")
|
212
|
+
cur_data = data
|
213
|
+
cur_filtered_data = my_data
|
214
|
+
namespaces.each_with_index do |ns, index|
|
215
|
+
if index != namespaces.length - 1
|
216
|
+
if cur_data
|
217
|
+
cur_data = cur_data[ns] || cur_data[ns.to_sym]
|
218
|
+
else
|
219
|
+
cur_data = nil
|
220
|
+
end
|
221
|
+
cur_filtered_data[ns] ||= {}
|
222
|
+
cur_filtered_data = cur_filtered_data[ns]
|
223
|
+
else
|
224
|
+
if cur_data.respond_to?("[]")
|
225
|
+
cur_filtered_data[ns] = cur_data[ns] || cur_data[ns.to_sym]
|
226
|
+
else
|
227
|
+
cur_filtered_data[ns] = nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
else
|
232
|
+
my_data[field] = data[field] || data[field.to_sym]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
return my_data
|
236
|
+
elsif exclude_fields
|
237
|
+
new_data = data.reject {|k, v| exclude_fields.include?(k.to_s) || exclude_fields.include?(k.to_sym) }
|
238
|
+
return new_data
|
239
|
+
end
|
240
|
+
else
|
241
|
+
return data # .clone
|
242
|
+
end
|
243
|
+
end
|
data/lib/morpheus/logging.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'logger'
|
2
|
+
require 'term/ansicolor'
|
2
3
|
|
3
4
|
# Provides global Logging behavior
|
4
5
|
# By default, Morpheus::Logging.logger is set to STDOUT with level INFO
|
@@ -75,4 +76,89 @@ module Morpheus::Logging
|
|
75
76
|
self.debug?
|
76
77
|
end
|
77
78
|
|
79
|
+
# An IO class for printing debugging info
|
80
|
+
# This is used as a proxy for ::RestClient.log printing right now.
|
81
|
+
class DarkPrinter
|
82
|
+
include Term::ANSIColor
|
83
|
+
|
84
|
+
# [IO] to write to
|
85
|
+
attr_accessor :io
|
86
|
+
|
87
|
+
# [String] ansi color code for output. Default is dark
|
88
|
+
attr_accessor :color
|
89
|
+
|
90
|
+
# DarkPrinter with io STDOUT
|
91
|
+
def self.instance
|
92
|
+
@instance ||= self.new(STDOUT, nil, true)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.print(*messages)
|
96
|
+
instance.print(*messages)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.puts(*messages)
|
100
|
+
instance.puts(*messages)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.<<(*messages)
|
104
|
+
instance.<<(*messages)
|
105
|
+
end
|
106
|
+
|
107
|
+
def initialize(io, color=nil, is_dark=true)
|
108
|
+
@io = io # || $stdout
|
109
|
+
@color = color # || cyan
|
110
|
+
@is_dark = is_dark
|
111
|
+
end
|
112
|
+
|
113
|
+
# mask well known secrets
|
114
|
+
def scrub_message(msg)
|
115
|
+
if msg.is_a?(String)
|
116
|
+
msg.gsub!(/Authorization\"\s?\=\>\s?\"Bearer [^"]+/, 'Authorization"=>"Bearer ************')
|
117
|
+
msg.gsub!(/Authorization\:\s?Bearer [^"]+/, 'Authorization"=>"Bearer ************')
|
118
|
+
msg.gsub!(/password\"\s?\=\>\s?\"[^"]+/, 'password"=>"************')
|
119
|
+
msg.gsub!(/password\=\"[^"]+/, 'password="************')
|
120
|
+
end
|
121
|
+
msg
|
122
|
+
end
|
123
|
+
|
124
|
+
def print_with_color(&block)
|
125
|
+
if Term::ANSIColor.coloring?
|
126
|
+
@io.print Term::ANSIColor.reset
|
127
|
+
@io.print @color if @color
|
128
|
+
@io.print Term::ANSIColor.dark if @is_dark
|
129
|
+
end
|
130
|
+
yield
|
131
|
+
if Term::ANSIColor.coloring?
|
132
|
+
@io.print Term::ANSIColor.reset
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def print(*messages)
|
137
|
+
if @io
|
138
|
+
messages = messages.flatten.collect {|it| scrub_message(it) }
|
139
|
+
print_with_color do
|
140
|
+
messages.each do |msg|
|
141
|
+
@io.print msg
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def puts(*messages)
|
148
|
+
if @io
|
149
|
+
messages = messages.flatten.collect {|it| scrub_message(it) }
|
150
|
+
print_with_color do
|
151
|
+
messages.each do |msg|
|
152
|
+
@io.puts msg
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def <<(*messages)
|
159
|
+
print(*messages)
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
78
164
|
end
|
@@ -0,0 +1,371 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'term/ansicolor'
|
3
|
+
require 'optparse'
|
4
|
+
require 'morpheus/cli'
|
5
|
+
require 'morpheus/rest_client'
|
6
|
+
require 'morpheus/cli/cli_registry'
|
7
|
+
require 'morpheus/cli/dot_file'
|
8
|
+
require 'morpheus/cli/error_handler'
|
9
|
+
require 'morpheus/logging'
|
10
|
+
require 'morpheus/cli'
|
11
|
+
|
12
|
+
module Morpheus
|
13
|
+
|
14
|
+
# Terminal is a class for executing morpheus commands
|
15
|
+
# The default IO is STDIN, STDOUT, STDERR
|
16
|
+
# The default home directory is $HOME/.morpheus
|
17
|
+
#
|
18
|
+
# ==== Example Usage
|
19
|
+
#
|
20
|
+
# morph = Morpheus::Terminal.new
|
21
|
+
# exit_code, err = morph.execute("instances list -m 10")
|
22
|
+
# assert exit_code == 0
|
23
|
+
# assert err == nil
|
24
|
+
#
|
25
|
+
# morph = Morpheus::Terminal.new(STDIN, File.new("/tmp/morph.log", "w+"))
|
26
|
+
# morph.execute("hosts get 23")
|
27
|
+
#
|
28
|
+
# morph = Morpheus::Terminal.new(STDIN, File.new("/tmp/host23.json", "w"))
|
29
|
+
# morph.execute("hosts get 23 --json")
|
30
|
+
# puts File.read("/tmp/host23.json")
|
31
|
+
#
|
32
|
+
class Terminal
|
33
|
+
|
34
|
+
# todo: this can be combined with Cli::Shell
|
35
|
+
|
36
|
+
class Blackhole # < IO
|
37
|
+
def accrete_data(*mgs)
|
38
|
+
# Singularity.push(*msgs)
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
alias :print :accrete_data
|
42
|
+
alias :puts :accrete_data
|
43
|
+
alias :'<<' :accrete_data
|
44
|
+
alias :write :accrete_data
|
45
|
+
alias :write :accrete_data
|
46
|
+
# alias :gets :do_nothing
|
47
|
+
end
|
48
|
+
|
49
|
+
# DEFAULT_TERMINAL_WIDTH = 80
|
50
|
+
|
51
|
+
def self.default_color
|
52
|
+
Term::ANSIColor.cyan
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.prompt
|
56
|
+
if @prompt.nil?
|
57
|
+
if ENV['MORPHEUS_PS1']
|
58
|
+
@prompt = ENV['MORPHEUS_PS1'].dup
|
59
|
+
else
|
60
|
+
#ENV['MORPHEUS_PS1'] = "#{Term::ANSIColor.cyan}morpheus> #{Term::ANSIColor.reset}"
|
61
|
+
@prompt = "#{Term::ANSIColor.cyan}morpheus>#{Term::ANSIColor.reset} "
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@prompt
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.prompt=(v)
|
68
|
+
@prompt = v
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.angry_prompt
|
72
|
+
"#{Term::ANSIColor.red}morpheus: #{Term::ANSIColor.reset}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.custom_prompt
|
76
|
+
#export MORPHEUS_PS1='\[\e[1;32m\]\u@\h:\w${text}$\[\e[m\] '
|
77
|
+
end
|
78
|
+
|
79
|
+
# the global Morpheus::Terminal instance
|
80
|
+
# This should go away, but it needed for now...
|
81
|
+
def self.instance
|
82
|
+
@morphterm
|
83
|
+
end
|
84
|
+
|
85
|
+
# hack alert! This should go away, but it needed for now...
|
86
|
+
def self.new(*args)
|
87
|
+
obj = super(*args)
|
88
|
+
@morphterm = obj
|
89
|
+
obj
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_accessor :prompt #, :angry_prompt
|
93
|
+
attr_reader :stdin, :stdout, :stderr, :homedir
|
94
|
+
|
95
|
+
|
96
|
+
# @param stdin [IO] Default is STDIN
|
97
|
+
# @param stdout [IO] Default is STDOUT
|
98
|
+
# @param stderr [IO] Default is STDERR
|
99
|
+
# @param [IO] stderr
|
100
|
+
# @stderr = stderr
|
101
|
+
# @homedir = homedir
|
102
|
+
# instead, setup global stuff for now...
|
103
|
+
def initialize(stdin=STDIN,stdout=STDOUT, stderr=STDERR, homedir=nil)
|
104
|
+
# todo: establish config context for executing commands,
|
105
|
+
# so you can run them in parallel within the same process
|
106
|
+
# that means not using globals and class instance vars
|
107
|
+
# just return an exit code / err, instead of raising SystemExit
|
108
|
+
|
109
|
+
|
110
|
+
# establish IO
|
111
|
+
# @stdin, @stdout, @stderr = stdin, stdout, stderr
|
112
|
+
set_stdin(stdin)
|
113
|
+
set_stdout(stdout)
|
114
|
+
set_stderr(stderr)
|
115
|
+
|
116
|
+
# establish home directory
|
117
|
+
@homedir = homedir || ENV['MORPHEUS_CLI_HOME'] || File.join(Dir.home, ".morpheus")
|
118
|
+
Morpheus::Cli.home_directory = @homedir
|
119
|
+
|
120
|
+
# use colors by default
|
121
|
+
set_coloring(STDOUT.isatty)
|
122
|
+
# Term::ANSIColor::coloring = STDOUT.isatty
|
123
|
+
# @coloring = Term::ANSIColor::coloring?
|
124
|
+
|
125
|
+
# startup script
|
126
|
+
if File.exists? Morpheus::Cli::DotFile.morpheus_profile_filename
|
127
|
+
@profile_dot_file = Morpheus::Cli::DotFile.new(Morpheus::Cli::DotFile.morpheus_profile_filename)
|
128
|
+
else
|
129
|
+
@profile_dot_file = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# the string to prompt for input with
|
133
|
+
@prompt ||= Morpheus::Terminal.prompt
|
134
|
+
@angry_prompt ||= Morpheus::Terminal.angry_prompt
|
135
|
+
end
|
136
|
+
|
137
|
+
# execute .morpheus_profile startup script
|
138
|
+
def execute_profile_script(rerun=false)
|
139
|
+
if @profile_dot_file
|
140
|
+
if rerun || !@profile_dot_file_has_run
|
141
|
+
@profile_dot_file_has_run = true
|
142
|
+
return @profile_dot_file.execute() # todo: pass io in here
|
143
|
+
else
|
144
|
+
return false # already run
|
145
|
+
end
|
146
|
+
else
|
147
|
+
return nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_stdin(io)
|
152
|
+
# if io.nil? || io == 'blackhole' || io == '/dev/null'
|
153
|
+
# @stdout = Morpheus::Terminal::Blackhole.new
|
154
|
+
# else
|
155
|
+
# @stdout = io
|
156
|
+
# end
|
157
|
+
@stdin = io
|
158
|
+
end
|
159
|
+
|
160
|
+
def set_stdout(io)
|
161
|
+
if io.nil? || io == 'blackhole' || io == '/dev/null'
|
162
|
+
@stdout = Morpheus::Terminal::Blackhole.new
|
163
|
+
else
|
164
|
+
@stdout = io
|
165
|
+
end
|
166
|
+
@stdout
|
167
|
+
end
|
168
|
+
|
169
|
+
def set_stderr(io)
|
170
|
+
if io.nil? || io == 'blackhole' || io == '/dev/null'
|
171
|
+
@stderr = Morpheus::Terminal::Blackhole.new
|
172
|
+
else
|
173
|
+
@stderr = io
|
174
|
+
end
|
175
|
+
@stderr
|
176
|
+
end
|
177
|
+
|
178
|
+
# def coloring=(v)
|
179
|
+
# set_coloring(enabled)
|
180
|
+
# end
|
181
|
+
|
182
|
+
def set_coloring(enabled)
|
183
|
+
@coloring = !!enabled
|
184
|
+
Term::ANSIColor::coloring = @coloring
|
185
|
+
coloring?
|
186
|
+
end
|
187
|
+
|
188
|
+
def coloring?
|
189
|
+
@coloring == true
|
190
|
+
end
|
191
|
+
|
192
|
+
# alias :'coloring=' :set_coloring
|
193
|
+
|
194
|
+
def usage
|
195
|
+
out = "Usage: morpheus [command] [options]\n"
|
196
|
+
# just for printing help. todo: start using this. maybe in class Cli::MainCommand
|
197
|
+
# maybe OptionParser's recover() instance method will do the trick
|
198
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
199
|
+
opts.banner = "Options:" # hack alert
|
200
|
+
opts.on('-v','--version', "Print the version.") do
|
201
|
+
@stdout.puts Morpheus::Cli::VERSION
|
202
|
+
# exit
|
203
|
+
end
|
204
|
+
opts.on('--noprofile','--noprofile', "Do not read and execute the personal initialization script .morpheus_profile") do
|
205
|
+
@noprofile = true
|
206
|
+
end
|
207
|
+
opts.on('-C','--nocolor', "Disable ANSI coloring") do
|
208
|
+
@coloring = false
|
209
|
+
Term::ANSIColor::coloring = false
|
210
|
+
end
|
211
|
+
opts.on('-V','--debug', "Print extra output for debugging. ") do |json|
|
212
|
+
@terminal_log_level = Morpheus::Logging::Logger::DEBUG
|
213
|
+
Morpheus::Logging.set_log_level(Morpheus::Logging::Logger::DEBUG)
|
214
|
+
::RestClient.log = Morpheus::Logging.debug? ? Morpheus::Logging::DarkPrinter.instance : nil
|
215
|
+
end
|
216
|
+
opts.on( '-h', '--help', "Prints this help" ) do
|
217
|
+
@stdout.puts opts
|
218
|
+
# exit
|
219
|
+
end
|
220
|
+
end
|
221
|
+
out << "Commands:\n"
|
222
|
+
Morpheus::Cli::CliRegistry.all.keys.sort.each {|cmd|
|
223
|
+
out << "\t#{cmd.to_s}\n"
|
224
|
+
}
|
225
|
+
# out << "Options:\n"
|
226
|
+
out << optparse.to_s
|
227
|
+
out << "\n"
|
228
|
+
out << "For more information, see https://github.com/gomorpheus/morpheus-cli/wiki"
|
229
|
+
out << "\n"
|
230
|
+
out
|
231
|
+
end
|
232
|
+
|
233
|
+
def prompt
|
234
|
+
@prompt # ||= Morpheus::Terminal.default_prompt
|
235
|
+
end
|
236
|
+
|
237
|
+
def prompt=(str)
|
238
|
+
@prompt = str
|
239
|
+
end
|
240
|
+
|
241
|
+
def angry_prompt
|
242
|
+
@angry_prompt ||= Morpheus::Terminal.angry_prompt
|
243
|
+
end
|
244
|
+
|
245
|
+
# def gets
|
246
|
+
# Readline.completion_append_character = " "
|
247
|
+
# Readline.completion_proc = @auto_complete
|
248
|
+
# Readline.basic_word_break_characters = ""
|
249
|
+
# #Readline.basic_word_break_characters = "\t\n\"\‘`@$><=;|&{( "
|
250
|
+
# input = Readline.readline("#{@prompt}", true).to_s
|
251
|
+
# input = input.strip
|
252
|
+
# execute(input)
|
253
|
+
# end
|
254
|
+
|
255
|
+
# def puts(*cmds)
|
256
|
+
# cmds.each do |cmd|
|
257
|
+
# self.execute(cmd) # exec
|
258
|
+
# end
|
259
|
+
# end
|
260
|
+
|
261
|
+
# def gets(*args)
|
262
|
+
# $stdin.gets(*args)
|
263
|
+
# end
|
264
|
+
|
265
|
+
# protected
|
266
|
+
|
267
|
+
def execute(input)
|
268
|
+
exit_code = 0
|
269
|
+
err = nil
|
270
|
+
args = nil
|
271
|
+
if input.is_a? String
|
272
|
+
args = Shellwords.shellsplit(input)
|
273
|
+
elsif input.is_a?(Array)
|
274
|
+
args = input.dup
|
275
|
+
else
|
276
|
+
raise "terminal execute() expects a String to be split or an Array of String arguments and instead got (#{args.class}) #{args}"
|
277
|
+
end
|
278
|
+
|
279
|
+
# include Term::ANSIColor # tempting
|
280
|
+
|
281
|
+
# short circuit version switch
|
282
|
+
if args.length == 1
|
283
|
+
if args[0] == '-v' || args[0] == '--version'
|
284
|
+
@stdout.puts Morpheus::Cli::VERSION
|
285
|
+
return 0, nil
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# looking for global help?
|
290
|
+
if args.length == 1
|
291
|
+
if args[0] == '-h' || args[0] == '--help' || args[0] == 'help'
|
292
|
+
@stdout.puts usage
|
293
|
+
return 0, nil
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
# process global options
|
299
|
+
|
300
|
+
# raise log level right away
|
301
|
+
if args.find {|it| it == '-V' || it == '--debug'}
|
302
|
+
@terminal_log_level = Morpheus::Logging::Logger::DEBUG
|
303
|
+
Morpheus::Logging.set_log_level(Morpheus::Logging::Logger::DEBUG)
|
304
|
+
::RestClient.log = Morpheus::Logging.debug? ? Morpheus::Logging::DarkPrinter.instance : nil
|
305
|
+
end
|
306
|
+
|
307
|
+
# execute startup script .morpheus_profile unless --noprofile is passed
|
308
|
+
# todo: this should happen in initialize..
|
309
|
+
noprofile = false
|
310
|
+
if args.find {|it| it == '--noprofile' }
|
311
|
+
noprofile = true
|
312
|
+
args.delete_if {|it| it == '--noprofile' }
|
313
|
+
end
|
314
|
+
|
315
|
+
if @profile_dot_file && !@profile_dot_file_has_run
|
316
|
+
if !noprofile && File.exists?(@profile_dot_file.filename)
|
317
|
+
execute_profile_script()
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# not enough arguments?
|
322
|
+
if args.count == 0
|
323
|
+
@stderr.puts "#{@angry_prompt}[command] argument is required."
|
324
|
+
#@stderr.puts "No command given, here's some help:"
|
325
|
+
@stderr.print usage
|
326
|
+
return 2, nil # CommandError.new("morpheus requires a command")
|
327
|
+
end
|
328
|
+
|
329
|
+
cmd_name = args[0]
|
330
|
+
cmd_args = args[1..-1]
|
331
|
+
|
332
|
+
# unknown command?
|
333
|
+
# all commands should be registered commands or aliases
|
334
|
+
if !(Morpheus::Cli::CliRegistry.has_command?(cmd_name) || Morpheus::Cli::CliRegistry.has_alias?(cmd_name))
|
335
|
+
@stderr.puts "#{@angry_prompt}'#{cmd_name}' is not a morpheus command. See 'morpheus --help'."
|
336
|
+
#@stderr.puts usage
|
337
|
+
return 127, nil
|
338
|
+
end
|
339
|
+
|
340
|
+
# ok, execute the command (or alias)
|
341
|
+
result = nil
|
342
|
+
begin
|
343
|
+
# shell is a Singleton command class
|
344
|
+
if args[0] == "shell"
|
345
|
+
result = Morpheus::Cli::Shell.instance.handle(args[1..-1])
|
346
|
+
else
|
347
|
+
result = Morpheus::Cli::CliRegistry.exec(args[0], args[1..-1])
|
348
|
+
end
|
349
|
+
# todo: clean up CliCommand return values, handle a few diff types for now
|
350
|
+
if result == nil || result == true || result == 0
|
351
|
+
exit_code = 0
|
352
|
+
elsif result == false
|
353
|
+
exit_code = 1
|
354
|
+
elsif result.is_a?(Array) # exit_code, err
|
355
|
+
exit_code = result[0] #.to_i
|
356
|
+
err = result[1]
|
357
|
+
else
|
358
|
+
exit_code = result #.to_i
|
359
|
+
end
|
360
|
+
rescue => e
|
361
|
+
exit_code = Morpheus::Cli::ErrorHandler.new(@stderr).handle_error(e)
|
362
|
+
err = e
|
363
|
+
end
|
364
|
+
|
365
|
+
return exit_code, err
|
366
|
+
|
367
|
+
end
|
368
|
+
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|