Class: BackupMinister::Agent

Inherits:
BackupMinister show all
Defined in:
lib/backup_minister/agent.rb

Instance Method Summary collapse

Methods inherited from BackupMinister

#backup_minister_installed?, #create_nested_directory, #execute, #execute_with_result, #file_sha256_hash, #project_config, #software_installed?, #system_config

Constructor Details

#initialize(file_name = nil) ⇒ Agent

Returns a new instance of Agent.



13
14
15
16
# File 'lib/backup_minister/agent.rb', line 13

def initialize(file_name = nil)
  super
  load_projects(config)
end

Instance Method Details

#archive_and_remove_file(file_name, file) ⇒ Object

Create TAR GZ archive and delete original file



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/backup_minister/agent.rb', line 227

def archive_and_remove_file(file_name, file)
  archive_name = archive_file(file_name, file)
  begin
    File.delete(file)
    # LOGGER.log "File #{file.path} was deleted."
    archive_name
  rescue StandardError => error
    LOGGER.warn "Can't delete file #{file.path} with error: #{error}."
    nil
  end
end

#archive_file(file_name, file) ⇒ Object

Create TAR GZ single file archive

Parameters:

  • file_name (String)

    file name for new archive (including extension)

  • file (String, nil)

    target file name to archive



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/backup_minister/agent.rb', line 197

def archive_file(file_name, file)
  target = Tempfile.new(file.path)
  Gem::Package::TarWriter.new(target) do |tar|
    file = File.open(file.path, 'r')
    tar.add_file(file.path, file.stat.mode) do |io|
      io.write(file.read)
    end
    file.close
  end
  target.close

  final_file_name = "#{File.expand_path(File.dirname(file.path))}/#{file_name}"
  Zlib::GzipWriter.open(final_file_name) do |gz|
    gz.mtime = File.mtime(target.path)
    gz.orig_name = final_file_name
    gz.write IO.binread(target.path)
  end

  if File.exist?(file_name)
    file_name
  else
    nil
  end
ensure
  target.close if target and !target.closed?
  nil
end

#backup_database(project_name) ⇒ String?

Create TAR GZ archive with database backup

Parameters:

Returns:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/backup_minister/agent.rb', line 38

def backup_database(project_name)
  base_name = project_name + '_' + Time.new.strftime('%Y%m%d_%H%M')
  if project_config(name: project_name)['database']
    backup_file = make_database_backup(project_config(name: project_name)['database'], base_name)
    if backup_file.nil?
      LOGGER.error "Can't create database backup for project #{project_name}."
    else
      archive_file_name = archive_and_remove_file("#{base_name}.tar.gz", backup_file)
      if archive_file_name
        LOGGER.debug "Database archive file #{archive_file_name} created."
        return archive_file_name
      end
    end
  else
    LOGGER.error "No database config found for #{project_name}."
  end

  nil
end

#check_server_requirementsBool

Check connection settings

Returns:

  • (Bool)

    is it possible to establish connection?



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/backup_minister/agent.rb', line 61

def check_server_requirements
  result = false
  host = system_config('server', 'host')
  user = system_config('server', 'user')

  if user and host
    begin
      connection = Net::SSH.start(host, user)
      LOGGER.info "Connection with server #{user}@#{host} established."

      remote_install = connection.exec!("gem list -i #{APP_NAME}")
      if remote_install.to_s.strip == 'true'
        LOGGER.debug "#{APP_NAME} installed on remote server."
        result = true
      else
        LOGGER.error "#{APP_NAME} is not installed on remote server: `#{remote_install.strip}`."
      end
    rescue Exception => error
      LOGGER.error "Could not establish connection: #{error.message}"
        result = false
    ensure
      connection.close if !connection.nil? and !connection.closed?
    end
  else
    LOGGER.error 'Server user or host (or both of them) are not defined.'
  end

  result
end

#database_file_valid?(file) ⇒ Bool

Check basic content of SQL backup file

Parameters:

  • file (File)

Returns:

  • (Bool)


135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/backup_minister/agent.rb', line 135

def database_file_valid?(file)
  if file.size
    LOGGER.debug "File #{file.path} is #{file.size} bytes."
    content = file.read
    if Regexp.new('(.)+(PostgreSQL database dump)(.)+(PostgreSQL database dump complete)(.)+', Regexp::MULTILINE).match(content).to_a.count >= 4
      LOGGER.debug "File #{file.path} looks like DB dump."
      result = true
    else
      LOGGER.warn "File #{file.path} doesn't looks like DB dump."
      result = false
    end
  else
    LOGGER.warn "File #{file.path} has 0 length or doesn't exists."
    result = false
  end

  result
