Linuxで定期的にバックアップをとるためにやること

この記事は、研究室の計算機の定期バックアップを行った時の作業をまとめたものです。この記事を書き終わるころにはもうまもなくこの研究室を卒業するので、これをもって後輩への引き継ぎ資料にする予定です。この前後輩はechoってなんすかって言ってたので、なるべく優しめに書ければと思っています。また、いくつかスクリプトを載せましたが、よりスマートに改善できる書き方があれば教えてください。文中の<>で囲まれた部分は適宜自分の環境に合わせて変更してください。


##作業環境
+ Linux version 2.6
+ CentOS release 6.8
+ sudo権限はまだない

#内容
1. Linuxにおける一般ユーザーへのsudo 権限の付与
2. Linuxにおける外付けHDDのフォーマット
3. Linuxにおける外付けHDDの認識と設定
4. バックアップ・スクリプトの作成
5. cronを用いたバックアップのスケジューリング
6. ファイル容量が大きくなりすぎたら警告を出す
7. 解凍


## 0. 準備
以下の準備を行います。

+ <div>まずこれから色々やるにあたってsudoの権限が必要になります。なので、sudo権限を与えるに当たって、準備をします。人のファイルをうっかり消したら大変なので、`~/.bashrc`か`~/.bash_profile`に
</div>
```~/.bashrc
alias rm="rm -i"
```
とでも付け加えておいて下さい。気休め程度ですが。


+ 今回登場するディレクトリは、以下の通りです。
    - スクリプト等を置く作業用ディレクトリ：/var/backup
    - バックアップを取るディレクトリ：/home
    - バックアップを保存するディレクトリ：/mnt/backup3TB



+ データを保存するための外付けHDDを買ってきました。今回はIO-DATAの3TBのものを使います。


#1. Linuxにおける一般ユーザーへのsudo 権限の付与

