morpheus-cli 2.10.3 → 2.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|