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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/bin/morpheus +5 -96
  3. data/lib/morpheus/api/api_client.rb +23 -1
  4. data/lib/morpheus/api/checks_interface.rb +106 -0
  5. data/lib/morpheus/api/incidents_interface.rb +102 -0
  6. data/lib/morpheus/api/monitoring_apps_interface.rb +47 -0
  7. data/lib/morpheus/api/monitoring_contacts_interface.rb +47 -0
  8. data/lib/morpheus/api/monitoring_groups_interface.rb +47 -0
  9. data/lib/morpheus/api/monitoring_interface.rb +36 -0
  10. data/lib/morpheus/api/option_type_lists_interface.rb +47 -0
  11. data/lib/morpheus/api/option_types_interface.rb +47 -0
  12. data/lib/morpheus/api/roles_interface.rb +0 -1
  13. data/lib/morpheus/api/setup_interface.rb +19 -9
  14. data/lib/morpheus/cli.rb +20 -1
  15. data/lib/morpheus/cli/accounts.rb +8 -3
  16. data/lib/morpheus/cli/app_templates.rb +19 -11
  17. data/lib/morpheus/cli/apps.rb +52 -37
  18. data/lib/morpheus/cli/cli_command.rb +229 -53
  19. data/lib/morpheus/cli/cli_registry.rb +48 -40
  20. data/lib/morpheus/cli/clouds.rb +55 -26
  21. data/lib/morpheus/cli/command_error.rb +12 -0
  22. data/lib/morpheus/cli/credentials.rb +68 -26
  23. data/lib/morpheus/cli/curl_command.rb +98 -0
  24. data/lib/morpheus/cli/dashboard_command.rb +2 -7
  25. data/lib/morpheus/cli/deployments.rb +4 -4
  26. data/lib/morpheus/cli/deploys.rb +1 -2
  27. data/lib/morpheus/cli/dot_file.rb +5 -8
  28. data/lib/morpheus/cli/error_handler.rb +179 -15
  29. data/lib/morpheus/cli/groups.rb +21 -13
  30. data/lib/morpheus/cli/hosts.rb +220 -110
  31. data/lib/morpheus/cli/instance_types.rb +2 -2
  32. data/lib/morpheus/cli/instances.rb +257 -167
  33. data/lib/morpheus/cli/key_pairs.rb +15 -9
  34. data/lib/morpheus/cli/library.rb +673 -27
  35. data/lib/morpheus/cli/license.rb +2 -2
  36. data/lib/morpheus/cli/load_balancers.rb +4 -4
  37. data/lib/morpheus/cli/log_level_command.rb +6 -4
  38. data/lib/morpheus/cli/login.rb +17 -3
  39. data/lib/morpheus/cli/logout.rb +25 -11
  40. data/lib/morpheus/cli/man_command.rb +388 -0
  41. data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -1
  42. data/lib/morpheus/cli/mixins/monitoring_helper.rb +434 -0
  43. data/lib/morpheus/cli/mixins/print_helper.rb +620 -112
  44. data/lib/morpheus/cli/mixins/provisioning_helper.rb +1 -1
  45. data/lib/morpheus/cli/monitoring_apps_command.rb +29 -0
  46. data/lib/morpheus/cli/monitoring_checks_command.rb +427 -0
  47. data/lib/morpheus/cli/monitoring_contacts_command.rb +373 -0
  48. data/lib/morpheus/cli/monitoring_groups_command.rb +29 -0
  49. data/lib/morpheus/cli/monitoring_incidents_command.rb +711 -0
  50. data/lib/morpheus/cli/option_types.rb +10 -1
  51. data/lib/morpheus/cli/recent_activity_command.rb +2 -5
  52. data/lib/morpheus/cli/remote.rb +874 -134
  53. data/lib/morpheus/cli/roles.rb +54 -27
  54. data/lib/morpheus/cli/security_group_rules.rb +2 -2
  55. data/lib/morpheus/cli/security_groups.rb +23 -19
  56. data/lib/morpheus/cli/set_prompt_command.rb +50 -0
  57. data/lib/morpheus/cli/shell.rb +222 -157
  58. data/lib/morpheus/cli/tasks.rb +19 -15
  59. data/lib/morpheus/cli/users.rb +27 -17
  60. data/lib/morpheus/cli/version.rb +1 -1
  61. data/lib/morpheus/cli/virtual_images.rb +28 -13
  62. data/lib/morpheus/cli/whoami.rb +131 -52
  63. data/lib/morpheus/cli/workflows.rb +24 -9
  64. data/lib/morpheus/formatters.rb +195 -3
  65. data/lib/morpheus/logging.rb +86 -0
  66. data/lib/morpheus/terminal.rb +371 -0
  67. data/scripts/generate_morpheus_commands_help.morpheus +60 -0
  68. metadata +21 -2
