morpheus-cli 3.5.2 → 3.5.3

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/morpheus/api/api_client.rb +16 -0
  3. data/lib/morpheus/api/blueprints_interface.rb +84 -0
  4. data/lib/morpheus/api/execution_request_interface.rb +33 -0
  5. data/lib/morpheus/api/instances_interface.rb +21 -0
  6. data/lib/morpheus/api/packages_interface.rb +25 -5
  7. data/lib/morpheus/api/processes_interface.rb +34 -0
  8. data/lib/morpheus/api/roles_interface.rb +7 -0
  9. data/lib/morpheus/api/servers_interface.rb +8 -0
  10. data/lib/morpheus/api/user_settings_interface.rb +76 -0
  11. data/lib/morpheus/cli.rb +5 -1
  12. data/lib/morpheus/cli/alias_command.rb +1 -1
  13. data/lib/morpheus/cli/app_templates.rb +2 -1
  14. data/lib/morpheus/cli/apps.rb +173 -19
  15. data/lib/morpheus/cli/blueprints_command.rb +2134 -0
  16. data/lib/morpheus/cli/cli_command.rb +3 -1
  17. data/lib/morpheus/cli/clouds.rb +4 -10
  18. data/lib/morpheus/cli/coloring_command.rb +14 -8
  19. data/lib/morpheus/cli/containers_command.rb +92 -5
  20. data/lib/morpheus/cli/execution_request_command.rb +313 -0
  21. data/lib/morpheus/cli/hosts.rb +188 -7
  22. data/lib/morpheus/cli/instances.rb +472 -9
  23. data/lib/morpheus/cli/login.rb +1 -1
  24. data/lib/morpheus/cli/mixins/print_helper.rb +8 -0
  25. data/lib/morpheus/cli/mixins/processes_helper.rb +134 -0
  26. data/lib/morpheus/cli/option_types.rb +21 -16
  27. data/lib/morpheus/cli/packages_command.rb +469 -17
  28. data/lib/morpheus/cli/processes_command.rb +313 -0
  29. data/lib/morpheus/cli/remote.rb +20 -9
  30. data/lib/morpheus/cli/roles.rb +186 -6
  31. data/lib/morpheus/cli/shell.rb +10 -1
  32. data/lib/morpheus/cli/tasks.rb +4 -1
  33. data/lib/morpheus/cli/user_settings_command.rb +431 -0
  34. data/lib/morpheus/cli/version.rb +1 -1
  35. data/lib/morpheus/cli/whoami.rb +1 -1
  36. data/lib/morpheus/formatters.rb +14 -0
  37. data/lib/morpheus/morpkg.rb +119 -0
  38. data/morpheus-cli.gemspec +1 -0
  39. metadata +26 -2
@@ -87,7 +87,7 @@ class Morpheus::Cli::Login
87
87
 
88
88
  if creds
89
89
  if !options[:quiet]
90
- print green,"Logged in to #{@appliance_name} as #{::Morpheus::Cli::Remote.load_active_remote()[:username]}#{reset}", reset, "\n"
90
+ print green,"Logged in to #{@appliance_name} as #{::Morpheus::Cli::Remote.load_remote(@appliance_name)[:username]}#{reset}", reset, "\n"
91
91
  end
92
92
  return 0 # , nil
93
93
  else
@@ -807,4 +807,12 @@ module Morpheus::Cli::PrintHelper
807
807
  end
808
808
  end
809
809
 
810
+ def sleep_with_dots(sleep_seconds, dots=3, dot_chr=".")
811
+ dot_interval = (sleep_seconds.to_f / dots.to_i)
812
+ dots.to_i.times do |dot_index|
813
+ sleep dot_interval
814
+ print dot_chr
815
+ end
816
+ end
817
+
810
818
  end
