2012年6月19日火曜日

第2水曜日にcronスケジュールしたい

第2水曜日および第3水曜日にcronスケジュールしたいと思ったのですが、調べてみるとcronで指定可能なパターンでは、スケジュールできないことを知りました。

http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=20020&forum=10

ZFS ファイルシステムの scrub 処理 (整合性チェック) を定期的に動かしたいのですが、zfs-fuse に標準で備わっている設定方法だと、毎週行うか否かの2択になっており、それはちょっと動かし過ぎと思いました。
RAIDZ と RAIDZ2 の2つのストレージプール (tank1, tank2) を作成して ZFS を利用していますが、tank1 は月1回、tank2 は2ヶ月に1回の頻度で scrub を動かしたいため、前述の URL の情報を参考に、自作スクリプトを作りました。どなたかの参考になれば幸いと思いますので、掲載します。
#!/bin/bash
#
# Name: my-zfs-fuse-scrub
#

TARGET=""
if [[ "$1" = 20??-??-?? ]] ; then
    TARGET="-d $1"
elif [ -n "$1" ] ; then
    echo "Usage: my-zfs-fuse-scrub [YYYY-MM-DD]" 1>&2
fi

MONTH=`date +%-m $TARGET`
DAY=`date +%-d $TARGET`
WEEKDAY=`LANG=C date +%a $TARGET`

get_W() {
    local D=$1
    local F L
    for W in 1 2 3 4 5
    do
        F=$((1+($W-1)*7))
        L=$((F+6))
        if let "$F <= $D && $D <= $L" ; then
            break
        fi
    done
}

test_get_W() {
    local D
    for D in `seq 1 31`
    do
        get_W $D
        printf "D=%-2d W=%d\n" $D $W
    done
}
##DEBUG## test_get_W && exit 1

get_W $DAY

if [ "$W,$WEEKDAY" = "2,Wed" ] ; then
    if [ -z "$TARGET" ] ; then
        /usr/bin/zpool scrub tank1
    else
        echo "DEBUG: zpool scrub tank1 run @ $1"
    fi
fi

if [ "$W,$WEEKDAY" = "3,Wed" ] && let "$MONTH%2 == 0" ; then
    if [ -z "$TARGET" ] ; then
        /usr/bin/zpool scrub tank2
    else
        echo "DEBUG: zpool scrub tank2 run @ $1"
    fi
fi

exit 0
これを、次のように crontab に登録することで、毎月第2水曜日 AM1:00 に tank1 の scrub を実行、偶数月の第3水曜日 AM1:00 に tank2 の scrub を実行するようにスケジューリングしました。
0 1 * * Wed /root/bin/my-zfs-fuse-scrub
なお、実行日を確認し易いように、デバッグ機能をつけてあります。
# cal 6 2012
      June 2012
Su Mo Tu We Th Fr Sa 
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

# ./my-zfs-fuse-scrub 2012-06-13
DEBUG: zpool scrub tank1 run @ 2012-06-13
# ./my-zfs-fuse-scrub 2012-06-20
DEBUG: zpool scrub tank2 run @ 2012-06-20
# ./my-zfs-fuse-scrub 2012-06-27
#
# cal 7 2012
      July 2012
Su Mo Tu We Th Fr Sa 
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

# ./my-zfs-fuse-scrub 2012-07-11
DEBUG: zpool scrub tank1 run @ 2012-07-11
# ./my-zfs-fuse-scrub 2012-07-18
#
さあこれで、まずは、今晩深夜 1:00 に tank2 への scrub がちゃんと動くはず...何か間違いを犯していなければ。。

2012-06-23追記
ログを確認したところ、うまく行っていました。
# cat /var/log/cron
...
Jun 20 01:00:01 xxxx CROND[28792]: (root) CMD (/root/bin/my-zfs-fuse-scrub)
...
# zpool status

  pool: tank1
 state: ONLINE
 scrub: none requested
config:

...

errors: No known data errors

  pool: tank2
 state: ONLINE
 scrub: scrub completed after 0h42m with 0 errors on Wed Jun 20 01:42:50 2012
config:

...