@@ -72,9 +72,20 @@ class Morpheus::Cli::Clouds
72
72
  print "\n"
73
73
  else
74
74
  clouds = json_response['zones']
75
- print "\n" ,cyan, bold, "Morpheus Clouds\n","==================", reset, "\n\n"
75
+ title = "Morpheus Clouds"
76
+ subtitles = []
77
+ if group
78
+ subtitles << "Group: #{group['name']}".strip
79
+ end
80
+ if cloud_type
81
+ subtitles << "Type: #{cloud_type['name']}".strip
82
+ end
83
+ if params[:phrase]
84
+ subtitles << "Search: #{params[:phrase]}".strip
85
+ end
86
+ print_h1 title, subtitles
76
87
  if clouds.empty?
77
- puts yellow,"No clouds found.",reset
88
+ print cyan,"No clouds found.",reset,"\n"
78
89
  else
79
90
  print_clouds_table(clouds)
80
91
  print_results_pagination(json_response)
@@ -114,26 +125,29 @@ class Morpheus::Cli::Clouds
114
125
  return
115
126
  end
116
127
  cloud_type = cloud_type_for_id(cloud['zoneTypeId'])
117
- print "\n" ,cyan, bold, "Cloud Details\n","==================", reset, "\n\n"
128
+ print_h1 "Cloud Details"
118
129
  print cyan
119
- puts "ID: #{cloud['id']}"
120
- puts "Name: #{cloud['name']}"
121
- puts "Type: #{cloud_type ? cloud_type['name'] : ''}"
122
- puts "Code: #{cloud['code']}"
123
- puts "Location: #{cloud['location']}"
124
- puts "Visibility: #{cloud['visibility'].to_s.capitalize}"
125
- puts "Groups: #{cloud['groups'].collect {|it| it.instance_of?(Hash) ? it['name'] : it.to_s }.join(', ')}"
126
- status = nil
127
- if cloud['status'] == 'ok'
128
- status = "#{green}OK#{cyan}"
129
- elsif cloud['status'].nil?
130
- status = "#{white}UNKNOWN#{cyan}"
131
- else
132
- status = "#{red}#{cloud['status'] ? cloud['status'].upcase : 'N/A'}#{cloud['statusMessage'] ? "#{cyan} - #{cloud['statusMessage']}" : ''}#{cyan}"
133
- end
134
- puts "Status: #{status}"
135
-
136
- print "\n" ,cyan, "Cloud Servers (#{cloud['serverCount']})\n","==================", reset, "\n\n"
130
+ description_cols = {
131
+ "ID" => 'id',
132
+ "Name" => 'name',
133
+ "Type" => lambda {|it| cloud_type ? cloud_type['name'] : '' },
134
+ "Code" => 'code',
135
+ "Location" => 'location',
136
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
137
+ "Groups" => lambda {|it| it['groups'].collect {|g| g.instance_of?(Hash) ? g['name'] : g.to_s }.join(', ') },
138
+ "Status" => lambda {|it| format_cloud_status(it) }
139
+ }
140
+ print_description_list(description_cols, cloud)
141
+ # puts "ID: #{cloud['id']}"
142
+ # puts "Name: #{cloud['name']}"
143
+ # puts "Type: #{cloud_type ? cloud_type['name'] : ''}"
144
+ # puts "Code: #{cloud['code']}"
145
+ # puts "Location: #{cloud['location']}"
146
+ # puts "Visibility: #{cloud['visibility'].to_s.capitalize}"
147
+ # puts "Groups: #{cloud['groups'].collect {|it| it.instance_of?(Hash) ? it['name'] : it.to_s }.join(', ')}"
148
+ # puts "Status: #{format_cloud_status(cloud)}"
149
+
150
+ print_h2 "Cloud Servers"
137
151
  print cyan
