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
@@ -0,0 +1,88 @@
1
+ module RHC
2
+ module Rest
3
+ class Api < Base
4
+ attr_reader :server_api_versions, :client_api_versions
5
+
6
+ def initialize(client, preferred_api_versions=[])
7
+ super(nil, client)
8
+
9
+ # API version negotiation
10
+ @server_api_versions = []
11
+ debug "Client supports API versions #{preferred_api_versions.join(', ')}"
12
+ @client_api_versions = preferred_api_versions
13
+ @server_api_versions, links = api_info({
14
+ :url => client.url,
15
+ :method => :get,
16
+ :lazy_auth => true,
17
+ })
18
+ debug "Server supports API versions #{@server_api_versions.join(', ')}"
19
+
20
+ if api_version_negotiated
21
+ unless server_api_version_current?
22
+ debug "Client API version #{api_version_negotiated} is not current. Refetching API"
23
+ # need to re-fetch API
24
+ @server_api_versions, links = api_info({
25
+ :url => client.url,
26
+ :method => :get,
27
+ :headers => {'Accept' => "application/json; version=#{api_version_negotiated}"},
28
+ :lazy_auth => true,
29
+ })
30
+ end
31
+ else
32
+ warn_about_api_versions
33
+ end
34
+
35
+ attributes['links'] = links
36
+
37
+ rescue RHC::Rest::ResourceNotFoundException => e
38
+ raise ApiEndpointNotFound.new(
39
+ "The OpenShift server is not responding correctly. Check "\
40
+ "that '#{client.url}' is the correct URL for your server. "\
41
+ "The server may be offline or misconfigured.")
42
+ end
43
+
44
+ ### API version related methods
45
+ def api_version_match?
46
+ ! api_version_negotiated.nil?
47
+ end
48
+
49
+ # return the API version that the server and this client can agree on
50
+ def api_version_negotiated
51
+ client_api_versions.reverse. # choose the last API version listed
52
+ detect { |v| @server_api_versions.include? v }
53
+ end
54
+
55
+ def client_api_version_current?
56
+ current_client_api_version == api_version_negotiated
57
+ end
58
+
59
+ def current_client_api_version
60
+ client_api_versions.last
61
+ end
62
+
63
+ def server_api_version_current?
64
+ @server_api_versions && @server_api_versions.max == api_version_negotiated
65
+ end
66
+
67
+ def warn_about_api_versions
68
+ if !api_version_match?
69
+ warn "WARNING: API version mismatch. This client supports #{client_api_versions.join(', ')} but
70
+ server at #{URI.parse(client.url).host} supports #{@server_api_versions.join(', ')}."
71
+ warn "The client version may be outdated; please consider updating 'rhc'. We will continue, but you may encounter problems."
72
+ end
73
+ end
74
+
75
+ protected
76
+ include RHC::Helpers
77
+
78
+ private
79
+ # execute +req+ with RestClient, and return [server_api_versions, links]
80
+ def api_info(req)
81
+ client.request(req) do |response|
82
+ json_response = ::RHC::Json.decode(response.content)
83
+ [ json_response['supported_api_versions'], json_response['data'] ]
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,15 +1,15 @@
1
1
  require 'uri'
2
- require 'rhc/rest/base'
3
2
 
4
3
  module RHC
5
4
  module Rest
6
5
  class Application < Base
7
6
  include Rest
8
7
 
9
- attr_reader :domain_id, :name, :creation_time, :uuid, :aliases,
8
+ define_attr :domain_id, :name, :creation_time, :uuid, :aliases,
10
9
  :git_url, :app_url, :gear_profile, :framework,
11
10
  :scalable, :health_check_path, :embedded, :gear_count,
12
- :ssh_url
11
+ :ssh_url, :building_app, :cartridges, :initial_git_url
12
+ alias_method :domain_name, :domain_id
13
13
 
14
14
  # Query helper to say consistent with cartridge
15
15
  def scalable?
@@ -23,14 +23,25 @@ module RHC
23
23
  carts.delete_if{|x| scales_with.include?(x.name)}
24
24
  end
25
25
 
26
- def add_cartridge(name, timeout=nil)
26
+ def add_cartridge(name, options={})
27
27
  debug "Adding cartridge #{name}"
