rhc 1.9.6 → 1.10.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/features/lib/rhc_helper.rb +0 -4
  3. data/features/lib/rhc_helper/app.rb +10 -10
  4. data/features/lib/rhc_helper/commandify.rb +6 -24
  5. data/features/support/before_hooks.rb +1 -1
  6. data/features/support/env.rb +9 -2
  7. data/features/support/platform_support.rb +11 -1
  8. data/lib/rhc/auth/basic.rb +4 -1
  9. data/lib/rhc/cartridge_helpers.rb +4 -8
  10. data/lib/rhc/commands.rb +6 -3
  11. data/lib/rhc/commands/alias.rb +1 -0
  12. data/lib/rhc/commands/app.rb +104 -67
  13. data/lib/rhc/commands/authorization.rb +2 -1
  14. data/lib/rhc/commands/base.rb +8 -1
  15. data/lib/rhc/commands/cartridge.rb +39 -16
  16. data/lib/rhc/commands/git_clone.rb +2 -1
  17. data/lib/rhc/commands/port_forward.rb +4 -6
  18. data/lib/rhc/commands/ssh.rb +54 -0
  19. data/lib/rhc/exceptions.rb +12 -7
  20. data/lib/rhc/git_helpers.rb +4 -5
  21. data/lib/rhc/help_formatter.rb +11 -6
  22. data/lib/rhc/helpers.rb +24 -3
  23. data/lib/rhc/highline_extensions.rb +31 -6
  24. data/lib/rhc/output_helpers.rb +0 -29
  25. data/lib/rhc/rest.rb +16 -1
  26. data/lib/rhc/rest/application.rb +7 -3
  27. data/lib/rhc/rest/base.rb +7 -2
  28. data/lib/rhc/rest/client.rb +7 -14
  29. data/lib/rhc/rest/gear_group.rb +10 -1
  30. data/lib/rhc/rest/mock.rb +34 -8
  31. data/lib/rhc/ssh_helpers.rb +144 -1
  32. data/lib/rhc/usage_templates/command_help.erb +16 -2
  33. data/spec/coverage_helper.rb +10 -7
  34. data/spec/rhc/commands/alias_spec.rb +28 -0
  35. data/spec/rhc/commands/app_spec.rb +64 -45
  36. data/spec/rhc/commands/authorization_spec.rb +16 -0
  37. data/spec/rhc/commands/cartridge_spec.rb +59 -3
  38. data/spec/rhc/commands/port_forward_spec.rb +6 -6
  39. data/spec/rhc/commands/ssh_spec.rb +125 -0
  40. data/spec/rhc/commands/tail_spec.rb +4 -3
  41. data/spec/rhc/helpers_spec.rb +70 -42
  42. data/spec/rhc/highline_extensions_spec.rb +23 -1
  43. data/spec/rhc/rest_client_spec.rb +6 -9
  44. data/spec/rhc/rest_spec.rb +41 -2
  45. data/spec/spec_helper.rb +38 -0
  46. metadata +336 -373
@@ -97,7 +97,22 @@ module RHC
97
97
 
98
98
  #I/O Exceptions Connection timeouts, etc
99
99
  class ConnectionException < Exception; end
100
- class TimeoutException < ConnectionException; end
100
+ class TimeoutException < ConnectionException
101
+ def initialize(message, nested=nil)
102
+ super(message)
103
+ @nested = nested
104
+ end
105
+
106
+ def on_receive?
107
+ @nested.is_a? HTTPClient::ReceiveTimeoutError
108
+ end
109
+ def on_connect?
110
+ @nested.is_a? HTTPClient::ConnectTimeoutError
111
+ end
112
+ def on_send?
113
+ @nested.is_a? HTTPClient::SendTimeoutError
114
+ end
115
+ end
101
116
 
102
117
  class SSLConnectionFailed < ConnectionException
103
118
  attr_reader :reason
@@ -63,11 +63,15 @@ module RHC
63
63
  rest_method "GET_GEAR_GROUPS"
64
64
  end
65
65
 
