ridley-connectors 2.0.0 → 2.0.1

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.
@@ -0,0 +1,76 @@
1
+ require 'erubis'
2
+
3
+ module Ridley
4
+ module CommandContext
5
+ # A base class to provide common functionality between OS specific command contexts. A
6
+ # command context takes an options hash and binds it against a template file. You can then
7
+ # retrieve the command to be run on a node by calling {CommandContext::Base#command}.
8
+ #
9
+ # @example
10
+ # my_context = MyCommandContext.new(message: "hello, world!")
11
+ # my_context.command #=> "echo 'hello, world!'"
12
+ class Base
13
+ class << self
14
+ # Build a command context and immediately run it's command
15
+ #
16
+ # @param [Hash] options
17
+ # an options hash to pass to the new CommandContext
18
+ def command(options = {})
19
+ new(options).command
20
+ end
21
+
22
+ # Set or get the path to the template file for the inheriting class
23
+ #
24
+ # @param [String] filename
25
+ # the filename (without extension) of the template file to use to bind
26
+ # the inheriting command context class to
27
+ #
28
+ # @return [Pathname]
29
+ def template_file(filename = nil)
30
+ return @template_file if filename.nil?
31
+ @template_file = Ridley.scripts.join("#{filename}.erb")
32
+ end
33
+ end
34
+
35
+ # @param [Hash] options
36
+ def initialize(options = {}); end
37
+
38
+ # @return [String]
39
+ def command
40
+ template.evaluate(self)
41
+ end
42
+
43
+ private
44
+
45
+ # @return [Erubis::Eruby]
46
+ def template
47
+ @template ||= Erubis::Eruby.new(IO.read(self.class.template_file).chomp)
48
+ end
49
+ end
50
+
51
+ # A command context for Unix based OSes
52
+ class Unix < Base
53
+ # @return [Boolean]
54
+ attr_reader :sudo
55
+
56
+ # @option options [Boolean] :sudo (true)
57
+ # bootstrap with sudo (default: true)
58
+ def initialize(options = {})
59
+ options = options.reverse_merge(sudo: true)
60
+ @sudo = options[:sudo]
61
+ end
62
+
63
+ # @return [String]
64
+ def command
65
+ sudo ? "sudo #{super}" : super
66
+ end
67
+ end
68
+
69
+ # A command context for Windows based OSes
70
+ class Windows < Base; end
71
+ end
72
+ end
73
+
74
+ Dir["#{File.dirname(__FILE__)}/command_context/*.rb"].sort.each do |path|
75
+ require_relative "command_context/#{File.basename(path, '.rb')}"
76
+ end
@@ -0,0 +1,42 @@
1
+ module Ridley
2
+ module CommandContext
3
+ # Context class for generating an uninstall command for Unix based OSes
4
+ class UnixUninstall < CommandContext::Unix
5
+ template_file 'unix_uninstall_omnibus'
6
+
7
+ # @return [Boolean]
8
+ attr_reader :skip_chef
9
+
10
+ # @option options [Boolena] :skip_chef (false)
11
+ # skip removal of the Chef package and the contents of the installation
12
+ # directory. Setting this to true will only remove any data and configurations
13
+ # generated by running Chef client.
14
+ def initialize(options = {})
15
+ super(options)
16
+ options = options.reverse_merge(skip_chef: false)
17
+ @skip_chef = options[:skip_chef]
18
+ end
19
+
20
+ # The path to the Chef configuration directory on the target host
21
+ #
22
+ # @return [String]
23
+ def config_directory
24
+ "/etc/chef"
25
+ end
26
+
27
+ # The path to the Chef data directory on the target host
28
+ #
29
+ # @return [String]
30
+ def data_directory
31
+ "/var/chef"
32
+ end
33
+
34
+ # The path to the Omnibus Chef installation on the target host
35
+ #
36
+ # @return [String]
37
+ def install_directory
38
+ "/opt/chef"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ module Ridley
2
+ module CommandContext
3
+ # Context class for generating an uninstall command for Unix based OSes
4
+ class WindowsUninstall < CommandContext::Windows
5
+ template_file 'windows_uninstall_omnibus'
6
+
7
+ # @return [Boolean]
8
+ attr_reader :skip_chef
9
+
10
+ # @option options [Boolena] :skip_chef (false)
11
+ # skip removal of the Chef package and the contents of the installation
12
+ # directory. Setting this to true will only remove any data and configurations
13
+ # generated by running Chef client.
14
+ def initialize(options = {})
15
+ super(options)
16
+ options = options.reverse_merge(skip_chef: false)
17
+ @skip_chef = options[:skip_chef]
18
+ end
19
+
20
+ # The path to the Chef configuration directory on the target host
21
+ #
22
+ # @return [String]
23
+ def config_directory
24
+ "C:\\chef"
25
+ end
26
+
27
+ # The path to the Omnibus Chef installation on the target host
28
+ #
29
+ # @return [String]
30
+ def install_directory
31
+ "C:\\opscode\\chef"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ module Ridley
2
+ module Errors
3
+ module ConnectorsError; end
4
+
5
+ class HostConnectionError < RidleyError
6
+ include ConnectorsError
7
+ end
8
+
9
+ class DNSResolvError < HostConnectionError
10
+ include ConnectorsError
11
+ end
12
+
13
+ class BootstrapError < RidleyError; end
14
+ class RemoteCommandError < RidleyError; end
15
+ class RemoteScriptError < RemoteCommandError; end
16
+ class CommandNotProvided < RemoteCommandError
17
+ attr_reader :connector_type
18
+
19
+ # @params [Symbol] connector_type
20
+ def initialize(connector_type)
21
+ @connector_type = connector_type
22
+ end
23
+
24
+ def to_s
25
+ "No command provided in #{connector_type.inspect}, however the #{connector_type.inspect} connector was selected."
26
+ end
27
+ end
28
+ end
29
+ end
@@ -28,7 +28,8 @@ module Ridley
28
28
 
