MENU

pythonでping監視 〜〜その2〜〜

前回、あるNWセグメント内のクライアントにpingを送って常時監視することを試みました。

kittagon.hateblo.jp

その別バージョンです。xargsで並列処理をしています。

# ping_xargs.py

import subprocess, requests, sys

def is_connectable(host):
    # ping = subprocess.Popen(["ping", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping = subprocess.Popen(["ping", "-W", "1", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

ipaddress = sys.argv[2]
num = sys.argv[1]
result = is_connectable(ipaddress)
print(ipaddress)
if result is not True:
    url = "https://my-line-bot-url.com/alert" + num
    requests.get(url)
$ while true; do sed -e 's/,/ /g' IPAdressTable.csv | xargs -L 1 -P20 python ping_xargs.py; done;

特に効率的になったわけではないですが。

しかも今回監視をしたいと思っていたNW設計を聞いたら一番上のL3SWではポート間のICMPを禁止しているらしい。。。
L3SWにぶら下がってるHUB間で全然疎通しないからなんでかなと思っていたら、そもそもpingで監視が出来るNW環境じゃなかった。。

pythonでping監視

はじめに

  • あるプライベートネットワークの中に存在するクライアントにpingを打ち、常時監視するツールが欲しかった。
  • メールでの通知とかも分かりづらいのでいろんな通知の方法が使いたかった(今回はLine Botで通知)。
  • 良さげなツールが無かったので自分でプログラムを書いて実現することにした

PythonPingを打つ

pythonではsubprocessを使えばpingを打てるらしいです。

qiita.com

この関数をお借りしてpingを打ちます。

