ridley 0.3.2 → 0.4.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/.rbenv-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p286
data/.travis.yml CHANGED
@@ -3,3 +3,4 @@ language: ruby
3
3
  rvm:
4
4
  - 1.9.2
5
5
  - 1.9.3
6
+ - jruby-19mode
data/Gemfile CHANGED
@@ -1,3 +1,48 @@
1
- source 'https://rubygems.org'
1
+ source :rubygems
2
2
 
3
3
  gemspec
4
+
5
+ platforms :jruby do
6
+ gem 'jruby-openssl'
7
+ end
8
+
9
+ group :development do
10
+ gem 'yard'
11
+ gem 'spork'
12
+ gem 'guard', '>= 1.5.0'
13
+ gem 'guard-yard'
14
+ gem 'guard-rspec'
15
+ gem 'guard-spork', platforms: :ruby
16
+ gem 'coolline'
17
+ gem 'redcarpet', platforms: :ruby
18
+ gem 'kramdown', platforms: :jruby
19
+
20
+ require 'rbconfig'
21
+
22
+ if RbConfig::CONFIG['target_os'] =~ /darwin/i
23
+ gem 'growl', require: false
24
+ gem 'rb-fsevent', require: false
25
+
26
+ if `uname`.strip == 'Darwin' && `sw_vers -productVersion`.strip >= '10.8'
27
+ gem 'terminal-notifier-guard', '~> 1.5.3', require: false
28
+ end rescue Errno::ENOENT
29
+
30
+ elsif RbConfig::CONFIG['target_os'] =~ /linux/i
31
+ gem 'libnotify', '~> 0.8.0', require: false
32
+ gem 'rb-inotify', require: false
33
+
34
+ elsif RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
35
+ gem 'win32console', require: false
36
+ gem 'rb-notifu', '>= 0.0.4', require: false
37
+ gem 'wdm', require: false
38
+ end
39
+ end
40
+
41
+ group :test do
42
+ gem 'thor'
43
+ gem 'rake', '>= 0.9.2.2'
44
+ gem 'rspec'
45
+ gem 'fuubar'
46
+ gem 'json_spec'
47
+ gem 'webmock'
48
+ end
data/Guardfile CHANGED
@@ -1,5 +1,4 @@
1
1
  notification :off
2
- interactor :coolline
3
2
 
4
3
  guard 'spork' do
5
4
  watch('Gemfile')
data/README.md CHANGED
@@ -345,6 +345,21 @@ And the same goes for setting an environment level override attribute
345
345
  obj.save
346
346
  end
347
347
 
348
+ ## Bootstrapping nodes
349
+
350
+ conn = Ridley.connection(
351
+ server_url: "https://api.opscode.com",
352
+ organization: "vialstudios",
353
+ validator_client: "vialstudios-validator",
354
+ validator_path: "/Users/reset/.chef/vialstudios-validator.pem",
355
+ ssh: {
356
+ user: "vagrant",
357
+ password: "vagrant"
358
+ }
359
+ )
360
+
361
+ conn.node.bootstrap("33.33.33.10", "33.33.33.11")
362
+
348
363
  # Authors and Contributors
349
364
 
350
365
  * Jamie Winsor (<jamie@vialstudios.com>)
data/Thorfile CHANGED
@@ -5,25 +5,28 @@ require 'bundler'
5
5
  require 'bundler/setup'
6
6
 
7
7
  require 'ridley'
8
- require 'thor/rake_compat'
9
8
 
10
9
  class Default < Thor
11
- include Thor::RakeCompat
12
- Bundler::GemHelper.install_tasks
13
-
14
- desc "build", "Build ridley-#{Ridley::VERSION}.gem into the pkg directory"
15
- def build
16
- Rake::Task["build"].execute
17
- end
10
+ unless jruby?
11
+ require 'thor/rake_compat'
12
+
13
+ include Thor::RakeCompat
14
+ Bundler::GemHelper.install_tasks
15
+
16
+ desc "build", "Build ridley-#{Ridley::VERSION}.gem into the pkg directory"
17
+ def build
18
+ Rake::Task["build"].execute
19
+ end
18
20
 
19
- desc "install", "Build and install ridley-#{Ridley::VERSION}.gem into system gems"
20
- def install
21
- Rake::Task["install"].execute
22
- end
21
+ desc "install", "Build and install ridley-#{Ridley::VERSION}.gem into system gems"
22
+ def install
23
+ Rake::Task["install"].execute
24
+ end
23
25
 
