Cosnomi

プログラミングをする医学生。物を作ること、自動化すること、今まで知らなかったことを知ることが好き。TypeScript書いたり、Pythonで機械学習したりなど。

Twitter / GitHub / GPG key / Fediverse / My Page
TOP >

CentOS7のLVMバックアップ作業をPythonで自動化してみた

この記事は、下の2つの記事が前提となっておりますので、xfsdumpやLVMスナップショットをご存知ない方はぜひ読んでみてください。

https://blog.cosnomi.com/archives/169

https://blog.cosnomi.com/archives/258

さて、前回までの記事でサーバーを停止せずにフルバックアップを取る方法を学びましたが、スナップショット領域を作成したり削除したりといろいろ面倒です。面倒なことは自動化するのが鉄則ですよね。ということで、今回はこのバックアップ作業を自動化したいと思います。

バックアップポリシー

いくらデータ消失が不安だからといって、毎日フルバックアップを行うのは、ストレージや負荷の面から現実的ではありません。xfsdumpでは差分バックアップを取れますので、私は次のような方針でバックアップを取りたいと思います。

  • 3月と9月の第一火曜日にフルバックアップ
  • 毎週水曜日にはフルバックアップとの差分バックアップ
  • それ以外の日には水曜日のバックアップとの差分バックアップ

なお、日付の判定は複雑になるのでPython側で行い、同じスクリプトを毎日一定の時刻にcronで呼び出すようにします。

コード

コードをいくつかの部分に分けて紹介します。結構前に書いたコードで命名規則がPythonっぽくないですが、ご容赦ください。なお、コピー&ペーストで済ませたい人は、最後にまとめてコードと変更すべき箇所を書いているのでそちらをご覧ください。

import

import subprocess
import datetime
import sys
import os

使うのでimportします。subprocessはシェルを実行するために、datetimeは日付判定のために、sysはexitのために、osはファイル関係の操作のためにです。

バックアップレベルの判定

def ShouldFullBackup(dt):
    """
    戻り値
    True: 行う
    False: 行わない
    """
    if dt.weekday()!=1:
        return False
    if dt.day > 7:
        return False
    if dt.month not in (3,9):
        return False
    return True

def ShouldLv1Backup(dt):
    """
    水曜日にはLv1のバックアップを行う
    戻り値
    True: 行う
    False: 行わない
    """
    if dt.weekday()==2:
        return True
    return False

def GetLevel(dt):
    if ShouldFullBackup(dt):
        return 0
    if ShouldLv1Backup(dt):
        return 1
    else:
        return 2

この部分ではdatetime型の変数dtを用いて「フルバックアップを行うか」「Lv1バックアップを行うか」を判定し、レベルの数字を返しています。フルバックアップは0です。

曜日判定を関数にすることで見通しが良くなります。自分好みに変えてみてください。

ファイルの命名

dumpファイルの名前に日付やレベルや場所の情報を入れて分かりやすくします。これらの情報を含めておかないと復元のときに面倒です。

def GetSaveName(dt, media, level):
    return '{0}-{1}-lv-{2}.dump'.format(dt.strftime('%Y-%m-%d-%H-%M-%S'), media, str(level))

各種コマンド

スナップショットを取ったり消したりなどのコマンドを、メソッドにして定義しておきます。

まず定数を宣言しておきます。

snap_path_prefix = '/mnt/snap-'
backup_path = '/mnt/backup1'
backup_dev = '/dev/sdb1'
vg_name = 'cl'

snap_path_prefixは、スナップショットのマウント先となるパスの前につくやつです。backup_devはバックアップを保存するディスクで、backup_pathにマウントされます。vg_nameはスナップショットのボリュームグループ名です。

基本的に変える必要がありそうなのは、backup_dev(とbackup_path)くらいですね。それ以外はそのままで大丈夫だと思います。

def DumpCommand(media_name, level, save_name, snap_path):
    com = ['/sbin/xfsdump', '-M', media_name, '-L', 'backup', '-l', str(level), '-u', '-f', save_name, snap_path, '-p', '300']
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Dump failed' ,'{0} failed:n{1}'.format(com, e.args))
        raise

def ZipCommand(save_name):
    com = ['/bin/pbzip2', '-p3', save_name]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Zip failed' ,'{0} failed:n{1}'.format(com, e.args))
        raise

def MakeSnapshotCommand(snap_name, media_path):
    com = ['/sbin/lvcreate', '-s', '--size=50G', '--name', snap_name, media_path]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Make snapshot failed' ,'{0} Snapshot failed:n{1}'.format(media_path, e.args))
        raise

