Class: RDSBackup::Job

Inherits:
Object
  • Object
show all
Defined in:
lib/rds_backup_service/model/backup_job.rb

Overview

Backs up the contents of a single RDS database to S3

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rds_instance_id, optional_params = {}) ⇒ Job

Constructor.

Parameters:

  • rds_instance_id (String)

    the ID of the RDS instance to backup

  • optional_params (Hash) (defaults to: {})

    optional additional parameters:

    • :email - an email address to be notified on completion

    • :backup_id - a unique ID for this job, if necessary

    • :requested - a Time when this job was requested

    • :logger - a Logger object, for printing this job’s ongoing status



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/rds_backup_service/model/backup_job.rb', line 20

def initialize(rds_instance_id, optional_params = {})
  @rds_id, @options = rds_instance_id, optional_params.symbolize_keys
  @backup_id  = options[:backup_id] || "%016x" % (rand * 0xffffffffffffffff)
  @requested  = options[:requested] ? Time.parse(options[:requested]) : Time.now
  @status     = 200
  @message    = "queued"
  @files      = []
  @config     = RDSBackup.settings
  @bucket     = @config['backup_bucket']
  @s3_path    = (@config['backup_prefix'] ? "#{@config['backup_prefix']}/" : "")+
                "#{requested.strftime("%Y/%m/%d")}/#{rds_id}/#{backup_id}"
  @snapshot_id  = "rds-backup-service-#{rds_id}-#{backup_id}"
  @new_rds_id   = "rds-backup-service-#{backup_id}"
  @new_password = "#{backup_id}"
  @account_name = options[:account_name]
end

Instance Attribute Details

#account_nameObject (readonly)

Returns the value of attribute account_name.



10
11
12
# File 'lib/rds_backup_service/model/backup_job.rb', line 10

def 
  @account_name
end

#backup_idObject (readonly)

Returns the value of attribute backup_id.



10
11
12
# File 'lib/rds_backup_service/model/backup_job.rb', line 10

def backup_id
  @backup_id
end

#filesObject (readonly)

Returns the value of attribute files.



11
12
13
# File 'lib/rds_backup_service/model/backup_job.rb', line 11

def files
  @files
end

#messageObject (readonly)

Returns the value of attribute message.



11
12
13
# File 'lib/rds_backup_service/model/backup_job.rb', line 11

def message
  @message
end

#optionsObject (readonly)

Returns the value of attribute options.



10
11
12
# File 'lib/rds_backup_service/model/backup_job.rb', line 10

def options
  @options
end

#rds_idObject (readonly)

Returns the value of attribute rds_id.



10
11
12
# File 'lib/rds_backup_service/model/backup_job.rb', line 10

def rds_id
  @rds_id
end

#requestedObject (readonly)

Returns the value of attribute requested.



11
12
13
# File 'lib/rds_backup_service/model/backup_job.rb', line 11

def requested
  @requested
end

#statusObject (readonly)

Returns the value of attribute status.



11
12
13
# File 'lib/rds_backup_service/model/backup_job.rb', line 11

def status
  @status
end

#status_urlObject (readonly)

Returns the value of attribute status_url.



11
12
13
# File 'lib/rds_backup_service/model/backup_job.rb', line 11

def status_url
  @status_url
end

Class Method Details

.perform(rds_instance_id, options = {}) ⇒ Object

Entry point for the Resque framework. Parameters are the same as for #initialize()



62
63
64
65
66
67
68
69
70
71
# File 'lib/rds_backup_service/model/backup_job.rb', line 62

def self.perform(rds_instance_id, options = {})
  job = Job.new(rds_instance_id, options)
  begin
    job.perform_backup
  rescue Resque::TermException => e
    ::Resque.enqueue_to(:backups, Job, rds_instance_id,
      options.merge(backup_id: job.backup_id, requested: job.requested.to_s))
    job.update_status "Terminated on interrupt signal - requeued"
  end
end

Instance Method Details

#configure_tmp_rdsObject

Updates the Master Password and applies the configured RDS Security Group



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/rds_backup_service/model/backup_job.rb', line 147

def configure_tmp_rds
  update_status "Waiting for instance #{@new_instance.id}..."
  @new_instance.wait_for { ready? }
  update_status "Modifying RDS attributes for new RDS #{@new_instance.id}"
  @rds.modify_db_instance(@new_instance.id, true, {
    'DBParameterGroupName'  => @original_server.db_parameter_groups.
                                first['DBParameterGroupName'],
    'DBSecurityGroups'      => [ @config['rds_security_group'] ],
    'MasterUserPassword'    => @new_password,
  })
end