24
- desc "release", "Create tag v#{Ridley::VERSION} and build and push ridley-#{Ridley::VERSION}.gem to Rubygems"
25
- def release
26
- Rake::Task["release"].execute
26
+ desc "release", "Create tag v#{Ridley::VERSION} and build and push ridley-#{Ridley::VERSION}.gem to Rubygems"
27
+ def release
28
+ Rake::Task["release"].execute
29
+ end
27
30
  end
28
31
 
29
32
  class Spec < Thor
@@ -0,0 +1,73 @@
1
+ bash -c '
2
+ <%= "export http_proxy=\"#{bootstrap_proxy}\"" if bootstrap_proxy -%>
3
+
4
+ exists() {
5
+ if command -v $1 &>/dev/null
6
+ then
7
+ return 0
8
+ else
9
+ return 1
10
+ fi
11
+ }
12
+
13
+ install_sh="http://opscode.com/chef/install.sh"
14
+ version_string="-v <%= chef_version %>"
15
+
16
+ if ! exists /usr/bin/chef-client; then
17
+ if exists wget; then
18
+ bash <(wget <%= "--proxy=on " if bootstrap_proxy %> ${install_sh} -O -) ${version_string}
19
+ else
20
+ if exists curl; then
21
+ bash <(curl -L <%= "--proxy=on " if bootstrap_proxy %> ${install_sh}) ${version_string}
22
+ fi
23
+ fi
24
+ fi
25
+
26
+ mkdir -p /etc/chef
27
+
28
+ (
29
+ cat <<'EOP'
30
+ <%= validation_key %>
31
+ EOP
32
+ ) > /tmp/validation.pem
33
+ awk NF /tmp/validation.pem > /etc/chef/validation.pem
34
+ rm /tmp/validation.pem
35
+ chmod 0600 /etc/chef/validation.pem
36
+
37
+ <% if encrypted_data_bag_secret -%>
38
+ (
39
+ cat <<'EOP'
40
+ <%= encrypted_data_bag_secret %>
41
+ EOP
42
+ ) > /tmp/encrypted_data_bag_secret
43
+ awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
44
+ rm /tmp/encrypted_data_bag_secret
45
+ chmod 0600 /etc/chef/encrypted_data_bag_secret
46
+ <% end -%>
47
+
48
+ <%# Generate Ohai Hints -%>
49
+ <% unless hints.empty? -%>
50
+ mkdir -p /etc/chef/ohai/hints
51
+
52
+ <% hints.each do |name, hash| -%>
53
+ (
54
+ cat <<'EOP'
55
+ <%= hash.to_json %>
56
+ EOP
57
+ ) > /etc/chef/ohai/hints/<%= name %>.json
58
+ <% end -%>
59
+ <% end -%>
60
+
61
+ (
62
+ cat <<'EOP'
63
+ <%= chef_config %>
64
+ EOP
65
+ ) > /etc/chef/client.rb
66
+
67
+ (
68
+ cat <<'EOP'
69
+ <%= first_boot %>
70
+ EOP
71
+ ) > /etc/chef/first-boot.json
72
+
73
+ <%= chef_run %>'
@@ -0,0 +1,172 @@
1
+ require 'erubis'
2
+
3
+ module Ridley
4
+ class Bootstrapper
5
+ # @author Jamie Winsor <jamie@vialstudios.com>
6
+ class Context
7
+ class << self
8
+ def validate_options(options = {})
9
+ if options[:server_url].nil?
10
+ raise Errors::ArgumentError, "A server_url is required for bootstrapping"
11
+ end
12
+
13
+ if options[:validator_path].nil?
14
+ raise Errors::ArgumentError, "A path to a validator is required for bootstrapping"
15
+ end
16
+ end
17
+ end
18
+
19
+ # @return [String]
20
+ attr_reader :host
21
+ # @return [String]
22
+ attr_reader :node_name
23
+ # @return [String]
24
+ attr_reader :server_url
25
+ # @return [String]
26
+ attr_reader :validator_client
27
+ # @return [String]
28
+ attr_reader :validator_path
29
+ # @return [String]
30
+ attr_reader :bootstrap_proxy
31
+ # @return [Hash]
32
+ attr_reader :hints
33
+ # @return [String]
34
+ attr_reader :chef_version
35
+ # @return [String]
36
+ attr_reader :environment
37
+
38
+ # @param [String] host
39
+ # name of the node as identified in Chef
40
+ # @option options [String] :validator_path
41
+ # filepath to the validator used to bootstrap the node (required)
42
+ # @option options [String] :node_name
43
+ # @option options [String] :server_url
44
+ # @option options [String] :validator_client
45
+ # @option options [String] :bootstrap_proxy
46
+ # URL to a proxy server to bootstrap through (default: nil)
47
+ # @option options [String] :encrypted_data_bag_secret_path
48
+ # filepath on your host machine to your organizations encrypted data bag secret (default: nil)
49
+ # @option options [Hash] :hints
50
+ # a hash of Ohai hints to place on the bootstrapped node (default: Hash.new)
51
+ # @option options [Hash] :attributes
52
+ # a hash of attributes to use in the first Chef run (default: Hash.new)
53
+ # @option options [Array] :run_list
54
+ # an initial run list to bootstrap with (default: Array.new)
55
+ # @option options [String] :chef_version
56
+ # version of Chef to install on the node (default: {Ridley::CHEF_VERSION})
57
+ # @option options [String] :environment
58
+ # environment to join the node to (default: '_default')
59
+ # @option options [Boolean] :sudo
60
+ # bootstrap with sudo (default: true)
61
+ # @option options [String] :template
62
+ # bootstrap template to use (default: omnibus)
63
+ def initialize(host, options = {})
64
+ self.class.validate_options(options)
65
+
66
+ @host = host
67
+ @server_url = options[:server_url]
68
+ @validator_path = options[:validator_path]
69
+ @node_name = options[:node_name]
70
+ @validator_client = options[:validator_client] || "chef-validator"
71
+ @bootstrap_proxy = options[:bootstrap_proxy]
72
+ @encrypted_data_bag_secret_path = options[:encrypted_data_bag_secret_path]
73
+ @hints = options[:hints] || Hash.new
74
+ @attributes = options[:attributes] || Hash.new
75
+ @run_list = options[:run_list] || Array.new
76
+ @chef_version = options[:chef_version] || Ridley::CHEF_VERSION
77
+ @environment = options[:environment] || "_default"
78
+ @sudo = options[:sudo] || true
79
+ @template_file = options[:template] || Bootstrapper.default_template
80
+ end
81
+
82
+ # @return [String]
83
+ def boot_command
84
+ cmd = template.evaluate(self)
85
+
86
+ if sudo
87
+ cmd = "sudo #{cmd}"
88
+ end
89
+
90
+ cmd
91
+ end
92
+
93
+ # @return [String]
94
+ def clean_command
95
+ "rm /etc/chef/first-boot.json; rm /etc/chef/validation.pem"
96
+ end
97
+
98
+ # @return [String]
99
+ def chef_run
100
+ "chef-client -j /etc/chef/first-boot.json -E #{environment}"
101
+ end
102
+
103
+ # @return [String]
104
+ def chef_config
105
+ body = <<-CONFIG
106
+ log_level :info
107
+ log_location STDOUT
108
+ chef_server_url "#{server_url}"
109
+ validation_client_name "#{validator_client}"
110
+ CONFIG
111
+
112
+ if node_name.present?
113
+ body << %Q{node_name "#{node_name}"\n}
114
+ else
115
+ body << "# Using default node name (fqdn)\n"
116
+ end
117
+
118
+ if bootstrap_proxy.present?
119
+ body << %Q{http_proxy "#{bootstrap_proxy}"\n}
120
+ body << %Q{https_proxy "#{bootstrap_proxy}"\n}
121
+ end
122
+
123
+ if encrypted_data_bag_secret.present?
124
+ body << %Q{encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"\n}
125
+ end
126
+
127
+ body
128
+ end
129
+
130
+ # @return [String]
131
+ def first_boot
132
+ attributes.merge(run_list: run_list).to_json
133
+ end
134
+
135
+ # The validation key to create a new client for the node
136
+ #
137
+ # @raise [Ridley::Errors::ValidatorNotFound]
138
+ #
139
+ # @return [String]
140
+ def validation_key
141
+ IO.read(validator_path).chomp
142
+ rescue Errno::ENOENT
143
+ raise Errors::ValidatorNotFound, "Error bootstrapping: Validator not found at '#{validator_path}'"
144
+ end
145
+
146
+ # @raise [Ridley::Errors::EncryptedDataBagSecretNotFound]
147
+ #
148
+ # @return [String, nil]
149
+ def encrypted_data_bag_secret
150
+ return nil if encrypted_data_bag_secret_path.nil?
151
+
152
+ IO.read(encrypted_data_bag_secret_path).chomp
153
+ rescue Errno::ENOENT => encrypted_data_bag_secret
154
+ raise Errors::EncryptedDataBagSecretNotFound, "Error bootstrapping: Encrypted data bag secret provided but not found at '#{encrypted_data_bag_secret_path}"
155
+ end
156
+
157
+ private
158
+
159
+ attr_reader :sudo
160
+ attr_reader :template_file
161
+ attr_reader :encrypted_data_bag_secret_path
162
+ attr_reader :validator_path
163
+ attr_reader :run_list
164
+ attr_reader :attributes
165
+
166
+ # @return [Erubis::Eruby]
167
+ def template
168
+ Erubis::Eruby.new(IO.read(template_file).chomp)
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,99 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class Bootstrapper
4
+ autoload :Context, 'ridley/bootstrapper/context'
5
+
6
+ class << self
7
+ # @return [Pathname]
8
+ def templates_path
9
+ Ridley.root.join('bootstrappers')
10
+ end
11
+
12
+ # @return [String]
13
+ def default_template
14
+ templates_path.join('omnibus.erb').to_s
15
+ end
16
+ end
17
+
18
+ include Celluloid
19
+ include Celluloid::Logger
20
+
21
+ # @return [Array<String>]
22
+ attr_reader :hosts
23
+
24
+ # @return [Array<Bootstrapper::Context>]
25
+ attr_reader :contexts
26
+
27
+ # @return [Hash]
28
+ attr_reader :ssh_config
29
+
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)
36
+ # @option options [String] :validator_client
37
+ # @option options [String] :validator_path
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
54
+ # bootstrap with sudo (default: true)
55
+ # @option options [String] :template
56
+ # bootstrap template to use (default: omnibus)
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
+ }
65
+
66
+ @contexts = @hosts.collect do |host|
67
+ Context.new(host, options)
68
+ end
69
+ end
70
+
71
+ # @return [SSH::ResponseSet]
72
+ 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
78
+
79
+ responses = contexts.collect do |context|
80
+ pool.future.run(context.host, context.boot_command)
81
+ end.collect(&:value)
82
+
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
93
+ end
94
+ end
95
+ ensure
96
+ pool.terminate if pool
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,25 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ # @api private
4
+ class ChainLink
5
+ attr_reader :parent
6
+ attr_reader :child
7
+
8
+ # @param [Class, Object] parent
9
+ # the parent class or object to send to the child
10
+ # @param [Class, Object] child
11
+ # the child class or instance to delegate functions to
12
+ def initialize(parent, child)
13
+ @parent = parent
14
+ @child = child
15
+ end
16
+
17
+ def new(*args)
18
+ child.send(:new, parent, *args)
19
+ end
20
+
21
+ def method_missing(fun, *args, &block)
22
+ child.send(fun, parent, *args, &block)
23
+ end
24
+ end
25
+ end
@@ -25,6 +25,11 @@ module Ridley
25
25
  attr_reader :client_name
