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

Create a server app instance. Supports the following argument configurations:

ServerApp.new app_inst, "myserver.com", :roles => :web
ServerApp.new "app_name", shell_inst, options_hash


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

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

  @all_deploy_names = nil
  @previous_deploy_name = nil
end

Instance Attribute Details

#appObject

Returns the value of attribute app.



98
99
100
# File 'lib/sunshine/server_app.rb', line 98

def app
  @app
end

#crontabObject

Returns the value of attribute crontab.



98
99
100
# File 'lib/sunshine/server_app.rb', line 98

def crontab
  @crontab
end

#infoObject

Returns the value of attribute info.



98
99
100
# File 'lib/sunshine/server_app.rb', line 98

def info
  @info
end

#pkg_managerObject

Returns the type of package management system to use.



352
353
354
355
356
357
# File 'lib/sunshine/server_app.rb', line 352

def pkg_manager
  @pkg_manager ||=
    DependencyLib.dependency_types.detect do |dt|
      dt.system_manager? @shell
    end
end

#rolesObject

Returns the value of attribute roles.



98
99
100
# File 'lib/sunshine/server_app.rb', line 98

def roles
  @roles
end

#scriptsObject

Returns the value of attribute scripts.



98
99
100
# File 'lib/sunshine/server_app.rb', line 98

def scripts
  @scripts
end

#shellObject

Returns the value of attribute shell.



98
99
100
# File 'lib/sunshine/server_app.rb', line 98

def shell
  @shell
end

Class Method Details

.app_attr(*attribs) ⇒ Object

Define an attribute 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

.from_info_file(path, shell = nil) ⇒ Object

Creates a ServerApp instance from a deploy info file.



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/sunshine/server_app.rb', line 80

def self.from_info_file path, shell=nil
  shell ||= Sunshine.shell

  opts = YAML.load shell.call("cat #{path}")
  opts[:root_path] = opts.delete :path

  sa_shell = shell.dup
  sa_shell.env = opts[:env] || Hash.new
  sa_shell.connect if shell.connected?

  new opts[:name], sa_shell, opts
end

.register_dependency_type(dep_class) ⇒ Object

Creates dependency instance methods such as gem_install, yum_install, etc on both App and ServerApp classes.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/sunshine/server_app.rb', line 54

