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