rhc 1.2.7 → 1.3.8

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 (97) hide show
  1. data/bin/rhc +6 -8
  2. data/bin/rhc-chk +23 -10
  3. data/features/domain.feature +1 -1
  4. data/features/lib/rhc_helper.rb +3 -2
  5. data/features/lib/rhc_helper/api.rb +7 -0
  6. data/features/lib/rhc_helper/app.rb +8 -10
  7. data/features/lib/rhc_helper/domain.rb +2 -1
  8. data/features/lib/rhc_helper/runnable.rb +2 -24
  9. data/features/sshkey.feature +3 -3
  10. data/features/step_definitions/cartridge_steps.rb +6 -6
  11. data/features/step_definitions/client_steps.rb +0 -1
  12. data/features/step_definitions/sshkey_steps.rb +2 -2
  13. data/features/support/before_hooks.rb +0 -1
  14. data/features/support/env.rb +5 -3
  15. data/lib/rhc-common.rb +1 -1
  16. data/lib/rhc.rb +9 -8
  17. data/lib/rhc/auth.rb +3 -0
  18. data/lib/rhc/auth/basic.rb +54 -0
  19. data/lib/rhc/cartridge_helpers.rb +11 -5
  20. data/lib/rhc/cli.rb +4 -2
  21. data/lib/rhc/command_runner.rb +35 -30
  22. data/lib/rhc/commands.rb +127 -18
  23. data/lib/rhc/commands/account.rb +24 -0
  24. data/lib/rhc/commands/alias.rb +1 -1
  25. data/lib/rhc/commands/app.rb +210 -209
  26. data/lib/rhc/commands/apps.rb +22 -0
  27. data/lib/rhc/commands/base.rb +10 -77
  28. data/lib/rhc/commands/cartridge.rb +35 -35
  29. data/lib/rhc/commands/domain.rb +20 -13
  30. data/lib/rhc/commands/git_clone.rb +30 -0
  31. data/lib/rhc/commands/{port-forward.rb → port_forward.rb} +3 -3
  32. data/lib/rhc/commands/server.rb +28 -16
  33. data/lib/rhc/commands/setup.rb +18 -1
  34. data/lib/rhc/commands/snapshot.rb +4 -4
  35. data/lib/rhc/commands/sshkey.rb +4 -18
  36. data/lib/rhc/commands/tail.rb +32 -9
  37. data/lib/rhc/config.rb +168 -99
  38. data/lib/rhc/context_helper.rb +22 -9
  39. data/lib/rhc/core_ext.rb +41 -1
  40. data/lib/rhc/exceptions.rb +21 -5
  41. data/lib/rhc/git_helpers.rb +81 -0
  42. data/lib/rhc/help_formatter.rb +21 -1
  43. data/lib/rhc/helpers.rb +222 -87
  44. data/lib/rhc/output_helpers.rb +94 -110
  45. data/lib/rhc/rest.rb +15 -198
  46. data/lib/rhc/rest/api.rb +88 -0
  47. data/lib/rhc/rest/application.rb +29 -30
  48. data/lib/rhc/rest/attributes.rb +27 -0
  49. data/lib/rhc/rest/base.rb +29 -33
  50. data/lib/rhc/rest/cartridge.rb +42 -20
  51. data/lib/rhc/rest/client.rb +351 -89
  52. data/lib/rhc/rest/domain.rb +7 -13
  53. data/lib/rhc/rest/gear_group.rb +1 -1
  54. data/lib/rhc/rest/key.rb +7 -2
  55. data/lib/rhc/rest/mock.rb +609 -0
  56. data/lib/rhc/rest/user.rb +6 -2
  57. data/lib/rhc/{ssh_key_helpers.rb → ssh_helpers.rb} +58 -28
  58. data/lib/rhc/{targz.rb → tar_gz.rb} +0 -0
  59. data/lib/rhc/usage_templates/command_help.erb +4 -1
  60. data/lib/rhc/usage_templates/help.erb +24 -11
  61. data/lib/rhc/usage_templates/options_help.erb +14 -0
  62. data/lib/rhc/wizard.rb +283 -213
  63. data/spec/keys/example.pem +23 -0
  64. data/spec/keys/example_private.pem +27 -0
  65. data/spec/keys/server.pem +19 -0
  66. data/spec/rest_spec_helper.rb +3 -371
  67. data/spec/rhc/auth_spec.rb +226 -0
  68. data/spec/rhc/cli_spec.rb +41 -14
  69. data/spec/rhc/command_spec.rb +44 -15
  70. data/spec/rhc/commands/account_spec.rb +41 -0
  71. data/spec/rhc/commands/alias_spec.rb +16 -15
  72. data/spec/rhc/commands/app_spec.rb +115 -92
  73. data/spec/rhc/commands/apps_spec.rb +39 -0
  74. data/spec/rhc/commands/cartridge_spec.rb +134 -112
  75. data/spec/rhc/commands/domain_spec.rb +31 -86
  76. data/spec/rhc/commands/git_clone_spec.rb +56 -0
  77. data/spec/rhc/commands/{port-forward_spec.rb → port_forward_spec.rb} +27 -32
  78. data/spec/rhc/commands/server_spec.rb +28 -3
  79. data/spec/rhc/commands/setup_spec.rb +29 -11
  80. data/spec/rhc/commands/snapshot_spec.rb +4 -3
  81. data/spec/rhc/commands/sshkey_spec.rb +24 -56
  82. data/spec/rhc/commands/tail_spec.rb +26 -9
  83. data/spec/rhc/commands/threaddump_spec.rb +12 -11
  84. data/spec/rhc/config_spec.rb +211 -164
  85. data/spec/rhc/context_spec.rb +2 -0
  86. data/spec/rhc/helpers_spec.rb +242 -46
  87. data/spec/rhc/rest_application_spec.rb +42 -28
  88. data/spec/rhc/rest_client_spec.rb +110 -93
  89. data/spec/rhc/rest_spec.rb +220 -131
  90. data/spec/rhc/targz_spec.rb +1 -1
  91. data/spec/rhc/wizard_spec.rb +435 -624
  92. data/spec/spec.opts +1 -1
  93. data/spec/spec_helper.rb +140 -6
  94. data/spec/wizard_spec_helper.rb +326 -0
  95. metadata +163 -143
  96. data/lib/rhc/client.rb +0 -17
  97. data/lib/rhc/git_helper.rb +0 -59
