プログラミングとかLinuxとかの備忘録

プログラミング、Linuxでハマった箇所や環境構築のメモ

PT3サーバ自動起動・シャットダウン用スクリプト

スポンサーリンク

BIOSの設定

RTCで起動できる様に設定する

Asrock H77M-ITXの場合は
Advanced Screen -> ACPI Configuration -> RTC Alarm Power On

自動復帰の確認

以下のコマンドを実行し,3分後に自動的に起動されることを確認する

$ echo `date +%s -d +3min` | sudo tee /sys/class/rtc/rtc0/wakealarm
$ sudo shutdown -h now

上記のコマンドで成功しない場合はrtcwakeを使用すると成功するかもしれません

スクリプト作成

とりあえず,スクリプト全体は以下の通り.

#!/bin/python
# -*- coding: utf-8 -*-

import subprocess
import datetime
import time

import logging
from logging.handlers import RotatingFileHandler

SUDO_PASS="実行ユーザのパスワード\n"


log_handler = RotatingFileHandler(
    filename="/var/www/html/epgrec/video/pt3_savepower.log",
    mode="a",
    maxBytes=1000000, backupCount=3,
    encoding="utf-8",
    delay=False
)
# log_handler.setLevel(logging.INFO)
log_handler.setFormatter(
    logging.Formatter("%(asctime)s - %(levelname)-8s - %(message)s")
)

logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(log_handler)


