engineyard-serverside 2.0.0.pre3 → 2.0.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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