def RemoveSnapshotCommand(snap_name):
    com = ['/sbin/lvremove', '{0}/{1}'.format(vg_name, snap_name), '-y']
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Remove snapshot failed' ,'Remove Snapshot failed:n{}'.format(e.args))
        raise

def MountCommand(source, target):
    com = ['/bin/mount', source, target]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Mount failed' ,'{0} Mount failed:n{1}'.format(source, e.args))
        raise

def MountSnapshotCommand(source, target):
    com = ['/bin/mount', '-t', 'xfs', '-o', 'ro,nouuid', source, target]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Mount snapshot failed' ,'{0} Mount failed:n{1}'.format(source, e.args))
        raise

def UmountCommand(target):
    com = ['/bin/umount', target]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Unmount failed' ,'{0} Umount failed:n{1}'.format(target, e.args))
        raise
    Log('Unmount Success')

def MakeMountDirectoryIfNot(snap_path):
    if not os.path.exists(snap_path):
        os.makedirs(snap_path)
    if not os.path.exists(backup_path):
        os.makedirs(backup_path)
    return

コードを見てもらえれば詳しい説明はいらないかと思いますが、ざっくり解説します。

各メソッドでは、subprocess.runを用いてシェルのコマンドを呼び出しています。snap_nameとはスナップショットの名前、snap_pathとはスナップショットへのパスです。

多分、これらのメソッドをどのように呼び出して使っているかを見てみたほうが早いと思うので次に進みます。

バックアップ

if __name__ == '__main__':
    target = [['root', '/dev/mapper/cl-root'], ['home', '/dev/mapper/cl-home']] # ['メディア', '場所']
    dt = datetime.datetime.now()
    MountCommand(backup_dev, backup_path)
    os.chdir(backup_path)
    Log('Backup Mount Success')
    try:
        for t in target:
            try:
                snap_name = 'snap-{}'.format(t[0])
                snap_path = snap_path_prefix + t[0]
                MakeSnapshotCommand(snap_name, t[1])
                MakeMountDirectoryIfNot(snap_path)
                MountSnapshotCommand('/dev/{0}/snap-{1}'.format(vg_name, t[0]), snap_path)
                level = GetLevel(dt)
                save_name = GetSaveName(dt,t[0], level)
                if os.path.exists(save_name):
                    Error('Duplication Error', 'Duplication Error : {}'.format(save_name))
                    raise Exception()
                DumpCommand(t[0], level, save_name, snap_path)
            except subprocess.CalledProcessError:
                raise
            finally:
                try:
                    UmountCommand(snap_path)
                except:
                    pass
                try:
                    RemoveSnapshotCommand(snap_name)
                except:
                    pass
            ZipCommand(save_name)
    except subprocess.CalledProcessError:
        sys.exit()
    finally:
        os.chdir('/')
        UmountCommand(backup_path)

これまで用意してきたメソッドを用いて実際にバックアップの処理をしている部分です。

targetは、第1要素がバックアップ対象の領域の名前(自由)で、第2要素がその領域のパスとなるようなリストのリストになります。ここは、自分の環境に合わせて適宜変更してください。

バックアップファイルを保存するディスクをマウントして、スナップショットを撮ったあと、それをマウントして、xfsdumpでダンプをとった後、そのdumpファイルを圧縮するという流れです。これをバックアップ対象の領域の数だけ繰り返しています。すべて完了したら、バックアップファイルを保存するディスクをumountします。

まとめると…

**下のスクリプトを適当な場所に配置して、cronで適当な時間(深夜か早朝あたり)に呼び出してください。**あくまで一例ですが、普段は2時間くらいで完了します。(フルバックアップ時はかなり時間かかりますが)

import subprocess
import datetime
import sys
import os

"""
cronからAM4:15に呼ばれる予定
要ルート権限
"""
snap_path_prefix = '/mnt/snap-'
backup_path = '/mnt/backup1'
backup_dev = '/dev/sdb1'
vg_name = 'cl'

def Log(title='Backup(server1)', str='', priority=-1):
    subprocess.call('echo %s' % str, shell=True)

def Error(title, str):
    subprocess.call('echo %s | mail -s "***Snapshot Backup System Error***" -r root guppy' % str, shell=True)

def ShouldFullBackup(dt):
    """
    1,5,9月の最初の火曜日にはフルバックアップを行う
    戻り値
    True: 行う
    False: 行わない
    """
    if dt.weekday()!=1:
        return False
    if dt.day > 7:
        return False
    if dt.month not in (3,9):
        return False
    return True