66
+ def gears
67
+ gear_groups.map{ |group| group.gears }.flatten
68
+ end
69
+
66
70
  def gear_ssh_url(gear_id)
67
- gear = gear_groups.map { |group| group.gears }.flatten.find { |g| g['id'] == gear_id }
71
+ gear = gears.find{ |g| g['id'] == gear_id }
68
72
 
69
- raise ArgumentError.new("Gear #{gear_id} not found") if gear.nil?
70
- gear['ssh_url'] or raise OperationNotSupportedException.new("The server does not support per gear operations")
73
+ raise ArgumentError, "Gear #{gear_id} not found" if gear.nil?
74
+ gear['ssh_url'] or raise NoPerGearOperations
71
75
  end
72
76
 
73
77
  def tidy
@@ -23,11 +23,16 @@ module RHC
23
23
  url = url.gsub(/:\w+/) { |s| options[:params][s] } if options[:params]
24
24
  method = options[:method] || link['method']
25
25
 
26
- client.request(options.merge({
26
+ result = client.request(options.merge({
27
27
  :url => url,
28
28
  :method => method,
29
29
  :payload => payload,
30
30
  }))
31
+ if result.is_a?(Hash) && (result['messages'] || result['errors'])
32
+ attributes['messages'] = Array(result['messages'])
33
+ result = self
34
+ end
35
+ result
31
36
  end
32
37
 
33
38
  def links
@@ -60,4 +65,4 @@ module RHC
60
65
  end
61
66
  end
62
67
  end
63
- end
68
+ end
@@ -175,7 +175,7 @@ module RHC
175
175
  # The list may not necessarily be sorted; we will select the last
176
176
  # matching one supported by the server.
177
177
  # See #api_version_negotiated
178
- CLIENT_API_VERSIONS = [1.1, 1.2, 1.3, 1.4]
178
+ CLIENT_API_VERSIONS = [1.1, 1.2, 1.3, 1.4, 1.5]
179
179
 
180
180
  def initialize(*args)
181
181
  options = args[0].is_a?(Hash) && args[0] || {}
@@ -207,8 +207,6 @@ module RHC
207
207
  self.headers.merge!(options.delete(:headers)) if options[:headers]
208
208
  self.options.merge!(options)
209
209
 
210
- update_http_proxy_env
211
-
212
210
  debug "Connecting to #{@end_point}"
213
211
  end
214
212
 
@@ -262,7 +260,7 @@ module RHC
262
260
  "Connection to server timed out. "\
263
261
  "It is possible the operation finished without being able "\
264
262
  "to report success. Use 'rhc domain show' or 'rhc app show' "\
265
- "to see the status of your applications.")
263
+ "to see the status of your applications.", e)
266
264
  rescue EOFError => e
267
265
  raise ConnectionException.new(
268
266
  "Connection to server got interrupted: #{e.message}")
@@ -306,6 +304,9 @@ module RHC
306
304
  raise ConnectionException.new(
307
305
  "Unable to connect to the server (#{e.message})."\
308
306
  "#{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]}'."}")
307
+ rescue Errno::ECONNRESET => e
308
+ raise ConnectionException.new(
309
+ "The server has closed the connection unexpectedly (#{e.message}). Your last operation may still be running on the server; please check before retrying your last request.")
309
310
  rescue RHC::Rest::Exception
310
311
  raise
311
312
  rescue => e
@@ -438,11 +439,11 @@ module RHC
438
439
  def parse_response(response)
439
440
  result = RHC::Json.decode(response)
440
441
  type = result['type']
441
- data = result['data']
442
+ data = result['data'] || {}
442
443
 
443
444
  # Copy messages to each object
444
445
  messages = Array(result['messages']).map do |m|
445
- m['text'] if m['field'].nil? or m['field'] == 'result'
446
+ m['text'] if m['field'].nil? or m['field'] == 'result' or m['severity'] == 'result'
446
447
  end.compact
447
448
  data.each{ |d| d['messages'] = messages } if data.is_a?(Array)
448
449
  data['messages'] = messages if data.is_a?(Hash)
@@ -559,14 +560,6 @@ module RHC
559
560
  keys = messages.group_by{ |m| m['field'] }.keys.compact.sort.map(&:to_sym) rescue []
560
561
  [messages_to_error(messages), keys]
561
562
  end
562
-
563
- def update_http_proxy_env
564
- # Set the http_proxy env variable, read by
565
- # HTTPClient, being sure to add the http protocol
566
- # if not specified already
567
- proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
568
- ENV['http_proxy'] = "http://#{proxy}" if proxy.present? && proxy !~ /^(\w+):\/\//
569
- end
570
563
  end
571
564
  end
572
565
  end
@@ -1,7 +1,16 @@
1
1
  module RHC
2
2
  module Rest
3
3
  class GearGroup < Base
4
- define_attr :gears, :cartridges, :gear_profile
4
+ define_attr :gears, :cartridges, :gear_profile, :additional_gear_storage, :base_gear_storage
5
+
6
+ def name(gear)
7
+ gear['name'] ||= "#{group.cartridges.collect{ |c| c['name'] }.join('+')}:#{gear['id']}"
8
+ end
9
+
10
+ def quota
11
+ return nil unless base_gear_storage
12
+ ((additional_gear_storage || 0) + base_gear_storage) * 1024 * 1024 * 1024
13
+ end
5
14
  end
6
15
  end
7
16
  end
@@ -52,7 +52,7 @@ module RHC::Rest::Mock
52
52
  to_return({
53
53
  :body => {
54
54
  :data => mock_response_links(authorizations ? mock_api_with_authorizations : mock_real_client_links),
55
- :supported_api_versions => [1.0, 1.1, 1.2, 1.3, 1.4],
55
+ :supported_api_versions => [1.0, 1.1, 1.2, 1.3, 1.4, 1.5],
56
56
  }.to_json
57
57
  })
58
58
  end
@@ -208,6 +208,21 @@ module RHC::Rest::Mock
208
208
  })
