月別アーカイブ: 2018年12月

lambdaとRoute53を使ってDDNS機能を作った

概要

固定IPを振っていない店用にlambdaとRoute53を使ってDDNS機能を作った

背景

この間、店の呼び出しブザーを作ったけど死活監視をしていない。zabbixエージェントを入れようとしたけど店には固定IPが来ていないのでDDNSなり(VPNを貼るなり)しないといけない。

システム

環境

  • python3.7.0
  • raspbian9.4
  • Raspberry Pi B+
  • API Gateway
  • Lambda
  • Route53

システム概要

店のネットワーク内にあるラズパイからAPI Gateway経由でLambdaを呼出。

呼び出されたLambdaで呼び出し元(ラズパイ)のグローバルIPを取得。

今回のグローバルIPが前回のグローバルIPと違ったらLambda内でbotoを使ってRoute53のレコードを変更

lambdaのソース

ポン置きのラズパイから起動しているのでセキュリティ的に不安。なので、対象サーバとかzoneIdは引数でなくlambda側で持っている。
対象のAレコードなかったりしたら動かないけどログ見たらなんとかなるはず。

import boto3
import logging

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

def lambda_handler(event, context):
        ZONE_ID = 'Route53のHosted Zone ID'
        logger.debug('call lambda')
        source_ip = event['source_ip']
        original_ip = event['original_ip']

        logger.debug( 'source_ip -> ' + source_ip)
        logger.debug( 'original_ip -> ' + original_ip)


        if original_ip == '' or original_ip != source_ip:
            logger.info('original_ip != source_ip')
            logger.info( 'source_ip -> ' + source_ip)
            logger.info( 'original_ip -> ' + original_ip)
            client = boto3.client('route53')

            try:
                response = client.list_resource_record_sets(HostedZoneId=ZONE_ID)
                target = [item for item in response['ResourceRecordSets'] if item['Name'] == 'hogehoge.epea.co.jp.' and item['Type'] == 'A'][0]
                logger.info(target)

                setting_ip = target['ResourceRecords'][0]['Value']

                if setting_ip != source_ip:
                    logger.info('modify start')
                    target['ResourceRecords'][0]['Value'] = source_ip

                    client.change_resource_record_sets(
                        HostedZoneId = ZONE_ID,
                        ChangeBatch = {
                            'Comment': '多分IPかわった',
                            'Changes': [{
                                'Action': 'UPSERT',
                                'ResourceRecordSet':target
                                }]
                            }
                    )
                    logger.info('modify finish')
            except Exception as e:
                logger.error('KOKODESUKOKODESU')
                import traceback
                traceback.print_exc()
                raise Exception("Check CloudWatch")
        return {
            'statusCode': 200,
            'body': source_ip
        }

全体の呼び出し元

Loopしながら呼び出し続けるのみ。

#!/usr/bin/env python3
# coding: utf-8
import json
import logging
import time
import os
import signal
import sys

import requests

def invoker(originalip):
    logger.debug('invocker start')
    logger.debug( 'original_ip -> ' + original_ip)

    headers = {'Content-Type' : 'application/json','x-api-key': ddns_token}
    payload = {'original_ip': original_ip}
    res = requests.post('https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/default/ddns'
        , data=json.dumps(payload)
        , headers=headers)
    if res.status_code != 200:
        print(res.text)
        print(res.status_code)
        raise Exception("TODO")

    logger.debug('res body ' + res.json()['body'])
    logger.debug('invocker finish')
    return res.json()['body']

def handler(signal, frame):
    logger.info('invocker stop')
    sys.exit(0)

signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)

try:
    formatter = '%(levelname)s : %(asctime)s : %(message)s'
    logging.basicConfig(level = logging.INFO, filename = 'ddns.log', format=formatter)
except:
    print >> sys.stderr, 'error: could not open log file'
    sys.exit(1)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

ddns_token = os.environ['DDNS_TOKEN']
original_ip = ''
logger.info('invocker start')
logger.debug('ddns_token ->[' + ddns_token + ']')
while True:
    logger.debug('in main loop')
    original_ip = invoker(original_ip)
    time.sleep(900)

権限

ラムダ作った時に作られる権限の他にRoute53のレコード参照/操作権限を付与

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "route53:ChangeResourceRecordSets",
                "route53:ListResourceRecordSets"
            ],
            "Resource": [
                "arn:aws:route53:::change/hostedzone/Route53のHosted Zone ID",
                "arn:aws:route53:::hostedzone/Route53のHosted Zone ID"
            ]
        }
    ]
}

API Gatewayの設定

リクエストのマッピング

#set ($body = $util.parseJson($input.json('$')))
{
   "original_ip" : "$body.original_ip",
   "source_ip" : "$context.identity.sourceIp"    
}

エラー時のマッピング

正規表現(でなくそのままだけど) “Check CloudWatch”でメソッドレスポンスのステータスを500に指定

systemd

特にコメントなし

[Unit]
Description=DDNS Daemon

[Service]
EnvironmentFile=/home/pi/.config/environment.d/ddns.conf
WorkingDirectory=/home/pi/develop/ddns/
ExecStart=/home/pi/develop/ddns/invoke_ddns.py
ExecStop=/bin/kill ${MAINPID}
Restart=always
Type=simple
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

それはそうとボルログに店の情報のせてもらったけど今はほとんど更新されてないのね。。

Raspberry Piで作った玄関の呼び出しブザーにLine通知も付けてみた

概要

このまえqiitaに書いたメール通知ができる玄関の呼び出しブザーにLine通知も付けてみたのでそのメモ。

環境

python3.7.0
raspbian9.4
Raspberry Pi B+

プログラム

事前準備

requestsモジュールを使うのでインストール

pip install requests

ソース