29
29
  CONNECTOR_PORT_ERRORS = [
30
30
  Errno::ETIMEDOUT, Timeout::Error, SocketError,
31
- Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL
31
+ Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL,
32
+ Resolv::ResolvError
32
33
  ]
33
34
 
34
35
  if Buff::RubyEngine.jruby?
@@ -190,7 +191,7 @@ module Ridley
190
191
  # @param block [Proc]
191
192
  # an optional block that is yielded the best HostConnector
192
193
  #
193
- # @return [HostConnector::SSH, HostConnector::WinRM]
194
+ # @return [HostConnector::SSH, HostConnector::WinRM, NilClass]
194
195
  def connector_for(host, options = {})
195
196
  options[:ssh] ||= Hash.new
196
197
  options[:winrm] ||= Hash.new
@@ -205,20 +206,33 @@ module Ridley
205
206
  options.delete(:winrm)
206
207
  ssh
207
208
  else
208
- raise Errors::HostConnectionError, "No connector ports open on '#{host}'"
209
+ nil
209
210
  end
210
211
  end
211
212
 
212
213
  private
213
214
 
215
+ # A helper method for sending the provided method to a proper
216
+ # connector actor.
217
+ #
218
+ # @param [Symbol] method
219
+ # the method to call on the connector actor
220
+ # @param [String] host
221
+ # the host to connect to
222
+ # @param [Array] args
223
+ # the splatted args passed to the method
224
+ #
225
+ # @return [HostConnector::Response]
214
226
  def execute(method, host, *args)
215
227
  options = args.last.is_a?(Hash) ? args.pop : Hash.new
216
228
 
217
- connector_for(host, options).send(method, host, *args, options)
218
- rescue Errors::HostConnectionError => ex
219
- abort(ex)
220
- rescue Resolv::ResolvError => ex
221
- abort Errors::DNSResolvError.new(ex)
229
+ connector = connector_for(host, options)
230
+ if connector.nil?
231
+ log.warn { "No connector ports open on '#{host}'" }
232
+ HostConnector::Response.new(host, stderr: "No connector ports open on '#{host}'")
233
+ else
234
+ connector.send(method, host, *args, options)
235
+ end
222
236
  end
223
237
 
224
238
  # Checks to see if the given port is open for TCP connections
@@ -242,7 +256,10 @@ module Ridley
242
256
  }
243
257
  rescue *CONNECTOR_PORT_ERRORS => ex
244
258
  @retry_count -= 1
245
- retry if @retry_count > 0
259
+ if @retry_count > 0
260
+ log.info { "Retrying connector_port_open? on '#{host}' #{port} due to: #{ex.class}" }
261
+ retry
262
+ end
246
263
  false
247
264
  end
248
265
  end
@@ -63,6 +63,9 @@ module Ridley
63
63
  rescue Net::SSH::Exception => ex
64
64
  response.exit_code = -1
65
65
  response.stderr = ex.inspect
66
+ rescue => ex
67
+ response.exit_code = -1
68
+ response.stderr = "An unknown error occurred: #{ex.class} - #{ex.message}"
66
69
  end
67
70
 
68
71
  case response.exit_code
@@ -50,8 +50,13 @@ module Ridley
50
50
 
51
51
  HostConnector::Response.new(host).tap do |response|
52
52
  begin
