vagrant-lxc-2.1-patch 1.4.0
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.
- checksums.yaml +7 -0
- data/.gitignore +31 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/.vimrc +1 -0
- data/BOXES.md +47 -0
- data/CHANGELOG.md +510 -0
- data/CONTRIBUTING.md +24 -0
- data/Gemfile +24 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +187 -0
- data/Rakefile +3 -0
- data/lib/vagrant-lxc.rb +10 -0
- data/lib/vagrant-lxc/action.rb +234 -0
- data/lib/vagrant-lxc/action/boot.rb +42 -0
- data/lib/vagrant-lxc/action/clear_forwarded_ports.rb +56 -0
- data/lib/vagrant-lxc/action/compress_rootfs.rb +30 -0
- data/lib/vagrant-lxc/action/create.rb +57 -0
- data/lib/vagrant-lxc/action/destroy.rb +18 -0
- data/lib/vagrant-lxc/action/destroy_confirm.rb +17 -0
- data/lib/vagrant-lxc/action/fetch_ip_with_lxc_info.rb +43 -0
- data/lib/vagrant-lxc/action/forced_halt.rb +20 -0
- data/lib/vagrant-lxc/action/forward_ports.rb +121 -0
- data/lib/vagrant-lxc/action/gc_private_network_bridges.rb +47 -0
- data/lib/vagrant-lxc/action/handle_box_metadata.rb +94 -0
- data/lib/vagrant-lxc/action/prepare_nfs_settings.rb +64 -0
- data/lib/vagrant-lxc/action/prepare_nfs_valid_ids.rb +19 -0
- data/lib/vagrant-lxc/action/private_networks.rb +46 -0
- data/lib/vagrant-lxc/action/setup_package_files.rb +60 -0
- data/lib/vagrant-lxc/action/warn_networks.rb +25 -0
- data/lib/vagrant-lxc/command/root.rb +58 -0
- data/lib/vagrant-lxc/command/sudoers.rb +97 -0
- data/lib/vagrant-lxc/config.rb +73 -0
- data/lib/vagrant-lxc/driver.rb +288 -0
- data/lib/vagrant-lxc/driver/cli.rb +166 -0
- data/lib/vagrant-lxc/errors.rb +62 -0
- data/lib/vagrant-lxc/plugin.rb +51 -0
- data/lib/vagrant-lxc/provider.rb +101 -0
- data/lib/vagrant-lxc/provider/cap/public_address.rb +17 -0
- data/lib/vagrant-lxc/sudo_wrapper.rb +104 -0
- data/lib/vagrant-lxc/synced_folder.rb +72 -0
- data/lib/vagrant-lxc/version.rb +5 -0
- data/locales/en.yml +82 -0
- data/scripts/lxc-template +171 -0
- data/scripts/pipework +422 -0
- data/spec/Vagrantfile +26 -0
- data/spec/fixtures/sample-ip-addr-output +2 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/.gitkeep +0 -0
- data/spec/unit/action/clear_forwarded_ports_spec.rb +43 -0
- data/spec/unit/action/compress_rootfs_spec.rb +29 -0
- data/spec/unit/action/forward_ports_spec.rb +117 -0
- data/spec/unit/action/handle_box_metadata_spec.rb +126 -0
- data/spec/unit/action/setup_package_files_spec.rb +83 -0
- data/spec/unit/driver/cli_spec.rb +263 -0
- data/spec/unit/driver_spec.rb +268 -0
- data/spec/unit/support/unit_example_group.rb +38 -0
- data/spec/unit_helper.rb +17 -0
- data/tasks/spec.rake +40 -0
- data/templates/sudoers.rb.erb +129 -0
- data/vagrant-lxc.gemspec +20 -0
- data/vagrant-spec.config.rb +24 -0
- metadata +119 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
module Vagrant
|
2
|
+
module LXC
|
3
|
+
module Action
|
4
|
+
# Prepare arguments to be used for lxc-create
|
5
|
+
class HandleBoxMetadata
|
6
|
+
SUPPORTED_VERSIONS = ['1.0.0', '2', '3']
|
7
|
+
|
8
|
+
def initialize(app, env)
|
9
|
+
@app = app
|
10
|
+
@logger = Log4r::Logger.new("vagrant::lxc::action::handle_box_metadata")
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@env = env
|
15
|
+
@box = @env[:machine].box
|
16
|
+
|
17
|
+
@env[:ui].info I18n.t("vagrant.actions.vm.import.importing",
|
18
|
+
:name => @env[:machine].box.name)
|
19
|
+
|
20
|
+
@logger.info 'Validating box contents'
|
21
|
+
validate_box
|
22
|
+
|
23
|
+
@logger.info 'Setting box options on environment'
|
24
|
+
@env[:lxc_template_src] = template_src
|
25
|
+
@env[:lxc_template_opts] = template_opts
|
26
|
+
|
27
|
+
# FIXME: Remove support for pre 1.0.0 boxes
|
28
|
+
if box_version != '1.0.0'
|
29
|
+
@env[:ui].warn "WARNING: You are using a base box that has a format that has been deprecated, please upgrade to a new one."
|
30
|
+
@env[:lxc_template_opts].merge!(
|
31
|
+
'--auth-key' => Vagrant.source_root.join('keys', 'vagrant.pub').expand_path.to_s
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
if template_config_file.exist?
|
36
|
+
@env[:lxc_template_opts].merge!('--config' => template_config_file.to_s)
|
37
|
+
elsif old_template_config_file.exist?
|
38
|
+
@env[:lxc_template_config] = old_template_config_file.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
@app.call env
|
42
|
+
end
|
43
|
+
|
44
|
+
def template_src
|
45
|
+
@template_src ||=
|
46
|
+
if (box_template = @box.directory.join('lxc-template')).exist?
|
47
|
+
box_template.to_s
|
48
|
+
else
|
49
|
+
Vagrant::LXC.source_root.join('scripts/lxc-template').to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def template_config_file
|
54
|
+
@template_config_file ||= @box.directory.join('lxc-config')
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO: Remove this once we remove compatibility for < 1.0.0 boxes
|
58
|
+
def old_template_config_file
|
59
|
+
@old_template_config_file ||= @box.directory.join('lxc.conf')
|
60
|
+
end
|
61
|
+
|
62
|
+
def template_opts
|
63
|
+
@template_opts ||= @box.metadata.fetch('template-opts', {}).dup.merge!(
|
64
|
+
'--tarball' => rootfs_tarball
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def rootfs_tarball
|
69
|
+
@rootfs_tarball ||= @box.directory.join('rootfs.tar.gz').to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_box
|
73
|
+
unless SUPPORTED_VERSIONS.include? box_version
|
74
|
+
raise Errors::IncompatibleBox.new name: @box.name,
|
75
|
+
found: box_version,
|
76
|
+
supported: SUPPORTED_VERSIONS.join(', ')
|
77
|
+
end
|
78
|
+
|
79
|
+
unless File.exists?(template_src)
|
80
|
+
raise Errors::TemplateFileMissing.new name: @box.name
|
81
|
+
end
|
82
|
+
|
83
|
+
unless File.exists?(rootfs_tarball)
|
84
|
+
raise Errors::RootFSTarballMissing.new name: @box.name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def box_version
|
89
|
+
@box.metadata.fetch('version')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Vagrant
|
2
|
+
module LXC
|
3
|
+
module Action
|
4
|
+
class PrepareNFSSettings
|
5
|
+
include Vagrant::Util::Retryable
|
6
|
+
|
7
|
+
def initialize(app, env)
|
8
|
+
@app = app
|
9
|
+
@logger = Log4r::Logger.new("vagrant::action::vm::nfs")
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
@machine = env[:machine]
|
14
|
+
|
15
|
+
@app.call(env)
|
16
|
+
|
17
|
+
# if using_nfs? # TODO: && !privileged_container?
|
18
|
+
# raise Errors::NfsWithoutPrivilegedError
|
19
|
+
# end
|
20
|
+
|
21
|
+
if using_nfs?
|
22
|
+
@logger.info("Using NFS, preparing NFS settings by reading host IP and machine IP")
|
23
|
+
add_ips_to_env!(env)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# We're using NFS if we have any synced folder with NFS configured. If
|
28
|
+
# we are not using NFS we don't need to do the extra work to
|
29
|
+
# populate these fields in the environment.
|
30
|
+
def using_nfs?
|
31
|
+
@machine.config.vm.synced_folders.any? { |_, opts| opts[:type] == :nfs }
|
32
|
+
end
|
33
|
+
|
34
|
+
# TODO:
|
35
|
+
# def privileged_container?
|
36
|
+
# @machine.provider.driver.privileged?(@machine.id)
|
37
|
+
# end
|
38
|
+
|
39
|
+
# Extracts the proper host and guest IPs for NFS mounts and stores them
|
40
|
+
# in the environment for the SyncedFolder action to use them in
|
41
|
+
# mounting.
|
42
|
+
#
|
43
|
+
# The ! indicates that this method modifies its argument.
|
44
|
+
def add_ips_to_env!(env)
|
45
|
+
provider = @machine.provider
|
46
|
+
|
47
|
+
host_ip = read_host_ip
|
48
|
+
machine_ip = provider.ssh_info[:host]
|
49
|
+
|
50
|
+
raise Vagrant::Errors::NFSNoHostonlyNetwork if !host_ip || !machine_ip
|
51
|
+
|
52
|
+
env[:nfs_host_ip] = host_ip
|
53
|
+
env[:nfs_machine_ip] = machine_ip
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_host_ip
|
57
|
+
@machine.communicate.execute 'echo $SSH_CLIENT' do |buffer, output|
|
58
|
+
return output.chomp.split(' ')[0] if buffer == :stdout
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Vagrant
|
2
|
+
module LXC
|
3
|
+
module Action
|
4
|
+
class PrepareNFSValidIds
|
5
|
+
def initialize(app, env)
|
6
|
+
@app = app
|
7
|
+
@logger = Log4r::Logger.new("vagrant::action::vm::nfs")
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
machine = env[:machine]
|
12
|
+
env[:nfs_valid_ids] = machine.provider.driver.all_containers
|
13
|
+
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Vagrant
|
2
|
+
module LXC
|
3
|
+
module Action
|
4
|
+
class PrivateNetworks
|
5
|
+
def initialize(app, env)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
@app.call(env)
|
11
|
+
|
12
|
+
if private_network_configured?(env[:machine].config)
|
13
|
+
env[:ui].output(I18n.t("vagrant_lxc.messages.setup_private_network"))
|
14
|
+
configure_private_networks(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def private_network_configured?(config)
|
19
|
+
config.vm.networks.find do |type, _|
|
20
|
+
type.to_sym == :private_network
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def configure_private_networks(env)
|
25
|
+
env[:machine].config.vm.networks.find do |type, config|
|
26
|
+
next if type.to_sym != :private_network
|
27
|
+
|
28
|
+
container_name = env[:machine].provider.driver.container_name
|
29
|
+
address_type = config[:type]
|
30
|
+
ip = config[:ip]
|
31
|
+
bridge_ip = config.fetch(:lxc__bridge_ip) { build_bridge_ip(ip) }
|
32
|
+
bridge = config.fetch(:lxc__bridge_name)
|
33
|
+
|
34
|
+
env[:machine].provider.driver.configure_private_network(bridge, bridge_ip, container_name, address_type, ip)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_bridge_ip(ip)
|
39
|
+
if ip
|
40
|
+
ip.sub(/^(\d+\.\d+\.\d+)\.\d+/, '\1.254')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Vagrant
|
4
|
+
module LXC
|
5
|
+
module Action
|
6
|
+
class SetupPackageFiles
|
7
|
+
def initialize(app, env)
|
8
|
+
@app = app
|
9
|
+
|
10
|
+
env["package.include"] ||= []
|
11
|
+
env["package.vagrantfile"] ||= nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
@env = env
|
16
|
+
|
17
|
+
create_package_temp_dir
|
18
|
+
move_rootfs_to_pkg_dir
|
19
|
+
copy_box_files_to_pkg_dir
|
20
|
+
|
21
|
+
@app.call env
|
22
|
+
|
23
|
+
recover # called to cleanup temp directory
|
24
|
+
end
|
25
|
+
|
26
|
+
def recover(*)
|
27
|
+
if @temp_dir && File.exist?(@temp_dir)
|
28
|
+
FileUtils.rm_rf(@temp_dir)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def create_package_temp_dir
|
35
|
+
@env[:ui].info I18n.t("vagrant.actions.vm.export.create_dir")
|
36
|
+
@temp_dir = @env["package.directory"] = @env[:tmp_path].join("container-export-#{Time.now.to_i.to_s}")
|
37
|
+
FileUtils.mkpath(@temp_dir)
|
38
|
+
end
|
39
|
+
|
40
|
+
def move_rootfs_to_pkg_dir
|
41
|
+
FileUtils.mv @env['package.rootfs'].to_s, @env['package.directory'].to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def copy_box_files_to_pkg_dir
|
45
|
+
box_dir = @env[:machine].box.directory
|
46
|
+
FileUtils.cp box_dir.join('metadata.json').to_s, @env['package.directory'].to_s
|
47
|
+
if (template = box_dir.join('lxc-template')).exist?
|
48
|
+
FileUtils.cp template.to_s, @env['package.directory'].to_s
|
49
|
+
end
|
50
|
+
if (conf = box_dir.join('lxc.conf')).exist?
|
51
|
+
FileUtils.cp conf.to_s, @env['package.directory'].to_s
|
52
|
+
end
|
53
|
+
if (conf = box_dir.join('lxc-config')).exist?
|
54
|
+
FileUtils.cp conf.to_s, @env['package.directory'].to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Vagrant
|
2
|
+
module LXC
|
3
|
+
module Action
|
4
|
+
class WarnNetworks
|
5
|
+
def initialize(app, env)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
if public_network_configured?(env[:machine].config)
|
11
|
+
env[:ui].warn(I18n.t("vagrant_lxc.messages.warn_networks"))
|
12
|
+
end
|
13
|
+
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
def public_network_configured?(config)
|
18
|
+
config.vm.networks.find do |type, _|
|
19
|
+
type.to_sym == :public_network
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Vagrant
|
2
|
+
module LXC
|
3
|
+
module Command
|
4
|
+
class Root < Vagrant.plugin("2", :command)
|
5
|
+
def self.synopsis
|
6
|
+
'vagrant-lxc specific commands'
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(argv, env)
|
10
|
+
@args, @sub_command, @sub_args = split_main_and_subcommand(argv)
|
11
|
+
@subcommands = Vagrant::Registry.new.tap do |registry|
|
12
|
+
registry.register(:sudoers) do
|
13
|
+
require_relative 'sudoers'
|
14
|
+
Sudoers
|
15
|
+
end
|
16
|
+
end
|
17
|
+
super(argv, env)
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute
|
21
|
+
# Print the help
|
22
|
+
return help if @args.include?("-h") || @args.include?("--help")
|
23
|
+
|
24
|
+
klazz = @subcommands.get(@sub_command.to_sym) if @sub_command
|
25
|
+
return help unless klazz
|
26
|
+
|
27
|
+
@logger.debug("Executing command: #{klazz} #{@sub_args.inspect}")
|
28
|
+
|
29
|
+
# Initialize and execute the command class
|
30
|
+
klazz.new(@sub_args, @env).execute
|
31
|
+
end
|
32
|
+
|
33
|
+
def help
|
34
|
+
opts = OptionParser.new do |opts|
|
35
|
+
opts.banner = "Usage: vagrant lxc <subcommand> [<args>]"
|
36
|
+
opts.separator ""
|
37
|
+
opts.separator "Available subcommands:"
|
38
|
+
|
39
|
+
# REFACTOR Use @subcommands.keys.sort
|
40
|
+
# https://github.com/mitchellh/vagrant/commit/4194da19c60956f6e59239c0145f772be257e79d
|
41
|
+
keys = []
|
42
|
+
@subcommands.each { |key, value| keys << key }
|
43
|
+
|
44
|
+
keys.sort.each do |key|
|
45
|
+
opts.separator " #{key}"
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.separator ""
|
49
|
+
opts.separator "For help on any individual subcommand run `vagrant lxc <subcommand> -h`"
|
50
|
+
end
|
51
|
+
|
52
|
+
@env.ui.info(opts.help, :prefix => false)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
require "vagrant-lxc/driver"
|
4
|
+
require "vagrant-lxc/sudo_wrapper"
|
5
|
+
|
6
|
+
module Vagrant
|
7
|
+
module LXC
|
8
|
+
module Command
|
9
|
+
class Sudoers < Vagrant.plugin("2", :command)
|
10
|
+
|
11
|
+
def initialize(argv, env)
|
12
|
+
super
|
13
|
+
@argv
|
14
|
+
@env = env
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute
|
18
|
+
options = { user: ENV['USER'] }
|
19
|
+
|
20
|
+
opts = OptionParser.new do |opts|
|
21
|
+
opts.banner = "Usage: vagrant lxc sudoers"
|
22
|
+
opts.separator ""
|
23
|
+
opts.on('-u user', '--user user', String, "The user for which to create the policy (defaults to '#{options[:user]}')") do |u|
|
24
|
+
options[:user] = u
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
argv = parse_options(opts)
|
29
|
+
return unless argv
|
30
|
+
|
31
|
+
wrapper_path = SudoWrapper.dest_path
|
32
|
+
wrapper = create_wrapper!
|
33
|
+
sudoers = create_sudoers!(options[:user], wrapper_path)
|
34
|
+
|
35
|
+
su_copy([
|
36
|
+
{source: wrapper, target: wrapper_path, mode: "0555"},
|
37
|
+
{source: sudoers, target: sudoers_path, mode: "0440"}
|
38
|
+
])
|
39
|
+
end
|
40
|
+
|
41
|
+
def sudoers_path
|
42
|
+
"/etc/sudoers.d/vagrant-lxc"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# This requires vagrant 1.5.2+ https://github.com/mitchellh/vagrant/commit/3371c3716278071680af9b526ba19235c79c64cb
|
48
|
+
def create_wrapper!
|
49
|
+
lxc_base_path = Driver.new("").containers_path
|
50
|
+
wrapper = Tempfile.new('lxc-wrapper').tap do |file|
|
51
|
+
template = Vagrant::Util::TemplateRenderer.new(
|
52
|
+
'sudoers.rb',
|
53
|
+
:template_root => Vagrant::LXC.source_root.join('templates').to_s,
|
54
|
+
:cmd_paths => build_cmd_paths_hash,
|
55
|
+
:lxc_base_path => lxc_base_path,
|
56
|
+
:pipework_regex => "#{ENV['HOME']}/\.vagrant\.d/gems/(?:\\d+?\\.\\d+?\\.\\d+?/)?gems/vagrant-lxc.+/scripts/pipework"
|
57
|
+
)
|
58
|
+
file.puts template.render
|
59
|
+
end
|
60
|
+
wrapper.close
|
61
|
+
wrapper.path
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_sudoers!(user, command)
|
65
|
+
sudoers = Tempfile.new('vagrant-lxc-sudoers').tap do |file|
|
66
|
+
file.puts "# Automatically created by vagrant-lxc"
|
67
|
+
file.puts "#{user} ALL=(root) NOPASSWD: #{command}"
|
68
|
+
end
|
69
|
+
sudoers.close
|
70
|
+
sudoers.path
|
71
|
+
end
|
72
|
+
|
73
|
+
def su_copy(files)
|
74
|
+
commands = files.map { |file|
|
75
|
+
[
|
76
|
+
"rm -f #{file[:target]}",
|
77
|
+
"cp #{file[:source]} #{file[:target]}",
|
78
|
+
"chown root:root #{file[:target]}",
|
79
|
+
"chmod #{file[:mode]} #{file[:target]}"
|
80
|
+
]
|
81
|
+
}.flatten
|
82
|
+
system "echo \"#{commands.join("; ")}\" | sudo sh"
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_cmd_paths_hash
|
86
|
+
{}.tap do |hash|
|
87
|
+
%w( which cat mkdir cp chown chmod rm tar chown ip ifconfig brctl ).each do |cmd|
|
88
|
+
hash[cmd] = `sudo which #{cmd}`.strip
|
89
|
+
end
|
90
|
+
hash['lxc_bin'] = Pathname(`sudo which lxc-create`.strip).parent.to_s
|
91
|
+
hash['ruby'] = Gem.ruby
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|