rhc 1.2.7 → 1.3.8
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/rhc +6 -8
- data/bin/rhc-chk +23 -10
- data/features/domain.feature +1 -1
- data/features/lib/rhc_helper.rb +3 -2
- data/features/lib/rhc_helper/api.rb +7 -0
- data/features/lib/rhc_helper/app.rb +8 -10
- data/features/lib/rhc_helper/domain.rb +2 -1
- data/features/lib/rhc_helper/runnable.rb +2 -24
- data/features/sshkey.feature +3 -3
- data/features/step_definitions/cartridge_steps.rb +6 -6
- data/features/step_definitions/client_steps.rb +0 -1
- data/features/step_definitions/sshkey_steps.rb +2 -2
- data/features/support/before_hooks.rb +0 -1
- data/features/support/env.rb +5 -3
- data/lib/rhc-common.rb +1 -1
- data/lib/rhc.rb +9 -8
- data/lib/rhc/auth.rb +3 -0
- data/lib/rhc/auth/basic.rb +54 -0
- data/lib/rhc/cartridge_helpers.rb +11 -5
- data/lib/rhc/cli.rb +4 -2
- data/lib/rhc/command_runner.rb +35 -30
- data/lib/rhc/commands.rb +127 -18
- data/lib/rhc/commands/account.rb +24 -0
- data/lib/rhc/commands/alias.rb +1 -1
- data/lib/rhc/commands/app.rb +210 -209
- data/lib/rhc/commands/apps.rb +22 -0
- data/lib/rhc/commands/base.rb +10 -77
- data/lib/rhc/commands/cartridge.rb +35 -35
- data/lib/rhc/commands/domain.rb +20 -13
- data/lib/rhc/commands/git_clone.rb +30 -0
- data/lib/rhc/commands/{port-forward.rb → port_forward.rb} +3 -3
- data/lib/rhc/commands/server.rb +28 -16
- data/lib/rhc/commands/setup.rb +18 -1
- data/lib/rhc/commands/snapshot.rb +4 -4
- data/lib/rhc/commands/sshkey.rb +4 -18
- data/lib/rhc/commands/tail.rb +32 -9
- data/lib/rhc/config.rb +168 -99
- data/lib/rhc/context_helper.rb +22 -9
- data/lib/rhc/core_ext.rb +41 -1
- data/lib/rhc/exceptions.rb +21 -5
- data/lib/rhc/git_helpers.rb +81 -0
- data/lib/rhc/help_formatter.rb +21 -1
- data/lib/rhc/helpers.rb +222 -87
- data/lib/rhc/output_helpers.rb +94 -110
- data/lib/rhc/rest.rb +15 -198
- data/lib/rhc/rest/api.rb +88 -0
- data/lib/rhc/rest/application.rb +29 -30
- data/lib/rhc/rest/attributes.rb +27 -0
- data/lib/rhc/rest/base.rb +29 -33
- data/lib/rhc/rest/cartridge.rb +42 -20
- data/lib/rhc/rest/client.rb +351 -89
- data/lib/rhc/rest/domain.rb +7 -13
- data/lib/rhc/rest/gear_group.rb +1 -1
- data/lib/rhc/rest/key.rb +7 -2
- data/lib/rhc/rest/mock.rb +609 -0
- data/lib/rhc/rest/user.rb +6 -2
- data/lib/rhc/{ssh_key_helpers.rb → ssh_helpers.rb} +58 -28
- data/lib/rhc/{targz.rb → tar_gz.rb} +0 -0
- data/lib/rhc/usage_templates/command_help.erb +4 -1
- data/lib/rhc/usage_templates/help.erb +24 -11
- data/lib/rhc/usage_templates/options_help.erb +14 -0
- data/lib/rhc/wizard.rb +283 -213
- data/spec/keys/example.pem +23 -0
- data/spec/keys/example_private.pem +27 -0
- data/spec/keys/server.pem +19 -0
- data/spec/rest_spec_helper.rb +3 -371
- data/spec/rhc/auth_spec.rb +226 -0
- data/spec/rhc/cli_spec.rb +41 -14
- data/spec/rhc/command_spec.rb +44 -15
- data/spec/rhc/commands/account_spec.rb +41 -0
- data/spec/rhc/commands/alias_spec.rb +16 -15
- data/spec/rhc/commands/app_spec.rb +115 -92
- data/spec/rhc/commands/apps_spec.rb +39 -0
- data/spec/rhc/commands/cartridge_spec.rb +134 -112
- data/spec/rhc/commands/domain_spec.rb +31 -86
- data/spec/rhc/commands/git_clone_spec.rb +56 -0
- data/spec/rhc/commands/{port-forward_spec.rb → port_forward_spec.rb} +27 -32
- data/spec/rhc/commands/server_spec.rb +28 -3
- data/spec/rhc/commands/setup_spec.rb +29 -11
- data/spec/rhc/commands/snapshot_spec.rb +4 -3
- data/spec/rhc/commands/sshkey_spec.rb +24 -56
- data/spec/rhc/commands/tail_spec.rb +26 -9
- data/spec/rhc/commands/threaddump_spec.rb +12 -11
- data/spec/rhc/config_spec.rb +211 -164
- data/spec/rhc/context_spec.rb +2 -0
- data/spec/rhc/helpers_spec.rb +242 -46
- data/spec/rhc/rest_application_spec.rb +42 -28
- data/spec/rhc/rest_client_spec.rb +110 -93
- data/spec/rhc/rest_spec.rb +220 -131
- data/spec/rhc/targz_spec.rb +1 -1
- data/spec/rhc/wizard_spec.rb +435 -624
- data/spec/spec.opts +1 -1
- data/spec/spec_helper.rb +140 -6
- data/spec/wizard_spec_helper.rb +326 -0
- metadata +163 -143
- data/lib/rhc/client.rb +0 -17
- data/lib/rhc/git_helper.rb +0 -59
data/lib/rhc/rest/api.rb
ADDED
@@ -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
|
data/lib/rhc/rest/application.rb
CHANGED
@@ -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
|
-
|
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,
|
26
|
+
def add_cartridge(name, options={})
|
27
27
|
debug "Adding cartridge #{name}"
|
28
|
-
|
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
|
-
|
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
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
data/lib/rhc/rest/base.rb
CHANGED
@@ -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
|
4
|
+
include Attributes
|
5
|
+
extend AttributesClass
|
8
6
|
|
9
|
-
|
7
|
+
define_attr :messages
|
10
8
|
|
11
|
-
def initialize(
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
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
|
-
|
16
|
+
messages << msg
|
19
17
|
end
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
32
|
+
def links
|
33
|
+
attributes['links'] || {}
|
34
|
+
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
36
|
+
protected
|
37
|
+
attr_reader :client
|
38
38
|
|
39
|
-
def
|
40
|
-
|
39
|
+
def debug(msg, obj=nil)
|
40
|
+
client.debug("#{msg}#{obj ? " #{obj}" : ''}") if client && client.debug?
|
41
41
|
end
|
42
42
|
|
43
|
-
def
|
44
|
-
|
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
|
data/lib/rhc/rest/cartridge.rb
CHANGED
@@ -1,32 +1,48 @@
|
|
1
|
-
require 'rhc/rest/base'
|
2
|
-
|
3
1
|
module RHC
|
4
2
|
module Rest
|
5
3
|
class Cartridge < Base
|
6
|
-
|
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
|
-
|
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
|
-
|
8
|
+
def scalable?
|
9
|
+
supported_scales_to != supported_scales_from
|
21
10
|
end
|
22
11
|
|
23
|
-
def
|
24
|
-
|
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
|
28
|
-
|
29
|
-
|
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
|
data/lib/rhc/rest/client.rb
CHANGED
@@ -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
|
-
|
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(
|
20
|
-
|
21
|
-
@end_point =
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|