ridley 0.9.1 → 0.10.0.rc1
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/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Guardfile +1 -1
- data/README.md +1 -1
- data/bootstrappers/{omnibus.erb → unix_omnibus.erb} +20 -5
- data/bootstrappers/windows_omnibus.erb +133 -0
- data/lib/ridley.rb +4 -1
- data/lib/ridley/bootstrap_bindings.rb +5 -0
- data/lib/ridley/bootstrap_bindings/unix_template_binding.rb +112 -0
- data/lib/ridley/bootstrap_bindings/windows_template_binding.rb +221 -0
- data/lib/ridley/bootstrapper.rb +14 -22
- data/lib/ridley/bootstrapper/context.rb +53 -162
- data/lib/ridley/chef.rb +0 -1
- data/lib/ridley/chef/cookbook.rb +0 -9
- data/lib/ridley/client.rb +12 -1
- data/lib/ridley/errors.rb +1 -0
- data/lib/ridley/host_connector.rb +76 -0
- data/lib/ridley/{ssh → host_connector}/response.rb +1 -1
- data/lib/ridley/{ssh → host_connector}/response_set.rb +11 -11
- data/lib/ridley/host_connector/ssh.rb +58 -0
- data/lib/ridley/host_connector/ssh/worker.rb +99 -0
- data/lib/ridley/host_connector/winrm.rb +55 -0
- data/lib/ridley/host_connector/winrm/worker.rb +126 -0
- data/lib/ridley/mixin/bootstrap_binding.rb +88 -0
- data/lib/ridley/resource.rb +1 -1
- data/lib/ridley/resources/client_resource.rb +7 -0
- data/lib/ridley/resources/node_resource.rb +9 -4
- data/lib/ridley/sandbox_uploader.rb +1 -7
- data/lib/ridley/version.rb +1 -1
- data/ridley.gemspec +1 -0
- data/spec/unit/ridley/bootstrap_bindings/unix_template_binding_spec.rb +102 -0
- data/spec/unit/ridley/bootstrap_bindings/windows_template_binding_spec.rb +118 -0
- data/spec/unit/ridley/bootstrapper/context_spec.rb +19 -106
- data/spec/unit/ridley/bootstrapper_spec.rb +2 -12
- data/spec/unit/ridley/client_spec.rb +20 -2
- data/spec/unit/ridley/{ssh → host_connector}/response_set_spec.rb +17 -17
- data/spec/unit/ridley/host_connector/ssh/worker_spec.rb +15 -0
- data/spec/unit/ridley/{ssh_spec.rb → host_connector/ssh_spec.rb} +5 -5
- data/spec/unit/ridley/host_connector/winrm/worker_spec.rb +41 -0
- data/spec/unit/ridley/host_connector/winrm_spec.rb +47 -0
- data/spec/unit/ridley/host_connector_spec.rb +102 -0
- data/spec/unit/ridley/mixin/bootstrap_binding_spec.rb +56 -0
- data/spec/unit/ridley/sandbox_uploader_spec.rb +1 -28
- metadata +53 -25
- data/.rbenv-version +0 -1
- data/lib/ridley/chef/chefignore.rb +0 -76
- data/lib/ridley/ssh.rb +0 -56
- data/lib/ridley/ssh/worker.rb +0 -87
- data/spec/fixtures/chefignore +0 -8
- data/spec/unit/ridley/chef/chefignore_spec.rb +0 -40
- data/spec/unit/ridley/ssh/worker_spec.rb +0 -13
@@ -1,15 +1,15 @@
|
|
1
1
|
module Ridley
|
2
|
-
|
2
|
+
module HostConnector
|
3
3
|
# @author Jamie Winsor <reset@riotgames.com>
|
4
4
|
class ResponseSet
|
5
5
|
class << self
|
6
6
|
# Merges the responses of the other ResponseSet with the target ResponseSet
|
7
7
|
# and returns the mutated target
|
8
8
|
#
|
9
|
-
# @param [
|
10
|
-
# @param [
|
9
|
+
# @param [HostConnector::ResponseSet] target
|
10
|
+
# @param [HostConnector::ResponseSet] other
|
11
11
|
#
|
12
|
-
# @return [
|
12
|
+
# @return [HostConnector::ResponseSet]
|
13
13
|
def merge!(target, other)
|
14
14
|
if other.is_a?(self)
|
15
15
|
target.add_response(other.responses)
|
@@ -34,9 +34,9 @@ module Ridley
|
|
34
34
|
add_response Array(responses)
|
35
35
|
end
|
36
36
|
|
37
|
-
# @param [
|
37
|
+
# @param [HostConnector::Response, Array<HostConnector::Response>] response
|
38
38
|
#
|
39
|
-
# @return [Array<
|
39
|
+
# @return [Array<HostConnector::Response>]
|
40
40
|
def add_response(response)
|
41
41
|
if response.is_a?(Array)
|
42
42
|
until response.empty?
|
@@ -64,9 +64,9 @@ module Ridley
|
|
64
64
|
# Merges the responses of another ResponseSet with self and returns
|
65
65
|
# a new instance of ResponseSet
|
66
66
|
#
|
67
|
-
# @param [Ridley::
|
67
|
+
# @param [Ridley::HostConnector::ResponseSet] other
|
68
68
|
#
|
69
|
-
# @return [Ridley::
|
69
|
+
# @return [Ridley::HostConnector::ResponseSet]
|
70
70
|
def merge(other)
|
71
71
|
target = self.class.new(self.responses) # Why the fuck can't I use #dup here?
|
72
72
|
self.class.merge!(target, other)
|
@@ -75,7 +75,7 @@ module Ridley
|
|
75
75
|
# Merges the respones of another ResponseSet with self and returns
|
76
76
|
# mutated self
|
77
77
|
#
|
78
|
-
# @param [Ridley::
|
78
|
+
# @param [Ridley::HostConnector::ResponseSet] other
|
79
79
|
#
|
80
80
|
# @return [self]
|
81
81
|
def merge!(other)
|
@@ -84,12 +84,12 @@ module Ridley
|
|
84
84
|
|
85
85
|
private
|
86
86
|
|
87
|
-
# @param [
|
87
|
+
# @param [HostConnector::Response] response
|
88
88
|
def add_failure(response)
|
89
89
|
self.failures << response
|
90
90
|
end
|
91
91
|
|
92
|
-
# @param [
|
92
|
+
# @param [HostConnector::Response] response
|
93
93
|
def add_success(response)
|
94
94
|
self.successes << response
|
95
95
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Ridley
|
4
|
+
module HostConnector
|
5
|
+
# @author Jamie Winsor <reset@riotgames.com>
|
6
|
+
class SSH
|
7
|
+
autoload :Worker, 'ridley/host_connector/ssh/worker'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
|
11
|
+
# @param [Hash] options
|
12
|
+
def start(nodes, options = {}, &block)
|
13
|
+
runner = new(nodes, options)
|
14
|
+
result = yield runner
|
15
|
+
runner.terminate
|
16
|
+
|
17
|
+
result
|
18
|
+
ensure
|
19
|
+
runner.terminate if runner && runner.alive?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
include Celluloid
|
24
|
+
include Celluloid::Logger
|
25
|
+
|
26
|
+
attr_reader :nodes
|
27
|
+
attr_reader :options
|
28
|
+
|
29
|
+
# @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
|
30
|
+
# @param [Hash] options
|
31
|
+
# @see Net::SSH
|
32
|
+
def initialize(nodes, options = {})
|
33
|
+
@nodes = Array(nodes)
|
34
|
+
@options = options
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [String] command
|
38
|
+
#
|
39
|
+
# @return [Array]
|
40
|
+
def run(command)
|
41
|
+
workers = Array.new
|
42
|
+
futures = self.nodes.collect do |node|
|
43
|
+
workers << worker = Worker.new(node.public_hostname, self.options.freeze)
|
44
|
+
worker.future.run(command)
|
45
|
+
end
|
46
|
+
|
47
|
+
ResponseSet.new.tap do |response_set|
|
48
|
+
futures.each do |future|
|
49
|
+
status, response = future.value
|
50
|
+
response_set.add_response(response)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
ensure
|
54
|
+
workers.map(&:terminate)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Ridley
|
2
|
+
module HostConnector
|
3
|
+
class SSH
|
4
|
+
# @author Jamie Winsor <reset@riotgames.com>
|
5
|
+
# @api private
|
6
|
+
class Worker
|
7
|
+
include Celluloid
|
8
|
+
include Celluloid::Logger
|
9
|
+
|
10
|
+
attr_reader :sudo
|
11
|
+
attr_reader :user
|
12
|
+
attr_reader :host
|
13
|
+
# @return [Hashie::Mash]
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
# @param [Hash] options
|
17
|
+
def initialize(host, options = {})
|
18
|
+
@options = options.deep_symbolize_keys
|
19
|
+
@options = options[:ssh] if options[:ssh]
|
20
|
+
@host = host
|
21
|
+
@sudo = @options[:sudo]
|
22
|
+
@user = @options[:user]
|
23
|
+
|
24
|
+
@options[:paranoid] = false
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [String] command
|
28
|
+
#
|
29
|
+
# @return [Array]
|
30
|
+
def run(command)
|
31
|
+
response = Ridley::HostConnector::Response.new(host)
|
32
|
+
debug "Running SSH command: '#{command}' on: '#{host}' as: '#{user}'"
|
33
|
+
|
34
|
+
channel_exec = ->(channel, command) do
|
35
|
+
channel.exec(command) do |ch, success|
|
36
|
+
unless success
|
37
|
+
raise "Channel execution failed while executing command #{command}"
|
38
|
+
end
|
39
|
+
|
40
|
+
channel.on_data do |ch, data|
|
41
|
+
response.stdout += data
|
42
|
+
info "NODE[#{host}] #{data}" if data.present? and data != "\r\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
channel.on_extended_data do |ch, type, data|
|
46
|
+
response.stderr += data
|
47
|
+
info "NODE[#{host}] #{data}" if data.present? and data != "\r\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
channel.on_request("exit-status") do |ch, data|
|
51
|
+
response.exit_code = data.read_long
|
52
|
+
end
|
53
|
+
|
54
|
+
channel.on_request("exit-signal") do |ch, data|
|
55
|
+
response.exit_signal = data.read_string
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Net::SSH.start(host, user, options.slice(*Net::SSH::VALID_OPTIONS)) do |ssh|
|
61
|
+
ssh.open_channel do |channel|
|
62
|
+
if self.sudo
|
63
|
+
channel.request_pty do |channel, success|
|
64
|
+
raise "Could not aquire pty: A pty is required for running sudo commands." unless success
|
65
|
+
|
66
|
+
channel_exec.call(channel, command)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
channel_exec.call(channel, command)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
ssh.loop
|
74
|
+
end
|
75
|
+
|
76
|
+
case response.exit_code
|
77
|
+
when 0
|
78
|
+
debug "Successfully ran SSH command on: '#{host}' as: '#{user}'"
|
79
|
+
[ :ok, response ]
|
80
|
+
else
|
81
|
+
error "Successfully ran SSH command on: '#{host}' as: '#{user}', but it failed"
|
82
|
+
error response.stdout
|
83
|
+
[ :error, response ]
|
84
|
+
end
|
85
|
+
rescue => e
|
86
|
+
error "Failed to run SSH command on: '#{host}' as: '#{user}'"
|
87
|
+
error "#{e.class}: #{e.message}"
|
88
|
+
response.exit_code = -1
|
89
|
+
response.stderr = e.message
|
90
|
+
[ :error, response ]
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :runner
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Ridley
|
2
|
+
module HostConnector
|
3
|
+
# @author Kyle Allan <kallan@riotgames.com>
|
4
|
+
class WinRM
|
5
|
+
autoload :Worker, 'ridley/host_connector/winrm/worker'
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
|
9
|
+
# @param [Hash] options
|
10
|
+
def start(nodes, options = {}, &block)
|
11
|
+
runner = new(nodes, options)
|
12
|
+
result = yield runner
|
13
|
+
runner.terminate
|
14
|
+
|
15
|
+
result
|
16
|
+
ensure
|
17
|
+
runner.terminate if runner && runner.alive?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
include Celluloid
|
22
|
+
include Celluloid::Logger
|
23
|
+
|
24
|
+
attr_reader :nodes
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
# @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
|
28
|
+
# @param [Hash] options
|
29
|
+
def initialize(nodes, options = {})
|
30
|
+
@nodes = Array(nodes)
|
31
|
+
@options = options
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [String] command
|
35
|
+
#
|
36
|
+
# @return [Array]
|
37
|
+
def run(command)
|
38
|
+
workers = Array.new
|
39
|
+
futures = self.nodes.collect do |node|
|
40
|
+
workers << worker = Worker.new(node.public_hostname, self.options.freeze)
|
41
|
+
worker.future.run(command)
|
42
|
+
end
|
43
|
+
|
44
|
+
Ridley::HostConnector::ResponseSet.new.tap do |response_set|
|
45
|
+
futures.each do |future|
|
46
|
+
status, response = future.value
|
47
|
+
response_set.add_response(response)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
ensure
|
51
|
+
workers.map(&:terminate)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Ridley
|
2
|
+
module HostConnector
|
3
|
+
class WinRM
|
4
|
+
# @author Kyle Allan <kallan@riotgames.com>
|
5
|
+
# @api private
|
6
|
+
class Worker
|
7
|
+
include Celluloid
|
8
|
+
include Celluloid::Logger
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :user
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :password
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :host
|
16
|
+
# @return [Hash]
|
17
|
+
attr_reader :options
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :winrm_endpoint
|
20
|
+
|
21
|
+
# @param host [String]
|
22
|
+
# the host the worker is going to work on
|
23
|
+
# @option options [Hash] :winrm
|
24
|
+
# * :user (String) a user that will login to each node and perform the bootstrap command on (required)
|
25
|
+
# * :password (String) the password for the user that will perform the bootstrap
|
26
|
+
# * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
|
27
|
+
def initialize(host, options = {})
|
28
|
+
@options = options.deep_symbolize_keys
|
29
|
+
@options = options[:winrm] if options[:winrm]
|
30
|
+
@host = host
|
31
|
+
@user = @options[:user]
|
32
|
+
@password = @options[:password]
|
33
|
+
@winrm_endpoint = "http://#{host}:#{winrm_port}/wsman"
|
34
|
+
end
|
35
|
+
|
36
|
+
def run(command)
|
37
|
+
command = get_command(command)
|
38
|
+
|
39
|
+
response = Ridley::HostConnector::Response.new(host)
|
40
|
+
debug "Running WinRM Command: '#{command}' on: '#{host}' as: '#{user}'"
|
41
|
+
|
42
|
+
output = winrm.run_cmd(command) do |stdout, stderr|
|
43
|
+
response.stdout += stdout unless stdout.nil?
|
44
|
+
response.stderr += stderr unless stderr.nil?
|
45
|
+
end
|
46
|
+
response.exit_code = output[:exitcode]
|
47
|
+
|
48
|
+
case response.exit_code
|
49
|
+
when 0
|
50
|
+
debug "Successfully ran WinRM command on: '#{host}' as: '#{user}'"
|
51
|
+
[ :ok, response ]
|
52
|
+
else
|
53
|
+
error "Successfully ran WinRM command on: '#{host}' as: '#{user}', but it failed"
|
54
|
+
error response.stdout
|
55
|
+
[ :error, response ]
|
56
|
+
end
|
57
|
+
rescue => e
|
58
|
+
error "Failed to run WinRM command on: '#{host}' as: '#{user}'"
|
59
|
+
error "#{e.class}: #{e.message}"
|
60
|
+
response.exit_code = -1
|
61
|
+
response.stderr = e.message
|
62
|
+
[ :error, response ]
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [::WinRM::WinRMWebService]
|
66
|
+
def winrm
|
67
|
+
::WinRM::WinRMWebService.new(winrm_endpoint, :plaintext, user: user, pass: password, disable_sspi: true, basic_auth_only: true)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Fixnum]
|
71
|
+
def winrm_port
|
72
|
+
options[:port] || Ridley::HostConnector::DEFAULT_WINRM_PORT
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the command if it does not break the WinRM command length
|
76
|
+
# limit. Otherwise, we return an execution of the command as a batch file.
|
77
|
+
#
|
78
|
+
# @param command [String]
|
79
|
+
#
|
80
|
+
# @return [String]
|
81
|
+
def get_command(command)
|
82
|
+
if command.length < 2047
|
83
|
+
command
|
84
|
+
else
|
85
|
+
debug "Detected a command that was longer than 2047 characters, uploading command as a file to the host."
|
86
|
+
upload_command_to_host(command)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Uploads the command encoded as base64 to a file on the host
|
93
|
+
# and then uses Powershell to transform the base64 file into the
|
94
|
+
# command that was originally passed through.
|
95
|
+
#
|
96
|
+
# @param command [String]
|
97
|
+
#
|
98
|
+
# @return [String] the command to execute the uploaded file
|
99
|
+
def upload_command_to_host(command)
|
100
|
+
base64_file = "winrm-upload-base64-#{Process.pid}-#{Time.now.to_i}"
|
101
|
+
base64_file_name = get_file_path(base64_file)
|
102
|
+
|
103
|
+
Base64.encode64(command).gsub("\n", '').chars.to_a.each_slice(8000 - base64_file_name.size) do |chunk|
|
104
|
+
out = winrm.run_cmd( "echo #{chunk.join} >> \"#{base64_file_name}\"" )
|
105
|
+
end
|
106
|
+
|
107
|
+
command_file = "winrm-upload-#{Process.pid}-#{Time.now.to_i}.bat"
|
108
|
+
command_file_name = get_file_path(command_file)
|
109
|
+
winrm.powershell <<-POWERSHELL
|
110
|
+
$base64_string = Get-Content \"#{base64_file_name}\"
|
111
|
+
$bytes = [System.Convert]::FromBase64String($base64_string)
|
112
|
+
$new_file = [System.IO.Path]::GetFullPath(\"#{command_file_name}\")
|
113
|
+
[System.IO.File]::WriteAllBytes($new_file,$bytes)
|
114
|
+
POWERSHELL
|
115
|
+
|
116
|
+
"cmd.exe /C #{command_file_name}"
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [String]
|
120
|
+
def get_file_path(file)
|
121
|
+
(winrm.run_cmd("echo %TEMP%\\#{file}"))[:data][0][:stdout].chomp
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'erubis'
|
2
|
+
|
3
|
+
module Ridley
|
4
|
+
module BootstrapBinding
|
5
|
+
module ClassMethods
|
6
|
+
def validate_options(options = {})
|
7
|
+
if options[:server_url].nil?
|
8
|
+
raise Errors::ArgumentError, "A server_url is required for bootstrapping"
|
9
|
+
end
|
10
|
+
|
11
|
+
if options[:validator_path].nil?
|
12
|
+
raise Errors::ArgumentError, "A path to a validator is required for bootstrapping"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# A hash of default options to be used in the Context initializer
|
17
|
+
#
|
18
|
+
# @return [Hash]
|
19
|
+
def default_options
|
20
|
+
@default_options ||= {
|
21
|
+
validator_client: "chef-validator",
|
22
|
+
attributes: Hash.new,
|
23
|
+
run_list: Array.new,
|
24
|
+
environment: "_default",
|
25
|
+
sudo: true,
|
26
|
+
hints: Hash.new
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def included(base)
|
33
|
+
base.extend(ClassMethods)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :template_file
|
38
|
+
attr_reader :bootstrap_proxy
|
39
|
+
attr_reader :chef_version
|
40
|
+
attr_reader :default_options
|
41
|
+
attr_reader :validator_path
|
42
|
+
attr_reader :encrypted_data_bag_secret_path
|
43
|
+
attr_reader :server_url
|
44
|
+
attr_reader :validator_client
|
45
|
+
attr_reader :node_name
|
46
|
+
attr_reader :attributes
|
47
|
+
attr_reader :run_list
|
48
|
+
attr_reader :environment
|
49
|
+
|
50
|
+
# @return [Pathname]
|
51
|
+
def templates_path
|
52
|
+
Ridley.root.join('bootstrappers')
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [String]
|
56
|
+
def first_boot
|
57
|
+
MultiJson.encode attributes.merge(run_list: run_list)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @raise [Ridley::Errors::EncryptedDataBagSecretNotFound]
|
61
|
+
#
|
62
|
+
# @return [String, nil]
|
63
|
+
def encrypted_data_bag_secret
|
64
|
+
return unless encrypted_data_bag_secret_path
|
65
|
+
|
66
|
+
IO.read(encrypted_data_bag_secret_path).chomp
|
67
|
+
rescue Errno::ENOENT
|
68
|
+
raise Errors::EncryptedDataBagSecretNotFound, "Error bootstrapping: Encrypted data bag secret provided but not found at '#{encrypted_data_bag_secret_path}'"
|
69
|
+
end
|
70
|
+
|
71
|
+
# The validation key to create a new client for the node
|
72
|
+
#
|
73
|
+
# @raise [Ridley::Errors::ValidatorNotFound]
|
74
|
+
#
|
75
|
+
# @return [String]
|
76
|
+
def validation_key
|
77
|
+
IO.read(File.expand_path(validator_path)).chomp
|
78
|
+
rescue Errno::ENOENT
|
79
|
+
raise Errors::ValidatorNotFound, "Error bootstrapping: Validator not found at '#{validator_path}'"
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Erubis::Eruby]
|
83
|
+
def template
|
84
|
+
Erubis::Eruby.new(IO.read(template_file).chomp)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|