pot 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.
Files changed (47) hide show
  1. checksums.yaml +15 -0
  2. data/CREDITS +32 -0
  3. data/LICENSE +20 -0
  4. data/README.md +21 -0
  5. data/bin/pot-console +9 -0
  6. data/lib/pot.rb +17 -0
  7. data/lib/pot/actor.rb +34 -0
  8. data/lib/pot/bundle.rb +66 -0
  9. data/lib/pot/config.rb +33 -0
  10. data/lib/pot/console.rb +7 -0
  11. data/lib/pot/deployment.rb +91 -0
  12. data/lib/pot/dsl.rb +39 -0
  13. data/lib/pot/installer.rb +131 -0
  14. data/lib/pot/installers/apt.rb +52 -0
  15. data/lib/pot/installers/binary.rb +46 -0
  16. data/lib/pot/installers/brew.rb +34 -0
  17. data/lib/pot/installers/deb.rb +41 -0
  18. data/lib/pot/installers/gem.rb +64 -0
  19. data/lib/pot/installers/group.rb +15 -0
  20. data/lib/pot/installers/npm.rb +19 -0
  21. data/lib/pot/installers/push_text.rb +49 -0
  22. data/lib/pot/installers/rake.rb +37 -0
  23. data/lib/pot/installers/replace_text.rb +45 -0
  24. data/lib/pot/installers/runner.rb +20 -0
  25. data/lib/pot/installers/source.rb +202 -0
  26. data/lib/pot/installers/transfer.rb +184 -0
  27. data/lib/pot/installers/user.rb +15 -0
  28. data/lib/pot/instance.rb +21 -0
  29. data/lib/pot/logger.rb +41 -0
  30. data/lib/pot/package.rb +352 -0
  31. data/lib/pot/policy.rb +74 -0
  32. data/lib/pot/role.rb +16 -0
  33. data/lib/pot/template.rb +20 -0
  34. data/lib/pot/transports/local.rb +31 -0
  35. data/lib/pot/transports/ssh.rb +81 -0
  36. data/lib/pot/verifiers/apt.rb +21 -0
  37. data/lib/pot/verifiers/brew.rb +21 -0
  38. data/lib/pot/verifiers/directory.rb +16 -0
  39. data/lib/pot/verifiers/executable.rb +53 -0
  40. data/lib/pot/verifiers/file.rb +34 -0
  41. data/lib/pot/verifiers/process.rb +21 -0
  42. data/lib/pot/verifiers/ruby.rb +25 -0
  43. data/lib/pot/verifiers/symlink.rb +30 -0
  44. data/lib/pot/verifiers/users_groups.rb +33 -0
  45. data/lib/pot/verify.rb +112 -0
  46. data/lib/pot/version.rb +13 -0
  47. metadata +118 -0
