search
LoginSignup
3

More than 1 year has passed since last update.

posted at

updated at

Organization

Wake on LAN用のPythonスクリプトを作成する(NAT超えWake on LAN[5])

はじめに

前回の続きです.
とりあえずやりたいことはこんなんです.

network_detail.png

今回は遂に「Wake on LAN用のPythonスクリプトを作成する」です.

DesktopPCの設定

前提:Ubuntu18.04です.
まずはWake on LANが実行できるように設定します.

固定IPの割り当て

sudo vi /etc/netplan/~~.yaml

以下を記入します,

/etc/netplan/~~.yaml
# Let NetworkManager manage all devices on this system
network:
  version: 2
  renderer: NetworkManager

  ethernets:
    eno1:
       dhcp4: no
       wakeonlan: true # enable wake on lan
       addresses: [192.168.1.**/24] # assign arbitrary address
       gateway4: 192.168.1.1  # your router(raspi)'s address
       nameservers:
         addresses: [8.8.8.8,8.8.4.4] # google's public dns

再起動します.

sudo netplan apply
sudo reboot

wake on lan が有効になっているか確認するためにethtoolをインストールします.

sudo apt-get install -y ethtool

Ethernetの名前をメモします.ついでにIPが固定されているか確認します.

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eno1<<<<copy<<<<: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.**/24 brd 192.168.1.255 scope global noprefixroute eno1
       valid_lft forever preferred_lft forever
    inet6 fe80::2d8:61ff:fe56:242d/64 scope link
       valid_lft forever preferred_lft forever

Wake on LANが有効になっているか確認します.Wake-on: gになっていればOKです.

$ sudo ethtool eno1
~~
Supports Wake-on: pumbg
Wake-on: g # if it's d, your wake on lan setting may be wrong
~~

BIOSの設定

省略します.起動される側のPCのWOL設定(1/3):BIOS/UEFIの設定wo
参考にしてください.

確認

ラズパイでip aを確認してDesktopに繋いでいるEthernetにipが割り当てられていればOKです.(UP,LOWER_UPになっている必要があります.ありがとうtelcomM

$ ip a
4: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global noprefixroute eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::83fa:6dee:9799:9a6e/64 scope link 
       valid_lft forever preferred_lft forever

確認(wakeonlanをインストールし起動できるか確認)

ラズパイにwakeonlanをインストールします.

sudo apt install wakeonlan
wakeonlan -i 192.168.1.255 -p 7 **:**:**:**:**:**

これで起動できればOKです.

mod_wsgi

まず,PythonスクリプトをApacheで動かすことができるmod_wsgiと諸々をインストールします.

sudo apt install python3-dev python3-pip apache2 libapache2-mod-wsgi-py3
sudo apt-get -y install python3 ipython3 python3-flask curl

Pythonスクリプト作成

とりあえず動くPythonスクリプトを作成します.今回はFlaskを用います.

cd ~
mkdir flask && cd flask
vi app.py
~/flask/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
    return 'This is vpn server for wake on lan!\n\n'
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

とりあえず,mod_wsgiが動くか確認します.

$ python3 app.py
$ curl http://localhost:5000 # 別タブで
This is vpn server for wake on lan!

”This is vpn server for wake on lan!”と出ればOKです.

Apacheとmod_wsgiの設定

新たにflask用の設定ファイルを作成します.

sudo vi /etc/apache2/sites-available/flask.conf

今回はGCPから80ポートにリダイレクトされるようにしているので以下のように記入します.

/etc/apache2/sites-available/flask.conf
<VirtualHost *:80>
    ServerName kado.example.com
    WSGIDaemonProcess flask user={username} group={username} threads=5
    WSGIScriptAlias / /home/{username}/flask/adapter.wsgi
    <Directory /home/{username}/flask/>
        WSGIProcessGroup flask
        WSGIApplicationGroup %{GLOBAL}
        WSGIScriptReloading On
        Require all granted
    </Directory>
</VirtualHost>

flask.confを有効化します.

sudo a2dissite 000-default.conf
sudo a2ensite flask.conf
sudo service apache2 restart

adapter.wsgiファイルを作成します.

vi ~/flask/adapter.wsgi
~/flask/adapter.wsgi
import sys
if sys.version_info[0]<3:       # require python3
    raise Exception("Python3 required! Current (wrong) version: '%s'" % sys.version_info)
sys.path.insert(0, '/home/kadorpi/flask/')
from app import app as application # <- app means app of app.py

Apacheを再起動します.

sudo service apache2 restart

動いているか確認します.

$ curl localhost
This is vpn server for wake on lan!

これで,PythonスクリプトがApache上で動くようになりました.

Line bot用のWake on LANスクリプト作成

ここまできたら後少しです.LineからGCPにHTTPリクエストを送り,受け取ったリクエストをラズパイの80ポートにリダイレクトします.そして,ラズパイで受け取ったリクエストをPythonで処理します.

まず,Line Developerにアカウントを登録します.
次にChannelを作成します.

1.png

2.png

3.png

Channel Secretを発行し,メモします.

4.png

Channel Access Tokenを発行し,メモします.

5.png

>Response settingsからAuto-replyなどを適宜設定します.

6.png

>Message APIからWebhookURLを入力します.今回はhttps://{domain name}/wake-on-lanとします.

7.png

これでLINE Botの設定は終わりです.

mod_wsgiの再設定

環境変数として,先ほどメモしたChannel SecretとChannel Access Tokenを設定します.

vi ~/flask/adapter.wsgi

以下を追記します.

~/flask/adapter.wsgi
import os
os.environ['LINE_CHANNEL_SECRET'] = 'fuga'
os.environ['LINE_CHANNEL_ACCESS_TOKEN'] = 'hogehoge'

Pythonスクリプト

必要なモジュールをインストールします.

pip3 install line-bot-sdk flask

pythonコードは以下のようにします.

import os, sys
from flask import Flask, request, abort
app = Flask(__name__)

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    TextMessage, TextSendMessage, MessageEvent
)

# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)

@app.route('/')
def show_web():
    return 'This is vpn server for wake on lan!\n\n'

@app.route('/wake-on-lan', methods=['POST'])
def wake_on_lan():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'ok'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message = event.message.text

    replies = []
    if re.compile("\s*(check)\s*", re.IGNORECASE).search(message):
        result = confirm()
        replies += [TextSendMessage(result)]

    elif re.compile("\s*(kick)\s*", re.IGNORECASE).search(message):
        if confirm() == 'Awake':
            replies += [TextSendMessage('Already awake')]
        else:
            result = kick()
            replies += [TextSendMessage(result)]

    buttons_template = ButtonsTemplate(
        title='usage', text='Tap below buttons', actions=[
            MessageAction(label=m, text=m) for m in message_list
        ])

    template_message = TemplateSendMessage(alt_text='Usage', template=buttons_template)
    replies += [template_message]

    line_bot_api.reply_message(event.reply_token, replies)


def confirm():
    hostname = "192.168.1.**"
    response = os.system("ping -c 1 " + hostname)
    # and then check the response...
    if response == 0:
        pingstatus = "Awake"
    else:
        pingstatus = "Sleeping"

    return pingstatus

def kick():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    s.sendto(b'\xFF' * 6 + b'\x**\x**\x**\x**\x**\x**' * 16, ('192.168.1.255', 7))
    s.close()

    return 'kicked'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

少し説明を入れると,まず以下でLINE用のChannel SecretとChannel Access Tokenを読み込みます.

# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)

以下は,Web表示用に一応加えました.

@app.route('/')
def show_web():
    return 'This is vpn server for wake on lan!\n\n'

ここに上記で設定したWebhookのリクエスト処理を書きます.

@app.route('/wake-on-lan', methods=['POST'])
def wake_on_lan():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'ok'

そして,最後にメッセージで"kick"が来たら,MagicPacketの送信,"check"が来たらPingで起動できているかを確認する処理をここで書いています.

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message = event.message.text

    replies = []
    if re.compile("\s*(check)\s*", re.IGNORECASE).search(message):
        result = confirm()
        replies += [TextSendMessage(result)]

    elif re.compile("\s*(kick)\s*", re.IGNORECASE).search(message):
        if confirm() == 'Awake':
            replies += [TextSendMessage('Already awake')]
        else:
            result = kick()
            replies += [TextSendMessage(result)]

    buttons_template = ButtonsTemplate(
        title='usage', text='Tap below buttons', actions=[
            MessageAction(label=m, text=m) for m in message_list
        ])

    template_message = TemplateSendMessage(alt_text='Usage', template=buttons_template)
    replies += [template_message]

    line_bot_api.reply_message(event.reply_token, replies)


def confirm():
    hostname = "192.168.1.**"
    response = os.system("ping -c 1 " + hostname)
    # and then check the response...
    if response == 0:
        pingstatus = "Awake"
    else:
        pingstatus = "Sleeping"

    return pingstatus

def kick():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    s.sendto(b'\xFF' * 6 + b'\x**\x**\x**\x**\x**\x**' * 16, ('192.168.1.255', 7))
    s.close()

    return 'kicked'

Apacheを再起動します.

sudo service apache2 restart

おわり

後半,疲れてだれてしまいました...
さらに備忘録的に書いているのでわかりづらいかもしれません...
とりあえず全部書けたので良かったです。参考になれば幸いです.

動画

参考

How To Use Apache HTTP Server As Reverse-Proxy Using mod_proxy Extension

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3