シェルスクリプトでの引数の取り方と文字列比較
#!/bin/sh
echo $1
if [ $1 == "abc" ]; then
echo $2
fi
実行方法と結果
sh test.sh a 123
#=> a
sh test.sh abc 123
#=> abc
#=> 123
シェルスクリプトでの引数で渡したファイルの読み取り
abc 111
bcd 222
efg 333
abc 444
abcd 555
ab 666
1abc 777
1ab 888
#!/bin/bash
cat $1 | while read line
do
echo "${line}"
done
実行方法と結果
sh test2.sh test1.txt
#=> abc 111
#=> bcd 222
#=> efg 333
#=> abc 444
#=> abcd 555
#=> ab 666
#=> 1abc 777
#=> 1ab 888
シェルスクリプトでの引数で渡したファイルの読み取り(スペース区切りで配列的な読み込み)
#!/bin/bash
cat $1 | while read col1 col2
do
echo "1:${col1}|2:${col2}"
done
実行方法と結果
sh test3.sh test1.txt
#=> 1:abc|2:111
#=> 1:bcd|2:222
#=> 1:efg|2:333
#=> 1:abc|2:444
#=> 1:abcd|2:555
#=> 1:ab|2:666
#=> 1:1abc|2:777
#=> 1:1ab|2:888
シェルスクリプトでの外部パラメータファイルの読み込み
val1="abc"
val2="def"
val3=123;
#!/bin/sh
. $1
echo "val1:${val1}"
echo "val2:${val2}"
echo "val3:${val3}"
実行方法と結果
sh test4.sh test.inc
#=> val1:abc
#=> val2:def
#=> val3:123
シェルスクリプトでのエラー出力する
#!/bin/sh
if [ $1 == "stdout" ]; then
echo "OK"
fi
if [ $1 == "stderr" ]; then
echo "error" >&2
exit 1;
fi
実行方法と結果
## 標準出力(正常終了)
sh test5.sh stdout
#=> OK
## 終了ステータス
echo $?
#=> 0
## エラー出力(エラー終了)
sh test5.sh stderr 1>/dev/null
#=> error
## 終了ステータス
echo $?
#=> 1
シェルスクリプトでリストでループする
#!/bin/bash
for STR in "aa" "bb" "cc" "dd"
do
echo "VAL: ${STR}"
done
実行方法と結果
sh test6.sh
VAL: aa
VAL: bb
VAL: cc
VAL: dd
シェルスクリプトでリスト(配列)でループする
#!/bin/bash
ary=("ab" "cd" "ef" "gh")
for STR in ${ary[@]}
do
echo "VAL: ${STR}"
done
実行方法と結果
sh test7.sh
VAL: ab
VAL: cd
VAL: ef
VAL: gh
バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する
単一シェルでエラー判別してメール送信しても良いが、シェルを単純化したいので、メイン処理は子スクリプトで処理し、エラー判別は、OSの標準エラー出力機能を使用した親スクリプトで行い、エラー時には親スクリプトでメール送信する
- メイン処理用(子スクリプト)サンプル
単純にfile1で指定したファイルを削除するだけのスクリプト
#!/bin/sh
file1=/tmp/testfile.txt
ls ${file1}
rm ${file1}
- 実行用親スクリプトサンプル
子スクリプトを実行するスクリプトで 実行可否ログ、実行詳細ログ、エラーログを出力し、エラー時にはメールを指定したアドレスにメール送信する
#!/bin/sh
export LANG=C
run_date=`date +"%Y%m"`
hostname=`hostname`
batch="batch_filedelete"
batchname="${hostname}.${batch}"
run_script="${batch}.sh"
# log
log1="/var/log/batch/${run_date}.${batchname}.log"
log2="/var/log/batch/${run_date}.${batchname}.status.log"
log3="/var/log/batch/${run_date}.${batchname}.error.log"
# mail
mail_title="[batch]${batchname}:NG"
mail_to="xxxx@localhost"
mail_from="batch@localhost"
# start batch script
stime=`date '+%Y/%m/%d %X'`
echo "${stime} | ${batchname} | start" >> ${log1}
echo "${stime} | ${batchname} | start" >> ${log2}
# run script file
/bin/sh ${run_script} 1>>${log2} 2>${log3}
# end batch script (NG end/OK end)
etime=`date '+%Y/%m/%d %X'`
if [ -s "${log3}" ]; then
# NG end
cat "${log3}" | mail -s "${mail_title}" -r "${mail_from}" "${mail_to}"
echo "${etime} | ${batchname} | end | NG" >> ${log1}
echo "${etime} | ${batchname} | end | NG" >> ${log2}
else
# OK end
echo "${etime} | ${batchname} | end | OK" >> ${log1}
echo "${etime} | ${batchname} | end | OK" >> ${log2}
rm -f "${log3}"
fi
実行方法と結果
## 正常終了時テスト
# 削除するファイルを作成する
touch /tmp/textfile.txt
sh batch_filedelete_run.sh
## エラー終了時テスト
# そのまま実行する、削除するファイルがないのでエラーになる
sh batch_filedelete_run.sh
バッチ処理のlogをチェックするスクリプト
バッチでerrorが発生した場合にメール送信するようにしておくだけでは、根本的にバッチが動かないような事象(未実行等)の際に発見することはできない。しかし、正常終了の際にメール送信して、正常終了メールが来ない場合をエラー(未実行等)としてキャッチするとして、仮に毎時2本、1日1回2本のバッチを2台のサーバーで実行させると、1日に100通のメールを受け取ることになる、さらにバッチ数やサーバー台数が増えていくと、メールフィルターを駆使したとしても、未到達によるエラー(未実行等)を発見するのはとても困難となる。そのために、実行ログを集積サーバに集めて、その実行ログに期待するログが含まれているかを1本のバッチでチェックすれば、100のログでも1本のメールに 集約することが可能となるため、そのようなスクリプトを考えてみる
- 集めるログのフォーマットはバッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信するのログ形式
- ログの集積部分はこのスクリプトでは考えない(バッチ側の最後にscpやrsyncや別のミドルウェアなどで実施するとする)
- cronで毎時実行すると、本スクリプトで1日1回〜毎時実行までの、バッチのログのチェックが可能
(例えば、55 * * * * /bin/sh batch_check.sh 1>/dev/null 2>&1 設定し、10時台のチェックが走ったとすると、ログの最終レコードが10:00〜10:54までに "end | OK" が出ているかをチェックする)
- スクリプト自体を単純化するために、目的時単位でのリストファイルを作成することにする
(00時台のチェックは00.list、01時台のチェックは01.list)
- 実行タイミングや実行時間の関係で、ログの時刻が時台を跨ぐ場合には、offsetに1を設定すると1時間までは対応する (例えば10時台のチェックで ログの最終レコードが9:00〜10:54までに "end | OK" が出ているかをチェックが可能になる)
- 日跨ぎに関しては考慮する (00時台のチェックで ログの最終レコードが前日の23:00〜当日の00:54までに "end | OK" が出ているかをチェック可能)
- ただし、ログの集約単位以上の跨ぎに関しては、考慮しない。つまり、チェック不可能 (バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信するのログの集約単位が月単位の為、月を跨ぐ日跨ぎに関してはチェック不可能)
# batch_name hostname log_path log_name offset
batch_1 srv1 /home/batch/log/srv1 .srv1.batch_1.log 0
batch_2 srv1 /home/batch/log/srv1 .srv1.batch_2.log 0
batch_3 srv1 /home/batch/log/srv1 .srv1.batch_3.log 1
batch_1 srv2 /home/batch/log/srv2 .srv2.batch_1.log 0
batch_2 srv2 /home/batch/log/srv2 .srv2.batch_2.log 0
batch_3 srv2 /home/batch/log/srv2 .srv2.batch_3.log 1
#!/bin/sh
# mail ini
mail_to="xxxx@localhost"
mail_from="batch@localhost"
mail_subject="Batch Log Check Hourly Report"
# set check date
dt=`date +"%Y%m"`
tday=`date +"%d"`
hour=`date +"%H"`
###################
### subroutine
# record_check
# $1:check hour | $2:check date | $3:offset | $4:log_time | $5:batchname | $6:runhost | $7:start/end | $8:OK/NG
function record_check()
{
log_hour=`date -d "${4}" "+%H"`
log_day=`date -d "${4}" "+%d"`
# record check
if [ ${7} == "end" ] && [ ${8} == "OK" ] && [ ${3} -ne 0 ] ; then
offset_hour=`date -d "${4} ${3} hour" "+%H"`
offset_day=`date -d "${4} ${3} hour" "+%d"`
if [ ${2} == ${log_day} ] && [ ${1} == ${log_hour} ] ; then
echo "OK | ${6}-${5}" >> /tmp/batch-log-check-body
elif [ ${2} == ${offset_day} ] && [ ${1} == ${offset_hour} ] ; then
echo "OK | ${6}-${5}" >> /tmp/batch-log-check-body
else
echo "NG | ${6}-${5}" >> /tmp/batch-log-check-body
fi
elif [ ${7} == "end" ] && [ ${8} == "OK" ] && [ ${2} == ${log_day} ] && [ ${1} == ${log_hour} ] ; then
echo "OK | ${6}-${5}" >> /tmp/batch-log-check-body
else
echo "NG | ${6}-${5}" >> /tmp/batch-log-check-body
fi
}
# mail_send
# $1:subject | $2:from | $3:to
function mail_send()
{
cat /tmp/batch-log-check-body | mail -s "${1}" -r "${2}" "${3}"
rm -f /tmp/batch-log-check-body
}
#################
## main
# output date
echo "check date:${dt}${tday} hour:${hour}" > /tmp/batch-log-check-body
# check ini file
if [ ! -e "${hour}.list" ]; then
echo "Not exist ${hour}.list" >> /tmp/batch-log-check-body
mail_send "${mail_subject}" "${mail_from}" "${mail_to}"
exit 1
fi
# read ini file
cat "./${hour}.list" |grep -v "^#"| while read batchname runhost path log offset
do
# check log file
if [ ! -e "${path}/${dt}${log}" ]; then
echo "Not exist ${path}/${dt}${log}" >> /tmp/batch-log-check-body
continue
fi
# read log file
tail -n1 "${path}/${dt}${log}" | (IFS="|" read log_time log_name log_status1 log_status2; record_check "${hour}" "${tday}" "${offset}" "${log_time}" "${batchname}" "${runhost}" "${log_status1}" "${log_status2}")
done
mail_send "${mail_subject}" "${mail_from}" "${mail_to}"
cronに毎時55分に実行するように設定
55 * * * * /bin/sh batch_check.sh 1>/dev/null 2>&1
itamae実行用のスクリプト
itamaeをCLIで手動コマンド実行してしまうと、Infrastructure as Codeが台無しになってしまうので、レシピ実行をスクリプトにまとめてやる
実行するレシピはここ
また、個人的な環境ではRVMでrubyをセットアップしているので、RVM用のスクリプトになっている
#!/bin/bash --login
# RVM gemset
rvm use ruby-2.3.3@itamae
# target host
target_host="192.168.56.102"
run_user="xxxx"
p_key="/root/ssh_keys/id_ed25519_xxx"
# itamae recipe
list_repo=("epel.rb" "remi.rb")
list_recipe=("centos7_default.rb" "centos7_chrony.rb" "selinux.rb" "ldap_client.rb" "sshd.rb" "php71.rb")
# execute or dry-run
if [ $# == 1 ]; then
if [ $1 == "--dry-run" ]; then
DRY_RUN="--dry-run"
fi
fi
###
# yum repository
cd /root/itamae_cookbooks/repos
for str in ${list_repo[@]}
do
itamae ssh -h ${target_host} -u ${run_user} -i ${p_key} ${str} ${DRY_RUN}
done
###
# os
cd /root/itamae_cookbooks/os_package
# with json
itamae ssh -h ${target_host} -u ${run_user} -i ${p_key} --node-json files/centos7-web.json hostname.rb ${DRY_RUN}
# without json
for str in ${list_recipe[@]}
do
itamae ssh -h ${target_host} -u ${run_user} -i ${p_key} ${str} ${DRY_RUN}
done