#create_disconnected_rds(new_rds_name = nil) ⇒ Object

Step 1 of the overall process - create a disconnected copy of the RDS



91
92
93
94
95
96
97
98
99
100
# File 'lib/rds_backup_service/model/backup_job.rb', line 91

def create_disconnected_rds(new_rds_name = nil)
  @new_rds_id = new_rds_name if new_rds_name
  prepare_backup unless @original_server  # in case run as a convenience method
  snapshot_original_rds
  create_tmp_rds_from_snapshot
  configure_tmp_rds
  wait_for_new_security_group
  wait_for_new_parameter_group # (reboots as needed)
  destroy_snapshot
end

#create_tmp_rds_from_snapshotObject

Creates a new RDS from the snapshot



127
128
129
130
131
132
133
134
135
136
# File 'lib/rds_backup_service/model/backup_job.rb', line 127

def create_tmp_rds_from_snapshot
  unless @new_instance
    update_status "Waiting for snapshot #{@snapshot_id}"
    @snapshot.wait_for { ready? }
    update_status "Booting new RDS #{@new_rds_id} from snapshot #{@snapshot.id}"
    @rds.restore_db_instance_from_db_snapshot(@snapshot.id,
      @new_rds_id, 'DBInstanceClass' => @original_server.flavor_id)
    @new_instance = @rds.servers.get @new_rds_id
  end
end

#delete_disconnected_rdsObject

Destroys the temporary RDS instance



208
209
210
211
# File 'lib/rds_backup_service/model/backup_job.rb', line 208

def delete_disconnected_rds
  update_status "Deleting RDS instance #{@new_instance.id}"
  @new_instance.destroy
end

#destroy_snapshotObject

Destroys the snapshot if it exists



139
140
141
142
143
144
# File 'lib/rds_backup_service/model/backup_job.rb', line 139

def destroy_snapshot
  if (@snapshot = @rds.snapshots.get @snapshot_id)
    update_status "Deleting snapshot #{@snapshot.id}"
    @snapshot.destroy
  end
end

#download_data_from_tmp_rdsObject

Connects to the RDS server, and dumps the database to a temp dir



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/rds_backup_service/model/backup_job.rb', line 191

def download_data_from_tmp_rds
  @new_instance.wait_for { ready? }
  db_name = @original_server.db_name
  db_user = @original_server.master_username
  update_status "Dumping database #{db_name} from #{@new_instance.id}"
  dump_time   = @snapshot ? Time.parse(@snapshot.created_at.to_s) : Time.now
  date_stamp  = dump_time.strftime("%Y-%m-%d-%H%M%S")
  @sql_file = "#{@config['tmp_dir']}/#{@s3_path}/#{db_name}.#{date_stamp}.sql.gz"
  hostname  = @new_instance.endpoint['Address']
  dump_cmd  = "mysqldump -u #{db_user} -h #{hostname} "+
    "-p#{@new_password} #{db_name} | gzip >#{@sql_file}"
  FileUtils.mkpath(File.dirname @sql_file)
  @log.debug "Executing command: #{dump_cmd}"
  `#{dump_cmd}`
end

#perform_backupObject

Top-level, long-running method for performing the backup.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/rds_backup_service/model/backup_job.rb', line 74

def perform_backup
  begin
    prepare_backup
    update_status "Backing up #{rds_id} from account #{}"
    create_disconnected_rds
    download_data_from_tmp_rds    # populates @sql_file
    delete_disconnected_rds
    upload_output_to_s3
    update_status "Backup of #{rds_id} complete"
    send_mail
  rescue Exception => e
    update_status "ERROR: #{e.message.split("\n").first}", 500
    raise e
  end
end

#prepare_backupObject

Queries RDS for any pre-existing entities associated with this job. Also waits for the original RDS to become ready.



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/rds_backup_service/model/backup_job.rb', line 104

def prepare_backup
  unless @original_server = RDSBackup.get_rds(rds_id)
    names = RDSBackup.rds_accounts.map {|name, | name }
    raise "Unable to find RDS #{rds_id} in accounts #{names.join ", "}"
  end
  @account_name = @original_server.[:name]
  @rds = ::Fog::AWS::RDS.new(
    RDSBackup.rds_accounts[@account_name][:credentials])
  @snapshot         = @rds.snapshots.get @snapshot_id
  @new_instance     = @rds.servers.get @new_rds_id
end

#s3Object

lazily initializes and returns S3 connection



250
# File 'lib/rds_backup_service/model/backup_job.rb', line 250

def s3 ; @s3 ||= RDSBackup.s3 end

#send_mailObject

Sends a status email