53
- command_uploaders << command_uploader = CommandUploader.new(connection)
54
- command = get_command(command, command_uploader, force_batch_file: options[:force_batch_file])
53
+ if options[:force_batch_file] || command.length > CommandUploader::CHUNK_LIMIT
54
+ log.debug "Detected a command that was longer than #{CommandUploader::CHUNK_LIMIT} characters. " +
55
+ "Uploading command as a file to the host."
56
+ command_uploaders << command_uploader = CommandUploader.new(connection)
57
+ command_uploader.upload(command)
58
+ command = command_uploader.command
59
+ end
55
60
 
56
61
  log.info "Running WinRM command: '#{command}' on: '#{host}' as: '#{user}'"
57
62
 
@@ -72,6 +77,15 @@ module Ridley
72
77
  rescue ::WinRM::WinRMHTTPTransportError => ex
73
78
  response.exit_code = :transport_error
74
79
  response.stderr = ex.message
80
+ rescue SocketError, Errno::EHOSTUNREACH
81
+ response.exit_code = -1
82
+ response.stderr = "Host unreachable"
83
+ rescue Errno::ECONNREFUSED
84
+ response.exit_code = -1
85
+ response.stderr = "Connection refused"
86
+ rescue => ex
87
+ response.exit_code = -1
88
+ response.stderr = "An unknown error occurred: #{ex.class} - #{ex.message}"
75
89
  end
76
90
 
77
91
  case response.exit_code
@@ -79,6 +93,8 @@ module Ridley
79
93
  log.info "Successfully ran WinRM command on: '#{host}' as: '#{user}'"
80
94
  when :transport_error
81
95
  log.info "A transport error occured while attempting to run a WinRM command on: '#{host}' as: '#{user}'"
96
+ when -1
97
+ log.info "Failed to run WinRM command on: '#{host}' as: '#{user}'"
82
98
  else
83
99
  log.info "Successfully ran WinRM command on: '#{host}' as: '#{user}', but it failed"
84
100
  end
@@ -86,29 +102,9 @@ module Ridley
86
102
  ensure
87
103
  begin
88
104
  command_uploaders.map(&:cleanup)
89
- rescue ::WinRM::WinRMHTTPTransportError => ex
90
- log.info "Error cleaning up leftover Powershell scripts on some hosts"
91
- end
92
- end
93
-
94
- # Returns the command if it does not break the WinRM command length
95
- # limit. Otherwise, we return an execution of the command as a batch file.
96
- #
97
- # @param command [String]
98
- # @param options [Hash]
99
- #
100
- # @option options [TrueClass, FalseClass] :force_batch_file
101
- # Always use a batch file to run the command regardless of command length.
102
- #
103
- # @return [String]
104
- def get_command(command, command_uploader, options = {})
105
- if !options[:force_batch_file] && command.length < CommandUploader::CHUNK_LIMIT
106
- command
107
- else
108
- log.debug "Detected a command that was longer than #{CommandUploader::CHUNK_LIMIT} characters. " +
109
- "Uploading command as a file to the host."
110
- command_uploader.upload(command)
111
- command_uploader.command
105
+ rescue ::WinRM::WinRMHTTPTransportError, SocketError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED => ex
106
+ log.info "Error cleaning up leftover Powershell scripts on some hosts due to: " +
107
+ "#{ex.class} - #{ex.message}"
112
108
  end
113
109
  end
114
110
 
@@ -1,5 +1,5 @@
1
1
  module Ridley
2
2
  module Connectors
3
- VERSION = "2.0.0"
3
+ VERSION = "2.0.1"
4
4
  end
5
5
  end
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_dependency 'celluloid', '~> 0.16.0.pre'
21
21
  s.add_dependency 'celluloid-io', '~> 0.16.0.pre'
22
+ s.add_dependency 'erubis'
22
23
  s.add_dependency 'net-ssh'
23
24
  s.add_dependency 'ridley', '~> 3.0'
24
25
  s.add_dependency 'winrm', '~> 1.1.0'
