engineyard-serverside 2.0.0.pre3 → 2.0.0.pre4

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.
@@ -44,7 +44,7 @@ module EY
44
44
 
45
45
  callback(:after_symlink)
46
46
  run_with_callbacks(:restart)
47
- disable_maintenance_page
47
+ conditionally_disable_maintenance_page
48
48
 
49
49
  cleanup_old_releases
50
50
  shell.status "Finished deploy at #{Time.now.asctime}"
@@ -67,7 +67,7 @@ module EY
67
67
  end
68
68
 
69
69
  def parse_configured_services
70
- result = YAML.load_file "#{c.shared_path}/config/ey_services_config_deploy.yml"
70
+ result = YAML.load_file "#{c.paths.shared_config}/ey_services_config_deploy.yml"
71
71
  return {} unless result.is_a?(Hash)
72
72
  result
73
73
  rescue
@@ -138,6 +138,7 @@ To fix this problem, commit your Gemfile.lock to your repository and redeploy.
138
138
  File.exists?(file)
139
139
  end
140
140
 
141
+ shell.status "Enabling maintenance page."
141
142
  @maintenance_up = true
142
143
  roles :app_master, :app, :solo do
143
144
  run Escape.shell_command(['mkdir', '-p', File.dirname(c.maintenance_page_enabled_path)])
@@ -148,22 +149,49 @@ To fix this problem, commit your Gemfile.lock to your repository and redeploy.
148
149
  def conditionally_enable_maintenance_page
149
150
  if c.enable_maintenance_page?
150
151
  enable_maintenance_page
152
+ else
153
+ explain_not_enabling_maintenance_page
151
154
  end
152
155
  end
153
156
 
154
- def disable_maintenance_page
155
- if c.enable_maintenance_page?
156
- @maintenance_up = false
157
- roles :app_master, :app, :solo do
158
- run "rm -f #{c.maintenance_page_enabled_path}"
157
+ def explain_not_enabling_maintenance_page
158
+ if c.migrate?
159
+ if !c.maintenance_on_migrate? && !c.maintenance_on_restart?
160
+ shell.status "Skipping maintenance page. (maintenance_on_migrate is false in ey.yml)"
161
+ shell.notice "[Caution] No maintenance migrations must be non-destructive!"
162
+ shell.notice "Requests may be served during a partially migrated state."
159
163
  end
160
164
  else
161
- if File.exists?(c.maintenance_page_enabled_path)
162
- shell.notice "[Attention] Maintenance page is still up. You must remove it manually."
165
+ if c.required_downtime_stack? && !c.maintenance_on_restart?
166
+ shell.status "Skipping maintenance page. (maintenance_on_restart is false in ey.yml, overriding recommended default)"
167
+ unless File.exist?(c.maintenance_page_enabled_path)
168
+ shell.warning <<-WARN
169
+ No maintenance page! Brief downtime may be possible during restart.
170
+ This application stack does not support no-downtime restarts.
171
+ WARN
172
+ end
173
+ elsif !c.required_downtime_stack?
174
+ shell.status "Skipping maintenance page. (no-downtime restarts supported)"
163
175
  end
164
176
  end
165
177
  end
166
178
 
179
+ def disable_maintenance_page
180
+ shell.status "Removing maintenance page."
181
+ @maintenance_up = false
182
+ roles :app_master, :app, :solo do
183
+ run "rm -f #{c.maintenance_page_enabled_path}"
184
+ end
185
+ end
186
+
187
+ def conditionally_disable_maintenance_page
188
+ if c.disable_maintenance_page?
189
+ disable_maintenance_page
190
+ elsif File.exists?(c.maintenance_page_enabled_path)
191
+ shell.notice "[Attention] Maintenance page is still up.\nYou must remove it manually using `ey web enable`."
192
+ end
193
+ end
194
+
167
195
  def run_with_callbacks(task)
168
196
  callback("before_#{task}")
169
197
  send(task)
