rick-vlad 1.2.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/vlad/core.rb ADDED
@@ -0,0 +1,178 @@
1
+ require 'vlad'
2
+
3
+ ##
4
+ # used by update, out here so we can ensure all threads have the same value
5
+ def now
6
+ @now ||= Time.now.utc.strftime("%Y%m%d%H%M.%S")
7
+ end
8
+
9
+ namespace :vlad do
10
+ desc "Show the vlad setup. This is all the default variables for vlad
11
+ tasks.".cleanup
12
+
13
+ task :debug do
14
+ require 'yaml'
15
+
16
+ # force them into values
17
+ Rake::RemoteTask.env.keys.each do |key|
18
+ next if key =~ /_release|releases|sudo_password/
19
+ Rake::RemoteTask.fetch key
20
+ end
21
+
22
+ puts "# Environment:"
23
+ puts
24
+ y Rake::RemoteTask.env
25
+ puts "# Roles:"
26
+ y Rake::RemoteTask.roles
27
+ end
28
+
29
+ desc "Setup your servers. Before you can use any of the deployment
30
+ tasks with your project, you will need to make sure all of your
31
+ servers have been prepared with 'rake vlad:setup'. It is safe to
32
+ run this task on servers that have already been set up; it will
33
+ not destroy any deployed revisions or data.".cleanup
34
+
35
+ task :setup do
36
+ Rake::Task['vlad:setup_app'].invoke
37
+ end
38
+
39
+ desc "Prepares application servers for deployment.".cleanup
40
+
41
+ remote_task :setup_app, :roles => :app do
42
+ dirs = [deploy_to, releases_path, scm_path, shared_path]
43
+ dirs += %w(config system log pids).map { |d| File.join(shared_path, d) }
44
+ run [ "umask 02 && mkdir -p #{dirs.join(' ')}",
45
+ "touch #{shared_path}/config/database.yml"
46
+ ].join(" && ")
47
+ end
48
+
49
+ desc "Updates your application server to the latest revision. Syncs
50
+ a copy of the repository, exports it as the latest release, fixes
51
+ up your symlinks, symlinks the latest revision to current and logs
52
+ the update.".cleanup
53
+
54
+ remote_task :update, :roles => :app do
55
+ symlink = false
56
+ begin
57
+ run [ "cd #{scm_path}",
58
+ "#{source.checkout revision, '.'}",
59
+ "#{source.export ".", release_path}",
60
+ "chmod -R g+w #{latest_release}",
61
+ "rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids",
62
+ "mkdir -p #{latest_release}/db #{latest_release}/tmp",
63
+ "ln -s #{shared_path}/log #{latest_release}/log",
64
+ "ln -s #{shared_path}/system #{latest_release}/public/system",
65
+ "ln -s #{shared_path}/pids #{latest_release}/tmp/pids",
66
+ "ln -s #{shared_path}/config/database.yml #{latest_release}/config/database.yml"
67
+ ].join(" && ")
68
+
69
+ symlink = true
70
+ run "rm -f #{current_path} && ln -s #{latest_release} #{current_path}"
71
+
72
+ run "echo #{now} $USER #{revision} #{File.basename release_path} >> #{deploy_to}/revisions.log"
73
+ rescue => e
74
+ run "rm -f #{current_path} && ln -s #{previous_release} #{current_path}" if
75
+ symlink
76
+ run "rm -rf #{release_path}"
77
+ raise e
78
+ end
79
+ end
80
+
81
+ desc "Run the migrate rake task for the the app. By default this is run in
82
+ the latest app directory. You can run migrations for the current app
83
+ directory by setting :migrate_target to :current. Additional environment
84
+ variables can be passed to rake via the migrate_env variable.".cleanup
85
+
86
+ # No application files are on the DB machine, also migrations should only be
87
+ # run once.
88
+ remote_task :migrate, :roles => :app do
89
+ break unless target_host == Rake::RemoteTask.hosts_for(:app).first
90
+
91
+ directory = case migrate_target.to_sym
92
+ when :current then current_path
93
+ when :latest then current_release
94
+ else raise ArgumentError, "unknown migration target #{migrate_target.inspect}"
95
+ end
96
+
97
+ run "cd #{current_path}; #{rake_cmd} RAILS_ENV=#{rails_env} db:migrate #{migrate_args}"
98
+ end
99
+
100
+ desc "Invoke a single command on every remote server. This is useful for
101
+ performing one-off commands that may not require a full task to be written
102
+ for them. Simply specify the command to execute via the COMMAND
103
+ environment variable. To execute the command only on certain roles,
104
+ specify the ROLES environment variable as a comma-delimited list of role
105
+ names.
106
+
107
+ $ rake vlad:invoke COMMAND='uptime'".cleanup
108
+
109
+ remote_task :invoke do
110
+ command = ENV["COMMAND"]
111
+ abort "Please specify a command to execute on the remote servers (via the COMMAND environment variable)" unless command
112
+ puts run(command)
113
+ end
114
+
115
+ desc "Copy arbitrary files to the currently deployed version using
116
+ FILES=a,b,c. This is useful for updating files piecemeal when you
117
+ need to quickly deploy only a single file.
118
+
119
+ To use this task, specify the files and directories you want to copy as a
120
+ comma-delimited list in the FILES environment variable. All directories
121
+ will be processed recursively, with all files being pushed to the
122
+ deployment servers. Any file or directory starting with a '.' character
123
+ will be ignored.
124
+
125
+ $ rake vlad:upload FILES=templates,controller.rb".cleanup
126
+
127
+ remote_task :upload do
128
+ file_list = (ENV["FILES"] || "").split(",")
129
+
130
+ files = file_list.map do |f|
131
+ f = f.strip
132
+ File.directory?(f) ? Dir["#{f}/**/*"] : f
133
+ end.flatten
134
+
135
+ files = files.reject { |f| File.directory?(f) || File.basename(f)[0] == ?. }
136
+
137
+ abort "Please specify at least one file to update (via the FILES environment variable)" if files.empty?
138
+
139
+ files.each do |file|
140
+ rsync file, File.join(current_path, file)
141
+ end
142
+ end
143
+
144
+ desc "Rolls back to a previous version and restarts. This is handy if you
145
+ ever discover that you've deployed a lemon; 'rake vlad:rollback' and
146
+ you're right back where you were, on the previously deployed
147
+ version.".cleanup
148
+
149
+ remote_task :rollback do
150
+ if releases.length < 2 then
151
+ abort "could not rollback the code because there is no prior release"
152
+ else
153
+ run "rm #{current_path}; ln -s #{previous_release} #{current_path} && rm -rf #{current_release}"
154
+ end
155
+
156
+ Rake::Task['vlad:start'].invoke
157
+ end
158
+
159
+ desc "Clean up old releases. By default, the last 5 releases are kept on
160
+ each server (though you can change this with the keep_releases variable).
161
+ All other deployed revisions are removed from the servers.".cleanup
162
+
163
+ remote_task :cleanup do
164
+ max = keep_releases
165
+ if releases.length <= max then
166
+ puts "no old releases to clean up #{releases.length} <= #{max}"
167
+ else
168
+ puts "keeping #{max} of #{releases.length} deployed releases"
169
+
170
+ directories = (releases - releases.last(max)).map { |release|
171
+ File.join(releases_path, release)
172
+ }.join(" ")
173
+
174
+ run "rm -rf #{directories}"
175
+ end
176
+ end
177
+
178
+ end # namespace vlad
data/lib/vlad/git.rb ADDED
@@ -0,0 +1,43 @@
1
+ class Vlad::Git
2
+
3
+ set :source, Vlad::Git.new
4
+ set :git_cmd, "git"
5
+
6
+ ##
7
+ # Returns the command that will check out +revision+ from the
8
+ # code repo into directory +destination+. +revision+ can be any
9
+ # SHA1 or equivalent (e.g. branch, tag, etc...)
10
+
11
+ def checkout(revision, destination)
12
+ destination = 'cached-copy' if destination == '.'
13
+ revision = 'HEAD' if revision =~ /head/i
14
+
15
+ [ "([ -d #{destination}/.git ] && echo 'Existing repository found' || #{git_cmd} clone #{code_repo} #{destination})",
16
+ "cd #{destination}",
17
+ "#{git_cmd} fetch",
18
+ "#{git_cmd} reset --hard #{revision}",
19
+ "#{git_cmd} submodule init",
20
+ "#{git_cmd} submodule update"
21
+ ].join(" && ")
22
+ end
23
+
24
+ ##
25
+ # Returns the command that will export +revision+ from the code repo into
26
+ # the directory +destination+.
27
+
28
+ def export(source, destination)
29
+ [ "cp -R #{source} #{destination}",
30
+ "rm -Rf #{destination}/.git"
31
+ ].join(" && ")
32
+ end
33
+
34
+ ##
35
+ # Returns a command that maps human-friendly revision identifier +revision+
36
+ # into a git SHA1.
37
+
38
+ def revision(revision)
39
+ revision = 'HEAD' if revision =~ /head/i
40
+
41
+ "`#{git_cmd} rev-parse #{revision}`"
42
+ end
43
+ end
@@ -0,0 +1,85 @@
1
+ require 'vlad'
2
+
3
+ namespace :vlad do
4
+
5
+ set :lighttpd_port, 65536
6
+ set :web_command, "lighttpd"
7
+ set :lighttpd_user, "nobody"
8
+ set :lighttpd_group, "nobody"
9
+ set(:lighttpd_init) { "#{shared_path}/lighttpd.sh" }
10
+ set(:lighttpd_conf) { "#{shared_path}/lighttpd.conf" }
11
+
12
+ desc "Prepares application servers for deployment. Lighttpd
13
+ configuration is set via the lighttpd_* variables.".cleanup
14
+
15
+ remote_task :setup_lighttpd, :roles => :app do
16
+ require 'tempfile'
17
+
18
+ put lighttpd_conf, 'vlad.lighttpd_config' do
19
+ conf = <<-"EOF"
20
+ server.modules = ( "mod_rewrite",
21
+ "mod_access",
22
+ "mod_fastcgi",
23
+ "mod_compress",
24
+ "mod_accesslog" )
25
+
26
+ server.document-root = "#{current_path}/public"
27
+ server.errorlog = "#{shared_path}/log/lighttpd.error.log"
28
+ accesslog.filename = "#{shared_path}/log/lighttpd.access.log"
29
+ server.pid-file = "#{shared_path}/pids/lighttpd.pid"
30
+ server.port = #{lighttpd_port}
31
+ server.username = "#{lighttpd_user}"
32
+ server.groupname = "#{lighttpd_group}"
33
+ server.error-handler-404 = "/dispatch.fcgi"
34
+ server.indexfiles = ( "index.html", "index.rb" )
35
+ url.access-deny = ( "~", ".inc" )
36
+ compress.cache-dir = "#{shared_path}/tmp/cache/compress"
37
+ compress.filetype = ("text/html","text/plain","text/javascript","text/css")
38
+ server.tag = "lighttpd | TextDriven"
39
+
40
+ fastcgi.server = (
41
+ ".fcgi" => (
42
+ "localhost" => (
43
+ "min-procs" => 1,
44
+ "max-procs" => 1,
45
+ "socket" => "#{shared_path}/pids/rubyholic.socket",
46
+ "bin-path" => "#{current_path}/public/dispatch.fcgi",
47
+ "bin-environment" => ( "RAILS_ENV" => "production" ) ) ) )
48
+ EOF
49
+ end
50
+
51
+ run "mkdir -p \"#{shared_path}/tmp/cache/compress\""
52
+ end
53
+
54
+ desc "(Re)Start the web servers"
55
+
56
+ remote_task :start_web, :roles => :web do
57
+ cmd = %w(lighttpd ruby).map {|app| "(killall #{app} || true)"}.join(" && ")
58
+ cmd += " && #{web_command} -f #{lighttpd_conf} </dev/null >/dev/null 2>&1"
59
+ run cmd
60
+ end
61
+
62
+ desc "Stop the web servers"
63
+ remote_task :stop_web, :roles => :web do
64
+ cmd = %w(lighttpd ruby).map {|app| "(killall #{app} || true)"}.join(" && ")
65
+
66
+ run cmd
67
+ end
68
+
69
+ ##
70
+ # Everything HTTP.
71
+
72
+ desc "(Re)Start the web and app servers"
73
+
74
+ remote_task :start do
75
+ Rake::Task['vlad:start_app'].invoke
76
+ Rake::Task['vlad:start_web'].invoke
77
+ end
78
+
79
+ desc "Stop the web and app servers"
80
+
81
+ remote_task :stop do
82
+ Rake::Task['vlad:stop_app'].invoke
83
+ Rake::Task['vlad:stop_web'].invoke
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ namespace :vlad do
2
+ desc "Remove the maintenance page"
3
+ remote_task :start_web, :roles => [:web] do
4
+ run "if [ -f #{shared_path}/system/maintenance.html ]; then rm -f #{shared_path}/system/maintenance.html; fi"
5
+ end
6
+
7
+ desc "Put the maintenance page in place"
8
+ remote_task :stop_web, :roles => [:web] do
9
+ run "cp -f #{shared_path}/config/maintenance.html #{shared_path}/system/"
10
+ end
11
+
12
+ desc "(Re)Start the web and app servers"
13
+ remote_task :start do
14
+ Rake::Task['vlad:start_app'].invoke
15
+ Rake::Task['vlad:start_web'].invoke
16
+ end
17
+
18
+ desc "Stop the web and app servers"
19
+ remote_task :stop do
20
+ Rake::Task['vlad:stop_app'].invoke
21
+ Rake::Task['vlad:stop_web'].invoke
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require 'vlad'
2
+ namespace :vlad do
3
+ set :merb_env, 'production'
4
+
5
+ remote_task :stop_app, :roles => [:app] do
6
+ run "sudo /usr/bin/god stop #{application}"
7
+ end
8
+ remote_task :start_app, :roles => [:app] do
9
+ run "sudo /usr/bin/god restart #{application}"
10
+ end
11
+
12
+ remote_task :symlink_configs, :roles => [:app] do
13
+ run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml && mkdir -p #{release_path}/tmp/cache"
14
+ end
15
+
16
+ namespace :dm do
17
+ remote_task :migrate, :roles => [:db] do
18
+ run "cd #{current_path}; MERB_ENV=#{merb_env} rake dm:db:migrate"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ class Vlad::Mercurial
2
+
3
+ set :source, Vlad::Mercurial.new
4
+
5
+ ##
6
+ # Returns the command that will check out +revision+ from the code repo
7
+ # into directory +destination+
8
+
9
+ def checkout(revision, destination)
10
+ revision = 'tip' if revision =~ /^head$/i
11
+ "hg pull -r #{revision} -R #{destination} #{code_repo}"
12
+ end
13
+
14
+ ##
15
+ # Returns the command that will export +revision+ from the code repo into
16
+ # the directory +destination+.
17
+
18
+ def export(revision_or_source, destination)
19
+ revision_or_source = 'tip' if revision_or_source =~ /^head$/i
20
+ if revision_or_source =~ /^(\d+|tip)$/i then
21
+ "hg archive -r #{revision_or_source} -R #{code_repo} #{destination}"
22
+ else
23
+ "hg archive -R #{revision_or_source} #{destination}"
24
+ end
25
+ end
26
+
27
+ ##
28
+ # Returns a command that maps human-friendly revision identifier +revision+
29
+ # into a subversion revision specification.
30
+
31
+ def revision(revision)
32
+ "`hg identify -R #{code_repo} | cut -f1 -d\\ `"
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ require 'vlad'
2
+
3
+ namespace :vlad do
4
+ ##
5
+ # Mongrel app server
6
+
7
+ set :mongrel_address, "127.0.0.1"
8
+ set :mongrel_clean, false
9
+ set :mongrel_command, 'mongrel_rails'
10
+ set(:mongrel_conf) { "#{shared_path}/mongrel_cluster.conf" }
11
+ set :mongrel_config_script, nil
12
+ set :mongrel_environment, "production"
13
+ set :mongrel_group, nil
14
+ set :mongrel_log_file, nil
15
+ set :mongrel_pid_file, nil
16
+ set :mongrel_port, 8000
17
+ set :mongrel_prefix, nil
18
+ set :mongrel_servers, 2
19
+ set :mongrel_user, nil
20
+
21
+ desc "Prepares application servers for deployment. Mongrel
22
+ configuration is set via the mongrel_* variables.".cleanup
23
+
24
+ remote_task :setup_app, :roles => :app do
25
+ cmd = [
26
+ "#{mongrel_command} cluster::configure",
27
+ "-N #{mongrel_servers}",
28
+ "-p #{mongrel_port}",
29
+ "-e #{mongrel_environment}",
30
+ "-a #{mongrel_address}",
31
+ "-c #{current_path}",
32
+ "-C #{mongrel_conf}",
33
+ ("-P #{mongrel_pid_file}" if mongrel_pid_file),
34
+ ("-l #{mongrel_log_file}" if mongrel_log_file),
35
+ ("--user #{mongrel_user}" if mongrel_user),
36
+ ("--group #{mongrel_group}" if mongrel_group),
37
+ ("--prefix #{mongrel_prefix}" if mongrel_prefix),
38
+ ("-S #{mongrel_config_script}" if mongrel_config_script),
39
+ ].compact.join ' '
40
+
41
+ run cmd
42
+ end
43
+
44
+ def mongrel(cmd) # :nodoc:
45
+ cmd = "#{mongrel_command} #{cmd} -C #{mongrel_conf}"
46
+ cmd << ' --clean' if mongrel_clean
47
+ cmd
48
+ end
49
+
50
+ desc "Restart the app servers"
51
+
52
+ remote_task :start_app, :roles => :app do
53
+ run mongrel("cluster::restart")
54
+ end
55
+
56
+ desc "Stop the app servers"
57
+
58
+ remote_task :stop_app, :roles => :app do
59
+ run mongrel("cluster::stop")
60
+ end
61
+ end
data/lib/vlad/nginx.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'vlad'
2
+
3
+ namespace :vlad do
4
+ ##
5
+ # Nginx web server on Gentoo/Debian init.d systems
6
+
7
+ set :web_command, "/etc/init.d/nginx"
8
+
9
+ desc "(Re)Start the web servers"
10
+
11
+ remote_task :start_web, :roles => :web do
12
+ if use_sudo
13
+ sudo "#{web_command} restart"
14
+ else
15
+ run "#{web_command} restart"
16
+ end
17
+ end
18
+
19
+ desc "Stop the web servers"
20
+
21
+ remote_task :stop_web, :roles => :web do
22
+ if use_sudo
23
+ sudo "#{web_command} stop"
24
+ else
25
+ run "#{web_command} stop"
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Everything HTTP.
31
+
32
+ desc "(Re)Start the web and app servers"
33
+ remote_task :start do
34
+ Rake::Task['vlad:start_app'].invoke
35
+ Rake::Task['vlad:start_web'].invoke
36
+ end
37
+
38
+ desc "Stop the web and app servers"
39
+
40
+ remote_task :stop do
41
+ Rake::Task['vlad:stop_app'].invoke
42
+ Rake::Task['vlad:stop_web'].invoke
43
+ end
44
+ end
@@ -0,0 +1,117 @@
1
+ class Vlad::Perforce
2
+
3
+ set :p4_cmd, "p4"
4
+ set :source, Vlad::Perforce.new
5
+
6
+ ##
7
+ # Returns the p4 command that will checkout +revision+ into the directory
8
+ # +destination+.
9
+
10
+ def checkout(revision, destination)
11
+ "#{p4_cmd} sync ...#{rev_no(revision)}"
12
+ end
13
+
14
+ ##
15
+ # Returns the p4 command that will export +revision+ into the directory
16
+ # +directory+.
17
+
18
+ def export(revision_or_source, destination)
19
+ if revision_or_source =~ /^(\d+|head)$/i then
20
+ "(cd #{destination} && #{p4_cmd} sync ...#{rev_no(revision_or_source)})"
21
+ else
22
+ "cp -r #{revision_or_source} #{destination}"
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Returns a command that maps human-friendly revision identifier +revision+
28
+ # into a Perforce revision specification.
29
+
30
+ def revision(revision)
31
+ "`#{p4_cmd} changes -s submitted -m 1 ...#{rev_no(revision)} | cut -f 2 -d\\ `"
32
+ end
33
+
34
+ ##
35
+ # Maps revision +revision+ into a Perforce revision.
36
+
37
+ def rev_no(revision)
38
+ case revision.to_s
39
+ when /head/i then
40
+ "#head"
41
+ when /^\d+$/ then
42
+ "@#{revision}"
43
+ else
44
+ revision
45
+ end
46
+ end
47
+ end
48
+
49
+ namespace :vlad do
50
+ remote_task :setup_app, :roles => :app do
51
+ p4data = p4port = p4user = p4passwd = nil
52
+
53
+ if ENV['P4CONFIG'] then
54
+ p4config_name = ENV['P4CONFIG']
55
+ p4config = nil
56
+ orig_dir = Dir.pwd.split File::SEPARATOR
57
+
58
+ until orig_dir.length == 1 do
59
+ p4config = orig_dir + [p4config_name]
60
+ p4config = File.join p4config
61
+ break if File.exist? p4config
62
+ orig_dir.pop
63
+ end
64
+
65
+ raise "couldn't find .p4config" unless File.exist? p4config
66
+
67
+ p4data = File.readlines(p4config).map { |line| line.strip.split '=', 2 }
68
+ p4data = Hash[*p4data.flatten]
69
+ else
70
+ p4data = ENV
71
+ end
72
+
73
+ p4port = p4data['P4PORT']
74
+ p4user = p4data['P4USER']
75
+ p4passwd = p4data['P4PASSWD']
76
+
77
+ raise "couldn't get P4PORT" if p4port.nil?
78
+ raise "couldn't get P4USER" if p4user.nil?
79
+ raise "couldn't get P4PASSWD" if p4passwd.nil?
80
+
81
+ p4client = [p4user, target_host, application].join '-'
82
+
83
+ require 'tmpdir'
84
+ require 'tempfile'
85
+
86
+ put File.join(scm_path, '.p4config'), 'vlad.p4config' do
87
+ [ "P4PORT=#{p4port}",
88
+ "P4USER=#{p4user}",
89
+ "P4PASSWD=#{p4passwd}",
90
+ "P4CLIENT=#{p4client}" ].join("\n")
91
+ end
92
+
93
+ p4client_path = File.join deploy_to, 'p4client.tmp'
94
+
95
+ put p4client_path, 'vlad.p4client' do
96
+ conf = <<-"CLIENT"
97
+ Client: #{p4client}
98
+
99
+ Owner: #{p4user}
100
+
101
+ Root: #{scm_path}
102
+
103
+ View:
104
+ #{code_repo}/... //#{p4client}/...
105
+ CLIENT
106
+ end
107
+
108
+ cmds = [
109
+ "cd #{scm_path}",
110
+ "p4 client -i < #{p4client_path}",
111
+ "rm #{p4client_path}",
112
+ ]
113
+
114
+ run cmds.join(' && ')
115
+ end
116
+ end
117
+
@@ -0,0 +1,34 @@
1
+ class Vlad::Subversion
2
+
3
+ set :source, Vlad::Subversion.new
4
+ set :svn_cmd, "svn"
5
+
6
+ ##
7
+ # Returns the command that will check out +revision+ from the code repo
8
+ # into directory +destination+
9
+
10
+ def checkout(revision, destination)
11
+ "#{svn_cmd} co -r #{revision} #{code_repo} #{destination}"
12
+ end
13
+
14
+ ##
15
+ # Returns the command that will export +revision+ from the code repo into
16
+ # the directory +destination+.
17
+
18
+ def export(revision_or_source, destination)
19
+ if revision_or_source =~ /^(\d+|head)$/i then
20
+ "#{svn_cmd} export -r #{revision_or_source} #{code_repo} #{destination}"
21
+ else
22
+ "#{svn_cmd} export #{revision_or_source} #{destination}"
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Returns a command that maps human-friendly revision identifier +revision+
28
+ # into a subversion revision specification.
29
+
30
+ def revision(revision)
31
+ "`#{svn_cmd} info #{code_repo} | grep 'Revision:' | cut -f2 -d\\ `"
32
+ end
33
+ end
34
+