Class: EY::Serverside::DeployBase

Inherits:
Task show all
Includes:
RailsAssetSupport
Defined in:
lib/engineyard-serverside/deploy.rb

Direct Known Subclasses

Deploy

Instance Attribute Summary

Attributes inherited from Task

#config, #servers, #shell

Instance Method Summary collapse

Methods included from RailsAssetSupport

#app_builds_own_assets?, #app_disables_assets?, #app_has_asset_task?, #app_needs_assets?, #bundled_rails_version, #compile_assets, #keep_existing_assets

Methods inherited from Task

#c, #initialize, #load_ey_yml, #paths, #require_custom_tasks, #roles, #run, #sudo

Constructor Details

This class inherits a constructor from EY::Serverside::Task

Instance Method Details

#bundleObject

task



210
211
212
213
214
215
# File 'lib/engineyard-serverside/deploy.rb', line 210

def bundle
  roles :app_master, :app, :solo, :util do
    check_ruby_bundler
    check_node_npm
  end
end

#cached_deployObject



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/engineyard-serverside/deploy.rb', line 20

def cached_deploy
  shell.status "Deploying app from cached copy at #{Time.now.asctime}"
  require_custom_tasks
  load_ey_yml
  push_code

  shell.status "Starting full deploy"
  copy_repository_cache
  check_repository

  with_failed_release_cleanup do
    create_revision_file
    run_with_callbacks(:bundle)
    setup_services
    check_for_ey_config
    symlink_configs
    setup_sqlite3_if_necessary
    run_with_callbacks(:compile_assets) # defined in RailsAssetSupport
    enable_maintenance_page
    run_with_callbacks(:migrate)
    callback(:before_symlink)
    # We don't use run_with_callbacks for symlink because we need
    # to clean up manually if it fails.
    symlink
  end

  callback(:after_symlink)
  run_with_callbacks(:restart)
  disable_maintenance_page

  cleanup_old_releases
  gc_repository_cache
  shell.status "Finished deploy at #{Time.now.asctime}"
rescue Exception
  shell.status "Finished failing to deploy at #{Time.now.asctime}"
  puts_deploy_failure
  raise
end

#callback(what) ⇒ Object



383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/engineyard-serverside/deploy.rb', line 383

def callback(what)
  @callbacks_reached ||= true
  if paths.deploy_hook(what).exist?
    shell.status "Running deploy hook: deploy/#{what}.rb"
    run Escape.shell_command(base_callback_command_for(what)) do |server, cmd|
      per_instance_args = []
      per_instance_args << '--current-roles' << server.roles.to_a.join(' ')
      per_instance_args << '--current-name'  << server.name.to_s if server.name
      per_instance_args << '--config'        << config.to_json
      cmd << " " << Escape.shell_command(per_instance_args)
    end
  end
end

#check_for_ey_configObject



83
84
85
86
87
88
89
90
# File 'lib/engineyard-serverside/deploy.rb', line 83

def check_for_ey_config
  if gemfile? && lockfile
    configured_services = parse_configured_services
    if !configured_services.empty? && !lockfile.has_ey_config?
      shell.warning "Gemfile.lock does not contain ey_config. Add it to get EY::Config access to: #{configured_services.keys.join(', ')}."
    end
  end
end

#check_repositoryObject



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/engineyard-serverside/deploy.rb', line 99

def check_repository
  if gemfile?
    shell.status "Gemfile found."
    if lockfile
      shell.status "Gemfile.lock found."
      if !config.ignore_database_adapter_warning? && has_database? && !lockfile.any_database_adapter?
        shell.warning "Gemfile.lock does not contain a recognized database adapter.\nA database-adapter gem such as pg, mysql2, mysql, or do_mysql was expected.\nThis can prevent applications that use PostgreSQL or MySQL from booting.\n\nTo fix, add any needed adapter to your Gemfile, bundle, commit, and redeploy.\nApplications not using PostgreSQL or MySQL can safely ignore this warning by\nadding ignore_database_adapter_warning: true to the application's ey.yml file\nunder this environment's name and adding the file to your repository.\n        WARN\n      end\n    else\n      shell.warning <<-WARN\nGemfile.lock is missing!\nYou can get different versions of gems in production than what you tested with.\nYou can get different versions of gems on every deployment even if your Gemfile hasn't changed.\nDeploying will take longer.\n\nTo fix this problem, commit your Gemfile.lock to your repository and redeploy.\n      WARN\n    end\n  else\n    shell.status \"No Gemfile. Deploying without bundler support.\"\n  end\nend\n"