26
26
  attr_reader :client_key
27
27
  attr_reader :organization
28
+ attr_reader :ssh
29
+
30
+ attr_reader :validator_client
31
+ attr_reader :validator_path
32
+ attr_reader :encrypted_data_bag_secret_path
28
33
 
29
34
  attr_accessor :thread_count
30
35
 
@@ -35,6 +40,7 @@ module Ridley
35
40
  def_delegator :conn, :path_prefix
36
41
 
37
42
  def_delegator :conn, :url_prefix=
43
+ def_delegator :conn, :url_prefix
38
44
 
39
45
  def_delegator :conn, :get
40
46
  def_delegator :conn, :put
@@ -48,7 +54,7 @@ module Ridley
48
54
  :server_url,
49
55
  :client_name,
50
56
  :client_key
51
- ]
57
+ ].freeze
52
58
 
53
59
  DEFAULT_THREAD_COUNT = 8
54
60
 
@@ -62,7 +68,15 @@ module Ridley
62
68
  # @option options [String] :organization
63
69
  # the Organization to connect to. This is only used if you are connecting to
64
70
  # private Chef or hosted Chef
71
+ # @option options [String] :validator_client
72
+ # (default: nil)
73
+ # @option options [String] :validator_path
74
+ # (default: nil)
75
+ # @option options [String] :encrypted_data_bag_secret_path
76
+ # (default: nil)
65
77
  # @option options [Integer] :thread_count
