class Ridoku::Backup

Attributes

dbase[RW]

Public Instance Methods

run() click to toggle source
# File lib/ridoku/backup.rb, line 15
def run
  cline = Base.config[:command]
  current = cline.shift
  command = cline.shift
  sub = cline.shift

  case command
  when 'list', nil, 'info'
    list
  when 'init'
    init
  when 'capture'
    capture(sub)
  when 'url'
    url(ARGV.shift)
  when 'restore'
    restore(sub, ARGV.shift)
  when 'delete', 'remove', 'rm'
    remove(ARGV.shift)
  else
    print_backup_help
  end
end

Protected Instance Methods

bucket_exists!() click to toggle source
# File lib/ridoku/backup.rb, line 150
def bucket_exists!
  # Fail if bucket not set
  fail_undefined_bucket! if Base.config[:backup_bucket].nil?

  s3 = AWS::S3.new
  bucket = s3.buckets[Base.config[:backup_bucket]]

  $stdout.print "Checking for '#{$stdout.colorize(bucket.name, :bold)}' "\
    'S3 Buckets... ' if $stdout.tty?

  fail_uninitialized_bucket! unless bucket.exists?

  $stdout.puts $stdout.colorize('Found!', :green) if $stdout.tty?
end
capture(opt) click to toggle source
# File lib/ridoku/backup.rb, line 213
def capture(opt)
  return dump_local if opt == 'local'

  bucket_exists!

  recipe_data = {
    backup: {
      databases: Base.config[:app].downcase.split(','),
      dump: {
        type: 's3',
        region: 'us-west-1',
        bucket: Base.config[:backup_bucket]
      }
    }
  }

  Base.fetch_app
  Base.fetch_instance
  instances = Base.get_instances_for_layer('postgresql')
  instance_ids = instances.map { |inst| inst[:instance_id] }

  command = Base.execute_recipes(Base.app[:app_id], instance_ids,
    'Capturing application DB backup.', ['s3',
      'postgresql::backup_database'], recipe_data)

  Base.run_command(command)
end
dump_local() click to toggle source
# File lib/ridoku/backup.rb, line 181
def dump_local()
  load_database

  command = ARGV.shift

  unless m = `#{command} --version`.match(/(9[.][23]([.][0-9]+)?)/)
    $stderr.puts "Invalid pg_dump version #{m[1]}."
    return
  end

  backup_file = "#{Base.config[:app]}-"\
    "#{Time.now.utc.strftime("%Y%m%d%H%M%S")}.sql"

  $stdout.puts "pg_dump version: #{$stdout.colorize(m[1], :green)}"
  $stdout.puts "Creating backup file: #{backup_file}"

  # Add PGPASSWORD to the environment.
  ENV['PGPASSWORD'] = dbase['password']

  system(["#{command}",
    "-Fc",
    "-h #{dbase['host']}",
    "-U #{dbase['username']}",
    "-p #{dbase['port']}",
    "#{dbase['database']}",
    "> #{backup_file}"].join(' '))

  $stdout.puts $stdout.colorize("pg_dump complete.", :green)
  $stdout.puts "File size: #{::File.size(backup_file)}"
  ENV['PGPASSWORD'] = nil
end
fail_object_not_found!() click to toggle source
# File lib/ridoku/backup.rb, line 380
    def fail_object_not_found!
        # Fail if bucket doesn't exist.
      fail ArgumentError.new(<<EOF

The specified S3 backup object does not yet exist.  Please create it manually,
or by running ([] represents optional arguments):

$ ridoku backup:capture [--backup-bucket <bucket name> --app <app name>]

EOF
      )
    end
fail_undefined_bucket!() click to toggle source
# File lib/ridoku/backup.rb, line 342
    def fail_undefined_bucket!
      fail ArgumentError.new(<<EOF
Your Backup S3 bucket name is undefined.
Please set it by passing in --set-backup-bucket followed by the desired bucket
name.

Example:
$ ridoku --set-backup-bucket zv1ns-database-backups

EOF
      )
    end
