ridley 0.12.4 → 1.0.0.rc1

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.
Files changed (39) hide show
  1. data/Gemfile +1 -1
  2. data/lib/ridley.rb +3 -3
  3. data/lib/ridley/bootstrap_context.rb +100 -0
  4. data/lib/ridley/bootstrap_context/unix.rb +74 -0
  5. data/lib/ridley/bootstrap_context/windows.rb +120 -0
  6. data/lib/ridley/chef_objects/node_object.rb +8 -5
  7. data/lib/ridley/host_commander.rb +207 -0
  8. data/lib/ridley/host_connector.rb +49 -87
  9. data/lib/ridley/host_connector/ssh.rb +153 -39
  10. data/lib/ridley/host_connector/winrm.rb +164 -39
  11. data/lib/ridley/resources/node_resource.rb +52 -56
  12. data/lib/ridley/version.rb +1 -1
  13. data/ridley.gemspec +0 -2
  14. data/spec/spec_helper.rb +4 -4
  15. data/spec/support/chef_server.rb +9 -3
  16. data/spec/unit/ridley/{bootstrap_bindings/unix_template_binding_spec.rb → bootstrap_context/unix_spec.rb} +2 -2
  17. data/spec/unit/ridley/{bootstrap_bindings/windows_template_binding_spec.rb → bootstrap_context/windows_spec.rb} +2 -2
  18. data/spec/unit/ridley/{mixin/bootstrap_binding_spec.rb → bootstrap_context_spec.rb} +2 -6
  19. data/spec/unit/ridley/host_commander_spec.rb +208 -0
  20. data/spec/unit/ridley/host_connector/ssh_spec.rb +37 -31
  21. data/spec/unit/ridley/host_connector/winrm_spec.rb +124 -31
  22. data/spec/unit/ridley/host_connector_spec.rb +23 -147
  23. data/spec/unit/ridley/resources/node_resource_spec.rb +55 -115
  24. metadata +17 -66
  25. data/lib/ridley/bootstrap_bindings.rb +0 -3
  26. data/lib/ridley/bootstrap_bindings/unix_template_binding.rb +0 -108
  27. data/lib/ridley/bootstrap_bindings/windows_template_binding.rb +0 -163
  28. data/lib/ridley/bootstrapper.rb +0 -89
  29. data/lib/ridley/bootstrapper/context.rb +0 -81
  30. data/lib/ridley/host_connector/response_set.rb +0 -98
  31. data/lib/ridley/host_connector/ssh/worker.rb +0 -135
  32. data/lib/ridley/host_connector/winrm/worker.rb +0 -159
  33. data/lib/ridley/log.rb +0 -10
  34. data/lib/ridley/mixin/bootstrap_binding.rb +0 -77
  35. data/spec/unit/ridley/bootstrapper/context_spec.rb +0 -45
  36. data/spec/unit/ridley/bootstrapper_spec.rb +0 -96
  37. data/spec/unit/ridley/host_connector/response_set_spec.rb +0 -112
  38. data/spec/unit/ridley/host_connector/ssh/worker_spec.rb +0 -57
  39. data/spec/unit/ridley/host_connector/winrm/worker_spec.rb +0 -139
data/Gemfile CHANGED
@@ -45,5 +45,5 @@ group :test do
45
45
  gem 'fuubar'
46
46
  gem 'json_spec'
47
47
  gem 'webmock'
48
- gem 'chef-zero', '~> 1.1.1'
48
+ gem 'chef-zero', '~> 1.1.3'
49
49
  end
data/lib/ridley.rb CHANGED
@@ -16,15 +16,15 @@ module Ridley
16
16
  CHEF_VERSION = '11.4.0'.freeze
17
17
 
18
18
  require_relative 'ridley/mixin'
19
- require_relative 'ridley/bootstrap_bindings'
20
- require_relative 'ridley/bootstrapper'
19
+ require_relative 'ridley/logging'
20
+ require_relative 'ridley/bootstrap_context'
21
21
  require_relative 'ridley/chef_object'
22
22
  require_relative 'ridley/chef_objects'