def self.register_dependency_type dep_class
  class_eval <<-STR, __FILE__, __LINE__ + 1
    def #{dep_class.short_name}_install(*names)
      options = Hash === names.last ? names.delete_at(-1) : Hash.new

      names.each do |name|
        dep = #{dep_class}.new(name, options)
        dep.install! :call => @shell
      end
    end
  STR

  App.class_eval <<-STR, __FILE__, __LINE__ + 1
    def #{dep_class.short_name}_install(*names)
      options = names.last if Hash === names.last
      with_server_apps options,
        :msg  => "Installing #{dep_class.short_name} packages",
        :send => [:#{dep_class.short_name}_install, *names]
    end
  STR
end

Instance Method Details

#add_shell_paths(*paths) ⇒ Object

Add paths the the shell $PATH env.



143
144
145
146
147
148
# File 'lib/sunshine/server_app.rb', line 143

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

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

#all_deploy_names(reload = false) ⇒ Object

Returns an array of all deploys in the deploys_path dir, starting with the oldest.



364
365
366
367
368
369
# File 'lib/sunshine/server_app.rb', line 364

def all_deploy_names reload=false
  return @all_deploy_names if @all_deploy_names && !reload

  @all_deploy_names =
    @shell.call("ls -rc1 #{self.deploys_path}").split("\n")
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.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/sunshine/server_app.rb', line 155

def build_control_scripts
  @shell.call "mkdir -p #{self.scripts_path}"

  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 status 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

  symlink_scripts_to_root @scripts.keys, "env"
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"


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

def build_deploy_info_file

  deploy_info   = get_deploy_info.to_yaml
  info_filepath = "#{self.scripts_path}/info"

  @shell.make_file info_filepath, deploy_info

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

#checkout_repo(repo, scm_info = {}) ⇒ Object

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



205
206
207
208
209
210
211
212
213
214
# File 'lib/sunshine/server_app.rb', line 205

def checkout_repo repo, scm_info={}
  install_deps repo.scm

  Sunshine.logger.info repo.scm,
    "Checking out to #{@shell.host} #{self.checkout_path}" do

    @info[:scm] = repo.checkout_to self.checkout_path, @shell
    @info[:scm].merge! scm_info
  end
end

#deploy_details(reload = false) ⇒ Object

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



222
223
224
225
226
227
228
229
230
231
# File 'lib/sunshine/server_app.rb', line 222

def deploy_details reload=false
  return @deploy_details if @deploy_details && !reload

  @deploy_details =
    YAML.load @shell.call("cat #{self.root_path}/info") rescue nil

  @deploy_details = nil unless Hash === @deploy_details

  @deploy_details
end

#deployed?Boolean

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

Returns:

  • (Boolean)


238
239
240
241
242
243
244
245
# File 'lib/sunshine/server_app.rb', line 238

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.



252
253
254
255
# File 'lib/sunshine/server_app.rb', line 252

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

#get_deploy_infoObject

Builds a hash with information about the deploy at hand.



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

def get_deploy_info
  { :deployed_at => @shell.call("date"),
    :deployed_as => @shell.call("whoami"),
    :deployed_by => Sunshine.shell.user,
    :deploy_name => File.basename(self.checkout_path),
    :name        => self.name,
    :env         => shell_env,
    :roles       => @roles,
    :path        => self.root_path,
    :sunshine_version => Sunshine::VERSION
  }.merge @info
end

#has_roles?(roles, match_any = false) ⇒ Boolean

Check if this server app includes the specified roles:

server_app.has_roles? :web
server_app.has_roles? [:web, :app]

The boolean operator may be changed to OR by passing true as the second argument:

server_app.roles = [:web, :app]
server_app.has_roles? [:web, :db]         #=> false
server_app.has_roles? [:web, :db], true   #=> true

Returns:

  • (Boolean)


286
287
288
289
290
291
292
293
# File 'lib/sunshine/server_app.rb', line 286

def has_roles? roles, match_any=false
  roles = [*roles]

  return true                     if @roles.include? :all
  return !(roles & @roles).empty? if match_any

  (roles & @roles).length == roles.length
end

#install_deps(*deps) ⇒ Object

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



300
301
302
303
304
305
306
307
308
# File 'lib/sunshine/server_app.rb', line 300

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.



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

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.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/sunshine/server_app.rb', line 323

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.



343
344
345
346
# File 'lib/sunshine/server_app.rb', line 343

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

#previous_deploy_name(reload = false) ⇒ Object

Returns the name of the previous deploy.



375
376
377
378
379
380
381
382
# File 'lib/sunshine/server_app.rb', line 375

def previous_deploy_name reload=false
  return @previous_deploy_name if @previous_deploy_name && !reload

  arr = all_deploy_names(reload)
  arr.delete(@deploy_name)

  @previous_deploy_name = arr.last
end

#rake(command) ⇒ Object

Run a rake task the deploy server.



388
389
390
391
# File 'lib/sunshine/server_app.rb', line 388

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



397
398
399
# File 'lib/sunshine/server_app.rb', line 397

def register_as_deployed
  AddCommand.exec "#{self.name}:#{self.root_path}", 'servers' => [@shell]
end

#remove_old_deploysObject

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



406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/sunshine/server_app.rb', line 406

def remove_old_deploys
  deploys = all_deploy_names true

  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.



424
425
426
427
# File 'lib/sunshine/server_app.rb', line 424

def restart
  # Permissions are handled by the script, use: :sudo => false
  run_script :stop, :sudo => false
end

#restart!Object

Run the app’s restart script. Raises an exception on failure. Post-deploy only.



434
435
436
437
# File 'lib/sunshine/server_app.rb', line 434

def restart!
  # Permissions are handled by the script, use: :sudo => false
  run_script! :restart, :sudo => false
end

#revert!Object

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



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/sunshine/server_app.rb', line 444

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

  last_deploy = previous_deploy_name(true)

  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}"

  else
    @crontab.delete!

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

#run_bundler(options = {}) ⇒ Object

Runs bundler. Installs the bundler gem if missing.



465
466
467
468
# File 'lib/sunshine/server_app.rb', line 465

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

#run_geminstaller(options = {}) ⇒ Object