ほぼこちらのとおり。他に指定できるものはドキュメント参照。

LINE_TOKENは環境変数から取得。引数のcurrent_timeは呼び出し元でフォーマット済みの文字列

(line_notify.py)

#!/usr/bin/env python3
# coding: utf-8
import os

import requests

def line_notify(current_time):
    url = 'https://notify-api.line.me/api/notify'
    token = os.environ['LINE_TOKEN']
    headers = {"Authorization" : "Bearer "+ token}

    message =  '呼出ブザーが押されました %s' % current_time
    payload = {"message" :  message}

    r = requests.post(url ,headers = headers ,params=payload)

if __name__ == '__main__':
    line_notify( '2018/11/12 固定' )

他のソース/設定ファイル

コントローラー

(control.py)

前の記事ではwait_for_edgeでボタンプッシュを検知していたが、SIGINTきても処理がとまらないとのことでadd_event_detectに変更

また、systemdに登録したのでsignalをハンドラで処理するように変更

#!/usr/bin/env python3
# coding: utf-8
from datetime import datetime
from datetime import timedelta
import logging
import signal
import sys
import time

import RPi.GPIO as GPIO

from mail import visitmail
from line_notify import line_notify

def handler(signal, frame):
    logger.info('break')
    GPIO.cleanup()
    sys.exit(0)

def callBuzzer(channel):
    try:
        # ノイズ対策 静電気等のノイズでないか0.1秒以上続いていることをチェック
        time.sleep(0.1)
        if GPIO.input(pin) == GPIO.LOW:

            # 連続クリック対応 何度もメールが飛ぶと面倒なので一定時間内のボタンプッシュは無視
            # ノイズ対策の前に置くとこの処理時間で継続時間が長くなってしまうのでこの処理順にしている
            global before_calltime
            current_time = datetime.now()
            if current_time < (before_calltime + timedelta(seconds=60)):
                logger.debug('callback yet')
                return

            logger.debug('before_calltime:%s current_time:%s'
                % (before_calltime.strftime("%Y/%m/%d %H:%M:%S"),current_time.strftime("%Y/%m/%d %H:%M:%S")))
            before_calltime = current_time

            logger.info('visit actions call')
            str_current_time = current_time.strftime("%Y/%m/%d %H:%M:%S")
            visitmail(str_current_time)
            line_notify(str_current_time)
    except Exception as e:
        logger.error('error:  %s' % e)

signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)

try:
    formatter = '%(levelname)s : %(asctime)s : %(message)s'
    logging.basicConfig(level = logging.INFO, filename = 'doorphone.log', format=formatter)
except:
    print >> sys.stderr, 'error: could not open log file'
    sys.exit(1)
logger = logging.getLogger(__name__)
#logger.setLevel(logging.DEBUG)
loop_logger = logging.getLogger('loop_logger')
#loop_logger.setLevel(logging.DEBUG)

logger.info('start doorphone moniter')
before_calltime = datetime.now() - timedelta(seconds=60)

GPIO.setmode(GPIO.BCM)
pin = 25

GPIO.setup(pin, GPIO.IN,pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(pin, GPIO.FALLING, callback=callBuzzer, bouncetime=300)

while True:
    loop_logger.debug('main loop runnning')
    time.sleep(1)

メール送信処理

(mail.py)

引数かえたぐらいで特に変更なし

#!/usr/bin/env python3
# coding: utf-8
import configparser
from email.mime.text import MIMEText
from smtplib import SMTP
import os

def visitmail(current_time):
    inifile = configparser.ConfigParser()
    inifile.read('./config.ini', 'UTF-8')

    ini_host = inifile.get('server', 'host')
    ini_port = inifile.get('server', 'port')
    ini_from = inifile.get('mail', 'from')
    ini_to = inifile.get('mail', 'to')
    ini_title = inifile.get('mail', 'title')

    with SMTP(host=ini_host, port=ini_port) as smtp:
        smtp.starttls()
        smtp.login(
                user = ini_from,
                password = os.environ['MAIL_PASSWORD'],
                )
        msg = MIMEText(current_time)
        msg['Subject'] = ini_title
        msg['To'] = ini_to
        msg['From'] = ini_from

        smtp.send_message(
                from_addr = ini_from,
                to_addrs = ini_to.split(','),
                msg = msg,
                )

if __name__ == '__main__':
    visitmail( '2018/11/12 固定' )

設定ファイル

(./config.ini)


[server]
host = mail.hoge.co.jp
port = 587

[mail]
from = kitamura@hoge.co.jp
to = kitamura@hoge.co.jp,keitaiaddr@ezweb.ne.jp
title = 呼出ブザーが押されました

systemd

(/etc/systemd/system/doorphone.service)

[Unit]
Description=doorphone Daemon

[Service]
EnvironmentFile=/home/pi/.config/environment.d/doormail.conf
WorkingDirectory=/home/pi/develop/doorphone
ExecStart=/home/pi/develop/doorphone/control.py
ExecStop=/bin/kill ${MAINPID}
Restart=always
Type=simple
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

そういえば嫁のサイトをfirebaseに準備しました

atom-runnerでpyenvで指定したpythonを使う

環境

  • CentOS Linux release 7.6.1810 (Core)
  • Atom 1.31.2
  • atom-runner 2.7.1

設定

Atomの設定ファイル(config.cson)にpyenvが指しているpythonのパスを指定する。ホームをチルダで指定したら動かなかったので絶対パスで指定している。

pythonのパス確認

$ which python
~/.pyenv/shims/python

~/.atom/config.csonに追記(“*”: は元からあるのでその下にrunnerからの3行を追記)

"*":
  'runner':
    'scopes':
      'python': '/home/ユーザ名/.pyenv/shims/python'
  以下、元からあった分