@@ -1,30 +1,43 @@
1
- require 'rhc/git_helper'
1
+ require 'rhc/git_helpers'
2
2
 
3
3
  module RHC
4
+ #
5
+ # Methods in this module should not attempt to read from the options hash
6
+ # in a recursive manner (server_context can't read options.server).
7
+ #
4
8
  module ContextHelpers
5
9
  include RHC::GitHelpers
6
10
 
11
+ def server_context
12
+ ENV['LIBRA_SERVER'] || (!options.clean && config['libra_server']) || "openshift.redhat.com"
13
+ end
14
+
7
15
  def app_context
8
16
  debug "Getting app context"
9
17
 
18
+ name = git_config_get "rhc.app-name"
19
+ return name if name.present?
20
+
10
21
  uuid = git_config_get "rhc.app-uuid"
11
22
 
12
- # proof of concept - we shouldn't be traversing
13
- # the broker should expose apis for getting the application via a uuid
14
- rest_client.domains.each do |rest_domain|
15
- rest_domain.applications.each do |rest_app|
16
- return rest_app.name if rest_app.uuid == uuid
23
+ if uuid.present?
24
+ # proof of concept - we shouldn't be traversing
25
+ # the broker should expose apis for getting the application via a uuid
26
+ rest_client.domains.each do |rest_domain|
27
+ rest_domain.applications.each do |rest_app|
28
+ return rest_app.name if rest_app.uuid == uuid
29
+ end
17
30
  end
18
- end
19
31
 
20
- debug "Couldn't find app with UUID == #{uuid}"
32
+ debug "Couldn't find app with UUID == #{uuid}"
33
+ end
21
34
  nil
22
35
  end
23
36
 
24
37
  def namespace_context
25
38
  # right now we don't have any logic since we only support one domain
26
39
  # TODO: add domain lookup based on uuid
27
- domain = rest_client.domains[0]
40
+ domain = rest_client.domains.first
28
41
  raise RHC::DomainNotFoundException, "No domains configured for this user. You may create one using 'rhc domain create'." if domain.nil?
29
42
 
30
43
  domain.id
@@ -2,6 +2,7 @@
2
2
  require 'rhc/json'
3
3
  require 'open-uri'
4
4
  require 'highline'
5
+ require 'httpclient'
5
6
 
6
7
  class Object
7
8
  def present?
@@ -11,6 +12,10 @@ class Object
11
12
  respond_to?(:empty?) ? empty? : !self