138
152
  if server_counts
139
153
  print "Container Hosts: #{server_counts['containerHost']}".center(20)
@@ -439,11 +453,13 @@ class Morpheus::Cli::Clouds
439
453
  return
440
454
  end
441
455
  securityGroups = json_response['securityGroups']
442
- print "\n" ,cyan, bold, "Morpheus Security Groups for Cloud: #{cloud['name']}\n","==================", reset, "\n\n"
443
- print cyan, "Firewall Enabled=#{json_response['firewallEnabled']}\n\n"
456
+ print_h1 "Morpheus Security Groups for Cloud: #{cloud['name']}"
457
+ print cyan
458
+ print_description_list({"Firewall Enabled" => lambda {|it| format_boolean it['firewallEnabled'] } }, json_response)
444
459
  if securityGroups.empty?
445
- puts yellow,"No security groups currently applied.",reset
460
+ print yellow,"\n","No security groups currently applied.",reset,"\n"
446
461
  else
462
+ print "\n"
447
463
  securityGroups.each do |securityGroup|
448
464
  print cyan, "= #{securityGroup['id']} (#{securityGroup['name']}) - (#{securityGroup['description']})\n"
449
465
  end
@@ -515,9 +531,9 @@ class Morpheus::Cli::Clouds
515
531
  print JSON.pretty_generate({zoneTypes: cloud_types})
516
532
  print "\n"
517
533
  else
518
- print "\n" ,cyan, bold, "Morpheus Cloud Types\n","==================", reset, "\n\n"
534
+ print_h1 "Morpheus Cloud Types"
519
535
  if cloud_types.empty?
520
- puts yellow,"No cloud types found.",reset
536
+ print yellow,"No instances found.",reset,"\n"
521
537
  else
522
538
  print cyan
523
539
  cloud_types = cloud_types.select {|it| it['enabled'] }
@@ -610,4 +626,17 @@ class Morpheus::Cli::Clouds
610
626
  get_available_cloud_types().select {|it| it['enabled'] }.collect {|it| {'name' => it['name'], 'value' => it['code']} }
611
627
  end
612
628
 
629
+ def format_cloud_status(cloud, return_color=cyan)
630
+ out = ""
631
+ status_string = cloud['status']
632
+ if status_string.nil? || status_string.empty? || status_string == "unknown"
633
+ out << "#{white}UNKNOWN#{return_color}"
634
+ elsif status_string == 'ok'
635
+ out << "#{green}#{status_string.upcase}#{return_color}"
636
+ else
637
+ out << "#{red}#{status_string ? status_string.upcase : 'N/A'}#{cloud['statusMessage'] ? "#{return_color} - #{cloud['statusMessage']}" : ''}#{return_color}"
638
+ end
639
+ out
640
+ end
641
+
613
642
  end
@@ -0,0 +1,12 @@
1
+ # A standard error to raise in your CliCommand classes.
2
+ class Morpheus::Cli::CommandError < StandardError
3
+
4
+ # attr_reader :args, :options
5
+
6
+ # def initialize(msg, args=[], options={})
7
+ # @args = args
8
+ # @options = options
9
+ # super(msg)
10
+ # end
11
+
12
+ end
@@ -11,18 +11,18 @@ module Morpheus
11
11
  class Credentials
12
12
  include Morpheus::Cli::PrintHelper
13
13
 
14
- @@saved_credentials_map = nil
14
+ @@appliance_credentials_map = nil
15
15
 
16
16
  def initialize(appliance_name, appliance_url)
17
- @appliance_url = appliance_url
18
17
  @appliance_name = appliance_name
18
+ @appliance_url = appliance_url
19
19
  end
20
20
 
21
21
  def request_credentials(opts = {})
22
22
  #puts "request_credentials(#{opts})"
23
23
  username = nil
24
24
  password = nil
25
- creds = nil
25
+ access_token = nil
26
26
  skip_save = false
27
27
  # We should return an access Key for Morpheus CLI Here
28
28
  if !opts[:remote_username].nil?
@@ -30,10 +30,10 @@ module Morpheus
30
30
  password = opts[:remote_password]
31
31
  skip_save = opts[:remote_url] ? true : false
32
32
  else