238
239
240
241
242
243
244
245
246
247
# File 'lib/rds_backup_service/model/backup_job.rb', line 238

def send_mail
  return unless @options[:email]
  @log.info "Emailing #{@options[:email]}..."
  begin
    RDSBackup::Email.new(self).send!
    @log.info "Email sent to #{@options[:email]} for job #{backup_id}"
  rescue Exception => e
    @log.warn "Error sending email: #{e.message.split("\n").first}"
  end
end

#snapshot_original_rdsObject

Snapshots the original RDS



117
118
119
120
121
122
123
124
# File 'lib/rds_backup_service/model/backup_job.rb', line 117

def snapshot_original_rds
  unless @new_instance || @snapshot
    update_status "Waiting for RDS instance #{@original_server.id}"
    @original_server.wait_for { ready? }
    update_status "Creating snapshot #{@snapshot_id} from RDS #{rds_id}"
    @snapshot = @rds.snapshots.create(id: @snapshot_id, instance_id: rds_id)
  end
end

#to_jsonObject

returns a JSON-format String representation of this backup job



38
39
40
41
42
43
44
45
46
47
# File 'lib/rds_backup_service/model/backup_job.rb', line 38

def to_json
  JSON.pretty_generate({
    rds_instance:   rds_id,
    account_name:   ,
    backup_status:  status,
    status_message: message,
    status_url:     status_url,
    files:          files,
  })
end

#update_status(message, new_status = nil) ⇒ Object

Writes a new status message to the log, and writes the job info to S3



229
230
231
232
233
234
235
# File 'lib/rds_backup_service/model/backup_job.rb', line 229

def update_status(message, new_status = nil)
  @log      = @options[:logger] || RDSBackup.default_logger(STDOUT)
  @message  = message
  @status   = new_status if new_status
  @status == 200 ? (@log.info message) : (@log.error message)
  write_to_s3
end

#upload_output_to_s3Object

Uploads the compressed SQL file to S3



214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/rds_backup_service/model/backup_job.rb', line 214

def upload_output_to_s3
  update_status "Uploading output file #{::File.basename @sql_file}"
  dump_path = "#{@s3_path}/#{::File.basename @sql_file}"
  s3.put_object(@bucket, dump_path, File.read(@sql_file))
  upload = s3.directories.get(@bucket).files.get dump_path
  @files = [ {
    name: ::File.basename(@sql_file),
    size: upload.content_length,
    url:  upload.url(Time.now + (3600 * 24 * 30))  # 30 days from now
  } ]
  @log.info "Deleting tmp directory #{File.dirname @sql_file}"
  FileUtils.rm_rf(File.dirname @sql_file)
end

#wait_for_new_parameter_groupObject

Wait for the new RDS Parameter Group to become ‘in-sync’



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/rds_backup_service/model/backup_job.rb', line 173

def wait_for_new_parameter_group
  old_name = @original_server.db_parameter_groups.first['DBParameterGroupName']
  update_status "Applying parameter group #{old_name} to #{@new_instance.id}"
  job = self  # save local var for closure in wait_for, below
  @new_instance.wait_for {
    new_group = (db_parameter_groups.select do |group|
      group['DBParameterGroupName'] == old_name
    end).first
    status = (new_group ? new_group['ParameterApplyStatus'] : 'Unknown')
    if (status == "pending-reboot")
      job.update_status "Rebooting RDS #{id} to apply ParameterGroup #{old_name}"
      reboot and wait_for { ready? }
    end
    status == 'in-sync' && ready?
  }
end

#wait_for_new_security_groupObject

Wait for the new RDS Security Group to become ‘active’



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/rds_backup_service/model/backup_job.rb', line 160

def wait_for_new_security_group
  old_group_name = @config['rds_security_group']
  update_status "Applying security group #{old_group_name}"+
    " to #{@new_instance.id}"
  @new_instance.wait_for {
    new_group = (db_security_groups.select do |group|
      group['DBSecurityGroupName'] == old_group_name
    end).first
    (new_group ? new_group['Status'] : 'Unknown') == 'active'
  }
end

#write_to_s3Object

Writes this job’s JSON representation to S3



50
51
52
53
54
55
56
57
58
# File 'lib/rds_backup_service/model/backup_job.rb', line 50

def write_to_s3
  status_path = "#{@s3_path}/status.json"
  s3.put_object(@bucket, status_path, "#{to_json}\n")
  unless @status_url
    expire_date = Time.now + (3600 * 24)  # one day from now
    @status_url = s3.get_object_http_url(@bucket, status_path, expire_date)
    s3.put_object(@bucket, status_path, "#{to_json}\n")
  end
end