daptiv-chef-ci 0.0.15 → 0.1.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 +4 -4
- data/daptiv-chef-ci.gemspec +21 -20
- data/lib/daptiv-chef-ci/clone_environment_task.rb +69 -0
- data/lib/daptiv-chef-ci/logger.rb +9 -10
- data/lib/daptiv-chef-ci/raketask_helper.rb +5 -6
- data/lib/daptiv-chef-ci/shell.rb +50 -28
- data/lib/daptiv-chef-ci/upload_cookbook_task.rb +62 -0
- data/lib/daptiv-chef-ci/vagrant_destroy_task.rb +11 -12
- data/lib/daptiv-chef-ci/vagrant_driver.rb +47 -56
- data/lib/daptiv-chef-ci/vagrant_provision_task.rb +12 -15
- data/lib/daptiv-chef-ci/vagrant_task.rb +34 -57
- data/lib/daptiv-chef-ci/vagrant_up_task.rb +14 -16
- data/lib/daptiv-chef-ci/version_cookbook_task.rb +69 -0
- data/lib/daptiv-chef-ci/virtualbox_driver.rb +11 -12
- data/spec/daptiv-chef-ci/logger_spec.rb +11 -15
- data/spec/daptiv-chef-ci/shell_spec.rb +23 -27
- data/spec/daptiv-chef-ci/vagrant_destroy_task_spec.rb +5 -7
- data/spec/daptiv-chef-ci/vagrant_driver_spec.rb +34 -72
- data/spec/daptiv-chef-ci/vagrant_provision_task_spec.rb +9 -13
- data/spec/daptiv-chef-ci/vagrant_task_spec.rb +29 -21
- data/spec/daptiv-chef-ci/vagrant_up_task_spec.rb +9 -12
- data/spec/daptiv-chef-ci/virtualbox_driver_spec.rb +21 -16
- data/spec/shared_contexts/rake.rb +7 -7
- metadata +21 -14
- data/lib/daptiv-chef-ci/basebox_builder_factory.rb +0 -19
- data/lib/daptiv-chef-ci/virtualbox_basebox_builder.rb +0 -33
- data/lib/daptiv-chef-ci/vmware_basebox_builder.rb +0 -103
- data/spec/daptiv-chef-ci/basebox_builder_factory_spec.rb +0 -25
- data/spec/daptiv-chef-ci/virtualbox_basebox_builder_spec.rb +0 -20
- data/spec/daptiv-chef-ci/vmware_basebox_builder_spec.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfeeac6ae1094f6d486f92af9fca0fc1a1915ec9
|
4
|
+
data.tar.gz: 489147267747924a25c9e1f6e350881f01996a35
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5707f54b532a6299e7f446add8ceaef337840207d337190cd8a7c9772e7e87362cbf922ee7f92342506d015459fa9d30f9601def5dee61ba93d1d8450eb5374b
|
7
|
+
data.tar.gz: 41db757953a4c6ce7100eff2217719354d71667763367b4969acb1d87e344f904c56ac0e7bd98759f1358c514df8d146ac37bb8d5e6f82be7ab213089f5b4b0f
|
data/daptiv-chef-ci.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
|
-
gem.authors = [
|
5
|
-
gem.email = [
|
6
|
-
gem.description =
|
7
|
-
gem.summary =
|
8
|
-
gem.homepage =
|
4
|
+
gem.authors = ['Shawn Neal']
|
5
|
+
gem.email = ['sneal@daptiv.com']
|
6
|
+
gem.description = 'Vagrant automation for CI'
|
7
|
+
gem.summary = 'A small gem to reduce Rake duplication'
|
8
|
+
gem.homepage = ''
|
9
9
|
|
10
10
|
# The following block of code determines the files that should be included
|
11
11
|
# in the gem. It does this by reading all the files in the directory where
|
@@ -13,9 +13,9 @@ Gem::Specification.new do |gem|
|
|
13
13
|
# Note that the entire gitignore(5) syntax is not supported, specifically
|
14
14
|
# the "!" syntax, but it should mostly work correctly.
|
15
15
|
root_path = File.dirname(__FILE__)
|
16
|
-
all_files = Dir.chdir(root_path) { Dir.glob(
|
17
|
-
all_files.reject! { |file| [
|
18
|
-
gitignore_path = File.join(root_path,
|
16
|
+
all_files = Dir.chdir(root_path) { Dir.glob('**/{*,.*}') }
|
17
|
+
all_files.reject! { |file| ['.', '..'].include?(File.basename(file)) }
|
18
|
+
gitignore_path = File.join(root_path, '.gitignore')
|
19
19
|
gitignore = File.readlines(gitignore_path)
|
20
20
|
gitignore.map! { |line| line.chomp.strip }
|
21
21
|
gitignore.reject! { |line| line.empty? || line =~ /^(#|!)/ }
|
@@ -34,22 +34,23 @@ Gem::Specification.new do |gem|
|
|
34
34
|
#
|
35
35
|
gitignore.any? do |ignore|
|
36
36
|
File.fnmatch(ignore, file, File::FNM_PATHNAME) ||
|
37
|
-
|
37
|
+
File.fnmatch(ignore, File.basename(file), File::FNM_PATHNAME)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
gem.files = unignored_files
|
42
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
42
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
43
43
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
44
|
-
gem.name =
|
45
|
-
gem.require_paths = [
|
46
|
-
gem.version = '0.0
|
47
|
-
|
48
|
-
gem.add_runtime_dependency "log4r", "~> 1.1.10"
|
49
|
-
gem.add_runtime_dependency "mixlib-shellout", "~> 1.6.0"
|
44
|
+
gem.name = 'daptiv-chef-ci'
|
45
|
+
gem.require_paths = ['lib']
|
46
|
+
gem.version = '0.1.0'
|
50
47
|
|
51
|
-
gem.
|
52
|
-
gem.
|
53
|
-
gem.
|
54
|
-
|
48
|
+
gem.add_runtime_dependency 'log4r', '~> 1.1.10'
|
49
|
+
gem.add_runtime_dependency 'mixlib-shellout', '~> 1.2'
|
50
|
+
gem.add_runtime_dependency 'versionomy', '~> 0.4.4'
|
51
|
+
|
52
|
+
gem.add_development_dependency 'rake'
|
53
|
+
gem.add_development_dependency 'rspec-core', '~> 2.12.2'
|
54
|
+
gem.add_development_dependency 'rspec-expectations', '~> 2.12.1'
|
55
|
+
gem.add_development_dependency 'rspec-mocks', '~> 2.12.1'
|
55
56
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'rake/dsl_definition'
|
4
|
+
require 'json'
|
5
|
+
require 'tempfile'
|
6
|
+
require_relative 'raketask_helper'
|
7
|
+
require_relative 'shell'
|
8
|
+
|
9
|
+
class CloneEnvironment
|
10
|
+
# Example usage, copies a Chef environment definition to another
|
11
|
+
# environment.
|
12
|
+
#
|
13
|
+
# CloneEnvironment::RakeTask.new do |t|
|
14
|
+
# t.src_env = 'dev'
|
15
|
+
# t.dest_env = "vagrant-#{ENV['LOGNAME']}-dotnetframework"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# This class lets you define Rake tasks to clone Chef environments.
|
19
|
+
class RakeTask < ::Rake::TaskLib
|
20
|
+
include ::Rake::DSL if defined? ::Rake::DSL
|
21
|
+
include DaptivChefCI::RakeTaskHelpers
|
22
|
+
|
23
|
+
attr_accessor :src_env
|
24
|
+
attr_accessor :dest_env
|
25
|
+
|
26
|
+
# @param [String] name The task name.
|
27
|
+
# @param [String] desc Description of the task.
|
28
|
+
def initialize(name = 'clone_environment', desc = 'Clone environment task')
|
29
|
+
@logger = Log4r::Logger.new('daptiv_chef_ci::clone_environment_task')
|
30
|
+
@shell = DaptivChefCI::Shell.new
|
31
|
+
@name, @desc = name, desc
|
32
|
+
yield self if block_given?
|
33
|
+
define_task
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def define_task
|
39
|
+
desc @desc
|
40
|
+
task @name do
|
41
|
+
execute do
|
42
|
+
fail 'src_env must be specified' unless @src_env
|
43
|
+
fail 'dest_env must be specified' unless @dest_env
|
44
|
+
clone_environment
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def clone_environment
|
50
|
+
out = @shell.exec_cmd_in_context(
|
51
|
+
"knife environment show #{@src_env} -F json").join(' ')
|
52
|
+
|
53
|
+
env = JSON.parse(out)
|
54
|
+
env['name'] = @dest_env
|
55
|
+
env['description'] = "The #{dest_env} environment"
|
56
|
+
|
57
|
+
env_file = Tempfile.new([@dest_env, '.json'])
|
58
|
+
begin
|
59
|
+
IO.write(env_file.path, JSON.pretty_generate(env))
|
60
|
+
|
61
|
+
@shell.exec_cmd_in_context(
|
62
|
+
"knife environment from file '#{env_file.path}'")
|
63
|
+
ensure
|
64
|
+
env_file.close
|
65
|
+
env_file.unlink
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,35 +1,34 @@
|
|
1
1
|
module DaptivChefCI
|
2
|
+
# Chef CI logger class
|
2
3
|
class Logger
|
3
|
-
|
4
4
|
# Initializes and enables logging to the given environments level
|
5
5
|
# By default logging only occurs at ERROR level or higher.
|
6
6
|
# Set CHEF_CI_LOG env var to change logging levels
|
7
|
-
def self.init
|
7
|
+
def self.init
|
8
8
|
require 'log4r'
|
9
|
-
|
9
|
+
|
10
10
|
# Set the logging level on all "chef-ci" namespaced
|
11
11
|
# logs as long as we have a valid level.
|
12
|
-
logger = Log4r::Logger.new(
|
12
|
+
logger = Log4r::Logger.new('daptiv_chef_ci')
|
13
13
|
logger.outputters = Log4r::Outputter.stderr
|
14
|
-
logger.level = log_level
|
14
|
+
logger.level = log_level
|
15
15
|
logger = nil
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
# LogLevels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL']
|
19
19
|
# DEBUG = 1
|
20
20
|
# INFO = 2
|
21
21
|
# WARN = 3
|
22
22
|
# ERROR = 4
|
23
23
|
# FATAL = 5
|
24
|
-
def self.log_level
|
25
|
-
level = ENV['CHEF_CI_LOG'].upcase
|
24
|
+
def self.log_level
|
25
|
+
level = ENV['CHEF_CI_LOG'].upcase.to_s
|
26
26
|
level_i = Log4r::Log4rConfig::LogLevels.index(level)
|
27
27
|
level_i + 1
|
28
28
|
rescue
|
29
29
|
return 2 # info
|
30
30
|
end
|
31
|
-
|
32
31
|
end
|
33
32
|
end
|
34
33
|
|
35
|
-
DaptivChefCI::Logger.init
|
34
|
+
DaptivChefCI::Logger.init
|
@@ -1,17 +1,17 @@
|
|
1
1
|
module DaptivChefCI
|
2
2
|
module RakeTaskHelpers
|
3
3
|
extend self
|
4
|
-
|
4
|
+
|
5
5
|
@@exit_on_failure = true
|
6
|
-
|
6
|
+
|
7
7
|
def exit_on_failure
|
8
8
|
@@exit_on_failure
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def exit_on_failure=(exit_on_failure)
|
12
12
|
@@exit_on_failure = exit_on_failure
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def execute(&block)
|
16
16
|
begin
|
17
17
|
block.call()
|
@@ -31,6 +31,5 @@ module DaptivChefCI
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
35
34
|
end
|
36
|
-
end
|
35
|
+
end
|
data/lib/daptiv-chef-ci/shell.rb
CHANGED
@@ -3,61 +3,83 @@ require 'bundler'
|
|
3
3
|
require 'mixlib/shellout'
|
4
4
|
|
5
5
|
module DaptivChefCI
|
6
|
+
# Command shell wrapper
|
6
7
|
class Shell
|
7
|
-
|
8
|
-
|
9
|
-
@logger = Log4r::Logger.new("daptiv_chef_ci::shell")
|
8
|
+
def initialize
|
9
|
+
@logger = Log4r::Logger.new('daptiv_chef_ci::shell')
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
# Executes the specified shell command and returns the stdout.
|
13
13
|
#
|
14
14
|
# This method ensure that any invoked command use the same PATH environment
|
15
15
|
# that the user has outside Ruby/Bundler.
|
16
16
|
#
|
17
17
|
# @param [String] The command line to execute
|
18
|
-
# @param [Int] The number of seconds to wait for the command to finish,
|
19
|
-
#
|
18
|
+
# @param [Int] The number of seconds to wait for the command to finish,
|
19
|
+
# defaults to 600
|
20
|
+
# @param [Hash] Key value pairs of environment variables to pass to the
|
21
|
+
# command's environment.
|
20
22
|
# @return [Array] Each entry represents a line from the stdout
|
21
|
-
def
|
23
|
+
def exec_cmd_in_context(command, timeout = nil, environment = {})
|
22
24
|
timeout ||= 600
|
23
25
|
environment = Hash[ environment.map{ |k, v| [k.to_s, v.to_s] } ]
|
24
|
-
environment['LC_ALL'] = ENV['LC_ALL']
|
26
|
+
environment['LC_ALL'] = ENV['LC_ALL'] unless environment.key?('LC_ALL')
|
27
|
+
|
28
|
+
@logger.info("Executing: '#{command}'")
|
29
|
+
@logger.debug("\n\ttimeout: #{timeout}\n\tenvironment: #{environment}")
|
30
|
+
|
31
|
+
shell_out = Mixlib::ShellOut.new(
|
32
|
+
command, timeout: timeout, environment: environment)
|
33
|
+
shell_out.live_stream = STDOUT
|
34
|
+
|
35
|
+
shell_out.run_command
|
36
|
+
shell_out.invalid! if shell_out.exitstatus != 0
|
37
|
+
@logger.info(shell_out.stdout)
|
38
|
+
shell_out.stdout.split("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Executes the specified shell command and returns the stdout.
|
42
|
+
#
|
43
|
+
# This method ensure that any invoked command use the same PATH environment
|
44
|
+
# that the user has outside Ruby/Bundler.
|
45
|
+
#
|
46
|
+
# @param [String] The command line to execute
|
47
|
+
# @param [Int] The number of seconds to wait for the command to finish,
|
48
|
+
# defaults to 600
|
49
|
+
# @param [Hash] Key value pairs of environment variables to pass to the
|
50
|
+
# command's environment.
|
51
|
+
# @return [Array] Each entry represents a line from the stdout
|
52
|
+
def exec_cmd(command, timeout = nil, environment = {})
|
25
53
|
path_at_start = ENV['PATH']
|
26
54
|
begin
|
27
|
-
ENV['PATH'] = path_without_gem_dir
|
28
|
-
@logger.debug("
|
29
|
-
|
30
|
-
@logger.info("Executing: '#{command}'\n\ttimeout: #{timeout}\n\tenvironment: #{environment}")
|
31
|
-
shell_out = Mixlib::ShellOut.new(command, :timeout => timeout, :environment => environment)
|
32
|
-
shell_out.run_command()
|
33
|
-
shell_out.invalid! if shell_out.exitstatus != 0
|
34
|
-
|
35
|
-
@logger.info(shell_out.stdout)
|
36
|
-
shell_out.stdout.split("\n")
|
55
|
+
ENV['PATH'] = path_without_gem_dir
|
56
|
+
@logger.debug("Setting PATH: #{ENV['PATH']}")
|
57
|
+
exec_cmd_in_context(command, timeout, environment)
|
37
58
|
ensure
|
38
59
|
@logger.debug("Resetting PATH: #{path_at_start}")
|
39
60
|
ENV['PATH'] = path_at_start
|
40
61
|
end
|
41
62
|
end
|
42
|
-
|
63
|
+
|
43
64
|
# Returns the PATH environment variable as it was before Bundler prepended
|
44
65
|
# the system gem directory to it.
|
45
66
|
#
|
46
|
-
# This can happen if the user has invoked "require 'bundler/setup'"
|
47
|
-
# like in this gems Rakefile.
|
67
|
+
# This can happen if the user has invoked "require 'bundler/setup'"
|
68
|
+
# somewhere, like in this gems Rakefile.
|
48
69
|
#
|
49
|
-
# This is needed because sometimes a user will have the Vagrant gem
|
50
|
-
# on their system and we don't want to use it, we should use the
|
51
|
-
# their PATH as if they invoked vagrant themselves
|
70
|
+
# This is needed because sometimes a user will have the Vagrant gem
|
71
|
+
# installed on their system and we don't want to use it, we should use the
|
72
|
+
# one that's in their PATH as if they invoked vagrant themselves
|
73
|
+
# (i.e. the installed version)
|
52
74
|
#
|
53
|
-
# @return [String] The ENV['PATH] without the Bundler system gem dir
|
75
|
+
# @return [String] The ENV['PATH] without the Bundler system gem dir
|
76
|
+
# prepended
|
54
77
|
def path_without_gem_dir
|
55
78
|
paths = ENV['PATH'].split(':')
|
56
79
|
system_gem_dir = "#{Bundler.bundle_path}/bin"
|
57
80
|
@logger.debug("System gem dir: #{system_gem_dir}")
|
58
|
-
paths.delete_if { |p| p.downcase
|
81
|
+
paths.delete_if { |p| p.downcase == system_gem_dir.downcase }
|
59
82
|
paths.join(':')
|
60
83
|
end
|
61
|
-
|
62
84
|
end
|
63
|
-
end
|
85
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'rake/dsl_definition'
|
4
|
+
require_relative 'raketask_helper'
|
5
|
+
require_relative 'shell'
|
6
|
+
|
7
|
+
class UploadCookbook
|
8
|
+
# Example usage, uploads the current cookbook to the Chef server freezing
|
9
|
+
# it to the Canary environment.
|
10
|
+
#
|
11
|
+
# UploadCookbook::RakeTask.new do |t|
|
12
|
+
# t.environment = 'canary'
|
13
|
+
# t.freeze = true
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# This class lets you define Rake tasks to upload cookbooks to Chef.
|
17
|
+
class RakeTask < ::Rake::TaskLib
|
18
|
+
include ::Rake::DSL if defined? ::Rake::DSL
|
19
|
+
include DaptivChefCI::RakeTaskHelpers
|
20
|
+
|
21
|
+
attr_accessor :environment
|
22
|
+
attr_accessor :freeze
|
23
|
+
|
24
|
+
# @param [String] name The task name.
|
25
|
+
# @param [String] desc Description of the task.
|
26
|
+
def initialize(name = 'upload_cookbook', desc = 'Upload cookbook task')
|
27
|
+
@logger = Log4r::Logger.new('daptiv_chef_ci::upload_cookbook_task')
|
28
|
+
@shell = DaptivChefCI::Shell.new
|
29
|
+
@name, @desc = name, desc
|
30
|
+
@freeze = false
|
31
|
+
yield self if block_given?
|
32
|
+
define_task
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def define_task
|
38
|
+
desc @desc
|
39
|
+
task @name do
|
40
|
+
execute do
|
41
|
+
upload_cookbook
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def upload_cookbook
|
47
|
+
cmd = "knife cookbook upload #{cookbook_name} -o ../"
|
48
|
+
cmd << " -E #{environment}" if @environment
|
49
|
+
@shell.exec_cmd_in_context(cmd)
|
50
|
+
end
|
51
|
+
|
52
|
+
def cookbook_name
|
53
|
+
unless @cookbook_name
|
54
|
+
metadata = IO.read(File.join(Dir.pwd, 'metadata.rb'))
|
55
|
+
@cookbook_name = /name\s+['|"](\w+)/.match(metadata)[1]
|
56
|
+
fail 'Cannot find cookbook name in metadata.rb' unless @cookbook_name
|
57
|
+
@logger.debug("found cookbook name: #{@cookbook_name}")
|
58
|
+
end
|
59
|
+
@cookbook_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -6,7 +6,6 @@ require_relative 'raketask_helper'
|
|
6
6
|
require_relative 'logger'
|
7
7
|
|
8
8
|
class VagrantDestroy
|
9
|
-
|
10
9
|
# Example usage, destroys a Vagrant box.
|
11
10
|
#
|
12
11
|
# VagrantUp::RakeTask.new 'up' do |t|
|
@@ -17,11 +16,11 @@ class VagrantDestroy
|
|
17
16
|
class RakeTask < ::Rake::TaskLib
|
18
17
|
include ::Rake::DSL if defined? ::Rake::DSL
|
19
18
|
include DaptivChefCI::RakeTaskHelpers
|
20
|
-
|
19
|
+
|
21
20
|
attr_accessor :vagrant_driver
|
22
21
|
attr_accessor :destroy_timeout_in_seconds
|
23
22
|
attr_accessor :environment
|
24
|
-
|
23
|
+
|
25
24
|
# @param [String] name The task name.
|
26
25
|
# @param [String] desc Description of the task.
|
27
26
|
def initialize(name = 'vagrant_destroy', desc = 'Vagrant destroy task')
|
@@ -31,22 +30,22 @@ class VagrantDestroy
|
|
31
30
|
yield self if block_given?
|
32
31
|
define_task
|
33
32
|
end
|
34
|
-
|
33
|
+
|
35
34
|
private
|
36
35
|
|
37
36
|
def define_task
|
38
37
|
desc @desc
|
39
38
|
task @name do
|
40
|
-
execute
|
41
|
-
|
42
|
-
:
|
39
|
+
execute do
|
40
|
+
vagrant_driver.destroy(
|
41
|
+
cmd_timeout_in_seconds: @destroy_timeout_in_seconds,
|
42
|
+
environment: @environment)
|
43
|
+
end
|
43
44
|
end
|
44
45
|
end
|
45
|
-
|
46
|
-
def vagrant_driver()
|
47
|
-
@vagrant_driver ||= DaptivChefCI::VagrantDriver.new()
|
48
|
-
end
|
49
46
|
|
47
|
+
def vagrant_driver
|
48
|
+
@vagrant_driver ||= DaptivChefCI::VagrantDriver.new
|
49
|
+
end
|
50
50
|
end
|
51
51
|
end
|
52
|
-
|