28
- rest_method "ADD_CARTRIDGE", {:name => name}, timeout
28
+ @cartridges = nil
29
+ attributes['cartridges'] = nil
30
+ rest_method "ADD_CARTRIDGE", {:name => name}, options
29
31
  end
30
32
 
31
33
  def cartridges
32
34
  debug "Getting all cartridges for application #{name}"
33
- rest_method "LIST_CARTRIDGES"
35
+ @cartridges ||=
36
+ unless (carts = attributes['cartridges']).nil?
37
+ carts.map{|x| Cartridge.new(x, client) }
38
+ else
39
+ rest_method "LIST_CARTRIDGES"
40
+ end
41
+ end
42
+
43
+ def gear_info
44
+ { :gear_count => gear_count, :gear_profile => gear_profile } unless gear_count.nil?
34
45
  end
35
46
 
36
47
  def gear_groups
@@ -87,7 +98,7 @@ module RHC
87
98
  end
88
99
 
89
100
  def remove_alias(app_alias)
90
- debug "Running add_alias for #{name}"
101
+ debug "Running remove_alias for #{name}"
91
102
  rest_method "REMOVE_ALIAS", :event => "remove-alias", :alias => app_alias
92
103
  end
93
104
 
@@ -122,9 +133,9 @@ module RHC
122
133
  filtered = Array.new
123
134
  cartridges.each do |cart|
124
135
  if regex
