Class: EY::Serverside::DeployBase

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

Direct Known Subclasses

Deploy

Instance Attribute Summary

Attributes inherited from Task

#config

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 included from LoggedOutput

#debug, #info, logfile, logfile=, #logged_system, verbose=, verbose?, #verbose?, #warning

Methods inherited from Task

#initialize, #require_custom_tasks, #roles, #run, #sudo

Constructor Details

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

Instance Method Details

#bundleObject

task



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

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
# File 'lib/engineyard-serverside/deploy.rb', line 20

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

  info "~> 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
    conditionally_enable_maintenance_page
    run_with_callbacks(:migrate)
    run_with_callbacks(:compile_assets) # defined in RailsAssetSupport
    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
  debug "Finished deploy at #{Time.now.asctime}"
rescue Exception
  debug "Finished failing to deploy at #{Time.now.asctime}"
  puts_deploy_failure
  raise
end

#callback(what) ⇒ Object



393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/engineyard-serverside/deploy.rb', line 393

def callback(what)
  @callbacks_reached ||= true
  if File.exist?("#{c.release_path}/deploy/#{what}.rb")
    run Escape.shell_command(base_callback_command_for(what)) do |server, cmd|
      per_instance_args = [
        '--current-roles', server.roles.join(' '),
        '--config', c.to_json,
      ]
      per_instance_args << '--current-name' << server.name.to_s if server.name
      cmd << " " << Escape.shell_command(per_instance_args)
    end
  end
end

#check_for_ey_configObject



65
66
67
68
69
70
71
72
# File 'lib/engineyard-serverside/deploy.rb', line 65

def check_for_ey_config
  if gemfile? && lockfile
    configured_services = parse_configured_services
    if !configured_services.empty? && !lockfile.has_ey_config?
      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



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/engineyard-serverside/deploy.rb', line 74

def check_repository
  if gemfile?
    info "~> Gemfile found."
    if lockfile
      info "~> Gemfile.lock found."
      unless lockfile.any_database_adapter?
        warning <<-WARN
Gemfile.lock does not contain a recognized database adapter.
A database-adapter gem such as mysql2, mysql, or do_mysql was expected.
This can prevent applications that use MySQL or PostreSQL from booting.

To fix, add any needed adapter to your Gemfile, bundle, commit, and redeploy.
Applications that don't use MySQL or PostgreSQL can safely ignore this warning.
        WARN
      end
    else
      warning <<-WARN
Gemfile.lock is missing!
You can get different versions of gems in production than what you tested with.
You can get different versions of gems on every deployment even if your Gemfile hasn't changed.
Deploying will take longer.

To fix this problem, commit your Gemfile.lock to your repository and redeploy.
      WARN
    end
  else
    info "~> No Gemfile. Deploying without bundler support."
  end
end

#clean_environmentObject

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



188
189
190
# File 'lib/engineyard-serverside/deploy.rb', line 188

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.



243
244
245
246
247
248
249
# File 'lib/engineyard-serverside/deploy.rb', line 243

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

#cleanup_old_releasesObject

task



234
235
236
237
# File 'lib/engineyard-serverside/deploy.rb', line 234

def cleanup_old_releases
  clean_release_directory(c.release_dir)
  clean_release_directory(c.failed_release_dir)
end

#conditionally_enable_maintenance_pageObject



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

def conditionally_enable_maintenance_page
  if c.migrate? || required_downtime_stack?
    enable_maintenance_page
  end
end

#copy_repository_cacheObject

task



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

def copy_repository_cache
  info "~> Copying to #{c.release_path}"
  run("mkdir -p #{c.release_path} #{c.failed_release_dir} && rsync -aq #{c.exclusions} #{c.repository_cache}/ #{c.release_path}")

  info "~> Ensuring proper ownership."
  sudo("chown -R #{c.user}:#{c.group} #{c.deploy_to}")
end

#create_revision_fileObject



291
292
293
# File 'lib/engineyard-serverside/deploy.rb', line 291

def create_revision_file
  run create_revision_file_command
end

#deployObject

default task



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

def deploy
  debug "Starting deploy at #{Time.now.asctime}"
  update_repository_cache
  cached_deploy
end

#disable_maintenance_pageObject



151
152
153
154
155
156
# File 'lib/engineyard-serverside/deploy.rb', line 151

def disable_maintenance_page
  @maintenance_up = false
  roles :app_master, :app, :solo do
    run "rm -f #{File.join(c.shared_path, "system", "maintenance.html")}"
  end
end

#enable_maintenance_pageObject



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/engineyard-serverside/deploy.rb', line 111