@@ -0,0 +1,134 @@
1
+ require 'table_print'
2
+ require 'morpheus/cli/mixins/print_helper'
3
+
4
+ # Mixin for Morpheus::Cli command classes
5
+ # Provides common methods for fetching and printing accounts, roles, and users.
6
+ # The including class must establish @accounts_interface, @roles_interface, @users_interface
7
+ module Morpheus::Cli::ProcessesHelper
8
+
9
+ def self.included(klass)
10
+ klass.send :include, Morpheus::Cli::PrintHelper
11
+ end
12
+
13
+
14
+ def print_process_details(process)
15
+ description_cols = {
16
+ "Process ID" => lambda {|it| it['id'] },
17
+ "Name" => lambda {|it| it['displayName'] },
18
+ "Description" => lambda {|it| it['description'] },
19
+ "Process Type" => lambda {|it| it['processType'] ? (it['processType']['name'] || it['processType']['code']) : it['processTypeName'] },
20
+ "Created By" => lambda {|it| it['createdBy'] ? (it['createdBy']['displayName'] || it['createdBy']['username']) : '' },
21
+ "Start Date" => lambda {|it| format_local_dt(it['startDate']) },
22
+ "End Date" => lambda {|it| format_local_dt(it['endDate']) },
23
+ "Duration" => lambda {|it| format_process_duration(it) },
24
+ "Status" => lambda {|it| format_process_status(it) },
25
+ # "# Events" => lambda {|it| (it['events'] || []).size() },
26
+ }
27
+ print_description_list(description_cols, process)
28
+
29
+ if process['error']
30
+ print_h2 "Error"
31
+ print reset
32
+ #puts format_process_error(process_event)
33
+ puts process['error'].to_s.strip
34
+ end
35
+
36
+ if process['output']
37
+ print_h2 "Output"
38
+ print reset
39
+ #puts format_process_error(process_event)
40
+ puts process['output'].to_s.strip
41
+ end
42
+ end
43
+
44
+ def print_process_event_details(process_event)
45
+ # process_event =~ process
46
+ description_cols = {
47
+ "Process ID" => lambda {|it| it['processId'] },
48
+ "Event ID" => lambda {|it| it['id'] },
49
+ "Name" => lambda {|it| it['displayName'] },
50
+ "Description" => lambda {|it| it['description'] },
51
+ "Process Type" => lambda {|it| it['processType'] ? (it['processType']['name'] || it['processType']['code']) : it['processTypeName'] },
52
+ "Created By" => lambda {|it| it['createdBy'] ? (it['createdBy']['displayName'] || it['createdBy']['username']) : '' },
53
+ "Start Date" => lambda {|it| format_local_dt(it['startDate']) },
54
+ "End Date" => lambda {|it| format_local_dt(it['endDate']) },
55
+ "Duration" => lambda {|it| format_process_duration(it) },
56
+ "Status" => lambda {|it| format_process_status(it) },
57
+ }
58
+ print_description_list(description_cols, process_event)
59
+
60
+ if process_event['error']
61
+ print_h2 "Error"
62
+ print reset
63
+ #puts format_process_error(process_event)
64
+ puts process_event['error'].to_s.strip
65
+ end
66
+
67
+ if process_event['output']
68
+ print_h2 "Output"
69
+ print reset
70
+ #puts format_process_error(process_event)
71
+ puts process_event['output'].to_s.strip
72
+ end
73
+ end
74
+
75
+
76
+ def format_process_status(process, return_color=cyan)
77
+ out = ""
78
+ status_string = process['status'].to_s
79
+ if status_string == 'complete'
80
+ out << "#{green}#{status_string.upcase}#{return_color}"
81
+ elsif status_string == 'failed'
82
+ out << "#{red}#{status_string.upcase}#{return_color}"
83
+ elsif status_string == 'expired'
84
+ out << "#{red}#{status_string.upcase}#{return_color}"
85
+ else
86
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
87
+ end
88
+ out
89
+ end
90
+
91
+ # decolorize, remove newlines and truncate for table cell
92
+ def format_process_error(process, max_length=50, return_color=cyan)
93
+ out = ""
94
+ if process['error']
95
+ lines = process['error'].split("\n").collect {|line| reset + "#{line.to_s.strip}" }
96
+ out = lines.join(" ")
97
+ if max_length
98
+ out = truncate_string(out, max_length)
99
+ end
100
+ out << return_color if return_color
101
+ end
102
+ out
103
+ end
104
+
105
+ # decolorize, remove newlines and truncate for table cell
106
+ def format_process_output(process, max_length=50, return_color=cyan)
107
+ out = ""
108
+ if process['output']
109
+ lines = process['output'].split("\n").collect {|line| reset + "#{line.to_s.strip}" }
110
+ out = lines.join(" ")
111
+ if max_length
112
+ out = truncate_string(out, max_length)
113
+ end
114
+ out << return_color if return_color
115
+ end
116
+ out
117
+ end
118
+
119
+ # format for either ETA/Duration
120
+ def format_process_duration(process, time_format="%H:%M:%S")
121
+ out = ""
122
+ if process['duration'] && process['duration'] > 0
123
+ out = format_duration_milliseconds(process['duration'], time_format)
124
+ elsif process['statusEta'] && process['statusEta'] > 0
125
+ out = format_duration_milliseconds(process['statusEta'], time_format)
126
+ elsif process['startDate'] && process['endDate']
127
+ out = format_duration(process['startDate'], process['endDate'], time_format)
128
+ else
129
+ ""
130
+ end
131
+ out
132
+ end
133
+
134
+ end
@@ -126,20 +126,10 @@ module Morpheus
126
126
  # I suppose the entered value should take precedence
