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.
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|