今使っているユーザーアカウントにsudo権限を付与してもらいましょう。
sudo権限を与えられるのはsudoが使えるユーザーのみです。
ここを参考にしました：[CentOS6 ユーザの追加と削除 sudoユーザに追加]
(http://d.hatena.ne.jp/dooluka/20130520/1368977853)

/etc/sudoersファイルの

```shell
## Allow root to run any commands anywhere
root      ALL=(ALL)       ALL
```

の下に

```sh

<my_user_name>  ALL=(ALL)       ALL
```
追加すればよいです。ここにユーザー名を書くと"Allow root to run any commands anywhere"ってことで、なんでもできるようになります。



##2. Linuxにおける外付けHDDのフォーマット
参考：[大容量USBハードディスクをLinuxで利用する方法](https://hit.hateblo.jp/entry/2016/01/17/010230)
この方も言っているように、購入時の初期ファイルシステムではLinuxは2TB以上のディスクを認識できないようです。なんじゃそりゃ。

なので買ってきたばかりの外付けHDDの形式では認識できません。一旦フォーマットして、新たに形式を設定しましょう。参考先のリンクに書いてある["ext4-でフォーマット開始"](https://qiita.com/hit/items/32ce5a0f4ea8130549af#ext4-でフォーマット開始)等を見ながらフォーマットをしました。



##3. Linuxにおける外付けHDDの認識と設定

###外付けHDDを認識させる
とりあえずは買ってきた外付けHDDを接続して下さい。接続したら以下のコマンドを入力し、現在接続されているディスクの使用状況を確認しましょう。(参考：[Ubuntuで外付けHDDをマウントする](https://qiita.com/sasayabaku/items/bcfcc37e8a34b00ca6e9))

```
sudo fdisk -l
```

これによって以下のように接続されたHDDの情報が分かります。

```sh
...前略...

ディスク /dev/sdc: 3000.6 GB, 3000592982016 バイト
ヘッド 255, セクタ 63, シリンダ 364801
Units = シリンダ数 of 16065 * 512 = 8225280 バイト
セクタサイズ (論理 / 物理): 512 バイト / 4096 バイト
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
ディスク識別子: 0x00000000

デバイス ブート      始点        終点     ブロック   Id  システム
/dev/sdc1               1      267350  2147483647+  ee  GPT
Partition 1 does not start on physical sector boundary.
```
この場合、`/dev/sdc1`がデバイス名ですね。
デバイス名が分かったのでマウントしましょうか。

```sh

sudo mount -t ext4 /dev/sdc1 /mnt/backup3TB/
```
ext4という形式でデバイス名`/dev/sdc1`を`/mnt/backup3TB/`という名前でマウントするという命令です。このコマンドを行うことで、ファイルシステム一覧(`df`とか打ってみて)に`/mnt/backup3TB/`が追加されたと思います。


###外付けHDDを取り外す

HDDを取り外したくなったら、乱暴に引っこ抜くんじゃなくて、アンマウントしてから抜いてあげましょう。コマンドは下の通りです。

```sh

sudo umount /mnt/backup3TB
```


###自動認識させる

電源がなんらかの原因で落ちた・落とした時に、上記の作業が毎回必要なようでは面倒くさくてたまりません。自動でマウントするようにしてあげましょう。
``/etc/fstab ``に起動時にどのようにファイルシステムを認識するかが書いてあります。

まずデバイス固有のUUID(Universally Unique Identifier)を調べます。

```
ls -l /dev/disk/by-uuid/
```
と打つと、UUIDが表示されます。接続前と接続後にやればどれがHDDのUUIDかわかります。この場合、下のように、`d0deb810-614f-4f28-87e0-c6967c1dfa68`がUUIDでした。

```
lrwxrwxrwx 1 root root 10  9月  5 16:42 2018 d0deb810-614f-4f28-87e0-c6967c1dfa68 -> ../../sdc1
```




UUIDが分かったので``/etc/fstab ``を開いて、

```sh

#/dev/sdc1:
UUID=d0deb810-614f-4f28-87e0-c6967c1dfa68 /mnt/backup3TB ext4 defaults 0 0
```
と追加します。

##4. バックアップ・スクリプトの作成

ここから頑張ってバックアップ用のスクリプトを作ります。適当にネットのスクリプトを見ながら書きます。

### 構成
- setting.sh : 共通の変数を設定してます。
- backup.sh : バックアップを取る作業をします。
- delete.sh : (古い)バックアップを消します。
- delete_or_not.py : 引数に応じてバックアップを消すべきかどうか判断します。

### setting.sh
```setting.sh
##################################################
# バックアップ対象
TARGET_DIR=/home
# バックアップ先
BACKUP_DIR=/mnt/backup3TB/home
# バックアップ後のファイル名
BACKUP_FILE_NAME=home-`/bin/date +%Y-%m-%d_%H:%M:%S`.tar.lzo
##################################################
function err(){
        echo $@ 1>&2
        exit 1
}
```

#### 所々説明
``home-`/bin/date +%Y-%m-%d_%H:%M:%S`.tar.lzo``の``` `` ```は、``` `` ```内のコマンドを実行し出力したものを文字として代入するよっていうコマンドです。`/bin/date +%Y-%m-%d_%H:%M:%S`は今の時間を`+%Y-%m-%d_%H:%M:%S`ってフォーマットで出力するって命令です。2019年11月2日 3時58分13秒は`2019-11-2_03:58:13`となります。

functionのerrってやつは、単にエラーメッセージを表示して終了するだけの処理をします。スクリプトがこけたらこいつで食い止めます。いろんなとこで使いたいので共通の変数と一緒のとこに置いときます。

### backup.sh
```backup.sh:
#!/bin/bash
source /var/backup/setting.sh 

echo -e "\nstart backup.sh : `date`"
if [ ! -e $BACKP_DIR ]; then
	err "Bckup directory is not found: dirname = "$BACKP_DIR 
fi

# バックアップ処理
echo start zip : `date` 
if [ -d $TARGET_DIR ]; then
#	echo tar --directory="${TARGET_DIR}" --use-compress-program=lzop --warning=no-file-changed -cpf ${BACKUP_DIR}/${BACKUP_FILE_NAME} ./
	tar --directory="${TARGET_DIR}" --use-compress-program=lzop --warning=no-file-changed -cpf ${BACKUP_DIR}/${BACKUP_FILE_NAME} ./
	if [ $? -eq 0 ] || [ $? -eq 1 ]; then 
		echo Backup ended : `date`
	else
		err "Something wrong in making backup files  :  `date` "
	fi
else
	err "Target directory is not found: dirname = "$TARGET_DIR
fi
```
####所々説明
`source /var/backup/setting.sh`：共通の設定を読み込ませてます。crontab(後述)に書いても良かったんだけど、誤操作で消えたりすることも考えて、わざわざ新しいファイル作ってる。なんかいい方法ないかなー。

バックアップ処理中のコメント：スクリプトの挙動を確認するのに、よくechoをつけて走らせてます。 これはその名残で、一応残してます。

`--use-compress-program=lzop`：圧縮形式はlzopで。
圧縮形式を考える上で[こちらの記事](http://d.hatena.ne.jp/tkrd/20120809/1344500358)を参考にしました。lzopは圧縮形式がかなり早いみたいです。バックアップ時間が長くなると外界の影響によるバックアップ失敗の確率が増えそうかなと思い、なるべく早くしたかったという理由があります。


### delete.sh
```delete.sh
#!/bin/bash
source /var/backup/setting.sh
if [ ! -e "${BACKUP_DIR}" ];then
	err "Not found BACKUP_DIR"
fi

# 古いバックアップを削除
for f in ${BACKUP_DIR}/*.tar.lzo; do
　　ret=`python /var/backup/delete_or_not.py -d $f -f $1 -t $2`
　　if [ "${ret}" = "Delete"  ]; then
　　　　rm -f $f
       if [ $? -eq 0 ] ; then 
　　　　　　echo Deleted $f : `date`
　　　　else
　　　　　　err "Something wrong in deleting files $f : `date` "
　　　　fi
　　fi
done

[ $? -eq 0 ] && echo Deleting old backups ended : `date`
```

### 所々説明
使い方例：`./delete.sh 7 14`
--> 現在から7日から14日前にバックアップが取られたファイルを消去する。



### delete_or_not.py
```delete_or_not.py
import datetime
import argparse
import re 

pars = argparse.ArgumentParser(description='Date')
pars.add_argument('date', type=str, default=None, nargs="?")
pars.add_argument('-d', '--dir',default=None)
pars.add_argument('-f','--from_d',default=None)
pars.add_argument('-t','--to_d',default=None)
args = pars.parse_args()

def main():
	global args
	
	try:
		date=get_date()
		d1 = datetime.datetime.strptime(date, '%Y-%m-%d_%H:%M:%S')
	except:
		exit()
	
	now = datetime.datetime.now()
	dt = now - d1
	
	del_flg=0	
	if (args.from_d is None) and (args.from_d is None):
		exit()
	else:
		if args.from_d is None:
			args.from_d=0	
		if args.to_d is None:
			args.to_d=1e30

	if ( dt.days >= int(args.from_d) ) and ( dt.days <= int(args.to_d) ):
		del_flg=1

	return_Keep_or_Delete( del_flg )
	
def get_date():
	global args
        if args.dir is not None:
                try:
                        return re.findall('\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}', args.dir)[0]
                except:
                        exit()
        else:
                return args.date

def return_Keep_or_Delete(i):
	if i==0:
		print("Keep")
	else:
	    print("Delete")

main()
```

### 所々説明
あるファイルを消すべきか・残すべきか判断するスクリプトです。少しややこしかったのでPythonで書きました。


##5. cronを用いたバックアップのスケジューリング

crontabに書かれた命令は定期的に実行されます。
編集方法は、

```
sudo crontab -u root -e
```
こうやってrootユーザーで編集します。crontabはユーザーごとに存在するので、権限もユーザーごとに違います(多分)。

crontabに記述されている内容は以下のようになります。

```sh:crontab

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  *  command to be executed

MAILTO="hoge@hoge.com"
PATH=/usr/local/sbin:/usr/bin:/bin:/usr/local/bin

0  8 * * * /bin/bash /var/backup/backup.sh        >>/var/backup/ct_log
21 8 * * 1 /bin/bash /var/backup/delete.sh 1 1   >>/var/backup/ct_log
23 8 * * 3 /bin/bash /var/backup/delete.sh 1 1   >>/var/backup/ct_log
24 8 * * 4 /bin/bash /var/backup/delete.sh 1 1   >>/var/backup/ct_log
26 8 * * 6 /bin/bash /var/backup/delete.sh 1 1   >>/var/backup/ct_log
30 8 * * 1 /bin/bash /var/backup/delete.sh 8 13   >>/var/backup/ct_log
50 8 * * 1 /bin/bash /var/backup/delete.sh 63 200 >>/var/backup/ct_log
00 9 * * * /bin/bash /var/backup/alert.sh

## Sketch of Backup
# I will take buckups with the following roule.
# I detete backups frequently due to limitation of HDD capacity.
# If there were large amount of the capacity, I couldn't care about it...
#
# Maximum number of backups (between making and deleting backups on Mon) : 15
# B=Backup x=Delete_a_Backup _=Nothing
#      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
# Mon  B _ _ _ _ _ _ _ _ _ _ _ _ _ _     <-- Begining of Backup
# Tue  B B _ _ _ _ _ _ _ _ _ _ _ _ _
# Wed  B x B _ _ _ _ _ _ _ _ _ _ _ _
# Thu  B x _ B _ _ _ _ _ _ _ _ _ _ _
# Fri  B B _ _ B _ _ _ _ _ _ _ _ _ _
# Sat  B x B _ _ B _ _ _ _ _ _ _ _ _
# Sun  B B _ B _ _ B _ _ _ _ _ _ _ _
# Mon  B x B _ B _ _ B x x x x x x _
# Tue  B B _ B _ B _ _ B _ _ _ _ _ _
#
# .................
#
# Mon  B x B _ B _ _ B x x x x x x B
# Tue  B B _ B _ B _ _ B _ _ _ _ _ _
# Wed  B x B _ B _ B _ _ B _ _ _ _ _
# Thu  B x _ B _ B _ B _ _ B _ _ _ _
# Fri  B B _ _ B _ B _ B _ _ B _ _ _
# Sat  B x B _ _ B _ B _ B _ _ B _ _
# Sun  B B _ B _ _ B _ B _ B _ _ B _
# Mon  B x B _ B _ _ B x x x x x x B
```



`MAILTO="hoge@hoge.com"`：ここにメールアドレスを書いておくと、エラーが出たときはメールで教えてくれます。これが結構便利。当然バグがないようにスクリプトは書くけども、何があるかわからんし、いつも監視してたくないので、プログラムがこけた時に自分から教えてくれると助かります。
ただしGmailへのメール送信は、googleが信頼性の低いアプリからのメールを受け付けないため、別途設定する必要があります。何を参考に設定したか忘れてしまったのでここでは省略しますが、"Linux CentOS6 mail gmail"みたいなキーワードを元に検索した気がします。



`0  8 * * * /bin/bash /var/backup/backup.sh        >>/var/backup/ct_log`：これは毎日朝8時に`/bin/bash /var/backup/backup.sh        >>/var/backup/ct_log`をするっていう命令です。つまりバックアップをとります。

実行結果は`ct_log`に流します。たまに眺めて変な挙動がないか確認します。 


```sh:
23 8 * * 3 /bin/bash /var/backup/delete.sh 1 1   >>/var/backup/ct_log
...
50 8 * * 1 /bin/bash /var/backup/delete.sh 63 200 >>/var/backup/ct_log
```
ここにはいつbackupを削除するかが書いてあります。HDDの容量がそんなに大きくないので、なるべく工夫して最近のものはある程度ちゃんと残しつつ昔のもの(ここでは8週間)も残すようにします。　

設計時のメモがコメントアウトで書かれてます。今の設定は月・水・木・土曜日に昨日のバックアップを消して、１週間に3~4のバックアップファイルが存在するようにします。そこから先は１週間ごとのファイルを残すよううまい具合にバックアップファイルを消します。この辺は適宜変えて下さい。


##6. ファイル容量が大きくなりすぎたら警告を出す
```alert.sh
current_filesize=`ls /mnt/backup3TB/home/ -lht | awk '(NR==2){print $5}'`

# current filesize * 15 files < 2.7TB --> current_filesize < 180G
if [[ ${current_filesize/G/} -gt 180 ]]; then 
        echo 'Yabai'
fi
```
一つ辺りのファイル容量が大きくなりすぎたら、忘れた頃に「ファイル容量100% --> バックアップ失敗 --> 予期せぬエラー」とかになりかねないです。最新のバックアップファイル容量を監視するようなスクリプトを書いておいて、これも毎日crontabで実行させます。標準出力の'Yabai'は最終的に自分のメールボックスに届くはずです。


##7.解凍
最後に圧縮したファイルを解凍してみましょう。
圧縮時にlzopを使ったので、解凍形式もlzopを指定します。(指定仕方：[圧縮、解凍速度が早いlzop圧縮を使おう](https://qiita.com/tukiyo3/items/938c8b8e747ec87434de))

```
tar --lzop -xf /mnt/backup3TB/home/なんとかかんとか.tar.lzo
```

<!--
##8.今後の展望
改善案を記しておきます。

まず現段階では毎回の真新しくコピーをとっているので、
-->


## おわりに
思ったより長くなってしまった。また気が向いたら編集します。