127
127
  # api_params = api_params.merge(options) # this might be good enough
128
128
  # dup it
129
- select_api_params = {}.merge(api_params || {})
130
- results.each do |k,v|
131
- if v.is_a?(Hash)
132
- # grailsify params k.k2, otherwise you'll get bracked param names like k[k2]
133
- v.each {|k2, v2|
134
- select_api_params["#{k}.#{k2}"] = v2
135
- }
136
- else
137
- # could be String/Symbol duplication issues here.
138
- select_api_params.delete(k.to_sym)
139
- select_api_params[k.to_s] = v
140
- end
141
- end
142
- value = select_prompt(option_type,api_client, select_api_params)
129
+ select_api_params = {}.merge(api_params || {}).merge(results)
130
+ grails_select_api_params = grails_params(select_api_params)
131
+
132
+ value = select_prompt(option_type,api_client, grails_select_api_params)
143
133
  elsif option_type['type'] == 'hidden'
144
134
  value = option_type['defaultValue']
145
135
  input = value
@@ -155,6 +145,21 @@ module Morpheus
155
145
  return results
156
146
  end
157
147
 
148
+ def self.grails_params(data, context=nil)
149
+ params = {}
150
+ data.each do |k,v|
151
+ if v.is_a?(Hash)
152
+ params.merge!(grails_params(v, context ? "#{context}.#{k.to_s}" : k))
153
+ else
154
+ if context
155
+ params["#{context}.#{k.to_s}"] = v
156
+ else
157
+ params[k.to_s] = v
158
+ end
159
+ end
160
+ end
161
+ return params
162
+ end
158
163
 
159
164
  def self.radio_prompt(option_type)
160
165
  value_found = false
@@ -368,7 +373,7 @@ module Morpheus
368
373
  # value = input.empty? ? option_type['defaultValue'] : input
369
374
  if input == '?' && value.nil?
370
375
  help_prompt(option_type)
371
- elsif (!value.nil? || option_type['required'] != true) && input.chomp == 'EOF'
376
+ elsif input.chomp == 'EOF'
372
377
  value_found = true
373
378
  else
374
379
  if value.nil?
@@ -377,7 +382,7 @@ module Morpheus
377
382
  value << input + "\n"
378
383
  end
379
384
  end
380
- return value
385
+ return value ? value.strip : value
381
386
  end
382
387
 
383
388
  def self.password_prompt(option_type)
@@ -1,14 +1,15 @@
1
1
  require 'fileutils'
2
2
  require 'morpheus/cli/cli_command'
3
3
  require 'morpheus/cli/mixins/library_helper'
4
+ require 'morpheus/morpkg'
4
5
 
5
6
  class Morpheus::Cli::LibraryPackagesCommand
6
7
  include Morpheus::Cli::CliCommand
7
8
  include Morpheus::Cli::LibraryHelper
8
9
 
9
10
  set_command_name :'packages'
10
- # register_subcommands :list, :get, :install, :update, :remove, :export
11
- register_subcommands :list, :search, :export
11
+ register_subcommands :list, :search, :get, :install, :update, :remove, :export
12
+ register_subcommands :'install-file' => :install_file
12
13
 
13
14
  # hide until this is released
14
15
  set_command_hidden
@@ -41,7 +42,7 @@ class Morpheus::Cli::LibraryPackagesCommand
41
42
 
42
43
  if options[:dry_run]
43
44
  print_dry_run @packages_interface.dry.list(params)
44
- return
45
+ return 0
45
46
  end
46
47
  json_response = @packages_interface.list(params)
47
48
  if options[:json]
@@ -69,7 +70,7 @@ class Morpheus::Cli::LibraryPackagesCommand
69
70
  description: package['description'],
70
71
  }
71
72
  }
72
- columns = [:code, :name, {:description => {:max_width => 50}}]
73
+ columns = [:code, :name, {:description => {:max_width => 50}}, :version]
73
74
  # custom pretty table columns ...