33
- creds = load_saved_credentials
33
+ access_token = load_saved_credentials
34
34
  end
35
- if creds
36
- return creds
35
+ if access_token
36
+ return access_token
37
37
  end
38
38
  unless opts[:quiet] || opts[:no_prompt]
39
39
  # if username.empty? || password.empty?
@@ -65,14 +65,14 @@ module Morpheus
65
65
  print reset, "\n"
66
66
  end
67
67
  access_token = json_response['access_token']
68
- if !access_token.empty?
68
+ if access_token && access_token != ""
69
69
  unless skip_save
70
70
  save_credentials(@appliance_name, access_token)
71
71
  end
72
- return access_token
72
+ # return access_token
73
73
  else
74
74
  print_red_alert "Credentials not verified."
75
- return nil
75
+ # return nil
76
76
  end
77
77
  rescue ::RestClient::Exception => e
78
78
  #raise e
@@ -86,8 +86,40 @@ module Morpheus
86
86
  else
87
87
  print_rest_exception(e, opts)
88
88
  end
89
+ access_token = nil
90
+ end
91
+
92
+
93
+ unless skip_save
94
+ begin
95
+ # save pertinent session info to the appliance
96
+ now = Time.now.to_i
97
+ appliance = ::Morpheus::Cli::Remote.load_remote(@appliance_name)
98
+ if appliance
99
+ if access_token
100
+ appliance = ::Morpheus::Cli::Remote.load_remote(@appliance_name)
101
+ appliance[:authenticated] = true
102
+ appliance[:username] = username
103
+ appliance[:status] = "ready"
104
+ appliance[:last_login_at] = now
105
+ appliance[:last_success_at] = now
106
+ ::Morpheus::Cli::Remote.save_remote(@appliance_name, appliance)
107
+ else
108
+ now = Time.now.to_i
109
+ appliance = ::Morpheus::Cli::Remote.load_remote(@appliance_name)
110
+ appliance[:authenticated] = false
111
+ #appliance[:username] = username
112
+ #appliance[:last_login_at] = now
113
+ #appliance[:error] = "Credentials not verified"
114
+ ::Morpheus::Cli::Remote.save_remote(@appliance_name, appliance)
115
+ end
116
+ end
117
+ rescue => e
118
+ #puts "failed to update remote appliance config: (#{e.class}) #{e.message}"
119
+ end
89
120
  end
90
121
 
122
+ return access_token
91
123
  end
92
124
 
93
125
  def login(opts = {})
@@ -97,35 +129,45 @@ module Morpheus
97
129
 
98
130
  def logout()
99
131
  clear_saved_credentials(@appliance_name)
132
+ # save pertinent session info to the appliance
133
+ appliance = ::Morpheus::Cli::Remote.load_remote(@appliance_name)
134
+ if appliance
135
+ appliance.delete(:username) # could leave this...
136
+ appliance[:authenticated] = false
137
+ appliance[:last_logout_at] = Time.now.to_i
138
+ ::Morpheus::Cli::Remote.save_remote(@appliance_name, appliance)
139
+ end
140
+ true
100
141
  end
101
142
 
102
143
  def clear_saved_credentials(appliance_name)
103
- @@saved_credentials_map = load_credentials_file || {}
104
- @@saved_credentials_map.delete(appliance_name)
105
- print "#{dark} #=> updating credentials file #{credentials_file_path}#{reset}\n" if Morpheus::Logging.debug?
106
- File.open(credentials_file_path, 'w') {|f| f.write @@saved_credentials_map.to_yaml } #Store
144
+ @@appliance_credentials_map = load_credentials_file || {}
145
+ @@appliance_credentials_map.delete(appliance_name)
146
+ Morpheus::Logging::DarkPrinter.puts "clearing credentials for #{appliance_name} from file #{credentials_file_path}" if Morpheus::Logging.debug?
147
+ File.open(credentials_file_path, 'w') {|f| f.write @@appliance_credentials_map.to_yaml } #Store
107
148
  end
108
149
 
109
150
  def load_saved_credentials(reload=false)
110
- if saved_credentials_map && !reload
111
- return saved_credentials_map
151
+ if saved_credentials && !reload
152
+ return saved_credentials
112
153
  end
