ridley 0.5.2 → 0.6.0

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