74
75
  if options[:include_fields]
75
76
  columns = options[:include_fields]
@@ -98,11 +99,14 @@ class Morpheus::Cli::LibraryPackagesCommand
98
99
  optparse.parse!(args)
99
100
  connect(options)
100
101
  begin
102
+ if args[0]
103
+ options[:phrase] = args[0]
104
+ # params['phrase'] = args[0]
105
+ end
101
106
  params.merge!(parse_list_options(options))
102
-
103
107
  if options[:dry_run]
104
108
  print_dry_run @packages_interface.dry.search(params)
105
- return
109
+ return 0
106
110
  end
107
111
  json_response = @packages_interface.search(params)
108
112
  if options[:json]
@@ -126,11 +130,11 @@ class Morpheus::Cli::LibraryPackagesCommand
126
130
  {
127
131
  code: package['code'],
128
132
  name: package['name'],
129
- version: package['version'],
130
- description: package['description']
133
+ description: package['description'],
134
+ versions: (package['versions'] || []).collect {|v| v['version'] }.join(', ')
131
135
  }
132
136
  }
133
- columns = [:code, :name, {:description => {:max_width => 50}}]
137
+ columns = [:code, :name, {:description => {:max_width => 50}}, {:versions => {:max_width => 30}}]
134
138
  # custom pretty table columns ...
135
139
  if options[:include_fields]
136
140
  columns = options[:include_fields]
@@ -149,11 +153,279 @@ class Morpheus::Cli::LibraryPackagesCommand
149
153
  end
150
154
 
151
155
  def get(args)
152
- raise "not yet implemented"
156
+ options = {}
157
+ params = {}
158
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
159
+ opts.banner = subcommand_usage("[code]")
160
+ opts.on( nil, '--versions', "Display Available Versions" ) do
161
+ options[:show_versions] = true
162
+ end
163
+ opts.on( nil, '--objects', "Display Installed Package Objects" ) do
164
+ options[:show_objects] = true
165
+ end
166
+ build_common_options(opts, options, [:query, :json, :yaml, :csv, :fields, :dry_run, :remote])
167
+ opts.footer = "Get details about a package.\n" +
168
+ "[code] is required. This is the package code."
169
+ end
170
+ optparse.parse!(args)
171
+ connect(options)
172
+ if args.count != 1
173
+ print_error Morpheus::Terminal.angry_prompt
174
+ puts_error "#{command_name} get expects 1 argument and received #{args.count} #{args.join(' ')}\n#{optparse}"
175
+ return 1
176
+ end
177
+ begin
178
+ params.merge!(parse_list_options(options))
179
+ params['code'] = args[0]
180
+ if options[:dry_run]
181
+ print_dry_run @packages_interface.dry.info(params)
182
+ return 0
183
+ end
184
+ json_response = @packages_interface.info(params)
185
+ if options[:json]
186
+ puts as_json(json_response, options, "package")
187
+ return 0
188
+ elsif options[:yaml]
189
+ puts as_yaml(json_response, options, "package")
190
+ return 0
191
+ elsif options[:csv]
192
+ puts records_as_csv(json_response["package"], options)
193
+ else
194
+ installed_package = json_response["package"]
195
+ available_package = json_response["availablePackage"]
196
+
197
+ if installed_package.nil? && available_package.nil?
198
+ print yellow,"No package found for code '#{params['code']}'",reset,"\n"
199
+ return 1
200
+ end
201
+
202
+ print_h1 "Package Info"
203
+
204
+ # merge installed and available package info
205
+ package_record = installed_package || available_package
206
+ #package_record['versions'] = available_package['versions'] if available_package
207
+ print cyan
208
+ description_cols = {
209
+ # "Organization" => 'organization',
210
+ "Code" => 'code',
211
+ "Name" => 'name',
212
+ "Description" => 'description',
213
+ # "Type" => lambda {|it| (it['packageType'] || it['type']).to_s },
214
+ "Latest Version" => lambda {|it|
215
+ if available_package && available_package['versions']
216
+ latest_version = available_package['versions'].find {|v| v['latestVersion'] }
217
+ if latest_version.nil?
218
+ sorted_versions = available_package['versions'].sort {|x,y| x['dateCreated'] <=> y['dateCreated'] }
219
+ latest_version = sorted_versions.first()
220
+ end
221
+ latest_version ? latest_version['version'] : ""
222
+ else
223
+ ""
224
+ end
225
+ },
226
+ "Installed Version" => lambda {|it|
227
+ installed_package ? installed_package['version'] : ""
228
+ },
229
+ "Status" => lambda {|it|
230
+ installed_package ? format_package_status(installed_package['status']) : format_package_status(nil)
231
+ },
232
+ }
233
+ if installed_package.nil?
234
+ description_cols.delete("Installed Version")
235
+ end
236
+
237
+ print_description_list(description_cols, package_record)
238
+ # print reset, "\n"
239
+ if options[:show_versions]
240
+ print_h2 "Available Versions"
241
+ if available_package.nil?
242
+ print yellow,"No marketplace package found for code '#{params['code']}'",reset,"\n"
243
+ else
244
+ if available_package['versions'].nil? || available_package['versions'].empty?
245
+ print yellow,"No available versions found",reset,"\n"
246
+ else
247
+ # api is sorting these with latestVersion first, right?
248
+ sorted_versions = available_package['versions'] || []
249
+ #sorted_versions = available_package['versions'].sort {|x,y| x['dateCreated'] <=> y['dateCreated'] }
250
+ rows = sorted_versions.collect {|package_version|
251
+ {
252
+ "PACKAGE VERSION": package_version['version'],
253
+ "MORPHEUS VERSION": package_version['minApplianceVersion'],
254
+ "PUBLISH DATE": format_local_dt(package_version['created'] || package_version['dateCreated'])
255
+ }
256
+ }
257
+ columns = ["PACKAGE VERSION", "MORPHEUS VERSION", "PUBLISH DATE"]
258
+ print cyan
259
+ print as_pretty_table(rows, columns)
260
+ # print reset, "\n"
261
+ end
262
+ end
263
+ end
264
+ if options[:show_objects]
265
+ print_h2 "Package Objects"
266
+ if installed_package.nil?
267
+ print cyan,"No objects to show",reset,"\n"
268
+ else
269
+ if installed_package['objects'].nil? || installed_package['objects'].empty?
270
+ print yellow,"No objects to show",reset,"\n"
271
+ else
272
+ rows = installed_package['objects'].collect {|it|
273
+ {
274
+ type: it['refType'],
275
+ id: it['refId'],
276
+ name: it['displayName']
277
+ }
278
+ }
279
+ columns = [:type, :id, :name]
280
+ print cyan
281
+ print as_pretty_table(rows, columns)
282
+ # print reset, "\n"
283
+ end
284
+ end
285
+ end
286
+
287
+ print reset, "\n"
288
+ return 0
289
+ end
290
+ rescue RestClient::Exception => e
291
+ print_rest_exception(e, options)
292
+ exit 1
293
+ end
153
294
  end