209
209
  end
210
210
 
211
+ def stub_application_cartridges(domain_name, app_name, cartridges, status = 200)
212
+ url = client_links['LIST_DOMAINS']['relative'] rescue "broker/rest/domains"
213
+ stub_api_request(:any, "#{url}/#{domain_name}/applications/#{app_name}/cartridges").
214
+ to_return({
215
+ :body => {
216
+ :type => 'cartridges',
217
+ :data => cartridges.map{ |c| c.is_a?(String) ? {:name => c} : c }.map do |cart|
218
+ cart[:links] ||= mock_response_links(mock_cart_links(domain_name,app_name, cart[:name]))
219
+ cart
220
+ end
221
+ }.to_json,
222
+ :status => status
223
+ })
224
+ end
225
+
211
226
  def stub_simple_carts
212
227
  stub_api_request(:get, 'broker/rest/cartridges', mock_user_auth).to_return(simple_carts)
213
228
  end
@@ -324,7 +339,7 @@ module RHC::Rest::Mock
324
339
 
325
340
  def mock_app_links(domain_id='test_domain',app_id='test_app')
326
341
  [['ADD_CARTRIDGE', "domains/#{domain_id}/apps/#{app_id}/carts/add", 'post'],
327
- ['LIST_CARTRIDGES', "domains/#{domain_id}/apps/#{app_id}/carts/", 'get' ],
342
+ ['LIST_CARTRIDGES', "broker/rest/domains/#{domain_id}/applications/#{app_id}/cartridges",'get' ],
328
343
  ['GET_GEAR_GROUPS', "domains/#{domain_id}/apps/#{app_id}/gear_groups", 'get' ],
329
344
  ['START', "domains/#{domain_id}/apps/#{app_id}/start", 'post'],
330
345
  ['STOP', "domains/#{domain_id}/apps/#{app_id}/stop", 'post'],
@@ -333,14 +348,14 @@ module RHC::Rest::Mock
333
348
  ['ADD_ALIAS', "domains/#{domain_id}/apps/#{app_id}/event", 'post'],
334
349
  ['REMOVE_ALIAS', "domains/#{domain_id}/apps/#{app_id}/event", 'post'],
335
350
  ['LIST_ALIASES', "domains/#{domain_id}/apps/#{app_id}/aliases", 'get'],
336
- ['DELETE', "domains/#{domain_id}/apps/#{app_id}/delete", 'post']]
351
+ ['DELETE', "broker/rest/domains/#{domain_id}/applications/#{app_id}", 'DELETE']]
337
352
  end
