rhc 1.2.7 → 1.3.8

Sign up to get free protection for your applications and to get access to all the features.
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