@@ -173,7 +201,7 @@ To fix this problem, commit your Gemfile.lock to your repository and redeploy.
173
201
  # task
174
202
  def push_code
175
203
  shell.status "Pushing code to all servers"
176
- commands = EY::Serverside::Server.all.reject { |server| server.local? }.map do |server|
204
+ commands = servers.remote.map do |server|
177
205
  cmd = server.sync_directory_command(config.repository_cache)
178
206
  proc { shell.logged_system(cmd) }
179
207
  end
@@ -229,7 +257,7 @@ chmod 0700 #{path}
229
257
  end
230
258
 
231
259
  def ssh_wrapper_path
232
- "#{c.shared_path}/config/#{c.app}-ssh-wrapper"
260
+ "#{c.paths.shared_config}/#{c.app}-ssh-wrapper"
233
261
  end
234
262
 
235
263
  # task
@@ -260,20 +288,23 @@ chmod 0700 #{path}
260
288
 
261
289
  # task
262
290
  def rollback
263
- if c.all_releases.size > 1
264
- rolled_back_release = c.latest_release
265
- c.release_path = c.previous_release(rolled_back_release)
266
-
267
- revision = File.read(File.join(c.release_path, 'REVISION')).strip
268
- shell.status "Rolling back to previous release: #{short_log_message(revision)}"
269
-
270
- run_with_callbacks(:symlink)
271
- sudo "rm -rf #{rolled_back_release}"
272
- bundle
273
- shell.status "Restarting with previous release."
274
- with_maintenance_page { run_with_callbacks(:restart) }
291
+ if c.rollback_paths!
292
+ begin
293
+ rolled_back_release = c.paths.latest_release
294
+ shell.status "Rolling back to previous release: #{short_log_message(c.active_revision)}"
295
+ run_with_callbacks(:symlink)
296
+ sudo "rm -rf #{rolled_back_release}"
297
+ bundle
298
+ shell.status "Restarting with previous release."
299
+ with_maintenance_page { run_with_callbacks(:restart) }
300
+ shell.status "Finished rollback at #{Time.now.asctime}"
301
+ rescue Exception
302
+ shell.status "Failed to rollback at #{Time.now.asctime}"
303
+ puts_deploy_failure
304
+ raise
305
+ end
275
306
  else
276
- shell.status "Already at oldest release, nothing to roll back to."
307
+ shell.fatal "Already at oldest release, nothing to roll back to."
277
308
  exit(1)
278
309
  end
279
310
  end
@@ -282,8 +313,8 @@ chmod 0700 #{path}
282
313
  def migrate
283
314
  return unless c.migrate?
284
315
  @migrations_reached = true
316
+ cmd = "cd #{c.release_path} && PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} #{c.migration_command}"
285
317
  roles :app_master, :solo do
286
- cmd = "cd #{c.release_path} && PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} #{c.migration_command}"
287
318
  shell.status "Migrating: #{cmd}"
288
319
  run(cmd)
289
320
  end
@@ -292,7 +323,8 @@ chmod 0700 #{path}
292
323
  # task
293
324
  def copy_repository_cache
294
325
  shell.status "Copying to #{c.release_path}"
295
- run("mkdir -p #{c.release_path} #{c.failed_release_dir} && rsync -aq #{c.exclusions} #{c.repository_cache}/ #{c.release_path}")
326
+ exclusions = Array(c.copy_exclude).map { |e| %|--exclude="#{e}"| }.join(' ')
327
+ run("mkdir -p #{c.release_path} #{c.failed_release_dir} #{c.paths.shared_config} && rsync -aq #{exclusions} #{c.repository_cache}/ #{c.release_path}")
296
328
 
297
329
  shell.status "Ensuring proper ownership."
298
330
  sudo("chown -R #{c.user}:#{c.group} #{c.release_path} #{c.failed_release_dir}")
@@ -337,7 +369,7 @@ Deploy again if your services configuration appears incomplete or out of date.
337
369
  ["Creating SQLite database if needed", "touch #{c.shared_path}/databases/#{c.framework_env}.sqlite3"],