338
353
 
339
354
  def mock_cart_links(domain_id='test_domain',app_id='test_app',cart_id='test_cart')
340
355
  [['START', "domains/#{domain_id}/apps/#{app_id}/carts/#{cart_id}/start", 'post'],
341
356
  ['STOP', "domains/#{domain_id}/apps/#{app_id}/carts/#{cart_id}/stop", 'post'],
342
357
  ['RESTART', "domains/#{domain_id}/apps/#{app_id}/carts/#{cart_id}/restart", 'post'],
343
- ['DELETE', "domains/#{domain_id}/apps/#{app_id}/carts/#{cart_id}/delete", 'post']]
358
+ ['DELETE', "broker/rest/domains/#{domain_id}/applications/#{app_id}/cartridges/#{cart_id}", 'DELETE']]
344
359
  end
345
360
 
346
361
  def mock_client_links
@@ -487,6 +502,8 @@ module RHC::Rest::Mock
487
502
  MockRestCartridge.new(self, "mock_cart-2", "embedded"),
488
503
  MockRestCartridge.new(self, "unique_mock_cart-1", "embedded"),
489
504
  MockRestCartridge.new(self, "jenkins-client-1.4", "embedded"),
505
+ MockRestCartridge.new(self, "embcart-1", "embedded"),
506
+ MockRestCartridge.new(self, "embcart-2", "embedded"),
490
507
  premium_embedded
491
508
  ]
492
509
  end
@@ -601,11 +618,14 @@ module RHC::Rest::Mock
601
618
 
602
619
  class MockRestGearGroup < RHC::Rest::GearGroup
603
620
  include Helpers
604
- def initialize(client=nil)
621
+ def initialize(client=nil, carts=['name' => 'fake_geargroup_cart-0.1'], count=1)
605
622
  super({}, client)
606
- @cartridges = [{'name' => 'fake_geargroup_cart-0.1'}]
607
- @gears = [{'state' => 'started', 'id' => 'fakegearid', 'ssh_url' => 'ssh://fakegearid@fakesshurl.com'}]
623
+ @cartridges = carts
624
+ @gears = count.times.map do |i|
625
+ {'state' => 'started', 'id' => "fakegearid#{i}", 'ssh_url' => "ssh://fakegearid#{i}@fakesshurl.com"}
626
+ end
608
627
  @gear_profile = 'small'
628
+ @base_gear_storage = 1
609
629
  end
610
630
  end
611
631
 
@@ -710,7 +730,13 @@ module RHC::Rest::Mock
710
730
 
711
731
  def gear_groups
712
732
  # we don't have heavy interaction with gear groups yet so keep this simple
713
- @gear_groups ||= [MockRestGearGroup.new(client)]
733
+ @gear_groups ||= begin
734
+ if @scalable
735
+ cartridges.map{ |c| MockRestGearGroup.new(client, [c.name], c.current_scale) if c.name != 'haproxy-1.4' }.compact
736
+ else
737
+ [MockRestGearGroup.new(client, cartridges.map{ |c| {'name' => c.name} }, 1)]
738
+ end
739
+ end
714
740
  end
715
741
 
716
742
  def cartridges
@@ -20,6 +20,134 @@ require 'rhc/vendor/sshkey'
20
20
 
21
21
  module RHC
22
22
  module SSHHelpers
