morpheus-cli 4.2.12 → 4.2.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/README.md +8 -6
  4. data/lib/morpheus/api/api_client.rb +32 -14
  5. data/lib/morpheus/api/auth_interface.rb +4 -2
  6. data/lib/morpheus/api/backup_jobs_interface.rb +9 -0
  7. data/lib/morpheus/api/backups_interface.rb +16 -0
  8. data/lib/morpheus/api/deploy_interface.rb +25 -56
  9. data/lib/morpheus/api/deployments_interface.rb +43 -54
  10. data/lib/morpheus/api/doc_interface.rb +57 -0
  11. data/lib/morpheus/api/instances_interface.rb +5 -0
  12. data/lib/morpheus/api/rest_interface.rb +40 -0
  13. data/lib/morpheus/api/user_sources_interface.rb +0 -15
  14. data/lib/morpheus/api/users_interface.rb +2 -3
  15. data/lib/morpheus/benchmarking.rb +2 -2
  16. data/lib/morpheus/cli.rb +3 -1
  17. data/lib/morpheus/cli/access_token_command.rb +27 -10
  18. data/lib/morpheus/cli/apps.rb +23 -16
  19. data/lib/morpheus/cli/backup_jobs_command.rb +276 -0
  20. data/lib/morpheus/cli/backups_command.rb +271 -0
  21. data/lib/morpheus/cli/boot_scripts_command.rb +1 -1
  22. data/lib/morpheus/cli/cli_command.rb +176 -45
  23. data/lib/morpheus/cli/cli_registry.rb +10 -1
  24. data/lib/morpheus/cli/clusters.rb +2 -19
  25. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +23 -20
  26. data/lib/morpheus/cli/commands/standard/man_command.rb +1 -1
  27. data/lib/morpheus/cli/containers_command.rb +2 -1
  28. data/lib/morpheus/cli/credentials.rb +14 -10
  29. data/lib/morpheus/cli/deploy.rb +374 -0
  30. data/lib/morpheus/cli/deployments.rb +521 -197
  31. data/lib/morpheus/cli/deploys.rb +271 -126
  32. data/lib/morpheus/cli/doc.rb +182 -0
  33. data/lib/morpheus/cli/error_handler.rb +23 -8
  34. data/lib/morpheus/cli/errors.rb +3 -2
  35. data/lib/morpheus/cli/health_command.rb +4 -3
  36. data/lib/morpheus/cli/hosts.rb +2 -1
  37. data/lib/morpheus/cli/image_builder_command.rb +2 -2
  38. data/lib/morpheus/cli/instances.rb +138 -18
  39. data/lib/morpheus/cli/invoices_command.rb +338 -223
  40. data/lib/morpheus/cli/library_layouts_command.rb +1 -1
  41. data/lib/morpheus/cli/library_option_lists_command.rb +61 -125
  42. data/lib/morpheus/cli/library_option_types_command.rb +32 -37
  43. data/lib/morpheus/cli/login.rb +9 -3
  44. data/lib/morpheus/cli/logs_command.rb +3 -2
  45. data/lib/morpheus/cli/mixins/accounts_helper.rb +158 -100
  46. data/lib/morpheus/cli/mixins/backups_helper.rb +115 -0
  47. data/lib/morpheus/cli/mixins/deployments_helper.rb +135 -0
  48. data/lib/morpheus/cli/mixins/library_helper.rb +32 -0
  49. data/lib/morpheus/cli/mixins/logs_helper.rb +18 -9
  50. data/lib/morpheus/cli/mixins/option_source_helper.rb +1 -1
  51. data/lib/morpheus/cli/mixins/print_helper.rb +149 -84
  52. data/lib/morpheus/cli/mixins/provisioning_helper.rb +2 -2
  53. data/lib/morpheus/cli/mixins/whoami_helper.rb +19 -6
  54. data/lib/morpheus/cli/network_routers_command.rb +1 -1
  55. data/lib/morpheus/cli/option_parser.rb +48 -5
  56. data/lib/morpheus/cli/option_types.rb +46 -10
  57. data/lib/morpheus/cli/price_sets_command.rb +1 -1
  58. data/lib/morpheus/cli/remote.rb +8 -10
  59. data/lib/morpheus/cli/roles.rb +49 -92
  60. data/lib/morpheus/cli/security_groups.rb +7 -1
  61. data/lib/morpheus/cli/service_plans_command.rb +10 -10
  62. data/lib/morpheus/cli/setup.rb +1 -1
  63. data/lib/morpheus/cli/shell.rb +7 -6
  64. data/lib/morpheus/cli/subnets_command.rb +1 -1
  65. data/lib/morpheus/cli/tenants_command.rb +133 -163
  66. data/lib/morpheus/cli/user_groups_command.rb +20 -65
  67. data/lib/morpheus/cli/user_settings_command.rb +115 -13
  68. data/lib/morpheus/cli/user_sources_command.rb +57 -24
  69. data/lib/morpheus/cli/users.rb +210 -186
  70. data/lib/morpheus/cli/version.rb +1 -1
  71. data/lib/morpheus/cli/whitelabel_settings_command.rb +29 -5
  72. data/lib/morpheus/cli/whoami.rb +113 -6
  73. data/lib/morpheus/cli/workflows.rb +1 -1
  74. data/lib/morpheus/ext/hash.rb +21 -0
  75. data/lib/morpheus/formatters.rb +7 -19
  76. data/lib/morpheus/terminal.rb +1 -0
  77. metadata +12 -3
  78. data/lib/morpheus/cli/auth_command.rb +0 -105