113
- @@saved_credentials_map = load_credentials_file || {}
114
- return @@saved_credentials_map[@appliance_name]
154
+ #Morpheus::Logging::DarkPrinter.puts "loading credentials for #{appliance_name}" if Morpheus::Logging.debug?
155
+ @@appliance_credentials_map = load_credentials_file || {}
156
+ return @@appliance_credentials_map[@appliance_name]
115
157
  end
116
158
 
117
159
  # Provides the current credential information, simply :appliance_name => "access_token"
118
- def saved_credentials_map
119
- if !defined?(@@saved_credentials_map)
120
- @@saved_credentials_map = load_credentials_file
160
+ def saved_credentials
161
+ if !defined?(@@appliance_credentials_map)
162
+ @@appliance_credentials_map = load_credentials_file
121
163
  end
122
- return @@saved_credentials_map ? @@saved_credentials_map[@appliance_name.to_sym] : nil
164
+ return @@appliance_credentials_map ? @@appliance_credentials_map[@appliance_name.to_sym] : nil
123
165
  end
124
166
 
125
167
  def load_credentials_file
126
168
  fn = credentials_file_path
127
169
  if File.exist? fn
128
- print "#{dark} #=> loading credentials file #{fn}#{reset}\n" if Morpheus::Logging.debug?
170
+ Morpheus::Logging::DarkPrinter.puts "loading credentials file #{fn}" if Morpheus::Logging.debug?
129
171
  return YAML.load_file(fn)
130
172
  else
131
173
  return nil
@@ -136,20 +178,20 @@ module Morpheus
136
178
  File.join(Morpheus::Cli.home_directory, "credentials")
137
179
  end
138
180
 
139
- def save_credentials(app_name, token)
140
- # credential_map = saved_credentials_map
181
+ def save_credentials(appliance_name, token)
182
+ # credential_map = appliance_credentials_map
141
183
  # reloading file is better for now, otherwise you can lose credentials with multiple shells.
142
184
  credential_map = load_credentials_file || {}
143
185
  if credential_map.nil?
144
186
  credential_map = {}
145
187
  end
146
- credential_map[app_name] = token
188
+ credential_map[appliance_name] = token
147
189
  begin
148
190
  fn = credentials_file_path
149
191
  if !Dir.exists?(File.dirname(fn))
150
192
  FileUtils.mkdir_p(File.dirname(fn))
151
193
  end
152
- print "#{dark} #=> adding credentials to #{fn}#{reset}\n" if Morpheus::Logging.debug?
194
+ Morpheus::Logging::DarkPrinter.puts "adding credentials for #{appliance_name} to #{fn}" if Morpheus::Logging.debug?
153
195
  File.open(fn, 'w') {|f| f.write credential_map.to_yaml } #Store
154
196
  FileUtils.chmod(0600, fn)
155
197
  rescue => e