23
+
24
+ class MultipleGearTask
25
+ def initialize(command, over, opts={})
26
+ requires_ssh_multi!
27
+
28
+ @command = command
29
+ @over = over
30
+ @opts = opts
31
+ end
32
+
33
+ def run(&block)
34
+ out = nil
35
+
36
+ Net::SSH::Multi.start(
37
+ :concurrent_connections => @opts[:limit],
38
+ :on_error => lambda{ |server| $stderr.puts "Unable to connect to gear #{server}" }
39
+ ) do |session|
40
+
41
+ @over.each do |item|
42
+ case item
43
+ when RHC::Rest::GearGroup
44
+ item.gears.each do |gear|
45
+ session.use ssh_host_for(gear), :properties => {:gear => gear, :group => item}
46
+ end
47
+ #when RHC::Rest::Gear
48
+ # session.use ssh_host_for(item), :properties => {:gear => item}
49
+ #end
50
+ else
51
+ raise "Cannot establish an SSH session to this type"
52
+ end
53
+ end
54
+ session.exec @command, &(
55
+ case
56
+ when @opts[:raw]
57
+ lambda { |ch, dest, data|
58
+ (dest == :stdout ? $stdout : $stderr).puts data
59
+ }
60
+ when @opts[:as] == :table
61
+ out = []
62
+ lambda { |ch, dest, data|
63
+ label = label_for(ch)
64
+ data.chomp.each_line do |line|
65
+ row = out.find{ |row| row[0] == label } || (out << [label, []])[-1]
66
+ row[1] << line
67
+ end
68
+ }
69
+ when @opts[:as] == :gear
70
+ lambda { |ch, dest, data| (ch.connection.properties[:gear]['data'] ||= "") << data }
71
+ else
72
+ width = 0
73
+ lambda { |ch, dest, data|
74
+ label = label_for(ch)
75
+ io = dest == :stdout ? $stdout : $stderr
76
+ data.chomp!
77
+
78
+ if data.each_line.to_a.count < 2
79
+ io.puts "[#{label}] #{data}"
80
+ elsif @opts[:always_prefix]
81
+ data.each_line do |line|
82
+ io.puts "[#{label}] #{line}"
83
+ end
84
+ else
85
+ io.puts "=== #{label}"
86
+ io.puts data
87
+ end
88
+ }
89
+ end)
90
+ session.loop
91
+ end
92
+
93
+ if block_given? && !@opts[:raw]
94
+ case
95
+ when @opts[:as] == :gear
96
+ out = []
97
+ @over.each do |item|
98
+ case item
99
+ when RHC::Rest::GearGroup then item.gears.each{ |gear| out << yield(gear, gear['data'], item) }
100
+ #when RHC::Rest::Gear then out << yield(gear, gear['data'], nil)
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ out
107
+ end
108
+ protected
109
+ def ssh_host_for(gear)
110
+ RHC::Helpers.ssh_string(gear['ssh_url']) or raise NoPerGearOperations
111
+ end
112
+
113
+ def label_for(channel)
114
+ channel.properties[:label] ||=
115
+ begin
116
+ group = channel.connection.properties[:group]
117
+ "#{key_for(channel)} #{group.cartridges.map{ |c| c['name'] }.join('+')}"
118
+ end
119
+ end
120
+
121
+ def key_for(channel)
122
+ channel.connection.properties[:gear]['id']
123
+ end
124
+
125
+ def requires_ssh_multi!
126
+ begin
127
+ require 'net/ssh/multi'
128
+ rescue LoadError
129
+ raise RHC::OperationNotSupportedException, "You must install Net::SSH::Multi to use the --gears option. Most systems: 'gem install net-ssh-multi'"
130
+ end
131
+ end
132
+ end
133
+
134
+ def run_on_gears(command, gears, opts={}, &block)
135
+ debug "Executing #{command} on each of #{gears.inspect}"
136
+ MultipleGearTask.new(command, gears, {:limit => options.limit, :always_prefix => options.always_prefix, :raw => options.raw}.merge(opts)).run(&block)
137
+ end
138
+
139
+ def table_from_gears(command, groups, opts={}, &block)
140
+ cells = run_on_gears(command, groups, {:as => :table}.merge(opts), &block)
141
+ cells.each{ |r| r.concat(r.pop.first.split(opts[:split_cells_on])) } if !block_given? && opts[:split_cells_on]
142
+ say table cells, opts unless options.raw
143
+ end
144
+
145
+ def ssh_command_for_op(operation)
146
+ #case operation
147
+ raise RHC::OperationNotSupportedException, "The operation #{operation} is not supported."
148
+ #end
149
+ end
150
+
23
151
  # Public: Run ssh command on remote host