@@ -9,6 +9,9 @@ module Morpheus
9
9
  module Cli
10
10
  class CliRegistry
11
11
 
12
+ class BadCommandDefinition < StandardError
13
+ end
14
+
12
15
  class BadAlias < StandardError
13
16
  end
14
17
 
@@ -128,6 +131,11 @@ module Morpheus
128
131
  previous_command_result = nil
129
132
  current_operator = nil
130
133
  still_executing = true
134
+ # need to error before executing anything, could be dangerous otherwise!
135
+ # also maybe only pass flow commands if they have a space on either side..
136
+ if flow.include?("|")
137
+ raise Morpheus::Cli::ExpressionParser::InvalidExpression.new "The PIPE (|) operator is not yet supported. You can wrap your arguments in quotations."
138
+ end
131
139
  flow.each do |flow_cmd|
132
140
  if still_executing
133
141
  if flow_cmd == '&&'
@@ -144,7 +152,8 @@ module Morpheus
144
152
  still_executing = false
145
153
  end
146
154
  elsif flow_cmd == '|' # or with previous command
147
- raise Morpheus::Cli::ExpressionParser::InvalidExpression.new "The PIPE (|) operator is not yet supported =["
155
+ # todo, handle pipe!
156
+ raise Morpheus::Cli::ExpressionParser::InvalidExpression.new "The PIPE (|) operator is not yet supported. You can wrap your arguments in quotations."
148
157
  previous_command_result = nil
149
158
  still_executing = false
150
159
  # or just continue?
@@ -609,15 +609,6 @@ class Morpheus::Cli::Clusters
609
609
  option_type_list = ((controller_type['optionTypes'].reject { |type| !type['enabled'] || type['fieldComponent'] } rescue []) + layout['optionTypes'] +
610
610
  (cluster_type['optionTypes'].reject { |type| !type['enabled'] || !type['creatable'] || type['fieldComponent'] } rescue [])).sort { |type| type['displayOrder'] }
611
611
 
612
- # remove volume options if volumes were configured
613
- if !server_payload['volumes'].empty?
614
- option_type_list = reject_volume_option_types(option_type_list)
615
- end
616
- # remove networkId option if networks were configured above
617
- if !server_payload['networkInterfaces'].empty?
618
- option_type_list = reject_networking_option_types(option_type_list)
619
- end
620
-
621
612
  server_payload.deep_merge!(Morpheus::Cli::OptionTypes.prompt(option_type_list, options[:options], @api_client, {zoneId: cloud['id'], siteId: group['id'], layoutId: layout['id']}))
622
613
 
623
614
  # Worker count
@@ -836,7 +827,7 @@ class Morpheus::Cli::Clusters
836
827
  options[:end] = parse_time(val) #.utc.iso8601
837
828
  end
838
829
  opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
839
- params['level'] = params['level'] ? [params['level'], val].flatten : val
830
+ params['level'] = params['level'] ? [params['level'], val].flatten : [val]
840
831
  end
841
832
  opts.on('--table', '--table', "Format ouput as a table.") do
842
833
  options[:table] = true
@@ -854,6 +845,7 @@ class Morpheus::Cli::Clusters
854
845
  begin
855
846
  cluster = find_cluster_by_name_or_id(args[0])
856
847
  params = {}
848
+ params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
857
849
  params.merge!(parse_list_options(options))
858
850
  params['query'] = params.delete('phrase') if params['phrase']
859
851
  params['startMs'] = (options[:start].to_i * 1000) if options[:start]
@@ -1196,15 +1188,6 @@ class Morpheus::Cli::Clusters
1196
1188
  (type['fieldContext'] == 'instance.networkDomain' && type['fieldName'] == 'id')
1197
1189
  } rescue [])
1198
1190
 