12
13
  end
13
14
 
15
+ def presence
16
+ present? ? self : nil
17
+ end
18
+
14
19
  # Avoid a conflict if to_json is already defined
15
20
  unless Object.new.respond_to? :to_json
16
21
  def to_json(options=nil)
@@ -34,6 +39,16 @@ class String
34
39
  end
35
40
  end
36
41
 
42
+ unless HTTP::Message.method_defined? :ok?
43
+ #:nocov:
44
+ class HTTP::Message
45
+ def ok?
46
+ HTTP::Status.successful?(status)
47
+ end
48
+ end
49
+ #:nocov:
50
+ end
51
+
37
52
  #
38
53
  # Allow http => https redirection, see
39
54
  # http://bugs.ruby-lang.org/issues/859 to 1.8.7 for rough
@@ -49,6 +64,32 @@ module OpenURI
49
64
  end
50
65
  end
51
66
 
67
+ class Hash
68
+ def stringify_keys!
69
+ keys.each do |key|
70
+ v = delete(key)
71
+ if v.is_a? Hash
72
+ v.stringify_keys!
73
+ elsif v.is_a? Array
74
+ v.each{ |value| value.stringify_keys! if value.is_a? Hash }
75
+ end
76
+ self[(key.to_s rescue key) || key] = v
77
+ end
78
+ self
79
+ end
80
+ def slice!(*args)
81
+ s = []
82
+ args.inject([]) do |a, k|
83
+ s << [k, delete(k)] if has_key?(k)
84
+ end
85
+ s
86
+ end
87
+ def reverse_merge!(other_hash)
88
+ # right wins if there is no left
89
+ merge!( other_hash ){|key,left,right| left }
90
+ end
91
+ end
92
+
52
93
  # Some versions of highline get in an infinite loop when trying to wrap.
53
94
  # Fixes BZ 866530.
54
95
  class HighLine
@@ -122,5 +163,4 @@ class HighLine
122
163
 
123
164
  return wrapped_text.join("\n")
124
165
  end
125
-
126
166
  end
@@ -1,12 +1,18 @@
1
1
  module RHC
2
2
  class Exception < StandardError
3
3
  attr_reader :code
4
- def initialize(message=nil, code=nil)
4
+ def initialize(message=nil, code=1)
5
5
  super(message)
6
6
  @code = code
7
7
  end
8
8
  end
9
9
 
10
+ class ConfirmationError < Exception
11
+ def initialize(message="This action requires the --confirm option (or entering 'yes' at a prompt) to run.", code=1)
12
+ super(message, code)
13
+ end
14
+ end
15
+
10
16
  class DomainNotFoundException < Exception
11
17
  def initialize(message="Domain not found")
12
18
  super message, 127
@@ -37,14 +43,15 @@ module RHC
37
43
  end
38
44
  end
39
45
 
40
- # Makes sense to use its own exit code since this is different from a
41
- # resource error
42
46
  class GitException < Exception
43
47
  def initialize(message="Git returned an error")
44
48
  super message, 216
45
49
  end
46
50
  end
47
51
 
52
+ class GitPermissionDenied < GitException; end
53
+ class GitDirectoryExists < GitException; end
54
+
48
55
  class DeprecatedError < RuntimeError; end
49
56
 
50
57
  class KeyFileNotExistentException < Exception
@@ -97,13 +104,22 @@ module RHC
97
104
 
98
105
  class MissingScalingValueException < Exception
99
106
  def initialize(message="Must provide either a min or max value for scaling")
100
- super message, 1
107
+ super message
101
108
  end
102
109
  end
103
110
 
104
111
  class CartridgeNotScalableException < Exception
105
112
  def initialize(message="Cartridge is not scalable")
106
- super message, 1
113
+ super message
114
+ end
115
+ end
116
+
117
+ class ConnectionFailed < Exception
118
+ end
119
+
120
+ class SSHConnectionRefused < ConnectionFailed
121
+ def initialize(host, user)
122
+ super "The server #{host} refused a connection with user #{user}. The application may be unavailable.", 1
107
123
  end
108
124
  end
109
125
 