154
295
 
155
296
  def install(args)
156
- raise "not yet implemented"
297
+ full_command_string = "#{command_name} install #{args.join(' ')}".strip
298
+ options = {}
299
+ params = {}
300
+ open_prog = nil
301
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
302
+ opts.banner = subcommand_usage("[code]")
303
+ opts.on('-v','--version VERSION', "Package Version number") do |val|
304
+ params['version'] = val
305
+ end
306
+ # opts.on('--package-version VALUE', String, "Version number for package.") do |val|
307
+ # params['version'] = val
308
+ # end
309
+ opts.on('--organization NAME', String, "Package Organization. Default is morpheus.") do |val|
310
+ params['organization'] = val
311
+ end
312
+ opts.on( '-f', '--force', "Force Install." ) do
313
+ params['force'] = true
314
+ end
315
+ build_common_options(opts, options, [:options, :json, :dry_run, :remote, :quiet])
316
+ opts.footer = "Install a morpheus package from the marketplace.\n" +
317
+ "[code] is required. This is the package code."
318
+ end
319
+ optparse.parse!(args)
320
+ if args.count != 1
321
+ print_error Morpheus::Terminal.angry_prompt
322
+ puts_error "#{command_name} install expects 1 argument and received #{args.count} #{args.join(' ')}\n#{optparse}"
323
+ return 1
324
+ end
325
+ connect(options)
326
+ begin
327
+ params['code'] = args[0]
328
+ if options[:dry_run]
329
+ #print cyan,bold, " - Uploading #{local_file_path} to #{bucket_id}:#{destination} DRY RUN", reset, "\n"
330
+ # print_h1 "DRY RUN"
331
+ print_dry_run @packages_interface.dry.install(params)
332
+ print "\n"
333
+ return 0
334
+ end
335
+
336
+ json_response = @packages_interface.install(params)
337
+ if options[:json]
338
+ puts as_json(json_response, options)
339
+ elsif !options[:quiet]
340
+ package_str = params['code'] || ""
341
+ installed_package = json_response['package']
342
+ if installed_package && installed_package['code']
343
+ package_str = installed_package['code']
344
+ end
345
+ if installed_package && installed_package['version']
346
+ package_str = "#{package_str} #{installed_package['version']}"
347
+ end
348
+ print_green_success "Installed package #{package_str}"
349
+ end
350
+
351
+ rescue RestClient::Exception => e
352
+ print_rest_exception(e, options)
353
+ return 1
354
+ end
355
+ end
356
+
357
+ def install_file(args)
358
+ full_command_string = "#{command_name} install-file #{args.join(' ')}".strip
359
+ options = {}
360
+ params = {}
361
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
362
+ opts.banner = subcommand_usage("[morpkg-file]")
363
+ # opts.on('--url URL', String, "Use a remote URL as the source .morpkg instead of uploading a file.") do |val|
364
+ # params['url'] = val
365
+ # end
366
+ opts.on( '-f', '--force', "Force Install." ) do
367
+ params['force'] = true
368
+ end
369
+ build_common_options(opts, options, [:options, :dry_run, :quiet])
370
+ opts.footer = "Install a morpheus package with a .morpkg file directly.\n" +
371
+ "[morpkg-file] is required. This is the local filepath of a .morpkg to be uploaded and installed."
372
+ end
373
+ optparse.parse!(args)
374
+
375
+ if args.count != 1
376
+ print_error Morpheus::Terminal.angry_prompt
377
+ puts_error "expects 1 argument and received #{args.count} #{args.join(' ')}\n#{optparse}"
378
+ return 1
379
+ end
380
+
381
+ # validate local file path
382
+ local_file_path = File.expand_path(args[0].squeeze('/'))
383
+ if local_file_path == "" || local_file_path == "/" || local_file_path == "."
384
+ print_error Morpheus::Terminal.angry_prompt
385
+ puts_error "bad argument: [morpkg-file]\nFile '#{local_file_path}' is invalid.\n#{optparse}"
386
+ return 1
387
+ end
388
+ if !File.exists?(local_file_path)
389
+ print_error Morpheus::Terminal.angry_prompt
390
+ puts_error "bad argument: [morpkg-file]\nFile '#{local_file_path}' was not found.\n"
391
+ return 1
392
+ end
393
+ if !File.file?(local_file_path)
394
+ print_error Morpheus::Terminal.angry_prompt
395
+ puts_error "bad argument: [morpkg-file]\nFile '#{local_file_path}' is not a file.\n"
396
+ return 1
397
+ end
398
+
399
+ connect(options)
400
+ begin
401
+
402
+ if options[:dry_run]
403
+ #print cyan,bold, " - Uploading #{local_file_path} to #{bucket_id}:#{destination} DRY RUN", reset, "\n"
404
+ # print_h1 "DRY RUN"
405
+ print_dry_run @packages_interface.dry.install_file(local_file_path, params)
406
+ print "\n"
407
+ return 0
408
+ end
409
+
410
+ json_response = @packages_interface.install_file(local_file_path, params)
411
+ if options[:json]
412
+ puts as_json(json_response, options)
413
+ elsif !options[:quiet]
414
+ package_str = params['code'] || ""
415
+ installed_package = json_response['package']
416
+ if installed_package && installed_package['code']
417
+ package_str = installed_package['code']
418
+ end
419
+ if installed_package && installed_package['version']
420
+ package_str = "#{package_str} #{installed_package['version']}"
421
+ end
422
+ print_green_success "Installed package #{package_str}"
423
+ end
424
+
425
+ rescue RestClient::Exception => e
426
+ print_rest_exception(e, options)
427
+ return 1
428
+ end
157
429
  end