@@ -0,0 +1,98 @@
1
+ require 'optparse'
2
+ require 'morpheus/logging'
3
+ require 'morpheus/cli/cli_command'
4
+
5
+ class Morpheus::Cli::CurlCommand
6
+ include Morpheus::Cli::CliCommand
7
+ set_command_name :curl
8
+ set_command_hidden
9
+
10
+ def handle(args)
11
+ split_args = args.join(" ").split(" -- ")
12
+ args = split_args[0].split(" ")
13
+ curl_args = split_args[1] ? split_args[1].split(" ") : []
14
+ # puts "args is : #{args}"
15
+ # puts "curl_args is : #{curl_args}"
16
+ options = {}
17
+ optparse = Morpheus::Cli::OptionParser.new do|opts|
18
+ opts.banner = "Usage: morpheus curl [path] -- [*args]"
19
+ build_common_options(opts, options, [:remote])
20
+ opts.footer = <<-EOT
21
+ This invokes the `curl` command with url "appliance_url/api/$0
22
+ and includes the authorization header -H "Authorization: Bearer access_token"
23
+ Arguments for the curl command should be passed after ' -- '
24
+ Example: morpheus curl "/api/servers/1" -- -XGET -sV
25
+
26
+ EOT
27
+ end
28
+ optparse.parse!(args)
29
+ if args.count < 1
30
+ puts optparse
31
+ return false
32
+ end
33
+
34
+ if !command_available?("curl")
35
+ print "#{red}The 'curl' command is not available on your system.#{reset}\n"
36
+ return false
37
+ end
38
+
39
+ @api_client = establish_remote_appliance_connection(options.merge({:no_prompt => true, :skip_verify_access_token => true}))
40
+
41
+ if !@appliance_name
42
+ print yellow,"Please specify a Morpheus Appliance with -r or see the command `remote use`#{reset}\n"
43
+ return false
44
+ end
45
+
46
+ # curry --insecure to curl
47
+ if options[:insecure] || !Morpheus::RestClient.ssl_verification_enabled?
48
+ #curl_args.unshift "-k"
49
+ curl_args.unshift "--inescure"
50
+ end
51
+
52
+ creds = Morpheus::Cli::Credentials.new(@appliance_name, @appliance_url).load_saved_credentials()
53
+ if !creds
54
+ print yellow,"You are not currently logged in to #{display_appliance(@appliance_name, @appliance_url)}",reset,"\n"
55
+ print yellow,"Use the 'login' command.",reset,"\n"
56
+ return 0
57
+ end
58
+
59
+ if !@appliance_url
60
+ raise "Unable to determine remote appliance url"
61
+ print "#{red}Unable to determine remote appliance url.#{reset}\n"
62
+ return false
63
+ end
64
+
65
+ # determine curl url
66
+ base_url = @appliance_url.chomp("/")
67
+ api_path = args[0].sub(/^\//, "")
68
+ url = "#{base_url}/#{api_path}"
69
+ curl_cmd = "curl \"#{url}\""
70
+ curl_cmd << " -H \"Authorization: Bearer #{@access_token}\""
71
+ if !curl_args.empty?
72
+ curl_cmd << " " + curl_args.join(' ')
73
+ end
74
+
75
+ # Morpheus::Logging::DarkPrinter.puts "#{curl_cmd}" if Morpheus::Logging.debug?
76
+ print cyan
77
+ print "#{cyan}#{curl_cmd}#{reset}"
78
+ print "\n\n"
79
+ print reset
80
+ # print result
81
+ curl_output = `#{curl_cmd}`
82
+ puts curl_output
83
+ return $?.success?
84
+
85
+ end
86
+
87
+ def command_available?(cmd)
88
+ has_it = false
89
+ begin
90
+ system("which #{cmd} > /dev/null 2>&1")
91
+ has_it = $?.success?
92
+ rescue => e
93
+ raise e
94
+ end
95
+ return has_it
96
+ end
97
+
98
+ end
@@ -45,14 +45,9 @@ class Morpheus::Cli::DashboardCommand
45
45
  print "\n"
46
46
  else
47
47
 
48
- # todo: impersonate command and show that info here
49
-
50
- print "\n" ,cyan, bold, "Dashboard\n","==================", reset, "\n\n"
48
+ print_h1 "Dashboard"
51
49
  print cyan
52
- print "\n"
53
- puts "Coming soon.... see --json"
54
- print "\n"
55
-
50
+ puts "Coming soon... see --json"
56
51
  print reset,"\n"
57
52
 
58
53
  end
@@ -44,9 +44,9 @@ class Morpheus::Cli::Deployments
44
44
  puts JSON.pretty_generate(json_response)
45
45
  else
46
46
  deployments = json_response['deployments']
47
- print "\n" ,cyan, bold, "Morpheus Deployments\n","====================", reset, "\n\n"
47
+ print_h1 "Morpheus Deployments"
48
48
  if deployments.empty?
49
- puts yellow,"No deployments currently configured.",reset
49
+ print yellow,"No deployments currently configured.",reset,"\n"
50
50
  else
51
51
  print cyan
52
52
  deployments_table_data = deployments.collect do |deployment|
@@ -92,9 +92,9 @@ class Morpheus::Cli::Deployments
92
92
  puts JSON.pretty_generate(json_response)
93
93
  else
94
94
  versions = json_response['versions']
95
- print "\n" ,cyan, bold, "Morpheus Deployment Versions\n","=============================", reset, "\n\n"
95
+ print_h1 "Deployment Versions: #{deployment['name']}"
96
96
  if versions.empty?
97
- puts yellow,"No deployment versions currently exist.",reset
97
+ print yellow,"No deployment versions currently exist.",reset,"\n"
98
98
  else
99
99
  print cyan
100
100
  versions_table_data = versions.collect do |version|