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'
  以下、元からあった分

pyenvでCentOS7にpython3.7.0をインストール

環境

CentOS Linux release 7.5.1804 (Core)
pyenv 1.2.7

依存パッケージインストール

pyenvの公式のwikiにある推奨される依存関係をまとめてインストール。

yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel

pythonインストール実施

[yoshitake@localhost ~]$ pyenv install 3.7.0
(略)
[yoshitake@localhost ~]$ pyenv versions
* system (set by /home/yoshitake/.pyenv/version)
3.7.0

切り替え

[yoshitake@localhost ~]$ pyenv global 3.7.0
[yoshitake@localhost ~]$ pyenv versions 
  system
* 3.7.0 (set by /home/yoshitake/.pyenv/version)

インストール失敗のログ

zlib本体はもとから入っていたけど、読めないと言われてインストール失敗。
zlib-develが入っていなかったからこのメッセージになった?

[yoshitake@localhost ~]$ pyenv install 3.7.0
Downloading Python-3.7.0.tar.xz...
-> https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz
Installing Python-3.7.0...

BUILD FAILED (CentOS 7.5.1804 using python-build 20180424)

Inspect or clean up the working tree at /tmp/python-build.20181029194007.1745
Results logged to /tmp/python-build.20181029194007.1745.log

Last 10 log lines:
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__main__.py", line 5, in 
    sys.exit(ensurepip._main())
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__init__.py", line 204, in _main
    default_pip=args.default_pip,
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__init__.py", line 117, in _bootstrap
    return _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__init__.py", line 27, in _run_pip
    import pip._internal
zipimport.ZipImportError: can't decompress data; zlib not available

zlib本体はもとから入っていた

[yoshitake@localhost ~]$ sudo yum install zlib -y
[sudo] yoshitake のパスワード:
読み込んだプラグイン:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirrors.cat.net
 * extras: mirrors.cat.net
 * updates: mirrors.cat.net
パッケージ zlib-1.2.7-17.el7.x86_64 はインストール済みか最新バージョンです
何もしません

jenkins pipeline内のpostgresにてDBダンプ取得

環境

CentOS Linux release 7.5.1804 (Core) DockerのHostマシン
Jenkins 2.138.1
Docker Commons Plugin
Docker Pipeline
postgres 11(公式のdocker-image latest)

やりたかったこと

他のDBサーバからデータを持って来てダンプファイルを作りたかった。

動いたJenkinsfile設定

.pgpassを任意のパス(ここではhostの/ci/app/hoge)に置き、そのパスをvオプションでマウント、さらにマウントしたファイル(ここでは/root/.pgpass)をeオプションで指定。

    
pipeline {
  agent none
  stages{
    stage('DBコピーフェーズ') {
      agent {
        docker {
          image 'postgres'
          args '-v /ci/app/hoge:/root -e PGPASSFILE=/root/.pgpass'
        }
      }
  
      steps {
        sh 'pg_dump -h ホスト名 -p 5432 -U ユーザ名 DB名 > /root/test.sql'
        sh 'echo DBコピー'
      }
    }
  }
}

動かなかった設定

.pgpassを任意のパス(ここではhostの/ci/app/hoge)に置き、そのパスをホームディレクトリとして環境変数で指定。

args '-v /ci/app/hoge:/home/ciuser -e HOME=/home/ciuser'

postgresコンテナ内の環境変数にはHOMEが反映されているが.pgpassは読み込まれなかった。
多分
getpwuidで実行ユーザ(uid1000)で環境変数ではなく/etc/passwdからホームを取得しようとする
postgresコンテナにはuid1000のユーザはいないからホームディレクトリが取得できない(echoしてみたら/で表示されてはいた)
/には.pgpassが無いからエラー
という流れと思うが調査は途中でやめた。

AtomのTerminal-Plusが動かなかったのでFork版に変更

環境

ATOM 1.31.2
CentOS Linux release 7.5.1804 (Core)
Terminal-Plus 0.14.5

症状

画面のプラグインから検索してインストールしたTerminal-Plusが入力を受け付けない状態だった。

対処

2018/10/15時点でメンテナンスされているFork版があった。

apm install Termination

 

issueを掘るとForkした修正済みのものがあったので

apm install LarsKumbier/terminal-plus
#すでにインストールしてしまったら
apm remove terminal-plus

なんか、先日も似たような記事を書いたけどメジャーどころも本家は結構開発止まってるのね。

補足

g++のyum install

自分の環境(CentOS7)だとg++がインストールされていなかったのでyumでインストール

make: g++: コマンドが見つかりませんでした

からの

sudo yum install yum install gcc-c++

メンテされていないパッケージ達

ATOMはメジャーなパッケージも結構メンテされていないものが多いみたい。
forkされて修正されていることが多いもようなので、イシュー掘り起こすよりPR見た方が効率的な気がする。

どっかに、プラグインのfork含むメンテされているやつのリストってあるのかな?

 

もうちょっと見てみたら

forkの修正内容見てみたらpty.jsというののバージョン変更のみ。
PTYをごにょごにょするやつが古かったので修正済みの問題を踏んでしまっているみたい。
ざっと見た感じ、同一内容のPR送っている人は多いけど、引き取って開発継続している人はいなそうなのでLarsKumbier/terminal-plusを使うのが一番省エネで済みそう。

forkされてplatformio-atom-ide-terminalになりメンテされていたけれども、そいつもメンテがとまり(?)またまたforkされてterminationが現在メンテされているようです。

owncloud clientがパスワードを入力できなくなったので対応

環境

