ridley 0.12.4 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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