errors: No known data errors
次は、7/11 (第2水曜) に tank1 の scrub が動き、7/18 (第3水曜日 && 奇数月) には tank2 への scrub が行われない、というふうにプログラムできたはずです。

2012-07-20追記
その後のログを確認したところ、意図した通りに動作していました。
# who -b
         system boot  2012-07-03 12:14
# uptime
 08:07:13 up 14 days, 19:53,  3 users,  load average: 0.12, 0.09, 0.09
# uname -a
Linux xxxx 2.6.32-220.13.1.el6.x86_64 #1 SMP Tue Apr 17 23:56:34 BST 2012 x86_64 x86_64 x86_64 GNU/Linux
# cat /etc/redhat-release 
CentOS release 6.3 (Final)
# grep zfs-fuse /var/log/cron
Jul  3 13:26:01 xxxx run-parts(/etc/cron.weekly)[12348]: starting 98-zfs-fuse-scrub
Jul  3 13:26:01 xxxx run-parts(/etc/cron.weekly)[12355]: finished 98-zfs-fuse-scrub
Jul  4 01:00:01 xxxx CROND[41538]: (root) CMD (/root/bin/my-zfs-fuse-scrub)
Jul 10 03:10:01 xxxx run-parts(/etc/cron.weekly)[30291]: starting 98-zfs-fuse-scrub
Jul 10 03:10:01 xxxx run-parts(/etc/cron.weekly)[30298]: finished 98-zfs-fuse-scrub
Jul 11 01:00:01 xxxx CROND[4220]: (root) CMD (/root/bin/my-zfs-fuse-scrub)
Jul 17 03:37:01 xxxx run-parts(/etc/cron.weekly)[43573]: starting 98-zfs-fuse-scrub
Jul 17 03:37:01 xxxx run-parts(/etc/cron.weekly)[43580]: finished 98-zfs-fuse-scrub
Jul 18 01:00:01 xxxx CROND[15841]: (root) CMD (/root/bin/my-zfs-fuse-scrub)
# zpool status | grep scrub:
 scrub: scrub completed after 1h13m with 0 errors on Wed Jul 11 02:13:35 2012
 scrub: none requested
# zpool status
  pool: tank1
 state: ONLINE
 scrub: scrub completed after 1h13m with 0 errors on Wed Jul 11 02:13:35 2012
config:

...

errors: No known data errors

  pool: tank2
 state: ONLINE
 scrub: none requested
config:

...

errors: No known data errors

これであとは、5年くらい運用・利用できたら、コスト(投資額+構築労力)に見合うかなと思います。

2015-02-10追記
備忘録。この記事を書いた当時は zfs-fuse を使ってたが、その後、ZFS on Linux に切り替えて継続利用中。
当時新品の HDD (ちなみに全て AFT の 2.5inch 7200rpm 750G)で組んで、2年半経過しましたが、今のところ不良セクタはありません。

2012年5月20日日曜日

Python でオプション解析

Python 初歩レベルな私ですが、自分用に Python 用のコマンドラインオプション解析テンプレートを作成しましたので、参考までに。。
#!/usr/bin/env python
#
# Name: getopt-template.py
#
import sys
import getopt

def usage_exit():
    sys.stderr.write("Usage: getopt-template.py [-a] [-d dir] item1 item2 ...\n")
    sys.exit(1)

#
# Options
#
print sys.argv  ####DEBUG

try:
    opts, argv = getopt.getopt(sys.argv[1:], "ad:h")
except getopt.GetoptError:
    usage_exit()

(opt_a, opt_d) = (0, None)
for (opt, optarg) in opts:
    if opt == "-a":
        opt_a = opt_a + 1
    elif opt == "-d":
        opt_d = optarg
    elif opt == "-h":
         usage_exit()
    else:
         usage_exit()