338
370
  ["Create config directory if needed", "mkdir -p #{c.release_path}/config"],
339
371
  ["Generating SQLite config", <<-WRAP],
340
- cat > #{c.shared_path}/config/database.sqlite3.yml<<'YML'
372
+ cat > #{c.paths.shared_config}/database.sqlite3.yml<<'YML'
341
373
  #{c.framework_env}:
342
374
  adapter: sqlite3
343
375
  database: #{c.shared_path}/databases/#{c.framework_env}.sqlite3
@@ -345,7 +377,7 @@ cat > #{c.shared_path}/config/database.sqlite3.yml<<'YML'
345
377
  timeout: 5000
346
378
  YML
347
379
  WRAP
348
- ["Symlink database.yml", "ln -nfs #{c.shared_path}/config/database.sqlite3.yml #{c.release_path}/config/database.yml"],
380
+ ["Symlink database.yml", "ln -nfs #{c.paths.shared_config}/database.sqlite3.yml #{c.release_path}/config/database.yml"],
349
381
  ].each do |what, cmd|
350
382
  shell.status "#{what}"
351
383
  run(cmd)
@@ -373,15 +405,15 @@ WRAP
373
405
  ["Set group write permissions", "chmod -R g+w #{release_to_link}"],
374
406
  ["Remove revision-tracked shared directories from deployment", "rm -rf #{release_to_link}/log #{release_to_link}/public/system #{release_to_link}/tmp/pids"],
375
407
  ["Create tmp directory", "mkdir -p #{release_to_link}/tmp"],
376
- ["Symlink shared log directory", "ln -nfs #{c.shared_path}/log #{release_to_link}/log"],
408
+ ["Symlink shared log directory", "ln -nfs #{c.paths.shared_log} #{release_to_link}/log"],
377
409
  ["Create public directory if needed", "mkdir -p #{release_to_link}/public"],
378
410
  ["Create config directory if needed", "mkdir -p #{release_to_link}/config"],
379
- ["Create system directory if needed", "ln -nfs #{c.shared_path}/system #{release_to_link}/public/system"],
411
+ ["Create system directory if needed", "ln -nfs #{c.paths.shared_system} #{release_to_link}/public/system"],
380
412
  ["Symlink shared pids directory", "ln -nfs #{c.shared_path}/pids #{release_to_link}/tmp/pids"],
381
- ["Symlink other shared config files", "find #{c.shared_path}/config -type f -not -name 'database.yml' -exec ln -s {} #{release_to_link}/config \\;"],
382
- ["Symlink mongrel_cluster.yml", "ln -nfs #{c.shared_path}/config/mongrel_cluster.yml #{release_to_link}/config/mongrel_cluster.yml"],
383
- ["Symlink database.yml", "ln -nfs #{c.shared_path}/config/database.yml #{release_to_link}/config/database.yml"],
384
- ["Symlink newrelic.yml if needed", "if [ -f \"#{c.shared_path}/config/newrelic.yml\" ]; then ln -nfs #{c.shared_path}/config/newrelic.yml #{release_to_link}/config/newrelic.yml; fi"],
413
+ ["Symlink other shared config files", "find #{c.paths.shared_config} -type f -not -name 'database.yml' -exec ln -s {} #{release_to_link}/config \\;"],
414
+ ["Symlink mongrel_cluster.yml", "ln -nfs #{c.paths.shared_config}/mongrel_cluster.yml #{release_to_link}/config/mongrel_cluster.yml"],
415
+ ["Symlink database.yml", "ln -nfs #{c.paths.shared_config}/database.yml #{release_to_link}/config/database.yml"],
416
+ ["Symlink newrelic.yml if needed", "if [ -f \"#{c.paths.shared_config}/newrelic.yml\" ]; then ln -nfs #{c.paths.shared_config}/newrelic.yml #{release_to_link}/config/newrelic.yml; fi"],
385
417
  ]