23
23
  require_relative 'ridley/client'
24
24
  require_relative 'ridley/connection'
25
25
  require_relative 'ridley/chef'
26
+ require_relative 'ridley/host_commander'
26
27
  require_relative 'ridley/host_connector'
27
- require_relative 'ridley/logging'
28
28
  require_relative 'ridley/middleware'
29
29
  require_relative 'ridley/resource'
30
30
  require_relative 'ridley/resources'
@@ -0,0 +1,100 @@
1
+ require 'erubis'
2
+
3
+ module Ridley
4
+ module BootstrapContext
5
+ class Base
6
+ class << self
7
+ def validate_options(options = {})
8
+ if options[:server_url].nil?
9
+ raise Errors::ArgumentError, "A server_url is required for bootstrapping"
10
+ end
11
+
12
+ if options[:validator_path].nil?
13
+ raise Errors::ArgumentError, "A path to a validator is required for bootstrapping"
14
+ end
15
+ end
16
+ end
17
+
18
+ attr_reader :template_file
19
+ attr_reader :bootstrap_proxy
20
+ attr_reader :chef_version
21
+ attr_reader :validator_path
22
+ attr_reader :encrypted_data_bag_secret
23
+ attr_reader :server_url
24
+ attr_reader :validator_client
25
+ attr_reader :node_name
26
+ attr_reader :attributes
27
+ attr_reader :run_list
28
+ attr_reader :environment
29
+ attr_reader :hints
30
+
31
+ def initialize(options = {})
32
+ options = options.reverse_merge(
33
+ validator_client: "chef-validator",
34
+ attributes: Hash.new,
35
+ run_list: Array.new,
36
+ environment: "_default",
37
+ sudo: true,
38
+ hints: Hash.new,
39
+ chef_version: "latest"
40
+ )
41
+ options[:template] ||= default_template
42
+ self.class.validate_options(options)
43
+
44
+ @template_file = options[:template]
45
+ @bootstrap_proxy = options[:bootstrap_proxy]
46
+ @chef_version = options[:chef_version]
47
+ @sudo = options[:sudo]
48
+ @validator_path = options[:validator_path]
49
+ @encrypted_data_bag_secret = options[:encrypted_data_bag_secret]
50
+ @hints = options[:hints]
51
+ @server_url = options[:server_url]
52
+ @validator_client = options[:validator_client]
53
+ @node_name = options[:node_name]
54
+ @attributes = options[:attributes]
55
+ @run_list = options[:run_list]
56
+ @environment = options[:environment]
57
+ end
58
+
59
+ # @return [String]
60
+ def bootstrap_command
61
+ raise RuntimeError, "abstract function: must be implemented on includer"
62
+ end
63
+
64
+ # @return [String]
65
+ def default_template
66
+ raise RuntimeError, "abstract function: must be implemented on includer"
67
+ end
68
+
69
+ # @return [Pathname]
70
+ def templates_path
71
+ Ridley.root.join('bootstrappers')
72
+ end
73
+
74
+ # @return [String]
75
+ def first_boot
76
+ JSON.fast_generate(attributes.merge(run_list: run_list))
77
+ end
78
+
79
+ # The validation key to create a new client for the node
80
+ #
81
+ # @raise [Ridley::Errors::ValidatorNotFound]
82
+ #
83
+ # @return [String]
84
+ def validation_key
85
+ IO.read(File.expand_path(validator_path)).chomp
86
+ rescue Errno::ENOENT
87
+ raise Errors::ValidatorNotFound, "Error bootstrapping: Validator not found at '#{validator_path}'"
88
+ end
89
+
90
+ # @return [Erubis::Eruby]
91
+ def template
92
+ Erubis::Eruby.new(IO.read(template_file).chomp)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ Dir["#{File.dirname(__FILE__)}/bootstrap_context/*.rb"].sort.each do |path|
99
+ require_relative "bootstrap_context/#{File.basename(path, '.rb')}"
100
+ end
@@ -0,0 +1,74 @@
1
+ module Ridley
2
+ module BootstrapContext
3
+ # Represents a binding that will be evaluated as an ERB template. When bootstrapping
4
+ # nodes, an instance of this class represents the customizable and necessary configurations
5
+ # needed by the Host in order to install and connect to Chef. By default, this class will be used
6
+ # when SSH is the best way to connect to the node.
7
+ #
8
+ # @author Kyle Allan <kallan@riotgames.com>
9
+ class Unix < BootstrapContext::Base
10
+ attr_reader :sudo
11
+
12
+ # @option options [Boolean] :sudo (true)
13
+ # bootstrap with sudo (default: true)
14
+ def initialize(options = {})
15
+ options = options.reverse_merge(sudo: true)
16
+ @sudo = options[:sudo]
17
+ super(options)
18
+ end
19
+
20
+ # @return [String]
21
+ def boot_command
22
+ cmd = template.evaluate(self)
23
+
24
+ if sudo
25
+ cmd = "sudo #{cmd}"
26
+ end
27
+
28
+ cmd
29
+ end
30
+
31
+ # @return [String]
32
+ def chef_config
33
+ body = <<-CONFIG
34
+ log_level :info
35
+ log_location STDOUT
36
+ chef_server_url "#{server_url}"
37
+ validation_client_name "#{validator_client}"
38
+ CONFIG
39
+
40
+ if node_name.present?
41
+ body << %Q{node_name "#{node_name}"\n}
42
+ else
43
+ body << "# Using default node name (fqdn)\n"
44
+ end
45
+
46
+ if bootstrap_proxy.present?
47
+ body << %Q{http_proxy "#{bootstrap_proxy}"\n}
48
+ body << %Q{https_proxy "#{bootstrap_proxy}"\n}
49
+ end
50
+
51
+ if encrypted_data_bag_secret.present?
52
+ body << %Q{encrypted_data_bag_secret "#{bootstrap_directory}/encrypted_data_bag_secret"\n}
53
+ end
54
+
55
+ body
56
+ end
57
+
58
+ # @return [String]
59
+ def bootstrap_directory
60
+ "/etc/chef"
61
+ end
62
+
63
+ # @return [String]
64
+ def chef_run
65
+ "chef-client -j #{bootstrap_directory}/first-boot.json -E #{environment}"
66
+ end
67
+
68
+ # @return [String]
69
+ def default_template
70
+ templates_path.join('unix_omnibus.erb').to_s
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,120 @@
1
+ module Ridley
2
+ module BootstrapContext
3
+ # Represents a binding that will be evaluated as an ERB template. When bootstrapping
4
+ # nodes, an instance of this class represents the customizable and necessary configurations
5
+ # needed by the Host in order to install and connect to Chef. By default, this class will be used
6
+ # when WinRM is the best way to connect to the node.
7
+ #
8
+ # @author Kyle Allan <kallan@riotgames.com>
9
+ #
10
+ # Windows Specific code written by Seth Chisamore (<schisamo@opscode.com>) in knife-windows
11
+ # https://github.com/opscode/knife-windows/blob/3b8886ddcfb928ca0958cd05b22f8c3d78bee86e/lib/chef/knife/bootstrap/windows-chef-client-msi.erb
12
+ # https://github.com/opscode/knife-windows/blob/78d38bbed358ac20107fc2b5b427f4b5e52e5cb2/lib/chef/knife/core/windows_bootstrap_context.rb
13
+ class Windows < BootstrapContext::Base
14
+ # @return [String]
15
+ def boot_command
16
+ template.evaluate(self)
17
+ end
18
+
19
+ # @return [String]
20
+ def chef_config
21
+ body = <<-CONFIG
22
+ log_level :info
23
+ log_location STDOUT
24
+ chef_server_url "#{server_url}"
25
+ validation_client_name "#{validator_client}"
26
+ CONFIG
27
+
28
+ if node_name.present?
29
+ body << %Q{node_name "#{node_name}"\n}
30
+ else
31
+ body << "# Using default node name (fqdn)\n"
32
+ end
33
+
34
+ if bootstrap_proxy.present?
35
+ body << %Q{http_proxy "#{bootstrap_proxy}"\n}
36
+ body << %Q{https_proxy "#{bootstrap_proxy}"\n}
37
+ end
38
+
39
+ if encrypted_data_bag_secret.present?
40
+ body << %Q{encrypted_data_bag_secret '#{bootstrap_directory}\\encrypted_data_bag_secret'\n}
41
+ end
42
+
43
+ escape_and_echo(body)
44
+ end
45
+
46
+ # @return [String]
47
+ def bootstrap_directory
48
+ "C:\\chef"
49
+ end
50
+
51
+ # @return [String]
52
+ def validation_key
53
+ escape_and_echo(super)
54
+ end
55
+
56
+ # @return [String]
57
+ def chef_run
58
+ "chef-client -j #{bootstrap_directory}\\first-boot.json -E #{environment}"
59
+ end
60
+
61
+ # @return [String]
62
+ def default_template
63
+ templates_path.join('windows_omnibus.erb').to_s
64
+ end
65
+
66
+ # @return [String]
67
+ def encrypted_data_bag_secret
68
+ return unless @encrypted_data_bag_secret
69
+
70
+ escape_and_echo(@encrypted_data_bag_secret)
71
+ end
72
+
73
+ # Implements a Powershell script that attempts a simple
74
+ # 'wget' to download the Chef msi
75
+ #
76
+ # @return [String]
77
+ def windows_wget_powershell
78
+ win_wget_ps = <<-WGET_PS
79
+ param(
80
+ [String] $remoteUrl,
81
+ [String] $localPath
82
+ )
83
+
84
+ $webClient = new-object System.Net.WebClient;
85
+
86
+ $webClient.DownloadFile($remoteUrl, $localPath);
87
+ WGET_PS
88
+
89
+ escape_and_echo(win_wget_ps)
90
+ end
91
+
92
+ # @return [String]
93
+ def install_chef
94
+ 'msiexec /qb /i "%LOCAL_DESTINATION_MSI_PATH%"'
95
+ end
96
+
97
+ # @return [String]
98
+ def first_boot
99
+ escape_and_echo(super)
100
+ end
101
+
102
+ # @return [String]
103
+ def set_path
104
+ "SET \"PATH=%PATH%;C:\\ruby\\bin;C:\\opscode\\chef\\bin;C:\\opscode\\chef\\embedded\\bin\"\n"
105
+ end
106
+
107
+ # @return [String]
108
+ def local_download_path
109
+ "%TEMP%\\chef-client-#{chef_version}.msi"
110
+ end
111
+
112
+ # escape WIN BATCH special chars
113
+ # and prefixes each line with an
114
+ # echo
115
+ def escape_and_echo(file_contents)
116
+ file_contents.gsub(/^(.*)$/, 'echo.\1').gsub(/([(<|>)^])/, '^\1')
117
+ end
118
+ end
119
+ end
120
+ end
@@ -144,14 +144,17 @@ module Ridley
144
144
  # @option options [Hash] :attributes