1199
- # remove volume options if volumes were configured
1200
- if !server_payload['volumes'].empty?
1201
- option_type_list = reject_volume_option_types(option_type_list)
1202
- end
1203
- # remove networkId option if networks were configured above
1204
- if !server_payload['networkInterfaces'].empty?
1205
- option_type_list = reject_networking_option_types(option_type_list)
1206
- end
1207
-
1208
1191
  server_payload.deep_merge!(Morpheus::Cli::OptionTypes.prompt(option_type_list, options[:options], @api_client, {zoneId: cloud['id'], siteId: group['id'], layoutId: layout['id']}))
1209
1192
 
1210
1193
  # Create User
@@ -7,11 +7,14 @@ class Morpheus::Cli::BenchmarkCommand
7
7
  include Morpheus::Cli::CliCommand
8
8
  set_command_name :'benchmark'
9
9
 
10
- # control global benchmark toggle
11
- register_subcommands :on, :off, :on?, :off?
12
- # record your own benchmarks
13
- register_subcommands :start, :stop, :status, :exec => :execute
14
-
10
+ register_subcommand :on, "Enable global benchmarking."
11
+ register_subcommand :off, "Disable global benchmarking."
12
+ register_subcommand :on?, "Print the value of the global benchmark setting. Exit 0 if on."
13
+ register_subcommand :off?, "Print the value of the global benchmark setting. Exit 0 if off."
14
+ register_subcommand :start, "Start recording a benchmark."
15
+ register_subcommand :stop, "Stop recording a benchmark."
16
+ register_subcommand :status, "Print status of benchmark."
17
+ register_subcommand :exec, :execute, "Benchmark a specified command or expression."
15
18
 
16
19
  # this would be cool, we should store all benchmarking results in memory or on disk =o
17
20
  # register_subcommands :list, :get, :put, :remove
@@ -35,7 +38,7 @@ class Morpheus::Cli::BenchmarkCommand
35
38
  opts.banner = subcommand_usage("")
36
39
  build_common_options(opts, options, [:quiet])
37
40
  opts.footer = <<-EOT
38
- Enable global benchmarking.
41
+ #{subcommand_description}
39
42
  This behaves the same as if you were to add the -B switch to every command.
40
43
  EOT
41
44
  end
@@ -65,7 +68,7 @@ EOT
65
68
  opts.banner = subcommand_usage("")
66
69
  build_common_options(opts, options, [:quiet])
67
70
  opts.footer = <<-EOT
68
- Disable global benchmarking.
71
+ #{subcommand_description}
69
72
  The default state for this setting is off.
70
73
  EOT
71
74
  end
@@ -95,7 +98,7 @@ EOT
95
98
  opts.banner = subcommand_usage("")
96
99
  build_common_options(opts, options, [:quiet])
97
100
  opts.footer = <<-EOT
98
- Print the value of the global benchmark setting.
101
+ #{subcommand_description}
99
102
  Exit 0 if on.
100
103
  EOT
101
104
  end
@@ -121,7 +124,7 @@ EOT
121
124
  opts.banner = subcommand_usage("")
122
125
  build_common_options(opts, options, [:quiet])
123
126
  opts.footer = <<-EOT
124
- Print the value of the global benchmark setting.
127
+ #{subcommand_description}
125
128
  Exit 0 if off.
126
129
  EOT
127
130
  end
@@ -149,7 +152,7 @@ EOT
149
152
  opts.banner = subcommand_usage("[name]")
150
153
  build_common_options(opts, options, [:quiet])
151
154
  opts.footer = <<-EOT
152
- Start recording a benchmark.
155
+ #{subcommand_description}
153
156
  [name] is required. This is just a name for the routine.
154
157
  This allows you to record how long it takes to run a series of commands.
155
158
  Just run `benchmark stop` when you are finished.
@@ -194,7 +197,7 @@ EOT
194
197
  end
195
198
  build_common_options(opts, options, [:quiet])
196
199
  opts.footer = <<-EOT
197
- Stop recording a benchmark.
200
+ #{subcommand_description}
198
201
  [name] is optional. This is the name of the benchmark to stop.
199
202
  The last benchmark is used by default.
200
203
  EOT
@@ -254,7 +257,7 @@ EOT
254
257
  opts.banner = subcommand_usage("[name]")
255
258
  build_common_options(opts, options, [:quiet])
256
259
  opts.footer = <<-EOT
257
- Print status of benchmark.
260
+ #{subcommand_description}
258
261
  [name] is optional. This is the name of the benchmark to inspect.
259
262
  The last benchmark is used by default.
260
263
  EOT
@@ -309,7 +312,7 @@ EOT
309
312
  end
310
313
  build_common_options(opts, options, [:quiet])
311
314
  opts.footer = <<-EOT
312
- Benchmark a specified command.
315
+ #{subcommand_description}
313
316
  [command] is required. This is the command to execute
314
317
  EOT
315
318
  end