386
418
  end
387
419
 
@@ -402,7 +434,7 @@ WRAP
402
434
  shell.status "Running deploy hook: deploy/#{what}.rb"
403
435
  run Escape.shell_command(base_callback_command_for(what)) do |server, cmd|
404
436
  per_instance_args = []
405
- per_instance_args << '--current-roles' << server.roles.join(' ')
437
+ per_instance_args << '--current-roles' << server.roles.to_a.join(' ')
406
438
  per_instance_args << '--current-name' << server.name.to_s if server.name
407
439
  per_instance_args << '--config' << c.to_json
408
440
  cmd << " " << Escape.shell_command(per_instance_args)
@@ -438,7 +470,7 @@ WRAP
438
470
  cmd << '--environment-name' << config.environment_name
439
471
  cmd << '--account-name' << config.account_name
440
472
  cmd << '--release-path' << config.release_path.to_s
441
- cmd << '--framework-env' << config.environment.to_s
473
+ cmd << '--framework-env' << config.framework_env.to_s
442
474
  cmd << '--verbose' if config.verbose
443
475
  cmd
444
476
  end
@@ -472,7 +504,7 @@ WRAP
472
504
  def with_maintenance_page
473
505
  conditionally_enable_maintenance_page
474
506
  yield if block_given?
475
- disable_maintenance_page
507
+ conditionally_disable_maintenance_page
476
508
  end
477
509
 
478
510
  def with_failed_release_cleanup
@@ -2,15 +2,22 @@ require 'engineyard-serverside/shell/helpers'
2
2
 
3
3
  module EY
4
4
  module Serverside
5
- class DeployHook < Task
5
+ class DeployHook
6
+ def initialize(config, shell, hook_name)
7
+ @config, @shell, @hook_name = config, shell, hook_name
8
+ end
9
+
10
+ def hook_path
11
+ "#{@config.release_path}/deploy/#{@hook_name}.rb"
12
+ end
13
+
6
14
  def callback_context
7
- @context ||= CallbackContext.new(config, shell)
15
+ @context ||= CallbackContext.new(@config, @shell, hook_path)
8
16
  end
9
17
 
10
- def run(hook)
11
- hook_path = "#{c.release_path}/deploy/#{hook}.rb"
18
+ def call
12
19
  if File.exist?(hook_path)
13
- Dir.chdir(c.release_path) do
20
+ Dir.chdir(@config.release_path) do
14
21
  if desc = syntax_error(hook_path)
15
22
  hook_name = File.basename(hook_path)
16
23
  abort "*** [Error] Invalid Ruby syntax in hook: #{hook_name} ***\n*** #{desc.chomp} ***"
@@ -23,6 +30,19 @@ module EY
23
30
 
24
31
  def eval_hook(code)
25
32
  callback_context.instance_eval(code)
33
+ rescue Exception => exception
34
+ display_hook_error(exception, code, hook_path)
35
+ raise exception
36
+ end
37
+
38
+ def display_hook_error(exception, code, hook_path)
39
+ @shell.fatal <<-ERROR
40
+ Exception raised in deploy hook #{hook_path.inspect}.
41
+
42
+ #{exception.class}: #{exception.to_s}
43
+
44
+ Please fix this error before retrying.
45
+ ERROR
26
46
  end
27
47
 
28
48
  def syntax_error(file)
@@ -35,17 +55,22 @@ module EY
35
55
 
36
56
  attr_reader :shell
37
57
 
38
- def initialize(config, shell)
58
+ def initialize(config, shell, hook_path)
39
59
  @configuration = config
40
60
  @configuration.set_framework_envs
41
61
  @shell = shell
42
62
  @node = node
63
+ @hook_path = hook_path
43
64
  end
44
65
 
45
66
  def config
46
67
  @configuration
47
68
  end
48
69
 
70
+ def inspect
71
+ "#<DeployHook::CallbackContext #{hook_path.inspect}>"
72
+ end
73
+
49
74
  def method_missing(meth, *args, &blk)
