bolt 2.38.0 → 3.0.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 +17 -17
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
- data/lib/bolt/analytics.rb +3 -2
- data/lib/bolt/applicator.rb +11 -1
- data/lib/bolt/bolt_option_parser.rb +3 -113
- data/lib/bolt/catalog.rb +10 -29
- data/lib/bolt/cli.rb +54 -155
- data/lib/bolt/config.rb +63 -269
- data/lib/bolt/config/options.rb +59 -97
- data/lib/bolt/config/transport/local.rb +1 -0
- data/lib/bolt/config/transport/options.rb +10 -2
- data/lib/bolt/config/transport/orch.rb +1 -0
- data/lib/bolt/config/transport/ssh.rb +0 -5
- data/lib/bolt/executor.rb +15 -5
- data/lib/bolt/inventory.rb +3 -2
- data/lib/bolt/inventory/group.rb +35 -12
- data/lib/bolt/inventory/inventory.rb +1 -1
- data/lib/bolt/logger.rb +115 -11
- data/lib/bolt/module.rb +10 -2
- data/lib/bolt/module_installer.rb +4 -2
- data/lib/bolt/module_installer/resolver.rb +65 -12
- data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
- data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
- data/lib/bolt/outputter/human.rb +9 -5
- data/lib/bolt/outputter/json.rb +16 -16
- data/lib/bolt/outputter/rainbow.rb +3 -3
- data/lib/bolt/pal.rb +93 -14
- data/lib/bolt/pal/yaml_plan.rb +8 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
- data/lib/bolt/pal/yaml_plan/step.rb +3 -24
- data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
- data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
- data/lib/bolt/plugin.rb +3 -3
- data/lib/bolt/plugin/cache.rb +8 -8
- data/lib/bolt/plugin/module.rb +0 -23
- data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
- data/lib/bolt/plugin/puppetdb.rb +1 -1
- data/lib/bolt/project.rb +54 -81
- data/lib/bolt/project_manager.rb +4 -3
- data/lib/bolt/project_manager/module_migrator.rb +6 -5
- data/lib/bolt/rerun.rb +1 -1
- data/lib/bolt/shell/bash.rb +1 -1
- data/lib/bolt/shell/bash/tmpdir.rb +4 -1
- data/lib/bolt/shell/powershell.rb +3 -4
- data/lib/bolt/shell/powershell/snippets.rb +9 -149
- data/lib/bolt/task.rb +1 -1
- data/lib/bolt/transport/docker/connection.rb +2 -2
- data/lib/bolt/transport/local.rb +1 -9
- data/lib/bolt/transport/orch/connection.rb +1 -1
- data/lib/bolt/transport/ssh.rb +1 -2
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/validator.rb +16 -15
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/config.rb +1 -1
- data/lib/bolt_server/schemas/partials/task.json +1 -1
- data/lib/bolt_server/transport_app.rb +3 -2
- data/libexec/bolt_catalog +1 -1
- data/modules/aggregate/plans/count.pp +21 -0
- data/modules/aggregate/plans/targets.pp +21 -0
- data/modules/puppet_connect/plans/test_input_data.pp +31 -0
- data/modules/puppetdb_fact/plans/init.pp +10 -0
- metadata +26 -17
- data/modules/aggregate/plans/nodes.pp +0 -36
data/lib/bolt/cli.rb
CHANGED
@@ -33,19 +33,18 @@ module Bolt
|
|
33
33
|
|
34
34
|
class CLI
|
35
35
|
COMMANDS = {
|
36
|
-
'command'
|
37
|
-
'script'
|
38
|
-
'task'
|
39
|
-
'plan'
|
40
|
-
'file'
|
41
|
-
'
|
42
|
-
'
|
43
|
-
'
|
44
|
-
'
|
45
|
-
'
|
46
|
-
'
|
47
|
-
'
|
48
|
-
'guide' => %w[]
|
36
|
+
'command' => %w[run],
|
37
|
+
'script' => %w[run],
|
38
|
+
'task' => %w[show run],
|
39
|
+
'plan' => %w[show run convert new],
|
40
|
+
'file' => %w[download upload],
|
41
|
+
'secret' => %w[encrypt decrypt createkeys],
|
42
|
+
'inventory' => %w[show],
|
43
|
+
'group' => %w[show],
|
44
|
+
'project' => %w[init migrate],
|
45
|
+
'module' => %w[add generate-types install show],
|
46
|
+
'apply' => %w[],
|
47
|
+
'guide' => %w[]
|
49
48
|
}.freeze
|
50
49
|
|
51
50
|
attr_reader :config, :options
|
@@ -147,10 +146,6 @@ module Bolt
|
|
147
146
|
end
|
148
147
|
|
149
148
|
validate(options)
|
150
|
-
|
151
|
-
# Deprecation warnings can't be issued until after config is loaded, so
|
152
|
-
# store them for later.
|
153
|
-
@parser_deprecations = parser.deprecations
|
154
149
|
rescue Bolt::Error => e
|
155
150
|
fatal_error(e)
|
156
151
|
raise e
|
@@ -159,25 +154,19 @@ module Bolt
|
|
159
154
|
# Loads the project and configuration. All errors that are raised here are not
|
160
155
|
# handled by the outputter, as it relies on config being loaded.
|
161
156
|
def load_config
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
Bolt::
|
157
|
+
project = if ENV['BOLT_PROJECT']
|
158
|
+
Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
|
159
|
+
elsif options[:project]
|
160
|
+
dir = Pathname.new(options[:project])
|
161
|
+
if (dir + Bolt::Project::BOLTDIR_NAME).directory?
|
162
|
+
Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
|
163
|
+
else
|
164
|
+
Bolt::Project.create_project(dir)
|
165
|
+
end
|
167
166
|
else
|
168
|
-
|
169
|
-
project = if cli_flag
|
170
|
-
dir = Pathname.new(cli_flag)
|
171
|
-
if (dir + Bolt::Project::BOLTDIR_NAME).directory?
|
172
|
-
Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
|
173
|
-
else
|
174
|
-
Bolt::Project.create_project(dir)
|
175
|
-
end
|
176
|
-
else
|
177
|
-
Bolt::Project.find_boltdir(Dir.pwd)
|
178
|
-
end
|
179
|
-
Bolt::Config.from_project(project, options)
|
167
|
+
Bolt::Project.find_boltdir(Dir.pwd)
|
180
168
|
end
|
169
|
+
@config = Bolt::Config.from_project(project, options)
|
181
170
|
rescue Bolt::Error => e
|
182
171
|
fatal_error(e)
|
183
172
|
raise e
|
@@ -185,20 +174,16 @@ module Bolt
|
|
185
174
|
|
186
175
|
# Completes the setup process by configuring Bolt and log messages
|
187
176
|
def finalize_setup
|
188
|
-
Bolt::Logger.configure(config.log, config.color)
|
177
|
+
Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
|
189
178
|
Bolt::Logger.analytics = analytics
|
179
|
+
Bolt::Logger.flush_queue
|
190
180
|
|
191
181
|
# Logger must be configured before checking path case and project file, otherwise logs will not display
|
192
182
|
config.check_path_case('modulepath', config.modulepath)
|
193
183
|
config.project.check_deprecated_file
|
194
184
|
|
195
|
-
|
196
|
-
|
197
|
-
@parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
|
198
|
-
config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
|
199
|
-
|
200
|
-
if options[:clear_cache] && File.exist?(config.project.cache_file)
|
201
|
-
FileUtils.rm(config.project.cache_file)
|
185
|
+
if options[:clear_cache] && File.exist?(config.project.plugin_cache_file)
|
186
|
+
FileUtils.rm(config.project.plugin_cache_file)
|
202
187
|
end
|
203
188
|
|
204
189
|
warn_inventory_overrides_cli(options)
|
@@ -212,12 +197,14 @@ module Bolt
|
|
212
197
|
|
213
198
|
private def validate_ps_version
|
214
199
|
if Bolt::Util.powershell?
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
200
|
+
command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
|
201
|
+
"Bypass -Command $PSVersionTable.PSVersion.Major"
|
202
|
+
stdout, _stderr, _status = Open3.capture3(command)
|
203
|
+
|
204
|
+
return unless !stdout.empty? && stdout.to_i < 3
|
205
|
+
|
206
|
+
msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
|
207
|
+
Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
|
221
208
|
end
|
222
209
|
end
|
223
210
|
|
@@ -311,10 +298,6 @@ module Bolt
|
|
311
298
|
"Unknown argument(s) #{options[:leftovers].join(', ')}"
|
312
299
|
end
|
313
300
|
|
314
|
-
if options.slice(:boltdir, :configfile, :project).length > 1
|
315
|
-
raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
|
316
|
-
end
|
317
|
-
|
318
301
|
if options[:noop] &&
|
319
302
|
!(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
|
320
303
|
raise Bolt::CLIError,
|
@@ -327,10 +310,6 @@ module Bolt
|
|
327
310
|
"Option '--env-var' may only be specified when running a command or script"
|
328
311
|
end
|
329
312
|
end
|
330
|
-
|
331
|
-
if options.key?(:debug) && options.key?(:log)
|
332
|
-
raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
|
333
|
-
end
|
334
313
|
end
|
335
314
|
|
336
315
|
def handle_parser_errors
|
@@ -373,7 +352,10 @@ module Bolt
|
|
373
352
|
conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
|
374
353
|
|
375
354
|
if inventory_source && conflicting_options.any?
|
376
|
-
|
355
|
+
Bolt::Logger.warn(
|
356
|
+
"cli_overrides",
|
357
|
+
"CLI arguments #{conflicting_options.to_a} may be overridden by Inventory: #{inventory_source}"
|
358
|
+
)
|
377
359
|
end
|
378
360
|
end
|
379
361
|
|
@@ -390,7 +372,7 @@ module Bolt
|
|
390
372
|
# Initialize inventory and targets. Errors here are better to catch early.
|
391
373
|
# options[:target_args] will contain a string/array version of the targetting options this is passed to plans
|
392
374
|
# options[:targets] will contain a resolved set of Target objects
|
393
|
-
unless %w[guide module project
|
375
|
+
unless %w[guide module project secret].include?(options[:subcommand]) ||
|
394
376
|
%w[convert new show].include?(options[:action])
|
395
377
|
update_targets(options)
|
396
378
|
end
|
@@ -445,9 +427,6 @@ module Bolt
|
|
445
427
|
list_modules
|
446
428
|
end
|
447
429
|
return 0
|
448
|
-
when 'show-modules'
|
449
|
-
list_modules
|
450
|
-
return 0
|
451
430
|
when 'convert'
|
452
431
|
pal.convert_plan(options[:object])
|
453
432
|
return 0
|
@@ -497,17 +476,6 @@ module Bolt
|
|
497
476
|
when 'generate-types'
|
498
477
|
code = generate_types
|
499
478
|
end
|
500
|
-
when 'puppetfile'
|
501
|
-
case options[:action]
|
502
|
-
when 'generate-types'
|
503
|
-
code = generate_types
|
504
|
-
when 'install'
|
505
|
-
code = install_puppetfile(
|
506
|
-
config.puppetfile_config,
|
507
|
-
config.puppetfile,
|
508
|
-
config.modulepath.first
|
509
|
-
)
|
510
|
-
end
|
511
479
|
when 'secret'
|
512
480
|
code = Bolt::Secret.execute(plugins, outputter, options)
|
513
481
|
when 'apply'
|
@@ -525,7 +493,6 @@ module Bolt
|
|
525
493
|
|
526
494
|
elapsed_time = Benchmark.realtime do
|
527
495
|
executor_opts = {}
|
528
|
-
executor_opts[:description] = options[:description] if options.key?(:description)
|
529
496
|
executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
|
530
497
|
executor.subscribe(outputter)
|
531
498
|
executor.subscribe(log_outputter)
|
@@ -538,15 +505,11 @@ module Bolt
|
|
538
505
|
validate_file('script', script)
|
539
506
|
executor.run_script(targets, script, options[:leftovers], executor_opts)
|
540
507
|
when 'task'
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
inventory,
|
547
|
-
options[:description])
|
548
|
-
end
|
549
|
-
r
|
508
|
+
pal.run_task(options[:object],
|
509
|
+
targets,
|
510
|
+
options[:task_options],
|
511
|
+
executor,
|
512
|
+
inventory)
|
550
513
|
when 'file'
|
551
514
|
src = options[:object]
|
552
515
|
dest = options[:leftovers].first
|
@@ -607,7 +570,7 @@ module Bolt
|
|
607
570
|
end
|
608
571
|
|
609
572
|
def list_plans
|
610
|
-
plans = filter_content(pal.
|
573
|
+
plans = filter_content(pal.list_plans_with_cache(filter_content: true), options[:filter])
|
611
574
|
outputter.print_plans(plans, pal.user_modulepath)
|
612
575
|
end
|
613
576
|
|
@@ -659,7 +622,7 @@ module Bolt
|
|
659
622
|
if node_param && target_param
|
660
623
|
msg = "Plan parameters include both 'nodes' and 'targets' with type 'TargetSpec', " \
|
661
624
|
"neither will populated with the value for --nodes or --targets."
|
662
|
-
|
625
|
+
Bolt::Logger.warn("nodes_targets_parameters", msg)
|
663
626
|
elsif node_param
|
664
627
|
plan_arguments['nodes'] = nodes.join(',')
|
665
628
|
elsif target_param
|
@@ -669,7 +632,6 @@ module Bolt
|
|
669
632
|
|
670
633
|
plan_context = { plan_name: plan_name,
|
671
634
|
params: plan_arguments }
|
672
|
-
plan_context[:description] = options[:description] if options[:description]
|
673
635
|
|
674
636
|
executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
|
675
637
|
if %w[human rainbow].include?(options.fetch(:format, 'human'))
|
@@ -681,9 +643,7 @@ module Bolt
|
|
681
643
|
|
682
644
|
executor.subscribe(log_outputter)
|
683
645
|
executor.start_plan(plan_context)
|
684
|
-
result =
|
685
|
-
pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
|
686
|
-
end
|
646
|
+
result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
|
687
647
|
|
688
648
|
# If a non-bolt exception bubbles up the plan won't get finished
|
689
649
|
executor.finish_plan(result)
|
@@ -706,7 +666,7 @@ module Bolt
|
|
706
666
|
"about defining and declaring classes and types in the Puppet documentation at "\
|
707
667
|
"https://puppet.com/docs/puppet/latest/lang_classes.html and "\
|
708
668
|
"https://puppet.com/docs/puppet/latest/lang_defined_types.html"
|
709
|
-
|
669
|
+
Bolt::Logger.warn("empty_manifest", message)
|
710
670
|
end
|
711
671
|
|
712
672
|
executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
|
@@ -735,14 +695,12 @@ module Bolt
|
|
735
695
|
end
|
736
696
|
|
737
697
|
def list_modules
|
738
|
-
assert_puppetfile_or_module_command(config.project.modules)
|
739
698
|
outputter.print_module_list(pal.list_modules)
|
740
699
|
end
|
741
700
|
|
742
701
|
def generate_types
|
743
|
-
assert_puppetfile_or_module_command(config.project.modules)
|
744
702
|
# generate_types will surface a nice error with helpful message if it fails
|
745
|
-
pal.generate_types
|
703
|
+
pal.generate_types(cache: true)
|
746
704
|
0
|
747
705
|
end
|
748
706
|
|
@@ -750,27 +708,17 @@ module Bolt
|
|
750
708
|
#
|
751
709
|
def install_project_modules(project, config, force, resolve)
|
752
710
|
assert_project_file(project)
|
753
|
-
assert_puppetfile_or_module_command(project.modules)
|
754
711
|
|
755
|
-
unless project.modules
|
712
|
+
unless project.modules.any?
|
756
713
|
outputter.print_message "Project configuration file #{project.project_file} does not "\
|
757
714
|
"specify any module dependencies. Nothing to do."
|
758
715
|
return 0
|
759
716
|
end
|
760
717
|
|
761
|
-
if resolve != false && config.any?
|
762
|
-
@logger.warn(
|
763
|
-
"Detected configuration for 'module-install'. This configuration is currently "\
|
764
|
-
"only supported when installing modules, not when resolving module dependencies. "\
|
765
|
-
"For more information, see https://pup.pt/bolt-module-install"
|
766
|
-
)
|
767
|
-
end
|
768
|
-
|
769
|
-
modules = project.modules || []
|
770
718
|
installer = Bolt::ModuleInstaller.new(outputter, pal)
|
771
719
|
|
772
720
|
ok = outputter.spin do
|
773
|
-
installer.install(modules,
|
721
|
+
installer.install(project.modules,
|
774
722
|
project.puppetfile,
|
775
723
|
project.managed_moduledir,
|
776
724
|
config,
|
@@ -785,22 +733,12 @@ module Bolt
|
|
785
733
|
#
|
786
734
|
def add_project_module(name, project, config)
|
787
735
|
assert_project_file(project)
|
788
|
-
assert_puppetfile_or_module_command(project.modules)
|
789
|
-
|
790
|
-
if config.any?
|
791
|
-
@logger.warn(
|
792
|
-
"Detected configuration for 'module-install'. This configuration is currently "\
|
793
|
-
"only supported when installing modules, not when resolving module dependencies. "\
|
794
|
-
"For more information, see https://pup.pt/bolt-module-install"
|
795
|
-
)
|
796
|
-
end
|
797
736
|
|
798
|
-
modules = project.modules || []
|
799
737
|
installer = Bolt::ModuleInstaller.new(outputter, pal)
|
800
738
|
|
801
739
|
ok = outputter.spin do
|
802
740
|
installer.add(name,
|
803
|
-
modules,
|
741
|
+
project.modules,
|
804
742
|
project.puppetfile,
|
805
743
|
project.managed_moduledir,
|
806
744
|
project.project_file,
|
@@ -831,8 +769,6 @@ module Bolt
|
|
831
769
|
# Loads a Puppetfile and installs its modules.
|
832
770
|
#
|
833
771
|
def install_puppetfile(puppetfile_config, puppetfile, moduledir)
|
834
|
-
assert_puppetfile_or_module_command(config.project.modules)
|
835
|
-
|
836
772
|
outputter.print_message("Installing modules from Puppetfile")
|
837
773
|
installer = Bolt::ModuleInstaller.new(outputter, pal)
|
838
774
|
ok = outputter.spin do
|
@@ -842,43 +778,6 @@ module Bolt
|
|
842
778
|
ok ? 0 : 1
|
843
779
|
end
|
844
780
|
|
845
|
-
# Raises an error if the 'puppetfile install' command is deprecated due to
|
846
|
-
# modules being configured.
|
847
|
-
#
|
848
|
-
def assert_puppetfile_or_module_command(modules)
|
849
|
-
if Bolt::Util.powershell?
|
850
|
-
case options[:action]
|
851
|
-
when 'generate-types'
|
852
|
-
old_command = 'Register-BoltPuppetfileTypes'
|
853
|
-
new_command = 'Register-BoltModuleTypes'
|
854
|
-
when 'install'
|
855
|
-
old_command = 'Install-BoltPuppetfile'
|
856
|
-
new_command = 'Install-BoltModule'
|
857
|
-
when 'show', 'show-modules'
|
858
|
-
old_command = 'Get-BoltPuppetfileModules'
|
859
|
-
new_command = 'Get-BoltModule'
|
860
|
-
end
|
861
|
-
else
|
862
|
-
old_command = "bolt puppetfile #{options[:action]}"
|
863
|
-
new_command = if options[:action] == 'show-modules'
|
864
|
-
'bolt module show'
|
865
|
-
else
|
866
|
-
"bolt module #{options[:action]}"
|
867
|
-
end
|
868
|
-
end
|
869
|
-
|
870
|
-
if modules && options[:subcommand] == 'puppetfile'
|
871
|
-
raise Bolt::CLIError,
|
872
|
-
"Unable to use command '#{old_command}' when 'modules' is configured in "\
|
873
|
-
"bolt-project.yaml. Use '#{new_command}' instead."
|
874
|
-
elsif modules.nil? && options[:subcommand] == 'module'
|
875
|
-
msg = "Unable to use command '#{new_command}' when 'modules' is not configured in "\
|
876
|
-
"bolt-project.yaml. "
|
877
|
-
msg += "Use '#{old_command}' instead." if options[:action] != 'add'
|
878
|
-
raise Bolt::CLIError, msg
|
879
|
-
end
|
880
|
-
end
|
881
|
-
|
882
781
|
def pal
|
883
782
|
@pal ||= Bolt::PAL.new(Bolt::Config::Modulepath.new(config.modulepath),
|
884
783
|
config.hiera_config,
|
@@ -975,7 +874,7 @@ module Bolt
|
|
975
874
|
set the BOLT_GEM environment variable.
|
976
875
|
MSG
|
977
876
|
|
978
|
-
|
877
|
+
Bolt::Logger.warn("gem_install", msg)
|
979
878
|
end
|
980
879
|
|
981
880
|
# We only need to enumerate bundled content when running a task or plan
|
data/lib/bolt/config.rb
CHANGED
@@ -20,10 +20,9 @@ module Bolt
|
|
20
20
|
class Config
|
21
21
|
include Bolt::Config::Options
|
22
22
|
|
23
|
-
attr_reader :config_files, :
|
23
|
+
attr_reader :config_files, :data, :transports, :project, :modified_concurrency
|
24
24
|
|
25
|
-
|
26
|
-
BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
|
25
|
+
DEFAULTS_NAME = 'bolt-defaults.yaml'
|
27
26
|
|
28
27
|
# The default concurrency value that is used when the ulimit is not low (i.e. < 700)
|
29
28
|
DEFAULT_DEFAULT_CONCURRENCY = 100
|
@@ -33,70 +32,9 @@ module Bolt
|
|
33
32
|
end
|
34
33
|
|
35
34
|
def self.from_project(project, overrides = {})
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
conf = if project.project_file == project.config_file
|
40
|
-
project.data
|
41
|
-
else
|
42
|
-
c = Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
|
43
|
-
|
44
|
-
# Validate the config against the schema. This will raise a single error
|
45
|
-
# with all validation errors.
|
46
|
-
Bolt::Validator.new.tap do |validator|
|
47
|
-
validator.validate(c, bolt_schema, project.config_file.to_s)
|
48
|
-
|
49
|
-
validator.warnings.each { |warning| logs << { warn: warning } }
|
50
|
-
|
51
|
-
validator.deprecations.each do |dep|
|
52
|
-
deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
logs << { debug: "Loaded configuration from #{project.config_file}" } if File.exist?(project.config_file)
|
57
|
-
c
|
58
|
-
end
|
59
|
-
data = load_defaults(project).push(
|
60
|
-
filepath: project.config_file,
|
61
|
-
data: conf,
|
62
|
-
logs: logs,
|
63
|
-
deprecations: deprecations
|
64
|
-
)
|
65
|
-
|
66
|
-
new(project, data, overrides)
|
67
|
-
end
|
68
|
-
|
69
|
-
def self.from_file(configfile, overrides = {})
|
70
|
-
project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
|
71
|
-
logs = []
|
72
|
-
deprecations = []
|
73
|
-
|
74
|
-
conf = if project.project_file == project.config_file
|
75
|
-
project.data
|
76
|
-
else
|
77
|
-
c = Bolt::Util.read_yaml_hash(configfile, 'config')
|
78
|
-
|
79
|
-
# Validate the config against the schema. This will raise a single error
|
80
|
-
# with all validation errors.
|
81
|
-
Bolt::Validator.new.tap do |validator|
|
82
|
-
validator.validate(c, bolt_schema, project.config_file.to_s)
|
83
|
-
|
84
|
-
validator.warnings.each { |warning| logs << { warn: warning } }
|
85
|
-
|
86
|
-
validator.deprecations.each do |dep|
|
87
|
-
deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
logs << { debug: "Loaded configuration from #{configfile}" }
|
92
|
-
c
|
93
|
-
end
|
94
|
-
|
95
|
-
data = load_defaults(project).push(
|
96
|
-
filepath: configfile,
|
97
|
-
data: conf,
|
98
|
-
logs: logs,
|
99
|
-
deprecations: deprecations
|
35
|
+
data = load_defaults.push(
|
36
|
+
filepath: project.project_file,
|
37
|
+
data: project.data
|
100
38
|
)
|
101
39
|
|
102
40
|
new(project, data, overrides)
|
@@ -115,7 +53,7 @@ module Bolt
|
|
115
53
|
def self.defaults_schema
|
116
54
|
schema = {
|
117
55
|
type: Hash,
|
118
|
-
properties:
|
56
|
+
properties: DEFAULTS_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
|
119
57
|
definitions: OPTIONS.merge(transport_definitions)
|
120
58
|
}
|
121
59
|
|
@@ -124,16 +62,6 @@ module Bolt
|
|
124
62
|
schema
|
125
63
|
end
|
126
64
|
|
127
|
-
# Builds the schema for bolt.yaml used by the validator.
|
128
|
-
#
|
129
|
-
def self.bolt_schema
|
130
|
-
{
|
131
|
-
type: Hash,
|
132
|
-
properties: (BOLT_OPTIONS + INVENTORY_OPTIONS.keys).map { |opt| [opt, _ref: opt] }.to_h,
|
133
|
-
definitions: OPTIONS.merge(transport_definitions)
|
134
|
-
}
|
135
|
-
end
|
136
|
-
|
137
65
|
def self.system_path
|
138
66
|
if Bolt::Util.windows?
|
139
67
|
Pathname.new(File.join(ENV['ALLUSERSPROFILE'], 'PuppetLabs', 'bolt', 'etc'))
|
@@ -149,41 +77,31 @@ module Bolt
|
|
149
77
|
end
|
150
78
|
|
151
79
|
# Loads a 'bolt-defaults.yaml' file, which contains default configuration that applies to all
|
152
|
-
# projects. This file does not allow project-specific configuration such as 'hiera-config'
|
153
|
-
#
|
80
|
+
# projects. This file does not allow project-specific configuration such as 'hiera-config'
|
81
|
+
# and nests all default inventory configuration under an 'inventory-config' key.
|
154
82
|
def self.load_bolt_defaults_yaml(dir)
|
155
|
-
filepath = dir +
|
83
|
+
filepath = dir + DEFAULTS_NAME
|
156
84
|
data = Bolt::Util.read_yaml_hash(filepath, 'config')
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
# Warn if 'bolt.yaml' detected in same directory.
|
161
|
-
if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
|
162
|
-
logs.push(
|
163
|
-
warn: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
|
164
|
-
"will be ignored."
|
165
|
-
)
|
166
|
-
end
|
85
|
+
|
86
|
+
Bolt::Logger.debug("Loaded configuration from #{filepath}")
|
167
87
|
|
168
88
|
# Validate the config against the schema. This will raise a single error
|
169
89
|
# with all validation errors.
|
170
90
|
Bolt::Validator.new.tap do |validator|
|
171
91
|
validator.validate(data, defaults_schema, filepath)
|
172
|
-
|
173
|
-
validator.
|
174
|
-
|
175
|
-
validator.deprecations.each do |dep|
|
176
|
-
deprecations << { type: "#{BOLT_DEFAULTS_NAME} #{dep[:option]}", msg: dep[:message] }
|
177
|
-
end
|
92
|
+
validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
|
93
|
+
validator.deprecations.each { |dep| Bolt::Logger.deprecate(dep[:id], dep[:msg]) }
|
178
94
|
end
|
179
95
|
|
180
96
|
# Remove project-specific config such as hiera-config, etc.
|
181
|
-
project_config = data.slice(*(
|
97
|
+
project_config = data.slice(*(PROJECT_OPTIONS - DEFAULTS_OPTIONS))
|
182
98
|
|
183
99
|
if project_config.any?
|
184
100
|
data.reject! { |key, _| project_config.include?(key) }
|
185
|
-
|
186
|
-
|
101
|
+
|
102
|
+
Bolt::Logger.warn(
|
103
|
+
"unsupported_project_config",
|
104
|
+
"Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
|
187
105
|
"Project configuration should be set in 'bolt-project.yaml'."
|
188
106
|
)
|
189
107
|
end
|
@@ -193,8 +111,10 @@ module Bolt
|
|
193
111
|
|
194
112
|
if transport_config.any?
|
195
113
|
data.reject! { |key, _| transport_config.include?(key) }
|
196
|
-
|
197
|
-
|
114
|
+
|
115
|
+
Bolt::Logger.warn(
|
116
|
+
"unsupported_inventory_config",
|
117
|
+
"Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
|
198
118
|
"Transport configuration should be set under the 'inventory-config' option or "\
|
199
119
|
"in 'inventory.yaml'."
|
200
120
|
)
|
@@ -219,55 +139,20 @@ module Bolt
|
|
219
139
|
data = data.merge(data.delete('inventory-config'))
|
220
140
|
end
|
221
141
|
|
222
|
-
{ filepath: filepath, data: data
|
223
|
-
end
|
224
|
-
|
225
|
-
# Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
|
226
|
-
# here since Bolt::Config will just ignore any invalid keys.
|
227
|
-
def self.load_bolt_yaml(dir)
|
228
|
-
filepath = dir + BOLT_CONFIG_NAME
|
229
|
-
data = Bolt::Util.read_yaml_hash(filepath, 'config')
|
230
|
-
logs = [{ debug: "Loaded configuration from #{filepath}" }]
|
231
|
-
deprecations = [{ type: 'Using bolt.yaml for system configuration',
|
232
|
-
msg: "Configuration file #{filepath} is deprecated and will be removed in Bolt 3.0. "\
|
233
|
-
"See https://pup.pt/update-bolt-config for how to update to the latest Bolt practices." }]
|
234
|
-
|
235
|
-
# Validate the config against the schema. This will raise a single error
|
236
|
-
# with all validation errors.
|
237
|
-
Bolt::Validator.new.tap do |validator|
|
238
|
-
validator.validate(data, bolt_schema, filepath)
|
239
|
-
|
240
|
-
validator.warnings.each { |warning| logs << { warn: warning } }
|
241
|
-
|
242
|
-
validator.deprecations.each do |dep|
|
243
|
-
deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
{ filepath: filepath, data: data, logs: logs, deprecations: deprecations }
|
142
|
+
{ filepath: filepath, data: data }
|
248
143
|
end
|
249
144
|
|
250
|
-
def self.load_defaults
|
145
|
+
def self.load_defaults
|
251
146
|
confs = []
|
252
147
|
|
253
|
-
# Load system-level config.
|
254
|
-
|
255
|
-
# config file, don't load it a second time.
|
256
|
-
if File.exist?(system_path + BOLT_DEFAULTS_NAME)
|
148
|
+
# Load system-level config.
|
149
|
+
if File.exist?(system_path + DEFAULTS_NAME)
|
257
150
|
confs << load_bolt_defaults_yaml(system_path)
|
258
|
-
elsif File.exist?(system_path + BOLT_CONFIG_NAME) &&
|
259
|
-
(system_path + BOLT_CONFIG_NAME) != project.config_file
|
260
|
-
confs << load_bolt_yaml(system_path)
|
261
151
|
end
|
262
152
|
|
263
|
-
# Load user-level config if there is a homedir.
|
264
|
-
|
265
|
-
|
266
|
-
if File.exist?(user_path + BOLT_DEFAULTS_NAME)
|
267
|
-
confs << load_bolt_defaults_yaml(user_path)
|
268
|
-
elsif File.exist?(user_path + BOLT_CONFIG_NAME)
|
269
|
-
confs << load_bolt_yaml(user_path)
|
270
|
-
end
|
153
|
+
# Load user-level config if there is a homedir.
|
154
|
+
if user_path && File.exist?(user_path + DEFAULTS_NAME)
|
155
|
+
confs << load_bolt_defaults_yaml(user_path)
|
271
156
|
end
|
272
157
|
|
273
158
|
confs
|
@@ -275,33 +160,26 @@ module Bolt
|
|
275
160
|
|
276
161
|
def initialize(project, config_data, overrides = {})
|
277
162
|
unless config_data.is_a?(Array)
|
278
|
-
config_data = [{ filepath: project.
|
279
|
-
data: config_data,
|
280
|
-
logs: [],
|
281
|
-
deprecations: [] }]
|
163
|
+
config_data = [{ filepath: project.project_file, data: config_data }]
|
282
164
|
end
|
283
165
|
|
284
166
|
@logger = Bolt::Logger.logger(self)
|
285
167
|
@project = project
|
286
|
-
@logs = @project.logs.dup
|
287
|
-
@deprecations = @project.deprecations.dup
|
288
168
|
@transports = {}
|
289
169
|
@config_files = []
|
290
170
|
|
291
171
|
default_data = {
|
292
172
|
'apply-settings' => {},
|
293
|
-
'apply_settings' => {},
|
294
173
|
'color' => true,
|
295
174
|
'compile-concurrency' => Etc.nprocessors,
|
296
175
|
'concurrency' => default_concurrency,
|
176
|
+
'disable-warnings' => [],
|
297
177
|
'format' => 'human',
|
298
178
|
'log' => { 'console' => {} },
|
299
179
|
'module-install' => {},
|
300
180
|
'plugin-hooks' => {},
|
301
|
-
'plugin_hooks' => {},
|
302
181
|
'plugins' => {},
|
303
182
|
'puppetdb' => {},
|
304
|
-
'puppetfile' => {},
|
305
183
|
'save-rerun' => true,
|
306
184
|
'spinner' => true,
|
307
185
|
'transport' => 'ssh'
|
@@ -315,9 +193,6 @@ module Bolt
|
|
315
193
|
end
|
316
194
|
|
317
195
|
loaded_data = config_data.each_with_object([]) do |data, acc|
|
318
|
-
@logs.concat(data[:logs]) if data[:logs].any?
|
319
|
-
@deprecations.concat(data[:deprecations]) if data[:deprecations].any?
|
320
|
-
|
321
196
|
if data[:data].any?
|
322
197
|
@config_files.push(data[:filepath])
|
323
198
|
acc.push(data[:data])
|
@@ -347,28 +222,25 @@ module Bolt
|
|
347
222
|
def normalize_overrides(options)
|
348
223
|
opts = options.transform_keys(&:to_s)
|
349
224
|
|
350
|
-
# Pull out config options. We need to add 'transport'
|
351
|
-
# OPTIONS hash but
|
352
|
-
overrides = opts.slice(*OPTIONS.keys, 'transport')
|
225
|
+
# Pull out config options. We need to add 'transport' and 'inventoryfile' as they're
|
226
|
+
# not part of the OPTIONS hash but are valid options that can be set with CLI options
|
227
|
+
overrides = opts.slice(*OPTIONS.keys, 'inventoryfile', 'transport')
|
353
228
|
|
354
229
|
# Pull out transport config options
|
355
230
|
TRANSPORT_CONFIG.each do |transport, config|
|
356
231
|
overrides[transport] = opts.slice(*config.options)
|
357
232
|
end
|
358
233
|
|
359
|
-
# Set console log to debug if in debug mode
|
360
|
-
if options[:debug]
|
361
|
-
overrides['log'] = { 'console' => { 'level' => 'debug' } }
|
362
|
-
end
|
363
|
-
|
364
|
-
if options[:puppetfile_path]
|
365
|
-
@puppetfile = options[:puppetfile_path]
|
366
|
-
end
|
367
|
-
|
368
234
|
overrides['trace'] = opts['trace'] if opts.key?('trace')
|
369
235
|
|
370
|
-
# Validate the overrides
|
371
|
-
|
236
|
+
# Validate the overrides that can have arbitrary values
|
237
|
+
schema = {
|
238
|
+
type: Hash,
|
239
|
+
properties: CLI_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
|
240
|
+
definitions: OPTIONS.merge(INVENTORY_OPTIONS)
|
241
|
+
}
|
242
|
+
|
243
|
+
Bolt::Validator.new.validate(overrides.slice(*CLI_OPTIONS), schema, 'command line')
|
372
244
|
|
373
245
|
overrides
|
374
246
|
end
|
@@ -386,8 +258,11 @@ module Bolt
|
|
386
258
|
when *TRANSPORT_CONFIG.keys
|
387
259
|
Bolt::Util.deep_merge(val1, val2)
|
388
260
|
# Hash values are shallow merged
|
389
|
-
when '
|
261
|
+
when 'apply-settings', 'log', 'plugin-hooks', 'puppetdb'
|
390
262
|
val1.merge(val2)
|
263
|
+
# Disabled warnings are concatenated
|
264
|
+
when 'disable-warnings'
|
265
|
+
val1.concat(val2)
|
391
266
|
# All other values are overwritten
|
392
267
|
else
|
393
268
|
val2
|
@@ -423,7 +298,7 @@ module Bolt
|
|
423
298
|
end
|
424
299
|
|
425
300
|
# Filter hashes to only include valid options
|
426
|
-
%w[apply-settings
|
301
|
+
%w[apply-settings module-install].each do |opt|
|
427
302
|
@data[opt] = @data[opt].slice(*OPTIONS.dig(opt, :properties).keys)
|
428
303
|
end
|
429
304
|
end
|
@@ -448,49 +323,19 @@ module Bolt
|
|
448
323
|
next if val == 'disable'
|
449
324
|
|
450
325
|
name = normalize_log(key)
|
451
|
-
|
452
|
-
# But otherwise it has to be a Hash
|
453
|
-
unless val.is_a?(Hash)
|
454
|
-
raise Bolt::ValidationError,
|
455
|
-
"config of log #{name} must be a Hash, received #{val.class} #{val.inspect}"
|
456
|
-
end
|
457
|
-
|
458
|
-
acc[name] = val.slice('append', 'level')
|
459
|
-
.transform_keys(&:to_sym)
|
460
|
-
|
461
|
-
if (v = acc[name][:level])
|
462
|
-
unless v.is_a?(String) || v.is_a?(Symbol)
|
463
|
-
raise Bolt::ValidationError,
|
464
|
-
"level of log #{name} must be a String or Symbol, received #{v.class} #{v.inspect}"
|
465
|
-
end
|
466
|
-
|
467
|
-
unless Bolt::Logger.valid_level?(v)
|
468
|
-
raise Bolt::ValidationError,
|
469
|
-
"level of log #{name} must be one of #{Bolt::Logger.levels.join(', ')}; received #{v}"
|
470
|
-
end
|
471
|
-
|
472
|
-
if v == 'notice'
|
473
|
-
@deprecations << {
|
474
|
-
type: 'notice log level',
|
475
|
-
msg: "Log level 'notice' is deprecated and will be removed in Bolt 3.0. Use 'info' instead."
|
476
|
-
}
|
477
|
-
end
|
478
|
-
end
|
479
|
-
|
480
|
-
if (v = acc[name][:append]) && v != true && v != false
|
481
|
-
raise Bolt::ValidationError,
|
482
|
-
"append flag of log #{name} must be a Boolean, received #{v.class} #{v.inspect}"
|
483
|
-
end
|
326
|
+
acc[name] = val.slice('append', 'level').transform_keys(&:to_sym)
|
484
327
|
end
|
485
328
|
end
|
486
329
|
|
487
330
|
def validate
|
488
331
|
if @data['future']
|
489
|
-
|
490
|
-
|
332
|
+
Bolt::Logger.warn(
|
333
|
+
"future_option",
|
334
|
+
"Configuration option 'future' no longer exposes future behavior."
|
335
|
+
)
|
491
336
|
end
|
492
337
|
|
493
|
-
if @
|
338
|
+
if @data['modulepath']&.include?(@project.managed_moduledir.to_s)
|
494
339
|
raise Bolt::ValidationError,
|
495
340
|
"Found invalid path in modulepath: #{@project.managed_moduledir}. This path "\
|
496
341
|
"is automatically appended to the modulepath and cannot be configured."
|
@@ -508,29 +353,6 @@ module Bolt
|
|
508
353
|
if File.exist?(default_inventoryfile)
|
509
354
|
Bolt::Util.validate_file('inventory file', default_inventoryfile)
|
510
355
|
end
|
511
|
-
|
512
|
-
unless TRANSPORT_CONFIG.include?(transport)
|
513
|
-
raise UnknownTransportError, transport
|
514
|
-
end
|
515
|
-
|
516
|
-
# Warn the user how they should be using the 'puppetfile' or
|
517
|
-
# 'module-install' config options. We don't error here since these
|
518
|
-
# settings can be set at the user or system level.
|
519
|
-
if @project.modules && puppetfile_config.any? && module_install.empty?
|
520
|
-
command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
|
521
|
-
@logs << { warn: "Detected configuration for 'puppetfile'. This setting is not "\
|
522
|
-
"used when 'modules' is configured. Use 'module-install' instead. "\
|
523
|
-
"To automatically update your project configuration, run '#{command}'." }
|
524
|
-
elsif @project.modules.nil? && puppetfile_config.empty? && module_install.any?
|
525
|
-
@logs << { warn: "Detected configuration for 'module-install'. This setting is not "\
|
526
|
-
"used when 'modules' is not configured. Use 'puppetfile' instead." }
|
527
|
-
elsif @project.modules && puppetfile_config.any? && module_install.any?
|
528
|
-
@logs << { warn: "Detected configuration for 'puppetfile' and 'module-install'. Using "\
|
529
|
-
"configuration for 'module-install' because 'modules' is also configured." }
|
530
|
-
elsif @project.modules.nil? && puppetfile_config.any? && module_install.any?
|
531
|
-
@logs << { warn: "Detected configuration for 'puppetfile' and 'module-install'. Using "\
|
532
|
-
"configuration for 'puppetfile' because 'modules' is not configured." }
|
533
|
-
end
|
534
356
|
end
|
535
357
|
|
536
358
|
def default_inventoryfile
|
@@ -546,21 +368,15 @@ module Bolt
|
|
546
368
|
end
|
547
369
|
|
548
370
|
def puppetfile
|
549
|
-
@
|
371
|
+
@project.puppetfile
|
550
372
|
end
|
551
373
|
|
552
374
|
def modulepath
|
553
|
-
|
554
|
-
|
555
|
-
if @project.modules
|
556
|
-
path + [@project.managed_moduledir.to_s]
|
557
|
-
else
|
558
|
-
path
|
559
|
-
end
|
375
|
+
(@data['modulepath'] || @project.modulepath) + [@project.managed_moduledir.to_s]
|
560
376
|
end
|
561
377
|
|
562
378
|
def modulepath=(value)
|
563
|
-
@data['modulepath'] = value
|
379
|
+
@data['modulepath'] = Array(value)
|
564
380
|
end
|
565
381
|
|
566
382
|
def plugin_cache
|
@@ -611,27 +427,12 @@ module Bolt
|
|
611
427
|
@data['compile-concurrency']
|
612
428
|
end
|
613
429
|
|
614
|
-
def puppetfile_config
|
615
|
-
@data['puppetfile']
|
616
|
-
end
|
617
|
-
|
618
430
|
def plugins
|
619
431
|
@data['plugins']
|
620
432
|
end
|
621
433
|
|
622
434
|
def plugin_hooks
|
623
|
-
|
624
|
-
Bolt::Logger.warn_once(
|
625
|
-
"plugin-hooks and plugin_hooks set",
|
626
|
-
"Detected configuration for 'plugin-hooks' and 'plugin_hooks'. Bolt will ignore 'plugin_hooks'."
|
627
|
-
)
|
628
|
-
|
629
|
-
@data['plugin-hooks']
|
630
|
-
elsif @data['plugin-hooks'].any?
|
631
|
-
@data['plugin-hooks']
|
632
|
-
else
|
633
|
-
@data['plugin_hooks']
|
634
|
-
end
|
435
|
+
@data['plugin-hooks']
|
635
436
|
end
|
636
437
|
|
637
438
|
def trusted_external
|
@@ -639,18 +440,7 @@ module Bolt
|
|
639
440
|
end
|
640
441
|
|
641
442
|
def apply_settings
|
642
|
-
|
643
|
-
Bolt::Logger.warn_once(
|
644
|
-
"apply-settings and apply_settings set",
|
645
|
-
"Detected configuration for 'apply-settings' and 'apply_settings'. Bolt will ignore 'apply_settings'."
|
646
|
-
)
|
647
|
-
|
648
|
-
@data['apply-settings']
|
649
|
-
elsif @data['apply-settings'].any?
|
650
|
-
@data['apply-settings']
|
651
|
-
else
|
652
|
-
@data['apply_settings']
|
653
|
-
end
|
443
|
+
@data['apply-settings']
|
654
444
|
end
|
655
445
|
|
656
446
|
def transport
|
@@ -661,6 +451,10 @@ module Bolt
|
|
661
451
|
@project.module_install || @data['module-install']
|
662
452
|
end
|
663
453
|
|
454
|
+
def disable_warnings
|
455
|
+
Set.new(@project.disable_warnings + @data['disable-warnings'])
|
456
|
+
end
|
457
|
+
|
664
458
|
# Check if there is a case-insensitive match to the path
|
665
459
|
def check_path_case(type, paths)
|
666
460
|
return if paths.nil?
|
@@ -669,7 +463,7 @@ module Bolt
|
|
669
463
|
if matches.any?
|
670
464
|
msg = "WARNING: Bolt is case sensitive when specifying a #{type}. Did you mean:\n"
|
671
465
|
matches.each { |path| msg += " #{path}\n" }
|
672
|
-
|
466
|
+
Bolt::Logger.warn("path_case", msg)
|
673
467
|
end
|
674
468
|
end
|
675
469
|
|