@@ -360,7 +363,7 @@ EOT
360
363
  else
361
364
  out << "\texit: 0 "
362
365
  end
363
- total_time_str = "#{benchmark_record.duration.round((benchmark_record.duration > 0.002) ? 3 : 6)}s"
366
+ total_time_str = "#{benchmark_record.duration.round((benchmark_record.duration > 0.002) ? 3 : 6)} s"
364
367
  out << "\t #{total_time_str.ljust(9, ' ')}"
365
368
  else
366
369
  benchmark_records = []
@@ -381,8 +384,8 @@ EOT
381
384
  # all_durations = benchmark_records.collect {|benchmark_record| benchmark_record.duration }
382
385
  # total_duration = all_durations.inject(0.0) {|acc, i| acc + i }
383
386
  # avg_duration = total_duration / all_durations.size
384
- # total_time_str = "#{total_duration.round((total_duration > 0.002) ? 3 : 6)}s"
385
- # avg_time_str = "#{avg_duration.round((total_duration > 0.002) ? 3 : 6)}s"
387
+ # total_time_str = "#{total_duration.round((total_duration > 0.002) ? 3 : 6)} s"
388
+ # avg_time_str = "#{avg_duration.round((total_duration > 0.002) ? 3 : 6)} s"
386
389
 
387
390
  all_durations = []
388
391
  stats = {total: 0, avg: nil, min: nil, max: nil}
@@ -403,10 +406,10 @@ EOT
403
406
  stats[:avg] = stats[:total].to_f / all_durations.size
404
407
  end
405
408
 
406
- total_time_str = "#{stats[:total].round((stats[:total] > 0.002) ? 3 : 6)}s"
407
- min_time_str = stats[:min] ? "#{stats[:min].round((stats[:min] > 0.002) ? 3 : 6)}s" : ""
408
- max_time_str = stats[:max] ? "#{stats[:max].round((stats[:max] > 0.002) ? 3 : 6)}s" : ""
409
- avg_time_str = stats[:avg] ? "#{stats[:avg].round((stats[:avg] > 0.002) ? 3 : 6)}s" : ""
409
+ total_time_str = "#{stats[:total].round((stats[:total] > 0.002) ? 3 : 6)} s"
410
+ min_time_str = stats[:min] ? "#{stats[:min].round((stats[:min] > 0.002) ? 3 : 6)} s" : ""
411
+ max_time_str = stats[:max] ? "#{stats[:max].round((stats[:max] > 0.002) ? 3 : 6)} s" : ""
412
+ avg_time_str = stats[:avg] ? "#{stats[:avg].round((stats[:avg] > 0.002) ? 3 : 6)} s" : ""
410
413
 
411
414
  out = ""
412
415
  # <benchmark name or command>
@@ -176,7 +176,7 @@ morpheus v#{Morpheus::Cli::VERSION}
176
176
 
177
177
  To learn more about the Morpheus Appliance, visit https://www.morpheusdata.com/features
178
178
 
179
- To learn more about the Morpheus API, visit http://bertramdev.github.io/morpheus-apidoc/
179
+ To learn more about the Morpheus API, visit https://apidocs.morpheusdata.com
180
180
 
181
181
  ## GLOBAL OPTIONS
182
182
 
@@ -519,7 +519,7 @@ class Morpheus::Cli::ContainersCommand
519
519
  options[:end] = parse_time(val) #.utc.iso8601
520
520
  end
521
521
  opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
522
- params['level'] = params['level'] ? [params['level'], val].flatten : val
522
+ params['level'] = params['level'] ? [params['level'], val].flatten : [val]
523
523
  end
524
524
  opts.on('--table', '--table', "Format ouput as a table.") do
525
525
  options[:table] = true
@@ -541,6 +541,7 @@ class Morpheus::Cli::ContainersCommand
541
541
  id_list = parse_id_list(args)
542
542
  begin
543
543
  containers = id_list # heh
544
+ params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
544
545
  params.merge!(parse_list_options(options))
545
546
  params['query'] = params.delete('phrase') if params['phrase']
546
547
  params[:order] = params[:direction] unless params[:direction].nil? # old api version expects order instead of direction
@@ -57,7 +57,7 @@ module Morpheus
57
57
  # for now, it just stores the access token without other wallet info
58
58
  begin
59
59
  # @setup_interface = Morpheus::SetupInterface.new({url:@appliance_url,access_token:@access_token})
60
- whoami_interface = Morpheus::WhoamiInterface.new({url: @appliance_url, token: options[:remote_token]})
60
+ whoami_interface = Morpheus::WhoamiInterface.new({url: @appliance_url, access_token: options[:remote_token]})
61
61
  whoami_interface.setopts(options)
62
62
  if options[:dry_run]
63
63
  print_dry_run whoami_interface.dry.get()