50
75
  if @configuration.respond_to?(meth)
51
76
  @configuration.send(meth, *args, &blk)
@@ -0,0 +1,106 @@
1
+ require 'pathname'
2
+
3
+ module EY
4
+ module Serverside
5
+ class Paths
6
+
7
+ module LegacyHelpers
8
+ def deploy_to() paths.deploy_root.to_s end
9
+ def release_dir() paths.releases.to_s end
10
+ def failed_release_dir() paths.releases_failed.to_s end
11
+ def release_path() paths.active_release.to_s end
12
+ def all_releases() paths.all_releases.map { |path| path.to_s } end
13
+ def previous_release(*a) paths.previous_release(*a).to_s end
14
+ def latest_release() paths.latest_release.to_s end
15
+ def current_path() paths.current.to_s end
16
+ def shared_path() paths.shared.to_s end
17
+ def maintenance_page_enabled_path() paths.enabled_maintenance_page.to_s end
18
+ def repository_cache() paths.repository_cache.to_s end
19
+ def bundled_gems_path() paths.bundled_gems.to_s end
20
+ def ruby_version_file() paths.ruby_version.to_s end
21
+ def system_version_file() paths.system_version.to_s end
22
+ def binstubs_path() paths.binstubs.to_s end
23
+ def gemfile_path() paths.gemfile.to_s end
24
+ def active_revision() paths.active_revision.read.strip end
25
+ def latest_revision() paths.latest_revision.read.strip end
26
+ alias revision latest_revision
27
+ def ssh_identity_file() paths.ssh_identity.to_s end
28
+ end
29
+
30
+ def self.def_path(name, parts)
31
+ define_method(name.to_sym) { path(*parts) }
32
+ end
33
+
34
+ def path(root, *parts)
35
+ send(root).join(*parts)
36
+ end
37
+
38
+ attr_reader :home, :deploy_root
39
+
40
+ def_path :current, [:deploy_root, 'current']
41
+ def_path :releases, [:deploy_root, 'releases']
42
+ def_path :releases_failed, [:deploy_root, 'releases_failed']
43
+ def_path :shared, [:deploy_root, 'shared']
44
+ def_path :shared_log, [:shared, 'log']
45
+ def_path :shared_config, [:shared, 'config']
46
+ def_path :shared_system, [:shared, 'system']
47
+ def_path :enabled_maintenance_page, [:shared_system, 'maintenance.html']
48
+ def_path :bundled_gems, [:shared, 'bundled_gems']
49
+ def_path :ruby_version, [:bundled_gems, 'RUBY_VERSION']
50
+ def_path :system_version, [:bundled_gems, 'SYSTEM_VERSION']
51
+ def_path :latest_revision, [:latest_release, 'REVISION']
52
+ def_path :active_revision, [:active_release, 'REVISION']
53
+ def_path :binstubs, [:active_release, 'ey_bundler_binstubs']
54
+ def_path :gemfile, [:active_release, 'Gemfile']
55
+
56
+ def initialize(opts)
57
+ @opts = opts
58
+ @home = Pathname.new(@opts[:hame] || ENV['HOME'])
59
+ @app_name = @opts[:app_name]
60
+ @active_release = Pathname.new(@opts[:active_release]) if @opts[:active_release]
61
+ @repository_cache = Pathname.new(@opts[:repository_cache]) if @opts[:repository_cache]
62
+ @deploy_root = Pathname.new(@opts[:deploy_root] || "/data/#{@app_name}")
63
+ end
64
+
65
+ def ssh_identity
66
+ path(:home, '.ssh', "#{@app_name}-deploy-key")
67
+ end
68
+
69
+ def repository_cache
70
+ @repository_cache ||= path(:shared, 'cached-copy')
71
+ end
72
+
73
+ def active_release
74
+ @active_release ||= path(:releases, Time.now.utc.strftime("%Y%m%d%H%M%S"))
75
+ end
76
+
77
+ def all_releases
78
+ @all_releases ||= Pathname.glob(releases.join('*')).sort
79
+ end
80
+
81
+ # deploy_root/releases/<release before argument release path>
82
+ def previous_release(current=latest_release)
83
+ index = all_releases.index(current)
84
+ if index && index > 0
85
+ all_releases[index-1]
86
+ else
87
+ nil
88
+ end
89
+ end
90
+
91
+ # deploy_root/releases/<latest timestamp>
92
+ def latest_release
93
+ all_releases.last
94
+ end
95
+
96
+ def rollback
97
+ if previous_release
98
+ self.class.new(@opts.dup.merge(:active_release => previous_release))
99
+ else
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
@@ -1,69 +1,27 @@
1
- require 'open-uri'
1
+ require 'set'
2
2
 