24
152
  #
25
153
  # host - The String of the remote hostname to ssh to.
@@ -100,7 +228,6 @@ module RHC
100
228
  end
101
229
  end
102
230
 
103
-
104
231
  # For Net::SSH versions (< 2.0.11) that does not have
105
232
  # Net::SSH::KeyFactory.load_public_key, we drop to shell to get
106
233
  # the key's fingerprint
@@ -147,6 +274,22 @@ module RHC
147
274
  ssh_key_triple_for(RHC::Config.ssh_pub_key_file_path)
148
275
  end
149
276
 
277
+ # check the version of SSH that is installed
278
+ def ssh_version
279
+ @ssh_version ||= `ssh -V 2>&1`.strip
280
+ end
281
+
282
+ # return whether or not SSH is installed
283
+ def has_ssh?
284
+ @has_ssh ||= begin
285
+ @ssh_version = nil
286
+ ssh_version
287
+ $?.success?
288
+ rescue
289
+ false
290
+ end
291
+ end
292
+
150
293
  private
151
294
 
152
295
  def ssh_add
@@ -1,7 +1,20 @@
1
1
  Usage: <%= Array(program :name).first %> <%= @command.name %> <%= @command.syntax %>
2
2
 
3
3
  <%= @command.description || @command.summary %>
4
- <% if @actions.blank? -%>
4
+ <% if @actions.blank? or @command.root? and @command.info[:method].present? -%>
5
+
6
+ <% unless @command.info[:args].blank? or @command.options.all?{ |o| o[:hide] or o[:switches].present? } -%>
7
+
8
+ Arguments
9
+ <%= table(
10
+ @command.info[:args].select do |opt|
11
+ not opt[:hide] and not opt[:switches].present?
12
+ end.map do |opt|
13
+ [opt[:name], opt[:description]]
14
+ end,
15
+ table_args(' ', 25)
16
+ ).join("\n") %>
17
+ <% end -%>
5
18
  <% unless @command.options.blank? or @command.options.all?{ |o| o[:hide] } -%>
6
19
 
7
20
  Options
@@ -26,7 +39,8 @@ Global Options
26
39
  ).join("\n") %>
27
40
 
28
41
  See 'rhc help options' for a full list of global options.
29
- <% else -%>
42
+ <% end -%>
43
+ <% if @actions.present? -%>
30
44
 
31
45
  List of Actions
32
46
  <%= table(@actions.map{ |a| [a[:name], a[:summary]] }, table_args(' ', 13)).join("\n") %>
@@ -16,19 +16,22 @@ unless RUBY_VERSION < '1.9'
16
16
  def print_missed_lines
17
17
  @files.each do |file|
18
18
  file.missed_lines.each do |line|
19
- puts "MISSED #{file.filename}:#{line.number}"
19
+ STDERR.puts "MISSED #{file.filename}:#{line.number}"
20
20
  end
21
21
  end
22
22
  end
23
23
  end
24
24
 
25
- original_stderr = $stderr # in case helpers don't properly cleanup
26
25
  SimpleCov.at_exit do
27
- SimpleCov.result.format!
28
- if SimpleCov.result.covered_percent < 100.0
29
- SimpleCov.result.print_missed_lines if SimpleCov.result.covered_percent > 98.0
30
- original_stderr.puts "Coverage not 100%, build failed."
31
- exit 1
26
+ begin
27
+ SimpleCov.result.format!
28
+ if SimpleCov.result.covered_percent < 100.0
29
+ SimpleCov.result.print_missed_lines if SimpleCov.result.covered_percent > 98.0
30
+ STDERR.puts "Coverage not 100%, build failed."
31
+ exit 1
32
+ end
33
+ rescue Exception => e
34
+ STDERR.puts "Exception at exit, #{e.message}"
32
35
  end
33
36
  end
34
37