Runs geminstaller. :( Deprecated: how about trying bundler or isolate? If sudo is required to install to your GEM_HOME, make sure to pass it as an argument:

server_app.run_geminstaller :sudo => true


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

def run_geminstaller options={}
  install_deps 'geminstaller', :type => Gem
  # Without sudo gems get installed to ~user/.gems
  @shell.call "cd #{self.checkout_path} && geminstaller -e", options
end

#run_script(name, options = nil, &block) ⇒ Object

Runs a script from the root_path. Post-deploy only.



489
490
491
492
# File 'lib/sunshine/server_app.rb', line 489

def run_script name, options=nil, &block
  options ||= {}
  run_script! name, options, &block rescue false
end

#run_script!(name, options = nil, &block) ⇒ Object

Runs a script from the root_path. Raises an exception if the status code is not 0. Post-deploy only.



500
501
502
503
504
505
# File 'lib/sunshine/server_app.rb', line 500

def run_script! name, options=nil, &block
  options ||= {}

  script_path = File.join self.scripts_path, name.to_s
  @shell.call script_path, options, &block
end

#running?Boolean

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

Returns:

  • (Boolean)


512
513
514
515
516
517
518
519
520
# File 'lib/sunshine/server_app.rb', line 512

def running?
  # Permissions are handled by the script, use: :sudo => false
  run_script! :status, :sudo => false
  true

rescue CmdError => e
  return false if e.exit_code == Daemon::STATUS_DOWN_CODE
  raise e
end

#sass(*sass_names) ⇒ Object

Run a sass task on any or all deploy servers.



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

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.



542
543
544
# File 'lib/sunshine/server_app.rb', line 542

def shell_env
  @shell.env
end

#start(options = nil) ⇒ Object

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



551
552
553
554
555
556
557
558
559
560
561
# File 'lib/sunshine/server_app.rb', line 551

def start options=nil
  options ||= {}

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

  # Permissions are handled by the script, use: :sudo => false
  run_script :start, :sudo => false
end

#start!(options = nil) ⇒ Object

Run the app’s start script. Raises an exception on failure. Post-deploy only.



568
569
570
571
572
573
574
575
576
577
578
# File 'lib/sunshine/server_app.rb', line 568

def start! options=nil
  options ||= {}

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

  # Permissions are handled by the script, use: :sudo => false
  run_script! :start, :sudo => false
end

#statusObject

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



584
585
586
# File 'lib/sunshine/server_app.rb', line 584

def status
  running? ? :running : :down
end

#stopObject

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



593
594
595
596
# File 'lib/sunshine/server_app.rb', line 593

def stop
  # Permissions are handled by the script, use: :sudo => false
  run_script :stop, :sudo => false
end

#stop!Object

Run the app’s stop script. Raises an exception on failure. Post-deploy only.



603
604
605
606
# File 'lib/sunshine/server_app.rb', line 603

def stop!
  # Permissions are handled by the script, use: :sudo => false
  run_script! :stop, :sudo => false
end

Creates a symlink to the app’s checkout path.



612
613
614
# File 'lib/sunshine/server_app.rb', line 612

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

Creates a symlink of every script_name from the scripts_path dir to the app’s root directory for easy access.



621
622
623
624
625
626
627
628
# File 'lib/sunshine/server_app.rb', line 621

def symlink_scripts_to_root *script_names
  script_names.flatten.each do |name, val|
    script_file  = File.join self.scripts_path, name.to_s
    pointer_file = File.join self.root_path, name.to_s

    @shell.symlink script_file, pointer_file
  end
end

#upload_codebase(code_dir, scm_info = {}) ⇒ Object

Assumes the passed code_dir is the root directory of the checked out codebase and uploads it to the checkout_path.



635
636
637
638
639
640
641
642
643
644
# File 'lib/sunshine/server_app.rb', line 635

def upload_codebase code_dir, scm_info={}
  excludes = scm_info.delete :exclude if scm_info[:exclude]
  excludes = [excludes].flatten.compact
  excludes.map!{|e| "--exclude #{e}"}

  repo = RsyncRepo.new code_dir, :flags => excludes
  repo.checkout_to self.checkout_path, @shell

  @info[:scm] = scm_info
end

#write_script(name, contents) ⇒ Object

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



651
652
653
654
655
656
# File 'lib/sunshine/server_app.rb', line 651

def write_script name, contents
  script_file = "#{self.scripts_path}/#{name}"

  @shell.make_file script_file, contents,
    :flags => '--chmod=ugo=rwx' unless @shell.file? script_file
end