argc = len(argv)
print "argc =", argc    ####DEBUG
print argv              ####DEBUG
print "opt_a =", opt_a  ####DEBUG
print "opt_d =", opt_d  ####DEBUG
# ./getopt-template.py -aa -d dir item1 item2
['./getopt-template.py', '-aa', '-d', 'dir', 'item1', 'item2']
argc = 2
['item1', 'item2']
opt_a = 2
opt_d = dir
# ./getopt-template.py item1 -d dir
['./getopt-template.py', 'item1', '-d', 'dir']
argc = 3
['item1', '-d', 'dir']
opt_a = 0
opt_d = None
getopt.getopt の場合、上記のように -d dir を後置すると、期待したように解釈されません。
getopt.gnu_getopt を使えば後置が可能なのですが、Python 2.x 系で追加されたもののようで、古い Python 1.x 系では使えないようです。

参考
perl でオプション解析(getoptコマンド編)

2012年5月19日土曜日

bashスクリプトでatexit(3)相当を行う

C 言語だと、atexit() という関数がありますが、bash にも似た機能があることを知りました。

bash スクリプトが Ctrl+C で途中中断される場合を想定し、スクリプトの処理途中で生成される一時ファイルを削除(ゴミ掃除)したい場合があります。そんな時には、trap "rm ..." SIGINT という具合に書くのが定石かと思っていました。しかし、そのような書き方だと、スクリプト全体の終了時にも rm の記述をすることになります。次のような具合に・・・
#!/bin/bash
#
# Name: test_trap_SIGINT.bash
#

TEMPFILE=$(mktemp /tmp/.${0##*/}.$RANDOM$RANDOM.XXXXXX)
trap "echo trapped ; rm -f $TEMPFILE" SIGINT

echo $TEMPFILE

# 一時ファイルを使った処理を行う ... 
sleep 10 # このサンプルでは、スリープ
#

rm -f $TEMPFILE
exit 0
このサンプルの実行イメージは次のようになります。
途中中断しない場合
# ./test_trap_SIGINT.bash 
/tmp/.test_trap_SIGINT.bash.2085610279.7jIDoS
# ls -l /tmp/.test_trap_SIGINT.bash.2085610279.7jIDoS
ls: /tmp/.test_trap_SIGINT.bash.2085610279.7jIDoS: No such file or directory

途中で Ctrl+C した場合
# ./test_trap_SIGINT.bash 
/tmp/.test_trap_SIGINT.bash.579227843.ewVGq4
trapped
# ls -l /tmp/.test_trap_SIGINT.bash.579227843.ewVGq4
ls: /tmp/.test_trap_SIGINT.bash.579227843.ewVGq4: No such file or directory
最近、このパターンでスクリプトを書いていたら、一時ファイル生成後に途中終了 (exit 1) させたい条件が複数あり、いちいち rm を記述しました。
しかしハテ?もしかして atexit(3) のような機能はないものか?
っと、help を見てみますと、、、
# help trap
trap: trap [arg] [signal_spec ...] or trap -l
    The command ARG is to be read and executed when the shell receives
    signal(s) SIGNAL_SPEC.  If ARG is absent all specified signals are
    reset to their original values.  If ARG is the null string each
    SIGNAL_SPEC is ignored by the shell and by the commands it invokes.
    If a SIGNAL_SPEC is EXIT (0) the command ARG is executed on exit from
    the shell.  If a SIGNAL_SPEC is DEBUG, ARG is executed after every
    command.  If ARG is `-p' then the trap commands associated with
    each SIGNAL_SPEC are displayed.  If no arguments are supplied or if
    only `-p' is given, trap prints the list of commands associated with
    each signal number.  Each SIGNAL_SPEC is either a signal name in 
    or a signal number.  `trap -l' prints a list of signal names and their
    corresponding numbers.  Note that a signal can be sent to the shell
    with "kill -signal $$".
あるではないですか・・・さすが bash ですね。
なので、次のように書けます。
#!/bin/bash
#
# Name: test_trap_EXIT.bash
#

TEMPFILE=$(mktemp /tmp/.${0##*/}.$RANDOM$RANDOM.XXXXXX)
trap "echo trapped ; rm -f $TEMPFILE" EXIT

echo $TEMPFILE

# 一時ファイルを使った処理を行う ... 
sleep 10 # このサンプルでは、スリープ
#

exit 0
注意点としては、もし trap "トラップ内容" SIGINT EXIT と書くと、Ctrl+C 中断の際にトラップ内容が2回実行されることになります。
人気ブログランキングへ にほんブログ村 IT技術ブログへ