158
430
 
159
431
  def update(args)
@@ -164,7 +436,7 @@ class Morpheus::Cli::LibraryPackagesCommand
164
436
  raise "not yet implemented"
165
437
  end
166
438
 
167
- # generate a new .morpkg file
439
+ # download a new .morpkg for remote instanceType(s)
168
440
  def export(args)
169
441
  full_command_string = "#{command_name} export #{args.join(' ')}".strip
170
442
  options = {}
@@ -241,15 +513,15 @@ class Morpheus::Cli::LibraryPackagesCommand
241
513
  end
242
514
  end
243
515
  build_common_options(opts, options, [:options, :dry_run, :quiet])
244
- opts.footer = "Export one or many instance types as a morpheus library package (.morpkg) file.\n" +
516
+ opts.footer = "Export one or many instance types as a morpheus package (.morpkg) file.\n" +
245
517
  "[instance-type] is required. This is the instance type code." +
246
- "--instance-types can bv. This is a list of instance type codes."
518
+ "--instance-types can be export multiple instance types at once. This is a list of instance type codes."
247
519
  end
248
520
  optparse.parse!(args)
249
521
 
250
522
  if args.count != 1 && !instance_type_codes && !params['all']
251
523
  print_error Morpheus::Terminal.angry_prompt
252
- puts_error "#{command_name} export expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
524
+ puts_error "#{command_name} export expects 1 argument and received #{args.count} #{args.join(' ')}\n#{optparse}"
253
525
  return 1