78
+ # @option options [Hash] :ssh
79
+ # authentication credentials for bootstrapping or connecting to nodes (default: Hash.new)
66
80
  # @option options [Hash] :params
67
81
  # URI query unencoded key/value pairs
68
82
  # @option options [Hash] :headers
@@ -76,10 +90,14 @@ module Ridley
76
90
  def initialize(options = {})
77
91
  self.class.validate_options(options)
78
92
 
79
- @client_name = options.fetch(:client_name)
80
- @client_key = options.fetch(:client_key)
81
- @organization = options.fetch(:organization, nil)
82
- @thread_count = options.fetch(:thread_count, DEFAULT_THREAD_COUNT)
93
+ @client_name = options.fetch(:client_name)
94
+ @client_key = options.fetch(:client_key)
95
+ @organization = options[:organization]
96
+ @thread_count = (options[:thread_count] || DEFAULT_THREAD_COUNT)
97
+ @ssh = (options[:ssh] || Hash.new)
98
+ @validator_client = options[:validator_client]
99
+ @validator_path = options[:validator_path]
100
+ @encrypted_data_bag_secret_path = options[:encrypted_data_bag_secret_path]
83
101
 
84
102
  unless @client_key.present? && File.exist?(@client_key)
85
103
  raise Errors::ClientKeyFileNotFound, "client key not found at: '#{@client_key}'"
