Class: Sunshine::ServerApp

Inherits:
Object
  • Object
show all
Defined in:
lib/sunshine/server_app.rb

Overview

Handles App deployment functionality for a single deploy server.

Server apps can be assigned any number of roles for classification.

:roles

sym|array - roles assigned (web, db, app, etc…)

By default server apps get the special :all role which will always return true when calling:

server_app.has_roles? :some_role

ServerApp objects can be instantiated several ways:

ServerApp.new app_instance, shell_instance, options_hash

When passing an App instance, the new ServerApp will keep an active link to the app’s properties. Name, deploy, and path attributes will be actively linked.

Rely on ServerApp to create a RemoteShell instance to use:

ServerApp.new app_instance, "host.com", options_hash

Instantiate with app name and rely on Sunshine defaults for app paths:

ServerApp.new "app_name", shell_instance, options_hash

Explicitely assign the app’s root path:

ServerApp.new "app_name", ..., :root_path => "/path/to/app_root"

Assigning a specific deploy name to use can be done with the :deploy_name option:

ServerApp.new "app_name", ..., :deploy_name => "deploy"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, host, options = {}) ⇒ ServerApp

Returns a new instance of ServerApp.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/sunshine/server_app.rb', line 57

def initialize app, host, options={}

  @app = App === app ? app : nil

  name = @app && @app.name || app
  assign_local_app_attr name, options

  @deploy_details = nil

  @roles = options[:roles] || [:all]
  @roles = @roles.split(" ") if String === @roles
  @roles = [*@roles].compact.map{|r| r.to_sym }

  @scripts = Hash.new{|h, k| h[k] = []}
  @info    = {:ports => {}}

  @pkg_manager = nil

  @shell = case host
           when String then RemoteShell.new host, options
           when Shell  then host
           else
             raise "Could not get remote shell '#{host}'"
           end

  @crontab = Crontab.new name, @shell
  @health  = Healthcheck.new shared_path, @shell
end

Instance Attribute Details

#appObject

Returns the value of attribute app.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def app
  @app
end

#crontabObject

Returns the value of attribute crontab.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def crontab
  @crontab
end

#healthObject

Returns the value of attribute health.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def health
  @health
end

#infoObject

Returns the value of attribute info.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def info
  @info
end

#pkg_managerObject

Returns the type of package management system to use.



314
315
316
317
# File 'lib/sunshine/server_app.rb', line 314

def pkg_manager
  @pkg_manager ||=
    (@shell.call("yum --version") && Yum) rescue Apt
end

#rolesObject

Returns the value of attribute roles.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def roles
  @roles
end

#scriptsObject

Returns the value of attribute scripts.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def scripts
  @scripts
end

#shellObject

Returns the value of attribute shell.



54
55
56
# File 'lib/sunshine/server_app.rb', line 54

def shell
  @shell
end

Class Method Details

.app_attr(*attribs) ⇒ Object

Define an attribue that will get a value from app, or locally if



39
40
41
42
43
44
45
46
47
# File 'lib/sunshine/server_app.rb', line 39