254
526
  end
255
527
  connect(options)
@@ -308,7 +580,7 @@ class Morpheus::Cli::LibraryPackagesCommand
308
580
 
309
581
  if options[:dry_run]
310
582
  print_dry_run @packages_interface.dry.export(params, outfile), full_command_string
311
- return 1
583
+ return 0
312
584
  end
313
585
  if !options[:quiet]
314
586
  print cyan + "Downloading morpheus package file #{outfile} ... "
@@ -362,8 +634,188 @@ class Morpheus::Cli::LibraryPackagesCommand
362
634
  end
363
635
  end
364
636
 
637
+ # generate a new .morpkg for a local source directory
638
+ def build(args)
639
+ full_command_string = "#{command_name} build #{args.join(' ')}".strip
640
+ options = {}
641
+ source_directory = nil
642
+ outfile = nil
643
+ do_overwrite = false
644
+ do_mkdir = false
645
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
646
+ opts.banner = subcommand_usage("[source] [target]")
647
+ opts.on('--source FILE', String, "Source directory of the package being built.") do |val|
648
+ source_directory = val
649
+ end
650
+ opts.on('--target FILE', String, "Destination filename for the .morpkg output file. Default is [code]-[version].morpkg") do |val|
651
+ outfile = val
652
+ end
653
+ opts.on( '-p', '--mkdir', "Create missing directories for [target] if they do not exist." ) do
654
+ do_mkdir = true
655
+ end
656
+ opts.on( '-f', '--force', "Overwrite existing [target] if it exists. Also creates missing directories." ) do
657
+ do_overwrite = true
658
+ do_mkdir = true
659
+ end
660
+
661
+ build_common_options(opts, options, [:options, :dry_run, :quiet])
662
+ opts.footer = "Generate a new morpheus package file. \n" +
663
+ "[source] is required. This is the source directory of the package.\n" +
664
+ "[target] is the output filename. The default is [code]-[version].morpkg"
665
+ end
666
+ optparse.parse!(args)
667
+
668
+ if args.count < 1 || args.count > 2
669
+ print_error Morpheus::Terminal.angry_prompt
670
+ puts_error "wrong number of arguments, expected 1-2 and got #{args.count}\n#{optparse}"
671
+ return 1
672
+ end
673
+ if args[0]
674
+ source_directory = args[0]
675
+ end
676
+ if args[1]
677
+ outfile = args[1]
678
+ end
679
+ if !source_directory
680
+ print_error Morpheus::Terminal.angry_prompt
681
+ puts_error "missing required argument [source]\n#{optparse}"
682
+ return 1
683
+ end
684
+
685
+ # connect(options)
686
+ begin
687
+ # validate source
688
+ source_directory = File.expand_path(source_directory)
689
+ if !File.exists?(source_directory)
690
+ puts_error "#{Morpheus::Terminal.angry_prompt}[source] is invalid. Directory not found: #{source_directory}"
691
+ return 1
692
+ end
693
+ if !File.directory?(source_directory)
694
+ puts_error "#{Morpheus::Terminal.angry_prompt}[source] is invalid. Not a directory: #{source_directory}"
695
+ return 1
696
+ end
697
+ # parse package source
698
+
699
+ package_org = nil
700
+ package_code = nil
701
+ package_version = nil
702
+ package_type = nil
703
+
704
+
705
+ # validate package
706
+ if !package_code.nil? || package_code.empty?
707
+ puts_error "#{Morpheus::Terminal.angry_prompt}package data is invalid. Missing package code."
708
+ return 1
709
+ end
710
+ if package_version.nil? || package_version.empty?
711
+ puts_error "#{Morpheus::Terminal.angry_prompt}package data is invalid. Missing package version."
712
+ return 1
713
+ end
714
+ # determine outfile
715
+ if !outfile
716
+ outfile = File.join(File.dirname(source_directory), "#{package_code}-#{package_version}.morpkg")
717
+ else
718
+ outfile = File.expand_path(outfile)
719
+ end
720
+ if Dir.exists?(outfile)
721
+ puts_error "#{Morpheus::Terminal.angry_prompt}[target] is invalid. It is the name of an existing directory: #{outfile}"
722
+ return 1
723
+ end
724
+ # always a .morpkg
725
+ if outfile[-7..-1] != ".morpkg"
726
+ outfile << ".morpkg"
727
+ end
728
+ destination_dir = File.dirname(outfile)
729
+ if !Dir.exists?(destination_dir)
730
+ if do_mkdir
731
+ print cyan,"Creating local directory #{destination_dir}",reset,"\n"
732
+ FileUtils.mkdir_p(destination_dir)
733
+ else
734
+ puts_error "#{Morpheus::Terminal.angry_prompt}[target] is invalid. Directory not found: #{destination_dir} Use -p to create the missing directory."
735
+ return 1
736
+ end
737
+ end
738
+ if File.exists?(outfile)
739
+ if do_overwrite
740
+ # uhh need to be careful wih the passed filepath here..
741
+ # don't delete, just overwrite.
742
+ # File.delete(outfile)
743
+ else
744
+ puts_error "#{Morpheus::Terminal.angry_prompt}[target] is invalid. File already exists: #{outfile}", "Use -f to overwrite the existing file."
745
+ return 1
746
+ end
747
+ end
748
+
749
+ if options[:dry_run]
750
+ print cyan + "Building morpheus package at #{source_directory} ..."
751
+ print cyan + "DRY RUN" + reset + "\n"
752
+ return 0
753
+ end
754
+ if !options[:quiet]
755
+ print cyan + "Building morpheus package at #{source_directory} ... "
756
+ end
757
+
758
+ # build it
759
+ #http_response, bad_body = @packages_interface.export(params, outfile)
760
+
761
+ # FileUtils.chmod(0600, outfile)
762
+ success = true
763
+ error_msg = nil
764
+ build_outfile = nil
765
+ begin
766
+ build_outfile = Morpheus::Morpkg.build_package(source_directory, outfile, options[:force])
767
+ success = true
768
+ rescue => ex
769
+ error_msg = ex.message
770
+ end
771
+ if success
772
+ if !options[:quiet]
773
+ print green + "SUCCESS" + reset + "\n"
774
+ print green + "Generated #{build_outfile}" + reset + "\n"
775
+ end
776
+ return 0
777
+ else
778
+ if !options[:quiet]
779
+ print red + "ERROR" + reset + "\n"
780
+ if error_msg
781
+ print_error red + error_msg + reset + "\n"
782
+ end
783
+ end
784
+ # F it, just remove a bad result
785
+ # if File.exists?(outfile) && File.file?(outfile)
786
+ # Morpheus::Logging::DarkPrinter.puts "Deleting bad build file: #{outfile}" if Morpheus::Logging.debug?
787
+ # File.delete(outfile)
788
+ # end
789
+ if options[:debug]
790
+ # puts_error error_msg
791
+ end
792
+ return 1
793
+ end
794
+
795
+ rescue RestClient::Exception => e
796
+ print_rest_exception(e, options)
797
+ return 1
798
+ end
799
+ end
800
+
365
801
  private
366
802
 
367
-
803
+ def format_package_status(status_string, return_color=cyan)
804
+ out = ""
805
+ if status_string == 'installed'
806
+ out << "#{green}#{status_string.upcase}#{return_color}"
807
+ elsif status_string == 'uninstalled' || status_string == 'removed'
808
+ out << "#{red}#{status_string.upcase}#{return_color}"
809
+ elsif status_string == 'installing'
810
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
811
+ elsif status_string.nil?
812
+ out << "#{cyan}NOT INSTALLED#{return_color}"
813
+ elsif !status_string.nil?
814
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
815
+ else
816
+ out << ""
817
+ end
818
+ out
819
+ end
368
820
 
369
821
  end