@@ -134,6 +152,10 @@ module Ridley
134
152
  api_type == :foss
135
153
  end
136
154
 
155
+ def server_url
156
+ self.url_prefix.to_s
157
+ end
158
+
137
159
  private
138
160
 
139
161
  attr_reader :conn
data/lib/ridley/errors.rb CHANGED
@@ -3,6 +3,9 @@ module Ridley
3
3
  module Errors
4
4
  class RidleyError < StandardError; end
5
5
  class InternalError < RidleyError; end
6
+ class ArgumentError < InternalError; end
7
+
8
+ class ValidatorNotFound < RidleyError; end
6
9
 
7
10
  class InvalidResource < RidleyError
8
11
  attr_reader :errors
@@ -17,7 +20,9 @@ module Ridley
17
20
  alias_method :to_s, :message
18
21
  end
19
22
 
20
- class ClientKeyFileNotFound < RidleyError; end
23
+ class BootstrapError < RidleyError; end
24
+ class ClientKeyFileNotFound < BootstrapError; end
25
+ class EncryptedDataBagSecretNotFound < BootstrapError; end
21
26
 
22
27
  class HTTPError < RidleyError
23
28
  class << self
@@ -0,0 +1,30 @@
1
+ require 'logger'
2
+
3
+ module Ridley
4
+ # @author Jamie Winsor <jamie@vialstudios.com>
5
+ module Logging
6
+ class << self
7
+ # @return [Logger]
8
+ def logger
9
+ @logger ||= begin
10
+ log = Logger.new(STDOUT)
11
+ log.level = Logger::INFO
12
+ log
13
+ end
14
+ end
15
+
16
+ # @param [Logger, nil] obj
17
+ #
18
+ # @return [Logger]
19
+ def set_logger(obj)
20
+ @logger = (obj.nil? ? Logger.new('/dev/null') : obj)
21
+ end
22
+ end
23
+
24
+ # @return [Logger]
25
+ def logger
26
+ Ridley::Logging.logger
27
+ end
28
+ alias_method :log, :logger
29
+ end
30
+ end
@@ -64,12 +64,12 @@ module Ridley
64
64
  # Coerces instance functions into class functions on Ridley::Client. This coercion
65
65
  # sends an instance of the including class along to the class function.
66
66
  #
67
- # @see Ridley::Context
67
+ # @see Ridley::ChainLink
68
68
  #
69
- # @return [Ridley::Context]
69
+ # @return [Ridley::ChainLink]
70
70
  # a context object to delegate instance functions to class functions on Ridley::Client
71
71
  def client
72
- Context.new(Ridley::Client, self)
72
+ ChainLink.new(self, Ridley::Client)
73
73
  end
74
74
  end
75
75
  end
@@ -42,12 +42,12 @@ module Ridley
42
42
  # Coerces instance functions into class functions on Ridley::Cookbook. This coercion
43
43
  # sends an instance of the including class along to the class function.
44
44
  #
45
- # @see Ridley::Context
45
+ # @see Ridley::ChainLink
46
46
  #
47
- # @return [Ridley::Context]
47
+ # @return [Ridley::ChainLink]
48
48
  # a context object to delegate instance functions to class functions on Ridley::Cookbook
49
49
  def cookbook
50
- Context.new(Ridley::Cookbook, self)
50
+ ChainLink.new(self, Ridley::Cookbook)
51
51
  end
52
52
  end
53
53
  end