def enable_maintenance_page
  maintenance_page_candidates = [
    "public/maintenance.html.custom",
    "public/maintenance.html.tmp",
    "public/maintenance.html",
    "public/system/maintenance.html.default",
  ].map do |file|
    File.join(c.latest_release, file)
  end

  # this one is guaranteed to exist
  maintenance_page_candidates <<  File.expand_path(
    "default_maintenance_page.html",
    File.dirname(__FILE__)
    )

  # put in the maintenance page
  maintenance_file = maintenance_page_candidates.detect do |file|
    File.exists?(file)
  end

  @maintenance_up = true
  roles :app_master, :app, :solo do
    maint_page_dir = File.join(c.shared_path, "system")
    visible_maint_page = File.join(maint_page_dir, "maintenance.html")
    run Escape.shell_command(['mkdir', '-p', maint_page_dir])
    run Escape.shell_command(['cp', maintenance_file, visible_maint_page])
  end
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 lists.mindrot.org/pipermail/openssh-unix-dev/2009-February/027271.html (Thanks Jim L.)



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/engineyard-serverside/deploy.rb', line 208

def generate_ssh_wrapper
  path = ssh_wrapper_path
  identity_file = "~/.ssh/#{c.app}-deploy-key"
<<-WRAP
[[ -x #{path} ]] || cat > #{path} <<'SSH'
#!/bin/sh
unset SSH_AUTH_SOCK
ssh -o 'CheckHostIP no' -o 'StrictHostKeyChecking no' -o 'PasswordAuthentication no' -o 'LogLevel DEBUG' -o 'IdentityFile #{identity_file}' -o 'IdentitiesOnly yes' -o 'UserKnownHostsFile /dev/null' $*
SSH
chmod 0700 #{path}
WRAP
end

#migrateObject

task



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

def migrate
  return unless c.migrate?
  @migrations_reached = true
  roles :app_master, :solo do
    cmd = "cd #{c.release_path} && PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} #{c.migration_command}"
    info "~> Migrating: #{cmd}"
    run(cmd)
  end
end

#node_package_manager_command_checkObject



303
304
305
# File 'lib/engineyard-serverside/deploy.rb', line 303

def node_package_manager_command_check
  "which npm"
end

#parse_configured_servicesObject



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

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

#push_codeObject

task



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

def push_code
  info "~> Pushing code to all servers"
  futures = EY::Serverside::Future.call(EY::Serverside::Server.all) do |server|
    server.sync_directory(config.repository_cache)
  end
  EY::Serverside::Future.success?(futures)
end

#required_downtime_stack?Boolean

Returns:

  • (Boolean)


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

def required_downtime_stack?
  %w[ nginx_mongrel glassfish ].include? c.stack
end

#restartObject

task



174
175
176
177
178
179
180
181
# File 'lib/engineyard-serverside/deploy.rb', line 174

def restart
  @restart_failed = true
  info "~> Restarting app servers"
  roles :app_master, :app, :solo do
    run(restart_command)
  end
  @restart_failed = false
end

#restart_commandObject



183
184
185
# File 'lib/engineyard-serverside/deploy.rb', line 183

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

#restart_with_maintenance_pageObject



104
105
106
107
108
109
# File 'lib/engineyard-serverside/deploy.rb', line 104

def restart_with_maintenance_page
  require_custom_tasks
  conditionally_enable_maintenance_page
  restart
  disable_maintenance_page
end

#rollbackObject

task



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/engineyard-serverside/deploy.rb', line 252

def rollback
  if c.all_releases.size > 1
    rolled_back_release = c.latest_release
    c.release_path = c.previous_release(rolled_back_release)

    revision = File.read(File.join(c.release_path, 'REVISION')).strip
    info "~> Rolling back to previous release: #{short_log_message(revision)}"

    run_with_callbacks(:symlink)
    sudo "rm -rf #{rolled_back_release}"
    bundle
    info "~> Restarting with previous release."
    with_maintenance_page { run_with_callbacks(:restart) }
  else
    info "~> Already at oldest release, nothing to roll back to."
    exit(1)
  end
end

#run_with_callbacks(task) ⇒ Object



158
159
160
161
162
# File 'lib/engineyard-serverside/deploy.rb', line 158

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

#services_command_checkObject



295
296
297
# File 'lib/engineyard-serverside/deploy.rb', line 295

def services_command_check
  "which /usr/local/ey_resin/ruby/bin/ey-services-setup"
end

#services_setup_commandObject



299
300
301
# File 'lib/engineyard-serverside/deploy.rb', line 299

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

#setup_servicesObject



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/engineyard-serverside/deploy.rb', line 307

def setup_services
  info "~> Setting up external services."
  previously_configured_services = parse_configured_services
  begin
    sudo(services_command_check)
  rescue StandardError => e
    info "Could not setup services. Upgrade your environment to get services configuration."
    return
  end
  sudo(services_setup_command)
rescue StandardError => e
  unless previously_configured_services.empty?
    warning <<-WARNING
External services configuration not updated. Using previous version.
Deploy again if your services configuration appears incomplete or out of date.
#{e}
    WARNING
  end
end

#setup_sqlite3_if_necessaryObject



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/engineyard-serverside/deploy.rb', line 327

def setup_sqlite3_if_necessary
  if gemfile? && lockfile && lockfile.uses_sqlite3?
    [
     ["Create databases directory if needed", "mkdir -p #{c.shared_path}/databases"],
     ["Creating SQLite database if needed", "touch #{c.shared_path}/databases/#{c.framework_env}.sqlite3"],
     ["Create config directory if needed", "mkdir -p #{c.release_path}/config"],
     ["Generating SQLite config", <<-WRAP],
cat > #{c.shared_path}/config/database.sqlite3.yml<<'YML'
#{c.framework_env}:
  adapter: sqlite3
  database: #{c.shared_path}/databases/#{c.framework_env}.sqlite3
  pool: 5
  timeout: 5000
YML
WRAP
     ["Symlink database.yml", "ln -nfs #{c.shared_path}/config/database.sqlite3.yml #{c.release_path}/config/database.yml"],
    ].each do |what, cmd|
      info "~> #{what}"
      run(cmd)
    end

    owner = [c.user, c.group].join(':')
    info "~> Setting ownership to #{owner}"
    sudo "chown -R #{owner} #{c.release_path}"
  end
end

#ssh_executableObject

If we don’t have a local version of the ssh wrapper script yet, create it on all the servers that will need it. TODO - This logic likely fails when people change deploy keys.



195
196
197
198
199
200
201
# File 'lib/engineyard-serverside/deploy.rb', line 195

def ssh_executable
  path = ssh_wrapper_path
  roles :app_master, :app, :solo, :util do
    run(generate_ssh_wrapper)
  end
  path
end

#ssh_wrapper_pathObject



221
222
223
# File 'lib/engineyard-serverside/deploy.rb', line 221

def ssh_wrapper_path
  "#{c.shared_path}/config/#{c.app}-ssh-wrapper"
end

task



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

def symlink(release_to_link=c.release_path)
  info "~> Symlinking code."
  run "rm -f #{c.current_path} && ln -nfs #{release_to_link} #{c.current_path} && chown -R #{c.user}:#{c.group} #{c.current_path}"
  @symlink_changed = true
rescue Exception
  sudo "rm -f #{c.current_path} && ln -nfs #{c.previous_release(release_to_link)} #{c.current_path} && chown -R #{c.user}:#{c.group} #{c.current_path}"
  @symlink_changed = false
  raise
end


354
355
356
357
358
359
360
361
362
363
# File 'lib/engineyard-serverside/deploy.rb', line 354

def symlink_configs(release_to_link=c.release_path)
  info "~> Preparing shared resources for release."
  symlink_tasks(release_to_link).each do |what, cmd|
    info "~> #{what}"
    run(cmd)
  end
  owner = [c.user, c.group].join(':')
  info "~> Setting ownership to #{owner}"
  sudo "chown -R #{owner} #{release_to_link}"
end


365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/engineyard-serverside/deploy.rb', line 365

def symlink_tasks(release_to_link)
  [
    ["Set group write permissions", "chmod -R g+w #{release_to_link}"],
    ["Remove revision-tracked shared directories from deployment", "rm -rf #{release_to_link}/log #{release_to_link}/public/system #{release_to_link}/tmp/pids"],
    ["Create tmp directory", "mkdir -p #{release_to_link}/tmp"],
    ["Symlink shared log directory", "ln -nfs #{c.shared_path}/log #{release_to_link}/log"],
    ["Create public directory if needed", "mkdir -p #{release_to_link}/public"],
    ["Create config directory if needed", "mkdir -p #{release_to_link}/config"],
    ["Create system directory if needed", "ln -nfs #{c.shared_path}/system #{release_to_link}/public/system"],
    ["Symlink shared pids directory", "ln -nfs #{c.shared_path}/pids #{release_to_link}/tmp/pids"],
    ["Symlink other shared config files", "find #{c.shared_path}/config -type f -not -name 'database.yml' -exec ln -s {} #{release_to_link}/config \\;"],
    ["Symlink mongrel_cluster.yml", "ln -nfs #{c.shared_path}/config/mongrel_cluster.yml #{release_to_link}/config/mongrel_cluster.yml"],
    ["Symlink database.yml", "ln -nfs #{c.shared_path}/config/database.yml #{release_to_link}/config/database.yml"],
    ["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"],
  ]
end