bolt 2.24.1 → 2.29.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +1 -1
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
- data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
- data/lib/bolt/analytics.rb +7 -3
- data/lib/bolt/applicator.rb +21 -21
- data/lib/bolt/bolt_option_parser.rb +77 -26
- data/lib/bolt/catalog.rb +4 -2
- data/lib/bolt/cli.rb +135 -147
- data/lib/bolt/config.rb +48 -25
- data/lib/bolt/config/options.rb +34 -2
- data/lib/bolt/executor.rb +1 -1
- data/lib/bolt/inventory.rb +8 -1
- data/lib/bolt/inventory/group.rb +1 -1
- data/lib/bolt/inventory/inventory.rb +1 -1
- data/lib/bolt/inventory/target.rb +1 -1
- data/lib/bolt/logger.rb +35 -21
- data/lib/bolt/outputter/logger.rb +1 -1
- data/lib/bolt/pal.rb +21 -10
- data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
- data/lib/bolt/plugin/puppetdb.rb +1 -1
- data/lib/bolt/project.rb +62 -17
- data/lib/bolt/puppetdb/client.rb +1 -1
- data/lib/bolt/puppetdb/config.rb +1 -1
- data/lib/bolt/puppetfile.rb +160 -0
- data/lib/bolt/puppetfile/installer.rb +43 -0
- data/lib/bolt/puppetfile/module.rb +89 -0
- data/lib/bolt/r10k_log_proxy.rb +1 -1
- data/lib/bolt/rerun.rb +2 -2
- data/lib/bolt/result.rb +23 -0
- data/lib/bolt/shell.rb +1 -1
- data/lib/bolt/task.rb +1 -1
- data/lib/bolt/transport/base.rb +5 -5
- data/lib/bolt/transport/docker/connection.rb +1 -1
- data/lib/bolt/transport/local/connection.rb +1 -1
- data/lib/bolt/transport/ssh.rb +1 -1
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
- data/lib/bolt/transport/winrm.rb +1 -1
- data/lib/bolt/transport/winrm/connection.rb +1 -1
- data/lib/bolt/util.rb +30 -11
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/base_config.rb +1 -1
- data/lib/bolt_server/config.rb +1 -1
- data/lib/bolt_server/file_cache.rb +12 -12
- data/lib/bolt_server/transport_app.rb +125 -26
- data/lib/bolt_spec/bolt_context.rb +4 -4
- data/lib/bolt_spec/run.rb +3 -0
- metadata +11 -14
data/lib/bolt/r10k_log_proxy.rb
CHANGED
data/lib/bolt/rerun.rb
CHANGED
@@ -8,7 +8,7 @@ module Bolt
|
|
8
8
|
def initialize(path, save_failures)
|
9
9
|
@path = path
|
10
10
|
@save_failures = save_failures
|
11
|
-
@logger =
|
11
|
+
@logger = Bolt::Logger.logger(self)
|
12
12
|
end
|
13
13
|
|
14
14
|
def data
|
@@ -53,7 +53,7 @@ module Bolt
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
rescue StandardError => e
|
56
|
-
|
56
|
+
Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
data/lib/bolt/result.rb
CHANGED
@@ -65,6 +65,25 @@ module Bolt
|
|
65
65
|
'msg' => msg,
|
66
66
|
'details' => { 'exit_code' => exit_code } }
|
67
67
|
end
|
68
|
+
|
69
|
+
if value.key?('_error')
|
70
|
+
unless value['_error'].is_a?(Hash) && value['_error'].key?('msg')
|
71
|
+
value['_error'] = {
|
72
|
+
'msg' => "Invalid error returned from task #{task}: #{value['_error'].inspect}. Error "\
|
73
|
+
"must be an object with a msg key.",
|
74
|
+
'kind' => 'bolt/invalid-task-error',
|
75
|
+
'details' => { 'original_error' => value['_error'] }
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
value['_error']['kind'] ||= 'bolt/error'
|
80
|
+
value['_error']['details'] ||= {}
|
81
|
+
end
|
82
|
+
|
83
|
+
if value.key?('_sensitive')
|
84
|
+
value['_sensitive'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(value['_sensitive'])
|
85
|
+
end
|
86
|
+
|
68
87
|
new(target, value: value, action: 'task', object: task)
|
69
88
|
end
|
70
89
|
|
@@ -205,5 +224,9 @@ module Bolt
|
|
205
224
|
|
206
225
|
end
|
207
226
|
end
|
227
|
+
|
228
|
+
def sensitive
|
229
|
+
value['_sensitive']
|
230
|
+
end
|
208
231
|
end
|
209
232
|
end
|
data/lib/bolt/shell.rb
CHANGED
data/lib/bolt/task.rb
CHANGED
data/lib/bolt/transport/base.rb
CHANGED
@@ -40,17 +40,17 @@ module Bolt
|
|
40
40
|
attr_reader :logger
|
41
41
|
|
42
42
|
def initialize
|
43
|
-
@logger =
|
43
|
+
@logger = Bolt::Logger.logger(self)
|
44
44
|
end
|
45
45
|
|
46
46
|
def with_events(target, callback, action)
|
47
47
|
callback&.call(type: :node_start, target: target)
|
48
48
|
|
49
49
|
result = begin
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
yield
|
51
|
+
rescue StandardError, NotImplementedError => e
|
52
|
+
Bolt::Result.from_exception(target, e, action: action)
|
53
|
+
end
|
54
54
|
|
55
55
|
callback&.call(type: :node_result, result: result)
|
56
56
|
result
|
@@ -10,7 +10,7 @@ module Bolt
|
|
10
10
|
def initialize(target)
|
11
11
|
raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
|
12
12
|
@target = target
|
13
|
-
@logger =
|
13
|
+
@logger = Bolt::Logger.logger(target.safe_name)
|
14
14
|
@docker_host = @target.options['service-url']
|
15
15
|
@logger.trace("Initializing docker connection to #{@target.safe_name}")
|
16
16
|
end
|
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -17,7 +17,7 @@ module Bolt
|
|
17
17
|
rescue LoadError
|
18
18
|
logger.debug("Authentication method 'gssapi-with-mic' (Kerberos) is not available.")
|
19
19
|
end
|
20
|
-
@transport_logger =
|
20
|
+
@transport_logger = Bolt::Logger.logger(Net::SSH)
|
21
21
|
@transport_logger.level = :warn
|
22
22
|
end
|
23
23
|
|
@@ -26,7 +26,7 @@ module Bolt
|
|
26
26
|
@user = @target.user || ssh_config[:user] || Etc.getlogin
|
27
27
|
@strict_host_key_checking = ssh_config[:strict_host_key_checking]
|
28
28
|
|
29
|
-
@logger =
|
29
|
+
@logger = Bolt::Logger.logger(@target.safe_name)
|
30
30
|
@transport_logger = transport_logger
|
31
31
|
@logger.trace("Initializing ssh connection to #{@target.safe_name}")
|
32
32
|
|
@@ -14,7 +14,7 @@ module Bolt
|
|
14
14
|
@target = target
|
15
15
|
ssh_config = Net::SSH::Config.for(target.host)
|
16
16
|
@user = @target.user || ssh_config[:user] || Etc.getlogin
|
17
|
-
@logger =
|
17
|
+
@logger = Bolt::Logger.logger(self)
|
18
18
|
end
|
19
19
|
|
20
20
|
# This is used to verify we can connect to targets with `connected?`
|
data/lib/bolt/transport/winrm.rb
CHANGED
@@ -18,7 +18,7 @@ module Bolt
|
|
18
18
|
@user = @target.user
|
19
19
|
# Build set of extensions from extensions config as well as interpreters
|
20
20
|
|
21
|
-
@logger =
|
21
|
+
@logger = Bolt::Logger.logger(@target.safe_name)
|
22
22
|
logger.trace("Initializing winrm connection to #{@target.safe_name}")
|
23
23
|
@transport_logger = transport_logger
|
24
24
|
end
|
data/lib/bolt/util.rb
CHANGED
@@ -3,10 +3,29 @@
|
|
3
3
|
module Bolt
|
4
4
|
module Util
|
5
5
|
class << self
|
6
|
+
# Gets input for an argument.
|
7
|
+
def get_arg_input(value)
|
8
|
+
if value.start_with?('@')
|
9
|
+
file = value.sub(/^@/, '')
|
10
|
+
read_arg_file(file)
|
11
|
+
elsif value == '-'
|
12
|
+
$stdin.read
|
13
|
+
else
|
14
|
+
value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Reads a file passed as an argument to a command.
|
19
|
+
def read_arg_file(file)
|
20
|
+
File.read(File.expand_path(file))
|
21
|
+
rescue StandardError => e
|
22
|
+
raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
|
23
|
+
end
|
24
|
+
|
6
25
|
def read_yaml_hash(path, file_name)
|
7
26
|
require 'yaml'
|
8
27
|
|
9
|
-
logger =
|
28
|
+
logger = Bolt::Logger.logger(self)
|
10
29
|
path = File.expand_path(path)
|
11
30
|
content = File.open(path, "r:UTF-8") { |f| YAML.safe_load(f.read) } || {}
|
12
31
|
unless content.is_a?(Hash)
|
@@ -179,16 +198,16 @@ module Bolt
|
|
179
198
|
# object was frozen
|
180
199
|
frozen = obj.frozen?
|
181
200
|
cl = begin
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
201
|
+
obj.clone(freeze: false)
|
202
|
+
# Some datatypes, such as FalseClass, can't be unfrozen. These
|
203
|
+
# aren't the types we recurse on, so we can leave them frozen
|
204
|
+
rescue ArgumentError => e
|
205
|
+
if e.message =~ /can't unfreeze/
|
206
|
+
obj.clone
|
207
|
+
else
|
208
|
+
raise e
|
209
|
+
end
|
210
|
+
end
|
192
211
|
rescue *error_types
|
193
212
|
cloned[obj.object_id] = obj
|
194
213
|
obj
|
data/lib/bolt/version.rb
CHANGED
data/lib/bolt_server/config.rb
CHANGED
@@ -7,7 +7,7 @@ require 'bolt/error'
|
|
7
7
|
module BoltServer
|
8
8
|
class Config < BoltServer::BaseConfig
|
9
9
|
def config_keys
|
10
|
-
super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri]
|
10
|
+
super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri projects-dir]
|
11
11
|
end
|
12
12
|
|
13
13
|
def env_keys
|
@@ -33,7 +33,7 @@ module BoltServer
|
|
33
33
|
@executor = executor
|
34
34
|
@cache_dir = config['cache-dir']
|
35
35
|
@config = config
|
36
|
-
@logger =
|
36
|
+
@logger = Bolt::Logger.logger(self)
|
37
37
|
@cache_dir_mutex = cache_dir_mutex
|
38
38
|
|
39
39
|
if do_purge
|
@@ -64,17 +64,17 @@ module BoltServer
|
|
64
64
|
|
65
65
|
def client
|
66
66
|
@client ||= begin
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
67
|
+
uri = URI(@config['file-server-uri'])
|
68
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
69
|
+
https.use_ssl = true
|
70
|
+
https.ssl_version = :TLSv1_2
|
71
|
+
https.ca_file = @config['ssl-ca-cert']
|
72
|
+
https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
|
73
|
+
https.key = OpenSSL::PKey::RSA.new(ssl_key)
|
74
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
75
|
+
https.open_timeout = @config['file-server-conn-timeout']
|
76
|
+
https
|
77
|
+
end
|
78
78
|
end
|
79
79
|
|
80
80
|
def request_file(path, params, file)
|
@@ -5,6 +5,7 @@ require 'addressable/uri'
|
|
5
5
|
require 'bolt'
|
6
6
|
require 'bolt/error'
|
7
7
|
require 'bolt/inventory'
|
8
|
+
require 'bolt/project'
|
8
9
|
require 'bolt/target'
|
9
10
|
require 'bolt_server/file_cache'
|
10
11
|
require 'bolt/task/puppet_server'
|
@@ -191,20 +192,44 @@ module BoltServer
|
|
191
192
|
end
|
192
193
|
|
193
194
|
def in_pe_pal_env(environment)
|
194
|
-
if environment.nil?
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
195
|
+
return [400, '`environment` is a required argument'] if environment.nil?
|
196
|
+
@pal_mutex.synchronize do
|
197
|
+
pal = BoltServer::PE::PAL.new({}, environment)
|
198
|
+
yield pal
|
199
|
+
rescue Puppet::Environments::EnvironmentNotFound
|
200
|
+
[400, {
|
201
|
+
"class" => 'bolt/unknown-environment',
|
202
|
+
"message" => "Environment #{environment} not found"
|
203
|
+
}.to_json]
|
204
|
+
rescue Bolt::Error => e
|
205
|
+
[400, e.to_json]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def in_bolt_project(bolt_project)
|
210
|
+
return [400, '`project_ref` is a required argument'] if bolt_project.nil?
|
211
|
+
project_dir = File.join(@config['projects-dir'], bolt_project)
|
212
|
+
return [400, "`project_ref`: #{project_dir} does not exist"] unless Dir.exist?(project_dir)
|
213
|
+
@pal_mutex.synchronize do
|
214
|
+
project = Bolt::Project.create_project(project_dir)
|
215
|
+
bolt_config = Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
|
216
|
+
pal = Bolt::PAL.new(bolt_config.modulepath, nil, nil, nil, nil, nil, bolt_config.project)
|
217
|
+
module_path = [
|
218
|
+
BoltServer::PE::PAL::PE_BOLTLIB_PATH,
|
219
|
+
Bolt::PAL::BOLTLIB_PATH,
|
220
|
+
*bolt_config.modulepath,
|
221
|
+
Bolt::PAL::MODULES_PATH
|
222
|
+
]
|
223
|
+
# CODEREVIEW: I *think* this is the only thing we need to make different between bolt's PAL. Is it acceptable
|
224
|
+
# to hack this? Modulepath is currently a readable attribute, could we make it writeable?
|
225
|
+
pal.instance_variable_set(:@modulepath, module_path)
|
226
|
+
context = {
|
227
|
+
pal: pal,
|
228
|
+
config: bolt_config
|
229
|
+
}
|
230
|
+
yield context
|
231
|
+
rescue Bolt::Error => e
|
232
|
+
[400, e.to_json]
|
208
233
|
end
|
209
234
|
end
|
210
235
|
|
@@ -221,14 +246,12 @@ module BoltServer
|
|
221
246
|
plan_info
|
222
247
|
end
|
223
248
|
|
224
|
-
def build_puppetserver_uri(file_identifier, module_name,
|
249
|
+
def build_puppetserver_uri(file_identifier, module_name, parameters)
|
225
250
|
segments = file_identifier.split('/', 3)
|
226
251
|
if segments.size == 1
|
227
252
|
{
|
228
253
|
'path' => "/puppet/v3/file_content/tasks/#{module_name}/#{file_identifier}",
|
229
|
-
'params' =>
|
230
|
-
'environment' => environment
|
231
|
-
}
|
254
|
+
'params' => parameters
|
232
255
|
}
|
233
256
|
else
|
234
257
|
module_segment, mount_segment, name_segment = *segments
|
@@ -241,14 +264,12 @@ module BoltServer
|
|
241
264
|
when 'lib'
|
242
265
|
"/puppet/v3/file_content/plugins/#{name_segment}"
|
243
266
|
end,
|
244
|
-
'params' =>
|
245
|
-
'environment' => environment
|
246
|
-
}
|
267
|
+
'params' => parameters
|
247
268
|
}
|
248
269
|
end
|
249
270
|
end
|
250
271
|
|
251
|
-
def pe_task_info(pal, module_name, task_name,
|
272
|
+
def pe_task_info(pal, module_name, task_name, parameters)
|
252
273
|
# Handle case where task name is simply module name with special `init` task
|
253
274
|
task_name = if task_name == 'init' || task_name.nil?
|
254
275
|
module_name
|
@@ -261,7 +282,7 @@ module BoltServer
|
|
261
282
|
'filename' => file_hash['name'],
|
262
283
|
'sha256' => Digest::SHA256.hexdigest(File.read(file_hash['path'])),
|
263
284
|
'size_bytes' => File.size(file_hash['path']),
|
264
|
-
'uri' => build_puppetserver_uri(file_hash['name'], module_name,
|
285
|
+
'uri' => build_puppetserver_uri(file_hash['name'], module_name, parameters)
|
265
286
|
}
|
266
287
|
end
|
267
288
|
{
|
@@ -271,6 +292,21 @@ module BoltServer
|
|
271
292
|
}
|
272
293
|
end
|
273
294
|
|
295
|
+
def allowed_helper(metadata, allowlist)
|
296
|
+
allowed = allowlist.nil? || allowlist.include?(metadata['name']) ? true : false
|
297
|
+
metadata.merge({ 'allowed' => allowed })
|
298
|
+
end
|
299
|
+
|
300
|
+
def task_list(pal)
|
301
|
+
tasks = pal.list_tasks
|
302
|
+
tasks.map { |task_name, _description| { 'name' => task_name } }
|
303
|
+
end
|
304
|
+
|
305
|
+
def plan_list(pal)
|
306
|
+
plans = pal.list_plans.flatten
|
307
|
+
plans.map { |plan_name| { 'name' => plan_name } }
|
308
|
+
end
|
309
|
+
|
274
310
|
get '/' do
|
275
311
|
200
|
276
312
|
end
|
@@ -401,12 +437,40 @@ module BoltServer
|
|
401
437
|
end
|
402
438
|
end
|
403
439
|
|
440
|
+
# Fetches the metadata for a single plan
|
441
|
+
#
|
442
|
+
# @param project_ref [String] the project to fetch the plan from
|
443
|
+
get '/project_plans/:module_name/:plan_name' do
|
444
|
+
in_bolt_project(params['project_ref']) do |context|
|
445
|
+
plan_info = pe_plan_info(context[:pal], params[:module_name], params[:plan_name])
|
446
|
+
plan_info = allowed_helper(plan_info, context[:config].project.plans)
|
447
|
+
[200, plan_info.to_json]
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
404
451
|
# Fetches the metadata for a single task
|
405
452
|
#
|
406
453
|
# @param environment [String] the environment to fetch the task from
|
407
454
|
get '/tasks/:module_name/:task_name' do
|
408
455
|
in_pe_pal_env(params['environment']) do |pal|
|
409
|
-
|
456
|
+
ps_parameters = {
|
457
|
+
'environment' => params['environment']
|
458
|
+
}
|
459
|
+
task_info = pe_task_info(pal, params[:module_name], params[:task_name], ps_parameters)
|
460
|
+
[200, task_info.to_json]
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# Fetches the metadata for a single task
|
465
|
+
#
|
466
|
+
# @param bolt_project_ref [String] the reference to the bolt-project directory to load task metadata from
|
467
|
+
get '/project_tasks/:module_name/:task_name' do
|
468
|
+
in_bolt_project(params['project_ref']) do |context|
|
469
|
+
ps_parameters = {
|
470
|
+
'project' => params['project_ref']
|
471
|
+
}
|
472
|
+
task_info = pe_task_info(context[:pal], params[:module_name], params[:task_name], ps_parameters)
|
473
|
+
task_info = allowed_helper(task_info, context[:config].project.tasks)
|
410
474
|
[200, task_info.to_json]
|
411
475
|
end
|
412
476
|
end
|
@@ -435,13 +499,30 @@ module BoltServer
|
|
435
499
|
end
|
436
500
|
end
|
437
501
|
|
502
|
+
# Fetches the list of plans for a project
|
503
|
+
#
|
504
|
+
# @param project_ref [String] the project to fetch the list of plans from
|
505
|
+
get '/project_plans' do
|
506
|
+
in_bolt_project(params['project_ref']) do |context|
|
507
|
+
plans_response = plan_list(context[:pal])
|
508
|
+
|
509
|
+
# Dig in context for the allowlist of plans from project object
|
510
|
+
plans_response.map! { |metadata| allowed_helper(metadata, context[:config].project.plans) }
|
511
|
+
|
512
|
+
# We structure this array of plans to be an array of hashes so that it matches the structure
|
513
|
+
# returned by the puppetserver API that serves data like this. Structuring the output this way
|
514
|
+
# makes switching between puppetserver and bolt-server easier, which makes changes to switch
|
515
|
+
# to bolt-server smaller/simpler.
|
516
|
+
[200, plans_response.to_json]
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
438
520
|
# Fetches the list of tasks for an environment
|
439
521
|
#
|
440
522
|
# @param environment [String] the environment to fetch the list of tasks from
|
441
523
|
get '/tasks' do
|
442
524
|
in_pe_pal_env(params['environment']) do |pal|
|
443
|
-
|
444
|
-
tasks_response = tasks.map { |task_name, _description| { 'name' => task_name } }.to_json
|
525
|
+
tasks_response = task_list(pal).to_json
|
445
526
|
|
446
527
|
# We structure this array of tasks to be an array of hashes so that it matches the structure
|
447
528
|
# returned by the puppetserver API that serves data like this. Structuring the output this way
|
@@ -451,6 +532,24 @@ module BoltServer
|
|
451
532
|
end
|
452
533
|
end
|
453
534
|
|
535
|
+
# Fetches the list of tasks for a bolt-project
|
536
|
+
#
|
537
|
+
# @param project_ref [String] the project to fetch the list of tasks from
|
538
|
+
get '/project_tasks' do
|
539
|
+
in_bolt_project(params['project_ref']) do |context|
|
540
|
+
tasks_response = task_list(context[:pal])
|
541
|
+
|
542
|
+
# Dig in context for the allowlist of tasks from project object
|
543
|
+
tasks_response.map! { |metadata| allowed_helper(metadata, context[:config].project.tasks) }
|
544
|
+
|
545
|
+
# We structure this array of tasks to be an array of hashes so that it matches the structure
|
546
|
+
# returned by the puppetserver API that serves data like this. Structuring the output this way
|
547
|
+
# makes switching between puppetserver and bolt-server easier, which makes changes to switch
|
548
|
+
# to bolt-server smaller/simpler.
|
549
|
+
[200, tasks_response.to_json]
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
454
553
|
error 404 do
|
455
554
|
err = Bolt::Error.new("Could not find route #{request.path}",
|
456
555
|
'boltserver/not-found')
|