ridley 0.3.2 → 0.4.0

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