3
3
  module EY
4
4
  module Serverside
5
5
  class Server < Struct.new(:hostname, :roles, :name, :user)
6
- class DuplicateHostname < StandardError
7
- def initialize(hostname)
8
- super "There is already an EY::Serverside::Server with hostname '#{hostname}'"
9
- end
6
+ def self.from_hash(server_hash)
7
+ new(server_hash[:hostname], Set.new(server_hash[:roles].map{|r|r.to_sym}), server_hash[:name], server_hash[:user])
10
8
  end
11
9
 
12
10
  def initialize(*fields)
13
11
  super
14
- self.roles = self.roles.map { |r| r.to_sym } if self.roles
15
- end
16
-
17
- attr_writer :default_task
18
-
19
- def self.from_roles(*want_roles)
20
- want_roles = want_roles.flatten.compact.map{|r| r.to_sym}
21
- return all if !want_roles || want_roles.include?(:all) || want_roles.empty?
22
-
23
- all.select do |s|
24
- !(s.roles & want_roles).empty?
25
- end
26
12
  end
27
13
 
28
14
  def role
29
15
  roles.first
30
16
  end
31
17
 
32
- def self.load_all_from_array(server_hashes)
33
- server_hashes.each do |instance_hash|
34
- add(instance_hash)
35
- end
36
- end
37
-
38
- def self.all
39
- @all
40
- end
41
-
42
- def self.by_hostname(hostname)
43
- all.find{|s| s.hostname == hostname}
44
- end
45
-
46
- def self.add(server_hash)
47
- hostname = server_hash[:hostname]
48
- if by_hostname(hostname)
49
- raise DuplicateHostname.new(hostname)
50
- end
51
- server = new(hostname, server_hash[:roles], server_hash[:name], server_hash[:user])
52
- @all << server
53
- server
54
- end
55
-
56
- def self.current
57
- all.find {|s| s.local? }
18
+ def matches_roles?(set)
19
+ (roles & set).any?
58
20
  end
59
21
 
60
- def self.reset
61
- @all = []
62
- end
63
- reset
64
-
65
22
  def roles=(roles)
66
- super(roles.map{|r| r.to_sym})
23
+ roles_set = Set.new roles.map{|r| r.to_sym}
24
+ super roles_set
67
25
  end
68
26
 
69
27
  def local?
@@ -102,16 +60,6 @@ module EY
102
60
  @known_hosts_file ||= Tempfile.new('ey-ss-known-hosts')
103
61
  end
104
62
 
105
- # Make a known hosts tempfile to absorb host fingerprints so we don't show
106
- #
107
- # Warning: Permanently added 'xxx' (RSA) to the list of known hosts.
108
- #
109
- # for every ssh command.
110
- # (even with StrictHostKeyChecking=no, the warning output is annoying)
111
- def self.known_hosts_file
112
- @known_hosts_file ||= Tempfile.new('ey-ss-known-hosts')
113
- end
114
-
115
63
  def ssh_command
116
64
  "ssh -i #{ENV['HOME']}/.ssh/internal -o StrictHostKeyChecking=no -o UserKnownHostsFile=#{self.class.known_hosts_file.path} -o PasswordAuthentication=no "
117
65
  end