Raspberry PI Zero WでGPSのPPS信号に同期したntpサーバの構築

ありふれた題材と思いますが、勉強を兼ねて実施したので、備忘

使用機器およびソフトウェア

本体: RaspberryPI Zero W
GPSモジュール: 秋月電子通商製 AE-GYSFDMAXB
OS/ディストリビューション: raspbian lite(stretch)

(* 使ったのがたまたまZero Wだっただけで、2,3,Zeroでもほぼ同じで動くはず。)

前提条件

とりあえずラズパイ自体は動作していて、インターネットに接続できていることが前提です。

パッケージ導入と周辺ソフトウェア設定

ntp, ntpdate,fake-hwclockパッケージを導入

fake-hwclockはntpの設定変更後、起動時に時計がリセットされてしまうため、対策として追加

$ sudo apt install ntp ntpdate fake-hwclock

知らぬ間に時計を修正されると気持ちがわるいので、systemdの時刻同期処理を停止

$ sudo timedatectl set-ntp false

DHCP配布されたIPアドレスを使用している場合,dhcpcdが用意した設定ファイルでntpdが起動するため
dhcpcd.confを変更して、この挙動を阻止

#/etc/dhcpcd.conf

option ntp_servers
↓
# option ntp_servers (コメントアウト)

GPSモジュールの設定

ラズパイ側の設定
UART(シリアルポート)とPPSドライバの有効化
/boot/config.txt に以下を追記

enable_uart=1
dtoverlay=pps-gpio,gpiopin=18,assert_falling_edge=true

enable_uart=1でUARTを有効化
dtoverlayほにゃららでpps信号をGPIO18で扱えるようにします.
また、このGPSモジュールはPPS信号を立ち下がりエッジで送信するため、assert_falling_edgeオプションを指定しています.
(デフォは立ち上がりエッジで検出)

また、bluetoothを使用しないのであれば,以下を追加することで、
ラズパイに搭載されている2つのUARTのうち、機能の良い方を使用することができます.

dtoverlay=pi3-diable-bt

このまま接続すると、シリアルポートからとんでもない値が入力されてOSが悲しいことになるので
/boot/cmdline.txtを変更してシリアルコンソールを無効化します。
具体的には

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 ....

となっているところの console=serial0,115200 を削除します。

ntpがGPSおよびPPS信号を読み込む際に指定するデバイスファイル名が/dev/gpsX, /dev/gpsppsXなため、
udevにルールを追加して、OS起動時に自動的に該当ファイルが生成されるようにします.
/etc/udev/rules.d に以下の内容でファイルを追加

/etc/udev/rules.d/99-pps.rules

KERNEL=="ttyS0",SYMLINK+="gps0"
KERNEL=="pps0",OWNER="root",GROUP="dialout",MODE="0660",SYMLINK+="gpspps0"

bluetoothを無効にした場合は,ttyS0の部分をttyAMA0に変更してください.

追加したデバイスファイルをntpが読めるようにグループdialoutにユーザntpを追加

$ sudo usermod -aG dialout ntp

ここまで設定が終わったら,ラズパイの電源を落として、GPSモジュールとラズパイを配線する
電源ブチ切りするとお亡くなりになる可能性があるので注意

GPSモジュール,ラズパイ間の配線は

モジュール - ラズパイ
5V - P4(5V)
GND - P6(GND)
TXD - P10(GPIO15 UART_RXD)
RXD - P8(GPIO14 UART_TXD)
1PPS - P12(GPIO18)

ラズパイの5VはP2からも出てますが、P4から取ると隙間なく5ピン揃って良いんじゃないでしょか。
配線が終わったらラズパイの電源を入れる.

GPSのPPS信号に同期したNTPDの設定

参考にしたサイトにはPPS信号を扱えるようにntp自体をソースファイルからビルドするとありましたが、
今はパッケージのままで大丈夫そうです。

/etc/ntp.conf に以下を追記

server 127.127.20.0 mode 16 minpoll 4 manpoll 4
fudge 127.127.20.0 flag1 1 flag3 0 flag4 0 time1 0.0 time2 0.488 refid GPS

server 127.127.20.0 … : Generic NMEA GPS Receiver を使用
mode 16 : シリアルポートのボーレート指定 9600bps