end

#execute_remotelyObject



256
257
258
259
260
# File 'lib/backup_minister/agent.rb', line 256

def execute_remotely
  remote_server_connect
  yield if block_given?
  remote_server_close_connection
end

#load_projects(config) ⇒ Object

Load projects list if exists



92
93
94
95
96
97
98
99
# File 'lib/backup_minister/agent.rb', line 92

def load_projects(config)
  if config['projects'].nil? or config['projects'].empty?
    LOGGER.warn 'No projects found. Nothing to do.'
  else
    @projects = config['projects'].keys
    LOGGER.info "Found projects: #{@projects.join(', ')}."
  end
end

#make_database_backup(database_config, base_name, container_name = nil) ⇒ File?

Generate database SQL backup file

Parameters:

  • database_config (Hash)

    connection database settings

  • base_name (String)

    name for backup

Returns:

  • (File, nil)

    path to SQL file

Raises:

  • (ArgumentError)


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/backup_minister/agent.rb', line 107

def make_database_backup(database_config, base_name, container_name = nil)
  raise ArgumentError, "Driver #{database_config['driver']} not supported." unless DATABASE_DRIVERS.include?(database_config['driver'])

  container_name = setting('database_container')
  if container_name
    backup_file_name = base_name + '.sql'
    command = "docker exec -i #{container_name} pg_dump #{database_config['name']}"
    command += " -U#{database_config['user']}" if database_config['user']
    command += " > #{backup_file_name}"
    if execute(command)
      if File.exist?(backup_file_name)
        LOGGER.debug "Database backup file #{backup_file_name} created."
        file = File.open(backup_file_name, 'r')
        database_file_valid?(file) ? file : nil
      else
        LOGGER.error "Can't create database backup file `#{backup_file_name}`."
      end
    end
  else
    LOGGER.error 'Database container name is missing. Can\'t backup.'
  end
end

#place_database_backup(file_path) ⇒ String?

Move database backup to server

Returns:

  • (String, nil)

    path to backup on remote server



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/backup_minister/agent.rb', line 157

def place_database_backup(file_path)
  result = nil
  destination_directory = '/tmp/'
  scp_command = "scp #{file_path} #{system_config('server', 'user')}@#{system_config('server', 'host')}:#{destination_directory}"
  if execute(scp_command)
    LOGGER.debug "Backup #{file_path} placed remotely to #{destination_directory}."

    FileUtils.rm(file_path)
    LOGGER.debug "Local file #{file_path} removed."

    result = destination_directory + File.basename(file_path)
  else
    LOGGER.error "Could not move #{file_path} to server."
  end
  result
end

#process_remote_database_backup(project_name, remote_file_path, sha256 = nil) ⇒ Bool

Execute command on remote server for processing database backup

Parameters:

Returns:

  • (Bool)

    is operation success?



181
182
183
184
185
186
187
188
189
190
191
# File 'lib/backup_minister/agent.rb', line 181

def process_remote_database_backup(project_name, remote_file_path, sha256 = nil)
  result = false
  command = "#{APP_NAME} store_database_backup"
  command += " --project_name=#{project_name}"
  command += " --file=#{remote_file_path}"
  command += " --sha256=#{sha256}" unless sha256.nil?

  execute_remotely { result = (@remote_server_connection.exec!(command).exitstatus == 0) }

  result
end

#remote_server_close_connectionObject



252
253
254
# File 'lib/backup_minister/agent.rb', line 252

def remote_server_close_connection
  @remote_server_connection.close if !@remote_server_connection.nil? and !@remote_server_connection.closed?
end

#remote_server_connectObject



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/backup_minister/agent.rb', line 239

def remote_server_connect
  if check_server_requirements
    begin
      host = system_config('server', 'host')
      user = system_config('server', 'user')

      @remote_server_connection = Net::SSH.start(host, user)
    rescue Exception => error
      LOGGER.error "Could not establish connection: #{error.message}"
    end
  end
end

#setting(name) ⇒ Object?

Get setting item value

Parameters:

  • name (String)

    Settings name

Returns:

  • (Object, nil)

    return settings value if present



23
24
25
26
27
28
29
30
31
# File 'lib/backup_minister/agent.rb', line 23

def setting(name)
  if @config['settings'].nil?
    LOGGER.error 'Settings are empty'
  elsif @config['settings'][name].nil?
    LOGGER.error "Setting #{name} is missing."
  else
    @config['settings'][name]
  end
end