@@ -0,0 +1,81 @@
1
+ require 'open4'
2
+
3
+ module RHC
4
+ module GitHelpers
5
+ def git_version
6
+ @git_version ||= `git --version 2>&1`.strip #:nocov:
7
+ end
8
+
9
+ def has_git?
10
+ @has_git ||= begin
11
+ @git_version = nil
12
+ git_version
13
+ $?.success?
14
+ rescue
15
+ false
16
+ end
17
+ end
18
+
19
+ def git_clone_application(app)
20
+ repo_dir = options.repo || app.name
21
+
22
+ debug "Pulling new repo down"
23
+ git_clone_repo(app.git_url, repo_dir)
24
+
25
+ debug "Configuring git repo"
26
+ Dir.chdir(repo_dir) do |dir|
27
+ git_config_set "rhc.app-uuid", app.uuid
28
+ git_config_set "rhc.app-name", app.name
29
+ git_config_set "rhc.domain-name", app.domain_id
30
+ end
31
+
32
+ true
33
+ end
34
+
35
+ # :nocov: These all call external binaries so test them in cucumber
36
+ def git_config_get(key)
37
+ config_get_cmd = "git config --get #{key}"
38
+ debug "Running #{config_get_cmd}"
39
+ uuid = %x[#{config_get_cmd}].strip
40
+ debug "UUID = '#{uuid}'"
41
+ uuid = nil if $?.exitstatus != 0 or uuid.empty?
42
+
43
+ uuid
44
+ end
45
+
46
+ def git_config_set(key, value)
47
+ unset_cmd = "git config --unset-all #{key}"
48
+ config_cmd = "git config --add #{key} #{value}"
49
+ debug "Adding #{key} = #{value} to git config"
50
+ commands = [unset_cmd, config_cmd]
51
+ commands.each do |cmd|
52
+ debug "Running #{cmd} 2>&1"
53
+ output = %x[#{cmd} 2>&1]
54
+ raise RHC::GitException, "Error while adding config values to git - #{output}" unless output.empty?
55
+ end
56
+ end
57
+ # :nocov:
58
+
59
+ def git_clone_repo(git_url, repo_dir)
60
+ # quote the repo to avoid input injection risk
61
+ destination = (repo_dir ? " \"#{repo_dir}\"" : "")
62
+ cmd = "git clone #{git_url}#{destination}"
63
+ debug "Running #{cmd}"
64
+
65
+ status, stdout, stderr = run_with_tee(cmd)
66
+
67
+ if status != 0
68
+ case stderr
69
+ when /fatal: destination path '[^']*' already exists and is not an empty directory./
70
+ raise RHC::GitDirectoryExists, "The directory you are cloning into already exists."
71
+ when /^Permission denied \(.*?publickey.*?\).$/
72
+ raise RHC::GitPermissionDenied, "You don't have permission to access this repository. Check that your SSH public keys are correct."
73
+ else
74
+ raise RHC::GitException, "Unable to clone your repository. Called Git with: #{cmd}"
75
+ end
76
+ end
77
+
78
+ success "Your application code is now in '#{repo_dir}'"
79
+ end
80
+ end
81
+ end
@@ -8,6 +8,26 @@ module RHC
8
8
  def render_command_syntax command
9
9
  template(:command_syntax_help).result command.get_binding
10
10
  end
11
+ def render_options runner
12
+ template(:options_help).result runner.get_binding
13
+ end
14
+ end
15
+
16
+ class CommandHelpBindings
17
+ def initialize(command, instance_commands, runner)
18
+ @command = command
19
+ @actions = instance_commands.collect do |command_name, command_class|
20
+ next if command_class.summary.nil?
21
+ m = /^#{command.name} ([^ ]+)/.match(command_name)
22
+ # if we have a match and it is not an alias then we can use it
23
+ m and command_name == command_class.name ? {:name => m[1], :summary => command_class.summary || ""} : nil
24
+ end
25
+ @actions.compact!
26
+ @global_options = runner.options
27
+ @runner = runner
28
+ end
29
+ def program(*args)
30
+ @runner.program *args
31
+ end
11
32
  end
12
- # TODO: class ManPageHelpFormatter
13
33
  end
@@ -1,7 +1,6 @@
1
1
  require 'commander/user_interaction'
2
2
  require 'rhc/version'
3
3
  require 'rhc/config'
4
- require 'rhc/commands'
5
4
  require 'rhc/output_helpers'
6
5
  require 'rbconfig'
7
6
 
@@ -47,12 +46,14 @@ module RHC
47
46
  end
48
47
 
49
48
  def date(s)
50
- now = Time.now
49
+ now = Date.today
51
50
  d = datetime_rfc3339(s)
52
51
  if now.year == d.year
53
52
  return d.strftime('%l:%M %p').strip if now.yday == d.yday
53
+ d.strftime('%b %d %l:%M %p')
54
+ else
55
+ d.strftime('%b %d, %Y %l:%M %p')
54
56
  end
55
- d.strftime('%b %d %l:%M %p')
56
57
  rescue ArgumentError
57
58
  "Unknown date"
58
59
  end
@@ -70,38 +71,86 @@ module RHC
70
71
  "rhc/#{RHC::VERSION::STRING} (ruby #{RUBY_VERSION}; #{RUBY_PLATFORM})#{" (API #{RHC::Rest::API_VERSION})" rescue ''}"
71
72
  end
72
73
 
73
- def get(uri, opts=nil, *args)
74
- opts = {'User-Agent' => user_agent}.merge(opts || {})
75
- RestClient.get(uri, opts, *args)
76
- end
77
-
78
74
  #
79
75
  # Global config
80
76
  #
77
+ global_option '-l', '--rhlogin LOGIN', "OpenShift login"
78
+ global_option '-p', '--password PASSWORD', "OpenShift password"
79
+ global_option '-d', '--debug', "Turn on debugging", :hide => true
81
80
 
82
- global_option '-l', '--rhlogin login', "OpenShift login"
83
- global_option '-p', '--password password', "OpenShift password"
84
- global_option '-d', '--debug', "Turn on debugging"
81
+ global_option '--server NAME', String, 'An OpenShift server hostname (default: openshift.redhat.com)'
82
+ global_option '-k', '--insecure', "Allow insecure SSL connections. Potential security risk.", :hide => true
85
83
 
86
- global_option('--timeout seconds', Integer, 'Set the timeout in seconds for network commands') do |value|
87
- abort(color("Timeout must be a positive integer",:red)) unless value > 0
88
- # FIXME: Refactor so we don't have to use a global var here
89
- $rest_timeout = value
84
+ OptionParser.accept(SSLVersion = Class.new){ |s| OpenSSL::SSL::SSLContext::METHODS.find{ |m| m.to_s.downcase == s.downcase } or raise OptionParser::InvalidOption.new(nil, "The provided SSL version '#{s}' is not valid. Supported values: #{OpenSSL::SSL::SSLContext::METHODS.map(&:to_s).map(&:downcase).join(', ')}") }
85
+ global_option '--ssl-version VERSION', SSLVersion, "The version of SSL to use", :hide => true do |value|
86
+ raise RHC::Exception, "You are using an older version of the httpclient gem which prevents the use of --ssl-version. Please run 'gem update httpclient' to install a newer version (2.2.6 or newer)." unless HTTPClient::SSLConfig.method_defined? :ssl_version
90
87
  end
91
- global_option '--noprompt', "Suppress the interactive setup wizard from running before a command"
92
- global_option '--config FILE', "Path of a different config file"
93
- def config
94
- raise "Operations requiring configuration must define a config accessor"
88
+ global_option '--ssl-ca-file FILE', "An SSL certificate CA file (may contain multiple certs)", :hide => true do |value|
89
+ debug certificate_file(value)
90
+ end
91
+ global_option '--ssl-client-cert-file FILE', "An SSL x509 client certificate file", :hide => true do |value|
92
+ debug certificate_file(value)
93
+ end
94
+
95
+ global_option('--timeout SECONDS', Integer, 'The timeout for operations') do |value|
96
+ raise RHC::Exception, "Timeout must be a positive integer" unless value > 0
97
+ end
98
+ global_option '--noprompt', "Suppress all interactive operations command", :hide => true do
99
+ $terminal.page_at = nil
100
+ end
101
+ global_option '--config FILE', "Path of a different config file", :hide => true
102
+ global_option '--clean', "Ignore any saved configuration options", :hide => true
103
+ global_option '--mock', "Run in mock mode", :hide => true do
104
+ #:nocov:
105
+ require 'rhc/rest/mock'
106
+ RHC::Rest::Mock.start
107
+ #:nocov:
95
108
  end
96
109
 
97
110
  def openshift_server
98
- config.get_value('libra_server')
111
+ to_host((options.server rescue nil) || ENV['LIBRA_SERVER'] || "openshift.redhat.com")
112
+ end
113
+ def openshift_online_server?
114
+ openshift_server =~ /openshift.redhat.com$/i
99
115
  end
100
116
  def openshift_url
101
117
  "https://#{openshift_server}"
102
118
  end
103
- def openshift_rest_node
104
- "#{openshift_url}/broker/rest/api"
119
+
120
+ def to_host(s)
121
+ s =~ %r(^http(?:s)?://) ? URI(s).host : s
122
+ end
123
+ def to_uri(s)
124
+ URI(s =~ %r(^http(?:s)?://) ? s : "https://#{s}")
125
+ end
126
+ def openshift_rest_endpoint
127
+ uri = to_uri((options.server rescue nil) || ENV['LIBRA_SERVER'] || "openshift.redhat.com")
128
+ uri.path = '/broker/rest/api' if uri.path.blank? || uri.path == '/'
129
+ uri
130
+ end
131
+
132
+ def client_from_options(opts)
133
+ RHC::Rest::Client.new({
134
+ :url => openshift_rest_endpoint.to_s,
135
+ :debug => options.debug,
136
+ :timeout => options.timeout,
137
+ }.merge!(ssl_options).merge!(opts))
138
+ end
139
+
140
+ def ssl_options
141
+ {
142
+ :ssl_version => options.ssl_version,
143
+ :client_cert => certificate_file(options.ssl_client_cert),
144
+ :ca_file => options.ssl_ca_file && File.expand_path(options.ssl_ca_file),
145
+ :verify_mode => options.insecure ? OpenSSL::SSL::VERIFY_NONE : nil,
146
+ }.delete_if{ |k,v| v.nil? }
147
+ end
148
+
149
+ def certificate_file(file)
150
+ file && OpenSSL::X509::Certificate.new(IO.read(File.expand_path(file)))
151
+ rescue => e
152
+ debug e
153
+ raise OptionParser::InvalidOption.new(nil, "The certificate '#{file}' cannot be loaded: #{e.message} (#{e.class})")
105
154
  end
106
155
 
107
156
  #
@@ -111,6 +160,9 @@ module RHC
111
160
  def debug(msg)
112
161
  $stderr.puts "DEBUG: #{msg}" if debug?
113
162
  end
163
+ def debug?
164
+ false
165
+ end
114
166
 
115
167
  def deprecated_command(correct,short = false)
116
168
  deprecated("This command is deprecated. Please use '#{correct}' instead.",short)
@@ -123,25 +175,61 @@ module RHC
123
175
  def deprecated(msg,short = false)
124
176
  HighLine::use_color = false if windows? # handle deprecated commands that does not start through highline
125
177
 
126
- info = " For porting and testing purposes you may switch this %s to %s by setting the DISABLE_DEPRECATED environment variable to %d. It is not recommended to do so in a production environment as this option may be removed in future releases."
127
-
178
+ info = " For porting and testing purposes you may switch this %s to %s by setting the DISABLE_DEPRECATED environment variable to %d. It is not recommended to do so in a production environment as this option will be removed in a future release."
128
179
  msg << info unless short
129
- if RHC::Helpers.disable_deprecated?
130
- raise DeprecatedError.new(msg % ['an error','a warning',0])
131
- else
132
- warn "Warning: #{msg}\n" % ['a warning','an error',1]
133
- end
180
+
181
+ raise DeprecatedError.new(msg % ['an error','a warning',0]) if disable_deprecated?
182
+
183
+ warn "Warning: #{msg}\n" % ['a warning','an error',1]
134
184
  end
135
185
 
186
+ @@indent = 0
187
+ @@last_line_open = false
136
188
  def say(msg, *args)
137
- if Hash[*args][:stderr]
138
- $stderr.puts msg
139
- else
140
- super(msg)
189
+ output = if Hash[*args][:stderr]
190
+ $stderr
191
+ else
192
+ separate_blocks
193
+ $terminal.instance_variable_get(:@output)
194
+ end
195
+
196
+ Array(msg).each do |statement|
197
+ statement = statement.to_str
198
+ next unless statement.present?
199
+
200
+ template = ERB.new(statement, nil, "%")
201
+ statement = template.result(binding)
202
+
203
+ statement = $terminal.wrap(statement) unless $terminal.instance_variable_get(:@wrap_at).nil?
204
+ statement = $terminal.send(:page_print, statement) unless $terminal.instance_variable_get(:@page_at).nil?
205
+
206
+ output.print(' ' * @@indent * INDENT) unless @@last_line_open
207
+
208
+ @@last_line_open =
209
+ if statement[-1, 1] == " " or statement[-1, 1] == "\t"
210
+ output.print(statement)
211
+ output.flush
212
+ else
213
+ output.puts(statement)
214
+ end
141
215
  end
216
+
142
217
  msg
143
218
  end
144
219
 
220
+ [:ask, :agree].each do |sym|
221
+ define_method(sym) do |*args, &block|
222
+ separate_blocks
223
+ super(*args, &block)
224
+ end
225
+ end
226
+
227
+ def confirm_action(question)
228
+ return if options.confirm
229
+ return if !options.noprompt && paragraph{ agree("#{question} (yes|no): ") }
230
+ raise RHC::ConfirmationError
231
+ end
232
+
145
233
  def success(msg, *args)
146
234
  say color(msg, :green), *args
147
235
  end
@@ -166,6 +254,14 @@ module RHC
166
254
  count == 1 ? "#{count} #{s}" : "#{count} #{s}s"
167
255
  end
168
256
 
257
+ ## report a result (true/false) and return result
258
+ ## if the result is false, msg is displayed
259
+ def report_result(result, msg, fatal = true)
260
+ result ? $terminal.instance_variable_get(:@output).print('.') : error(msg)
261
+ # ignore the result if non-fatal
262
+ fatal ? result : true
263
+ end
264
+
169
265
  # given an array of arrays "items", construct an array of strings that can
170
266
  # be used to print in tabular form.
171
267
  def table(items, opts={}, &block)
@@ -174,7 +270,7 @@ module RHC
174
270
  items.each do |item|
175
271
  item.each_with_index do |s, i|
176
272
  item[i] = s.to_s
177
- widths[i] = [widths[i] || 0, s.length].max if s.respond_to?(:length)
273
+ widths[i] = [widths[i] || 0, item[i].length].max
178
274
  end
179
275
  end
180
276
  align = opts[:align] || []
@@ -186,7 +282,7 @@ module RHC
186
282
  items.unshift(opts[:header])
187
283
  end
188
284
  items.map do |item|
189
- item.each_with_index.map{ |s,i| s.send((align[i] == :right ? :rjust : :ljust), widths[i], ' ') }.join(join).strip
285
+ item.each_with_index.map{ |s,i| s.send((align[i] == :right ? :rjust : :ljust), widths[i], ' ') }.join(join).rstrip
190
286
  end
191
287
  end
192
288
 
@@ -212,29 +308,63 @@ module RHC
212
308
  :current_scale => "Current",
213
309
  :scales_from => "Minimum",
214
310
  :scales_to => "Maximum",
311
+ :gear_sizes => "Allowed Gear Sizes",
312
+ :consumed_gears => "Gears Used",
313
+ :max_gears => "Gears Allowed",
314
+ :gear_info => "Gears",
315
+ :plan_id => "Plan",
215
316
  :url => "URL",
216
- :ssh => "SSH",
217
- :gear_profile => "Gear Size"
317
+ :ssh_string => "SSH",
318
+ :connection_info => "Connection URL",
319
+ :gear_profile => "Gear Size",
320
+ :visible_to_ssh? => 'Available',
218
321
  })
219
322
 
220
323
  headings[value]
221
324
  end
222
325
 
223
- def header(s,opts = {})
224
- @indent ||= 0
225
- indent s
226
- indent "="*s.length
326
+ class StringTee < StringIO
327
+ attr_reader :tee
328
+ def initialize(other)
329
+ @tee = other
330
+ super()
331
+ end
332
+ def <<(buf)
333
+ tee << buf
334
+ super
335
+ end
336
+ end
337
+
338
+ #def tee(&block)
339
+ # original = [$stdout, $stderr]
340
+ # $stdout, $stderr = (tees = original.map{ |io| StringTee.new(io) })
341
+ # yield
342
+ #ensure
343
+ # $stdout, $stderr = original
344
+ # tees.each(&:close_write).map(&:string)
345
+ #end
346
+
347
+ def header(str,opts = {}, &block)
348
+ str = underline(str)
349
+ str = str.map{ |s| color(s, opts[:color]) } if opts[:color]
350
+ say str
227
351
  if block_given?
228
- @indent += 1
229
- yield
230
- @indent -= 1
352
+ indent &block
231
353
  end
232
354
  end
233
355
 
356
+ def underline(s)
357
+ [s, "-"*s.length]
358
+ end
359
+
234
360
  INDENT = 2
235
- def indent(str)
236
- @indent ||= 0
237
- say "%s%s" % [" " * @indent * INDENT,str]
361
+ def indent(&block)
362
+ @@indent += 1
363
+ begin
364
+ yield
365
+ ensure
366
+ @@indent -= 1
367
+ end
238
368
  end
239
369
 
240
370
  ##
@@ -265,37 +395,21 @@ module RHC
265
395
  # top - top margin specified in lines
266
396
  # bottom - bottom margin specified in line
267
397
  #
268
- @@section_bottom_last = 0
398
+ @@margin = nil
269
399
  def section(params={}, &block)
270
- top = params[:top]
271
- top = 0 if top.nil?
272
- bottom = params[:bottom]
273
- bottom = 0 if bottom.nil?
274
-
275
- # add more newlines if top is greater than the last section's bottom margin
276
- top_margin = @@section_bottom_last
277
-
278
- # negitive previous bottoms indicate that an untracked newline was
279
- # printed and so we do our best to negate it since we can't remove it
280
- if top_margin < 0
281
- top += top_margin
282
- top_margin = 0
283
- end
400
+ top = params[:top] || 0
401
+ bottom = params[:bottom] || 0
284
402
 
285
- until top_margin >= top
286
- say "\n"
287
- top_margin += 1
288
- end
403
+ # the first section cannot take a newline
404
+ top = 0 unless @@margin
405
+ @@margin = [top, @@margin || 0].max
289
406
 
290
- block.call
407
+ value = block.call
291
408
 
292
- bottom_margin = 0
293
- until bottom_margin >= bottom
294
- say "\n"
295
- bottom_margin += 1
296
- end
409
+ say "\n" if @@last_line_open
410
+ @@margin = [bottom, @@margin].max
297
411
 
298
- @@section_bottom_last = bottom
412
+ value
299
413
  end
300
414
 
301
415
  ##
@@ -314,7 +428,7 @@ module RHC
314
428
  # to distinguish the final results of a command from other output
315
429
  #
316
430
  def results(&block)
317
- paragraph do
431
+ section(:top => 1, :bottom => 0) do
318
432
  say "RESULT:"
319
433
  yield
320
434
  end
@@ -326,14 +440,9 @@ module RHC
326
440
  def unix? ; !jruby? && !windows? end
327
441
  def mac? ; RbConfig::CONFIG['host_os'] =~ /^darwin/ end
328
442
 
329
- # common SSH key display format in ERB
330
- def ssh_key_display_format
331
- ERB.new <<-FORMAT
332
- Name: <%= key.name %>
333
- Type: <%= key.type %>
334
- Fingerprint: <%= key.fingerprint %>
335
-
336
- FORMAT
443
+ def system_path(path)
444
+ return path.gsub(File::SEPARATOR, File::ALT_SEPARATOR) if File.const_defined?('ALT_SEPARATOR') and File::ALT_SEPARATOR.present?
445
+ path
337
446
  end
338
447
 
339
448
  #
@@ -346,15 +455,41 @@ Fingerprint: <%= key.fingerprint %>
346
455
  dns.getresources(host, Resolv::DNS::Resource::IN::A).any?
347
456
  # :nocov:
348
457
  end
349
-
458
+
350
459
  def hosts_file_contains?(host)
351
460
  # :nocov:
352
461
  resolver = Resolv::Hosts.new
353
- begin
354
- resolver.getaddress host
355
- rescue Resolv::ResolvError
356
- end
462
+ resolver.getaddress host
463
+ rescue Resolv::ResolvError
357
464
  # :nocov:
358
465
  end
466
+
467
+ # Run a command and export its output to the user. Output is not capturable
468
+ # on all platforms.
469
+ def run_with_tee(cmd)
470
+ status, stdout, stderr = nil
471
+
472
+ if windows?
473
+ #:nocov: TODO: Test block
474
+ system(cmd)
475
+ status = $?.exitstatus
476
+ #:nocov:
477
+ else
478
+ stdout, stderr = [$stdout, $stderr].map{ |t| StringTee.new(t) }
479
+ status = Open4.spawn(cmd, 'stdout' => stdout, 'stderr' => stderr, 'quiet' => true)
480
+ stdout, stderr = [stdout, stderr].map(&:string)
481
+ end
482
+
483
+ [status, stdout, stderr]
484
+ end
485
+
486
+ private
487
+
488
+ def separate_blocks
489
+ if (@@margin ||= 0) > 0 && !@@last_line_open
490
+ $terminal.instance_variable_get(:@output).print "\n" * @@margin
491
+ @@margin = 0
492
+ end
493
+ end
359
494
  end
360
495
  end