@@ -0,0 +1,52 @@
1
+ module Pot
2
+ module Installers
3
+ # = Apt Package Installer
4
+ #
5
+ # The Apt package installer uses the +apt-get+ command to install
6
+ # packages. The apt installer has only one option which can be
7
+ # modified which is the +dependencies_only+ option. When this is
8
+ # set to true, the installer uses +build-dep+ instead of +install+
9
+ # to only build the dependencies.
10
+ #
11
+ # == Example Usage
12
+ #
13
+ # First, a simple installation of the magic_beans package:
14
+ #
15
+ # package :magic_beans do
16
+ # description "Beans beans they're good for your heart..."
17
+ # apt 'magic_beans_package'
18
+ # end
19
+ #
20
+ # Second, only build the magic_beans dependencies:
21
+ #
22
+ # package :magic_beans_depends do
23
+ # apt 'magic_beans_package' { dependencies_only true }
24
+ # end
25
+ #
26
+ # As you can see, setting options is as simple as creating a
27
+ # block and calling the option as a method with the value as
28
+ # its parameter.
29
+ class Apt < Installer
30
+ attr_accessor :packages #:nodoc:
31
+
32
+ def initialize(parent, *packages, &block) #:nodoc:
33
+ packages.flatten!
34
+
35
+ options = { :dependencies_only => false }
36
+ options.update(packages.pop) if packages.last.is_a?(Hash)
37
+
38
+ super parent, options, &block
39
+
40
+ @packages = packages
41
+ end
42
+
43
+ protected
44
+
45
+ def install_commands #:nodoc:
46
+ command = @options[:dependencies_only] ? 'build-dep' : 'install'
47
+ "env DEBCONF_TERSE='yes' DEBIAN_PRIORITY='critical' DEBIAN_FRONTEND=noninteractive apt-get --force-yes -qyu #{command} #{@packages.join(' ')}"
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ module Pot
2
+ module Installers
3
+ # = Binary Installer
4
+ #
5
+ # binary "http://some.url.com/archive.tar.gz" do
6
+ # prefix "/home/user/local"
7
+ # archives "/home/user/sources"
8
+ # end
9
+ #
10
+ class Binary < Installer
11
+ def initialize(parent, binary_archive, options = {}, &block) #:nodoc:
12
+ @binary_archive = binary_archive
13
+ @options = options
14
+ super parent, options, &block
15
+ end
16
+
17
+ def prepare_commands #:nodoc:
18
+ raise 'No installation area defined' unless @options[:prefix]
19
+ raise 'No archive download area defined' unless @options[:archives]
20
+
21
+ [ "mkdir -p #{@options[:prefix]}",
22
+ "mkdir -p #{@options[:archives]}" ]
23
+ end
24
+
25
+ def install_commands #:nodoc:
26
+ commands = [ "bash -c 'wget -cq --directory-prefix=#{@options[:archives]} #{@binary_archive}'" ]
27
+ commands << "bash -c 'cd #{@options[:prefix]} && #{extract_command} #{@options[:archives]}/#{@binary_archive.split("/").last}'"
28
+ end
29
+
30
+ def extract_command(archive_name = @binary_archive.split("/").last)
31
+ case archive_name
32
+ when /(tar.gz)|(tgz)$/
33
+ 'tar xzf'
34
+ when /(tar.bz2)|(tb2)$/
35
+ 'tar xjf'
36
+ when /tar$/
37
+ 'tar xf'
38
+ when /zip$/
39
+ 'unzip -o'
40
+ else
41
+ raise "Unknown binary archive format: #{archive_name}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ module Pot
2
+ module Installers
3
+ # = Homebrew Package Installer
4
+ #
5
+ # The Homebrew package installer uses the +brew+ command to install
6
+ # packages on OSX.
7
+ #
8
+ # == Example Usage
9
+ #
10
+ # package :magic_beans do
11
+ # description "Beans beans they're good for your heart..."
12
+ # brew 'magic_beans_package'
13
+ # end
14
+ #
15
+ class Brew < Installer
16
+ attr_accessor :formulas #:nodoc:
17
+
18
+ def initialize(parent, *formulas, &block) #:nodoc:
19
+ formulas.flatten!
20
+
21
+ super parent, &block
22
+
23
+ @formulas = formulas
24
+ end
25
+
26
+ protected
27
+
28
+ def install_commands #:nodoc:
29
+ "brew install #{@formulas.join(' ')}"
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module Pot
2
+ module Installers
3
+ # = Deb Package Installer
4
+ #
5
+ # The Deb installer installs deb packages sourced from a remote URL
6
+ #
7
+ # == Example Usage
8
+ #
9
+ # Installing the magic_beans deb.
10
+ #
11
+ # package :magic_beans do
12
+ # deb 'http://debs.example.com/magic_beans.deb'
13
+ # end
14
+ #
15
+ class Deb < Installer
16
+ attr_accessor :packages #:nodoc:
17
+
18
+ def initialize(parent, packages, &block) #:nodoc:
19
+ super parent, &block
20
+ packages = [packages] unless packages.is_a? Array
21
+ @packages = packages
22
+ end
23
+
24
+ protected
25
+
26
+ def install_commands #:nodoc:
27
+ [
28
+ "wget -cq --directory-prefix=/tmp #{@packages.join(' ')}",
29
+ "dpkg -i #{@packages.collect{|p| "/tmp/#{package_name(p)}"}.join(" ")}"
30
+ ]
31
+ end
32
+
33
+ private
34
+
35
+ def package_name(url)
36
+ url.split('/').last
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,64 @@
1
+ module Pot
2
+ module Installers
3
+ # = Ruby Gem Package Installer
4
+ #
5
+ # The gem package installer installs ruby gems.
6
+ #
7
+ # The installer has a single optional configuration: source.
8
+ # By changing source you can specify a given ruby gems
9
+ # repository from which to install.
10
+ #
11
+ # == Example Usage
12
+ #
13
+ # First, a simple installation of the magic_beans gem:
14
+ #
15
+ # package :magic_beans do
16
+ # description "Beans beans they're good for your heart..."
17
+ # gem 'magic_beans'
18
+ # end
19
+ #
20
+ # Second, install magic_beans gem from github:
21
+ #
22
+ # package :magic_beans do
23
+ # gem 'magic_beans_package' do
24
+ # source 'http://gems.github.com'
25
+ # end
26
+ # end
27
+ #
28
+ # As you can see, setting options is as simple as creating a
29
+ # block and calling the option as a method with the value as
30
+ # its parameter.
31
+ class Gem < Installer
32
+ attr_accessor :gem #:nodoc:
33
+
34
+ def initialize(parent, gem, options = {}, &block) #:nodoc:
35
+ super parent, options, &block
36
+ @gem = gem
37
+ end
38
+
39
+ def source(location = nil) #:nodoc:
40
+ # package defines an installer called source so here we specify a method directly
41
+ # rather than rely on the automatic options processing since packages' method missing
42
+ # won't be run
43
+ return @options[:source] unless location
44
+ @options[:source] = location
45
+ end
46
+
47
+ protected
48
+
49
+ # rubygems 0.9.5+ installs dependencies by default, and does platform selection
50
+
51
+ def install_commands #:nodoc:
52
+ cmd = "gem install #{gem}"
53
+ cmd << " --version '#{version}'" if version
54
+ cmd << " --source #{source}" if source
55
+ cmd << " --install-dir #{repository}" if option?(:repository)
56
+ cmd << " --no-rdoc --no-ri" unless option?(:build_docs)
57
+ cmd << " --http-proxy #{http_proxy}" if option?(:http_proxy)
58
+ cmd << " -- #{build_flags}" if option?(:build_flags)
59
+ cmd
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ module Pot
2
+ module Installers
3
+ class Group < Installer
4
+ def initialize(package, groupname, options, &block)
5
+ super package, &block
6
+ @groupname = groupname
7
+ @options = options
8
+ end
9
+ protected
10
+ def install_commands
11
+ "addgroup #{@options[:flags]} #{@groupname}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Pot
2
+ module Installers
3
+
4
+ class Npm < Installer
5
+ attr_accessor :package_name
6
+
7
+ def initialize(parent, package_name, &block)
8
+ super parent, &block
9
+ @package_name = package_name
10
+ end
11
+
12
+ protected
13
+ def install_commands #override
14
+ "npm install --global #{@package_name}"
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ module Pot
2
+ module Installers
3
+ # Beware, strange "installer" coming your way.
4
+ #
5
+ # = Text configuration installer
6
+ #
7
+ # This installer pushes simple configuration into a file.
8
+ #
9
+ # == Example Usage
10
+ #
11
+ # Installing magic_beans into apache2.conf
12
+ #
13
+ # package :magic_beans do
14
+ # push_text 'magic_beans', '/etc/apache2/apache2.conf'
15
+ # end
16
+ #
17
+ # If you user has access to 'sudo' and theres a file that requires
18
+ # priveledges, you can pass :sudo => true
19
+ #
20
+ # package :magic_beans do
21
+ # push_text 'magic_beans', '/etc/apache2/apache2.conf', :sudo => true
22
+ # end
23
+ #
24
+ # A special verify step exists for this very installer
25
+ # its known as file_contains, it will test that a file indeed
26
+ # contains a substring that you send it.
27
+ #
28
+ class PushText < Installer
29
+ require 'shellwords'
30
+
31
+ attr_accessor :text, :path #:nodoc:
32
+
33
+ def initialize(parent, text, path, options={}, &block) #:nodoc:
34
+ super parent, options, &block
35
+ @text = text
36
+ @path = path
37
+ end
38
+
39
+ protected
40
+
41
+ def install_commands #:nodoc:
42
+ #"#{"#{'sudo ' if option?(:sudo)}grep \"^#{@text.gsub("'", "'\\\\''").gsub("\n", '\n')}$\" #{@path} ||" if option?(:idempotent) }/bin/echo -e '#{@text.gsub("'", "'\\\\''").gsub("\n", '\n')}' |#{'sudo ' if option?(:sudo)}tee -a #{@path}"
43
+ text = Shellwords.escape(@text)
44
+ "bash -c '/bin/echo -e #{text} | tee -a #{@path}'"
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ module Pot
2
+ module Installers
3
+ # = Rake Installer
4
+ #
5
+ # This installer runs a rake command.
6
+ #
7
+ # == Example Usage
8
+ #
9
+ # The following example runs the command "rake spec" on
10
+ # the remote server.
11
+ #
12
+ # package :spec do
13
+ # rake 'spec'
14
+ # end
15
+ #
16
+ # Specify a Rakefile with the :rakefile option.
17
+ #
18
+ # package :spec, :rakefile => "/var/setup/Rakefile" do
19
+ # rake 'spec'
20
+ # end
21
+
22
+ class Rake < Installer
23
+ def initialize(parent, commands, options = {}, &block) #:nodoc:
24
+ super parent, options, &block
25
+ @commands = commands
26
+ end
27
+
28
+ protected
29
+
30
+ def install_commands #:nodoc:
31
+ file = @options[:rakefile] ? "-f #{@options[:rakefile]} " : ""
32
+ "rake #{file}#{@commands}"
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ module Pot
2
+ module Installers
3
+ # = Replace text installer
4
+ #
5
+ # This installer replaces a text with another one in a file.
6
+ #
7
+ # == Example Usage
8
+ #
9
+ # Change ssh port in /etc/ssh/sshd_config
10
+ #
11
+ # package :magic_beans do
12
+ # replace_text 'Port 22', 'Port 2500', '/etc/ssh/sshd_config'
13
+ # end
14
+ #
15
+ # If you user has access to 'sudo' and theres a file that requires
16
+ # priveledges, you can pass :sudo => true
17
+ #
18
+ # package :magic_beans do
19
+ # replace_text 'Port 22', 'Port 2500', '/etc/ssh/sshd_config', :sudo => true
20
+ # end
21
+ #
22
+ # A special verify step exists for this very installer
23
+ # its known as file_contains, it will test that a file indeed
24
+ # contains a substring that you send it.
25
+ #
26
+ class ReplaceText < Installer
27
+ attr_accessor :regex, :text, :path #:nodoc:
28
+
29
+ def initialize(parent, regex, text, path, options={}, &block) #:nodoc:
30
+ super parent, options, &block
31
+ @regex = regex
32
+ @text = text
33
+ @path = path
34
+ end
35
+
36
+ protected
37
+
38
+ def install_commands #:nodoc:
39
+ logger.info "--> Replace '#{@regex}' with '#{@text}' in file #{@path}"
40
+ "#{'sudo ' if option?(:sudo)}sed -i 's/#{@regex.gsub("'", "'\\\\''").gsub("/", "\\\\/").gsub("\n", '\n')}/#{@text.gsub("'", "'\\\\''").gsub("/", "\\\\/").gsub("\n", '\n')}/g' #{@path}"
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ module Pot
2
+ module Installers
3
+ class Runner < Installer
4
+ require 'shellwords'
5
+
6
+ attr_accessor :cmd #:nodoc:
7
+
8
+ def initialize(parent, cmd, options = {}, &block) #:nodoc:
9
+ super parent, options, &block
10
+ @cmd = cmd
11
+ end
12
+
13
+ protected
14
+
15
+ def install_commands #:nodoc:
16
+ @cmd
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,202 @@
1
+ module Pot
2
+ module Installers
3
+ # = Source Package Installer
4
+ #
5
+ # The source package installer installs software from source.
6
+ # It handles downloading, extracting, configuring, building,
7
+ # and installing software.
8
+ #
9
+ # == Configuration Options
10
+ #
11
+ # The source installer has many configuration options:
12
+ # * <b>prefix</b> - The prefix directory that is configured to.
13
+ # * <b>archives</b> - The location all the files are downloaded to.
14
+ # * <b>builds</b> - The directory the package is extracted to to configure and install
15
+ #
16
+ # == Pre/Post Hooks
17
+ #
18
+ # The source installer defines a myriad of new stages which can be hooked into:
19
+ # * <b>prepare</b> - Prepare is the stage which all the prefix, archives, and build directories are made.
20
+ # * <b>download</b> - Download is the stage which the software package is downloaded.
21
+ # * <b>extract</b> - Extract is the stage which the software package is extracted.
22
+ # * <b>configure</b> - Configure is the stage which the ./configure script is run.
23
+ # * <b>build</b> - Build is the stage in which `make` is called.
24
+ # * <b>install</b> - Install is the stage which `make install` is called.
25
+ #
26
+ # == Example Usage
27
+ #
28
+ # First, a simple package, no configuration:
29
+ #
30
+ # package :magic_beans do
31
+ # source 'http://magicbeansland.com/latest-1.1.1.tar.gz'
32
+ # end
33
+ #
34
+ # Second, specifying exactly where I want my files:
35
+ #
36
+ # package :magic_beans do
37
+ # source 'http://magicbeansland.com/latest-1.1.1.tar.gz' do
38
+ # prefix '/usr/local'
39
+ # archives '/tmp'
40
+ # builds '/tmp/builds'
41
+ # end
42
+ # end
43
+ #
44
+ # Third, specifying some hooks:
45
+ #
46
+ # package :magic_beans do
47
+ # source 'http://magicbeansland.com/latest-1.1.1.tar.gz' do
48
+ # prefix '/usr/local'
49
+ #
50
+ # pre :prepare { 'echo "Here we go folks."' }
51
+ # post :extract { 'echo "I believe..."' }
52
+ # pre :build { 'echo "Cross your fingers!"' }
53
+ # end
54
+ # end
55
+ #
56
+ # Fourth, specifying a custom archive name because the downloaded file name
57
+ # differs from the source URL:
58
+ #
59
+ # package :gitosis do
60
+ # source 'http://github.com/crafterm/sprinkle/tarball/master' do
61
+ # custom_archive 'crafterm-sprinkle-518e33c835986c03ec7ae8ea88c657443b006f28.tar.gz'
62
+ # end
63
+ # end
64
+ #
65
+ # As you can see, setting options is as simple as creating a
66
+ # block and calling the option as a method with the value as
67
+ # its parameter.
68
+
69
+ class Source < Installer
70
+ attr_accessor :source #:nodoc:
71
+
72
+ def initialize(parent, source, options = {}, &block) #:nodoc:
73
+ @source = source
74
+ super parent, options, &block
75
+ @options[:prefix] ||= '/usr/local'
76
+ @options[:archives] ||= '/usr/src'
77
+ @options[:builds] ||= '/tmp'
78
+ end
79
+
80
+ def commands #:nodoc:
81
+ prepare + download + extract + configure + build + install
82
+ end
83
+
84
+ private
85
+
86
+ %w( prepare download extract configure build install ).each do |stage|
87
+ define_method stage do
88
+ pre_commands(stage.to_sym) + self.send("#{stage}_commands") + post_commands(stage.to_sym)
89
+ end
90
+ end
91
+
92
+ def prepare_commands #:nodoc:
93
+ raise 'No installation area defined' unless @options[:prefix]
94
+ raise 'No build area defined' unless @options[:builds]
95
+ raise 'No source download area defined' unless @options[:archives]
96
+
97
+ [ "mkdir -p #{@options[:prefix]}",
98
+ "mkdir -p #{@options[:builds]}",
99
+ "mkdir -p #{@options[:archives]}" ]
100
+ end
101
+
102
+ def download_commands #:nodoc:
103
+ if File.exist? @source
104
+ [ "cp #{@source} #{@options[:archives]}/#{archive_name}" ]
105
+ else
106
+ [ "/usr/bin/curl -L -o '#{@options[:archives]}/#{archive_name}' #{@source}" ]
107
+ end
108
+ end
109
+
110
+ def extract_commands #:nodoc:
111
+ [ "bash -c 'cd #{@options[:builds]} && #{extract_command} #{@options[:archives]}/#{archive_name}'" ]
112
+ end
113
+
114
+ def configure_commands #:nodoc:
115
+ return [] if custom_install?
116
+
117
+ command = "bash -c 'cd #{build_dir} && ./configure --prefix=#{@options[:prefix]} "
118
+
119
+ extras = {
120
+ :enable => '--enable', :disable => '--disable',
121
+ :with => '--with', :without => '--without',
122
+ :option => '-',
123
+ }
124
+
125
+ extras.inject(command) { |m, (k, v)| m << create_options(k, v) if options[k]; m }
126
+
127
+ if options[:configure_options]
128
+ command << options[:configure_options]
129
+ command << ' '
130
+ end
131
+
132
+ [ command << " > #{@package.name}-configure.log 2>&1'" ]
133
+ end
134
+
135
+ def build_commands #:nodoc:
136
+ return [] if custom_install?
137
+ [ "bash -c 'cd #{build_dir} && make > #{@package.name}-build.log 2>&1'" ]
138
+ end
139
+
140
+ def install_commands #:nodoc:
141
+ return custom_install_commands if custom_install?
142
+ [ "bash -c 'cd #{build_dir} && make install > #{@package.name}-install.log 2>&1'" ]
143
+ end
144
+
145
+ def custom_install? #:nodoc:
146
+ !! @options[:custom_install]
147
+ end
148
+
149
+ # REVISIT: must be better processing of custom install commands somehow? use splat operator?
150
+ def custom_install_commands #:nodoc:
151
+ dress @options[:custom_install], :install
152
+ end
153
+
154
+ # dress is overriden from the base Pot::Installers::Installer class so that the command changes
155
+ # directory to the build directory first. Also, the result of the command is logged.
156
+ def dress(commands, stage)
157
+ commands.collect { |command| "bash -c 'cd #{build_dir} && #{command} >> #{@package.name}-#{stage}.log 2>&1'" }
158
+ end
159
+
160
+ def create_options(key, prefix) #:nodoc:
161
+ @options[key].inject('') { |m, option| m << "#{prefix}-#{option} "; m }
162
+ end
163
+
164
+ def extract_command #:nodoc:
165
+ case archive_name
166
+ when /(tar.gz)|(tgz)$/
167
+ 'tar xzf'
168
+ when /(tar.bz2)|(tb2)$/
169
+ 'tar xjf'
170
+ when /tar$/
171
+ 'tar xf'
172
+ when /zip$/
173
+ 'unzip -o'
174
+ else
175
+ raise "Unknown source archive format: #{archive_name}"
176
+ end
177
+ end
178
+
179
+ def archive_name #:nodoc:
180
+ name = @source.split('/').last
181
+ if options[:custom_archive]
182
+ name = options[:custom_archive]
183
+ name = name.join if name.is_a? Array
184
+ end
185
+ raise "Unable to determine archive name for source: #{source}, please update code knowledge" unless name
186
+ name
187
+ end
188
+
189
+ def build_dir #:nodoc:
190
+ "#{@options[:builds]}/#{options[:custom_dir] || base_dir}"
191
+ end
192
+
193
+ def base_dir #:nodoc:
194
+ if archive_name.split('/').last =~ /(.*)\.(tar\.gz|tgz|tar\.bz2|tar|tb2|zip)/
195
+ return $1
196
+ end
197
+ raise "Unknown base path for source archive: #{@source}, please update code knowledge"
198
+ end
199
+
200
+ end
201
+ end
202
+ end