bolt 2.25.0 → 2.30.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 +4 -3
- 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 +116 -26
- data/lib/bolt/catalog.rb +5 -3
- data/lib/bolt/cli.rb +194 -185
- data/lib/bolt/config.rb +61 -26
- data/lib/bolt/config/options.rb +35 -2
- data/lib/bolt/executor.rb +2 -2
- 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/module_installer.rb +172 -0
- data/lib/bolt/outputter.rb +4 -0
- data/lib/bolt/outputter/human.rb +53 -11
- data/lib/bolt/outputter/json.rb +7 -1
- data/lib/bolt/outputter/logger.rb +3 -3
- data/lib/bolt/pal.rb +29 -20
- data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
- data/lib/bolt/plugin/module.rb +1 -1
- data/lib/bolt/plugin/puppetdb.rb +1 -1
- data/lib/bolt/project.rb +89 -28
- data/lib/bolt/project_migrator.rb +80 -0
- data/lib/bolt/project_migrator/base.rb +39 -0
- data/lib/bolt/project_migrator/config.rb +67 -0
- data/lib/bolt/project_migrator/inventory.rb +67 -0
- data/lib/bolt/project_migrator/modules.rb +198 -0
- data/lib/bolt/puppetdb/client.rb +1 -1
- data/lib/bolt/puppetdb/config.rb +1 -1
- data/lib/bolt/puppetfile.rb +142 -0
- data/lib/bolt/puppetfile/installer.rb +43 -0
- data/lib/bolt/puppetfile/module.rb +90 -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/shell/bash.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 +52 -11
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/acl.rb +2 -2
- data/lib/bolt_server/base_config.rb +3 -3
- 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/plans/mock_executor.rb +1 -1
- metadata +15 -13
- data/lib/bolt/project_migrate.rb +0 -138
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/project_migrator/config'
|
4
|
+
require 'bolt/project_migrator/inventory'
|
5
|
+
require 'bolt/project_migrator/modules'
|
6
|
+
|
7
|
+
module Bolt
|
8
|
+
class ProjectMigrator
|
9
|
+
def initialize(config, outputter)
|
10
|
+
@config = config
|
11
|
+
@outputter = outputter
|
12
|
+
end
|
13
|
+
|
14
|
+
def migrate
|
15
|
+
unless $stdin.tty?
|
16
|
+
raise Bolt::Error.new(
|
17
|
+
"stdin is not a tty, unable to migrate project",
|
18
|
+
'bolt/stdin-not-a-tty-error'
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
@outputter.print_message("Migrating project #{@config.project.path}\n\n")
|
23
|
+
|
24
|
+
@outputter.print_migrate_step(
|
25
|
+
"Migrating a Bolt project may make irreversible changes to the project's "\
|
26
|
+
"configuration and inventory files. Before continuing, make sure the "\
|
27
|
+
"project has a backup or uses a version control system."
|
28
|
+
)
|
29
|
+
|
30
|
+
return 0 unless Bolt::Util.prompt_yes_no("Continue with project migration?", @outputter)
|
31
|
+
|
32
|
+
@outputter.print_message('')
|
33
|
+
|
34
|
+
ok = migrate_inventory && migrate_config && migrate_modules
|
35
|
+
|
36
|
+
if ok
|
37
|
+
@outputter.print_message("Project successfully migrated")
|
38
|
+
else
|
39
|
+
@outputter.print_error("Project could not be migrated completely")
|
40
|
+
end
|
41
|
+
|
42
|
+
ok ? 0 : 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# Migrates the project-level configuration file to the latest version.
|
46
|
+
#
|
47
|
+
private def migrate_config
|
48
|
+
migrator = Bolt::ProjectMigrator::Config.new(@outputter)
|
49
|
+
|
50
|
+
migrator.migrate(
|
51
|
+
@config.project.config_file,
|
52
|
+
@config.project.project_file,
|
53
|
+
@config.inventoryfile || @config.project.inventory_file,
|
54
|
+
@config.project.backup_dir
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Migrates the inventory file to the latest version.
|
59
|
+
#
|
60
|
+
private def migrate_inventory
|
61
|
+
migrator = Bolt::ProjectMigrator::Inventory.new(@outputter)
|
62
|
+
|
63
|
+
migrator.migrate(
|
64
|
+
@config.inventoryfile || @config.project.inventory_file,
|
65
|
+
@config.project.backup_dir
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Migrates the project's modules to use current best practices.
|
70
|
+
#
|
71
|
+
private def migrate_modules
|
72
|
+
migrator = Bolt::ProjectMigrator::Modules.new(@outputter)
|
73
|
+
|
74
|
+
migrator.migrate(
|
75
|
+
@config.project,
|
76
|
+
@config.modulepath
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'bolt/error'
|
5
|
+
|
6
|
+
module Bolt
|
7
|
+
class ProjectMigrator
|
8
|
+
class Base
|
9
|
+
def initialize(outputter)
|
10
|
+
@outputter = outputter
|
11
|
+
end
|
12
|
+
|
13
|
+
protected def backup_file(origin_path, backup_dir)
|
14
|
+
unless File.exist?(origin_path)
|
15
|
+
@outputter.print_migrate_step(
|
16
|
+
"Could not find file #{origin_path}, skipping backup."
|
17
|
+
)
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
date = Time.new.strftime("%Y%m%d_%H%M%S%L")
|
22
|
+
FileUtils.mkdir_p(backup_dir)
|
23
|
+
|
24
|
+
filename = File.basename(origin_path)
|
25
|
+
backup_path = File.join(backup_dir, "#{filename}.#{date}.bak")
|
26
|
+
|
27
|
+
@outputter.print_migrate_step(
|
28
|
+
"Backing up #{filename} from #{origin_path} to #{backup_path}"
|
29
|
+
)
|
30
|
+
|
31
|
+
begin
|
32
|
+
FileUtils.cp(origin_path, backup_path)
|
33
|
+
rescue StandardError => e
|
34
|
+
raise Bolt::FileError.new("#{e.message}; unable to create backup of #{filename}.", origin_path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/project_migrator/base'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class ProjectMigrator
|
7
|
+
class Config < Base
|
8
|
+
def migrate(config_file, project_file, inventory_file, backup_dir)
|
9
|
+
bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
private def bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir)
|
13
|
+
if File.exist?(project_file)
|
14
|
+
return true
|
15
|
+
end
|
16
|
+
|
17
|
+
unless File.exist?(config_file)
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
|
21
|
+
@outputter.print_message "Migrating project configuration\n\n"
|
22
|
+
|
23
|
+
config_data = Bolt::Util.read_optional_yaml_hash(config_file, 'config')
|
24
|
+
transport_data, project_data = config_data.partition do |k, _|
|
25
|
+
Bolt::Config::INVENTORY_OPTIONS.keys.include?(k)
|
26
|
+
end.map(&:to_h)
|
27
|
+
|
28
|
+
if transport_data.any?
|
29
|
+
if File.exist?(inventory_file)
|
30
|
+
inventory_data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
|
31
|
+
merged = Bolt::Util.deep_merge(transport_data, inventory_data['config'] || {})
|
32
|
+
inventory_data['config'] = merged
|
33
|
+
backup_file(inventory_file, backup_dir)
|
34
|
+
else
|
35
|
+
FileUtils.touch(inventory_file)
|
36
|
+
inventory_data = { 'config' => transport_data }
|
37
|
+
end
|
38
|
+
|
39
|
+
backup_file(config_file, backup_dir)
|
40
|
+
|
41
|
+
begin
|
42
|
+
@outputter.print_migrate_step(
|
43
|
+
"Moving transportation configuration options '#{transport_data.keys.join(', ')}' "\
|
44
|
+
"from bolt.yaml to inventory.yaml"
|
45
|
+
)
|
46
|
+
|
47
|
+
File.write(inventory_file, inventory_data.to_yaml)
|
48
|
+
File.write(config_file, project_data.to_yaml)
|
49
|
+
rescue StandardError => e
|
50
|
+
raise Bolt::FileError.new("#{e.message}; unable to write inventory.", inventory_file)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@outputter.print_migrate_step("Renaming bolt.yaml to bolt-project.yaml")
|
55
|
+
FileUtils.mv(config_file, project_file)
|
56
|
+
|
57
|
+
@outputter.print_migrate_step(
|
58
|
+
"Successfully migrated config. Please add a 'name' key to bolt-project.yaml "\
|
59
|
+
"to use project-level tasks and plans. Learn more about projects by running "\
|
60
|
+
"'bolt guide project'."
|
61
|
+
)
|
62
|
+
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/project_migrator/base'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class ProjectMigrator
|
7
|
+
class Inventory < Base
|
8
|
+
def migrate(inventory_file, backup_dir)
|
9
|
+
inventory_1_to_2(inventory_file, backup_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Migrates an inventory v1 file to inventory v2.
|
13
|
+
#
|
14
|
+
private def inventory_1_to_2(inventory_file, backup_dir)
|
15
|
+
unless File.exist?(inventory_file)
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
|
19
|
+
data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
|
20
|
+
data.delete('version') if data['version'] != 2
|
21
|
+
migrated = migrate_group(data)
|
22
|
+
|
23
|
+
return true unless migrated
|
24
|
+
|
25
|
+
@outputter.print_message "Migrating inventory\n\n"
|
26
|
+
|
27
|
+
backup_file(inventory_file, backup_dir)
|
28
|
+
|
29
|
+
begin
|
30
|
+
File.write(inventory_file, data.to_yaml)
|
31
|
+
@outputter.print_migrate_step(
|
32
|
+
"Successfully migrated Bolt inventory to the latest version."
|
33
|
+
)
|
34
|
+
true
|
35
|
+
rescue StandardError => e
|
36
|
+
raise Bolt::FileError.new(
|
37
|
+
"Unable to write to #{inventory_file}: #{e.message}. See "\
|
38
|
+
"http://pup.pt/bolt-inventory to manually update.",
|
39
|
+
inventory_file
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Walks an inventory hash and replaces all 'nodes' keys with 'targets'
|
45
|
+
# keys and all 'name' keys nested in a 'targets' hash with 'uri' keys.
|
46
|
+
# Data is modified in place.
|
47
|
+
#
|
48
|
+
private def migrate_group(group)
|
49
|
+
migrated = false
|
50
|
+
if group.key?('nodes')
|
51
|
+
migrated = true
|
52
|
+
targets = group['nodes'].map do |target|
|
53
|
+
target['uri'] = target.delete('name') if target.is_a?(Hash)
|
54
|
+
target
|
55
|
+
end
|
56
|
+
group.delete('nodes')
|
57
|
+
group['targets'] = targets
|
58
|
+
end
|
59
|
+
(group['groups'] || []).each do |subgroup|
|
60
|
+
migrated_group = migrate_group(subgroup)
|
61
|
+
migrated ||= migrated_group
|
62
|
+
end
|
63
|
+
migrated
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/project_migrator/base'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class ProjectMigrator
|
7
|
+
class Modules < Base
|
8
|
+
def migrate(project, configured_modulepath)
|
9
|
+
return true unless project.modules.nil?
|
10
|
+
|
11
|
+
@outputter.print_message "Migrating project modules\n\n"
|
12
|
+
|
13
|
+
config = project.project_file
|
14
|
+
puppetfile = project.puppetfile
|
15
|
+
managed_moduledir = project.managed_moduledir
|
16
|
+
modulepath = [(project.path + 'modules').to_s,
|
17
|
+
(project.path + 'site-modules').to_s,
|
18
|
+
(project.path + 'site').to_s]
|
19
|
+
|
20
|
+
# Notify user to manually migrate modules if using non-default modulepath
|
21
|
+
if configured_modulepath != modulepath
|
22
|
+
@outputter.print_migrate_step(
|
23
|
+
"Project has a non-default configured modulepath, unable to automatically "\
|
24
|
+
"migrate project modules. To migrate project modules manually, see "\
|
25
|
+
"http://pup.pt/bolt-modules"
|
26
|
+
)
|
27
|
+
true
|
28
|
+
# Migrate modules from Puppetfile
|
29
|
+
elsif File.exist?(puppetfile)
|
30
|
+
migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, modulepath)
|
31
|
+
# Migrate modules to updated modulepath
|
32
|
+
else
|
33
|
+
consolidate_modules(modulepath)
|
34
|
+
update_project_config([], config)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Migrates modules by reading a Puppetfile and prompting the user for
|
39
|
+
# which ones are direct dependencies for the project. Once the user has
|
40
|
+
# selected the direct dependencies, this will resolve the modules, write a
|
41
|
+
# new Puppetfile, install the modules, and then move any remaining modules
|
42
|
+
# to the new moduledir.
|
43
|
+
#
|
44
|
+
private def migrate_modules_from_puppetfile(config, puppetfile_path, managed_moduledir, modulepath)
|
45
|
+
require 'bolt/puppetfile'
|
46
|
+
require 'bolt/puppetfile/installer'
|
47
|
+
|
48
|
+
begin
|
49
|
+
@outputter.print_migrate_step("Parsing Puppetfile at #{puppetfile_path}")
|
50
|
+
puppetfile = Bolt::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true)
|
51
|
+
rescue Bolt::Error => e
|
52
|
+
@outputter.print_migrate_error("#{e.message}\nSkipping module migration.")
|
53
|
+
return false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Prompt for direct dependencies
|
57
|
+
modules = select_modules(puppetfile.modules)
|
58
|
+
|
59
|
+
# Create new Puppetfile object
|
60
|
+
puppetfile = Bolt::Puppetfile.new(modules)
|
61
|
+
|
62
|
+
# Attempt to resolve dependencies
|
63
|
+
begin
|
64
|
+
@outputter.print_message('')
|
65
|
+
@outputter.print_migrate_step("Resolving module dependencies, this may take a moment")
|
66
|
+
puppetfile.resolve
|
67
|
+
rescue Bolt::Error => e
|
68
|
+
@outputter.print_migrate_error("#{e.message}\nSkipping module migration.")
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
|
73
|
+
|
74
|
+
# Move remaining modules to 'modules'
|
75
|
+
consolidate_modules(modulepath)
|
76
|
+
|
77
|
+
# Delete old modules that are now managed
|
78
|
+
delete_modules(modulepath.first, puppetfile.modules)
|
79
|
+
|
80
|
+
# Add modules to project
|
81
|
+
update_project_config(modules.map(&:to_hash), config)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Migrates the managed modules. If modules were selected to be managed,
|
85
|
+
# the Puppetfile is rewritten and modules are installed. If no modules
|
86
|
+
# were selected, the Puppetfile is deleted.
|
87
|
+
#
|
88
|
+
private def migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
|
89
|
+
if puppetfile.modules.any?
|
90
|
+
# Show the new Puppetfile content
|
91
|
+
message = "Generated new Puppetfile content:\n\n"
|
92
|
+
message += puppetfile.modules.map(&:to_spec).join("\n").to_s
|
93
|
+
@outputter.print_migrate_step(message)
|
94
|
+
|
95
|
+
# Write Puppetfile
|
96
|
+
@outputter.print_migrate_step("Updating Puppetfile at #{puppetfile_path}")
|
97
|
+
puppetfile.write(puppetfile_path, managed_moduledir)
|
98
|
+
|
99
|
+
# Install Puppetfile
|
100
|
+
@outputter.print_migrate_step("Syncing modules from #{puppetfile_path} to #{managed_moduledir}")
|
101
|
+
Bolt::Puppetfile::Installer.new({}).install(puppetfile_path, managed_moduledir)
|
102
|
+
else
|
103
|
+
@outputter.print_migrate_step(
|
104
|
+
"Project does not include any managed modules, deleting Puppetfile "\
|
105
|
+
"at #{puppetfile_path}"
|
106
|
+
)
|
107
|
+
FileUtils.rm(puppetfile_path)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Prompts the user to select modules, returning a list of
|
112
|
+
# the selected modules.
|
113
|
+
#
|
114
|
+
private def select_modules(modules)
|
115
|
+
@outputter.print_migrate_step(
|
116
|
+
"Select modules that are direct dependencies of your project. Bolt will "\
|
117
|
+
"automatically manage dependencies for each module selected, so do not "\
|
118
|
+
"select a module's dependencies unless you use content from it directly "\
|
119
|
+
"in your project."
|
120
|
+
)
|
121
|
+
|
122
|
+
all = Bolt::Util.prompt_yes_no("Select all modules?", @outputter)
|
123
|
+
return modules if all
|
124
|
+
|
125
|
+
modules.select do |mod|
|
126
|
+
Bolt::Util.prompt_yes_no("Select #{mod.title}?", @outputter)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Consolidates all modules on the modulepath to 'modules'.
|
131
|
+
#
|
132
|
+
private def consolidate_modules(modulepath)
|
133
|
+
moduledir, *sources = modulepath
|
134
|
+
|
135
|
+
sources.select! { |source| Dir.exist?(source) }
|
136
|
+
|
137
|
+
if sources.any?
|
138
|
+
@outputter.print_migrate_step(
|
139
|
+
"Moving modules from #{sources.join(', ')} to #{moduledir}"
|
140
|
+
)
|
141
|
+
|
142
|
+
FileUtils.mkdir_p(moduledir)
|
143
|
+
move_modules(moduledir, sources)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Moves modules from a list of source directories to the specified
|
148
|
+
# moduledir, deleting the source directory after it's done.
|
149
|
+
#
|
150
|
+
private def move_modules(moduledir, sources)
|
151
|
+
moduledir = Pathname.new(moduledir)
|
152
|
+
|
153
|
+
sources.each do |source|
|
154
|
+
source = Pathname.new(source)
|
155
|
+
|
156
|
+
source.each_child do |mod|
|
157
|
+
next unless mod.directory?
|
158
|
+
next if (moduledir + mod.basename).directory?
|
159
|
+
FileUtils.mv(mod, moduledir)
|
160
|
+
end
|
161
|
+
|
162
|
+
FileUtils.rm_r(source)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Deletes modules from a specified directory.
|
167
|
+
#
|
168
|
+
private def delete_modules(moduledir, modules)
|
169
|
+
@outputter.print_migrate_step("Cleaning up #{moduledir}")
|
170
|
+
moduledir = Pathname.new(moduledir)
|
171
|
+
|
172
|
+
modules.each do |mod|
|
173
|
+
path = moduledir + mod.name
|
174
|
+
FileUtils.rm_r(path) if path.directory?
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Adds a list of modules to the project configuration file.
|
179
|
+
#
|
180
|
+
private def update_project_config(modules, config_file)
|
181
|
+
@outputter.print_migrate_step("Updating project configuration at #{config_file}")
|
182
|
+
data = Bolt::Util.read_optional_yaml_hash(config_file, 'project')
|
183
|
+
data.merge!('modules' => modules)
|
184
|
+
data.delete('modulepath')
|
185
|
+
|
186
|
+
begin
|
187
|
+
File.write(config_file, data.to_yaml)
|
188
|
+
true
|
189
|
+
rescue StandardError => e
|
190
|
+
raise Bolt::FileError.new(
|
191
|
+
"Unable to write to #{config_file}: #{e.message}",
|
192
|
+
config_file
|
193
|
+
)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|