capistrano-edge 2.5.6

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 (109) hide show
  1. data/CHANGELOG.rdoc +770 -0
  2. data/Manifest +104 -0
  3. data/README.rdoc +66 -0
  4. data/Rakefile +35 -0
  5. data/bin/cap +4 -0
  6. data/bin/capify +95 -0
  7. data/capistrano.gemspec +51 -0
  8. data/examples/sample.rb +14 -0
  9. data/lib/capistrano.rb +2 -0
  10. data/lib/capistrano/callback.rb +45 -0
  11. data/lib/capistrano/cli.rb +47 -0
  12. data/lib/capistrano/cli/execute.rb +84 -0
  13. data/lib/capistrano/cli/help.rb +125 -0
  14. data/lib/capistrano/cli/help.txt +75 -0
  15. data/lib/capistrano/cli/options.rb +224 -0
  16. data/lib/capistrano/cli/ui.rb +40 -0
  17. data/lib/capistrano/command.rb +283 -0
  18. data/lib/capistrano/configuration.rb +43 -0
  19. data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
  20. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  21. data/lib/capistrano/configuration/actions/invocation.rb +293 -0
  22. data/lib/capistrano/configuration/callbacks.rb +148 -0
  23. data/lib/capistrano/configuration/connections.rb +204 -0
  24. data/lib/capistrano/configuration/execution.rb +143 -0
  25. data/lib/capistrano/configuration/loading.rb +197 -0
  26. data/lib/capistrano/configuration/namespaces.rb +197 -0
  27. data/lib/capistrano/configuration/roles.rb +73 -0
  28. data/lib/capistrano/configuration/servers.rb +85 -0
  29. data/lib/capistrano/configuration/variables.rb +127 -0
  30. data/lib/capistrano/errors.rb +15 -0
  31. data/lib/capistrano/extensions.rb +57 -0
  32. data/lib/capistrano/logger.rb +59 -0
  33. data/lib/capistrano/processable.rb +53 -0
  34. data/lib/capistrano/recipes/compat.rb +32 -0
  35. data/lib/capistrano/recipes/deploy.rb +438 -0
  36. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  37. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  38. data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
  39. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  40. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  41. data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
  42. data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
  43. data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
  44. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  45. data/lib/capistrano/recipes/deploy/scm/git.rb +274 -0
  46. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  47. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  48. data/lib/capistrano/recipes/deploy/scm/perforce.rb +138 -0
  49. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  50. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  51. data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
  52. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  53. data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
  54. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  55. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  56. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
  57. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  58. data/lib/capistrano/recipes/ext/rails-database-migrations.rb +50 -0
  59. data/lib/capistrano/recipes/ext/web-disable-enable.rb +40 -0
  60. data/lib/capistrano/recipes/standard.rb +37 -0
  61. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  62. data/lib/capistrano/recipes/upgrade.rb +33 -0
  63. data/lib/capistrano/role.rb +102 -0
  64. data/lib/capistrano/server_definition.rb +56 -0
  65. data/lib/capistrano/shell.rb +260 -0
  66. data/lib/capistrano/ssh.rb +99 -0
  67. data/lib/capistrano/task_definition.rb +70 -0
  68. data/lib/capistrano/transfer.rb +216 -0
  69. data/lib/capistrano/version.rb +18 -0
  70. data/setup.rb +1346 -0
  71. data/test/cli/execute_test.rb +132 -0
  72. data/test/cli/help_test.rb +165 -0
  73. data/test/cli/options_test.rb +317 -0
  74. data/test/cli/ui_test.rb +28 -0
  75. data/test/cli_test.rb +17 -0
  76. data/test/command_test.rb +286 -0
  77. data/test/configuration/actions/file_transfer_test.rb +61 -0
  78. data/test/configuration/actions/inspect_test.rb +65 -0
  79. data/test/configuration/actions/invocation_test.rb +224 -0
  80. data/test/configuration/callbacks_test.rb +220 -0
  81. data/test/configuration/connections_test.rb +349 -0
  82. data/test/configuration/execution_test.rb +175 -0
  83. data/test/configuration/loading_test.rb +132 -0
  84. data/test/configuration/namespace_dsl_test.rb +311 -0
  85. data/test/configuration/roles_test.rb +144 -0
  86. data/test/configuration/servers_test.rb +121 -0
  87. data/test/configuration/variables_test.rb +184 -0
  88. data/test/configuration_test.rb +88 -0
  89. data/test/deploy/local_dependency_test.rb +76 -0
  90. data/test/deploy/remote_dependency_test.rb +114 -0
  91. data/test/deploy/scm/accurev_test.rb +23 -0
  92. data/test/deploy/scm/base_test.rb +55 -0
  93. data/test/deploy/scm/git_test.rb +184 -0
  94. data/test/deploy/scm/mercurial_test.rb +129 -0
  95. data/test/deploy/scm/none_test.rb +35 -0
  96. data/test/deploy/strategy/copy_test.rb +258 -0
  97. data/test/extensions_test.rb +69 -0
  98. data/test/fixtures/cli_integration.rb +5 -0
  99. data/test/fixtures/config.rb +5 -0
  100. data/test/fixtures/custom.rb +3 -0
  101. data/test/logger_test.rb +123 -0
  102. data/test/role_test.rb +11 -0
  103. data/test/server_definition_test.rb +121 -0
  104. data/test/shell_test.rb +90 -0
  105. data/test/ssh_test.rb +104 -0
  106. data/test/task_definition_test.rb +101 -0
  107. data/test/transfer_test.rb +160 -0
  108. data/test/utils.rb +38 -0
  109. metadata +321 -0