#clean_environmentObject

GIT_SSH needs to be defined in the environment for customers with private bundler repos in their Gemfile.



177
178
179
# File 'lib/engineyard-serverside/deploy.rb', line 177

def clean_environment
  %Q[export GIT_SSH="#{ssh_executable}" && export LANG="en_US.UTF-8" && unset RUBYOPT BUNDLE_PATH BUNDLE_FROZEN BUNDLE_WITHOUT BUNDLE_BIN BUNDLE_GEMFILE]
end

#clean_release_directory(dir, count = 3) ⇒ Object

Remove all but the most-recent count releases from the specified release directory. IMPORTANT: This expects the release directory naming convention to be something with a sensible lexical order. Violate that at your peril.



227
228
229
230
231
232
233
# File 'lib/engineyard-serverside/deploy.rb', line 227

def clean_release_directory(dir, count = 3)
  @cleanup_failed = true
  ordinal = count.succ.to_s
  shell.status "Cleaning release directory: #{dir}"
  sudo "ls -r #{dir} | tail -n +#{ordinal} | xargs -I@ rm -rf #{dir}/@"
  @cleanup_failed = false
end

#cleanup_old_releasesObject

task



218
219
220
221
# File 'lib/engineyard-serverside/deploy.rb', line 218

def cleanup_old_releases
  clean_release_directory(paths.releases)
  clean_release_directory(paths.releases_failed)
end

#copy_repository_cacheObject

task



272
273
274
275
276
277
278
279
# File 'lib/engineyard-serverside/deploy.rb', line 272

def copy_repository_cache
  shell.status "Copying to #{paths.active_release}"
  exclusions = Array(config.copy_exclude).map { |e| %|--exclude="#{e}"| }.join(' ')
  run("mkdir -p #{paths.active_release} #{paths.releases_failed} #{paths.shared_config} && rsync -aq #{exclusions} #{paths.repository_cache}/ #{paths.active_release}")

  shell.status "Ensuring proper ownership."
  sudo("chown -R #{config.user}:#{config.group} #{paths.active_release} #{paths.releases_failed}")
end

#create_revision_fileObject



281
282
283
# File 'lib/engineyard-serverside/deploy.rb', line 281

def create_revision_file
  run create_revision_file_command
end

#create_revision_file_commandObject



67
68
69
# File 'lib/engineyard-serverside/deploy.rb', line 67

def create_revision_file_command
  strategy.create_revision_file_command(paths.active_release)
end

#deployObject

default task



14
15
16
17
18
# File 'lib/engineyard-serverside/deploy.rb', line 14

def deploy
  shell.status "Starting deploy at #{shell.start_time.asctime}"
  update_repository_cache
  cached_deploy
end

#disable_maintenance_pageObject



143
144
145
# File 'lib/engineyard-serverside/deploy.rb', line 143

def disable_maintenance_page
  maintenance.conditionally_disable
end

#enable_maintenance_pageObject



139
140
141
# File 'lib/engineyard-serverside/deploy.rb', line 139

def enable_maintenance_page
  maintenance.conditionally_enable
end

#gc_repository_cacheObject



63
64
65
# File 'lib/engineyard-serverside/deploy.rb', line 63

def gc_repository_cache
  strategy.gc_repository_cache
end

#generate_ssh_wrapperObject

We specify 'IdentitiesOnly' to avoid failures on systems with > 5 private keys available. We set UserKnownHostsFile to /dev/null because StrickHostKeyChecking no doesn't ignore existing entries in known_hosts; we want to actively ignore all such. Learned this at http://lists.mindrot.org/pipermail/openssh-unix-dev/2009-February/027271.html (Thanks Jim L.)



196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/engineyard-serverside/deploy.rb', line 196

def generate_ssh_wrapper
  path = paths.ssh_wrapper
  "mkdir -p \#{path.dirname}\n[[ -x \#{path} ]] || cat > \#{path} <<'SSH'\n#!/bin/sh\nunset SSH_AUTH_SOCK\nssh -o CheckHostIP=no -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o LogLevel=INFO -o IdentityFile=\#{paths.deploy_key} -o IdentitiesOnly=yes $*\nSSH\nchmod 0700 \#{path}\n  SCRIPT\nend\n"

#has_database?Boolean

The nodatabase.yml file is dropped by server configuration when there is no database in the cluster.

Returns:

  • (Boolean)


94
95
96
97
# File 'lib/engineyard-serverside/deploy.rb', line 94

def has_database?
  paths.shared_config.join('database.yml').exist? &&
    !paths.shared_config.join('nodatabase.yml').exist?