owncloud-client.x86_64 2.5.0.10650-10213.1 @isv_ownCloud_desktop
CentOS Linux release 7.5.1804 (Core)

症状

owncloudのデスクトップ向けクライアントをアップデートしたらパスワードを始めキーボード入力全般を受け付けなくなった。
なお、マウス右クリックやGUI上のボタンによる操作は可能。

対処

issueが上がっていた。

関連するissueも見るとバーチャルキーボードの設定で、(redhat系だと?)QT_XKB_CONFIG_ROOTの設定がされていなのが問題とのこと。(コマンドラインからクライアントを起動するとエラーメッセージが見れるっぽいが未確認)

確認すると設定なし。

[yoshitake@localhost ~]$ env|grep QT_XKB_CONFIG_ROOT
[yoshitake@localhost ~]$ 

bash_profileに追記

export QT_XKB_CONFIG_ROOT=/usr/share/X11/xkb

一旦ログアウトして再ログインでなおった
source ~/.bash_profile からのclientをGUIで再起動では反映されなかった。(書きながら考えたらXWindowのレベルでリロードしないといけない。)

we did not send a packet, disable method

環境

OpenSSH_7.4p1 Raspbian-10+deb9u3, OpenSSL 1.0.2l 25 May 2017
(手元のラズパイしばらく更新していなかったのでこの後更新しました)

事象&対策

sshの設定をしたが失敗したのかつながらない。
ssh -vvvv 接続先
とデバッグモードで出力したが以下の様にあまり参考になるメッセージはででていない。

debug1: Authentications that can continue: publickey
debug2: we did not send a packet, disable method
debug1: No more authentication methods to try.
Permission denied (publickey).

調べなおしたら、authorized_keysに転記するときに最初の一文字目がコピペミスで消えていた。
他に、ファイルのパーミッションエラーでも同じメッセージらしい。

結論として、sshのデバックメッセージで役に立つ情報が出なかったら、一つ一つ設定を見直していった方が速い。

atomのremote-editプラグインがエラーでフォーク版に変更

環境

atom 1.31.2
remote-edit 1.9.0

事象

公式のを入れて起動したが以下のエラーが出て使用できなかった。

Failed to load the remote-edit package

TypeError: Path must be a string. Received undefined
at assertPath (path.js:28:11)
at Object.resolve (path.js:1186:7)
(略)

The error was thrown from the remote-edit package. This issue has already been reported.

 

対処

報告済みということで見てみると開発は2年ぐらい止まっているようで、fork版は動作する模様なのでそれをいれる。
インストール

apm install remote-edit-ni

補足

  • 秘密鍵でログインする場合パスワード保管しないとコネクトできていない。(未解決)
  • デフォルトだと隠しファイルが表示されないが、emote-editプラグインの設定画面より変更可

一応エラー全部

TypeError: Path must be a string. Received undefined
at assertPath (path.js:28:11)
at Object.resolve (path.js:1186:7)
at Object. (/home/yoshitake/.atom/packages/remote-edit/lib/model/remote-edit-editor.coffee:7:36)
at Object. (/home/yoshitake/.atom/packages/remote-edit/lib/model/remote-edit-editor.coffee:1:1)
at Object. (/home/yoshitake/.atom/packages/remote-edit/lib/model/remote-edit-editor.coffee:1:1)
at Module.get_Module._compile (/usr/share/atom/resources/app/src/native-compile-cache.js:106:36)
at Object.value [as .coffee] (/usr/share/atom/resources/app/src/compile-cache.js:240:29)
at Module.load (module.js:561:32)
at tryModuleLoad (module.js:504:12)
at Function.Module._load (module.js:496:3)
at Module.require (file:///usr/share/atom/resources/app.asar/static/index.js:47:45)
at require (/usr/share/atom/resources/app/src/native-compile-cache.js:66:33)
at Object. (/home/yoshitake/.atom/packages/remote-edit/lib/main.coffee:3:20)
at Object. (/home/yoshitake/.atom/packages/remote-edit/lib/main.coffee:1:1)
at Object. (/home/yoshitake/.atom/packages/remote-edit/lib/main.coffee:1:1)
at Module.get_Module._compile (/usr/share/atom/resources/app/src/native-compile-cache.js:106:36)
at Object.value [as .coffee] (/usr/share/atom/resources/app/src/compile-cache.js:240:29)
at Module.load (module.js:561:32)
at tryModuleLoad (module.js:504:12)
at Function.Module._load (module.js:496:3)
at Module.require (file:///usr/share/atom/resources/app.asar/static/index.js:47:45)
at require (internal/module.js:11:18)
at customRequire (/usr/share/atom/resources/app/static/:96:26)
at Package.requireMainModule (/usr/share/atom/resources/app/src/package.js:782:33)
at measure (/usr/share/atom/resources/app/src/package.js:143:22)
at Package.measure (/usr/share/atom/resources/app/src/package.js:88:25)
at Package.load (/usr/share/atom/resources/app/src/package.js:129:16)
at PackageManager.loadAvailablePackage (/usr/share/atom/resources/app/src/package-manager.js:619:16)
at config.transact (/usr/share/atom/resources/app/src/package-manager.js:532:20)
at Config.transact (/usr/share/atom/resources/app/src/config.js:819:20)

Qrunchはじめました

こちらが自分のページ

クロス投稿という機能を使うとカノニカル設定入るとのことなので結構SEO的にもよさそう

ただ、アカウント毎にサブドメインきられるのでQrunchの更新がほとんどないとクローラーはあまりきてくれなそう。