@@ -87,7 +87,7 @@ module Morpheus
87
87
  rescue ::RestClient::Exception => e
88
88
  #raise e
89
89
  print_red_alert "Token not valid."
90
- if options[:debug] || options[:debug]
90
+ if options[:debug]
91
91
  print_rest_exception(e, options)
92
92
  end
93
93
  wallet = nil
@@ -255,8 +255,7 @@ module Morpheus
255
255
  true
256
256
  end
257
257
 
258
- def use_refresh_token(options = {})
259
- #puts "use_refresh_token(#{options})"
258
+ def use_refresh_token(refresh_token_value, options = {})
260
259
 
261
260
  wallet = load_saved_credentials
262
261
 
@@ -265,22 +264,25 @@ module Morpheus
265
264
  return nil
266
265
  end
267
266
 
268
- if wallet['refresh_token'].nil?
269
- print_red_alert yellow,"No refresh token found for #{display_appliance(@appliance_name, @appliance_url)}",reset,"\n"
270
- return nil
267
+ if refresh_token_value.nil?
268
+ if wallet['refresh_token']
269
+ refresh_token_value = wallet['refresh_token']
270
+ else
271
+ print_red_alert yellow,"No refresh token found for #{display_appliance(@appliance_name, @appliance_url)}",reset,"\n"
272
+ return nil
273
+ end
271
274
  end
272
275
 
273
-
274
276
  username = wallet['username']
275
277
 
276
278
  begin
277
279
  auth_interface = Morpheus::AuthInterface.new({url:@appliance_url})
278
280
  auth_interface.setopts(options)
279
281
  if options[:dry_run]
280
- print_dry_run auth_interface.dry.use_refresh_token(wallet['refresh_token'])
282
+ print_dry_run auth_interface.dry.use_refresh_token(refresh_token_value)
281
283
  return nil
282
284
  end
283
- json_response = auth_interface.use_refresh_token(wallet['refresh_token'])
285
+ json_response = auth_interface.use_refresh_token(refresh_token_value)
284
286
  #wallet = json_response
285
287
  login_date = Time.now
286
288
  expire_date = nil
@@ -311,6 +313,8 @@ module Morpheus
311
313
  print_rest_exception(e, options)
312
314
  end
313
315
  wallet = nil
316
+ # return now or else it will log them out
317
+ return nil
314
318
  end
315
319
 
316
320
  # save wallet to credentials file