fudge 127.127.20.0 … : Generic NMEA GPS Receiver の設定
flag1 1 : PPS信号を扱う
flag3 0 : ntpモード/カーネルモード 0でntpモード, 1にするにはlinuxカーネルのビルドが必要なので、今は0
time1 0.0 : PPS信号の遅延時間(ミリ秒)を指定, とりあえず0.0
time2 0.488 : シリアルポートで受信される電文の遅延時間(ミリ秒)を指定, 0.0だと頻繁に時間飛びが発生したのでとりあえずこのぐらい。要チューニング箇所

設定の詳細はntpの公式を参照のこと.

ntpを一旦停止し,ドリフトファイルを削除,時計合わせを行なって,ntpを起動する.

$ sudo systemctl stop ntp
$ sudo rm -f /var/lib/ntp/ntp.drift
$ sudo ntpdate ntp.nict.jp
$ sudo systemctl start ntp

以上で、問題がなければ、ntpqコマンドで以下のような出力が出てくると思います。
(下記はカーネルモードを有効にしてるので、若干違いますが。)
GPSモジュールから受信した電文で直近採用されたものがtimecode=の箇所にでてくるので、
動作が怪しい時はこの辺り見るとよいかと。

$ watch -n 16 ntpq -crv -ccv -pn

associd=0 status=041d leap_none, sync_uhf_radio, 1 event, kern,
version="ntpd 4.2.8p10@1.3728-o Sat Oct  7 14:29:08 UTC 2017 (1)",
processor="armv6l", system="Linux/4.9.59-pps+", leap=00, stratum=1,
precision=-19, rootdelay=0.000, rootdisp=62.729, refid=GPS,
reftime=dda64fc0.471cca3c  Fri, Nov  3 2017 11:15:28.277,
clock=dda64fc0.d335cd8a  Fri, Nov  3 2017 11:15:28.825, peer=38248, tc=4,
mintc=3, offset=0.000019, frequency=-10.019, sys_jitter=0.001907,
clk_jitter=0.003, clk_wander=0.000
associd=0 status=0000 no events, clk_unspec,
device="NMEA GPS Clock",
timecode="$GPGGA,021528.000,3442.5578,N,13527.6561,E,1,7,1.20,15.5,M,34.2,M,,*68",
poll=7, noreply=0, badformat=0, baddata=0, fudgetime2=488.000, stratum=0,
refid=GPS, flags=5
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
o127.127.20.0    .GPS.            0 l    -   16  177    0.000    0.000   0.002

カーネルpps対応

TODO:

電源ブチ切り対策(リードオンリーなrootfs)

TODO:

参考サイト

ipv6とtlsでトラブル発覚

Sakura VPSにおいてあるマシンから、自ドメイン宛にメールを送信した際、
Sakura側のmtaとして使っているexam4に以下のログが残っていたので調査
(送信後,5分ほど経過してから受信していたので、何かおかしいんだろうなと
思ってはいたものの放置してた。)

2017-10-10 08:31:05 1e1hQz-000487-Cd H=mta [mta ipv6 addr] TLS error on connection (gnutls_handshake): timed out

結果として,根本的な原因は不明なままだが,
mtaへipv4で接続すれば,見た所はエラーは解消した為,
dnsのエントリからmtaのAAAAレコードを削除することで対処した。

追記:
自ドメイン側でipv6 Path MTU Blackholeを発生させていたことが原因でした.
ルータの設定を見直して対処完了です。恥ずかしい x)

以下は、調査履歴と泣き言。

続きを読む ipv6とtlsでトラブル発覚

Debian stretch のpostfix

今朝気づいたらpostfixが止まっててびっくりした。

systemctl start postfix
とかしても、エラーは出ないが、動作もしない状態なので
頭抱えながら /lib/systemd/system/postfix.service を眺める

exec = /bin/true

… それはエラーにはならないねぇ。と今までどうやって動いてたんだ??と再びお悩み状態。
藁をもすがる状態で、/usr/share/doc/postfix/README.Debian を見てみたところ

postfixマルチインスタンスモードの時は下記をやれ
プライマリインスタンスは${INSTANCE_NAME}が”-“な

# systemctl daemon-reload
# systemctl enable postfix@${INSTANCE_NAME}.service
# systemctl start postfix@${INSTANCE_NAME}.service

みたいなことが書いてあるので、実施。
すると、まぁ、動作する
が、いつからこうなったんだろう・・・

メモ: import_tasks の探索順

ansible 2.4 での import_tasks の探索順