fail_undefined_object!(sub, action) click to toggle source
# File lib/ridoku/backup.rb, line 368
    def fail_undefined_object!(sub, action)
      # Fail if object name not set
      fail ArgumentError.new(<<EOF
The specified backup (#{Base.config[:backup_bucket]}/#{sub}) doesn't exist!

Example:
$ ridoku backup:#{action} <name>

EOF
        )
    end
fail_uninitialized_bucket!() click to toggle source
# File lib/ridoku/backup.rb, line 355
    def fail_uninitialized_bucket!
        # Fail if bucket doesn't exist.
      fail ArgumentError.new(<<EOF

The specified S3 backup bucket does not yet exist.  Please create it manually,
or by running ([] represents optional arguments):

$ ridoku backup:init [--backup-bucket <bucket name>]

EOF
      )
    end
init() click to toggle source
# File lib/ridoku/backup.rb, line 126
def init
  fail_undefined_bucket! if Base.config[:backup_bucket].nil?

  s3 = AWS::S3.new
  bucket = s3.buckets[Base.config[:backup_bucket]]
  $stdout.puts "Checking for '#{$stdout.colorize(bucket.name, :bold)}' "\
    'S3 Buckets...'
  if bucket.exists?
    $stdout.puts "Bucket exists!"
  else
    $stdout.puts "Bucket does not exist.  Generating..."
    begin
      bucket = s3.buckets.create(Base.config[:backup_bucket],
        acl: :bucket_owner_full_control)
    rescue => e
      $stderr.puts "Oops! Something went wrong when trying to create your bucket!"
      raise e
    end

    $stdout.puts 'Bucket created successfully!'
    $stdout.puts "#{$stdout.colorize(bucket.name, [:bold, :green])}: owner read/write."
  end
end
list() click to toggle source
# File lib/ridoku/backup.rb, line 111
def list
  bucket_exists!

  list = objects_from_bucket(Base.config[:backup_bucket])
  app_hash, max = object_list_to_hash(list)

  app_hash.each do |key, value|
    $stdout.puts "#{$stdout.colorize(key, :green)}:"
    value.each do |obj|
      $stdout.puts "  #{pspace(obj[:key], max)}  "\
        "#{obj[:date]}\t#{obj[:type]}"
    end
  end
end
load_database() click to toggle source
# File lib/ridoku/backup.rb, line 41
def load_database
  Base.fetch_stack
  Base.fetch_layer
  self.dbase =
    Base.custom_json['deploy'][Base.config[:app]]['database']
end
object_exists!(sub, action) click to toggle source
# File lib/ridoku/backup.rb, line 165
def object_exists!(sub, action)
  fail_undefined_object!(sub, action) if sub.nil? || !sub.present?

  s3 = AWS::S3.new
  bucket = s3.buckets[Base.config[:backup_bucket]]
  $stdout.print "Checking for '#{$stdout.colorize(sub, :bold)}' "\
    'in bucket... ' if $stdout.tty?

  object = bucket.objects[sub]
  fail_object_not_found! unless object.exists?

  $stdout.puts $stdout.colorize('Found!', :green) if $stdout.tty?

  object
end
object_list_to_hash(list) click to toggle source
# File lib/ridoku/backup.rb, line 84
def object_list_to_hash(list)
  final = {}
  max = 0

  list.each do |obj|
    comp = obj[:key].match(
      %r(^(.*)-([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2}).sqd$)
    )

    max = obj[:key].length if obj[:key].length > max

    unless comp.nil?
      app = comp[1]
      final[app] ||= []
      obj[:app] = app
      obj[:date] = "#{comp[3]}/#{comp[4]}/#{comp[2]} #{comp[5]}:#{comp[6]}"
      final[app] << obj
    end
  end

  [ final, max ]
end
objects_from_bucket(bucket) click to toggle source
# File lib/ridoku/backup.rb, line 71
def objects_from_bucket(bucket)
  s3 = AWS::S3.new

  bucket = s3.buckets[bucket]

  # hack: couldn't find how to get an object count...
  objects = 0

  bucket.objects.map do |obj|
    { key: obj.key, size: obj.content_length, type: obj.content_type }
  end
end
print_backup_help() click to toggle source
pspace(str, max) click to toggle source
# File lib/ridoku/backup.rb, line 107
def pspace(str, max)
  str + ' ' * (max - str.length)
end
remove(sub) click to toggle source
# File lib/ridoku/backup.rb, line 323
def remove(sub)
  bucket_exists!
  object = object_exists!(sub, 'remove')

  $stdout.puts 'Are you sure you want to delete this file? [yes/N]'
  res = $stdin.gets.chomp

  if res.present? && res == 'yes'
    s3 = AWS::S3.new
    object.delete

    $stdout.puts 'Request object deleted.'
  else
    $stdout.puts 'Aborting.'
  end
end
restore(sub, arg) click to toggle source
# File lib/ridoku/backup.rb, line 271
def restore(sub, arg)
  return restore_local(file) if sub == 'local'

  bucket_exists!
  object_exists!(arg, 'restore')
  $stdout.puts 'Are you sure you want to restore this database dump? [yes/N]'
  res = $stdin.gets.chomp

  if res.present? && res == 'yes'
    recipe_data = {
      backup: {
        databases: Base.config[:app].downcase.split(','),
        force: Base.config[:force] || false,
        dump: {
          type: 's3',
          region: 'us-west-1',
          bucket: Base.config[:backup_bucket],
          key: arg
        }
      }
    }

    Base.fetch_app
    Base.fetch_instance
    layer_ids = Base.get_layer_ids('postgresql')

    Base.instances.select! do |inst|
      next false unless inst[:status] == 'online'
      next layer_ids.each do |id|
        break true if inst[:layer_ids].include?(id)
      end || false
    end

    instance_ids = Base.instances.map { |inst| inst[:instance_id] }

    command = Base.execute_recipes(Base.app[:app_id], instance_ids,
      'Restoring application DB backup.', ['s3',
        'postgresql::restore_database'], recipe_data)

    Base.run_command(command)
  else
    $stdout.puts 'Aborting.'
  end
end
restore_local(file) click to toggle source
# File lib/ridoku/backup.rb, line 241
def restore_local(file)
  load_database

  command = ARGV.shift

  unless m = `#{command} --version`.match(/(9[.][23]([.][0-9]+)?)/)
    $stderr.puts "Invalid pg_restore version #{m[1]}."
    return
  end

  $stdout.puts "pg_restore version: #{$stdout.colorize(m[1], :green)}"
  $stdout.puts "Using backup file: #{file}"

  # Add PGPASSWORD to the environment.
  ENV['PGPASSWORD'] = dbase['password']

  system(["#{command}",
    "--clean",
    "#{'--single-transaction' unless Base.config[:force]}",
    "-h #{dbase['host']}",
    "-U #{dbase['username']}",
    "-p #{dbase['port']}",
    "-d #{dbase['database']}",
    "#{file}"].join(' '))

  $stdout.puts $stdout.colorize("pg_restore complete.", :green)
  $stdout.puts "File size: #{::File.size(backup_file)}"
  ENV['PGPASSWORD'] = nil
end
url(sub) click to toggle source
# File lib/ridoku/backup.rb, line 316
def url(sub)
  bucket_exists!
  object = object_exists!(sub, 'get url')

  $stdout.puts object.url_for(:read, expires: 60)
end