end

#migrateObject

task



261
262
263
264
265
266
267
268
269
# File 'lib/engineyard-serverside/deploy.rb', line 261

def migrate
  return unless config.migrate?
  @migrations_reached = true
  cmd = "cd #{paths.active_release} && PATH=#{paths.binstubs}:$PATH #{config.framework_envs} #{config.migration_command}"
  roles :app_master, :solo do
    shell.status "Migrating: #{cmd}"
    run(cmd)
  end
end

#parse_configured_servicesObject



75
76
77
78
79
80
81
# File 'lib/engineyard-serverside/deploy.rb', line 75

def parse_configured_services
  result = YAML.load_file "#{paths.shared_config}/ey_services_config_deploy.yml"
  return {} unless result.is_a?(Hash)
  result
rescue
  {}
end

#push_codeObject

task



154
155
156
157
158
159
160
# File 'lib/engineyard-serverside/deploy.rb', line 154

def push_code
  shell.status "Pushing code to all servers"
  servers.remote.run_on_each do |server|
    cmd = server.sync_directory_command(paths.repository_cache)
    shell.logged_system(cmd)
  end
end

#restartObject

task



163
164
165
166
167
168
169
170
# File 'lib/engineyard-serverside/deploy.rb', line 163

def restart
  @restart_failed = true
  shell.status "Restarting app servers"
  roles :app_master, :app, :solo do
    run(restart_command)
  end
  @restart_failed = false
end

#restart_commandObject



172
173
174
# File 'lib/engineyard-serverside/deploy.rb', line 172