145
145
  # attributes of normal precedence to merge
146
146
  #
147
- # @return [Ridley::NodeResource]
147
+ # @return [Ridley::NodeObject]
148
148
  def merge_data(options = {})
149
- unless options[:run_list].nil?
150
- self.run_list = (self.run_list + Array(options[:run_list])).uniq
149
+ new_run_list = Array(options[:run_list])
150
+ new_attributes = options[:attributes]
151
+
152
+ unless new_run_list.empty?
153
+ self.run_list = self.run_list | new_run_list
151
154
  end
152
155
 
153
- unless options[:attributes].nil?
154
- self.normal = self.normal.deep_merge(options[:attributes])
156
+ unless new_attributes.nil?
157
+ self.normal = self.normal.deep_merge(new_attributes)
155
158
  end
156
159
 
157
160
  self
@@ -0,0 +1,207 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+
4
+ module Ridley
5
+ class ConnectorSupervisor < ::Celluloid::SupervisionGroup
6
+ # @param [Celluloid::Registry] registry
7
+ def initialize(registry)
8
+ super(registry)
9
+ supervise_as :ssh, HostConnector::SSH
10
+ supervise_as :winrm, HostConnector::WinRM
11
+ end
12
+ end
13
+
14
+ class HostCommander
15
+ class << self
16
+ # Checks to see if the given port is open for TCP connections
17
+ # on the given host.
18
+ #
19
+ # @param [String] host
20
+ # the host to attempt to connect to
21
+ # @param [Fixnum] port
22
+ # the port to attempt to connect on
23
+ # @param [Float] timeout
24
+ # the number of seconds to wait (default: {PORT_CHECK_TIMEOUT})
25
+ #
26
+ # @return [Boolean]
27
+ def connector_port_open?(host, port, timeout = nil)
28
+ Timeout.timeout(timeout || PORT_CHECK_TIMEOUT) { TCPSocket.new(host, port).close; true }
29
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL => ex
30
+ false
31
+ end
32
+ end
33
+
34
+ include Celluloid
35
+ include Ridley::Logging
36
+
37
+ PORT_CHECK_TIMEOUT = 3
38
+
39
+ finalizer :finalize_callback
40
+
41
+ def initialize
42
+ @connector_registry = Celluloid::Registry.new
43
+ @connector_supervisor = ConnectorSupervisor.new_link(@connector_registry)
44
+ end
45
+
46
+ # Execute a shell command on a node
47
+ #
48
+ # @param [String] host
49
+ # the host to perform the action on
50
+ # @param [String] command
51
+ #
52
+ # @option options [Hash] :ssh
53
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on
54
+ # * :password (String) the password for the shell user that will perform the bootstrap
55
+ # * :keys (Array, String) an array of key(s) to authenticate the ssh user with instead of a password
56
+ # * :timeout (Float) timeout value for SSH bootstrap (5.0)
57
+ # * :sudo (Boolean) run as sudo
58
+ # @option options [Hash] :winrm
59
+ # * :user (String) a user that will login to each node and perform the bootstrap command on
60
+ # * :password (String) the password for the user that will perform the bootstrap (required)
61
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
62
+ #
63
+ # @return [HostConnector::Response]
64
+ def run(host, command, options = {})
65
+ execute(__method__, host, command, options)
66
+ end
67
+
68
+ # Bootstrap a node
69
+ #
70
+ # @param [String] host
71
+ # the host to perform the action on
72
+ #
73
+ # @option options [Hash] :ssh
74
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on
75
+ # * :password (String) the password for the shell user that will perform the bootstrap
76
+ # * :keys (Array, String) an array of key(s) to authenticate the ssh user with instead of a password
77
+ # * :timeout (Float) timeout value for SSH bootstrap (5.0)
78
+ # * :sudo (Boolean) run as sudo
79
+ # @option options [Hash] :winrm
80
+ # * :user (String) a user that will login to each node and perform the bootstrap command on
81
+ # * :password (String) the password for the user that will perform the bootstrap (required)
82
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
83
+ #
84
+ # @return [HostConnector::Response]
85
+ def bootstrap(host, options = {})
86
+ execute(__method__, host, options)
87
+ end
88
+
89
+ # Perform a chef client run on a node
90
+ #
91
+ # @param [String] host
92
+ # the host to perform the action on
93
+ #
94
+ # @option options [Hash] :ssh
95
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on
96
+ # * :password (String) the password for the shell user that will perform the bootstrap
97
+ # * :keys (Array, String) an array of key(s) to authenticate the ssh user with instead of a password
98
+ # * :timeout (Float) timeout value for SSH bootstrap (5.0)
99
+ # * :sudo (Boolean) run as sudo
100
+ # @option options [Hash] :winrm
101
+ # * :user (String) a user that will login to each node and perform the bootstrap command on
102
+ # * :password (String) the password for the user that will perform the bootstrap (required)
103
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
104
+ #
105
+ # @return [HostConnector::Response]
106
+ def chef_client(host, options = {})
107
+ execute(__method__, host, options)
108
+ end
109
+
110
+ # Write your encrypted data bag secret on a node
111
+ #
112
+ # @param [String] host
113
+ # the host to perform the action on
114
+ # @param [String] secret
115
+ # your organization's encrypted data bag secret
116
+ #
117
+ # @option options [Hash] :ssh
118
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on
119
+ # * :password (String) the password for the shell user that will perform the bootstrap
120
+ # * :keys (Array, String) an array of key(s) to authenticate the ssh user with instead of a password
121
+ # * :timeout (Float) timeout value for SSH bootstrap (5.0)
122
+ # * :sudo (Boolean) run as sudo
123
+ # @option options [Hash] :winrm
124
+ # * :user (String) a user that will login to each node and perform the bootstrap command on
125
+ # * :password (String) the password for the user that will perform the bootstrap (required)
126
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
127
+ #
128
+ # @return [HostConnector::Response]
129
+ def put_secret(host, secret, options = {})
130
+ execute(__method__, host, secret, options)
131
+ end
132
+
133
+ # Execute line(s) of Ruby code on a node using Chef's embedded Ruby
134
+ #
135
+ # @param [String] host
136
+ # the host to perform the action on
137
+ # @param [Array<String>] command_lines
138
+ # An Array of lines of the command to be executed
139
+ #
140
+ # @option options [Hash] :ssh
141
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on
142
+ # * :password (String) the password for the shell user that will perform the bootstrap
143
+ # * :keys (Array, String) an array of key(s) to authenticate the ssh user with instead of a password
144
+ # * :timeout (Float) timeout value for SSH bootstrap (5.0)
145
+ # * :sudo (Boolean) run as sudo
146
+ # @option options [Hash] :winrm
147
+ # * :user (String) a user that will login to each node and perform the bootstrap command on
148
+ # * :password (String) the password for the user that will perform the bootstrap (required)
149
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
150
+ #
151
+ # @return [HostConnector::Response]
152
+ def ruby_script(host, command_lines, options = {})
153
+ execute(__method__, host, command_lines, options)
154
+ end
155
+
156
+ private
157
+
158
+ def execute(method, host, *args)
159
+ options = args.last.is_a?(Hash) ? args.pop : Hash.new
160
+
161
+ connector_for(host, options).send(method, host, *args, options)
162
+ rescue Errors::HostConnectionError => ex
163
+ abort(ex)
164
+ end
165
+
166
+ # Finds and returns the best HostConnector for a given host
167
+ #
168
+ # @param [String] host
169
+ # the host to attempt to connect to
170
+ # @option options [Hash] :ssh
171
+ # * :port (Fixnum) the ssh port to connect on the node the bootstrap will be performed on (22)
172
+ # * :timeout (Float) [5.0] timeout value for testing SSH connection
173
+ # @option options [Hash] :winrm
174
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
175
+ # @param block [Proc]
176
+ # an optional block that is yielded the best HostConnector
177
+ #
178
+ # @return [Symbol]
179
+ def connector_for(host, options = {})
180
+ options = options.reverse_merge(ssh: Hash.new, winrm: Hash.new)
181
+ options[:ssh][:port] ||= HostConnector::SSH::DEFAULT_PORT
182
+ options[:winrm][:port] ||= HostConnector::WinRM::DEFAULT_PORT
183
+
184
+ if self.class.connector_port_open?(host, options[:winrm][:port])
185
+ options.delete(:ssh)
186
+ winrm
187
+ elsif self.class.connector_port_open?(host, options[:ssh][:port], options[:ssh][:timeout])
188
+ options.delete(:winrm)
189
+ ssh
190
+ else
191
+ raise Errors::HostConnectionError, "No connector ports open on '#{host}'"
192
+ end
193
+ end
194
+
195
+ def finalize_callback
196
+ @connector_supervisor.terminate if @connector_supervisor && @connector_supervisor.alive?
197
+ end
198
+
199
+ def ssh
200
+ @connector_registry[:ssh]
201
+ end
202
+
203
+ def winrm
204
+ @connector_registry[:winrm]
205
+ end
206
+ end
207
+ end