Blog

  • Debian 9 Stretch で unattended-upgrades による自動更新

    Debian 9 Stretch で unattended-upgrades による自動更新

    apt update & upgrade は日常的に実行するものですが、忙しかったりすると疎かになりがちです。そこで unattended-upgrades を有効にしておくことで、j自動で更新を行えます。

    インストール

    $ sudo apt install unattended-upgrades
    Reading package lists… Done
    Building dependency tree
    Reading state information… Done
    Suggested packages:
    bsd-mailx needrestart
    The following NEW packages will be installed:
    unattended-upgrades
    0 upgraded, 1 newly installed, 0 to remove and 1 not upgraded.
    Need to get 61.7 kB of archives.
    After this operation, 252 kB of additional disk space will be used.
    Get:1 http://http.us.debian.org/debian stretch/main amd64 unattended-upgrades all 0.93.1+nmu1 [61.7 kB]
    Fetched 61.7 kB in 0s (82.8 kB/s)
    Preconfiguring packages …
    Selecting previously unselected package unattended-upgrades.
    (Reading database … 38885 files and directories currently installed.)
    Preparing to unpack …/unattended-upgrades_0.93.1+nmu1_all.deb …
    Unpacking unattended-upgrades (0.93.1+nmu1) …
    Processing triggers for systemd (232-25+deb9u6) …
    Setting up unattended-upgrades (0.93.1+nmu1) …
    Processing triggers for man-db (2.7.6.1-2) …

    インストール後に dpkg-reconfigure で有効化します。

    有効化

    $ sudo dpkg-reconfigure unattended-upgrades
    dpkg-reconfigure unattended-upgrades 1/2
    dpkg-reconfigure unattended-upgrades 2/2

    自動更新の対象とするパッケージの指定です。

    autoremove の設定

    /etc/apt/apt.conf.d/50unattended-upgrades を編集し、不要になったパッケージの削除を有効にします。

    // ...略...

    Unattended-Upgrade::Remove-Unused-Dependencies "true";

    // ...略...

    /etc/apt/apt.conf.d/20auto-upgrades に以下を追記し、autoremove の実行間隔を設定します。

    APT::Periodic::AutocleanInterval "1";

    ここの単位は「日」ですので、”1″ であれば 1 日毎に実行、”0″ なら無効になります。

    // Do “apt-get autoclean” every n-days (0=disable)
    APT::Periodic::AutocleanInterval “21”;

    UnattendedUpgrades – Debian Wiki

    メール通知

    mailx コマンドでメールが送れることを確認しておきましょう。

    $ sudo apt install mailutils

    /etc/apt/apt.conf.d/50unattended-upgrades を編集し、 次の一行を編集して有効にします。

    Unattended-Upgrade::Mail "[email protected]";

    エラーの時のみメール通知する場合は、次の一行も編集します。

    Unattended-Upgrade::MailOnlyOnError "true";

    動作確認

    root で unattended-upgrades を実行すれば実際の動作を確認できます。-d(–debug) オプションを付けると処理内容を表示してくれます。

    $ sudo unattended-upgrade -d
    Initial blacklisted packages:
    Initial whitelisted packages:
    Starting unattended upgrades script
    Allowed origins are: ['origin=Debian,codename=stretch']
    pkgs that look like they should be upgraded:
    Fetched 0 B in 0s (0 B/s)
    fetch.run() result: 0
    blacklist: []
    whitelist: []
    No packages found that can be upgraded unattended and no pending auto-removals

    参考

  • Python でメールの添付ファイルを zip して送る

    Python でメールの添付ファイルを zip して送る

    Python スクリプトからメールを送るのは smtplib があるおかげで、わりと簡単にできます。ですが、添付ファイルを付けようと思うとちょっと手間でした。

    今回は簡潔に書くため、全て localhost の SMTP サーバを利用しています。Gmail の SMTP サーバで送るときは TLS を使う必要があるのでご注意ください。

    テキストだけのメール

    import smtplib
    from email.mime.text import MIMEText
    from email.header import Header
    from email.utils import formatdate
    
    def mime_message(from_addr, to_addr, subject, body):
    	encoding = 'utf-8'
    
    	m = MIMEText(body, 'plain', encoding)
    	m['Subject'] = Header(subject, encoding)
    	m['To'] = to_addr
    	m['From'] = from_addr
    	m['Date'] = formatdate()
    	
    	return m
    
    def send_message(m):
    	smtp_host = 'localhost'
    	
    	s = smtplib.SMTP(smtp_host)
    	s.sendmail(m['From'], m['To'], m.as_string())
    	s.quit()
    
    to_addr = '[email protected]'
    from_addr = '[email protected]'
    subject = 'メールのテスト'
    body = 'このメールはテストメールです。'
    
    send_message(mime_message(from_addr, to_addr, subject, body))

    smtplib.sendmail() の from と to は RFC を確認せず書いているので、もしかしたら引っかかるかもしれません(“atmark <[email protected]>” みたいな形式)。

    ファイルを zip して添付

    変更するのはメッセージの作成部分だけです。

    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.base import MIMEBase
    from email.mime.text import MIMEText
    from email.header import Header
    from email.utils import formatdate
    from email import encoders
    
    import tempfile, zipfile
    import os.path
    
    def mime_email(to, subject, body, attachment_path = None):
    	encoding = 'utf-8'
    
    	m = MIMEMultipart()
    	m['Subject'] = Header(subject, encoding)
    	m['To'] = to
    	m['Date'] = formatdate()
    
    	# 本文
    	m.attach(MIMEText(body, 'plain', encoding))
    
    	if attachment_path:
    		# 添付ファイルを zip でまとめる
    		temp = tempfile.TemporaryFile()
    		with zipfile.ZipFile(temp, 'w', compression = zipfile.ZIP_DEFLATED) as zip:
    			zip.write(attachment_path, arcname = os.path.basename(attachment_path))
    		temp.seek(0)
    
    		# attach file
    		a = MIMEBase('application', 'zip')
    		a.set_payload(temp.read())
    		encoders.encode_base64(a)
    		m.attach(a)
    		a.add_header('Content-Disposition', 'attachment', filename = date.today().strftime('attachment.zip'))
    	
    	return m

    zipfile.write(filename) は与えられたパスのファイルをアーカイブします。このとき引数 arcname でアーカイブ時のファイル名を指定できます。指定しないとファイル名がそのまま arcname になりますが、無駄なディレクトリ構造を排除したいので os.path.basename() でファイル名を取得して指定しています。同じファイル名が存在する場合は適宜変更してください。

    複数のファイルを zip するときも、次のように zip.write() を続け呼び出すだけです。

    attachment_files = ['path_to_file.txt', 'long_path_to_file.txt']
    for p in attachment_files:
    	zip.write(p, arcname = os.path.basename(p))

    compression = zipfile.ZIP_DEFLATED を指定することで zip 全体を圧縮しています。compression には以下の値を指定できます。

    compression説明依存
    zipfile.ZIP_STORED無圧縮
    zipfile.DEFLATED通常の圧縮zlib
    zipfile.BZIP2BZIP2 圧縮bz2, Python 3.3 以降
    zipfile.LZMALZMA 圧縮lzma, Python 3.3 以降

    compression は zipfile.write() で個別に指定することもできます。

    参考

  • Python でのプロファイリング

    Python でのプロファイリング

    サッと確認

    $ python3 -m cProfile slow_script.py

    時間がかかっている順に並び替え。

    $ python3 -m cProfile -s tottime slow_script.py

    視覚的に確認

    準備

    $ sudo pip3 install pycallgraph
    $ sudo apt install graphviz

    プロファイリング

    $ pycallgraph graphviz -- slow_script.py

    pycallgraph.png が生成されます。

  • Python と SQLite3 で pivot を実現する

    Python と SQLite3 で pivot を実現する

    Excel で集計を行うときに使う「ピボットテーブル」という機能があります。複雑な集計表をマウス操作だけで作れるので、使い方によってはとても便利な機能です。

    使ったことがない方に簡単に説明します。次のようなデータベースを考えます。

    品目産地数量単位
    りんご青森県5
    りんご長野県30
    りんご長野県2
    りんご青森県10
    みかん長崎県7
    みかん静岡県120
    みかん福岡県50

    このデータから、都道府県と品目ごとの数量を集計するとします。ただし、集計するのは単位が「箱」のものだけ。すると次のような表になります。

    単位:箱りんごみかん
    青森県15
    長野県2
    長崎県7

    これを項目を選ぶだけで自動で生成してくれるのが、Excel のピボットテーブルです。

    SQL だけでこのピボットを簡単に作る方法はないのかというと、SQL Server には PIVOT 句があって簡単に実現できます。Oracle にもあるようです。しかし、それ以外の MySQL, MariaDB, PostgreSQL, SQLite3 では PIVOT 句は今のところ無いようでした。無いからといってできないわけではなく、例えば次の記事のように色々と組み合わせればできないこともないようです。

    記事を読まなくても SQL 文を見てもらえばわかると思いますが、よほど SQL だけで解決しないといけない状況でない限り、正直採りたくない手法です。どうせ SQL でデータを取得してからプログラム側で処理するのだから、プログラム側で処理すればいいと思い直したのが今回のお話です。

    説明用に SQLite3 で次のようなデータベースを作成しました。

    CREATE TABLE shipping (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        item TEXT NOT NULL,
        qua INTEGER NOT NULL,
        unit TEXT NOT NULL,
        pref TEXT NOT NULL
    );

    サンプルデータとして次の CSV を予め insert しておきます。

     0,"いちご",10,"箱","岡山県"
    1,"ぶどう",33,"個","長崎県"
    2,"りんご",65,"箱","長野県"
    3,"みかん",25,"箱","福岡県"
    4,"ぶどう",2,"個","長野県"
    5,"ぶどう",42,"個","長野県"
    6,"いちご",31,"箱","岡山県"
    7,"ぶどう",51,"箱","長崎県"
    8,"りんご",79,"箱","福岡県"
    9,"いちご",92,"箱","長崎県"
    10,"ぶどう",40,"箱","岡山県"
    11,"いちご",12,"箱","長崎県"
    12,"いちご",56,"箱","長野県"
    13,"いちご",11,"個","長野県"
    14,"みかん",90,"個","山梨県"
    15,"ぶどう",90,"個","長野県"
    16,"みかん",44,"箱","岡山県"
    17,"いちご",93,"箱","長崎県"
    18,"りんご",13,"個","青森県"
    19,"ぶどう",20,"個","長野県"

    余談ですが、このデータは下記の Python スクリプトで生成させてます。

    import random
    
    item = ('りんご', 'みかん', 'ぶどう', 'いちご')
    pref = ('青森県', '長野県', '山梨県', '岡山県', '福岡県', '長崎県')
    unit = ('箱', '個')
    
    for i in range(20):
    	print('{index},"{item}",{qua},"{unit}","{pref}"'.format(index = i, item = random.choice(item), pref = random.choice(pref), unit = random.choice(unit), qua = random.randrange(1, 100)))

    CSV を SQLite のデータベースに取り込むには、予めテーブルを用意した上でコマンドラインで .import FILE TABLE を実行します。SQLite の標準区切り子は “|” なので、CSV を読み込む前に “,” に変更しておく必要があります。

    sqlite> .separator ,
    sqlite> .import data.csv shipping

    さて、肝心の Python プログラム側ですが、今回は実験も兼ねて SQL は完全にデータの取得だけ、処理は全て Python で行いました。

    import sqlite3
    
    # pivotのフィルタ
    pivot_filter = 'unit'
    pivot_filter_value = '箱'
    
    # pivotする列と行と値
    pivot_col = 'item'
    pivot_row = 'pref'
    pivot_val = 'qua'
    
    # SQLite3からデータ取得
    connect = sqlite3.connect('db.sqlite3')
    connect.text_factory = str
    connect.row_factory = sqlite3.Row
    rows = connect.execute('SELECT * FROM shipping;').fetchall()
    rows = [r for r in rows if r[pivot_filter] == pivot_filter_value]
    
    # pivotの列項目と行項目
    pivot_cols = {r[pivot_col] for r in rows}
    pivot_rows = {r[pivot_row] for r in rows}
    
    # pivot初期化
    pivot = {row : {col : 0 for col in pivot_cols} for row in pivot_rows}
    
    # pivot集計
    for r in rows:
    	pivot[r[pivot_row]][r[pivot_col]] += r[pivot_val]
    
    # pivot出力
    # 列項目
    for c in sorted(pivot_cols):
    	print('\t{}'.format(c), end = '')
    print('')
    # 一度行毎に出力
    for r in sorted(pivot_rows):
    	# 行項目
    	print('{}'.format(r), end = '')
    	# 集計値
    	for c in sorted(pivot_cols):
    		print('\t{}'.format(pivot[r][c]), end = '')
    	print('')

    これを先ほどのデータで実行した結果は次のようになりました。

           いちご ぶどう みかん りんご
    岡山県 41 40 44 0
    福岡県 0 0 25 79
    長崎県 197 51 0 0
    長野県 56 0 0 65

    タブ区切りのテキスト表示なのでちょっと見難いですが、ちゃんとピボットできていますね。

    pivotの出力結果

    この記事はここで終了です、と書こうと思って、試しにデータを 100 万件にしてみたところ、処理がめちゃくちゃ遅いに気付いてしまいました。予想するに SQL で全てしてしまった方が圧倒的に速いのですが、それでは今回の目的である手間を最小にすることから外れてしまいます。なので出来る限り手間を掛けずに高速化できないか、試してみます。

    高速化前

    以下、先に挙げたランダムなデータ生成スクリプトで 100 万件のデータを用意し、SQLite3 に INSERT 済の状態での検証です。

    $ time python3 pivot.py > result.txt

    シェルで上記を 5 回実行し、その平均時間を処理時間とします。

    1. 22.426
    2. 22.441
    3. 22.789
    4. 22.366
    5. 22.335

    高速化前は 22.471 秒となりました。

    高速化 1 – フィルタ処理を SQL で

    まずはすぐにでも思いつきそうな、Python のリスト内包表記で行っているフィルタ処理を SQL の WHERE 句で行う場合。ソースは差分だけ載せます。

    rows = list(connect.execute('SELECT * FROM shipping WHERE {} = ?;'.format(pivot_filter), (pivot_filter_value, )).fetchall())
    1. 13.371
    2. 13.327
    3. 13.226
    4. 13.336
    5. 13.227

    平均 13.297 秒、約 9 秒の高速化。割合にすると 40% ですね。

    高速化 2 – GROUP BY で集計

    これもこの記事を書きながら思いついた方法。純粋にこの方法での影響を調べるために高速化 1 は一度元に戻して検証します。

    rows = connect.execute('''
     	SELECT
     		{0}, {1}, {2}, SUM({3}) AS {3}
    	FROM
     		shipping
    	GROUP BY
     		{0}, {1}, {2};'''.format(
     			pivot_row, pivot_col, pivot_filter, pivot_val)
     		).fetchall()
    1. 17.721
    2. 17.597
    3. 17.565
    4. 17.601
    5. 18.192
    6. 17.792

    平均 17.749 秒、約 5 秒の高速化。約 20% の高速化です。

    高速化 3 – 1 と 2 の組合せ

    単純に高速化 1 と 2 を両方使います。

    rows = list(connect.execute('''
    	SELECT
    		{0}, {1}, {2}, SUM({3}) AS {3}
    	FROM
    		shipping
    	WHERE
    		{2} = ?
    	GROUP BY
    		{0}, {1}, {2};'''.format(
    			pivot_row,
    			pivot_col,
    			pivot_filter,
    			pivot_val),
    			(pivot_filter_value, )
    		).fetchall())
    1. 8.916
    2. 8.853
    3. 8.850
    4. 8.886
    5. 8.807

    平均 8.862 秒、約 13 秒の高速化。60% の高速化です。

    結論

    わかりきったことですが、SQLite は速くて Python は遅いという結果になりました。楽を取るとか速さを取るか、それに尽きます。

    Special Thanks

    実装に当って、れお(@reoreo125)さんの多大なるご協力をいただきました。いつもありがとうございます。

  • OpenVPN でクライアント接続時にスクリプト実行

    OpenVPN でクライアント接続時にスクリプト実行

    /etc/openvpn/server.conf 内に以下の行を追加します。

    script-security 2
    client-connect /path/to/script-con.sh

    script-security 2 は外部スクリプトを実行するために必要です。デフォルトは 1 です。

    0 — Strictly no calling of external programs. 
    1 — (Default) Only call built-in executables such as ifconfig, ip, route, or netsh. 
    2 — Allow calling of built-in executables and user-defined scripts. 
    3 — Allow passwords to be passed to scripts via environmental variables (potentially unsafe).

    OpenVPN man page

    client-connect にはクライアントの接続時に実行するスクリプトを指定します。実行権限を与えるのを忘れないように。 client-disconnect で切断時に実行するスクリプトを指定できます。

    スクリプト内には定義された環境変数を使えます。全ての環境変数は man page の “Environmental Variables” 節に記載されています。

    今回はシェルスクリプト内から更に Python スクリプトを呼び出してメールを送るという二段ロケットな形を採りましたが、問題なく実行されました。簡単な例を記載しておきます。

    #!/bin/sh
    /usr/bin/env python3 /path/to/python-script.py $common_name $trusted_ip
    #!/usr/bin/env python3
    # coding: utf-8
    
    import sys
    
    args = sys.argv
    
    # Send messages to server admins
    ...

    参考

  • reCAPTCHA v3 を Python CGI で使う

    reCAPTCHA v3 を Python CGI で使う

    以前に reCAPTCHA v2 の導入記事を書きましたが、今回は reCAPTCHA v3 です。v2 の画像選択が手間になってきて、よりシンプルに扱える v3 に更新しようと思ったのがきっかけです。

    チェックボックスにチェックする v2 と異なり、v3 はフォームに埋め込むものがありません。JavaScript で token を取得するところまでは一緒なのですが、その token を自分でフォームに埋め込む必要があります。

    reCAPTCHA への登録の仕方は v2 も v3 も同じなので、前回の記事に譲ります。

    クライアントサイド

    head 要素内に次のコードを埋め込みます。

    <script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>

    SITE_KEY の部分は各々発行されたものに置換えてください。

    次に、どこでも良いので次のコードを埋め込みます。head 要素内でも良いですし、body の最後でも問題ありません。

    <script>
    grecaptcha.ready(function() {
      grecaptcha.execute('SITE_KEY', {action: 'action_name'})
      .then(function(token) {
        document.getElementById('g-recaptcha-response').value = token;
      });
    });
    </script>

    ‘action_name’ は名前分けしておくと、admin console で分析が行えるようです。

    最後に、フォーム要素の中に次の隠し要素を埋め込んでおきます。

    <input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">

    先ほどの JavaScript が実行されると、token がこの要素の value に入ります。

    サーバーサイド (Python2)

    今回はお手軽さで cgi.FieldStorage() と urllib を使います。

    import cgi
    import urllib, urllib2
    import json
    
    # フォームの取得
    form = cgi.FieldStorage()
    
    # g-recaptcha-responseの値を取得
    response = form.getfirst('g-recaptcha-response', '')
    
    # APIに照合を行う
    params = urllib.urlencode({'secret': SECRECT_KEY, 'response': response})
    req = urllib2.Request('https://www.google.com/recaptcha/api/siteverify', params)
    result = json.loads(urllib2.urlopen(req).read())

    返ってくる json には次の値が含まれています。

    {u'action': u'action_name', u'score': 0.9, u'hostname': u'kuratsuki.net', u'challenge_ts': u'2018-12-06T06:51:20Z', u'success': True}
    success
    True|False 正しい token が送られたかどうかの判定
    score
    0.0-1.0 bot(0.0) に近いか人間(1.0)に近いかを数値で表します
    action
    string action 名
    challenge_ts
    yyyy-MM-dd’T’HH:mm:ssZZ タイムスタンプ
    hostname
    string reCAPTCHA が行われたホスト名
    error-codes
    [] エラーがあったときのエラーのリスト

    サーバーサイドではこの score を用いて処理を分岐させれば良いようです。

    参考

  • Vultr Cloud Compute(VC2) で swap を設定する

    Vultr Cloud Compute(VC2) で swap を設定する

    Vultr の VPS こと Vultr Cloud Compute(VC2) の標準提供イメージでは、swap の設定がされていません。CPU やディスクの性能的には最安の $3.5/month のプランでも十分事足りますが、メモリが 512MB しかないために簡易なサーバ用途でもギリギリになりがちです。

    top

    これは実際に運用しているサーバで top した様子です。メモリの free が 10MB 程度しかありません。250MB はキャッシュなので実際には不足しているわけではありませんが、余裕はない状態です。Vultr 公式に swap の設定ガイドがあるので、これを参考に swap の設定を行います。

    まずは既に swap が設定されていないかを確認します。”free -m” して swap が 0 であることを確認します。

    $ free -m
    total used free shared buff/cache available
    Mem: 492 228 8 26 255 224
    Swap: 0 0 0

    次に swap 先のファイルを準備します。今回はガイド通りの 2GB の領域を確保しました。

    $ sudo dd if=/dev/zero of=/swapfile count=2048 bs=1M
    2048+0 records in
    2048+0 records out
    2147483648 bytes (2.1 GB, 2.0 GiB) copied, 6.51387 s, 330 MB/s

    作成した /swapfile の権限を、root のみが読み書きできるように変更します。

    $ sudo chmod 600 /swapfile

    mkswap を実行して /swapfile のセットアップを行います。

    $ sudo mkswap /swapfile
    Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
    no label, UUID=0e842c1b-20d4-4da0-853d-78b708da7003

    swapon で swap を有効化します。

    $ sudo swapon /swapfile

    特に何も表示されなければ成功です。”free -m” してメモリ使用量を見てみましょう。

    $ free -m
    total used free shared buff/cache available
    Mem: 492 229 7 26 255 223
    Swap: 2047 0 2047

    すぐには使われないようですが、ちゃんと Swap の total が 0 ではなくなっていますね。

    永続化

    このままだと再起動するたびに swapon しないといけないので、/etc/fstab に書いて起動時に自動で swap を設定するようにします。次の 1 行を追記してください。

    /swapfile none swap sw 0 0

    参考