playbook ... (0) カレントディレクトリ
+ import.yml ... (I0)
+ roles
  + role_a
    + tasks
      - main.yml ... (1)
      - import.yml ... (I1)
    + handlers
      - main.yml ... (2)
      - import.yml ... (I2)

(1)でimport_tasksを実行 -> (I1) が優先, 無ければ(I0)
(2)でimport_tasksを実行 -> (I2) が優先, 無ければ(I1), (I0)の順で探索

配置がややこしくなるので、handlers以下にはmain.ymlだけを置いて
タスクはtasks以下に配置するのがよさげ.

todo

private docker registry のイメージ管理を行いたいので自前でイメージ管理スクリプトの作成
-> とりあえず作った 要python3 オレオレ仕様
https://github.com/mokemoke88/docker_reg_helper

自前ビルドしたDockerイメージを、registryに登録する際の手順を考える

今の所の想定
手元でビルド -> <image名>:build(local)

Jenkins で ビルド ( master or releaseブランチは自動ビルド対象から外しておく)
<registryパス>/<image名>:gitブランチ名 を registryからpull
<image名>:gitブランチ名-日付-ビルド番号 でビルド
<registryパス>/<image名>:gitブランチ名 でtaggingして、registryへpush

最終成果物は個別で作成
git にtagging (0.0.1 とか)
<registryパス>/<image名>:latest を registry から pull
<image名>:release-日付-ビルド番号 でビルド
<registryパス>/<image名>:<gitのtag> で tagging して、registryへpush
<registryパス>/<image名>:latest でtagging して、registryへpush

ゴミがいっぱい残りそうなのと、最終成果物の生成がうまくいきそうになくて、もやもやする。

Declarative Pipeline で 自動Checkoutしたブランチ名を取得する

取得した値は環境変数 BUILD_BRANCH に登録する.

Jenkinsの設定画面

pipeline の場合

SCMの追加処理で “specific local branch” を追加する.

multi branch pipeline で Branch Sources に GitHubを使用している場合

Additional で “Check out to matching local branch” を追加する.

Jenkinsfile

最初のstageとして以下を追加

stage('pre setup'){
  steps{
    script{
      env.BUILD_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref @').trim()
    }
  }
}

続きを読む Declarative Pipeline で 自動Checkoutしたブランチ名を取得する

.svn 削除

何度やってもfindの使い方が頭に入ってこないのでメモ。

カレント以下の.svnを抹殺

$find . -name .svn -exec rm -fr {} \;

もしくは

$find . -name .svn | xargs rm -rf

Debian jessie 導入したdocker daemon へ Macのdocker-machieからアクセスする

やってみたら、ダラダラ面倒だったので、メモ

デフォルトで導入したままだと、unix socketでしかlistenしていないのでtcpでもlistenするようにする.
tcpでlistenする際、sslでの認証が必要になるので、ssl証明書やら、キーも作成する.
(といっても公式やらに書いていることやるだけ)

SSL証明書やらの作成

認証局の秘密キーと公開キーを作成

[認証局秘密キー ca-key.pem]
$openssl genrsa -aes256 -out ca-key.pem 4096
(パスフレーズの問い合わせがあるので、入力)
[認証局公開キー(証明書) ca.pem]
$openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
(認証局秘密キーのパスフレーズ入力)
(Common Name の入力で、ホストのFQDNを入力)

サーバ鍵と証明書署名要求の作成

[サーバ鍵 server-key.pem]
$openssl genrsa -out server-key.pem 4096
[証明書署名要求 server.csr]
$openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr
($HOSTは、対象のFQDN)
[証明書の追加情報 extfile.cnf]
$echo subjectAltName = IP:www.xxx.yyy.zzz,IP:127.0.0.1 > extfile.cnf
(www.xxx.yyy.zzzは対象のIPアドレス)

サーバ証明書の作成

[認証局署名付きサーバ証明書 server-cert.pem]
$openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem ¥
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf
(出力メッセージのsubject=/CN=FQDN が証明書署名要求で使ったFQDNになっていることを確認)
(認証局秘密キーのパスフレーズの問い合わせがあるので入力)

クライアント鍵と証明書署名要求の作成(docker-machine側で使用)

[クライアント鍵 key.pem]
$openssl genrsa -out key.pem 4096
[証明書署名要求 client.csr]
$openssl req -subj '/CN=client' -new -key key.pem -out client.csr
[クライアント認証用の追加設定ファイル extfile.cnf]
$echo extendedKeyUsage = clientAuth > extfile.cnf

