Category Archives: Computer

[zfs] zfsnap2を使った日々のzfs snapshot

Pocket

zfsnap2(面倒くさいので以降zfsnap)のおかげで気軽にzfs snapshotできるようになった。
あとはこれをcronに仕込んで日々、snapshotを取得、削除するだけである。

zfsnapをpkg、あるいはportsからインストールすると、cron用のシェルスクリプトも付いてくる。
これを加工してperiodic用のディレクトリに放り込めば、あとは全部、zfsnapがやってくれる。

基本はここを見ながら。
https://github.com/zfsnap/zfsnap/wiki/zfSnap-periodic.conf

なお、本稿で扱う周期、契機は日週月次に限る。

全体の流れ

  1. スナップショットの方針を決める
  2. cron用シェルスクリプトを日・週・月ごとのperiodicディレクトリにコピーし、ファイル名を変更する
  3. スクリプトの内容を修正する
  4. /etc/periodic.confにバックアップ方針に沿った設定を書き加える

では早速。

スナップショット方針を決める

ここでは簡単に、あるzpool丸ごとに対して;

  • 毎日のスナップショットは1週間と1日、保存する
  • 毎週のスナップショットは1か月と1日、保存する
  • 毎月のスナップショットは1年と1週間、保存する

とする

[補足]periodicから実行されるときのsnapname

zfsnap付属のスクリプトを使うと、snapnameの頭にdaily, weekly, monthlyと付く。
もちろん変更可能だが、本稿ではそのまま使う。

zfsnapのcron用スクリプトはどこにある

cron用スクリプトは、zfsnapインストール時にshareにコピーされる。
例えば以下のようにして探す

$ pkg list|grep zfsnap
/usr/local/etc/bash_completion.d/zfsnap.sh
/usr/local/man/man8/zfsnap.8.gz
/usr/local/sbin/zfsnap
/usr/local/share/doc/zfsnap/AUTHORS
/usr/local/share/doc/zfsnap/NEWS
/usr/local/share/doc/zfsnap/PORTABILITY
/usr/local/share/doc/zfsnap/README.md
/usr/local/share/examples/zfsnap/completion/zfsnap-completion.bash
/usr/local/share/examples/zfsnap/completion/zfsnap-completion.tcsh
/usr/local/share/examples/zfsnap/completion/zfsnap-completion.zsh
/usr/local/share/examples/zfsnap/periodic/xPERIODICx_zfsnap.sh
/usr/local/share/examples/zfsnap/periodic/xPERIODICx_zfsnap_delete.sh
/usr/local/share/licenses/zfsnap2-2.0.0.b3/BSD3CLAUSE
/usr/local/share/licenses/zfsnap2-2.0.0.b3/LICENSE
/usr/local/share/licenses/zfsnap2-2.0.0.b3/catalog.mk
/usr/local/share/zfsnap/commands/destroy.sh
/usr/local/share/zfsnap/commands/recurseback.sh
/usr/local/share/zfsnap/commands/snapshot.sh
/usr/local/share/zfsnap/core.sh
/usr/local/share/zsh/site-functions/_zfsnap

上記のうち以下が狙いのスクリプトである。
スナップショット取得用、削除用の2種類。
/usr/local/share/examples/zfsnap/periodic/xPERIODICx_zfsnap.sh
/usr/local/share/examples/zfsnap/periodic/xPERIODICx_zfsnap_delete.sh

スクリプトのコピーとファイル名の変更

さてこれを、日・週・月のperiodicディレクトリにコピーし設定していくが、まずは日次で試す。
日ごとのperiodicディレクトリは/usr/local/etc/periodic/dailyである。
ここにコピーし、ファイル名を変える。
ファイル名のxPERIODICxを、日次・週次・月次、どの周期用かに合わせて変える。
具体的にはそれぞれdaily, weekly, monthlyに。
以下の例では日次のスクリプトなので、dailyに変えている。
prefixの番号は好きなようにしたまえ。