125
- filtered.push(cart) if cart.name.match(regex) and (type.nil? or cart.type == type)
136
+ filtered.push(cart) if cart.name.match(/(?i:#{regex})/) and (type.nil? or cart.type == type)
126
137
  else
127
- filtered.push(cart) if cart.name == name and (type.nil? or cart.type == type)
138
+ filtered.push(cart) if cart.name.downcase == name.downcase and (type.nil? or cart.type == type)
128
139
  end
129
140
  end
130
141
  filtered
@@ -134,27 +145,15 @@ module RHC
134
145
  @host ||= URI(app_url).host
135
146
  end
136
147
 
137
- #Application log file tailing
138
- def tail(options)
139
- debug "Tail in progress for #{name}"
140
-
141
- file_glob = options.files ? options.files : "#{cartridges.first.name}/logs/*"
142
- remote_cmd = "tail#{options.opts ? ' --opts ' + Base64::encode64(options.opts).chomp : ''} #{file_glob}"
143
- ssh_cmd = "ssh -t #{uuid}@#{host} '#{remote_cmd}'"
144
- begin
145
- #Use ssh -t to tail the logs
146
- debug ssh_cmd
147
- ssh_ruby(host, uuid, remote_cmd)
148
- rescue SocketError => e
149
- msg =<<MESSAGE
150
- Could not connect: #{e.message}
151
- You can try to run this manually if you have ssh installed:
152
- #{ssh_cmd}
153
-
154
- MESSAGE
155
- debug "DEBUG: #{e.message}\n"
156
- raise SocketError, msg
157
- end
148
+ def ssh_string
149
+ uri = URI(ssh_url)
150
+ "#{uri.user}@#{uri.host}"
151
+ end
152
+
153
+ def <=>(other)
154
+ c = name.downcase <=> other.name.downcase
155
+ return c unless c == 0
156
+ domain_id <=> other.domain_id
158
157
  end
159
158
  end
160
159
  end
@@ -0,0 +1,27 @@
1
+ module RHC::Rest::Attributes
2
+ def attributes
3
+ @attributes
4
+ end
5
+
6
+ def attributes=(attr=nil)
7
+ @attributes = (attr || {}).stringify_keys!
8
+ end
9
+
10
+ def attribute(name)
11
+ instance_variable_get("@#{name}") || attributes[name.to_s]
12
+ end
13
+ end
14
+
15
+ module RHC::Rest::AttributesClass
16
+ def define_attr(*names)
17
+ names.map(&:to_sym).each do |name|
18
+ define_method(name) do
19
+ attribute(name)
20
+ end
21
+ define_method("#{name}=") do |value|
22
+ instance_variable_set(:"@#{name}", nil)
23
+ attributes[name.to_s] = value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,51 +1,47 @@
1
- require 'base64'
2
- require 'rhc/json'
3
-
4
1
  module RHC
5
2
  module Rest
6
3
  class Base
7
- include Rest
4
+ include Attributes
5
+ extend AttributesClass
8
6
 
9
- attr_reader :messages
7
+ define_attr :messages
10
8
 
11
- def initialize(json_args={}, use_debug=false)
12
- @debug = use_debug
13
- @__json_args__ = json_args
14
- @messages = []
9
+ def initialize(attrs=nil, client=nil)
10
+ @attributes = (attrs || {}).stringify_keys!
11
+ @attributes['messages'] ||= []
12
+ @client = client
15
13
  end
16
14
 
17
15
  def add_message(msg)
18
- @messages << msg
16
+ messages << msg
19
17
  end
20
18
 
21
- protected
22
- def debug?
23
- @debug
24
- end
25
-
26
- private
27
- def debug(msg, obj=nil)
28
- logger.debug("#{msg}#{obj ? " #{obj}" : ''}") if debug?
29
- end
19
+ def rest_method(link_name, payload={}, options={})
20
+ link = links[link_name.to_s]
21
+ raise "No link defined for #{link_name}" unless link
22
+ url = link['href']
23
+ method = link['method']
24
+
25
+ client.request(options.merge({
26
+ :url => url,
27
+ :method => method,
28
+ :payload => payload,
29
+ }))
30
+ end
30
31
 
31
- def rest_method(link_name, payload={}, timeout=nil)
32
- url = links[link_name]['href']
33
- method = links[link_name]['method']
32
+ def links
33
+ attributes['links'] || {}
34
+ end
34
35
 
35
- request = new_request(:url => url, :method => method, :headers => @@headers, :payload => payload, :timeout => timeout)
36
- request(request)
37
- end
36
+ protected
37
+ attr_reader :client
38
38
 
39
- def links
40
- @__json_args__[:links] || @__json_args__['links']
39
+ def debug(msg, obj=nil)
40
+ client.debug("#{msg}#{obj ? " #{obj}" : ''}") if client && client.debug?
41
41
  end
42
42
 
43
- def self.attr_reader(*names)
44
- names.each do |name|
45
- define_method(name) do
46
- instance_variable_get("@#{name}") || @__json_args__[name] || @__json_args__[name.to_s]
47
- end
48
- end
43
+ def debug?
44
+ client && client.debug?
49
45
  end
50
46
  end
51
47
  end
@@ -1,32 +1,48 @@
1
- require 'rhc/rest/base'
2
-
3
1
  module RHC
4
2
  module Rest
5
3
  class Cartridge < Base
6
- attr_reader :type, :name, :display_name, :properties, :status_messages, :scales_to, :scales_from, :scales_with, :current_scale, :base_gear_storage, :additional_gear_storage
7
- def initialize(args, use_debug=false)
8
- @properties = {}
9
- props = args[:properties] || args["properties"] || []
10
- props.each do |p|
11
- category = @properties[:"#{p['type']}"] || {}
12
- category[:"#{p['name']}"] = p
13
- @properties[:"#{p['type']}"] = category
14
- end
4
+ HIDDEN_TAGS = [:framework, :web_framework, :cartridge].map(&:to_s)
15
5
 
16
- # Make sure that additional gear storage is an integer
17
- # TODO: This should probably be fixed in the broker
18
- args['additional_gear_storage'] = args['additional_gear_storage'].to_i rescue 0
6
+ define_attr :type, :name, :display_name, :properties, :gear_profile, :status_messages, :scales_to, :scales_from, :scales_with, :current_scale, :supported_scales_to, :supported_scales_from, :tags, :description, :collocated_with
19
7
 
20
- super
8
+ def scalable?
9
+ supported_scales_to != supported_scales_from
21
10
  end
22
11
 
23
- def scalable?
24
- [scales_to,scales_from].map{|x| x > 1 || x == -1}.inject(:|)
12
+ def only_in_new?
13
+ type == 'standalone'
14
+ end
15
+ def shares_gears?
16
+ Array(collocated_with).present?
17
+ end
18
+ def collocated_with
19
+ Array(attribute(:collocated_with))
20
+ end
21
+
22
+ def tags
23
+ Array(attribute('tags'))
25
24
  end
26
25
 
27
- def property(category, key)
28
- category = properties[category]
29
- category ? category[key] : nil
26
+ def additional_gear_storage
27
+ attribute(:additional_gear_storage).to_i rescue 0
28
+ end
29
+
30
+ def display_name
31
+ attribute(:display_name) || name
32
+ end
33
+
34
+ def scaling
35
+ {
36
+ :current_scale => current_scale,
37
+ :scales_from => scales_from,
38
+ :scales_to => scales_to,
39
+ :gear_profile => gear_profile,
40
+ } if scalable?
41
+ end
42
+
43
+ def property(type, key)
44
+ key, type = key.to_s, type.to_s
45
+ properties.select{ |p| p['type'] == type }.find{ |p| p['name'] == key }
30
46
  end
31
47
 
32
48
  def status
@@ -76,6 +92,12 @@ module RHC
76
92
  info = property(:cart_data, :connection_url) || property(:cart_data, :job_url) || property(:cart_data, :monitoring_url)
77
93
  info ? (info["value"] || '').rstrip : nil
78
94
  end
95
+
96
+ def <=>(other)
97
+ return -1 if other.type == 'standalone' && type != 'standalone'
98
+ return 1 if type == 'standalone' && other.type != 'standalone'
99
+ name <=> other.name
100
+ end
79
101
  end
80
102
  end
81
103
  end
@@ -1,82 +1,174 @@
1
- require 'base64'
2
1
  require 'rhc/json'
3
- require 'rhc/rest/base'
4
2
  require 'rhc/helpers'
5
3
  require 'uri'
4
+ require 'logger'
5
+ require 'httpclient'
6
6
 
7
7
  module RHC
8
8
  module Rest
9
9
  class Client < Base
10
- include RHC::Helpers
11
-
12
- attr_reader :server_api_versions, :client_api_versions
10
+
13
11
  # Keep the list of supported API versions here
14
12
  # The list may not necessarily be sorted; we will select the last
15
13
  # matching one supported by the server.
16
14
  # See #api_version_negotiated
17
15
  CLIENT_API_VERSIONS = [1.1, 1.2, 1.3]
18
-
19
- def initialize(end_point, username, password, use_debug=false, preferred_api_versions = CLIENT_API_VERSIONS)
20
- @debug = use_debug
21
- @end_point = end_point
22
- @server_api_versions = []
23
- debug "Connecting to #{end_point}"
24
-
25
- credentials = nil
26
- userpass = "#{username}:#{password}"
27
- # :nocov: version dependent code
28
- if RUBY_VERSION.to_f == 1.8
29
- credentials = Base64.encode64(userpass).delete("\n")
30
- else
31
- credentials = Base64.strict_encode64(userpass)
32
- end
33
- # :nocov:
34
- @@headers["Authorization"] = "Basic #{credentials}"
35
- @@headers["User-Agent"] = RHC::Helpers.user_agent rescue nil
36
- RestClient.proxy = URI.parse(ENV['http_proxy']).to_s if ENV['http_proxy']
37
-
38
- # API version negotiation
39
- begin
40
- debug "Client supports API versions #{preferred_api_versions.join(', ')}"
41
- @client_api_versions = preferred_api_versions
42
- default_request = new_request(:url => @end_point, :method => :get, :headers => @@headers)
43
- @server_api_versions, links = api_info(default_request)
44
- debug "Server supports API versions #{@server_api_versions.join(', ')}"
45
-
46
- if api_version_negotiated
47
- unless server_api_version_current?
48
- debug "Client API version #{api_version_negotiated} is not current. Refetching API"
49
- # need to re-fetch API
50
- @@headers["Accept"] = "application/json; version=#{api_version_negotiated}"
51
- req = new_request(:url => @end_point, :method => :get, :headers => @@headers)
52
- @server_api_versions, links = api_info req
53
- end
16
+
17
+ def initialize(*args)
18
+ options = args[0].is_a?(Hash) && args[0] || {}
19
+ @end_point, @debug, @preferred_api_versions =
20
+ if options.empty?
21
+ options[:user] = args.delete_at(1)
22
+ options[:password] = args.delete_at(1)
23
+ args
54
24
  else
55
- warn_about_api_versions
25
+ [
26
+ options.delete(:url) ||
27
+ (options[:server] && "https://#{options.delete(:server)}/broker/rest/api"),
28
+ options.delete(:debug),
29
+ options.delete(:preferred_api_versions)
30
+ ]
31
+ end
32
+
33
+ @preferred_api_versions ||= CLIENT_API_VERSIONS
34
+ @debug ||= false
35
+
36
+ @auth = options.delete(:auth)
37
+
38
+ self.headers.merge!(options.delete(:headers)) if options[:headers]
39
+ self.options.merge!(options)
40
+
41
+ debug "Connecting to #{@end_point}"
42
+ end
43
+
44
+ def debug?
45
+ @debug
46
+ end
47
+
48
+ def request(options, &block)
49
+ (0..(1.0/0.0)).each do |i|
50
+ begin
51
+ client, args = new_request(options.dup)
52
+
53
+ #debug "Request: #{client.object_id} #{args.inspect}\n-------------" if debug?
54
+ response = client.request(*(args << true))
55
+ #debug "Response: #{response.status} #{response.headers.inspect}\n#{response.content}\n-------------" if debug? && response
56
+
57
+ next if retry_proxy(response, i, args, client)
58
+ auth.retry_auth?(response) and redo if auth
59
+ handle_error!(response, args[1], client) unless response.ok?
60
+
61
+ break (if block_given?
62
+ yield response
63
+ else
64
+ parse_response(response.content) unless response.nil? or response.code == 204
65
+ end)
66
+ rescue HTTPClient::BadResponseError => e
67
+ if e.res
68
+ debug "Response: #{e.res.status} #{e.res.headers.inspect}\n#{e.res.content}\n-------------" if debug?
69
+
70
+ next if retry_proxy(e.res, i, args, client)
71
+ auth.retry_auth?(e.res) and redo if auth
72
+ handle_error!(e.res, args[1], client)
73
+ end
74
+ raise ConnectionException.new(
75
+ "An unexpected error occured when connecting to the server: #{e.message}")
76
+ rescue HTTPClient::TimeoutError => e
77
+ raise TimeoutException.new(
78
+ "Connection to server timed out. "\
79
+ "It is possible the operation finished without being able "\
80
+ "to report success. Use 'rhc domain show' or 'rhc app show' "\
81
+ "to see the status of your applications.")
82
+ rescue EOFError => e
83
+ raise ConnectionException.new(
84
+ "Connection to server got interrupted: #{e.message}")
85
+ rescue OpenSSL::SSL::SSLError => e
86
+ raise SelfSignedCertificate.new(
87
+ 'self signed certificate',
88
+ "The server is using a self-signed certificate, which means that a secure connection can't be established '#{args[1]}'.\n\n"\
89
+ "You may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.") if self_signed?
90
+ raise case e.message
91
+ when /self signed certificate/
92
+ CertificateVerificationFailed.new(
93
+ e.message,
94
+ "The server is using a self-signed certificate, which means that a secure connection can't be established '#{args[1]}'.\n\n"\
95
+ "You may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
96
+ when /certificate verify failed/
97
+ CertificateVerificationFailed.new(
98
+ e.message,
99
+ "The server's certificate could not be verified, which means that a secure connection can't be established to the server '#{args[1]}'.\n\n"\
100
+ "If your server is using a self-signed certificate, you may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
101
+ when /unable to get local issuer certificate/
102
+ SSLConnectionFailed.new(
103
+ e.message,
104
+ "The server's certificate could not be verified, which means that a secure connection can't be established to the server '#{args[1]}'.\n\n"\
105
+ "You may need to specify your system CA certificate file with --ssl-ca-file=<path_to_file>. If your server is using a self-signed certificate, you may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
106
+ when /^SSL_connect returned=1 errno=0 state=SSLv2\/v3 read server hello A/
107
+ SSLVersionRejected.new(
108
+ e.message,
109
+ "The server has rejected your connection attempt with an older SSL protocol. Pass --ssl-version=sslv3 on the command line to connect to this server.")
110
+ when /^SSL_CTX_set_cipher_list:: no cipher match/
111
+ SSLVersionRejected.new(
112
+ e.message,
113
+ "The server has rejected your connection attempt because it does not support the requested SSL protocol version.\n\n"\
114
+ "Check with the administrator for a valid SSL version to use and pass --ssl-version=<version> on the command line to connect to this server.")
115
+ else
116
+ SSLConnectionFailed.new(
117
+ e.message,
118
+ "A secure connection could not be established to the server (#{e.message}). You may disable secure connections to your server with the -k (or --insecure) option '#{args[1]}'.\n\n"\
119
+ "If your server is using a self-signed certificate, you may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
120
+ end
121
+ rescue SocketError => e
122
+ raise ConnectionException.new(
123
+ "Unable to connect to the server (#{e.message})."\
124
+ "#{client.proxy.present? ? " Check that you have correctly specified your proxy server '#{client.proxy}' as well as your OpenShift server '#{args[1]}'." : " Check that you have correctly specified your OpenShift server '#{args[0]}'."}")
125
+ rescue RHC::Rest::Exception
126
+ raise
127
+ rescue => e
128
+ if debug?
129
+ logger.debug "#{e.message} (#{e.class})"
130
+ logger.debug e.backtrace.join("\n ")
131
+ end
132
+ raise ConnectionException.new("An unexpected error occured: #{e.message}").tap{ |n| n.set_backtrace(e.backtrace) }
56
133
  end
57
134
  end
135
+ end
136
+
137
+ def url
138
+ @end_point
139
+ end
58
140
 
59
- super({:links => links}, use_debug)
141
+ def api
142
+ @api ||= RHC::Rest::Api.new(self, @preferred_api_versions)
60
143
  end
61
144
 
145
+ def api_version_negotiated
146
+ api.api_version_negotiated
147
+ end
148
+
149
+ ################################################
150
+ # Delegate methods to API, should be moved there
151
+ # and then simply passed through.
152
+
62
153
  def add_domain(id)
63
154
  debug "Adding domain #{id}"
64
- rest_method "ADD_DOMAIN", :id => id
155
+ @domains = nil
156
+ api.rest_method "ADD_DOMAIN", :id => id
65
157
  end
66
158
 
67
159
  def domains
68
160
  debug "Getting all domains"
69
- rest_method "LIST_DOMAINS"
161
+ @domains ||= api.rest_method "LIST_DOMAINS"
70
162
  end
71
163
 
72
164
  def cartridges
73
165
  debug "Getting all cartridges"
74
- rest_method("LIST_CARTRIDGES")
166
+ @cartridges ||= api.rest_method("LIST_CARTRIDGES", nil, :lazy_auth => true)
75
167
  end
76
168
 
77
169
  def user
78
170
  debug "Getting user info"
79
- rest_method "GET_USER"
171
+ @user ||= api.rest_method "GET_USER"
80
172
  end
81
173
 
82
174
  def sshkeys
@@ -150,51 +242,221 @@ module RHC
150
242
  debug "Logout/Close client"
151
243
  end
152
244
  alias :close :logout
153
-
154
-
155
- ### API version related methods
156
- def api_version_match?
157
- ! api_version_negotiated.nil?
158
- end
159
-
160
- # return the API version that the server and this client can agree on
161
- def api_version_negotiated
162
- client_api_versions.reverse. # choose the last API version listed
163
- detect { |v| @server_api_versions.include? v }
164
- end
165
-
166
- def client_api_version_current?
167
- current_client_api_version == api_version_negotiated
168
- end
169
-
170
- def current_client_api_version
171
- client_api_versions.last
172
- end
173
-
174
- def server_api_version_current?
175
- @server_api_versions && @server_api_versions.max == api_version_negotiated
176
- end
177
-
178
- def warn_about_api_versions
179
- if !api_version_match?
180
- warn "WARNING: API version mismatch. This client supports #{client_api_versions.join(', ')} but
181
- server at #{URI.parse(@end_point).host} supports #{@server_api_versions.join(', ')}."
182
- warn "The client version may be outdated; please consider updating 'rhc'. We will continue, but you may encounter problems."
245
+
246
+ protected
247
+ include RHC::Helpers
248
+
249
+ attr_reader :auth
250
+ def headers
251
+ @headers ||= {
252
+ 'Accept' => 'application/json',
253
+ }
183
254
  end
184
- end
185
-
186
- def debug?
187
- @debug
188
- end
189
-
255
+
256
+ def user_agent
257
+ RHC::Helpers.user_agent
258
+ end
259
+
260
+ def options
261
+ @options ||= {
262
+ }
263
+ end
264
+
265
+ def httpclient_for(options)
266
+ return @httpclient if @last_options == options
267
+ @httpclient = HTTPClient.new(:agent_name => user_agent).tap do |http|
268
+ http.cookie_manager = nil
269
+ http.debug_dev = $stderr if ENV['HTTP_DEBUG']
270
+
271
+ options.select{ |sym, value| http.respond_to?("#{sym}=") }.map{ |sym, value| http.send("#{sym}=", value) }
272
+ http.set_auth(nil, options[:user], options[:password]) if options[:user]
273
+
274
+ ssl = http.ssl_config
275
+ options.select{ |sym, value| ssl.respond_to?("#{sym}=") }.map{ |sym, value| ssl.send("#{sym}=", value) }
276
+ ssl.add_trust_ca(options[:ca_file]) if options[:ca_file]
277
+ ssl.verify_callback = default_verify_callback
278
+
279
+ @last_options = options
280
+ end
281
+ end
282
+
283
+ def default_verify_callback
284
+ lambda do |is_ok, ctx|
285
+ @self_signed = false
286
+ unless is_ok
287
+ cert = ctx.current_cert
288
+ if cert && (cert.subject.cmp(cert.issuer) == 0)
289
+ @self_signed = true
290
+ debug "SSL Verification failed -- Using self signed cert" if debug?
291
+ else
292
+ debug "SSL Verification failed -- Preverify: #{is_ok}, Error: #{ctx.error_string} (#{ctx.error})" if debug?
293
+ end
294
+ return false
295
+ end
296
+ true
297
+ end
298
+ end
299
+ def self_signed?
300
+ @self_signed
301
+ end
302
+
303
+ def new_request(options)
304
+ options.reverse_merge!(self.options)
305
+
306
+ headers = (self.headers.to_a + (options.delete(:headers) || []).to_a).inject({}) do |h,(k,v)|
307
+ v = "application/#{v}" if k == :accept && v.is_a?(Symbol)
308
+ h[k.to_s.downcase.gsub(/_/, '-')] = v
309
+ h
310
+ end
311
+
312
+ options[:connect_timeout] ||= options[:timeout] || 120
313
+ options[:receive_timeout] ||= options[:timeout] || 0
314
+ options[:send_timeout] ||= options[:timeout] || 0
315
+ options[:timeout] = nil
316
+
317
+ auth.to_request(options) if auth
318
+
319
+ query = options.delete(:query) || {}
320
+ payload = options.delete(:payload)
321
+ if options[:method].to_s.upcase == 'GET'
322
+ query = payload
323
+ payload = nil
324
+ else
325
+ headers['content-type'] ||= begin
326
+ payload = payload.to_json unless payload.nil? || payload.is_a?(String)
327
+ 'application/json'
328
+ end
329
+ end
330
+ query = nil if query.blank?
331
+
332
+ args = [options.delete(:method), options.delete(:url), query, payload, headers, true]
333
+ [httpclient_for(options), args]
334
+ end
335
+
336
+ def retry_proxy(response, i, args, client)
337
+ if response.status == 502
338
+ debug "ERROR: Received bad gateway from server, will retry once if this is a GET" if debug?
339
+ return true if i == 0 && args[0] == :get
340
+ raise ConnectionException.new(
341
+ "An error occurred while communicating with the server. This problem may only be temporary."\
342
+ "#{client.proxy.present? ? " Check that you have correctly specified your proxy server '#{client.proxy}' as well as your OpenShift server '#{args[1]}'." : " Check that you have correctly specified your OpenShift server '#{args[1]}'."}")
343
+ end
344
+ end
345
+
346
+ def parse_response(response)
347
+ result = RHC::Json.decode(response)
348
+ type = result['type']
349
+ data = result['data']
350
+
351
+ # Copy messages to each object
352
+ messages = Array(result['messages']).map do |m|
353
+ m['text'] if m['field'].nil? or m['field'] == 'result'
354
+ end.compact
355
+ data.each{ |d| d['messages'] = messages } if data.is_a?(Array)
356
+ data['messages'] = messages if data.is_a?(Hash)
357
+
358
+ case type
359
+ when 'domains'
360
+ data.map{ |json| Domain.new(json, self) }
361
+ when 'domain'
362
+ Domain.new(data, self)
363
+ when 'applications'
364
+ data.map{ |json| Application.new(json, self) }
365
+ when 'application'
366
+ Application.new(data, self)
367
+ when 'cartridges'
368
+ data.map{ |json| Cartridge.new(json, self) }
369
+ when 'cartridge'
370
+ Cartridge.new(data, self)
371
+ when 'user'
372
+ User.new(data, self)
373
+ when 'keys'
374
+ data.map{ |json| Key.new(json, self) }
375
+ when 'key'
376
+ Key.new(data, self)
377
+ when 'gear_groups'
378
+ data.map{ |json| GearGroup.new(json, self) }
379
+ else
380
+ data
381
+ end
382
+ end
383
+
384
+ def raise_generic_error(url, client)
385
+ raise ServerErrorException.new(generic_error_message(url, client), 129)
386
+ end
387
+ def generic_error_message(url, client)
388
+ "The server did not respond correctly. This may be an issue "\
389
+ "with the server configuration or with your connection to the "\
390
+ "server (such as a Web proxy or firewall)."\
391
+ "#{client.proxy.present? ? " Please verify that your proxy server is working correctly (#{client.proxy}) and that you can access the OpenShift server #{url}" : "Please verify that you can access the OpenShift server #{url}"}"
392
+ end
393
+
394
+ def handle_error!(response, url, client)
395
+ messages = []
396
+ parse_error = nil
397
+ begin
398
+ result = RHC::Json.decode(response.content)
399
+ messages = Array(result['messages'])
400
+ messages.delete_if do |m|
401
+ m.delete_if{ |k,v| k.nil? || v.blank? } if m.is_a? Hash
402
+ m.blank?
403
+ end
404
+ rescue => e
405
+ logger.debug "Response did not include a message from server: #{e.message}" if debug?
406
+ end
407
+ case response.status
408
+ when 400
409
+ raise_generic_error(url, client) if messages.empty?
410
+ message, keys = messages_to_fields(messages)
411
+ raise ValidationException.new(message || "The operation could not be completed.", keys)
412
+ when 401
413
+ raise UnAuthorizedException, "Not authenticated"
414
+ when 403
415
+ raise RequestDeniedException, messages_to_error(messages) || "You are not authorized to perform this operation."
416
+ when 404
417
+ raise ResourceNotFoundException, messages_to_error(messages) || generic_error_message(url, client)
418
+ when 409
419
+ raise_generic_error(url, client) if messages.empty?
420
+ message, keys = messages_to_fields(messages)
421
+ raise ValidationException.new(message || "The operation could not be completed.", keys)
422
+ when 422
423
+ raise_generic_error(url, client) if messages.empty?
424
+ message, keys = messages_to_fields(messages)
425
+ raise ValidationException.new(message || "The operation was not valid.", keys)
426
+ when 400
427
+ raise ClientErrorException, messages_to_error(messages) || "The server did not accept the requested operation."
428
+ when 500
429
+ raise ServerErrorException, messages_to_error(messages) || generic_error_message(url, client)
430
+ when 503
431
+ raise ServiceUnavailableException, messages_to_error(messages) || generic_error_message(url, client)
432
+ else
433
+ raise ServerErrorException, messages_to_error(messages) || "Server returned an unexpected error code: #{response.status}"
434
+ end
435
+ raise_generic_error
436
+ end
437
+
190
438
  private
191
- # execute +req+ with RestClient, and return [server_api_versions, links]
192
- def api_info(req)
193
- request(req) do |response|
194
- json_response = ::RHC::Json.decode(response)
195
- [ json_response['supported_api_versions'], json_response['data'] ]
439
+ def logger
440
+ @logger ||= Logger.new(STDOUT)
441
+ end
442
+
443
+ def messages_to_error(messages)
444
+ errors, remaining = messages.partition{ |m| (m['severity'] || "").upcase == 'ERROR' }
445
+ if errors.present?
446
+ if errors.length == 1
447
+ errors.first['text']
448
+ else
449
+ "The server reported multiple errors:\n* #{errors.map{ |m| m['text'] || "An unknown server error occurred.#{ " (exit code: #{m['exit_code']}" if m['exit_code']}}" }.join("\n* ")}"
450
+ end
451
+ elsif remaining.present?
452
+ "The operation did not complete successfully, but the server returned additional information:\n* #{remaining.map{ |m| m['text'] || 'No message'}.join("\n* ")}"
453
+ end
454
+ end
455
+
456
+ def messages_to_fields(messages)
457
+ keys = messages.group_by{ |m| m['field'] }.keys.compact.sort.map(&:to_sym) rescue []
458
+ [messages_to_error(messages), keys]
196
459
  end
197
- end
198
460
  end
199
461
  end
200
462
  end