@@ -0,0 +1,31 @@
1
+ bash -c '
2
+ if [ -f "/etc/lsb-release" ]; then
3
+ platform=$(grep DISTRIB_ID /etc/lsb-release | cut -d "=" -f 2 | tr "[A-Z]" "[a-z]")
4
+ elif [ -f "/etc/debian_version" ]; then
5
+ platform="debian"
6
+ elif [ -f "/etc/redhat-release" ]; then
7
+ platform="el"
8
+ elif [ -f "/etc/system-release" ]; then
9
+ platform=$(sed "s/^\(.\+\) release.\+/\1/" /etc/system-release | tr "[A-Z]" "[a-z]")
10
+ if [ "$platform" = "amazon linux ami" ]; then
11
+ platform="el"
12
+ fi
13
+ elif [ -f "/etc/SuSE-release" ]; then
14
+ platform="el"
15
+ fi
16
+
17
+ <% unless skip_chef -%>
18
+ echo "Un-Installing installed Chef package"
19
+ case "$platform" in
20
+ "el") yum remove chef -y ;;
21
+ "debian") apt-get purge chef -y ;;
22
+ esac
23
+ <% end -%>
24
+
25
+ rm -Rdf <%= config_directory %>
26
+ rm -Rdf <%= data_directory %>
27
+
28
+ <% unless skip_chef -%>
29
+ rm -Rdf <%= install_directory %>
30
+ <% end -%>
31
+ '
@@ -0,0 +1,10 @@
1
+ $productName = "Chef"
2
+ $installDirectory = "<%= install_directory %>"
3
+ $configDirectory = "<%= config_directory %>"
4
+
5
+ <% unless skip_chef -%>
6
+ $app = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -match $productName }
7
+ If ($app) { $app.Uninstall() }
8
+ If (Test-Path $installDirectory) { Remove-Item -Recurse -Force $installDirectory }
9
+ <% end -%>
10
+ If (Test-Path $configDirectory) { Remove-Item -Recurse -Force $configDirectory }
data/spec/spec_helper.rb CHANGED
@@ -15,6 +15,9 @@ def setup_rspec
15
15
  WebMock.disable_net_connect!(allow_localhost: true, net_http_connect_on_start: true)
16
16
  end
17
17
 
18
+ config.filter_run focus: true
19
+ config.run_all_when_everything_filtered = true
20
+
18
21
  config.before(:all) { Ridley.logger = Celluloid.logger = nil }
19
22
 
20
23
  config.before(:each) do
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ridley::BootstrapContext::Unix do
4
+ let(:options) do
5
+ {
6
+ server_url: "https://api.opscode.com/organizations/vialstudios",
7
+ validator_client: "chef-validator",
8
+ validator_path: fixtures_path.join("my-fake.pem").to_s,
9
+ encrypted_data_bag_secret: File.read(fixtures_path.join("my-fake.pem"))
10
+ }
11
+ end
12
+
13
+ describe "ClassMethods" do
14
+ subject { described_class }
15
+
16
+ describe "::new" do
17
+ context "when no sudo option is passed through" do
18
+ it "sets a default value of 'true' to 'sudo'" do
19
+ options.delete(:sudo)
20
+ obj = subject.new(options)
21
+
22
+ obj.send(:sudo).should be_true
23
+ end
24
+ end
25
+
26
+ context "when the sudo option is passed through as false" do
27
+ it "sets the value of sudo to 'false' if provided" do
28
+ options.merge!(sudo: false)
29
+ obj = subject.new(options)
30
+
31
+ obj.send(:sudo).should be_false
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ subject { described_class.new(options) }
38
+
39
+ describe "MixinMethods" do
40
+
41
+ describe "#templates_path" do
42
+ it "returns a pathname" do
43
+ subject.templates_path.should be_a(Pathname)
44
+ end
45
+ end
46
+
47
+ describe "#first_boot" do
48
+ it "returns a string" do
49
+ subject.first_boot.should be_a(String)
50
+ end
51
+ end
52
+
53
+ describe "#encrypted_data_bag_secret" do
54
+ it "returns a string" do
55
+ subject.encrypted_data_bag_secret.should be_a(String)
56
+ end
57
+ end
58
+
59
+ describe "#validation_key" do
60
+ it "returns a string" do
61
+ subject.validation_key.should be_a(String)
62
+ end
63
+ end
64
+
65
+ describe "template" do
66
+ it "returns a string" do
67
+ subject.template.should be_a(Erubis::Eruby)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#boot_command" do
73
+ it "returns a string" do
74
+ subject.boot_command.should be_a(String)
75
+ end
76
+ end
77
+
78
+ describe "#chef_run" do
79
+ it "returns a string" do
80
+ subject.chef_run.should be_a(String)
81
+ end
82
+ end
83
+
84
+ describe "#chef_config" do
85
+ it "returns a string" do
86
+ subject.chef_config.should be_a(String)
87
+ end
88
+ end
89
+
90
+ describe "#default_template" do
91
+ it "returns a string" do
92
+ subject.default_template.should be_a(String)
93
+ end
94
+ end
95
+
96
+ describe "#bootstrap_directory" do
97
+ it "returns a string" do
98
+ subject.bootstrap_directory.should be_a(String)
99
+ end
100
+ end
101
+ end