@@ -0,0 +1,374 @@
1
+ require 'morpheus/cli/cli_command'
2
+ require 'yaml'
3
+
4
+ class Morpheus::Cli::Deploy
5
+ include Morpheus::Cli::CliCommand
6
+ include Morpheus::Cli::DeploymentsHelper
7
+
8
+ set_command_name :deploy
9
+
10
+ def connect(opts)
11
+ @api_client = establish_remote_appliance_connection(opts)
12
+ @instances_interface = @api_client.instances
13
+ @deploy_interface = @api_client.deploy
14
+ @deployments_interface = @api_client.deployments
15
+ end
16
+
17
+ def handle(args)
18
+ options={}
19
+ optparse = Morpheus::Cli::OptionParser.new do|opts|
20
+ opts.banner = "Usage: morpheus deploy [environment]"
21
+ build_common_options(opts, options, [:auto_confirm, :remote, :dry_run])
22
+ opts.footer = <<-EOT
23
+ Deploy to an instance using the morpheus.yml file, located in the working directory.
24
+ [environment] is optional. Merge settings under environments.{environment}. Default is no environment.
25
+
26
+ First this parses the morpheus.yml file and merges the specified environment settings.
27
+ The specified instance must exist and the specified version must not exist.
28
+ If the settings are valid, the new deployment version will be created and
29
+ all the specified files are uploaded to the new deployment version.
30
+ Finally, it deploys the new version to the instance.
31
+
32
+ The morpheus.yml should be located in the working directory.
33
+ This file contains the information necessary to perform a deployment via the cli.
34
+
35
+ File Settings
36
+ ==================
37
+
38
+ * name - (required) The instance name we are deploying to and, by default, name of the deployment being created.
39
+ * version - (required) The version identifier of the deployment being created (userVersion)
40
+ * deployment - The name of the deployment being created, name is used by default
41
+ * script - The initial script to run before looking for files to upload.
42
+ * files - List of file patterns to use for uploading files and their target destination.
43
+ Each item should contain path and pattern, path may be relative to the working directory, default pattern is: '**/*'
44
+ * options - Map of deployment options depending on deployment type
45
+ * post_script - A post operation script to be run on the local machine
46
+ * stage_only - If set to true the deploy will only be staged and not actually run
47
+ * environments - Map of objects that contain nested properties for each environment name
48
+
49
+ It is possible to nest these properties in an "environments" map to override based on a passed environment.
50
+
51
+ Example
52
+ ==================
53
+
54
+ name: neatsite
55
+ version: 5.0
56
+ script: "rake build"
57
+ files:
58
+ - path: build
59
+ environments:
60
+ production:
61
+ files:
62
+ - path: production-build
63
+ EOT
64
+ end
65
+ optparse.parse!(args)
66
+ verify_args!(args:args, optparse:optparse, min:0, max:1)
67
+ options[:options]['name'] = args[0] if args[0]
68
+ connect(options)
69
+ payload = {}
70
+
71
+ environment = default_deploy_environment
72
+ if args.count > 0
73
+ environment = args[0]
74
+ end
75
+ if load_deploy_file().nil?
76
+ raise_command_error "Morpheus Deploy File `morpheus.yml` not detected. Please create one and try again."
77
+ end
78
+
79
+ # Parse and validate config, need instance + deployment + version + files
80
+ # name can be specified as a single value for both instance and deployment
81
+
82
+ deploy_args = merged_deploy_args(environment)
83
+
84
+ instance_name = deploy_args['name']
85
+ if deploy_args['instance'].is_a?(String)
86
+ instance_name = deploy_args['instance']
87
+ end
88
+ if instance_name.nil?
89
+ raise_command_error "Instance not specified. Please specify the instance name and try again."
90
+ end
91
+
92
+ deployment_name = deploy_args['name'] || instance_name
93
+ if deploy_args['deployment'].is_a?(String)
94
+ deployment_name = deploy_args['deployment']
95
+ end
96
+
97
+ version_number = deploy_args['version']
98
+ if version_number.nil?
99
+ raise_command_error "Version not specified. Please specify the version and try again."
100
+ end
101
+
102
+ instance_results = @instances_interface.list(name: instance_name)
103
+ if instance_results['instances'].empty?
104
+ raise_command_error "Instance not found by name '#{instance_name}'"
105
+ end
106
+ instance = instance_results['instances'][0]
107
+ instance_id = instance['id']
108
+
109
+ # ok do it
110
+ # fetch/create deployment, create deployment version, upload files, and deploy it to instance.
111
+
112
+ print_h1 "Morpheus Deployment"
113
+
114
+ columns = {
115
+ "Instance" => :name,
116
+ "Deployment" => :deployment,
117
+ "Version" => :version,
118
+ "Script" => :script,
119
+ "Post Script" => :post_script,
120
+ "Files" => :files,
121
+ "Environment" => :environment,
122
+ }
123
+ pretty_file_config = deploy_args['files'].collect {|it|
124
+ [(it['path'] ? "path: #{it['path']}" : nil), (it['pattern'] ? "pattern: #{it['pattern']}" : nil)].compact.join(", ")
125
+ }.join(", ")
126
+ deploy_settings = {
127
+ :name => instance_name,
128
+ :deployment => deployment_name,
129
+ :version => version_number,
130
+ :script => deploy_args['script'],
131
+ :post_script => deploy_args['post_script'],
132
+ :files => pretty_file_config,
133
+ # :files => deploy_args['files'],
134
+ # :files => deploy_files.size,
135
+ # :file_config => (deploy_files.size == 1 ? deploy_files[0][:destination] : deploy_args['files'])
136
+ :environment => environment
137
+ }
138
+ columns.delete("Script") if deploy_settings[:script].nil?
139
+ columns.delete("Post Script") if deploy_settings[:post_script].nil?
140
+ columns.delete("Environment") if deploy_settings[:environment].nil?
141
+ print_description_list(columns, deploy_settings)
142
+ print reset, "\n"
143
+
144
+ if !deploy_args['script'].nil?
145
+ # do this for dry run too since this is usually what creates the files to be uploaded
146
+ print cyan, "Executing Pre Deploy Script...", reset, "\n"
147
+ puts "running command: #{deploy_args['script']}"
148
+ if !system(deploy_args['script'])
149
+ raise_command_error "Error executing pre script..."
150
+ end
151
+ end
152
+
153
+ # Find Files to Upload
154
+ deploy_files = []
155
+ if deploy_args['files'].nil? || deploy_args['files'].empty? || !deploy_args['files'].is_a?(Array)
156
+ raise_command_error "Files not specified. Please specify files array, each item may specify a path or pattern of file(s) to upload"
157
+ else
158
+ #print "\n",cyan, "Finding Files...", reset, "\n"
159
+ current_working_dir = Dir.pwd
160
+ deploy_args['files'].each do |fmap|
161
+ Dir.chdir(fmap['path'] || current_working_dir)
162
+ files = Dir.glob(fmap['pattern'] || '**/*')
163
+ files.each do |file|
164
+ if File.file?(file)
165
+ destination = file.split("/")[0..-2].join("/")
166
+ # deploy_files << {filepath: File.expand_path(file), destination: destination}
167
+ deploy_files << {filepath: File.expand_path(file), destination: file}
168
+ end
169
+ end
170
+ end
171
+ #print cyan, "Found #{deploy_files.size} Files to Upload!", reset, "\n"
172
+ Dir.chdir(current_working_dir)
173
+ end
174
+
175
+ if deploy_files.empty?
176
+ raise_command_error "0 files found for: #{deploy_args['files'].inspect}"
177
+ else
178
+ print cyan, "Found #{deploy_files.size} Files to Upload!", reset, "\n"
179
+ end
180
+
181
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to create deployment version #{version_number} (#{deploy_files.size} #{deploy_files.size == 1 ? 'file' : 'files'}) and deploy it to instance #{instance['name']}?")
182
+ return 9, "aborted command"
183
+ end
184
+
185
+ # Find or Create Deployment
186
+ deployment = nil
187
+ deployments = @deployments_interface.list(name: deployment_name)['deployments']
188
+
189
+ @instances_interface.setopts(options)
190
+ @deploy_interface.setopts(options)
191
+ @deployments_interface.setopts(options)
192
+
193
+ if deployments.size > 1
194
+ raise_command_error "#{deployments.size} deployment versions found by deployment '#{name}'"
195
+ elsif deployments.size == 1
196
+ deployment = deployments[0]
197
+ # should update here, eg description
198
+ else
199
+ # create it
200
+ payload = {
201
+ 'deployment' => {
202
+ 'name' => deployment_name
203
+ }
204
+ }
205
+ payload['deployment']['description'] = deploy_args['description'] if deploy_args['description']
206
+
207
+ if options[:dry_run]
208
+ print_dry_run @deployments_interface.dry.create(payload)
209
+ # return 0, nil
210
+ deployment = {'id' => ':deploymentId', 'name' => deployment_name}
211
+ else
212
+ json_response = @deployments_interface.create(payload)
213
+ deployment = json_response['deployment']
214
+ end
215
+ end
216
+
217
+ # Find or Create Deployment Version
218
+ # Actually, for now this this errors if the version already exists, but it should update it.
219
+
220
+ @deployments_interface = @api_client.deployments
221
+ deployment_version = nil
222
+ if options[:dry_run]
223
+ print_dry_run @deployments_interface.dry.list_versions(deployment['id'], {userVersion: version_number})
224
+ # return 0, nil
225
+ #deployment_versions =[{'id' => ':versionId', 'version' => version_number}]
226
+ deployment_versions = []
227
+ else
228
+ deployment_versions = @deployments_interface.list_versions(deployment['id'], {userVersion: version_number})['versions']
229
+ @deployments_interface.setopts(options)
230
+ end
231
+
232
+
233
+ if deployment_versions.size > 0
234
+ raise_command_error "Deployment '#{deployment['name']}' version '#{version_number}' already exists. Specify a new version or delete the existing version."
235
+ # if deployment_versions.size > 1
236
+ # raise_command_error "#{deployment_versions.size} versions found by version '#{name}'"
237
+ # elsif deployment_versions.size == 1
238
+ # deployment_version = deployment_versions[0]
239
+ # # should update here, eg description
240
+ else
241
+ # create it
242
+ payload = {
243
+ 'version' => {
244
+ 'userVersion' => version_number,
245
+ 'deployType' => (deploy_args['type'] || deploy_args['deployType'] || 'file')
246
+ }
247
+ }
248
+ payload['version']['fetchUrl'] = deploy_args['fetchUrl'] if deploy_args['fetchUrl']
249
+ payload['version']['gitUrl'] = deploy_args['gitUrl'] if deploy_args['gitUrl']
250
+ payload['version']['gitRef'] = deploy_args['gitRef'] if deploy_args['gitRef']
251
+
252
+ if options[:dry_run]
253
+ print_dry_run @deployments_interface.dry.create_version(deployment['id'], payload)
254
+ # return 0, nil
255
+ deployment_version = {'id' => ':versionId', 'version' => version_number}
256
+ else
257
+ json_response = @deployments_interface.create_version(deployment['id'], payload)
258
+ deployment_version = json_response['version']
259
+ end
260
+ end
261
+
262
+
263
+ # Upload Files
264
+ if deploy_files && !deploy_files.empty?
265
+ print "\n",cyan, "Uploading #{deploy_files.size} Files...", reset, "\n"
266
+ current_working_dir = Dir.pwd
267
+ deploy_files.each do |f|
268
+ destination = f[:destination]
269
+ if options[:dry_run]
270
+ print_dry_run @deployments_interface.upload_file(deployment['id'], deployment_version['id'], f[:filepath], f[:destination])
271
+ else
272
+ print cyan," - Uploading #{f[:destination]} ...", reset if !options[:quiet]
273
+ upload_result = @deployments_interface.upload_file(deployment['id'], deployment_version['id'], f[:filepath], f[:destination])
274
+ #print green + "SUCCESS" + reset + "\n" if !options[:quiet]
275
+ print reset, "\n" if !options[:quiet]
276
+ end
277
+ end
278
+ print cyan, "Upload Complete!", reset, "\n"
279
+ Dir.chdir(current_working_dir)
280
+ else
281
+ print "\n",cyan, "0 files to upload", reset, "\n"
282
+ end
283
+
284
+ # TODO: support deploying other deployTypes too, git and fetch
285
+
286
+ if !deploy_args['post_script'].nil?
287
+ print cyan, "Executing Post Script...", reset, "\n"
288
+ puts "running command: #{deploy_args['post_script']}"
289
+ if !system(deploy_args['post_script'])
290
+ raise_command_error "Error executing post script..."
291
+ end
292
+ end
293
+
294
+ # JD: restart for evars eh?
295
+ if deploy_args['env']
296
+ evars = []
297
+ deploy_args['env'].each_pair do |key, value|
298
+ evars << {name: key, value: value, export: false}
299
+ end
300
+ payload = {envs: evars}
301
+ if options[:dry_run]
302
+ print_dry_run @instances_interface.dry.create_env(instance_id, payload)
303
+ print_dry_run @instances_interface.dry.restart(instance_id)
304
+ else
305
+ @instances_interface.create_env(instance_id, payload)
306
+ @instances_interface.restart(instance_id)
307
+ end
308
+ end
309
+ # Create the AppDeploy, this does the deploy async (as of 4.2.2-3)
310
+ payload = {'appDeploy' => {} }
311
+ payload['appDeploy']['versionId'] = deployment_version['id']
312
+ if deploy_args['options']
313
+ payload['appDeploy']['config'] = deploy_args['options']
314
+ end
315
+ # stageOnly means do not actually deploy yet, can invoke @deploy_interface.deploy(deployment['id']) later
316
+ # there is no cli command for that yet though..
317
+ stage_only = deploy_args['stage_deploy'] || deploy_args['stage_only'] || deploy_args['stageOnly']
318
+ if stage_only
319
+ payload['appDeploy']['stageOnly'] = true
320
+ end
321
+ app_deploy_id = nil
322
+ if options[:dry_run]
323
+ print_dry_run @deploy_interface.dry.create(instance_id, payload)
324
+ # return 0, nil
325
+ app_deploy_id = ':appDeployId'
326
+ else
327
+ # Create a new appDeploy record, without stageOnly, this actually does the deployment
328
+ print cyan, "Deploying #{deployment_name} version #{version_number} to instance #{instance_name} ...", reset, "\n"
329
+ deploy_result = @deploy_interface.create(instance_id, payload)
330
+ app_deploy = deploy_result['appDeploy']
331
+ app_deploy_id = app_deploy['id']
332
+ print_green_success "Deploy Successful!"
333
+ end
334
+ return 0, nil
335
+ end
336
+
337
+ protected
338
+
339
+ # Loads a morpheus.yml file from within the current working directory.
340
+ # This file contains information necessary to perform a deployment via the cli.
341
+ #
342
+ # === Example File Attributes
343
+ # * +script+ - The initial script to run before uploading files
344
+ # * +name+ - The instance name we are deploying to (can be overridden in CLI)
345
+ # * +files+ - List of file patterns to use for uploading files and their target destination
346
+ # * +options+ - Map of deployment options depending on deployment type
347
+ # * +post_script+ - A post operation script to be run on the local machine
348
+ # * +stage_deploy+ - If set to true the deploy will only be staged and not actually run
349
+ #
350
+ # +NOTE: + It is also possible to nest these properties in an "environments" map to override based on a passed environment deploy name
351
+ #
352
+ def load_deploy_file
353
+ if !File.exist? "morpheus.yml"
354
+ puts "No morpheus.yml file detected in the current directory. Nothing to do."
355
+ return nil
356
+ end
357
+
358
+ @deploy_file = YAML.load_file("morpheus.yml")
359
+ return @deploy_file
360
+ end
361
+
362
+ def merged_deploy_args(environment)
363
+ deploy_args = @deploy_file.reject { |key,value| key == 'environment'}
364
+ if environment && !@deploy_file['environment'].nil? && !@deploy_file['environment'][environment].nil?
365
+ deploy_args = deploy_args.merge(@deploy_file['environment'][environment])
366
+ end
367
+ return deploy_args
368
+ end
369
+
370
+ def default_deploy_environment
371
+ nil
372
+ end
373
+
374
+ end