@@ -0,0 +1,79 @@
1
+ require 'capistrano/recipes/deploy/dependencies'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module Strategy
6
+
7
+ # This class defines the abstract interface for all Capistrano
8
+ # deployment strategies. Subclasses must implement at least the
9
+ # #deploy! method.
10
+ class Base
11
+ attr_reader :configuration
12
+
13
+ # Instantiates a strategy with a reference to the given configuration.
14
+ def initialize(config={})
15
+ @configuration = config
16
+ end
17
+
18
+ # Executes the necessary commands to deploy the revision of the source
19
+ # code identified by the +revision+ variable. Additionally, this
20
+ # should write the value of the +revision+ variable to a file called
21
+ # REVISION, in the base of the deployed revision. This file is used by
22
+ # other tasks, to perform diffs and such.
23
+ def deploy!
24
+ raise NotImplementedError, "`deploy!' is not implemented by #{self.class.name}"
25
+ end
26
+
27
+ # Performs a check on the remote hosts to determine whether everything
28
+ # is setup such that a deploy could succeed.
29
+ def check!
30
+ Dependencies.new(configuration) do |d|
31
+ d.remote.directory(configuration[:releases_path]).or("`#{configuration[:releases_path]}' does not exist. Please run `cap deploy:setup'.")
32
+ d.remote.writable(configuration[:deploy_to]).or("You do not have permissions to write to `#{configuration[:deploy_to]}'.")
33
+ d.remote.writable(configuration[:releases_path]).or("You do not have permissions to write to `#{configuration[:releases_path]}'.")
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ # This is to allow helper methods like "run" and "put" to be more
40
+ # easily accessible to strategy implementations.
41
+ def method_missing(sym, *args, &block)
42
+ if configuration.respond_to?(sym)
43
+ configuration.send(sym, *args, &block)
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ # A wrapper for Kernel#system that logs the command being executed.
50
+ def system(*args)
51
+ cmd = args.join(' ')
52
+ if RUBY_PLATFORM =~ /win32/
53
+ cmd.gsub!('/','\\') # Replace / with \\
54
+ cmd.gsub!(/^cd /,'cd /D ') # Replace cd with cd /D
55
+ cmd.gsub!(/&& cd /,'&& cd /D ') # Replace cd with cd /D
56
+ logger.trace "executing locally: #{cmd}"
57
+ super(cmd)
58
+ else
59
+ logger.trace "executing locally: #{cmd}"
60
+ super
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def logger
67
+ @logger ||= configuration[:logger] || Capistrano::Logger.new(:output => STDOUT)
68
+ end
69
+
70
+ # The revision to deploy. Must return a real revision identifier,
71
+ # and not a pseudo-id.
72
+ def revision
73
+ configuration[:real_revision]
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,20 @@
1
+ require 'capistrano/recipes/deploy/strategy/remote'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module Strategy
6
+
7
+ # Implements the deployment strategy which does an SCM checkout on each
8
+ # target host. This is the default deployment strategy for Capistrano.
9
+ class Checkout < Remote
10
+ protected
11
+
12
+ # Returns the SCM's checkout command for the revision to deploy.
13
+ def command
14
+ @command ||= source.checkout(revision, configuration[:release_path])
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,210 @@
1
+ require 'capistrano/recipes/deploy/strategy/base'
2
+ require 'fileutils'
3
+ require 'tempfile' # Dir.tmpdir
4
+
5
+ module Capistrano
6
+ module Deploy
7
+ module Strategy
8
+
9
+ # This class implements the strategy for deployments which work
10
+ # by preparing the source code locally, compressing it, copying the
11
+ # file to each target host, and uncompressing it to the deployment
12
+ # directory.
13
+ #
14
+ # By default, the SCM checkout command is used to obtain the local copy
15
+ # of the source code. If you would rather use the export operation,
16
+ # you can set the :copy_strategy variable to :export.
17
+ #
18
+ # set :copy_strategy, :export
19
+ #
20
+ # For even faster deployments, you can set the :copy_cache variable to
21
+ # true. This will cause deployments to do a new checkout of your
22
+ # repository to a new directory, and then copy that checkout. Subsequent
23
+ # deploys will just resync that copy, rather than doing an entirely new
24
+ # checkout. Additionally, you can specify file patterns to exclude from
25
+ # the copy when using :copy_cache; just set the :copy_exclude variable
26
+ # to a file glob (or an array of globs).
27
+ #
28
+ # set :copy_cache, true
29
+ # set :copy_exclude, ".git/*"
30
+ #
31
+ # Note that :copy_strategy is ignored when :copy_cache is set. Also, if
32
+ # you want the copy cache put somewhere specific, you can set the variable
33
+ # to the path you want, instead of merely 'true':
34
+ #
35
+ # set :copy_cache, "/tmp/caches/myapp"
36
+ #
37
+ # This deployment strategy also supports a special variable,
38
+ # :copy_compression, which must be one of :gzip, :bz2, or
39
+ # :zip, and which specifies how the source should be compressed for
40
+ # transmission to each host.
41
+ class Copy < Base
42
+ # Obtains a copy of the source code locally (via the #command method),
43
+ # compresses it to a single file, copies that file to all target
44
+ # servers, and uncompresses it on each of them into the deployment
45
+ # directory.
46
+ def deploy!
47
+ if copy_cache
48
+ if File.exists?(copy_cache)
49
+ logger.debug "refreshing local cache to revision #{revision} at #{copy_cache}"
50
+ system(source.sync(revision, copy_cache))
51
+ else
52
+ logger.debug "preparing local cache at #{copy_cache}"
53
+ system(source.checkout(revision, copy_cache))
54
+ end
55
+
56
+ logger.debug "copying cache to deployment staging area #{destination}"
57
+ Dir.chdir(copy_cache) do
58
+ FileUtils.mkdir_p(destination)
59
+ queue = Dir.glob("*", File::FNM_DOTMATCH)
60
+ while queue.any?
61
+ item = queue.shift
62
+ name = File.basename(item)
63
+
64
+ next if name == "." || name == ".."
65
+ next if copy_exclude.any? { |pattern| File.fnmatch(pattern, item) }
66
+
67
+ if File.symlink?(item)
68
+ FileUtils.ln_s(File.readlink(File.join(copy_cache, item)), File.join(destination, item))
69
+ elsif File.directory?(item)
70
+ queue += Dir.glob("#{item}/*", File::FNM_DOTMATCH)
71
+ FileUtils.mkdir(File.join(destination, item))
72
+ else
73
+ FileUtils.ln(File.join(copy_cache, item), File.join(destination, item))
74
+ end
75
+ end
76
+ end
77
+ else
78
+ logger.debug "getting (via #{copy_strategy}) revision #{revision} to #{destination}"
79
+ system(command)
80
+
81
+ if copy_exclude.any?
82
+ logger.debug "processing exclusions..."
83
+ if copy_exclude.any?
84
+ copy_exclude.each do |pattern|
85
+ delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH)
86
+ # avoid the /.. trap that deletes the parent directories
87
+ delete_list.delete_if { |dir| dir =~ /\/\.\.$/ }
88
+ FileUtils.rm_rf(delete_list.compact)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
95
+
96
+ logger.trace "compressing #{destination} to #{filename}"
97
+ Dir.chdir(tmpdir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
98
+
99
+ upload(filename, remote_filename)
100
+ run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
101
+ ensure
102
+ FileUtils.rm filename rescue nil
103
+ FileUtils.rm_rf destination rescue nil
104
+ end
105
+
106
+ def check!
107
+ super.check do |d|
108
+ d.local.command(source.local.command) if source.local.command
109
+ d.local.command(compress(nil, nil).first)
110
+ d.remote.command(decompress(nil).first)
111
+ end
112
+ end
113
+
114
+ # Returns the location of the local copy cache, if the strategy should
115
+ # use a local cache + copy instead of a new checkout/export every
116
+ # time. Returns +nil+ unless :copy_cache has been set. If :copy_cache
117
+ # is +true+, a default cache location will be returned.
118
+ def copy_cache
119
+ @copy_cache ||= configuration[:copy_cache] == true ?
120
+ File.join(Dir.tmpdir, configuration[:application]) :
121
+ configuration[:copy_cache]
122
+ end
123
+
124
+ private
125
+
126
+ # Specify patterns to exclude from the copy. This is only valid
127
+ # when using a local cache.
128
+ def copy_exclude
129
+ @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
130
+ end
131
+
132
+ # Returns the basename of the release_path, which will be used to
133
+ # name the local copy and archive file.
134
+ def destination
135
+ @destination ||= File.join(tmpdir, File.basename(configuration[:release_path]))
136
+ end
137
+
138
+ # Returns the value of the :copy_strategy variable, defaulting to
139
+ # :checkout if it has not been set.
140
+ def copy_strategy
141
+ @copy_strategy ||= configuration.fetch(:copy_strategy, :checkout)
142
+ end
143
+
144
+ # Should return the command(s) necessary to obtain the source code
145
+ # locally.
146
+ def command
147
+ @command ||= case copy_strategy
148
+ when :checkout
149
+ source.checkout(revision, destination)
150
+ when :export
151
+ source.export(revision, destination)
152
+ end
153
+ end
154
+
155
+ # Returns the name of the file that the source code will be
156
+ # compressed to.
157
+ def filename
158
+ @filename ||= File.join(tmpdir, "#{File.basename(destination)}.#{compression.extension}")
159
+ end
160
+
161
+ # The directory to which the copy should be checked out
162
+ def tmpdir
163
+ @tmpdir ||= configuration[:copy_dir] || Dir.tmpdir
164
+ end
165
+
166
+ # The directory on the remote server to which the archive should be
167
+ # copied
168
+ def remote_dir
169
+ @remote_dir ||= configuration[:copy_remote_dir] || "/tmp"
170
+ end
171
+
172
+ # The location on the remote server where the file should be
173
+ # temporarily stored.
174
+ def remote_filename
175
+ @remote_filename ||= File.join(remote_dir, File.basename(filename))
176
+ end
177
+
178
+ # A struct for representing the specifics of a compression type.
179
+ # Commands are arrays, where the first element is the utility to be
180
+ # used to perform the compression or decompression.
181
+ Compression = Struct.new(:extension, :compress_command, :decompress_command)
182
+
183
+ # The compression method to use, defaults to :gzip.
184
+ def compression
185
+ type = configuration[:copy_compression] || :gzip
186
+ case type
187
+ when :gzip, :gz then Compression.new("tar.gz", %w(tar czf), %w(tar xzf))
188
+ when :bzip2, :bz2 then Compression.new("tar.bz2", %w(tar cjf), %w(tar xjf))
189
+ when :zip then Compression.new("zip", %w(zip -qr), %w(unzip -q))
190
+ else raise ArgumentError, "invalid compression type #{type.inspect}"
191
+ end
192
+ end
193
+
194
+ # Returns the command necessary to compress the given directory
195
+ # into the given file.
196
+ def compress(directory, file)
197
+ compression.compress_command + [file, directory]
198
+ end
199
+
200
+ # Returns the command necessary to decompress the given file,
201
+ # relative to the current working directory. It must also
202
+ # preserve the directory structure in the file.
203
+ def decompress(file)
204
+ compression.decompress_command + [file]
205
+ end
206
+ end
207
+
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,20 @@
1
+ require 'capistrano/recipes/deploy/strategy/remote'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module Strategy
6
+
7
+ # Implements the deployment strategy which does an SCM export on each
8
+ # target host.
9
+ class Export < Remote
10
+ protected
11
+
12
+ # Returns the SCM's export command for the revision to deploy.
13
+ def command
14
+ @command ||= source.export(revision, configuration[:release_path])
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,52 @@
1
+ require 'capistrano/recipes/deploy/strategy/base'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module Strategy
6
+
7
+ # An abstract superclass, which forms the base for all deployment
8
+ # strategies which work by grabbing the code from the repository directly
9
+ # from remote host. This includes deploying by checkout (the default),
10
+ # and deploying by export.
11
+ class Remote < Base
12
+ # Executes the SCM command for this strategy and writes the REVISION
13
+ # mark file to each host.
14
+ def deploy!
15
+ scm_run "#{command} && #{mark}"
16
+ end
17
+
18
+ def check!
19
+ super.check do |d|
20
+ d.remote.command(source.command)
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ # Runs the given command, filtering output back through the
27
+ # #handle_data filter of the SCM implementation.
28
+ def scm_run(command)
29
+ run(command) do |ch,stream,text|
30
+ ch[:state] ||= { :channel => ch }
31
+ output = source.handle_data(ch[:state], stream, text)
32
+ ch.send_data(output) if output
33
+ end
34
+ end
35
+
36
+ # An abstract method which must be overridden in subclasses, to
37
+ # return the actual SCM command(s) which must be executed on each
38
+ # target host in order to perform the deployment.
39
+ def command
40
+ raise NotImplementedError, "`command' is not implemented by #{self.class.name}"
41
+ end
42
+
43
+ # Returns the command which will write the identifier of the
44
+ # revision being deployed to the REVISION file on each host.
45
+ def mark
46
+ "(echo #{revision} > #{configuration[:release_path]}/REVISION)"
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ require 'capistrano/recipes/deploy/strategy/remote'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module Strategy
6
+
7
+ # Implements the deployment strategy that keeps a cached checkout of
8
+ # the source code on each remote server. Each deploy simply updates the
9
+ # cached checkout, and then does a copy from the cached copy to the
10
+ # final deployment location.
11
+ class RemoteCache < Remote
12
+ # Executes the SCM command for this strategy and writes the REVISION
13
+ # mark file to each host.
14
+ def deploy!
15
+ update_repository_cache
16
+ copy_repository_cache
17
+ end
18
+
19
+ def check!
20
+ super.check do |d|
21
+ d.remote.writable(shared_path)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def repository_cache
28
+ File.join(shared_path, configuration[:repository_cache] || "cached-copy")
29
+ end
30
+
31
+ def update_repository_cache
32
+ logger.trace "updating the cached checkout on all servers"
33
+ command = "if [ -d #{repository_cache} ]; then " +
34
+ "#{source.sync(revision, repository_cache)}; " +
35
+ "else #{source.checkout(revision, repository_cache)}; fi"
36
+ scm_run(command)
37
+ end
38
+
39
+ def copy_repository_cache
40
+ logger.trace "copying the cached version to #{configuration[:release_path]}"
41
+ if copy_exclude.empty?
42
+ run "cp -RPp #{repository_cache} #{configuration[:release_path]} && #{mark}"
43
+ else
44
+ exclusions = copy_exclude.map { |e| "--exclude=\"#{e}\"" }.join(' ')
45
+ run "rsync -lrpt #{exclusions} #{repository_cache}/* #{configuration[:release_path]} && #{mark}"
46
+ end
47
+ end
48
+
49
+ def copy_exclude
50
+ @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4
+
5
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
6
+
7
+ <head>
8
+ <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
9
+ <title>System down for maintenance</title>
10
+
11
+ <style type="text/css">
12
+ div.outer {
13
+ position: absolute;
14
+ left: 50%;
15
+ top: 50%;
16
+ width: 500px;
17
+ height: 300px;
18
+ margin-left: -260px;
19
+ margin-top: -150px;
20
+ }
21
+
22
+ .DialogBody {
23
+ margin: 0;
24
+ padding: 10px;
25
+ text-align: left;
26
+ border: 1px solid #ccc;
27
+ border-right: 1px solid #999;
28
+ border-bottom: 1px solid #999;
29
+ background-color: #fff;
30
+ }
31
+
32
+ body { background-color: #fff; }
33
+ </style>
34
+ </head>
35
+
36
+ <body>
37
+
38
+ <div class="outer">
39
+ <div class="DialogBody" style="text-align: center;">
40
+ <div style="text-align: center; width: 200px; margin: 0 auto;">
41
+ <p style="color: red; font-size: 16px; line-height: 20px;">
42
+ The system is down for <%= reason ? reason : "maintenance" %>
43
+ as of <%= Time.now.strftime("%H:%M %Z") %>.
44
+ </p>
45
+ <p style="color: #666;">
46
+ It'll be back <%= deadline ? deadline : "shortly" %>.
47
+ </p>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ </body>
53
+ </html>