def self.app_attr *attribs
  attribs.each do |attrib|
    class_eval <<-STR, __FILE__, __LINE__ + 1
      def #{attrib}
        @app ? @app.send(:#{attrib}) : @#{attrib}
      end
    STR
  end
end

Instance Method Details

#add_shell_paths(*paths) ⇒ Object

Add paths the the shell $PATH env.



90
91
92
93
94
95
# File 'lib/sunshine/server_app.rb', line 90

def add_shell_paths(*paths)
  path = shell_env["PATH"] || "$PATH"
  paths << path

  shell_env.merge! "PATH" => paths.join(":")
end

#build_control_scriptsObject

Creates and uploads all control scripts for the application. To add to, or define a control script, see App#add_to_script.



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
# File 'lib/sunshine/server_app.rb', line 102

def build_control_scripts

  write_script "env", make_env_bash_script

  build_scripts = @scripts.dup

  if build_scripts[:restart].empty? &&
    !build_scripts[:start].empty? && !build_scripts[:stop].empty?
    build_scripts[:restart] << "#{self.root_path}/stop"
    build_scripts[:restart] << "#{self.root_path}/start"
  end

  if build_scripts[:status].empty?
    build_scripts[:status] << "echo 'No daemons for #{self.name}'; exit 1;"
  end

  build_scripts.each do |name, cmds|
    if cmds.empty?
      Sunshine.logger.warn @shell.host, "#{name} script is empty"
    end

    bash = make_bash_script name, cmds

    write_script name, bash
  end
end

#build_deploy_info_fileObject

Creates a yaml file with deploy information. To add custom information to the info file, use the app’s info hash attribute:

app.info[:key] = "some value"


135
136
137
138
139
140
141
142
# File 'lib/sunshine/server_app.rb', line 135

def build_deploy_info_file

  deploy_info = get_deploy_info.to_yaml

  @shell.make_file "#{self.checkout_path}/info", deploy_info

  @shell.symlink "#{self.current_path}/info", "#{self.root_path}/info"
end

#checkout_repo(repo) ⇒ Object

Checks out the app’s codebase to the checkout path.



148
149
150
151
# File 'lib/sunshine/server_app.rb', line 148

def checkout_repo repo
  install_deps repo.scm
  @info[:scm] = repo.checkout_to self.checkout_path, @shell
end

#deploy_details(reload = false) ⇒ Object

Get post-mortum information about the app’s deploy, from the generated deploy info file. Post-deploy only.



159
160
161
162
163
# File 'lib/sunshine/server_app.rb', line 159

def deploy_details reload=false
  return @deploy_details if @deploy_details && !reload
  @deploy_details =
    YAML.load @shell.call("cat #{self.current_path}/info") rescue nil
end

#deployed?Boolean

Checks if the server_app’s current info file deploy_name matches the server_app’s deploy_name attribute.

Returns:

  • (Boolean)


170
171
172
173
174
175
176
177
# File 'lib/sunshine/server_app.rb', line 170

def deployed?
  success =
    @deploy_details[:deploy_name] == self.deploy_name if @deploy_details

  return success if success

  deploy_details(true)[:deploy_name] == self.deploy_name rescue false
end

#directoriesObject

An array of all directories used by the app. Does not include symlinked directories.



184
185
186
# File 'lib/sunshine/server_app.rb', line 184

def directories
  [root_path, deploys_path, shared_path, log_path, checkout_path]
end

#get_deploy_infoObject

Returns information about the deploy at hand.



192
193
194
195
196
197
198
199
200
# File 'lib/sunshine/server_app.rb', line 192

def get_deploy_info
  { :deployed_at => Time.now.to_s,
    :deployed_as => @shell.call("whoami"),
    :deployed_by => Sunshine.shell.user,
    :deploy_name => File.basename(self.checkout_path),
    :roles       => @roles,
    :path        => self.root_path
  }.merge @info
end

#gpg_decrypt(gpg_file, options = {}) ⇒ Object

Decrypt a file using gpg. Allows options:

:output

str - the path the output file should go to

:passphrase

str - the passphrase gpg should use



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/sunshine/server_app.rb', line 208

def gpg_decrypt gpg_file, options={}
  output_file     = options[:output] || gpg_file.gsub(/\.gpg$/, '')

  passphrase      = options[:passphrase]
  passphrase_file = "#{self.root_path}/tmp/gpg_passphrase"

  gpg_cmd = "gpg --batch --no-tty --yes --output #{output_file} "+
    "--passphrase-file #{passphrase_file} --decrypt #{gpg_file}"

  @shell.call "mkdir -p #{File.dirname(passphrase_file)}"

  @shell.make_file passphrase_file, passphrase

  @shell.call "cd #{self.checkout_path} && #{gpg_cmd}"
  @shell.call "rm -f #{passphrase_file}"
end

#has_roles?(*roles) ⇒ Boolean

Check if this server app includes the specified roles:

Returns:

  • (Boolean)


229
230
231
232
233
234
235
236
237
# File 'lib/sunshine/server_app.rb', line 229

def has_roles? *roles
  return true if @roles.include? :all

  roles.each do |role|
    return false unless @roles.include? role
  end

  true
end

#install_deps(*deps) ⇒ Object

Install dependencies previously defined in Sunshine.dependencies. Will not execute if Sunshine.auto_dependencies? is false.



262
263
264
265
266
267
268
269
270
# File 'lib/sunshine/server_app.rb', line 262

def install_deps(*deps)
  return unless Sunshine.auto_dependencies?

  options = {:call => @shell, :prefer => pkg_manager}
  options.merge! deps.delete_at(-1) if Hash === deps.last

  args = deps << options
  Sunshine.dependencies.install(*args)
end

#make_app_directoriesObject

Creates the required application directories.



276
277
278
# File 'lib/sunshine/server_app.rb', line 276

def make_app_directories
  @shell.call "mkdir -p #{self.directories.join(" ")}"
end

#make_bash_script(name, cmds) ⇒ Object

Makes an array of bash commands into a script that echoes ‘true’ on success.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/sunshine/server_app.rb', line 285

def make_bash_script name, cmds
  cmds = cmds.map{|cmd| "(#{cmd})" }

  cmds << "echo true"

  bash = <<-STR
#!/bin/bash
if [ "$1" == "--no-env" ]; then
  #{cmds.flatten.join(" && ")}
else
  #{self.root_path}/env #{self.root_path}/#{name} --no-env
fi
  STR
end

#make_env_bash_scriptObject

Creates the one-off env script that will be used by other scripts to correctly set their env variables.



305
306
307
308
# File 'lib/sunshine/server_app.rb', line 305

def make_env_bash_script
  env_str = shell_env.map{|e| e.join("=")}.join(" ")
  "#!/bin/bash\nenv #{env_str} \"$@\""
end

#rake(command) ⇒ Object

Run a rake task the deploy server.



323
324
325
326
# File 'lib/sunshine/server_app.rb', line 323

def rake command
  install_deps 'rake', :type => Gem
  @shell.call "cd #{self.checkout_path} && rake #{command}"
end

#register_as_deployedObject

Adds the app to the deploy server’s deployed-apps list



332
333
334
# File 'lib/sunshine/server_app.rb', line 332

def register_as_deployed
  AddCommand.exec self.root_path, 'servers' => [@shell]
end

#remove_old_deploysObject

Removes old deploys from the checkout_dir based on Sunshine’s max_deploy_versions.



341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/sunshine/server_app.rb', line 341

def remove_old_deploys
  deploys = @shell.call("ls -1 #{self.deploys_path}").split("\n")

  return unless deploys.length > Sunshine.max_deploy_versions

  lim = Sunshine.max_deploy_versions + 1

  rm_deploys = deploys[0..-lim]
  rm_deploys.map!{|d| "#{self.deploys_path}/#{d}"}

  @shell.call("rm -rf #{rm_deploys.join(" ")}")
end

#restartObject

Run the app’s restart script. Returns false on failure. Post-deploy only.



359
360
361
# File 'lib/sunshine/server_app.rb', line 359

def restart
  @shell.call "#{self.root_path}/restart" rescue false
end

#revert!Object

Symlink current directory to previous checkout and remove the current deploy directory.



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/sunshine/server_app.rb', line 368

def revert!
  @shell.call "rm -rf #{self.checkout_path}"

  last_deploy = @shell.call("ls -rc1 #{self.deploys_path}").split("\n").last

  if last_deploy && !last_deploy.empty?
    @shell.symlink "#{self.deploys_path}/#{last_deploy}", self.current_path

    Sunshine.logger.info @shell.host, "Reverted to #{last_deploy}"

    unless start :force => true
      Sunshine.logger.error @shell.host, "Failed #{@name} startup"
    end

  else
    @crontab.delete!

    Sunshine.logger.info @shell.host, "No previous deploy to revert to."
  end
end

#run_bundlerObject

Runs bundler. Installs the bundler gem if missing.



393
394
395
396
# File 'lib/sunshine/server_app.rb', line 393

def run_bundler
  install_deps 'bundler', :type => Gem
  @shell.call "cd #{self.checkout_path} && gem bundle"
end

#run_geminstallerObject

Runs geminstaller. :( Deprecated: use bundler



403
404
405
406
# File 'lib/sunshine/server_app.rb', line 403

def run_geminstaller
  install_deps 'geminstaller', :type => Gem
  @shell.call "cd #{self.checkout_path} && geminstaller -e"
end

#running?Boolean

Check if the app pids are present. Post-deploy only.

Returns:

  • (Boolean)


413
414
415
# File 'lib/sunshine/server_app.rb', line 413

def running?
  @shell.call "#{self.root_path}/status" rescue false
end

#sass(*sass_names) ⇒ Object

Run a sass task on any or all deploy servers.



421
422
423
424
425
426
427
428
429
430
431
# File 'lib/sunshine/server_app.rb', line 421

def sass *sass_names
  install_deps 'haml', :type => Gem

  sass_names.flatten.each do |name|
    sass_file = "public/stylesheets/sass/#{name}.sass"
    css_file  = "public/stylesheets/#{name}.css"
    sass_cmd  = "cd #{self.checkout_path} && sass #{sass_file} #{css_file}"

    @shell.call sass_cmd
  end
end

#shell_envObject

Get the deploy server’s shell environment.



437
438
439
# File 'lib/sunshine/server_app.rb', line 437

def shell_env
  @shell.env
end

#start(options = nil) ⇒ Object

Run the app’s start script. Returns false on failure. Post-deploy only.



446
447
448
449
450
451
452
453
454
455
# File 'lib/sunshine/server_app.rb', line 446

def start options=nil
  options ||= {}

  if running?
    return unless options[:force]
    stop
  end

  @shell.call "#{self.root_path}/start" rescue false
end

#statusObject

Get the app’s status: :running or :down.



461
462
463
# File 'lib/sunshine/server_app.rb', line 461

def status
  running? ? :running : :down
end

#stopObject

Run the app’s stop script. Returns false on failure. Post-deploy only.



470
471
472
# File 'lib/sunshine/server_app.rb', line 470

def stop
  @shell.call "#{self.root_path}/stop" rescue false
end

Creates a symlink to the app’s checkout path.



478
479
480
# File 'lib/sunshine/server_app.rb', line 478

def symlink_current_dir
  @shell.symlink self.checkout_path, self.current_path
end

#upload_tasks(*files) ⇒ Object

Upload common rake tasks from a local path or the sunshine lib.

app.upload_tasks
  #=> upload all tasks
app.upload_tasks 'app', 'common', ...
  #=> upload app and common rake files

File paths may also be used instead of the file’s base name but directory structures will not be followed:

app.upload_tasks 'lib/common/app.rake', 'lib/do_thing.rake'

Allows options:

:local_path

str - the path to get rake tasks from

:remote_path

str - the remote absolute path to upload the files to



498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/sunshine/server_app.rb', line 498

def upload_tasks *files
  options     = Hash === files[-1] ? files.delete_at(-1) : {}
  remote_path = options[:remote_path] || "#{self.checkout_path}/lib/tasks"
  local_path  = options[:local_path] || "#{Sunshine::ROOT}/templates/tasks"

  @shell.call "mkdir -p #{remote_path}"

  files.map! do |file|
    if File.basename(file) == file
      File.join(local_path, "#{file}.rake")
    else
      file
    end
  end

  files = Dir.glob("#{Sunshine::ROOT}/templates/tasks/*") if files.empty?

  files.each do |file|
    remote_file = File.join remote_path, File.basename(file)
    @shell.upload file, remote_file
  end
end

#write_script(name, contents) ⇒ Object

Write an executable bash script to the app’s checkout dir on the deploy server, and symlink them to the current dir.



526
527
528
529
530
531
532
533
# File 'lib/sunshine/server_app.rb', line 526

def write_script name, contents

  @shell.make_file "#{self.checkout_path}/#{name}", contents,
      :flags => '--chmod=ugo=rwx'

  @shell.symlink "#{self.current_path}/#{name}",
    "#{self.root_path}/#{name}"
end