$ cd /usr/local/etc/periodic/daily
$ sudo cp /usr/local/share/examples/zfsnap/periodic/* .
$ sudo mv ./xPERIODICx_zfsnap.sh ./500.daily_zfsnap.sh
$ sudo mv ./xPERIODICx_zfsnap_delete.sh  ./500.daily_zfsnap_delete.sh
$ ls
411.pkg-backup                  500.daily_zfsnap_delete.sh      998.ntpdate
490.status-pkg-changes          500.daily_zfsnap.sh             smart

スクリプトの修正

スクリプト(取得用)の内容は以下の通りである。
変える場所はxPERIODICxとxPREFIXxである。
xPERIODICxは周期に合わせて、xPREFIXxはzfsnapのお置いてあるパス、つまり/usr/local/sbin/zfsnapに。


#!/bin/sh

# This file is licensed under the BSD-3-Clause license.
# See the AUTHORS and LICENSE files for more information.

# If there is a global system configuration file, suck it in.
if [ -r /etc/defaults/periodic.conf ]; then
    . /etc/defaults/periodic.conf
    source_periodic_confs
fi

# xPERIODICx_zfsnap_enable          - Enable xPERIODICx snapshots (values: YES | NO)
# xPERIODICx_zfsnap_flags           - `zfsnap snapshot` flags
# xPERIODICx_zfsnap_fs              - Space-separated ZFS filesystems to create non-recursive snapshots
# xPERIODICx_zfsnap_recursive_fs    - Space-separated ZFS filesystems to create recursive snapshots
# xPERIODICx_zfsnap_ttl             - Explicit TTL value
# xPERIODICx_zfsnap_verbose         - Verbose output (values: YES | NO)
# xPERIODICx_zfsnap_enable_prefix   - Create snapshots with prefix (values: YES | NO) (Default = YES)
# xPERIODICx_zfsnap_prefix          - set prefix for snapshots (Default = xPERIODICx)

case "${xPERIODICx_zfsnap_enable-"NO"}" in
    [Yy][Ee][Ss])
        OPTIONS="$xPERIODICx_zfsnap_flags"

        case "${xPERIODICx_zfsnap_verbose-"NO"}" in
            [Yy][Ee][Ss]) OPTIONS="$OPTIONS -v" ;;
        esac

        case "${xPERIODICx_zfsnap_enable_prefix-"YES"}" in
            [Yy][Ee][Ss]) OPTIONS="$OPTIONS -p ${xPERIODICx_zfsnap_prefix:-"xPERIODICx-"}" ;;
        esac

        case 'xPERIODICx' in
            'hourly')
                default_ttl='3d'
                ;;
            'daily'|'reboot')
                default_ttl='1w'
                ;;
            'weekly')
                default_ttl='1m'
                ;;
            'monthly')
                default_ttl='6m'
                ;;
            *)
                printf '%s\n' "ERR: Unexpected error" >&2
                exit 1
                ;;
        esac

        xPREFIXx/zfsnap snapshot $OPTIONS -a ${xPERIODICx_zfsnap_ttl:-"$default_ttl"} $xPERIODICx_zfsnap_fs -r $xPERIODICx_zfsnap_recursive_fs
        exit $?
        ;;

    *)
        exit 0
        ;;
esac

# vim: set ts=4 sw=4:

vimなら:%s/xPERIODICx/daily/g などとコマンド叩いて一括置換する。
以下が修正後。


#!/bin/sh

# This file is licensed under the BSD-3-Clause license.
# See the AUTHORS and LICENSE files for more information.

# If there is a global system configuration file, suck it in.
if [ -r /etc/defaults/periodic.conf ]; then
    . /etc/defaults/periodic.conf
    source_periodic_confs
fi

# daily_zfsnap_enable          - Enable daily snapshots (values: YES | NO)
# daily_zfsnap_flags           - `zfsnap snapshot` flags
# daily_zfsnap_fs              - Space-separated ZFS filesystems to create non-recursive snapshots
# daily_zfsnap_recursive_fs    - Space-separated ZFS filesystems to create recursive snapshots
# daily_zfsnap_ttl             - Explicit TTL value
# daily_zfsnap_verbose         - Verbose output (values: YES | NO)
# daily_zfsnap_enable_prefix   - Create snapshots with prefix (values: YES | NO) (Default = YES)
# daily_zfsnap_prefix          - set prefix for snapshots (Default = daily)

case "${daily_zfsnap_enable-"NO"}" in
    [Yy][Ee][Ss])
        OPTIONS="$daily_zfsnap_flags"

        case "${daily_zfsnap_verbose-"NO"}" in
            [Yy][Ee][Ss]) OPTIONS="$OPTIONS -v" ;;
        esac

        case "${daily_zfsnap_enable_prefix-"YES"}" in
            [Yy][Ee][Ss]) OPTIONS="$OPTIONS -p ${daily_zfsnap_prefix:-"daily-"}" ;;
        esac

        case 'daily' in
            'hourly')
                default_ttl='3d'
                ;;
            'daily'|'reboot')
                default_ttl='1w'
                ;;
            'weekly')
                default_ttl='1m'
                ;;
            'monthly')
                default_ttl='6m'
                ;;
            *)
                printf '%s\n' "ERR: Unexpected error" >&2
                exit 1
                ;;
        esac

        /usr/local/sbin/zfsnap snapshot $OPTIONS -a ${daily_zfsnap_ttl:-"$default_ttl"} $daily_zfsnap_fs
 -r $daily_zfsnap_recursive_fs
        exit $?
        ;;

    *)
        exit 0
        ;;
esac

# vim: set ts=4 sw=4:

削除用も同じようにしたら、periodic.confに指示を書き入れる。

/etc/periodic.confにおけるzfsnap設定の書式

/etc/periodic.confは無ければ作る。

zfsnapのperiodic向け書式は以下の通り。
https://github.com/zfsnap/zfsnap/wiki/zfSnap-periodic.conf

以下、日次のものとして記す。
週、月次の場合はdailyをそれぞれweekly, monthlyに変えればよい。

snapshot取得の有効化

daily_zfsnap_enable
YES/NOで指定

共通オプション(generic option)

daily_zfsnap_flags
例えばscrub, resilver中は避け、日付シリアルの秒を切り捨て、というような設定をここで行う。。
-v, -dを指定しないこと。
初めて作るときにはテスト実行オプションを付けて試験する。

zfs, zpoolの指定

daily_zfsnap_fs
daily_zfsnap_recursive_fs
recursiveとなっているのは、当然ながら子孫も含めて再帰的に取得される
複数あるなら、スペースを空けて列挙

冗長アウトプット指定

daily_zfsnap_verbose
YES/NOで指定

保存期間指定

daily_zfsnap_ttl
指定がなければ日次は1週間、週次は1か月、月次は6か月になる。

スナップショット削除有効化

daily_zfsnap_delete_enable
YES/NOで指定

periodic.conf例

ということで、periodic.confに追加したのは以下の通り。


# zfsnap_daily
daily_zfsnap_enable="YES"
daily_zfsnap_recursive_fs="vault"
daily_zfsnap_verbose="YES"
daily_zfsnap_delete_enable="YES"
daily_zfsnap_flags="-n -s -S -z"
daily_zfsnap_ttl="1w1d"

試験

zfsnap_flagsに-nを付けた状態で試験。


$ sudo /usr/local/etc/periodic/daily/500.daily_zfsnap.sh
/sbin/zfs snapshot  vault/chamber@daily-2018-12-03_02.14.00--1w1d
/sbin/zfs snapshot  vault/itunes@daily-2018-12-03_02.14.00--1w1d

大丈夫そうなので-nを外す。

最終的なperiodic.confの結果

週次、月次で同じようにperiodic用のスクリプトを作成する。


$ pwd
/usr/local/etc/periodic
$ find ./ -name '*zfsnap*'
./weekly/999.weekly_zfsnap.sh
./weekly/999.weekly_zfsnap_delete.sh
./daily/500.daily_zfsnap.sh
./daily/500.daily_zfsnap_delete.sh
./monthly/910.monthly_zfsnap.sh
./monthly/910.monthly_zfsnap_delete.sh

最終的なperiodic.confの結果は以下の通り。


# zfsnap_weekly
weekly_zfsnap_enable="YES"
weekly_zfsnap_recursive_fs="vault"
weekly_zfsnap_verbose="YES"
weekly_zfsnap_delete_enable="YES"
weekly_zfsnap_flags="-s -S -z"
weekly_zfsnap_ttl="1m1d"
# zfsnap_daily
daily_zfsnap_enable="YES"
daily_zfsnap_recursive_fs="vault"
daily_zfsnap_verbose="YES"
daily_zfsnap_delete_enable="YES"
daily_zfsnap_flags="-s -S -z"
daily_zfsnap_ttl="1w1d"
# zfsnap_monthly
monthly_zfsnap_enable="YES"
monthly_zfsnap_recursive_fs="vault"
monthly_zfsnap_verbose="YES"
monthly_zfsnap_delete_enable="YES"
monthly_zfsnap_flags="-s -S -z"
monthly_zfsnap_ttl="1y1d"

翌日以降、root宛には以下のようなメールが届くので、結果はここで確認する。

/sbin/zfs snapshot -r vault@daily-2018-12-02_04.47.0<wbr />0--1w1d ... DONE

[zfs][zpool] らくらくスナップショットzfsnap2

Pocket

zfsは本当に本当に便利で助かるのだが、それでも面倒なことはある。
私にとって面倒なのは、スナップショット作成とスナップショットの差分転送である。

スナップショットの簡素化について。
スナップショットを作成する際には、ファイルシステムやボリュームに続けてスナップショットの名前、スナップネームを指定する必要がある。
スナップネームはスナップショットを特定するために必要なものなので文句を言う筋合いはないのだが、いちいち名前を付けるのが面倒なんである。日付でもなんでも自動的に付けてほしい。

なんとか改善したくていろいろ調べてみた。

スナップショット取得補助ツール

スナップショット関連ではzfstools, zfsnap2, zapの三つを調べた。
結論から言うと、zfsnap2を選択した。

zfstools

https://github.com/bdrewery/zfstools
https://www.freshports.org/sysutils/zfstools/
よさそうだけどruby依存なのでその一点で対象外に。
このツールのためだけにrubyを入れますか、という話で。
なるべく依存の少ないツールがよい。

zap

https://www.freshports.org/sysutils/zap
https://github.com/Jehops/zap
よさそうだけど、zfsに独自のプロパティ(zap:)を設定する必要があるっぽい。
独自プロパティの設定が、zfsとして許容されている作法であっても、対象のファイルシステムに何らかの手を加えるのは躊躇する

zfsnap2

https://www.freshports.org/sysutils/zfsnap2
https://github.com/zfsnap/zfsnap
単純なシェルスクリプト。
よさそう。

zfsnap2 お手軽snapshot

まず、zfsnap2は、取得すると日付シリアルをsnapnameに付けてくれる。
例えば。

 
vault/chamber@2018-11-11_09.48.00--1y
 

ご覧の通り2018/11/11 09:48:00と日付をsnapnameに付ける
なお、末尾に1yと書いてあるがこれは1年という意味である。
これは何かというと(次項につづく)

zfsnap2 snapshot削除の仕組みとTTL

snapshotは取得するだけでなく、消す方の面倒を見ることも重要。
zfsnap2は、取得したタイムスタンプとTTL(Time To Live)をsnapnameに含めておくのがアイデア(他みも同じようなツールあるかもしれないけど)。
先ほどの例でいえば、末尾の1yがそれ。1yだから1 year、1年ですな。

vault/chamber@2018-11-11_09.48.00--1y

上記の通り、取得日付、保存期間をsnapname自体に持たせるので、ツール側では状態を記憶しておく必要がない。
削除を指示されたら、zfsnap2はその都度、snapnameだけを見て判断・処理をするということ。
何より人間もsnapnameから保存期限を判断できる。これはよい。

とりあえず使う

管理者権限でzfsnap snapshotに続けてzfs/zpoolを指定するだけ。
日付も付けてくれる。

# zfsnap snapshot zroot/var/tmp
$ zfs list -t snapshot|grep 'zroot/var/tmp'
zroot/var/tmp@20181120                   56K      -    88K  -
zroot/var/tmp@2018-12-01_22.19.13--1m      0      -    88K  -

zfsnap書式概要

詳細はman等するとして、概要を示す。
書式は以下の通り。

zfsnap コマンド <共通オプション> <個別オプション> zpool/zfs

zfsnapのコマンド

コマンドにはsnapshot, destroy, recursebackの三つがある。
それらに続けてzpool, zfsを指定する。こちらの指定は複数でもよい。
共通オプション(generic option)は、その名の通り、共通的なオプション。
個別オプションはzpool, zfsごとのオプションである。

コマンドのsnapshot, destroyは当然ながらそれぞれsnapshot作成、削除。
recursebackは、zfs rollbackの拡張で、rollbackの際、指定のdataset配下すべてをrollbackするもの。
本稿では扱わない。

共通オプション(generic option)

主なものだけ示す

-s  : resilver中は作業しない
-S : scrub中は作業しない
-z : 日付シリアルの秒切り捨て(18:06:15を18:06:00にする)
-n : dry run。テスト実行。ファイルシステムに変更を加えない。
-v : 冗長output

-s, -Sは常時付けておいたほうがいいかな。

個別オプション

個別オプションなので、これに続けて指定されたzpool/zfsにのみ効果がある。
主なものだけ示す

-a : TTL。指定がなければ1m = 1か月
-r : 再帰オプション。再帰的にsnapshotを取得
-R : 非再帰オプション。再帰的なsnapshotを取得「しない」

TTLオプション

以下のうち、必要なものだけをズラズラ書けばよい

y : 年(365日)
m : 月(30日)
w : 週(7日)
d : 日
h : 時
M : 分
s : 秒

1週間と1日なら-a 1w1d。
2か月なら-a 2m。

実行例

以上を踏まえて;
resilver, scrub中は実行せず、日付シリアルの秒切り捨て、冗長アウトプットでsnapshotをテスト実行。
対象zfsその1はzroot/usrで、再帰的に取得し、TTLは1週間と1日。その2はzroot/ROOTで、再帰的に取得「せず」、TTLは1分。
という実行例。

# zfsnap snapshot -sSvzn -a1w1d -r zroot/usr -R -a1M zroot/ROOT
/sbin/zfs snapshot -r zroot/usr@2018-12-01_22.33.00--1w1d
/sbin/zfs snapshot  zroot/ROOT@2018-12-01_22.33.00--1M

よさそうなのでテスト実行オプションを省いて本番実行すると;

# zfsnap snapshot -sSvz -a1w1d -r zroot/usr -R -a1M zroot/ROOT
/sbin/zfs snapshot -r zroot/usr@2018-12-01_22.34.00--1w1d ... DONE
/sbin/zfs snapshot  zroot/ROOT@2018-12-01_22.34.00--1M ... DONE

zfs listで結果を確認。zroot/usrにのみ配下にもスナップショットが出来ている。

$ zfs list -t snapshot | grep '2018-12-01'
zroot/ROOT@2018-12-01_22.34.00--1M             0      -    88K  -
zroot/usr@2018-12-01_22.34.00--1w1d            0      -    88K  -
zroot/usr/home@2018-12-01_22.34.00--1w1d       0      -  5.97M  -
zroot/usr/ports@2018-12-01_22.34.00--1w1d      0      -    88K  -
zroot/usr/src@2018-12-01_22.34.00--1w1d        0      -    88K  -

では削除のほうも。
zroot/ROOT、zroot/usr配下のTTL切れsnapshotを削除
ただしzroot/ROOTは非再帰的、zroot/usrは再帰的に。
テスト実行。

# zfsnap destroy -sSvn -R zroot/ROOT -r zroot/usr
/sbin/zfs destroy zroot/ROOT@2018-12-01_22.34.00--1M

期限切れはzroot/ROOT@2018-12-01_22.34.00–1Mのみ。
じゃ、それで本番実行

# zfsnap destroy -sSv -R zroot/ROOT -r zroot/usr
/sbin/zfs destroy zroot/ROOT@2018-12-01_22.34.00--1M ... DONE

以上。

[ZFS][zpool]zpoolのHDDをスタンバイモードにして消費電力を減らそう

Pocket

我が家のNASサーバーはFreeBSDの入ったHP microserverである。
HP microserverにはHDDを4つ入れることができる。
たいへん便利なのだが、HDDが4つとも24時間動きっぱなしというのは精神衛生上、よろしくない。

実は、HDD4つのうちメインで使っているのはOSの入っているものと、Samba共有しているものの2つだけで、残りの2つはバックアップ用である。
少なくとも、残りの2本はバックアップを取得するときだけ動いてくれればよいので、それ以外の間は、HDD内円盤の回転を止めておきたい(スピンダウン)。

しかしSamba共有しているもの、バックアップ用のHDD(要するにOSの入っているHDD)以外はすべてzfsのzpoolを構成しているものである。
気軽にスピンダウンしてしまってよいものだろうか。

というのが調査を始めたきっかけである。

ああ、それと、頻繁に止めたり動かしたりすることがHDDに悪い、というのは重々承知の上。

結論(camcontrol standbyしろ)

結論から言うと、camcontrolでディスクを止めろ。
ディスク停止には、”standby”で。”idle”はやめておけ。

ATAデバイスのモード

ATAデバイスには4つのPower Modeがあり、条件によりそれぞれを行き来する。

  1. Active
  2. Idle
  3. Standby
  4. Sleep

Activeモードは、通常のデータのやり取りができる状態のこと。
それ以外は、Idle > Standby > Sleep の順に消費電力が下がり、反応速度も遅くなる。
詳細は以下書類(PDF注意)の4.18.3 Power modesを参照されたし。
http://www.t13.org/Documents/UploadedDocuments/docs2007/e07163r2-Comments_on_ATA8-ACS_revision_4a.pdf

これを踏まえて、任意のタイミング、あるいは、一定時間アクセスがないタイミングでIdle/Standby/Sleepに遷移させられれば目的が達成できる。
FreeBSDにおいて、Power modeの遷移にはcamcontrolコマンドを使う。
しかしその前に、zfs/zpoolで使えるPower Modeを調べねばならない。

zfsにはStandby

FreeBSD Forumにあるcamcontrol standby weirdnessというスレッドを見ると、以下のような記述がある。要するにStandbyを使えと。Idleだとzfsは当該ディスクをlost/defect/detachedと判断し、zpoolはdegradedになるか、unavailableになるという。

First of all: ZFS is fine with disks that go into STANDBY (accessing a zpool with disks in STANDBY will wake them up and ZFS will wait for it). ZFS cannot handle disks that are in IDLE mode (it considers the disks lost/defect/detached and the zpool will go degraded or even unavailable).

https://forums.freebsd.org/threads/camcontrol-standby-weirdness.59296/#post-340121

自分で試す気にはならないので、そのまま受け取る。

camcontrolによるstabdby操作

再びFreeBSD Forum。[AHCI] Spinning down ada(4) disksというスレッドの以下のポストを参照
https://forums.freebsd.org/threads/ahci-spinning-down-ada-4-disks.8841/page-2#post-98640

camcontrol standby adaXとすればよい。
またcamcontrol standby adaX -T sssとすれば、ただちにstandbyモードに移行し、その後もsss秒、アクセスがなければstandbyモードに移行することができる。
以下なら/dev/ada3をすぐにstandbyモードに移行させるとともに、20分の間アクセスがなければやはりstandbyモードに遷移するようタイマー設定している。今すぐはstandbyモードにしなくてもいいけど、タイマーだけ設定したい、という方法はないみたいだ。

# camcontrol standby ada3 -T 12000

しかし上記のコマンドを実行したとしても、応答はない。なしのつぶてである。
standbyモードに移ったのか。かくなる上はHDDの回転数はいかほどか、耳を澄ますほかはない。
そんなご無体な、とgoogle先生に訴えたところ、FreeNASのフォーラムに以下のポストを発見。

Spinning Down your Drives and Checking Power States. (New Script)

上記にあるスクリプトを使えば、power modeを知ることができる。
本記事の末尾に転載しておく。
get_drive_status.shとして保存しておいた。
実行してみると;

$ sudo drivestatus.sh
ada0:  FF
ada0: Running
ada1:  FF
ada1: Running
ada2:  FF
ada2: Running
ada3:  FF
ada3: Running

camcontrolでstandbyにしておいて再実行。

$ sudo drivestatus.sh
ada0:  FF
ada0: Running
ada1:  00
ada1: Standby
ada2:  00
ada2: Standby
ada3:  00
ada3: Standby

/dev/ada0はOSのあるHDDだからActiveですな。

運用方法(なにかが変)

よしこれで解決。
と思ったのだが、タイマー満了してもpower modeがstandbyに移らないのである。
その割に、そのディスクのディレクトリにアクセスすると、ファイル一覧の表示に待たされたりするのである。

先に引用した彼も同じことに悩んでいる。

But, then, when the disk is idle for > 1800sec (monitor with “zpool iostat 60”), it will not spin down. Huh?

https://forums.freebsd.org/threads/camcontrol-standby-weirdness.59296/#post-339820

この彼は、zpool iostat でディスクへの読み書きをモニターできるので、何分かアクセスがなければidleモードに移すスクリプトを書いた、と言っている。
常駐スクリプトを運用するのが面倒なので、cronジョブでstandbyさせるようにした。

結果

ワットチェッカーなんてものは持っていないので、単純に温度で確認した。
以下がそのグラフ。
ある日を境に、(OSのある/dev/ada0を除いて)温度が劇的に下がっていて、現時点ではまあ満足。

power mode確認スクリプト

#!/bin/bash
#
# Created by: Motorahead
# Date: 02/19/2016
# Checks For Running Status of Connected ADA Devices

# Look for Connected Devices
DEVICELIST=($( camcontrol devlist | grep -o 'ada[0-9]' ))

# Checks Drive Status, but only outputs relative field $10
STATUS(){
camcontrol cmd ${LIST} -a "E5 00 00 00 00 00 00 00 00 00 00 00" -r - | awk '{print $10}'
}

# Loop through each device found in ${DEVICELIST}
for LIST in ${DEVICELIST[@]}; do
        echo -n "${LIST}:  "
        STATUS
# If the Output is 00, then the drive is in Standby. If it's FF, then it's active.
        if [[ "$(STATUS)" == "FF" ]]; then
                echo "${LIST}: Running"
        elif [[ "$(STATUS)" -eq "00" ]]; then
                echo "${LIST}: Standby"
        else
                echo "${LIST} is in a unrecognized state."
    fi
done

[zfs][zpool] zfsの別ディスクへの移行

Pocket

zpoolを容量の多い別HDDに移したのでメモ。
一度、同じようなことはやっているが、時間も経っているので。
最初に書いておくが移行元でまずscrubしておくこと。

流れとしては以下の通り。

  1. 移行元HDD
    1. scrubしておく
    2. snapshot
  2. 移行先HDD
    1. GPTでzfsのパーティションを作成
    2. zpool作成
  3. zfs send, recvで丸々コピー

以下、実作業に触れる前に補足。

方針についての補足

なぜmirrorにしないのか

上記の方法のほかには、追加のディスクをmirrorとしてzpoolに追加し、resilveringが終わったら旧ディスクを外してexpandという手がある。
が、その手は採らない。
実は、zpool statusをする都度、以下のメッセージが表示されていた。
旧ディスクは512Bセクタになっていると文句を言っているのである。
ディスクを交換するか、新Poolに移せ、と。

  pool: vault
 state: ONLINE
status: One or more devices are configured to use a non-native block size.
        Expect reduced performance.
action: Replace affected devices with devices that support the
        configured block size, or migrate data to a properly configured
        pool.
  scan: scrub repaired 0 in 8h3m with 0 errors on Sun Nov  4 04:16:39 2018
config:

        NAME        STATE     READ WRITE CKSUM
        vault       ONLINE       0     0     0
          ada1      ONLINE       0     0     0  block size: 512B configured, 4096B native

 

512Bセクタのpoolにmirrorを追加したら、やっぱり512Bになるんではと心配になったので、mirrorではなく新規ディスクの新規Poolに移す。

なぜディスクをまるっと使わずgptでパーティションを切るのか

zpoolは、わざわざgptパーティションにしなくても、ディスクをまるっと使ってzpoolに指定できる。
できるのだが、/dev/ada2なんて名前よりも、gptラベルでpoolを作成したいのである。
これの有利な点は、例えば当該ディスクの物理的な位置で名前を付けることができる点である。
具体的には、slot_1とか名前を付けておくと、ディスク交換の時に楽。
幸いにもFreeBSDにおいては、gptパーティションを切ってもディスクをそのまま使っても、いずれもパフォーマンスに違いはない。
それならgptにしましょう。

https://www.freebsd.org/doc/handbook/zfs-zpool.html
There is no performance penalty on FreeBSD when using a partition rather than a whole disk.
(2018/11/09)

ではさっそく作業を。

実作業(移行元ディスクの準備)

scrubをしておく。

zpool scrub で。
かなり時間がかかるので覚悟しておくように。
なおscrubは最低3か月に一回が推奨
https://www.freebsd.org/doc/handbook/zfs-term.html#zfs-term-scrub
recommended at least once every three months

作業直前にsnapshot。

pool内に複数のディレクトリがある場合、最上階層でsnapshot -r(recursive)すればよい。
たとえばvaultというpoolにchamber, itunesがある場合に;

vault
vault/chamber
vault/chamber@20170503
vault/itunes
vault/itunes@20170503

※zfs list -t allの出力を一部削除したもの

zfs snapshot vault@20181109とすれば;

vault
vault@20181109
vault/chamber
vault/chamber@20180503
vault/chamber@20181109
vault/itunes
vault/itunes@20180503
vault/itunes@20181109

※zfs list -t allの出力を一部削除したもの

となる。いちいち各ディレクトリ配下でコマンドを叩かなくてよい。

 

移行先作業

GPTパーティションの作成

以下のようなディスク

ada3 at ahcich3 bus 0 scbus3 target 0 lun 0
ada3: ACS-3 ATA SATA 3.x device
ada3: Serial Number WD-WCC7K3PU64NE
ada3: 300.000MB/s transfers (SATA 2.x, UDMA6, PIO 8192bytes)
ada3: Command Queueing enabled
ada3: 3815447MB (7814037168 512 byte sectors)

GPTスキームをcreateし、全領域をfreebsd-zfsに割り当てる。
このとき、-lオプションでラベルを付ける。
同じくgpart showに-lオプションを付ければ名前を確認できる。

# gpart create -s GPT ada3
ada3 created
# gpart add -l slot_4 -t freebsd-zfs /dev/ada3
ada3p1 added
$ gpart show ada3
          40  7814037088  ada3  GPT  (3.6T)
          40  7814037088     1  freebsd-zfs  (3.6T)

$ gpart show -l /dev/ada3
          40  7814037088  ada3  GPT  (3.6T)
          40  7814037088     1  slot_4  (3.6T)

zpoolの作成

/dev/ada3ではなくGPTラベルで指定する。
GPTラベルで指定するときは、gpt/<GPTラベル>というように指定する。

# zpool create warehouse gpt/slot_4
# zpool status warehouse
  pool: warehouse
 state: ONLINE
  scan: none requested
config:
        NAME               STATE     READ WRITE CKSUM
        warehouse          ONLINE       0     0     0
          gpt/slot_4  ONLINE       0     0     0

プロパティの変更

zfsを作成する前に。
圧縮モードをlz4にする。lz4はCPU負荷の割には非常に効率が良いので積極的に有効にしたい。
また、atime(アクセスタイムの記録)もoffに。これがonだと差分snapshotが失敗するから、というのが一つと、これをoffにするとパフォーマンスが上がる(can result in significant performance gains)から。
zfs get <プロパティ名> で現在の値を取得。
zfs set <プロパティ名>=<値> で値のセット。
圧縮モードのプロパティ名はcompression、atimeはatime。

$ zfs get compression warehouse
NAME       PROPERTY     VALUE     SOURCE
warehouse  compression  off       default
$ zfs get atime warehouse
NAME       PROPERTY   VALUE     SOURCE
warehouse  atime      on        default

# zfs set compression=lz4 warehouse
# zfs set atime=off warehouse
$ zfs get compression warehouse
NAME       PROPERTY     VALUE     SOURCE
warehouse  compression  lz4       local
$ zfs get atime warehouse
NAME       PROPERTY   VALUE     SOURCE
warehouse  atime      off       local

ディレクトリの用意とsend/recv

# zfs create warehouse/itunes
# zfs create warehouse/chamber

このとき、勝手にマウントされないようにすべきだった。-uを付ければよかったかな。
いずれにせよzfs set mountpointで後から変えられる。

sendに-Rを付けると、子孫やスナップショットもまとめてsendされる。

# zfs send -R vault/chamber@20181109 | zfs receive warehouse/chamber
# zfs send -R vault/itunes@20181109 | zfs receive warehouse/itunes

かかった時間、容量は以下の通り。うーむ。ちょっとかかったかな。

 991GiB 3:09:00 [89.5MiB/s]
1.10TiB 3:58:24 [80.3MiB/s] 

このとき適当にhtopを叩いた結果。CPU負荷はそんなんでもないので、ボトルネックは別のところにあるように思われる。

  1  [|||||||||||||||||||||                47.6%]   Tasks: 43, 0 thr; 2 running
  2  [|||||||||||||||||                    37.7%]   Load average: 0.91 1.77 1.86 
  Mem[||||||||||||||||||||||||||||||||642M/1.84G]   Uptime: 12:03:30
  Swp[|||||||||                        176M/907M]

なおCPUはAMDのTurionである。

compression=lz4の結果は.
プロパティused(HDD上の容量)とlogicalused(圧縮前容量)で調べられる。

warehouse  used                  2.00T                    -
warehouse  logicalused           2.02T                    -

本来なら2.02Tのところ、2.00Tで済んでる。

これで引っ越し完了。

以上

Linuxに注文の多い料理店をしゃべらそう(Open Jtalk)

Pocket

Nano Pi Neo2にOpen JTalkでしゃべらせたのでメモ。

Open Jtalkは「入力された日本語テキストに基づいて自由な音声を生成するHMMテキスト音声合成システム」とのこと。
http://open-jtalk.sp.nitech.ac.jp/

open-jtalk、辞書をインストールする。
open-jtalkをインストールすれば辞書もついてくる。

$ apt search open-jtalk
ソート中... 完了
全文検索... 完了  
open-jtalk/artful 1.10-1 amd64
  日本語音声合成システム

open-jtalk-mecab-naist-jdic/artful,artful,now 1.10-1 all
  NAIST Japanese Dictionary for Open JTalk

$ sudo apt-get install open-jtalk
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
提案パッケージ:
  hts-voice-nitech-jp-atr503-m001
以下のパッケージが新たにインストールされます:
  open-jtalk open-jtalk-mecab-naist-jdic
(略)

提案パッケージであるhts-voice-nitech-jp-atr503-m001も。
これは音声データ。男性の声。

hts-voice-nitech-jp-atr503-m001 - Japanese male voice data for Open JTalk

open-jtalkのコマンドはopen_jtalk(間はアンダースコア)として、open_jtalkの辞書は/var/lib/mecab/dic/open-jtalk/naist-jdicに、音声データは/usr/share/hts-voice/にインストールされる。
「こんにちは」と書いたvoice.txtを作り、open_jtalkに食わす。

$ cat ./voice.txt 
こんにちは

$ open_jtalk -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -x /var/lib/mecab/dic/open-jtalk/naist-jdic -ow test.wav ./voice.txt 

喋ってくれるはず。
やはり女性の声のほうが良いので、名工大のMMDAgentを使う。
http://share.udialogue.org/meissen/login.htm
2018/3/21時点で1.7が最新
ダウンロードして展開し、/usr/share/hts-voice/に格納する。
使うのは中身の「mei」ディレクトリ配下のみ。
Meiさんは名工大キャラクターだそうな。

$ wget https://sourceforge.net/projects/mmdagent/files/MMDAgent_Example/MMDAgent_Example-1.7/MMDAgent_Example-1.7.zip
$ unzip ./MMDAgent_Example-1.7.zip 
$ sudo cp -R ./MMDAgent_Example-1.7/Voice/mei /usr/share/hts-voice/

声には以下の5種類がある。実際に発声させる際に選ぶ。
mei_angry.htsvoice mei_happy.htsvoice mei_sad.htsvoice
mei_bashful.htsvoice mei_normal.htsvoice

以下のスクリプトを用意する。
どこかのサイトから持ってきたんですが、どのサイトからだったか忘れてしもうた…。
これをjtalk.pyとして保存する。1行目は環境に応じて変えること。
htsvoiceは上記の通り喜怒哀通常から選べる。
speedも選べる。
このあたりは試行錯誤。

あとはこれをimportして使う。

#!/usr/bin/python

#coding: utf-8
import subprocess
from datetime import datetime

def jtalk(t):
    open_jtalk=['/usr/bin/open_jtalk']
    mech=['-x','/var/lib/mecab/dic/open-jtalk/naist-jdic']
    htsvoice=['-m','/usr/share/hts-voice/mei/mei_normal.htsvoice']
    speed=['-r','1.0']
    outwav=['-ow','/tmp/open_jtalk.wav']
    cmd=open_jtalk+mech+htsvoice+speed+outwav
    c = subprocess.Popen(cmd,stdin=subprocess.PIPE)
    c.stdin.write(t.encode('utf-8'))
    c.stdin.close()
    c.wait()
    aplay = ['aplay','-q','/tmp/open_jtalk.wav']
    wr = subprocess.Popen(aplay)

def say_datetime():
    d = datetime.now()
    text = '%s月%s日、%s時%s分%s秒' % (d.month, d.day, d.hour, d.minute, d.second)
    jtalk(text)

if __name__ == '__main__':
    say_datetime()

先ほどのファイルをimportして、jtalk.jtalk(string)で、発話させたい文章を渡せばOK。

#!/usr/bin/python

#coding: utf-8
import jtalk

jtalk.jtalk('何か話してください')

さあテストである。
青空文庫「注文の多い料理店」を朗読させてみる。
「新字新仮名」版をダウンロード。
http://www.aozora.gr.jp/cards/000081/card43754.html

そのままではOpen Jtalkに渡せないので、以下の変更を加える。

  • 文字コードをShift-JISからUTF-8へ
  • ルビを削除(正規表現パターンは”《.*?》”。)
  • ルビの個所を示す区切り記号を削除(”|”)
  • 構成を示す注釈を削る(正規表現パターンは”\[.*?\]”。)
  • 改行コードをCR+LFをLFのみに。(\rを削る)

できれば以下も。

  • 行頭字下げ、要するに行頭の全角スペースを削除(正規表現パターンは”^ ”。)
  • セリフのカギかっこを削る。行頭のものだけでよい。(正規表現パターンは”^「”。)

以下までできれば完璧

  • 改行をいったん全て削り、句点のあとに改行コード(\n)を挿入

neko.txtとでも保存し、以下のPythonスクリプトを実行。

#!/usr/bin/python

import jtalk

for line in open('neko.txt','r'):
    print(line)
    jtalk.jtalk(line)

こんな感じ。

試してみるとわかるが、漢字の読み方が変だったり(山猫軒を「やまねこのき」と読んだり)、読めない漢字があったりする。
そりゃまあ仕方ないよね。

 
それはさておき。
十分なスペックがあるときに複数行の文章を読ませると、発話が終わっていないのに次の行の処理を始めてしまって困ることがある。
そういうときは発話の終了を待つよう、wait()を入れればよい。
関数jtalk()の最後に一行追加。

def jtalk(t):
    open_jtalk=['/usr/bin/open_jtalk']
    mech=['-x','/var/lib/mecab/dic/open-jtalk/naist-jdic']
    htsvoice=['-m','/usr/share/hts-voice/mei/mei_normal.htsvoice']
    speed=['-r','1.0']
    outwav=['-ow','/tmp/open_jtalk.wav']
    cmd=open_jtalk+mech+htsvoice+speed+outwav
    c = subprocess.Popen(cmd,stdin=subprocess.PIPE)
    c.stdin.write(t.encode('utf-8'))
    c.stdin.close()
    c.wait()
    aplay = ['aplay','-q','/tmp/open_jtalk.wav']
    wr = subprocess.Popen(aplay)
    wr.wait()

 

参考サイト
Raspberry piで日本語音声合成(Open JTalk)を試してみる。

Linuxで音声の出力先をUSBスピーカーにする

Pocket

Nano Pi Neo2にスピーカーを繋いだのだが、ノイズが入ってたまらない。
そこでUSBスピーカーから音を出すようにしてみる。
USBスピーカーはこれ。USBケーブル一本でいいので重宝している。

まずalsamixerで音量を確かめておく。

次。
aplayコマンドを使って、音声出力のハードウェアデバイスのcardとdevice番号を調べる。
—helpオプションで使い方を。

$ aplay --help
Usage: aplay [OPTION]... [FILE]...

-h, --help help
--version print current version
-l, --list-devices list all soundcards and digital audio devices
-L, --list-pcms list device names
(略)

-lオプションで一覧を得る。

$ aplay -l
<span class="gs">****</span> List of PLAYBACK Hardware Devices <span class="gs">****</span>
card 0: Codec <span class="p">[</span><span class="nv">H3 Audio Codec</span><span class="p">]</span>, device 0: CDC PCM Codec-0 <span class="err">[</span>]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: allwinnerhdmi <span class="p">[</span><span class="nv">allwinner,hdmi</span><span class="p">]</span>, device 0: 1c22800.i2s-i2s-hifi i2s-hifi-0 <span class="err">[</span>]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 2: MicroII <span class="p">[</span><span class="nv">Audio Advantage MicroII</span><span class="p">]</span>, device 0: USB Audio <span class="p">[</span><span class="nv">USB Audio</span><span class="p">]</span>
  Subdevices: 1/1
  Subdevice #0: subdevice #0

USBスピーカーだから最後のものが該当。
MicroIIということがわかる。

speaker-testコマンドでテストする。
–helpで確認するのが定石

$ speaker-test --help

speaker-test 1.1.0

Usage: speaker-test [OPTION]... 
-h,--help       help
-D,--device     playback device
-r,--rate       stream rate in Hz
-c,--channels   count of channels in stream
-f,--frequency  sine wave frequency in Hz
-F,--format     sample format
-b,--buffer     ring buffer size in us
-p,--period     period size in us
-P,--nperiods   number of periods
-t,--test       pink=use pink noise, sine=use sine wave, wav=WAV file
-l,--nloops     specify number of loops to test, 0 = infinite
-s,--speaker    single speaker test. Values 1=Left, 2=right, etc
-w,--wavfile    Use the given WAV file as a test sound
-W,--wavdir     Specify the directory containing WAV files
-m,--chmap      Specify the channel map to override
-X,--force-frequency    force frequencies outside the 30-8000hz range
-S,--scale      Scale of generated test tones in percent (default=80)

Recognized sample formats are: S8 S16_LE S16_BE FLOAT_LE S32_LE S32_BE

-Dオプションで出力先を選ぶ。
出力先の指定はデバイスネームだからさっきのaplayコマンドで再チェック

$ aplay -L|grep MicroII
sysdefault:CARD=MicroII
    Audio Advantage MicroII, USB Audio
front:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
surround21:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
surround40:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
surround41:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
surround50:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
surround51:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
surround71:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
iec958:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
dmix:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
dsnoop:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
hw:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio
plughw:CARD=MicroII,DEV=0
    Audio Advantage MicroII, USB Audio

ということで、hw:MicroIIで試す。

$ speaker-test -Dhw:MicroII

speaker-test 1.1.0

Playback device is hw:MicroII
Stream parameters are 48000Hz, S16_LE, 1 channels
Using 16 octaves of pink noise
Channels count (1) not available for playbacks: Invalid argument
Setting of hwparams failed: Invalid argument

鳴らない。

この辺を読み、hwではなくplughwにしてみる。
http://www.volkerschatz.com/noise/alsa.html

$ speaker-test -Dplughw:MicroII

speaker-test 1.1.0

Playback device is plughw:MicroII
Stream parameters are 48000Hz, S16_LE, 1 channels
Using 16 octaves of pink noise
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 96 to 262144
Period size range from 48 to 131072
Using max buffer size 262144
Periods = 4
was set period_size = 65536
was set buffer_size = 262144
 0 - Front Left
Time per period = 2.769702
 0 - Front Left
^CTime per period = 2.426893

鳴った。
チャンネル2つにしてみると

$ speaker-test -c2 -Dplughw:MicroII

speaker-test 1.1.0

Playback device is plughw:MicroII
Stream parameters are 48000Hz, S16_LE, 2 channels
Using 16 octaves of pink noise
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 96 to 262144
Period size range from 48 to 131072
Using max buffer size 262144
Periods = 4
was set period_size = 65536
was set buffer_size = 262144
 0 - Front Left
 1 - Front Right
^CTime per period = 3.587277

左右で鳴る。
さらにaplayで試す。

$ aplay /usr/share/sounds/alsa/Front_Center.wav -D plughw:MicroII
Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono

よさそう。
これでmpg123などを使えば、mp3ファイルなども鳴らすことができる。
mpg123でデバイスを指定するには-aオプションで。

mpg123 -q -a plughw:MicroII XXXX.mp3

ためしにデバイスにdefaultを指定すると文字通り標準設定のデバイスから出力を試みる。
USBスピーカーはならないはず。

$ aplay /usr/share/sounds/alsa/Front_Center.wav -D default

デフォルトの出力先を変えるには、以下の手順に沿えばよい。
https://www.alsa-project.org/main/index.php/Setting_the_default_device

 

[メモ][FreeBSD] 複数Jailのpkg upgradeを一気に済ませたい

Pocket

メモ。

jailのpkg操作は、いちいちそのjailの中に入らなくてもpkg -j <jail名 or jail ID> …というように-jオプションを付与すればできる。

しかしjailの指定は、個々に行う必要がある。
たとえばpkg -j * upgradeというように、ワイルドカードを使うようなことは、もってのほかのようである。
jailなんてどんどん増えていくのに、いちいち-jで指定していくのもなあ。
仕方ないんで、やっつけでパイプを繋いで実現。

jls -N|tail -n +2|cut -f2 -d" "|xargs -I{} sudo pkg -j {} upgrade

以上

[メモ] SlackとMattermostそれぞれのincomingWebhookへのポスト方法の違い

Pocket

MattermostはSlackのクローンであるが、incomingWebhookへのポストだけは、どうも違いがあるようだ。
以下にSlack/Mattermostそれぞれでの実例を示す。
まずmattermost

# mattermost

import requests

incomWebHook = 'http://192.168.10.1/hooks/xxxxx'
msg = 'test message'
icon_url = 'http://192.168.10.10/graphix/robot.png'
username = 'bot'

def sendMsg(incomWebHook, icon_url, username, msg):
    payload = {
            'text': msg,
            'icon_url': icon_url,
            'username': username}

    r = requests.post(
        incomWebHook,
        json=payload)

    return r

r = sendMsg(incomWebHook, icon_url, username, msg)

Slack

# slack
import requests

incomWebHook ="https://slack.com/api/chat.postMessage"
msg = 'test message'
username = 'bot'
token = "xoxb-xxxx-xxxx"

def sendMsg(incomWebHook, token, username, msg):
    payload_dic = {
            "token": token
            "type": "message",
            "channel": "#general",
            "text": msg,
            "username": username,
            "icon_emoji": ":bird:",
            }

    headers = {
            'Content-type': 'application/json',
            'Accept': 'text/plain'
            }

    r = requests.post(
            incomWebHook,
            params=payload_dic,
            headers=headers)

    return r

r = sendMsg(incomWebHook, token, username, msg)

[メモ][Python] AnacondaをWindows xpにインストールしようとすると失敗する

Pocket

Anacondaは、Pythonのほかに、IPython、Spyder、pandas他便利ライブラリが一式でついてくるお手軽Python環境である。

しかしAnacondaをWindows xpにインストールしようとすると失敗する。
具体的には、インストール途中の「メニューを作る」ステップで失敗。
無視して進めると、C:\Anaconda3に実行ファイルは揃っているので、「なんだ気のせいか」と安心して起動しようとすると「有効なWin32アプリケーションではありません」という旨のエラーが発生する。

原因は、最新版がxpに対応していないせい。
以下から2.2.0をダウンロードし、インストールする。

https://repo.continuum.io/archive/

念のためファイル名とハッシュ値も残す。

Anaconda3-2.2.0-Windows-x86.exe
7c49a4e76e1c383038c4a1e8c4ac506f

Anaconda-2.2.0-Windows-x86.exe
32246b48658d4c3faeef425cec64a131

AnacondaのFAQにしっかり記載がある。

https://docs.continuum.io/anaconda/faq

How can I use Anaconda on Windows XP?
Windows XP is supported on Anaconda versions 2.2 and earlier. Download it from our archive.

Slack/MattermostのbotをPythonのFlaskで作ろう

Pocket

Slack/Mattermost(以降、まとめてSlack)のbotといえばHubotが有名だ。
Hubotはcoffeescriptで書かれているのだが、botのためだけに新しい言語を覚えるのも面倒。
加えてHubotを動作させるにはnpmが必要。
ちょっとそこまでは…。
Pythonなら普段使いで最初から環境は作ってあるし、楽だし。
そう思っていろいろと調べてみた。

Slackのbotに必要なことは?

Slackのbotに必要なのは、端的に以下の二つ。
1. Slackからのデータ送信を受ける。
2. Slackへ何かをする。

1.は、HTTP POSTでやってくるので、POSTを受けることさえできればよい。
2.は、例えばSlack上で何かの発言をすることだが、こちらはcurl(Pythonならrequests)でも使えば簡単にできる。

PythonでHTTP POSTを受けるには?

Flaskである。
Flaskとはごくごく軽量なwebフレームワークである。
驚くくらい簡単にwebサーバを作ることができる。
もちろんPOSTを受けるのもすぐにできる。
以下、説明。

Flaskのインストール

pipでもなんでも、好きなものを使ってインストールせい。

Flaskのテスト

以下のようなスクリプトを書く。
適当な名前で保存する。
flask.pyとかはやめようね。

#!/usr/local/bin/python

from flask import Flask

app = Flask(__name__)

@app.route('/')
def welcome():
    html = '<html><title>welcome</title>'
    html = html + '<body>welcome</body></html>'
    return html

if __name__ == '__main__':
    app.debug = True
    app.run(host='0.0.0.0')

上記のapp.route(‘/’)とは、ルーティング。
ルートディレクトリ(’/’)へのアクセスがあったら、次のdef welcome()が(デコレートによって)呼ばれる、ということである。

上記の場合、welcomeと表示するhtmlが返る。

app.debug = Trueについては後述。
app.run(host=’0.0.0.0)のhost=…の記述により、サーバを外部に公開することになる。

テストスクリプトの実行

$ python ./flasktest.py
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 213-546-048

localhostのポート5000でサーバが立ち上がったと分かる。
ブラウザでアクセスして「welcome」と表示されればよい。

ところで、app.debut = Trueだが、スクリプトを書き換えると、自動的に再読み込みをしてくれる。
実際のログは以下。

 * Detected change in '/usr/home/doe/py/flasktest.py', reloading
 * Restarting with stat
 * Debugger is active!

激烈に便利な機能なので、開発中は有効にしておくこと。

SlackからのPOSTを受ける。

先ほどのスクリプト冒頭のimportを以下のように変更。

from flask import Flask, request

さらに以下を追加。

@app.route('/matter', methods=['POST'])
def post():
    print request.form

上記のようにしておいて、Slack側のOutgoing Webhookで、呼び出すURLに「http://<あなたのサーバのアドレス>:5000/matter」を指定する。

Slack側からwebhookを呼び出してみると、POSTされた内容がログに表示されるはず。
なお、def post()が何も返さないのでエラーは出るが気にしない。

以下が例。

ImmutableMultiDict([('user_id', u'xxxxx'), ('channel_id', u'xxxxx'), ('timestamp', u'1414562326'), ('team_id', u'xxxx'), ('trigger_word', u'test'), ('channel_name', u'off-topic'), ('token', u'xxxtokenxxx'), ('text', u'test'), ('team_domain', u'xxxx'), ('user_name', u'xxxx')])

つまりPOSTの内容はディクショナリになっている。

Slackへ応答を返そう

Slackへ応答を返す場合には、POSTでもらった中にあるtokenが必要である。
応答文、tokenのディクショナリを作っておいて、それをjsonに変換し、返してやればよい。
Mattermostの場合、tokenのキーは’MATTERMOST_TOKEN’

先ほどのスクリプトを最終的に以下のようにする。
(わざわざjsonをimportしているが、Flaskにもそういう機能がある気がしてならない)
これでSlackから通知を受けると、「You said <トリガーワード>」という応答がSlackへ帰る。

#!/usr/local/bin/python

from flask import Flask, request
import requests
import json

app = Flask(__name__)

@app.route('/')
def welcome():
    html = '<html><title>welcome</title>'
    html = html + '<body>welcome</body></html>'
    return html

@app.route('/matter', methods=['POST'])
def post():
    postdic = request.form
    incomeText = postdic['text']
    token = postdic['token']
    text = 'You said ' + incomeText

    payload = {
            'text': text,
            'MATTERMOST_TOKEN': token}
    json_payload = json.dumps(payload)
    return json_payload

if __name__ == '__main__':
    app.debug = True
    app.run(host='0.0.0.0')

あとはどんどん膨らませていけばよい。