def restart_command
  %{LANG="en_US.UTF-8" /engineyard/bin/app_#{config.app} deploy}
end

#restart_with_maintenance_pageObject



131
132
133
134
135
136
137
# File 'lib/engineyard-serverside/deploy.rb', line 131

def restart_with_maintenance_page
  require_custom_tasks
  load_ey_yml
  enable_maintenance_page
  restart
  disable_maintenance_page
end

#rollbackObject

task



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/engineyard-serverside/deploy.rb', line 236

def rollback
  if config.rollback_paths!
    begin
      rolled_back_release = paths.latest_release
      shell.status "Rolling back to previous release: #{short_log_message(config.active_revision)}"
      run_with_callbacks(:symlink)
      sudo "rm -rf #{rolled_back_release}"
      bundle
      shell.status "Restarting with previous release."
      enable_maintenance_page
      run_with_callbacks(:restart)
      disable_maintenance_page
      shell.status "Finished rollback at #{Time.now.asctime}"
    rescue Exception
      shell.status "Failed to rollback at #{Time.now.asctime}"
      puts_deploy_failure
      raise
    end
  else
    shell.fatal "Already at oldest release, nothing to roll back to."
    exit(1)
  end
end

#run_with_callbacks(task) ⇒ Object



147
148
149
150
151
# File 'lib/engineyard-serverside/deploy.rb', line 147

def run_with_callbacks(task)
  callback("before_#{task}")
  send(task)
  callback("after_#{task}")
end

#services_command_checkObject



285
286
287
# File 'lib/engineyard-serverside/deploy.rb', line 285

def services_command_check
  "which /usr/local/ey_resin/ruby/bin/ey-services-setup >/dev/null 2>&1"
end

#services_setup_commandObject



289
290
291
# File 'lib/engineyard-serverside/deploy.rb', line 289

def services_setup_command
  "/usr/local/ey_resin/ruby/bin/ey-services-setup #{config.app}"
end

#setup_servicesObject



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/engineyard-serverside/deploy.rb', line 293

def setup_services
  shell.status "Setting up external services."
  previously_configured_services = parse_configured_services

  begin
    sudo(services_command_check)
  rescue StandardError => e
    shell.info "Could not setup services. Upgrade your environment to get services configuration."
    return
  end

  begin
    sudo(services_setup_command)
  rescue StandardError => e
    unless previously_configured_services.empty?
      shell.warning "External services configuration not updated. Using previous version.\nDeploy again if your services configuration appears incomplete or out of date.\n\#{e}\n      WARNING\n    end\n  end\nend\n"

#setup_sqlite3_if_necessaryObject



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/engineyard-serverside/deploy.rb', line 317

def setup_sqlite3_if_necessary
  if gemfile? && lockfile && lockfile.uses_sqlite3?
    [
     ["Create databases directory if needed", "mkdir -p #{paths.shared}/databases"],
     ["Creating SQLite database if needed", "touch #{paths.shared}/databases/#{config.framework_env}.sqlite3"],
     ["Create config directory if needed", "mkdir -p #{paths.active_release_config}"],
     ["Generating SQLite config", "cat > \#{paths.shared_config}/database.sqlite3.yml<<'YML'\n\#{config.framework_env}:\n  adapter: sqlite3\n  database: \#{paths.shared}/databases/\#{config.framework_env}.sqlite3\n  pool: 5\n  timeout: 5000\nYML\n"],
     ["Symlink database.yml", "ln -nfs #{paths.shared_config}/database.sqlite3.yml #{paths.active_release_config}/database.yml"],
    ].each do |what, cmd|
      shell.status "#{what}"
      run(cmd)
    end

    owner = [config.user, config.group].join(':')
    shell.status "Setting ownership to #{owner}"
    sudo "chown -R #{owner} #{paths.active_release}"
  end
end

#short_log_message(revision) ⇒ Object



71
72
73
# File 'lib/engineyard-serverside/deploy.rb', line 71

def short_log_message(revision)
  strategy.short_log_message(revision)
end

#ssh_executableObject

create ssh wrapper on all servers



182
183
184
185
186
187
188
189
# File 'lib/engineyard-serverside/deploy.rb', line 182

def ssh_executable
  @ssh_executable ||= begin
                        roles :app_master, :app, :solo, :util do
                          run(generate_ssh_wrapper)
                        end
                        paths.ssh_wrapper
                      end
end

task



373
374
375
376
377
378
379
380
381
# File 'lib/engineyard-serverside/deploy.rb', line 373

def symlink
  shell.status "Symlinking code."
  run "rm -f #{paths.current} && ln -nfs #{paths.active_release} #{paths.current} && find #{paths.current} -not -user #{config.user} -or -not -group #{config.group} -exec chown #{config.user}:#{config.group} {} +"
  @symlink_changed = true
rescue Exception
  sudo "rm -f #{paths.current} && ln -nfs #{paths.previous_release(paths.active_release)} #{paths.current} && chown -R #{config.user}:#{config.group} #{paths.current}"
  @symlink_changed = false
  raise
end


344
345
346
347
348
349
350
351
352
353
# File 'lib/engineyard-serverside/deploy.rb', line 344

def symlink_configs
  shell.status "Preparing shared resources for release."
  symlink_tasks.each do |what, cmd|
    shell.substatus what
    run(cmd)
  end
  owner = [config.user, config.group].join(':')
  shell.status "Setting ownership to #{owner}"
  sudo "chown -R #{owner} #{paths.active_release}"
end


355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/engineyard-serverside/deploy.rb', line 355

def symlink_tasks
  [
    ["Set group write permissions",           "chmod -R g+w #{paths.active_release}"],
    ["Remove symlinked shared directories",   "rm -rf #{paths.active_log} #{paths.public_system} #{paths.active_release}/tmp/pids"],
    ["Create tmp directory",                  "mkdir -p #{paths.active_release}/tmp"],
    ["Create public directory",               "mkdir -p #{paths.public}"],
    ["Create config directory",               "mkdir -p #{paths.active_release_config}"],
    ["Symlink shared log directory",          "ln -nfs #{paths.shared_log} #{paths.active_log}"],
    ["Symlink public system directory",       "ln -nfs #{paths.shared_system} #{paths.public_system}"],
    ["Symlink shared pids directory",         "ln -nfs #{paths.shared}/pids #{paths.active_release}/tmp/pids"],
    ["Symlink other shared config files",     "find #{paths.shared_config} -type f -not -name 'database.yml' -exec ln -s {} #{paths.active_release_config} \\;"],
    ["Symlink database.yml if needed",        "if [ -f \"#{paths.shared_config}/database.yml\" ]; then ln -nfs #{paths.shared_config}/database.yml #{paths.active_release_config}/database.yml; fi"],
    ["Symlink newrelic.yml if needed",        "if [ -f \"#{paths.shared_config}/newrelic.yml\" ]; then ln -nfs #{paths.shared_config}/newrelic.yml #{paths.active_release_config}/newrelic.yml; fi"],
    ["Symlink mongrel_cluster.yml if needed", "if [ -f \"#{paths.shared_config}/mongrel_cluster.yml\" ]; then ln -nfs #{paths.shared_config}/mongrel_cluster.yml #{paths.active_release_config}/mongrel_cluster.yml; fi"],
  ]
end

#update_repository_cacheObject



59
60
61
# File 'lib/engineyard-serverside/deploy.rb', line 59

def update_repository_cache
  strategy.update_repository_cache
end