認証局署名付きクライアント証明書の作成

[認証局署名付きクライアント証明書 cert.pem]
$openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem ¥
  -CAcreateserial -out cert.pem -extfile extfile.cnf
(認証局秘密キーのパスフレーズの問い合わせがあるので入力)

証明書署名要求ファイルは必要なくなったので、削除。

$rm -v client.csr server.csr extfile.cnf

鍵を本人しか触れないようにパーミッション変更

$chmod -v 400 ca-key.pem key.pem server-key.pem

証明書は変更できないように書き込み権限を落としておく

$chmod -v 0444 ca.pem server-cert.pem cert.pem

Debian(Docker-engine)側の設定変更

作成したサーバ鍵、証明書、認証局証明書を/etc/docker以下に移動

$mv ca.pem server-cert.pem server-key.pem /etc/docker/

dockerデーモンの起動設定はsystemdで制御されているので,デフォルト設定ファイル/lib/systemd/system/docker.serviceを/etc/systemd/system/以下にコピーしてから編集

$cp /lib/systemd/system/docker.service /etc/systemd/system/
[変更前]
ExecStart=/usr/bin/dockerd -H fd://
[変更後]
ExecStart=/usr/bin/dockerd ¥
 --tlsverify ¥
 --tlscacert=/etc/docker/ca.pem ¥
 --tlscert=/etc/docker/server-cert.pem ¥
 --tlskey=/etc/docker/server-key.pem ¥
 -H fd:// ¥
 -H tcp://0.0.0.0:2376
[変更前]
ExecStart=/usr/bin/docker -H fd://
[変更後]
ExecStart=/usr/bin/docker ¥
  --tlsverify ¥
  --tlscacert=/etc/docker/ca.pem ¥
  --tlscert=/etc/docker/server-cert.pem ¥
  --tlskey=/etc/docker/server-key.pem ¥
  -H fd://

/etc/systemd/system/docker-tcp.socket を以下の内容で作成

[Unit]
Description=Docker Soccer for the API

[Socket]
ListenStream=2376
BindIPv6Only=both
Service=docker.service

[Install]
WantedBy=socket.target

(2017.02.06 更新)

systemdに設定の変更を教え込んで、デーモン再起動

$systemctl daemon-reload
$systemctl stop docker
$systemctl enable docker-tcp.socket
$systemctl start docker-tcp.socket
$systemctl start docker

Debian側、これにて終了。続いてMac(Docker-machine)側

Mac(Docker-machine)側の設定

docker-machineに新規マシン登録

$docker-machine create -d "none" --url tcp://www.xxx.yyy.zzz:2376 DOCKER_HOST
 (www.xxx.yyy.zzzはdebianのIPアドレス)
 ($DOCKER_HOSTはdocker-machineに登録する名前)

~/.docker/machine/machines/以下にDOCKER_HOSTなディレクトリが作成され、設定ファイル config.jsonが生成されているので、とりまディレクトリに移動

$cd ~/.docker/machine/machines/DOCKER_HOST

先に生成したクライアント秘密キー, 証明書,認証局証明書(key.pem, cert.pem, ca.pem)を何とかして、ここにコピー

docker-machineがコピーしたファイルを見るようにconfig.jsonを変更

"AuthOptions": {
  "CertDir": "/Users/hogehoge/.docker/machine/certs",
  "CaCertPath": "/Users/hogehoge/.docker/machine/machines/DOCKER_HOST/ca.pem",
  "CaPrivateKeyPath": "",
  "CaCertRemotePath": "",
  "SeverCertPath": "",
  "ServerKeyPath": "",
  "ServerCertRemotePath": "",
  "ServerKeyRemotePath": "",
  "ServerCertSANS": [],
  "ClientKeyPath": "/Users/hogehoge/.docker/machine/machines/DOCKER_HOST/key.pem",
  "ClientCertPath": "/Users/hogehoge/.docker/machine/machines/DOCKER_HOST/cert.pem",
  "StorePath": "/Users/hogehoge/.docker/machine/machines/DOCKER_HOST"
}

docker-machine ls とかして、接続できているか、確認

$docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
DOCKER_HOST - none Running tcp://www.xxx.yyy.zzz:2376 v1.13.0

バージョンが表示されていれば、とりま動作しているかと思われる。
そして、息切れ。以上。