import subprocess
def is_connectable(host):
    ping = subprocess.Popen(["ping", "-w", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

今回監視したい対象は、あるセグメントの中で固定IPを振られて存在しているクライアントが対象なので、クライアントのIPアドレスcsvファイルから読み込みます。

取得したIPアドレスのリストをforループで回して全てにpingを打ち、NGだった場合は通知を送ります。
(今回はLine Botに通知させたいのでBotサーバのURLにリクエストしています。)

1,192.168.101.1
2,192.168.101.2
3,192.168.101.3
4,192.168.101.4
:
:
import csv, subprocess, requests

def is_connectable(host):
    ping = subprocess.Popen(["ping", "-W", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

while True:
    f = open('IPAdressTable.csv', 'r')
    reader = csv.reader(f)
    for index, row in enumerate(reader):
        print(row)
        result = is_connectable(row[1])
        if result is not True:
            url = "https://my-line-bot-url.com/alert" + row[0]
            requests.get(url)
    f.close()

これで、まずは機能としては完成ですが、同セグメント内に存在する100個近くのクライアントにpingを打っていると、一周するのも時間がかかってしまい、あまり常時監視している感じがしなくなってしまいます。

そこで並列処理を導入して、複数スレッドで処理させようと思います。

joblibで並列処理

qiita.com

pythonの並列処理にはjoblibが便利みたいです。

ここを参考にループ処理の部分を書き換えます。

import csv, subprocess, requests
from joblib import Parallel, delayed

def is_connectable(host):
    ping = subprocess.Popen(["ping", "-W", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

def send_connection_status(host, number):
    print(host)
    result = is_connectable(host)
    if result is not True:
        url = "https://my-line-bot-url.com/alert" + row[0]
        requests.get(url)

while True:
    f = open('IPAdressTable.csv', 'r')
    reader = csv.reader(f)
    header = next(reader)
    Parallel(n_jobs=-1)( [delayed(send_connection_status)(row[1], row[0]) for index, row in enumerate(reader)] )
    f.close()

これで複数スレッド立てて処理が出来るようです。
とりあえず実装しただけで細かいことはまだよくわかりませんが。

時間があるときにもう少し勉強したいと思います。

Flask-AskでAlexaのカスタムスキルを作成

はじめに

昨年末にAmazon Echo Dotを購入したので、カスタムスキルを作ろうと思い、その方法を備忘としてまとめます。

Alexaのカスタムスキルは以前作ったことがあり、そのときに使用したFlask-Askを使用して実装したいと思います。

Flask-Askをインストールする際の注意点はこちらの記事にまとめています。

kittagon.hateblo.jp

環境構築

DockerでPython環境の構築

今回は作ったプログラムと環境をHerokuにデプロイするので、コンテナをそのままデプロイ出来るようDockerで環境を作ります。

まずは簡単に以下のようなDockerfileを作ります。

FROM python:3.7.1-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && apk add git vim

以下コマンドでDockerコンテナを起動します。

$ docker build . -t alexa_env
$ docker run -it -v $PWD:/app -p 8000:8000 alexa_env /bin/ash

今回はalpineのDocker imageを使用しているので、/bin/bashではなくbin/ashであることに注意。

qiita.com

docs.docker.jp

Dockerコンテナに入ったら入ったら、flaskgunicornインストールします。

/app $ pip install flask gunicorn

動作確認のためのapp.pyを作成します。

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

gunicornを起動します。

/app $ gunicorn app:app -b 0.0.0.0:8000

ブラウザからhttp://localhost:8000を開いて"Hello World!"が表示されればOKです。

Herokuにデプロイ

続いてDockerfileを編集してHerokuにデプロイ出来る環境を作っていきます。
事前にコンテナ内でpip freeze > requirements.txtを実行しrequirement.txtを作成しておきます。
また前回の記事にて実施したFlask-Askのパッケージのローカルへのダウンロードと編集したパッケージを/src配下に準備するとともに、build-base,libffi-dev,openssl-devのインストールを追記しておきます。

最終的なDockerfileは以下の通り。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

以前の記事に書いたように、以下コマンドでherokuにデプロイします。

$ heroku login # herokuにログイン
$ heroku container:login # Heroku 上の Container Registry へログイン
$ heroku create alexacustomskil # Heorkuアプリの作成
$ heroku container:push web --app yourappname
$ heroku container:release web --app yourappname

これでHerokuにデプロイされ、デプロイされたURLにアクセスし先ほどと同様"hello World!"が表示されればOKです。

スキル作成

いよいよここからはカスタムスキルを作成していきます。
基本的にはこちらの開発ブログに掲載されているチュートリアルに従って実装していきます。

Flask-Ask: A New Python Framework for Rapid Alexa Skills Kit Development : Alexa Blogs

こちらのチュートリアルでは、数字が3つ読み上げられるのでその数字を覚えて逆から読むというゲームを作っています。

Alexa Developer Console上での設定と、Flask-Askでのサーバ側の処理実装が必要になります。

Amazon Developper Consoleで設定

チュートリアルとは少し画面構成が違いますが、Console上で設定するものは以下のものです。

  • スキルの作成
    • スキルの名前を設定 「アレクサ、〇〇を開いて」のときの名前
    • スキルのインテントを設定 インテントとはユーザーがアレクサに対して応答する言葉のカテゴリのようなもの そのユーザー応答が何を意味するのかを定義します。
      今回登録するのは以下の2つ
      • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
      • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義
    • それぞれのインテントのスロットを設定 インテントにて定義する発話の中で使用する、変数の器のようなもの 今回のAnswerIntentでは発話の中で数字を読み上げるので、固定値で発話パターンを定義出来ないので、変数としてスロットを定義したうえで発話を定義します。

まずはAlexa Developer ConsoleにAmazonアカウントでログインします。
開発者アカウントにするために追加で情報入力が必要な際は入力します。

https://developer.amazon.com/alexa/console/ask

開発者コンソールが開いたらスキルの作成をクリックします。

f:id:ti_taka:20190203184434p:plain

新しいスキルの登録画面で、カスタムスキルを作成します。

f:id:ti_taka:20190203184439p:plain f:id:ti_taka:20190203213541p:plain

必要情報を入力すると、スキル編集画面にきます。

f:id:ti_taka:20190203184452p:plain

まずはインテントを登録します。

f:id:ti_taka:20190203184452p:plain

今回は上で説明したとおり、以下の2つを登録します。

  • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
  • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義

まずはYesIntentを登録します。

f:id:ti_taka:20190203184457p:plain

また、YesIntentのサンプルを登録し、このIntentがどういった発話パターンを取りうるのかAlexaに知ってもらいます。

f:id:ti_taka:20190203184502p:plain

次にAnswerIntentを登録します。

f:id:ti_taka:20190203184509p:plain

AnserIntentについては、発話パターンのサンプルを登録する前に、発話パターンの中で使用されるスロット(変数、器のようなもの)を登録します。

f:id:ti_taka:20190203184515p:plain

今回は3つの数字のスロットを使用しますので、それぞれfirst、second、thirdの3個を登録します。 スロットのタイプはAMAZON.numberとします。

f:id:ti_taka:20190203184522p:plain

次にこのスロットを使用してAnswerIntentの発話パターンを登録します。 今回は以下の2パターンを登録しました。

f:id:ti_taka:20190203184526p:plain

次に、このスキルを呼び出した際にそれを処理するサーバーのURLを設定します。 先程デプロイしたherokuのURLを設定します。

f:id:ti_taka:20190203184530p:plain

最後に今回のスキルを呼び出すためのスキル名を設定します。 設定を保存し、ビルドします。

f:id:ti_taka:20190203184536p:plain

これで開発コンソールでの設定は完了です。

左メニューのJSONエディター上は以下のような表示になります。
このJSONをそのまま編集してもOKです。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "メモリーゲーム",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "YesIntent",
                    "slots": [],
                    "samples": [
                        "うん",
                        "はい",
                        "もちろん",
                        "OKです",
                        "OK",
                        "了解"
                    ]
                },
                {
                    "name": "AnswerIntent",
                    "slots": [
                        {
                            "name": "first",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "second",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "third",
                            "type": "AMAZON.NUMBER"
                        }
                    ],
                    "samples": [
                        "{first} と {second} と {third}",
                        "{first} {second} {third}"
                    ]
                }
            ],
            "types": []
        }
    }
}

Flask-ASKで実装

サーバ側の処理はFlask-Askで実装していきます。

Welcome to Flask-Ask — Flask-Ask documentation

チュートリアルの通り、app.pyを以下のように書き換えます。

import logging
from random import randint
from flask import Flask, render_template
from flask_ask import Ask, statement, question, session

app = Flask(__name__)
ask = Ask(app, "/")
logging.getLogger("flask_ask").setLevel(logging.DEBUG)

@ask.launch
``def new_game():
    welcome_msg = render_template('welcome')
    return question(welcome_msg)

@ask.intent("YesIntent")
def next_round():
    numbers = [randint(0, 9) for _ in range(3)]
    round_msg = render_template('round', numbers=numbers)
    session.attributes['numbers'] = numbers[::-1]  # reverse
    return question(round_msg)

@ask.intent("AnswerIntent", convert={'first': int, 'second': int, 'third': int})
def answer(first, second, third):
    winning_numbers = session.attributes['numbers']
    if [first, second, third] == winning_numbers:
        msg = render_template('win')
    else:
        msg = render_template('lose')
    return statement(msg)

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

templateファイルの中にはAlexaが話すセリフを記載します。

welcome: メモリーゲームへようこそ! これから5つの数字を読み上げるので、アタナはその数字を逆の順番で答えてください。準備はいいですか?

round: 今回の数字は {{ numbers|join(", ") }} です。反対の順番で読み上げてください。

win: 正解です!

lose: 不正解です。

サーバー側の処理はこれでOKなので、再度Herokuにデプロイします。

そして開発用アカウントに紐づけたEcho端末や、アレクサシミュレーターで「アレクサ、メモリーゲームを開いて」と話しかけてゲームがスタートすれば成功です。

が、返事が返ってきません。

Herokuのログを確認すると以下のようなエラーが出ていました。

2019-01-24T15:02:16.836079+00:00 app[web.1]: File "/usr/local/lib/python3.7/site-packages/OpenSSL/crypto.py", line 740, in _subjectAltNameString
2019-01-24T15:02:16.836081+00:00 app[web.1]: method = _lib.X509V3_EXT_get(self._extension)
2019-01-24T15:02:16.836082+00:00 app[web.1]: AttributeError: module 'lib' has no attribute 'X509V3_EXT_get'

調べてみるとpyOpenSSLを再インストールする必要があるようです。

electricsheep.hatenadiary.jp

記事を参考にDockerfileを以下のように修正します。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz && \
    pip uninstall --yes pyOpenSSL && \
    yes | pip install pyOpenSSL
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

再度Herokuにデプロイすると無事動作するようになりました。

まとめ

前回の続きで、Flask-Askを使用してAlexaのカスタムスキルを作成してみました。
Flaskを使って簡単に実装できるので、アイデア次第でいろんなスキルが作れそうです。

Dockerのalpine上 Flask-Askをpipでインストールしようとするとエラー

はじめに

年末にEcho dotを購入したので、Alexaのカスタムスキルを作ろうと思いました。

Alexaのカスタムスキルは以前作成したことがあったのでその際に使用したFlask_Askを使って作成しようと思い取り掛かりました。

ところがFlask_Askはしばらくメンテナンスされていないようで、pipでインストールする際にエラーが出てしまったのでその対処を備忘としてまとめます。

pip installでエラー

今回はDocker上に環境を作っており、Python3.7.2-alpine3.8のDockerimageを使用しました。

Docker Hub

早速開発しようとFlask_Askをpipでインストールしようとしたらエラーになりました。

$ pip install flask_ask
Collecting flask_ask
  Using cached https://files.pythonhosted.org/packages/6a/f5/d4709ae94584a0b1541e9b52b2d25a8a1bdb6e2da9d6870f23fdd0523a30/Flask-Ask-0.9.8.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-install-la1z83pq/flask-ask/setup.py", line 8, in <module>
        from pip.req import parse_requirements
    ModuleNotFoundError: No module named 'pip.req'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-la1z83pq/flask-ask/

'pip.req'が無いと言われています。
いろいろ調べてみると既知のエラーらしく、コミュニティでも話題になっていました。
どうやらsetup.pyを編集すれば解消するようです。

stackoverflow.com

pip downloadでダウンロードしてからオフラインでインストール

setup.pyを編集するためには、まずはソースをダウンロードしなくてはなりません。

qiita.com

こちらを参考に以下の手順で実施しました。

まずはsrcディレクトリを作成し、そこにパッケージをダウンロードします。

$ mkdir src
$ pip download -d src --no-binary :all: flask_ask
Collecting flask_ask
  Downloading https://files.pythonhosted.org/packages/6a/f5/d4709ae94584a0b1541e9b52b2d25a8a1bdb6e2da9d6870f23fdd0523a30/Flask-Ask-0.9.8.tar.gz (40kB)
    100% |████████████████████████████████| 40kB 2.8MB/s
  Saved ./src/Flask-Ask-0.9.8.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-download-on3tlt2h/flask-ask/setup.py", line 8, in <module>
        from pip.req import parse_requirements
    ModuleNotFoundError: No module named 'pip.req'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-download-on3tlt2h/flask-ask/

$ ls -l src/
total 40
-rw-r--r--    1 root     root         40142 Jan  8 07:03 Flask-Ask-0.9.8.tar.gz

何やらまたsetup.pyでエラーが出ていますが、ちゃんとダウンロード出来ているようです。

setup.pyの編集

ダウンロードしたtar.gzファイルを解凍し、setup.pyを編集します。

$ cd src/
$ tar -zxvf Flask-Ask-0.9.8.tar.gz
$ ls -l
total 40
drwxr-xr-x   15 501      wheel          480 Jan  8 07:05 Flask-Ask-0.9.8
-rw-r--r--    1 root     root         40142 Jan  8 07:03 Flask-Ask-0.9.8.tar.gz
$ cd Flask-Ask-0.9.8
$ vim setup.py

setup.pyを以下のように編集します。

"""
Flask-Ask
-------------

Easy Alexa Skills Kit integration for Flask
"""
from setuptools import setup
# from pip.req import parse_requirements
try: # for pip >= 10
    from pip._internal.req import parse_requirements
except ImportError: # for pip <= 9.0.3
    from pip.req import parse_requirements

setup(
    name='Flask-Ask',
    version='0.9.8',
    url='https://github.com/johnwheeler/flask-ask',
    license='Apache 2.0',
    author='John Wheeler',
    author_email='john@johnwheeler.org',
    description='Rapid Alexa Skills Kit Development for Amazon Echo Devices in Python',
    long_description=__doc__,
    packages=['flask_ask'],
    zip_safe=False,
    include_package_data=True,
    platforms='any',
    install_requires=[
        str(item.req) for item in
        parse_requirements('requirements.txt', session=False)
    ],
    test_requires=[
        'mock',
        'requests'
    ],
    test_suite='tests',
    classifiers=[
        'License :: OSI Approved :: Apache Software License',
        'Framework :: Flask',
        'Programming Language :: Python',
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'Operating System :: OS Independent',
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
        'Topic :: Software Development :: Libraries :: Python Modules'
    ]
)

setup.pyを編集したパッケージフォルダを再度tarで固めます。

$ mv Flask-Ask-0.9.8.tar.gz Flask-Ask-0.9.8_bk.tar.gz
$ tar -zcvf Flask-Ask-0.9.8.tar.gz Flask-Ask-0.9.8
$ ls -l
total 80
drwxr-xr-x   15 501      wheel          480 Jan  8 07:06 Flask-Ask-0.9.8
-rw-r--r--    1 root     root         40284 Jan  8 07:08 Flask-Ask-0.9.8.tar.gz
-rw-r--r--    1 root     root         40142 Jan  8 07:03 Flask-Ask-0.9.8_bk.tar.gz

alpineにgccのインストール

pipでインストールを実行したところ、何やら別のエラーが出ました。

$ pip install Flask-Ask-0.9.8.tar.gz
Processing ./Flask-Ask-0.9.8.tar.gz
Collecting aniso8601==1.2.0 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/5b/fb/251a0dd2f4710e60664ddd8bd3485bd8362530f47af9e88f4061fe589ebf/aniso8601-1.2.0.tar.gz (59kB)
    100% |████████████████████████████████| 61kB 2.2MB/s
Collecting Flask==0.12.1 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/f4/43/fb2d5fb1d10e1d0402dd57836cf9a78b7f69c8b5f76a04b6e6113d0d7c5a/Flask-0.12.1-py2.py3-none-any.whl (82kB)
    100% |████████████████████████████████| 92kB 5.1MB/s
Collecting pyOpenSSL==17.0.0 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/8c/b7/0048adbf09ad93f571e70fb3100e696d4711cdcbfe38161690d716a5ed35/pyOpenSSL-17.0.0-py2.py3-none-any.whl (51kB)
    100% |████████████████████████████████| 61kB 3.1MB/s
Collecting PyYAML==3.12 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/4a/85/db5a2df477072b2902b0eb892feb37d88ac635d36245a72a6a69b23b383a/PyYAML-3.12.tar.gz (253kB)
    100% |████████████████████████████████| 256kB 7.0MB/s
Collecting six==1.11.0 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/67/4b/141a581104b1f6397bfa78ac9d43d8ad29a7ca43ea90a2d863fe3056e86a/six-1.11.0-py2.py3-none-any.whl
Collecting python-dateutil (from aniso8601==1.2.0->Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/74/68/d87d9b36af36f44254a8d512cbfc48369103a3b9e474be9bdfe536abfc45/python_dateutil-2.7.5-py2.py3-none-any.whl (225kB)
    100% |████████████████████████████████| 235kB 5.4MB/s
Requirement already satisfied: click>=2.0 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (7.0)
Requirement already satisfied: Jinja2>=2.4 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (2.10)
Requirement already satisfied: itsdangerous>=0.21 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (1.1.0)
Requirement already satisfied: Werkzeug>=0.7 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (0.14.1)
Collecting cryptography>=1.7 (from pyOpenSSL==17.0.0->Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/f3/39/d3904df7c56f8654691c4ae1bdb270c1c9220d6da79bd3b1fbad91afd0e1/cryptography-2.4.2.tar.gz (468kB)
    100% |████████████████████████████████| 471kB 6.3MB/s
  Installing build dependencies ... error
  Complete output from command /usr/local/bin/python -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-x0_m8e40 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple -- setuptools>=18.5 wheel "cffi>=1.7,!=1.11.3; python_implementation != 'PyPy'":
  Collecting setuptools>=18.5
    Downloading https://files.pythonhosted.org/packages/37/06/754589caf971b0d2d48f151c2586f62902d93dc908e2fd9b9b9f6aa3c9dd/setuptools-40.6.3-py2.py3-none-any.whl (573kB)
  Collecting wheel
    Downloading https://files.pythonhosted.org/packages/ff/47/1dfa4795e24fd6f93d5d58602dd716c3f101cfd5a77cd9acbe519b44a0a9/wheel-0.32.3-py2.py3-none-any.whl
  Collecting cffi!=1.11.3,>=1.7
    Downloading https://files.pythonhosted.org/packages/e7/a7/4cd50e57cc6f436f1cc3a7e8fa700ff9b8b4d471620629074913e3735fb2/cffi-1.11.5.tar.gz (438kB)
      Complete output from command python setup.py egg_info:

          No working compiler found, or bogus compiler options passed to
          the compiler from Python's standard "distutils" module.  See
          the error messages above.  Likely, the problem is not related
          to CFFI but generic to the setup.py of any Python package that
          tries to compile C code.  (Hints: on OS/X 10.8, for errors about
          -mno-fused-madd see http://stackoverflow.com/questions/22313407/
          Otherwise, see https://wiki.python.org/moin/CompLangPython or
          the IRC channel #python on irc.freenode.net.)

      ----------------------------------------
  Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-zydf2ff8/cffi/

  ----------------------------------------
Command "/usr/local/bin/python -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-x0_m8e40 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple -- setuptools>=18.5 wheel "cffi>=1.7,!=1.11.3; python_implementation != 'PyPy'"" failed with error code 1 in None

コンパイラが見つからないよーというエラーが出ています。
調べると対処方法が見つかりました。

ja.stackoverflow.com

alpineにgccが含まれていない事が原因のようです。

早速alpine上でパッケージをインストールしてから再試行しますと、また別のエラーが、、、

$ apk add build-base libffi-dev
$ pip install Flask-Ask-0.9.8.tar.gz

〜〜〜〜略〜〜〜〜
  build/temp.linux-x86_64-3.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory
   #include <openssl/opensslv.h>
                                ^
  compilation terminated.
  error: command 'gcc' failed with exit status 1

  ----------------------------------------
  Failed building wheel for cryptography
〜〜〜〜略〜〜〜〜
    build/temp.linux-x86_64-3.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory
     #include <openssl/opensslv.h>
                                  ^
    compilation terminated.
    error: command 'gcc' failed with exit status 1

    ----------------------------------------
Command "/usr/local/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-z1_hjgoi/cryptography/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-rfu9q4ne/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-install-z1_hjgoi/cryptography/

今度はopensslが無い!と怒られてしまっています。インストールしてから再度pipでインストール実行します。

$ apk add openssl-dev
$ pip install Flask-Ask-0.9.8.tar.gz
〜〜〜〜略〜〜〜〜
Successfully built Flask-Ask cryptography
Installing collected packages: cryptography, pyOpenSSL, PyYAML, Flask-Ask
Successfully installed Flask-Ask-0.9.8 PyYAML-3.12 cryptography-2.4.2 pyOpenSSL-17.0.0

無事インストール出来ました。

$ python
Python 3.7.1 (default, Dec 21 2018, 03:21:42)
[GCC 6.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from flask_ask import Ask, statement, question, session
>>>
>>>

ちゃんとimportも出来るようになりました。
これでFlask_Askを使用できるようになったので、これからカスタムスキルを開発していきたいと思います。

locustでLINEメッセージ受信サーバに負荷試験

はじめに

業務でLINE Messeging APIを使用しているのですが、先日突然メッセージ受信サーバ(LINEからのWebhookを受け付けるサーバ)に対して、負荷試験を実施してほしいと先輩に依頼されました。
負荷試験ツールではjMeterが有名ですがノウハウがなく、もっと簡単に実施できるものが無いか調べたところ、 locustというPythonで記述できるツールがあることを知りました。
その内容を、自分用メモレベルですが備忘としてまとめます。

locustのインストール

公式サイト

Locust - A modern load testing framework

LocustはPython3.6までにしか対応していないので、Python3.6系の環境をあらかじめpyenvなどで準備しておきます。

Locust supports Python 2.7, 3.4, 3.5, and 3.6.

Macにてインストールする場合、libevが必要なので先にインストールしておきます。

$ brew install libev
$ pip install locustio

基本的な使い方は上記公式サイトや以下サイトを参照しました。

シナリオ作成

LINEから送られるWebhookを模してJSONのhttpヘッダーにx-line-signatureパラメータを追加して送っています。
リクエスト先はLine developerで設定しているcallbackURLのパスを指定しています。(今回は/callback
メッセージIDは、受け付けるサーバ側(今回負荷をかけるサーバ)でユニークなものでないと処理できない仕様になっていたようなので、タイムスタンプを設定して送っています。
Messaging API - LINE Developers

from locust import HttpLocust, TaskSet, task
import base64
import hashlib
import hmac
import datetime
import json

channel_secret = '  channnel secret   '

class UserBehavior(TaskSet):
    @task(1)
    def index(self):
        payload = {
                    "events": 
                    [
                        {
                            "replyToken": "0f377",
                            "type": "message",
                            "timestamp": 1462629479859,
                            "source": {
                                "type": "user",
                                "userId": "Ue636d0*****************"
                            },
                            "message": {
                                "id": str(int(datetime.datetime.now().timestamp()*1000000)),
                                "type": "text",
                                "text": "性能試験実施中"
                            }
                        }
                    ],
                    "destination":"U6415f13f***********"
                }
        hash = hmac.new(channel_secret.encode('utf-8'),json.dumps(payload).encode('utf-8'), hashlib.sha256).digest()
        signature = base64.b64encode(hash)
        headers = {'content-type': 'application/json', 'x-line-signature': signature}
        r = self.client.post("/callback", data=json.dumps(payload), headers=headers, catch_response=True)
        print(r)

class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    min_wait = 1000
    max_wait = 1000

以下コマンドで起動します。

$ locust -H http://"負荷をかけるサーバURL"

画面が起動して

  • Number of users to simulate:
    何クライアント作成するか
  • Hatch rate:
    クライアントの作成スピード(毎秒)

を入力するとリクエストが生成され、負荷試験できました!

Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その3〜〜ログイン認証追加〜〜

はじめに

今回は、前回作成したフロントのUIがVue.js、バックエンドのAPIFlaskのWebアプリケーションに対してFlask-Loginを使用してログイン機能を作成します。
Flask-Login

↓前回
kittagon.hateblo.jp

↓前々回 kittagon.hateblo.jp

まずはFlask-Loginとログインフォームの作成に使用するFlask-WTFを以下コマンドでインストールします。

$ pip install flask-login flask_wtf

ユーザーモデルの追加

まずはログインユーザーとなるユーザーモデルをmodels.pyに記述します。
公式サイトにも以下のように記載されており、UserクラスはUserMixinを継承する形で記述すると簡単なようです。

To make implementing a user class easier, you can inherit from UserMixin, which provides default implementations for all of these properties and methods. (It’s not required, though.)

また、ログインフォームはとあえず簡単にflask_wtfを使いLoginFormクラスを作成しておきます。

# backend/models.py


from flask_login import UserMixin
from flask_wtf import FlaskForm
from wtforms import TextField, PasswordField, validators
from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from backend import db

class LoginForm(FlaskForm):
    email = TextField('email', validators=[validators.Required()])
    password = PasswordField('Password', validators=[validators.Required()])
    def __str__(self):
        return '''
<form action="/-/login" method="POST">
<p>%s: %s</p>
<p>%s: %s</p>
%s
<p><input type="submit" name="submit" /></p>
</form>
''' % (self.email.label, self.email,
       self.password.label, self.password,
       self.csrf_token)

class User(UserMixin, db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True, nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False)
    _password = db.Column('password', db.String(255), nullable=False)
    active = db.Column(db.Boolean(), default=True)

    def _get_password(self):
        return self._password
    def _set_password(self, password):
        if password:
            password = password.strip()
        self._password = generate_password_hash(password)
    password_descriptor = property(_get_password, _set_password)
    password = synonym('_password', descriptor=password_descriptor)

    # フォームで送信されたパスワードの検証
    def check_password(self, password):
        password = password.strip()
        if not password:
            return False
        return check_password_hash(self.password, password)

    # 認証処理
    @classmethod
    def auth(cls, query, email, password):
        user = query(cls).filter(cls.email==email).first()
        if user is None:
            return None, False
        return user, user.check_password(password)

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)

    def to_dict(self):
        return dict(
            id=self.id,
            title=self.title,
            text=self.text
        )

    def __repr__(self):
        return '<Entry id={id} title={title!r}>'.format(
            id=self.id, title=self.title)

def init():
    db.create_all()

ログイン処理の追加

次にFlask側の処理の中に実際のログイン処理を記述していきます。
__init__.pyFlask-Loginの初期化
view.pyでログイン画面のルーティングとログイン処理を実装します。

このあたりを参考。
Flask-Loginの使い方 - Qiita
Python Flaskでつくる LDAPログインページ - AAA Blog
Flaskで認証 - Kensuke Kousaka's Blog

config.py

# backend/config.py

import os
class BaseConfig(object):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///backend.db'
    # cookieを暗号化する秘密鍵
    SECRET_KEY = os.urandom(24)
    # csrf token用秘密鍵
    WTF_CSRF_SECRET_KEY = os.urandom(24)

init.py

Flask-loginを使うとcurrent_userlogin_userlogout_userという変数が払い出されて払い出されていい感じにログイン状態の確認ができます。
また、後述するview.pyに記載するルーティングを-というプレフィックスをつけて取り込んでいます。

# backend/__init__.py

from flask import Flask, request, redirect, url_for
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user, login_user, logout_user, login_required # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app)

######ここから追記######

login_manager = LoginManager()
login_manager.init_app(app)

from backend.models import User

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)

# 未認証の際のリダイレクト先を設定
@login_manager.unauthorized_handler
def unauthorized_callback():
    return redirect(url_for('app.login'))

############# route ##############

from backend.view import view
app.register_blueprint(view, url_prefix="/-")

######ここまで追記######

from backend.api import api
app.register_blueprint(api, url_prefix="/api")
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
@login_required # ここを追記
def catch_all(path):
    if app.debug:
        return requests.get('http://localhost:8080/{}'.format(path)).text
    return render_template("index.html")

view.py

from flask import Blueprint, redirect, request
from flask_login import login_user, logout_user

from backend import db
from backend.models import User, LoginForm

view = Blueprint('app', __name__,
                    template_folder='templates',
                    static_folder='templates/static')

@view.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user, authenticated = User.auth(db.session.query, form.email.data, form.password.data)
        if authenticated:
            login_user(user, remember=True)
            print('Login successfully.')
            return redirect('/tasks')
        else:
            print('Login failed.')
    return str(form)

@view.route('/logout', methods=['POST'])
def logout():
    logout_user()
    return redirect('/login')

ログアウトボタンの追加

ログインした後のタスク画面にログアウトボタンを追加します。
以下のボタンをTasks.vueの中に記述します。
場所はどこでも結構です。

// frontend/src/component/Tasks.vue

<b-form v-if="show" action="/-/logout" method="post" >
  <b-button type="submit">ログアウト</b-button>
</b-form>

ログインユーザーの作成

まだログインユーザーを作成する画面を作っていないので、
Pythonインタラクティブモードからユーザーを作成し、動作確認をします。

$ python
>>> from backend.models import User, db
>>> User.query.all()
[]
>>> user = User(name='test_user', email='test@example.com')
>>> user._set_password('password')
>>> user
<User (transient 4334868296)>
>>> db.session.add(entry)
>>> db.session.commit()
>>> User.query.all()
[<User 1>]

http://localhost:5000/-/loginを開いてログイン画面が表示され、
'test@example.com'と'password'でログインできればOKです。
また、ログインしていない状態でhttp://localhost:5000/tasksを表示しようとすると
ログイン画面にリダイレクトされます。
ログアウトボタンでログアウトできます。

ログイン画面修正

最初に作成したのは最低限の要素のみ記述したので、もう少しきれいなレイアウトに変更します。
現状は↓のようにフォームを文字列で定義しておき、それを表示しているだけだったので、
画面をテンプレートから呼び出せるよう変更します。

    def __str__(self):
        return '''
<form action="/-/login" method="POST">
<p>%s: %s</p>
<p>%s: %s</p>
%s
<p><input type="submit" name="submit" /></p>
</form>
''' % (self.email.label, self.email,
       self.password.label, self.password,
       self.csrf_token)

テンプレートの作成には、Vue.js側で作成している画面と揃えるために、Flask-Bootstrapを使用します。

Flask-Bootstrap

まずは以下コマンドでインストールします。

$ pip install flask-bootstrap

__init__.pyでflask-bootstrapの初期化をします。

# backend/__init__.py

from flask import Flask, request, redirect, url_for
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user, login_user, logout_user
from flask_bootstrap import Bootstrap # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app)

bootstrap = Bootstrap(app) # ここを追記

#############以下略#############

view.pyのログイン画面表示を修正します。
テンプレートの呼び出しに加えて、actionをFlaskテンプレートの変数としてurl_forを渡すように修正しています。

# backend/view.py

from flask import Blueprint, redirect, request, render_template, url_for # render_template, url_forを追加

#############略#############

@view.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user, authenticated = User.auth(db.session.query, form.email.data, form.password.data)
        if authenticated:
            login_user(user, remember=True)
            print('Login successfully.')
            return redirect('/tasks')
        else:
            print('Login failed.')
    # return str(form)
    ## ここを修正
    return render_template('login.html', form=form, action=url_for('app.login'))
#############略#############

models.pyからstr要素を削除します。
また、Loginform内に新たにsubmit要素を定義します。

# backend/models.py

from flask_login import UserMixin
from flask_wtf import FlaskForm
from wtforms import TextField, PasswordField, validators, SubmitField # SubmitFieldの追加
from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from backend import db

class LoginForm(FlaskForm):
    email = TextField('email', validators=[validators.Required()])
    password = PasswordField('Password', validators=[validators.Required()])
    submit = SubmitField('ログイン') # submit追加
    # def __str__(self):の削除

次にログインテンプレートを作成します。
テンプレートフォルダはview.pyにて以下のように定義されています。

view = Blueprint('app', __name__,
                    template_folder='templates',
                    static_folder='templates/static')

view.pyと同階層にtemplatesフォルダを作成します。

app_dir
 ┣manage.py
 ┣frontend/
 ┗backend/
   ┣__init__.py
   ┣api.py
   ┣view.py
   ┣models.py
   ┣config.py
   ┗templates/

templatesフォルダ内にログイン画面用テンプレートを作成します。

<!-- backend/templates/login.html-->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "bootstrap/base.html" %}
{% block title %}This is a login page{% endblock %}
{% block content %}
  <div class="container">
    {{ wtf.quick_form(form, action=action, method="post") }}
  </div>
{% endblock %}

またまた画面デザインは適当ですが、Bootstrapにより良い感じのフォームができました。

まとめ

これで認証までも含めたWebアプリができました。
基本的な機能はほぼ実装することができたので、今後はこれをベースにいろんな機能を実装していきたいと思います。

参考サイト

vue-cliのnpm run devでwebpack-dev-serverのエラー

はじめに

vue-cliを使ってVue.jsの開発をしていたら、あるときからnpm run dev実行時に↓のようなエラーが出てしまい開発用サーバが立ち上がらなくなりました。
(なんの操作をしていたときか忘れてしまいました。。)

$ npm run dev
> frontend@1.0.0 dev /Users/hogehoge/Develop/practice-flask-vue/frontend
> webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

The CLI moved into a separate package: webpack-cli
Please install 'webpack-cli' in addition to webpack itself to use the CLI
-> When using npm: npm i -D webpack-cli
-> When using yarn: yarn add -D webpack-cli
internal/modules/cjs/loader.js:583
    throw err;
    ^

Error: Cannot find module 'webpack-cli/bin/config-yargs'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
    at Function.Module._load (internal/modules/cjs/loader.js:507:25)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:20:18)
    at Object.<anonymous> (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:84:1)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! frontend@1.0.0 dev: `webpack-dev-server --inline --progress --config build/webpack.dev.conf.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the frontend@1.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/hogehoge/.npm/_logs/2018-11-19T11_12_55_011Z-debug.log

webpack-cliのインストール

しばらくパッケージをアップデートしていないので、なぜ今のタイミングで

The CLI moved into a separate package: webpack-cli
Please install 'webpack-cli' in addition to webpack itself to use the CLI
-> When using npm: npm i -D webpack-cli
-> When using yarn: yarn add -D webpack-cli

が出るのか不明ですが、ひとまず指示どおりにインストールします。

$ npm i -D webpack-cli
npm WARN url-loader@1.1.0 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN webpack-dev-middleware@3.4.0 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN webpack-dev-server@3.1.10 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN webpack-cli@3.1.2 requires a peer of webpack@^4.x.x but none is installed. You must install peer dependencies yourself.

+ webpack-cli@3.1.2
added 25 packages from 7 contributors and audited 34200 packages in 37.642s
found 0 vulnerabilities

インストールが完了しました。
webpack@^4.0.0をインストールせよと言われてしまいます。

$ npm run dev

> frontend@1.0.0 dev /Users/hogehoge/Develop/practice-flask-vue/frontend
> webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

TypeError: Cannot destructure property `compile` of 'undefined' or 'null'.
    at addHooks (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/lib/Server.js:114:49)
    at new Server (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/lib/Server.js:127:5)
    at startDevServer (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:355:14)
    at processOptions (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:309:5)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at startup (internal/bootstrap/node.js:266:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

やっぱりまだだめみたいです。

webpack-dev-serverのダウングレード

TypeError: Cannot destructure property `compile` of 'undefined' or 'null'.

そこでこちらのエラーについて更にいろいろと調べてみると、同じようなissueがありました。

TypeError: Cannot read property 'compile' of undefined · Issue #1334 · webpack/webpack-dev-server · GitHub

1 - Upgrade webpack to v4, you must change the webpack.config.js file for compatibility. or
2 - Downgrade webpack-dev-server to exactly v3.0.0 as @Nufeen said.

やはり上で突っ込まれているように、v4以上にアップグレードするか、v3にダウングレードすれば直るようです。
webpackの知識はほぼ無く、webpack.config.jsを編集するのもハードルが高そうだったので今回はダウングレードして対応することとしました。

$ npm install -D webpack-dev-server@3.0.0
npm WARN webpack-cli@3.1.2 requires a peer of webpack@^4.x.x but none is installed. You must install peer dependencies yourself.
npm WARN webpack-dev-server@3.0.0 requires a peer of webpack@^4.0.0-beta.1 but none is installed. You must install peer dependencies yourself.

+ webpack-dev-server@3.0.0
added 37 packages from 16 contributors, removed 28 packages, updated 10 packages and audited 35284 packages in 30.206s
found 1 high severity vulnerability
  run `npm audit fix` to fix them, or `npm audit` for details

インストール出来たので再度試してみます。

$ npm un dev


 DONE  Compiled successfully in 26990ms                                                    8:59:34 PM

 I  Your application is running here: http://localhost:8080

無事開発用サーバが起動しました。