def is_terminal_login():
    stdout, stderr = subprocess.Popen(
        ["/usr/bin/who"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    ).communicate()

    result = len([line for line in stdout.split("\n") if not line == ""])

    logger.info("[terminal]: %d" % result)
    return False if result is 0 else True


def is_recording_at():
    stdout, stderr = subprocess.Popen(
        ["sudo", "-S", "/bin/atq"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    ).communicate(SUDO_PASS)
    result = len([line for line in stdout.split("\n") if "=" in line])

    logger.info("[rec_at  ]: %d" % result)
    logger.debug(stdout)
    return False if result is 0 else True


def is_recording_ps():
    stdout, stderr = subprocess.Popen(
        ["/bin/ps", "-el"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    ).communicate()

    ps_recpt1 = [line for line in stdout.split("\n") if "recpt1" in line]
    result = len(ps_recpt1)

    logger.info("[rec_ps  ]: %d" % result)
    logger.debug(stdout)
    return False if result is 0 else True


def is_port_established():
    stdout, stderr = subprocess.Popen(
        ["sudo", "-S", "/sbin/lsof", "-i", ":ssh,http,https"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    ).communicate(SUDO_PASS)

    port_est = [line for line in stdout.split("\n") if "ESTABLISHED" in line]
    result = len(port_est)

    logger.info("[port    ]: %d" % result)
    logger.debug(stdout)
    return False if result is 0 else True


def convert_timestr(atq_line):
    result = atq_line.replace("Jan", "01")
    result = result.replace("Feb", "02")
    result = result.replace("Mar", "03")
    result = result.replace("Apr", "04")
    result = result.replace("May", "05")
    result = result.replace("Jun", "06")
    result = result.replace("Jul", "07")
    result = result.replace("Aug", "08")
    result = result.replace("Sep", "09")
    result = result.replace("Oct", "10")
    result = result.replace("Nov", "11")
    return result.replace("Dec", "12")


def get_at_schedules():
    stdout, stderr = subprocess.Popen(
        ["sudo", "-S", "/bin/atq"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    ).communicate(SUDO_PASS)

    schedule = []
    for line in stdout.split("\n"):
        if line is "":
            continue

        tmp = line.split()
        time_str = convert_timestr(" ".join([tmp[5], tmp[2], tmp[3], tmp[4]]))
        time_str = datetime.datetime.strptime(time_str, "%Y %m %d %H:%M:%S")
        unixtime = int(time.mktime(time_str.timetuple()))
        schedule.append(unixtime)

    return schedule


def get_closest_schedule():
    return sorted(get_at_schedules())[0]


def is_scheduled(unixtime):
    return any(s == unixtime for s in get_at_schedules())


def set_getepg_schedule(unixtime):
    if is_scheduled(unixtime):
        logger.info("getepg is already scheduled")
    else:
        getepg_at = datetime.datetime.fromtimestamp(unixtime).strftime("%y%m%d%H%M")
        stdout, stderr = subprocess.Popen(
            ["/bin/at", "-f", "/var/www/html/epgrec/video/at_getepg.sh", "-t", getepg_at],
            stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        ).communicate()

    logger.info("[getepg  ]: %s" % datetime.datetime.fromtimestamp(unixtime))


def set_rtcwake(unixtime):
    logger.info("[wakeup  ]: %s" % datetime.datetime.fromtimestamp(unixtime))
    stdout, stderr = subprocess.Popen(
        ["sudo", "-S", "/usr/sbin/rtcwake", "-m", "off", "-t", str(unixtime)],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    ).communicate(SUDO_PASS)

    logger.debug(stdout)


def main():
    logger.info("=============== START LOGGING ===============")

    if any([
            is_terminal_login(),
            is_recording_at(),
            is_recording_ps(),
            is_port_established()
        ]):
        logger.info("someone using")
    else:
        now = int(time.time())
        closest_waketime = now + 1800
        closest_schedule = get_closest_schedule()
        wake_to_record = closest_schedule - 300
        wake_to_getepg = closest_schedule - 1800
        getepg_time = wake_to_getepg + 300

        logger.info("NOW  : %s" % datetime.datetime.fromtimestamp(now))
        logger.info("WEPG : %s" % datetime.datetime.fromtimestamp(wake_to_getepg))
        logger.info("GEPG : %s" % datetime.datetime.fromtimestamp(getepg_time))
        logger.info("WREC : %s" % datetime.datetime.fromtimestamp(wake_to_record))
        logger.info("CLOSE: %s" % datetime.datetime.fromtimestamp(closest_schedule))

        if wake_to_getepg > closest_waketime:
            set_getepg_schedule(getepg_time)
            set_rtcwake(wake_to_getepg)
        elif wake_to_record > closest_waketime:
            set_rtcwake(wake_to_record)
        else:
            logger.info("close to wakeup time")


if __name__ == '__main__':
    main()

グローバルスコープ

  • SUDO_PASS
    スクリプト実行ユーザーのパスワードを入れてsudoを実行できるようにする。
    "パスワード\n"のように末尾に\nが入っているが,これがパスワード入力後のエンターキーの代わりになる。
  • log_handlerに循環ログを定義する

main()

  • is_terminal_login()
    ターミナルにログインしているユーザがいるか
  • is_recording_at()
    録画中かをatコマンドで確認
  • is_recording_ps()
    録画中かをpsコマンドで確認
  • is_port_established()
    ssh,http,httpsのポートが使用中(接続中)であるかを確認

で,サーバの使用状況を確認し,シャットダウンして問題がなければ,

  • now = int(time.time())
    現在時刻
  • closest_waketime = now + 1800
    次回起動の最短時刻(シャットダウンした場合に現在時刻から1800秒以内に起動する必要がある場合はシャットダウンしないようにするための値)
  • closest_schedule = get_closest_schedule()
    一番近い録画開始時刻
  • wake_to_record = closest_schedule - 300
    録画のために起動する時間(起動から録画開始まで,300秒の余裕をもたせる)
  • wake_to_getepg = closest_schedule - 1800
    時間に余裕がある場合は録画開始の1800秒前に起動しEPGの更新を行う
  • getepg_time = wake_to_getepg + 300
    EPGの更新を行う場合にEPGの更新を開始する時刻(起動からEPG更新開始まで,300秒の余裕をもたせる)

上で設定した時刻を使用し

  • 時間に余裕がある(wake_to_getepg > closest_waketime)場合
    atEPG取得のスケジュールを登録した後,rtcwakeでシステムを停止する
  • EPG更新の余裕がない(wake_to_record > closest_waketime)場合は
    rtcwakeでシステムを停止するだけにする
  • それ以外の場合 現在時刻から30分以内に録画開始する必要があるのでPCへの負荷を考慮し,シャットダウンせずに待機する

is_terminal_login()

whoを使用し,ログイン中のユーザを取得する

以下のコマンドを実行し,行数を数えることでログイン中のユーザがいるかを判断する

$ who
ユーザー名    pts/0        2016-01-23 18:42 (192.168.19.5)
ユーザー名    pts/1        2016-01-23 19:09 (192.168.19.5)

whoの実行結果stdoutに対し,以下の処理をすることで行数を数える。

len([line for line in stdout.split("\n") if not line == ""])

whoの実行結果を\nで分割すると

[
    "ユーザ1...",
    "ユーザ2...",
    ""
]

のように最後の改行コード後にも文字列があるように分割されるので
if not line == ""を入れている

is_recording_at()

atqを使用し,実行中のatジョブを取得する

$ sudo atq
524 Fri Jan 29 00:29:00 2016 a ユーザー名
455 Sat Jan 23 02:30:00 2016 = ユーザー名
465 Sun Jan 24 03:45:00 2016 a ユーザー名
466 Sun Jan 24 12:15:00 2016 a ユーザー名
467 Sun Jan 24 09:35:00 2016 a ユーザー名

sudo atqの実行結果stdoutに対し,以下の処理で=を含む行数を数える。

len([line for line in stdout.split("\n") if "=" in line])

is_recording_ps()

psを使用し,recpt1を使用しているプロセスがあるかを確認する

$ ps -el
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
...
0 S  1000  2936  2935  0  82   2 - 68925 hrtime ?        00:00:00 recorder.php
0 S  1000  2940  2936  0  82   2 -  2902 wait   ?        00:00:00 do-record.sh
0 S  1000  2941  2940  4  82   2 - 59385 pt3_dm ?        00:02:01 recpt1
0 S     0  2942     1  0  80   0 - 61852 poll_s ?        00:00:00 pcscd
...

ps -elの実行結果stdoutに対し,以下の処理でrecpt1を含む行数を数える。

len([line for line in stdout.split("\n") if "recpt1" in line])

is_port_established()

lsofを使用し,ssh,http,httpsが使用されているかを確認する

$ sudo lsof -i :ssh,http,https
COMMAND  PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
sshd    1081  root    3u  IPv4  17772      0t0  TCP *:ssh (LISTEN)
sshd    1081  root    4u  IPv6  17783      0t0  TCP *:ssh (LISTEN)
nginx   1127  root    6u  IPv4  18486      0t0  TCP *:http (LISTEN)
nginx   1128 nginx    3u  IPv4  27768      0t0  TCP PT3:http->192.168.19.5:34158 (ESTABLISHED)
nginx   1128 nginx    6u  IPv4  18486      0t0  TCP *:http (LISTEN)
sshd    3032  root    3u  IPv4  25384      0t0  TCP PT3:ssh->192.168.19.5:60890 (ESTABLISHED)
sshd    3034   pt3    3u  IPv4  25384      0t0  TCP PT3:ssh->192.168.19.5:60890 (ESTABLISHED)
sshd    3043  root    3u  IPv4  25436      0t0  TCP PT3:ssh->192.168.19.5:60902 (ESTABLISHED)
sshd    3045   pt3    3u  IPv4  25436      0t0  TCP PT3:ssh->192.168.19.5:60902 (ESTABLISHED)

sudo lsof -i :ssh,http,httpsの実行結果stdoutに対し,以下の処理でESTABLISHEDを含む行数を数える。

len([line for line in stdout.split("\n") if "ESTABLISHED" in line])

get_at_schedule()

atqでスケジュールされているスケジュールの各時刻をunixtimeに変換したリストを返す

atqからは以下のフォーマットで出力されるので

467  Sun Jan 24 09:35:00 2016 a ユーザー名

以下のようにフォーマットを変更し
convert_timestr()関数で月コード(Jan)を数値(01)に変換する

2016 01 24 09:35:00

この文字列を以下でunixtimeに変換する

time_str = datetime.datetime.strptime(time_str, "%Y %m %d %H:%M:%S")
unixtime = int(time.mktime(time_str.timetuple()))

get_closest_schedule()

get_at_schedule()から得られたunixtimeのリストをソートし,最小値(最も近いスケジュールのunixtime)を取得する

set_getepg_schedule()

引数のunixtimeの時刻にatEPG更新をスケジュールする

  • サーバが自動起動される前に手動でサーバを起動した場合には同じ時刻に複数EPG更新がスケジュールする可能性があるのでis_scheduled()で引数のunixtimeの時刻にatにスケジュールがないかを確認する
  • atは直接php(getepg.php)を実行できないため,以下のシェルスクリプトを経由してスケジュールを登録する
#!/bin/bash

/var/www/html/epgrec/getepg.php

set_rtcwake()

rtcwakeを使用し,自動起動時刻を設定する
また,rtcwakeを実行すると自動的にシャットダウンされる

スクリプトのcron実行

sudoを含むスクリプトをcronで実行すると

sudo: sorry, you must have a tty to run sudo

と言われ実行できないのでttyなしでsudoできるようにする

今回はPT3専用のサーバなので簡単に以下のように56行目のDefaults requirettyコメントアウトした

$ sudo visudo
# Disable "ssh hostname sudo <cmd>", because it will show the password in clear.
#         You have to run "ssh -t hostname sudo <cmd>".
#
#Defaults    requiretty

以上で実行できるはずなので10分ごとにスクリプトを実行するようにする

$ crontab -e
*/10 * * * * python /path/to/script.py

References