ridley 0.5.2 → 0.6.0

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/.travis.yml CHANGED
@@ -4,3 +4,7 @@ rvm:
4
4
  - 1.9.2
5
5
  - 1.9.3
6
6
  - jruby-19mode
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: jruby-19mode
10
+
data/Guardfile CHANGED
@@ -12,7 +12,7 @@ guard 'yard', stdout: '/dev/null', stderr: '/dev/null' do
12
12
  watch(%r{ext/.+\.c})
13
13
  end
14
14
 
15
- guard 'rspec', version: 2, cli: "--color --drb --format Fuubar", all_on_start: false, all_after_pass: false do
15
+ guard 'rspec', cli: "--color --drb --format Fuubar", all_on_start: false, all_after_pass: false do
16
16
  watch(%r{^spec/unit/.+_spec\.rb$})
17
17
  watch(%r{^spec/acceptance/.+_spec\.rb$})
18
18
 
data/README.md CHANGED
@@ -291,22 +291,12 @@ Given the previous example you could set the default node attribute with the `se
291
291
 
292
292
  ### Node Attributes
293
293
 
294
- Setting the `default[:my_app][:billing][:enabled]` node level default attribute on the node "jwinsor-1"
294
+ Setting the `node[:my_app][:billing][:enabled]` node level attribute on the node "jwinsor-1"
295
295
 
296
296
  conn = Ridley.connection
297
297
  conn.sync do
298
298
  obj = node.find("jwinsor-1")
299
- obj.set_default_attribute("my_app.billing.enabled", false)
300
- obj.save
301
- end
302
-
303
- Other attribute precedence levels can be set with their own respective set attribute functions
304
-
305
- conn = Ridley.connection
306
- conn.sync do
307
- obj = node.find("jwinsor-1")
308
- obj.set_override_attribute("my_app.proxy.enabled", false)
309
- obj.set_normal_attribute("my_app.webapp.enabled", false)
299
+ obj.set_attribute("my_app.billing.enabled", false)
310
300
  obj.save
311
301
  end
312
302
 
@@ -64,6 +64,9 @@ cat <<'EOP'
64
64
  EOP
65
65
  ) > /etc/chef/client.rb
66
66
 
67
+ <%# Remove client pem if it already exists -%>
68
+ rm /etc/chef/client.pem
69
+
67
70
  (
68
71
  cat <<'EOP'
69
72
  <%= first_boot %>
@@ -146,7 +146,7 @@ CONFIG
146
146
 
147
147
  # @return [String]
148
148
  def first_boot
149
- attributes.merge(run_list: run_list).to_json
149
+ MultiJson.encode attributes.merge(run_list: run_list)
150
150
  end
151
151
 
152
152
  # The validation key to create a new client for the node
@@ -155,7 +155,7 @@ CONFIG
155
155
  #
156
156
  # @return [String]
157
157
  def validation_key
158
- IO.read(validator_path).chomp
158
+ IO.read(File.expand_path(validator_path)).chomp
159
159
  rescue Errno::ENOENT
160
160
  raise Errors::ValidatorNotFound, "Error bootstrapping: Validator not found at '#{validator_path}'"
161
161
  end
@@ -25,43 +25,45 @@ module Ridley
25
25
  attr_reader :contexts
26
26
 
27
27
  # @return [Hash]
28
- attr_reader :ssh_config
28
+ attr_reader :options
29
29
 
30
30
  # @param [Array<#to_s>] hosts
31
- # @option options [String] :ssh_user
32
- # @option options [String] :ssh_password
33
- # @option options [Array<String>, String] :ssh_keys
34
- # @option options [Float] :ssh_timeout
35
- # timeout value for SSH bootstrap (default: 1.5)
31
+ # @option options [Hash] :ssh
32
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on (required)
33
+ # * :password (String) the password for the shell user that will perform the bootstrap
34
+ # * :keys (Array, String) an array of keys (or a single key) to authenticate the ssh user with instead of a password
35
+ # * :timeout (Float) [5.0] timeout value for SSH bootstrap
36
36
  # @option options [String] :validator_client
37
37
  # @option options [String] :validator_path
38
38
  # filepath to the validator used to bootstrap the node (required)
39
- # @option options [String] :bootstrap_proxy
40
- # URL to a proxy server to bootstrap through (default: nil)
41
- # @option options [String] :encrypted_data_bag_secret_path
42
- # filepath on your host machine to your organizations encrypted data bag secret (default: nil)
43
- # @option options [Hash] :hints
44
- # a hash of Ohai hints to place on the bootstrapped node (default: Hash.new)
45
- # @option options [Hash] :attributes
46
- # a hash of attributes to use in the first Chef run (default: Hash.new)
47
- # @option options [Array] :run_list
48
- # an initial run list to bootstrap with (default: Array.new)
49
- # @option options [String] :chef_version
50
- # version of Chef to install on the node (default: {Ridley::CHEF_VERSION})
51
- # @option options [String] :environment
52
- # environment to join the node to (default: '_default')
53
- # @option options [Boolean] :sudo
39
+ # @option options [String] :bootstrap_proxy (nil)
40
+ # URL to a proxy server to bootstrap through
41
+ # @option options [String] :encrypted_data_bag_secret_path (nil)
42
+ # filepath on your host machine to your organizations encrypted data bag secret
43
+ # @option options [Hash] :hints (Hash.new)
44
+ # a hash of Ohai hints to place on the bootstrapped node
45
+ # @option options [Hash] :attributes (Hash.new)
46
+ # a hash of attributes to use in the first Chef run
47
+ # @option options [Array] :run_list (Array.new)
48
+ # an initial run list to bootstrap with
49
+ # @option options [String] :chef_version (Ridley::CHEF_VERSION)
50
+ # version of Chef to install on the node
51
+ # @option options [String] :environment ('_default')
52
+ # environment to join the node to
53
+ # @option options [Boolean] :sudo (true)
54
54
  # bootstrap with sudo (default: true)
55
- # @option options [String] :template
56
- # bootstrap template to use (default: omnibus)
55
+ # @option options [String] :template ('omnibus')
56
+ # bootstrap template to use
57
57
  def initialize(hosts, options = {})
58
- @hosts = Array(hosts).collect(&:to_s).uniq
59
- @ssh_config = {
60
- user: options.fetch(:ssh_user),
61
- password: options[:ssh_password],
62
- keys: options[:ssh_keys],
63
- timeout: (options[:ssh_timeout] || 1.5)
64
- }
58
+ @hosts = Array(hosts).collect(&:to_s).uniq
59
+ @options = options.dup
60
+ @options[:ssh] ||= Hash.new
61
+ @options[:ssh] = {
62
+ timeout: 5.0,
63
+ sudo: true
64
+ }.merge(@options[:ssh])
65
+
66
+ @options[:sudo] = @options[:ssh][:sudo]
65
67
 
66
68
  @contexts = @hosts.collect do |host|
67
69
  Context.new(host, options)
@@ -70,30 +72,22 @@ module Ridley
70
72
 
71
73
  # @return [SSH::ResponseSet]
72
74
  def run
73
- if contexts.length >= 2
74
- pool = SSH::Worker.pool(size: contexts.length, args: [self.ssh_config])
75
- else
76
- pool = SSH::Worker.new(self.ssh_config)
77
- end
75
+ workers = Array.new
76
+ futures = contexts.collect do |context|
77
+ info "Running bootstrap command on #{context.host}"
78
78
 
79
- responses = contexts.collect do |context|
80
- pool.future.run(context.host, context.boot_command)
81
- end.collect(&:value)
79
+ workers << worker = SSH::Worker.new_link(self.options[:ssh].freeze)
80
+ worker.future.run(context.host, context.boot_command)
81
+ end
82
82
 
83
83
  SSH::ResponseSet.new.tap do |response_set|
84
- responses.each do |message|
85
- status, response = message
86
-
87
- case status
88
- when :ok
89
- response_set.add_ok(response)
90
- when :error
91
- response_set.add_error(response)
92
- end
84
+ futures.each do |future|
85
+ status, response = future.value
86
+ response_set.add_response(response)
93
87
  end
94
88
  end
95
89
  ensure
96
- pool.terminate if pool
90
+ workers.map(&:terminate)
97
91
  end
98
92
  end
99
93
  end
@@ -3,7 +3,10 @@ module Ridley
3
3
  class Connection
4
4
  class << self
5
5
  def sync(options, &block)
6
- new(options).sync(&block)
6
+ conn = new(options)
7
+ conn.sync(&block)
8
+ ensure
9
+ conn.terminate if conn && conn.alive?
7
10
  end
8
11
 
9
12
  # @raise [ArgumentError]
@@ -16,6 +19,12 @@ module Ridley
16
19
  missing.collect! { |opt| "'#{opt}'" }
17
20
  raise ArgumentError, "Missing required option(s): #{missing.join(', ')}"
18
21
  end
22
+
23
+ missing_values = options.slice(*REQUIRED_OPTIONS).select { |key, value| !value.present? }
24
+ unless missing_values.empty?
25
+ values = missing_values.keys.collect { |opt| "'#{opt}'" }
26
+ raise ArgumentError, "Missing value for required option(s): '#{values.join(', ')}'"
27
+ end
19
28
  end
20
29
 
21
30
  # A hash of default options to be used in the Connection initializer
@@ -30,17 +39,20 @@ module Ridley
30
39
  end
31
40
 
32
41
  extend Forwardable
42
+
43
+ include Celluloid
33
44
  include Ridley::DSL
45
+ include Ridley::Logging
34
46
 
35
- attr_reader :client_name
36
- attr_reader :client_key
37
47
  attr_reader :organization
38
- attr_reader :ssh
39
48
 
40
- attr_reader :validator_client
41
- attr_reader :validator_path
42
- attr_reader :encrypted_data_bag_secret_path
49
+ attr_accessor :client_name
50
+ attr_accessor :client_key
51
+ attr_accessor :validator_client
52
+ attr_accessor :validator_path
53
+ attr_accessor :encrypted_data_bag_secret_path
43
54
 
55
+ attr_accessor :ssh
44
56
  attr_accessor :thread_count
45
57
 
46
58
  def_delegator :conn, :build_url
@@ -60,6 +72,18 @@ module Ridley
60
72
 
61
73
  def_delegator :conn, :in_parallel
62
74
 
75
+ OPTIONS = [
76
+ :server_url,
77
+ :client_name,
78
+ :client_key,
79
+ :organization,
80
+ :validator_client,
81
+ :validator_path,
82
+ :encrypted_data_bag_secret_path,
83
+ :thread_count,
84
+ :ssl
85
+ ].freeze
86
+
63
87
  REQUIRED_OPTIONS = [
64
88
  :server_url,
65
89
  :client_name,
@@ -73,20 +97,20 @@ module Ridley
73
97
  # @option options [String] :client_name
74
98
  # name of the client used to authenticate with the Chef API
75
99
  # @option options [String] :client_key
76
- # filepath to the client's private key used to authenticate with
77
- # the Chef API
100
+ # filepath to the client's private key used to authenticate with the Chef API
78
101
  # @option options [String] :organization
79
102
  # the Organization to connect to. This is only used if you are connecting to
80
103
  # private Chef or hosted Chef
81
- # @option options [String] :validator_client
82
- # (default: nil)
83
- # @option options [String] :validator_path
84
- # (default: nil)
85
- # @option options [String] :encrypted_data_bag_secret_path
86
- # (default: nil)
87
- # @option options [Integer] :thread_count
88
- # @option options [Hash] :ssh
89
- # authentication credentials for bootstrapping or connecting to nodes (default: Hash.new)
104
+ # @option options [String] :validator_client (nil)
105
+ # @option options [String] :validator_path (nil)
106
+ # @option options [String] :encrypted_data_bag_secret_path (nil)
107
+ # @option options [Integer] :thread_count (DEFAULT_THREAD_COUNT)
108
+ # @option options [Hash] :ssh (Hash.new)
109
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on (required)
110
+ # * :password (String) the password for the shell user that will perform the bootstrap
111
+ # * :keys (Array, String) an array of keys (or a single key) to authenticate the ssh user with instead of a password
112
+ # * :timeout (Float) [5.0] timeout value for SSH bootstrap
113
+ # * :sudo (Boolean) [true] bootstrap with sudo
90
114
  # @option options [Hash] :params
91
115
  # URI query unencoded key/value pairs
92
116
  # @option options [Hash] :headers
@@ -94,10 +118,18 @@ module Ridley
94
118
  # @option options [Hash] :request
95
119
  # request options
96
120
  # @option options [Hash] :ssl
97
- # SSL options
121
+ # * :verify (Boolean) [true] set to false to disable SSL verification
98
122
  # @option options [URI, String, Hash] :proxy
99
123
  # URI, String, or Hash of HTTP proxy options
100
124
  def initialize(options = {})
125
+ log.info { "Ridley starting..." }
126
+ configure(options)
127
+ end
128
+
129
+ # Configure this instance of Ridley::Connection
130
+ #
131
+ # @param [Hash] options
132
+ def configure(options)
101
133
  options = self.class.default_options.merge(options)
102
134
  self.class.validate_options(options)
103
135
 
@@ -180,6 +212,10 @@ module Ridley
180
212
  raise Errors::EncryptedDataBagSecretNotFound, "Encrypted data bag secret provided but not found at '#{encrypted_data_bag_secret_path}'"
181
213
  end
182
214
 
215
+ def finalize
216
+ log.info { "Ridley stopping..." }
217
+ end
218
+
183
219
  private
184
220
 
185
221
  attr_reader :conn
@@ -19,6 +19,7 @@ module Ridley
19
19
  def set_logger(obj)
20
20
  @logger = (obj.nil? ? Logger.new('/dev/null') : obj)
21
21
  end
22
+ alias_method :logger=, :set_logger
22
23
  end
23
24
 
24
25
  # @return [Logger]
@@ -234,13 +234,26 @@ module Ridley
234
234
  # if the resource does not pass validations
235
235
  #
236
236
  # @return [Boolean]
237
- # true if successful and false for failure
238
237
  def save
239
238
  raise Errors::InvalidResource.new(self.errors) unless valid?
240
239
 
241
240
  self.attributes = self.class.create(connection, self).attributes
242
241
  true
243
242
  rescue Errors::HTTPConflict
243
+ self.update
244
+ true
245
+ end
246
+
247
+ # Updates the instantiated resource on the target remote with any changes made
248
+ # to self
249
+ #
250
+ # @raise [Errors::InvalidResource]
251
+ # if the resource does not pass validations
252
+ #
253
+ # @return [Boolean]
254
+ def update
255
+ raise Errors::InvalidResource.new(self.errors) unless valid?
256
+
244
257
  self.attributes = self.class.update(connection, self).attributes
245
258
  true
246
259
  end
@@ -5,11 +5,11 @@ module Ridley
5
5
  # @overload bootstrap(connection, nodes, options = {})
6
6
  # @param [Ridley::Connection] connection
7
7
  # @param [Array<String>, String] nodes
8
- # @option options [String] :ssh_user
9
- # @option options [String] :ssh_password
10
- # @option options [Array<String>, String] :ssh_keys
11
- # @option options [Float] :ssh_timeout
12
- # timeout value for SSH bootstrap (default: 1.5)
8
+ # @param [Hash] ssh
9
+ # * :user (String) a shell user that will login to each node and perform the bootstrap command on (required)
10
+ # * :password (String) the password for the shell user that will perform the bootstrap
11
+ # * :keys (Array, String) an array of keys (or a single key) to authenticate the ssh user with instead of a password
12
+ # * :timeout (Float) [5.0] timeout value for SSH bootstrap
13
13
  # @option options [String] :validator_client
14
14
  # @option options [String] :validator_path
15
15
  # filepath to the validator used to bootstrap the node (required)
@@ -31,22 +31,40 @@ module Ridley
31
31
  # bootstrap with sudo (default: true)
32
32
  # @option options [String] :template
33
33
  # bootstrap template to use (default: omnibus)
34
+ #
35
+ # @return [SSH::ResponseSet]
34
36
  def bootstrap(connection, *args)
35
- options = args.last.is_a?(Hash) ? args.pop : Hash.new
37
+ options = args.extract_options!
36
38
 
37
39
  default_options = {
38
40
  server_url: connection.server_url,
39
- ssh_user: connection.ssh[:user],
40
- ssh_password: connection.ssh[:password],
41
- ssh_timeout: connection.ssh[:timeout],
42
41
  validator_path: connection.validator_path,
43
42
  validator_client: connection.validator_client,
44
- encrypted_data_bag_secret_path: connection.encrypted_data_bag_secret_path
43
+ encrypted_data_bag_secret_path: connection.encrypted_data_bag_secret_path,
44
+ ssh: connection.ssh
45
45
  }
46
46
 
47
47
  options = default_options.merge(options)
48
48
 
49
- Bootstrapper.new(args, options).run
49
+ Bootstrapper.new(*args, options).run
50
+ end
51
+
52
+ # Merges the given data with the the data of the target node on the remote
53
+ #
54
+ # @param [Ridley::Cnonection] connection
55
+ # @param [Ridley::Node, String] target
56
+ # node or identifier of the node to merge
57
+ # @option options [Array] :run_list
58
+ # run list items to merge
59
+ # @option options [Hash] :attributes
60
+ # attributes of normal precedence to merge
61
+ #
62
+ # @raise [Errors::HTTPNotFound]
63
+ # if the target node is not found
64
+ #
65
+ # @return [Ridley::Node]
66
+ def merge_data(connection, target, options = {})
67
+ find!(connection, target).merge_data(options)
50
68
  end
51
69
  end
52
70
 
@@ -184,12 +202,65 @@ module Ridley
184
202
 
185
203
  # Run Chef-Client on the instantiated node
186
204
  #
205
+ # @param [Hash] options
206
+ # a hash of options to pass to {Ridley::SSH.start}
207
+ #
187
208
  # @return [SSH::Response]
188
- def chef_client
189
- Ridley::SSH.start(self, connection.ssh) do |ssh|
209
+ def chef_client(options = {})
210
+ options = connection.ssh.merge(options)
211
+
212
+ Ridley.log.debug "Running Chef Client on: #{self.public_hostname}"
213
+ Ridley::SSH.start(self, options) do |ssh|
190
214
  ssh.run("sudo chef-client").first
191
215
  end
192
216
  end
217
+
218
+ # Put the connection's encrypted data bag secret onto the instantiated node. If no
219
+ # encrypted data bag key path is set on the resource's connection then nil will be
220
+ # returned
221
+ #
222
+ # @param [Hash] options
223
+ # a hash of options to pass to {Ridley::SSH.start}
224
+ #
225
+ # @return [SSH::Response, nil]
226
+ def put_secret(options = {})
227
+ if connection.encrypted_data_bag_secret_path.nil? ||
228
+ !File.exists?(connection.encrypted_data_bag_secret_path)
229
+
230
+ return nil
231
+ end
232
+
233
+ options = connection.ssh.merge(options)
234
+ secret = File.read(connection.encrypted_data_bag_secret_path).chomp
235
+ command = "echo '#{secret}' > /etc/chef/encrypted_data_bag_secret; chmod 0600 /etc/chef/encrypted_data_bag_secret"
236
+
237
+ Ridley.log.debug "Writing Encrypted Data Bag Secret to: #{self.public_hostname}"
238
+ Ridley::SSH.start(self, options) do |ssh|
239
+ ssh.run(command).first
240
+ end
241
+ end
242
+
243
+ # Merges the instaniated nodes data with the given data and updates
244
+ # the remote with the merged results
245
+ #
246
+ # @option options [Array] :run_list
247
+ # run list items to merge
248
+ # @option options [Hash] :attributes
249
+ # attributes of normal precedence to merge
250
+ #
251
+ # @return [Ridley::Node]
252
+ def merge_data(options = {})
253
+ unless options[:run_list].nil?
254
+ self.run_list = (self.run_list + Array(options[:run_list])).uniq
255
+ end
256
+
257
+ unless options[:attributes].nil?
258
+ self.normal = self.normal.deep_merge(options[:attributes])
259
+ end
260
+
261
+ self.update
262
+ self
263
+ end
193
264
  end
194
265
 
195
266
  module DSL
@@ -3,15 +3,19 @@ module Ridley
3
3
  class << self
4
4
  # @param [Ridley::Connection] connection
5
5
  # @param [Array] checksums
6
+ # @option options [Integer] :size (12)
7
+ # size of the upload pool
6
8
  #
7
9
  # @return [Ridley::Sandbox]
8
- def create(connection, checksums = [])
10
+ def create(connection, checksums = [], options = {})
11
+ options.reverse_merge!(size: 12)
12
+
9
13
  sumhash = { checksums: Hash.new }.tap do |chks|
10
14
  Array(checksums).each { |chk| chks[:checksums][chk] = nil }
11
15
  end
12
16
 
13
17
  attrs = connection.post("sandboxes", sumhash.to_json).body
14
- new(connection, attrs[:sandbox_id], attrs[:checksums])
18
+ pool(size: options[:size], args: [connection, attrs[:sandbox_id], attrs[:checksums]])
15
19
  end
16
20
 
17
21
  # Checksum the file at the given filepath for a Chef API.
@@ -43,23 +47,51 @@ module Ridley
43
47
  end
44
48
  digest.hexdigest
45
49
  end
50
+
51
+ def future(connection, *args)
52
+ puts connection
53
+ connection.future(*args)
54
+ end
46
55
  end
47
56
 
57
+ include Celluloid
58
+
48
59
  attr_reader :sandbox_id
49
60
  attr_reader :checksums
50
61
 
51
62
  def initialize(connection, id, checksums)
52
63
  @connection = connection
53
64
  @sandbox_id = id
54
- @checksums = checksums
65
+ @checksums = checksums
55
66
  end
56
67
 
57
68
  def checksum(chk_id)
58
69
  checksums.fetch(chk_id.to_sym)
59
70
  end
60
71
 
72
+ # Concurrently upload multiple files into a sandbox
73
+ #
74
+ # @param [Hash] checksums
75
+ # a hash of file checksums and file paths
76
+ #
77
+ # @example uploading multiple checksums
78
+ #
79
+ # sandbox.multi_upload(
80
+ # "e5a0f6b48d0712382295ff30bec1f9cc" => "/Users/reset/code/rbenv-cookbook/recipes/default.rb",
81
+ # "de6532a7fbe717d52020dc9f3ae47dbe" => "/Users/reset/code/rbenv-cookbook/recipes/ohai_plugin.rb"
82
+ # )
83
+ def multi_upload(checksums)
84
+ checksums.collect do |chk_id, path|
85
+ future.upload(chk_id, path)
86
+ end.map(&:value)
87
+ end
88
+
89
+ # Upload one file into the sandbox for the given checksum id
90
+ #
61
91
  # @param [String] chk_id
92
+ # checksum of the file being uploaded
62
93
  # @param [String] path
94
+ # path to the file to upload
63
95
  #
64
96
  # @return [Hash, nil]
65
97
  def upload(chk_id, path)
@@ -85,9 +117,9 @@ module Ridley
85
117
  # value of the given checksum.
86
118
  conn = connection.send(:conn).dup
87
119
 
88
- url = URI(checksum[:url])
120
+ url = URI(checksum[:url])
89
121
  upload_path = url.path
90
- url.path = ""
122
+ url.path = ""
91
123
 
92
124
  conn.url_prefix = url.to_s
93
125
 
@@ -95,7 +127,7 @@ module Ridley
95
127
  end
96
128
 
97
129
  def commit
98
- connection.put("sandboxes/#{sandbox_id}", { is_completed: true }.to_json).body
130
+ connection.put("sandboxes/#{sandbox_id}", MultiJson.encode(is_completed: true)).body
99
131
  end
100
132
 
101
133
  def to_s
@@ -1,7 +1,22 @@
1
1
  module Ridley
2
2
  class SSH
3
3
  # @author Jamie Winsor <jamie@vialstudios.com>
4
- class Response < Struct.new(:stdout, :stderr, :exit_code, :exit_signal)
4
+ class Response
5
+ attr_reader :host
6
+
7
+ attr_accessor :stdout
8
+ attr_accessor :stderr
9
+ attr_accessor :exit_code
10
+ attr_accessor :exit_signal
11
+
12
+ def initialize(host, options = {})
13
+ @host = host
14
+ @stdout = options[:stdout] || String.new
15
+ @stderr = options[:stderr] || String.new
16
+ @exit_code = options[:exit_code] || -1
17
+ @exit_signal = options[:exit_signal] || nil
18
+ end
19
+
5
20
  # Return true if the response was not successful
6
21
  #
7
22
  # @return [Boolean]