def ShouldLv1Backup(dt):
    """
    水曜日にはLv1のバックアップを行う
    戻り値
    True: 行う
    False: 行わない
    """
    if dt.weekday()==2:
        return True
    return False

def GetLevel(dt):
    if ShouldFullBackup(dt):
        return 0
    if ShouldLv1Backup(dt):
        return 1
    else:
        return 2

def GetSaveName(dt, media, level):
    return '{0}-{1}-lv-{2}.dump'.format(dt.strftime('%Y-%m-%d-%H-%M-%S'), media, str(level))

def DumpCommand(media_name, level, save_name, snap_path):
    com = ['/sbin/xfsdump', '-M', media_name, '-L', 'backup', '-l', str(level), '-u', '-f', save_name, snap_path, '-p', '300']
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Dump failed' ,'{0} failed:n{1}'.format(com, e.args))
        raise

def ZipCommand(save_name):
    com = ['/bin/pbzip2', '-p3', save_name]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Zip failed' ,'{0} failed:n{1}'.format(com, e.args))
        raise

def MakeSnapshotCommand(snap_name, media_path):
    com = ['/sbin/lvcreate', '-s', '--size=50G', '--name', snap_name, media_path]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Make snapshot failed' ,'{0} Snapshot failed:n{1}'.format(media_path, e.args))
        raise

def RemoveSnapshotCommand(snap_name):
    com = ['/sbin/lvremove', '{0}/{1}'.format(vg_name, snap_name), '-y']
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Remove snapshot failed' ,'Remove Snapshot failed:n{}'.format(e.args))
        raise

def MountCommand(source, target):
    com = ['/bin/mount', source, target]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Mount failed' ,'{0} Mount failed:n{1}'.format(source, e.args))
        raise

def MountSnapshotCommand(source, target):
    com = ['/bin/mount', '-t', 'xfs', '-o', 'ro,nouuid', source, target]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Mount snapshot failed' ,'{0} Mount failed:n{1}'.format(source, e.args))
        raise

def UmountCommand(target):
    com = ['/bin/umount', target]
    try:
        subprocess.run(com)
    except subprocess.CalledProcessError as e:
        Error('[ERROR]Unmount failed' ,'{0} Umount failed:n{1}'.format(target, e.args))
        raise

def MakeMountDirectoryIfNot(snap_path):
    if not os.path.exists(snap_path):
        os.makedirs(snap_path)
    if not os.path.exists(backup_path):
        os.makedirs(backup_path)
    return

if __name__ == '__main__':
    target = [['root', '/dev/mapper/cl-root'], ['home', '/dev/mapper/cl-home']] # ['メディア', '場所']
    dt = datetime.datetime.now()
    MountCommand(backup_dev, backup_path)
    os.chdir(backup_path)
    try:
        for t in target:
            try:
                snap_name = 'snap-{}'.format(t[0])
                snap_path = snap_path_prefix + t[0]
                MakeSnapshotCommand(snap_name, t[1])
                MakeMountDirectoryIfNot(snap_path)
                MountSnapshotCommand('/dev/{0}/snap-{1}'.format(vg_name, t[0]), snap_path)
                level = GetLevel(dt)
                save_name = GetSaveName(dt,t[0], level)
                if os.path.exists(save_name):
                    Error('Duplication Error', 'Duplication Error : {}'.format(save_name))
                    raise Exception()
                DumpCommand(t[0], level, save_name, snap_path)
            except subprocess.CalledProcessError:
                raise
            finally:
                try:
                    UmountCommand(snap_path)
                except:
                    pass
                try:
                    RemoveSnapshotCommand(snap_name)
                except:
                    pass
            ZipCommand(save_name)
    except subprocess.CalledProcessError:
        sys.exit()
    finally:
        os.chdir('/')
        UmountCommand(backup_path)

こうなります。コピー&ペーストで済ませたい人も多いので最低限変更するべき箇所をまとめると、

  • backup_dev
  • backup_path
  • vg_name
  • バックアップポリシーの箇所(何曜日にどのレベルのバックアップをするか)
  • target

あたりです。

最後に

お疲れ様でした。

毎回マニュアルに沿って同じ作業を繰り返すのはミスを誘発しますし、何よりエンジニアらしくありません。 面倒な部分は自動化して、より本質的な部分に集中したいですね。

(紹介したスクリプトは私の自宅サーバーで使用しているものですが、利用は自己責任でお願いします。)


Comments

記事一覧へ