LXD 使ってみた

内容

LXDの導入、bridge, macvlanのコンテナを作成するまで

LXDについて

LXD は次世代のシステムコンテナーおよび仮想マシンのマネージャーです。 コンテナーあるいは仮想マシンの内部で稼働する完全な Linux システムに対して統一されたユーザーエクスペリエンスを提供します。

いろいろな Linux ディストリビューション のあらかじめビルドされたイメージを使ったイメージベースのマネージャーであり、非常に強力でありながら、非常にシンプルに、REST API を使って構築されます。

はじめに - LXDドキュメント翻訳プロジェクト

導入は簡単だったが、VM/物理マシンでのネットワークの設定で戸惑った。。

環境

Ubuntu 20.04 LTS (物理マシン)

よさげLink

linuxcontainers.org

linuxcontainers.org

よく使うコマンドリスト(ページ下部)


install

linuxcontainers.org

snapでパッケージインストール

# snap install lxd

バイナリからインストールもできるよう。

起動、初期設定

/etc/sysctl.confnet.ipv4.ip_forward=1のコメントを外しておく。←いらないかも?

lxdグループに入っていたら、sudoなしでlxdlxcコマンドを利用できる。

# lxd init

今回は、すべてデフォルト設定にした。(lxd init --auto)

image

lxd-ja.readthedocs.io

OSimageは配布されているものを使う。それを基にしたオリジナルimageを使うことも可能。
配布imageにはUbuntuCentOSなどバージョンも多くの種類が用意されている。
Cloud-initが動作する/しないimage、vmバージョンのimageなど様々。

imageの検索

lxc image list [image_server]: [keyword] ..

# lxc image list images: amd64 gentoo

インスタンスの実行

lxc launch [image_server]:[image_alias] [name] [flags]

# lxc launch ubuntu:f uf1

[flags]--vmとすれば、VMが起動する。

今回使ったものは、ubuntu:f

インスタンスを操作

名前を指定してインスタンスにコマンドを実行させる。

名前はlxc listなどで確認する。

lxc exec [name] -- [command]

# lxc exec uf1 bash

--はなくてもいいが、オプションを渡す場合はこれをつけないとうまくいかない。

また、コンテナ側でリダイレクトを利用するときは、bash -c 'echo hoge > /tmp/hoge.txt'というようにまとめてから実行させないと、ホスト側にリダイレクトされるので注意。

ネットワークについて

ここまではGettingStartedをみればできるが、ネットワークの設定で躓いた。

lxd-ja.readthedocs.io

まず、インスタンスの設定項目にはnetworkprofileがある。

network

ip lで出てくるデバイスについて詳細を見る

# lxc network show lxdbr0

lxdbr0は、デフォルト設定でlxd initすれば作成されるデバイス
インスタンスを作成するとき、デフォルトではこれがbridgeとして使われる。

bridgeというのは、ネットワークタイプの設定で、ほかにはmacvlanovnなどがある。

bridgeは、ホストからインスタンスには通信可能だが、外部からインスタンスには通らない。(private)
macvlanは、ホストの有線nI/Fを使う(ブリッジする)必要がある(複数のMACアドレスを扱うため。無線は複数MACに対応していないらしい)。外部からインスタンスにアクセスできる(public)

macvlanを使う場合は、ブリッジするホストのデバイスDHCPをオンにしておく必要がある。今回は、/etc/netplan/のconfigからdhcp4: trueとなっているのを確認。

VM(VBox)では、インスタンスから外部に通信できないなどうまくできなかった。
物理マシンでは、アパート管理のDHCPサーバーを利用しているためか、ipの割り振りが時々されず。。
結局、安定しているbridgeを使うことにした。
外部からホストを中継してsshすればアクセスできる(後述)し、 bridgeなら、ホストマシンのlxddnsmasq(lxdのdnsmasqなので、マシンに入れていなくておkだしsystemd-resolvedとの競合もなさげ。)を使ってDHCP割り振りを行っているので管理しやすい。

profile

profileは、インスタンスの設定をテンプレにしたもので、何のprofileを適用するかlaunch時に指定できる。
デフォルトではdefaultというのが使われる。オリジナルのprofileを作るときはこれを基に書き換えるなどする。
defaultにはnetwork: lxdbr0とあるため、デフォルトインスタンスlxdbr0bridgeとなる。

# lxc profile show default

例えばmacvlanのインスタンスを作成するprofileを作成する場合

# lxc profile copy default mvlprof
# lxc profile edit mvlprof

とすることで、viを使って編集できる。

このように設定した。

config: {}
description: MACVLAN profile
devices:
  eth0:
    name: eth0
    nictype: macvlan
    parent: enp1s0
    type: nic
  root:
    path: /
    pool: default
    type: disk
name: mvlprof
used_by: []

parentの値は、ip lで出てくる有線デバイス

これを適用してlaunchすると、macvlanのインスタンスが作成される。

# lxc launch -p mvlprof ubuntu:f mvluf1
# lxc list

(launchしただけのときにparentの範囲のipが振られていない場合は、インスタンスを再起動する。)
# lxc restart mvluf1

外部からpingを通してみる

$ ping [instance_ip]

64 bytes from 192.168.12.16: icmp_seq=1 ttl=63 time=4.67 ms
...

ipの固定

作成
lxc network attach [network] [instance_name] [insatnce_new_I/F]
割り当て
lxc config device set [instance_name] [instance_new_I/F] ipv4.address [ip_address]

# lxc network attach lxdbr0 uf1 eth1
# lxc config device set uf1 eth1 ipv4.address 10.91.103.50
(反映させるためにインスタンスの再起動または dhclient を実行させる)
# lxc restart uf1
または
# lxc exec uf1 dhclient eth1

新しくインスタンス内のネットワークデバイスを作成して割り当てている。
だが、既存のデバイスを指定してもおk。
lxc config device set uf1 eth0 ipv4.address 10.91.103.50のようにすると、eth0に2つのipを割り当てることになる。


個人的に、"network"と"profile"の関係性を整理するのに少し躓いた。

networkip aで出るあのデバイス(正しくはlxc network listで出るやつ)のことで、
profileには、それらのどれを物理デバイスとして使うか、ソフト的にはbridgeやmacvlanなどどれを使うのか、を記述している。

…という認識。

/sbin/initが動くので、ansibleのテスト環境としてよさそうに思う。 imageの、cloud-init動作版やvm版との違いがまだわからないが、VMより軽快なのでansibleの簡易テスト環境として使ってみる所存。

よく使うコマンドリスト

状態確認

lxc network show lxdbr0
lxc network list
lxc profile show default
lxc profile list
lxc config device show default
lxc config device show uf1
lxc config device list uf1
lxc config show uf1 --expanded

// dnsmasq leases
cat /var/snap/lxd/common/lxd/networks/lxdbr0/dnsmasq.leases
lxc network list-leases

ネットワーク関連

networkデバイス新規作成
# lxc network create newbr0
defaultを流用してnetworkを作成
# lxc profile copy default ufnet
書き換え(bridgeなら lxdbr0 を ufnet に書き換えるだけ)
# lxc profile edit ufnet

profileの変更
# lxc profile assign uf1 ufnet
lxdbr0を利用してコンテナ内のeth1としてネットワークデバイスを作成
# lxc network attach lxdbr0 uf1 eth1
固定IPを設定(lxdbr0のsubnet範囲内)
# lxc config device set uf1 eth1 ipv4.address 10.91.103.50
コンテナに反映
# lxc exec f2 dhclient eth1

シェルスクリプト

引数に指定したコンテナのipアドレスを取得(CIDR表記)

#!/bin/bash
container=$1
echo $container
lxc exec $container -- ip a | awk '$1=="inet"{print $2}' | grep -v 127.0.0.1

全コンテナ名を取得

lxc list | awk 'NR>3{print $2}' | grep -v '^$'

全コンテナを削除

lxc delete `lxc list | awk 'NR>3{print $2}' | grep -v '^$'` --force

ブリッジに鍵登録

cat ~/.ssh/id_rsa.pub | xargs -i% lxc exec container -- bash -c 'echo % >> /root/.ssh/authorized_keys'

外部からlxdホストマシンを中継してコンテナに接続

ssh -t [lxd_host] ssh root@[container_ip]

公式のほかで参考にしたサイト様たち

gihyo.jp

gihyo.jp

www.waguri-soft.com

qiita.com

qiita.com

qiita.com

qiita.com

Dockerのubuntuにopenssh-server入れたらsystemctlできた(がsnapdが動かない?)

タイトルで落ちてる

docker run時のオプション指定に--privileged、プロセス指定に/sbin/initとすることも必要。

追記 2021.06.28

コンテナでvm寄りのことをしたいなら、LXDが便利です。おすすめ。
簡単な解説記事書いてますのでどうぞー

yassi.hatenablog.com


追記 2021.03.30
snapdが動かない。

root@cf10f188a61e:/# apt update && apt upgrade -y && apt install -y snapd
root@cf10f188a61e:/# systemctl start snapd
root@cf10f188a61e:/# systemctl status snapd
● snapd.service - Snap Daemon
     Loaded: loaded (/lib/systemd/system/snapd.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2021-03-30 12:36:13 UTC; 2s ago
TriggeredBy: ● snapd.socket
   Main PID: 1111 (snapd)
      Tasks: 10 (limit: 4585)
     Memory: 16.6M
     CGroup: /docker/cf10f188a61ef15c9cc1a860c7fa3056c92aeb57f22f753698c307d4803d6b55/system.slice/snapd.service
             └─1111 /usr/lib/snapd/snapd

Mar 30 12:36:13 cf10f188a61e systemd[1]: Starting Snap Daemon...
Mar 30 12:36:13 cf10f188a61e snapd[1111]: AppArmor status: apparmor is enabled and all features are available
Mar 30 12:36:13 cf10f188a61e snapd[1111]: daemon.go:347: started snapd/2.48.3+20.04 (series 16; classic) ubuntu/20.04 (>
Mar 30 12:36:13 cf10f188a61e snapd[1111]: daemon.go:440: adjusting startup timeout by 30s (pessimistic estimate of 30s >
Mar 30 12:36:13 cf10f188a61e snapd[1111]: backend.go:134: snapd enabled root filesystem on overlay support, additional >
Mar 30 12:36:13 cf10f188a61e snapd[1111]: helpers.go:105: error trying to compare the snap system key: system-key missi>
Mar 30 12:36:13 cf10f188a61e systemd[1]: Started Snap Daemon.

root@cf10f188a61e:/# sudo snap install core; sudo snap refresh core
error: cannot perform the following tasks:
- Setup snap "core" (10908) security profiles (cannot reload udev rules: exit status 1
udev output:
Failed to send reload request: No such file or directory
)
snap "core" is not installed

やっぱり、コンテナをVM代わりにするのは無理があるのか…?
仕方ないのでDockerでなく大人しくVMを使うとするか……

追記終わり


ことの経緯

ansible のテスト環境をDockerで構築できたら便利なのになー、でもVMじゃないからWSLみたいにsystemctlできないのでは??

検索してこの記事が出てくる

qiita.com

あー、やっぱだめなんだ。。検索結果みる限り、CentOSではできるっぽい?うーん。。。

記事の最後にあったsupervisorを調べてみるか。

うーむ、supervisorではだめだな。ansibleのテストをするのにsystemdが使えないのは言語道断だ。
とりあえず、このページを参考に、dockerの練習としてコンテナを作成してみる。(docker初体験故)

docs.docker.jp

supervisorは入れたらめんどくさそうなので、それ以外のパッケージだけ入れる。

コンテナつくれた。こんなんなのかすげー

…でもマジでsystemctl動かないのか、どれどれ、確かCentOSではこういう風にしなきゃだっけ…

$ docker run -itd --privileged <imageID> /sbin/init
$ docker exec -it <コンテナID>
# systemctl status ssh
● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2021-03-29 20:19:48 UTC; 1min 58s ago
       Docs: man:sshd(8)
             man:sshd_config(5)
    Process: 41 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS)
   Main PID: 45 (sshd)
      Tasks: 1 (limit: 4585)
     Memory: 1.5M
     CGroup: /docker/955ad934203feab908973ded3b82ab7859f294a53cb727abd3000b1ed285d22f/system.slice/ssh.service
             └─45 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups

う、動いたーーー!?!?

じゃあ素のubuntuでは……できない。

何が要因なのか調査

シンプルなDockerfileを書いてちまちまimageを作成、systemctlできるようになる要因を探る。

で、動作確認が取れたのがこの構成

FROM ubuntu:20.04

RUN apt update && apt install -y openssh-server
RUM mkdir -p /var/run/sshd

ENTRYPOINT ["/sbin/init"]

あらかじめサービスをインストールすることで使えるようになる…?

試しにssh-serverの代わりにnginxにして作成→だめ 今度はsambaにして作成→だめ

…なんなのか。

まあ、ansibleのテスト環境は作れそうなのでとりあえずヨシ!

apt-keyがUbuntu 22.04までしか使えないらしいので手動でGPG鍵追加メモ

apt-key addの代替手法

Ubuntu 20.04 LTS では非推奨について書かれていないようだが、Ubuntu 20.10からは( del 以外は)非推奨になり、Ubuntu 22.04で最後となるらしい。
man pageをみると、20.10からdeprecatedになっていると確認できる。

manpages.ubuntu.com

非推奨となる理由としては、/etc/apt/trusted.gpg/etc/apt/trusted.gpg.d/に鍵を置くだけでaptは完全にそのリポジトリサーバーを信頼するため、リポジトリサーバーがパッケージを任意のものに置き換えると、自サーバーのパッケージも任意のものに置き換えられてしまうかもしれないため。らしい。

まあ、お偉いさんが非推奨って言ったら黙って従うまで そういうことで調べてみると、apt-key add の代替手法がいくつか見つかったので、この際忘れないように自分なりに解釈して書き留めておく。

この回答 https://askubuntu.com/a/1307181 が詳しくてわかりよい。ただしここでは少し違う(import, exportを使わない)やり方を紹介する。

環境

Ubuntu 20.04 LTS

つまり

こういうこと

#!/bin/bash
keyURI=$1
keyName=$2
wget -O - $keyURI | gpg --dearmor > /usr/share/keyrings/${keyName}.gpg

手順概要

  1. wget/curl で鍵をDL
  2. ascii-armordなら gpg --dearmor で解除
  3. /usr/share/keyrings/ に鍵を追加
    --- ここまでで鍵追加完了 ---
  4. /etc/apt/sources.list.d/ にlistファイルを追加
  5. /etc/apt/preferences.d/ にprefファイルを追加(任意)

詳細

1. wget/curl で鍵をDL

wget

# path指定するとき
wget -O <filePath> <URL>

# カレントディレクトリにそのままDLするとき
wget <URL>

curl

# path指定するとき
curl -o <URL>

# カレントディレクトリにそのままDLするとき
curl -O <URL>

2. ascii-armordなら gpg --dearmor で解除

file <鍵path>

もしPGP public key block Public-Key (old) と出力されれば、ascii-armorとなっているため、gpg --dearmor でバイナリに変換する。
もしdata と出力されれば、変換作業はいらない。

ascii-armorを解除する

# path指定するとき
cat <鍵path> | gpg --dearmor > <鍵path>

# カレントディレクトリに<鍵名>.gpgで出力するとき( key.gpg.gpg みたいになるので注意)
gpg --dearmor <鍵path>

.asc などgpg以外の鍵名をそのまま使うと、 apt update のときに鍵を認識してくれずにエラーが出るので、 必ず .gpg の拡張子を追加する。

3. /usr/share/keyrings/ に鍵を追加

/usr/share/keyrings/は、以前の/etc/apt/trusted.gpg.d/と同じように、鍵を格納するディレクトリとして扱われる。 鍵を置いておく場所は/usr/local/share/keyrings/でもどこでもいいが、その場合は新たにディレクトリを作成する必要がある。

鍵を移動

mv <鍵path> /usr/share/keyrings/

鍵追加はこれで完了。
続いてaptにリポジトリを登録する。

4. /etc/apt/sources.list.d/ にlistファイルを追加

signed-by=<鍵path> と書くところがポイント。
ソースURIやdistributionのところに入れる文字は、それぞれのサイトを見れば書いてあるはず。
ここでは、google cloud sdk のインストール を例に挙げている。
サイトの指示通りであるhttp:とせずに、勝手にhttps:と設定したが問題なく動作したため、動作確認時にエラーが出ないならhttps:でいいのではと思う。

deb [signed-by=/usr/share/keyrings/追加した鍵.gpg] <リポジトリのソースURI> <distribution> <component>

# example
# deb [signed-by=/usr/share/keyrings/apt-key.gpg] https://packages.cloud.google.com/apt cloud-sdk main
Deb822 formatで、listではなくsourcesファイルとして追加

よりわかりやすい書式として、deb822ファイルフォーマットで書くこともできる。
ただし、拡張子がlistではなくsourcesとなることには注意する。

Types: deb
URIs: https://packages.cloud.google.com/apt
Suites: cloud-sdk
Components: main
Signed-By: /usr/share/keyrings/apt-key.gpg

5. /etc/apt/preferences.d/ にprefファイルを追加 (任意)

prefファイルを記述することで、追加したリポジトリから特定のパッケージのみをインストールするように設定できるらしい。(ここらへんよくわからない。ドキュメント語読みづら過ぎる…)

Package: <インストールを許可するパッケージ> 全て許可ならばPackage: *とする。
Pin: origin <ソースURIのオリジン> など (originの他にもlocal mark?などがあるらしいがよくわからない。DebianRepository/UseThirdParty - Debian Wiki

Pin-Priority: <num> 100はupgradeを許可。1はupgradeを禁止。ディストリビューションのデフォルトのパッケージを上書きするかもしれない(1000を超えるとダウングレードが可能になる)から、高い値にしてはいけないらしい。
Linux Certif - Man apt_preferences(5)

Package: google-cloud-sdk
Pin: origin packages.cloud.google.com
Pin-Priority: 100

最後に、動作確認として、apt updateしてみて問題なければおしまい。

うーん、面倒、、代替となるコマンド、提供されるのかしら。。。

References

DebianRepository/UseThirdParty - Debian Wiki

apt-key Is Deprecated. How To Add OpenPGP Repository Signing Keys Without It On Debian, Ubuntu, Linux Mint, Pop!_OS, Etc. - Linux Uprising Blog

What commands (exactly) should replace the deprecated apt-key? - Ask Ubuntu

APT の設定 (/etc/apt/sources.list) をちゃんと理解する - くじらにっき++

DebianRepository/Format - Debian Wiki

Ubuntu20.04 固定ip設定+VMware(NAT)とSSH接続準備メモ

Ubuntuipアドレスをnetplanとsystemd-resolvedで固定化、
ホストからSSH接続できるようにホスト側(Win)の設定を調整する。
ゲストはNAT接続(VMnet8)。

環境

Windows10 20H2
WSL2 Ubuntu20.04 LTS (ホスト)
VMware Workstation 16
Ubuntu20.04 LTS (NAT)(ゲスト)

netplanでipを固定する

/etc/netplan/hogehoge を編集
nameserversのアドレスは任意のDNSサーバーを記述。

network:
  ethernets:
    ens33:
      dhcp4: false
      addresses: [192.168.47.100/24]
      gateway4: 192.168.47.2
      nameservers:
        addresses: [1.1.1.1,1.0.0.1]
  version: 2
  renderer: networkd

ゲストと通信するための調整

上記設定にて、ネットワークデバイスのaddressesは、C:\ProgramData\VMware\vmnetdhcp.conf のrange外かつsubnet内にしておく。
ゲートウェイC:\ProgramData\VMware\vmnetnat.conf のNAT gateway address。

また、Windowsコントロール パネル\ネットワークとインターネット\ネットワーク接続から、使用しているネットワークデバイスを選択、プロパティから共有タブを開き、共有するアダプタにVMnet8を選択する。
その後、VMnet8のIPv4のプロパティから、アドレスを vmnetdhcp.conffixed-address と同じにする。

編集が終わったら、最後に

netplan try

エラーでなければ(serviceがstopとか出てもENTER押してacceptedなら問題ない)

netplan apply

2021.04.01 追記

netplan tryするとエラーが出る
環境: 無線LAN接続

Job for netplan-wpa-wlp3s0.service canceled.

An error occurred: Command '['systemctl', 'stop', 'systemd-networkd.service', 'netplan-wpa-*.service']' returned non-zero exit status 1.

Reverting.
Warning: Stopping systemd-networkd.service, but it can still be activated by:
  systemd-networkd.socket

以下の方法で解決した。

systemctl stop netplan-wpa-*.service

としてから、直接

netplan try

エラーがないことを確認したら、
またnetplan-wpa-*.serviceが起動しているので手動で止める。

systemctl stop netplan-wpa-*.service

直接applyする。

netplan apply

(注意: サービス名は、起動中なら*で補えるが、停止中はそうはいかないようなので、stopした後でstatusなどしたいときは*を使わない。)

systemd-resolvedでdnsを設定

/etc/systemd/resolved.conf を編集

先のnetplanで書いたnameserversのアドレス。
NetworkManagerとは共存できないらしい?のでnetworkmanagerはdisableであると確認しておく。

[Resolve]
DNS=1.1.1.1
FallbackDNS=1.0.0.1

バグ(?)対策

/etc/resolve.conf -> ../run/systemd/resolve/stub-resolv.conf のリンクを張りなおす

unlink /etc/resolv.conf
ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf

これでおしまい。
ゲストに接続するには、普通に、ゲストでip aして出てくるアドレスにリクエスト投げればよい。

おまけ

タイムゾーンの設定

timedatectl set-timezone Asia/Tokyo

JavaScriptで動体検知PWAを作ってみた【OpenCV.js】

ぶぉんなたーれ!クリスマスはリベといっしょにパネトーネを食べたい人生だった…。

クリスマスといえばサンタさん。
その姿を一目見てやろうと夜更かしすると,サンタさんは来なくなります。
夜更かしは悪い子のすることなので。

悪い子にならずに,サンタさんの姿を確認したい…

ということで,カメラに映る動くものを検知するサイトを作りました。

この記事は SLP KBIT Advent Calendar 2020 25日目の記事です。

adventar.org

こんな感じ。

f:id:yassi-htn:20201224185410p:plainf:id:yassi-htn:20201224185407p:plain
動作例

ここで動作します。
ただし,いまのところ(2020.12.25現在),DesktopのfirefoxではA2HS(ホーム画面に追加)が利用できません。利用可否の確認
代わりに,chrome系のブラウザやEdgeを使うと利用できます。モバイル版ならfirefoxでも利用できます。

https://yassi-github.github.io/move-capture-pwa/

コードはこちら。

https://github.com/yassi-github/move-capture-pwa

PWAとは

developer.mozilla.org

PWAとは,プログレッシブウェブアプリの略です。
ざっくりと表現すると,DLしてインストールするよくあるアプリのようなWebアプリケーションのことです。

  • URLを持つため簡単にアクセスできかつ,オフラインでも利用できる。
  • 端末にDL,インストールしてアプリアイコンから起動することができる。
  • さらに,カメラや位置情報など端末の周辺機器を利用した動作も可能。

他にも,通知やレスポンシブであることなどPWAに関する機能はありますが,今回実装したのは以上のようなこと。

PWAは,その名前にあることから,プログレッシブであることが重要です。
つまり,先ほど挙げた機能を使うためにはブラウザーの機能を利用することが必要ですが,ブラウザーが古くて機能が制限された状態でも,一応は使えるようであることが重要ということです。

…今回の場合はカメラが使えないと何もできないのでプログレッシブではないですね。あ,じゃあPWAではないのでは…?

OpenCV.jsについて

docs.opencv.org

カメラの動きを検知するには,フレームごとの画像処理が必要です。
画像処理といえば,OpenCVですね。
今回はWebアプリケーションなので,JavaScript版となるOpenCV.jsを使います。

これを使うには,releasesからopencv-<virsion>-docs.zipをDLし,その中にあるopencv.jsをhtmlでスクリプトとして読み込みます。
解凍したディレクトリにはたくさんのディレクトリがありますが,opencv.jsは直下にあるので無駄に探し回らないように注意です(一敗)

詳しい手順は,公式サイト(英語)の使い方チュートリアルをご覧ください。
サンプルコードも豊富にありますし,英語を読まなくても雰囲気でわかるかと思います。

実装部分

参考サイトは大体以下の通りです。

プログレッシブウェブアプリ | MDN

OpenCV: Image Thresholding

OpenCV: Getting Started with Videos

鳥取大学さんのサイトには,OpenCVのページを日本語に翻訳したものがあります。Python版ですが。

画像のしきい値処理 — OpenCV-Python Tutorials 1 documentation

PWAの機能

サービスワーカー

サービスワーカーを設定することで,キャッシュにhtmlや何やらを保存し,オフラインでの利用が可能になります。
リソースの取得リクエストを,サーバーの代わりにサービスワーカーが受け取ることでオフライン利用や高速な再読み込みが実現します。

この機能を使うには,サービスワーカーの動作を記述したjsファイルを作成し,それを登録させる必要があります。それにより,初回起動時のインストールの設定,リクエストの受け取りと応答設定,データ更新時のキャッシュ削除設定などを記述できます。

sw.js

let cacheName = 'v1';

self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll([
                '/move-capture-pwa/',
                '/move-capture-pwa/index.html',
                '/move-capture-pwa/main.js',
                '/move-capture-pwa/opencv.js'
            ]);
        })
    );
});

このようにサービスワーカーを設定したら,main.jsのほうで

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('sw.js')
  .then(function(register) {
    if (register.installing) {
      console.log("sw installing");
    } else if (register.waiting) {
      console.log("sw installed");
    } else if (register.active) {
      console.log("sw active");
    }
  }).catch(function(error) {
    console.log("Registration failed with " + error);
  });
}

と書くことで,キャッシュストレージが利用できるようになります。

リクエストをサービスワーカーが応答するなど,他の設定はすべてsw.js(サービスワーカーのjsファイル)のほうに記述していきます。

ホーム画面に追加(A2HS)

モバイルブラウザでQiitaを見ると画面下に出てくるあれです。
これで,ネイティブアプリのように直接起動でき,見た目もフルスクリーンにしたりできます。

この機能を実現するためには,JSON形式のマニフェストファイルを作成する必要があります。
なお,ここで必須となるアイコン画像ですが,サイズが144x144以上でないといけないので注意です。

マニフェストファイルにはこのように,アプリとしての動作設定を記述します。

manifest.webmanifest

{
    "backgroung_color": "LightGreen",
    "display": "fullscreen",
    "icons": [
        {
            "src": "icon/icon256.png",
            "sizes": "256x256",
            "type": "image/png"
        }
    ],
    "name": "Move capture",
    "short_name": "Detect",
    "start_url": "/move-capture-pwa/index.html"
}

その後,main.jsでこのように記述すると,ホーム画面に追加機能が使えるようになります。
(この部分はMDNさんの解説コードをそのまま使っています…)

main.js

let deferredPrompt;
const addBtn = document.querySelector('.a2hs-button');
addBtn.style.display = 'none';

window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;
  // Update UI to notify the user they can add to home screen
  addBtn.style.display = 'block';

  addBtn.addEventListener('click', (e) => {
    // hide our user interface that shows our A2HS button
    addBtn.style.display = 'none';
    // Show the prompt
    deferredPrompt.prompt();
    // Wait for the user to respond to the prompt
    deferredPrompt.userChoice.then((choiceResult) => {
        if (choiceResult.outcome === 'accepted') {
          console.log('User accepted the A2HS prompt');
        } else {
          console.log('User dismissed the A2HS prompt');
        }
        deferredPrompt = null;
      });
  });
});

通知

通知を許可するように求めるダイアログを見たことがあるかもしれませんが,それのことです。

別のタブを見ていても通知が届くので,強力ですが煩わしいかもしれません。

そして,通知APIは2種類あります。
Notificationのほうは簡単に実装できますが,
Push通知のほうはサーバーを使う必要があるようで,複雑です。

なお,今回は実装していません。

カメラの使用

PWAの機能というわけではないのですが,端末の周辺機器を使うことはPWAに関連する事項なので,ここで紹介します。

getUserMediaでvideo要素をオンにして,カメラを利用しています。

ここで内カメラと外カメラのどちらを使うかや,カメラのアスペクト比を変えられるらしいのですが,うまくいかず。。

main.js

// video要素にカメラをストリーム?
function startCapture(opt) {
  if (video.srcObject != null) {
    // stop both video and audio
    stopButton.click();
  }

  let optionSetting = {
    video: true,
    audio: false,
    facingMode: null,
    width: {
      ideal: window.screen.width/2
    },
    height: {
      ideal: window.screen.height/2
    },
    aspectRatio: window.screen.width/window.screen.height
  };

  if (opt === 1) {
    optionSetting.video.facingMode = "environment";
  } else {
    optionSetting.video.facingMode = "user";
  }

  navigator.mediaDevices.getUserMedia(optionSetting)
  .then(function(stream) {
    video.srcObject = stream;
    video.play();
  })
  .catch(function(err) {
    console.log("An error occurred! " + err);
    document.body.innerHTML = 'Camera is NOT available!';
    window.stop();
  });
}

optionSettingがうまく動いていない模様…?

動体検知の処理部分について

ここは昔にPythonで動体検知スクリプトを作ってみたときのコードをベースにしました。
OpenCV.jsでは使えない関数(accumulateWeighted)がありましたが,なくても検知はできているのでおそらく問題ないです。

輪郭を検出し,それの画素?数を前のフレームと比べ,変化量を見ています。
変化量が閾値以上なら検知する仕組みです。

また,動きの検知はオンオフが激しいため,ある程度連続して動いた場合にだけ検知するようにしました。

SlackのWebhookを使って通知させる部分について

api.slack.com

動きを検知したら,Webhookを通してSlack botに通知させられるようにしました。

検知時の画像を表示させたかったのですが,画像はブラウザ上で一時的に表示させているだけなので無理でした。

通知メッセージはJSON形式で自由に設定できるのですが,わかりやすい例があったのでここで検証しながら作成しました。
いろいろなテンプレートを選べるので便利です。

main.js

function sendSlackNotify() {
  const data = {
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*Detected*\n" + new Date().toTimeString().replaceAll(' ', '_') + "\n"
        },
        "accessory": {
          "type": "image",
          "image_url": "https://" +document.location.host +"/move-capture-pwa/icon/icon64.png",
          "alt_text": "Detected camera image"
        }
      }
    ]
  }

  const option  = {
    "method": "POST",
    "body": JSON.stringify(data)
  }

  const url = document.getElementById('webhook-url').value;

  fetch(url, option)
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => console.log('Success!!:', data))
  .catch((error) => console.log('Error!!:', error));
  // SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
}

コメントにあるように,なぜか構文エラーが出ちゃうんですよね。JSONの文法がミスってるのか…?まあ動くのでヨシ!

UIについて

面倒なのでほぼ触ってないです。


長くなってしまいましたが,今回は以上です。

はしがき

動作の軽量化やインタフェースの変更など,課題はたくさんあるので,また改善していきたいと思います。

では,これで2020年のアドベントカレンダーが無事終了となります。
皆さん,お疲れ様でした。ありがとうございました。
よいお年を!

JavaScriptでライフゲームを作った

この記事は SLP KBIT Advent Calendar 2020 2日目の記事です。

adventar.org

ライフゲームをつくりました。紹介と振り返っての感想です。
GitHub Pagesで公開してます。

https://yassi-github.github.io/LifeGame/

ライフゲームとは

ちょっと昔の数学者が考えた,生物集団の生死を簡単に再現するもの。らしい。
集団は過疎でも過密でも死んじゃう。ほどよい具合だと誕生したり生存できたりする。考えた人すごい。

で,これ,作られる模様がいいんですよ。ドット絵のピコピコ感が非常によき。
さらに,作られるパターンに名前があって,宇宙船とかパルサーとかSFみがあってすき。シュシュポッポ列車なんてのもあるみたい。かわいい。

f:id:yassi-htn:20201130215616g:plain
パルサー - Wikipediaより

こういうのをつくって遊べるのをつくりました。

JavaScriptでの実装

コードはここに置いてます
GitHub - yassi-github/LifeGame: 古典的で簡単なシミュレーションゲーム

機能紹介

  1. お絵かきの要領で,フィールドにドット絵を描きます。
  2. PLAYボタンでシミュレート!固定か滅亡するまでつづきます。

マスの数は3から30まで変えられるようになっています。

シミュレート中に変更を加えることもできます。

作成過程

  1. お絵かきをするコードを参考に,ドット絵描画ができるようにする
  2. 配列とドット絵をリンクさせる
  3. アルゴリズムを実装する
  4. 動作の不具合やミスを修正
  5. インタフェースを改善

って感じです。

参考にしたコードは「JavaScript お絵かき」などと検索してでてきたサイトで解説してたものです。
そもそも,イベントリスナーってなんぞ?みたいに,JSの基本的な書き方がよくわかっていないので,全体的な形がわかると(テンプレ的に使えるので)すごく助かる。キャンバス要素なんて存在程度しか知らなかったし,自分で暗中模索して時間つぶすぐらいなら,基礎となる手法は既存の手法を真似るのがいいよね…。

ハマったとこ

  • 開発ツールの仕様

2次元配列の値を変更する過程を出力しようと,ループ中にconsole.logして開発ツールで見たら,全部の出力が最終結果と同じになってる。
Node.jsで動かしたら正常に表示されるのに…。

試行錯誤した結果…

開発ツールのコンソールで出力される2次元配列は,そのままだとタブで閉じられてるまま。この状態では値が入っていない?みたいで,タブを開いた時点の状態が表示される仕様という認識に至った。

  • 2次元配列のコピー,比較

concatだと1次元しか対応してない。でもエラーは出ないしコピーはできてるので問題ないと判断してしまった。後から値を変更したらコピー元とコピー先両方に変更が加えられてしまうのだった。

比較も,イコールで結ぶのはダメだし,どうしたものかと思っていた。Google先生に聞くとJSON.parse(JSON.stringify())を使えばいいと。

結果,コピーも比較もJSONさんで解決。

  • マスサイズの更新

入力値に基づいてマスサイズを決定するため,どうやってJSを更新させよう…?とグローバル領域に変数置いたりごちゃごちゃしていた。

結局,パラメータによってページごとに決定するものになった。ページ自体を遷移させるのは気が進まないが,他に方法見つからなかったので仕方なし。

おわりに

動くようになったときはプログラミング楽しい~~~~!!!ってなったのでこの気持ちを忘れないように精進してまいります。

まだアドカレの担当残ってるんでそっちも進めなきゃ…終わるかな…

CentOS8を入れてハマったとこメモ

そろそろLinuxを触り始めてみたいと思ったので,
最近出たらしいCentOS 8をノートPCに入れてみた.
インストールからハマったので,この際メモを作っておこうと思います.

問題点と解決法を(できるかぎり)順次載せていく予定です.

1. OSインストール時にエラーが出て入れられない

解決法: ddモードにする

インストールメディアはUSBで,Rufusを使って作成.

USBから起動する途中でハングアップしちゃうので,ファイルをDLし直したりしてました.(SHA-256のCHECKSUMをSHA-1での結果と比較しちゃってたのは内緒)

Rufusでメディアを作成するときに,iso(推奨)にするかddにするか を聞かれると思うので,ddモードに変更すると成功しました.

2. VNCクライアントからアプリの操作ができない

解決法: Xorg を使うようにする

Tiger vnc server を使用.Windows の UltraVNC Client からローカル内で直接つなぎます.

dnfでインストールしてパスワード設定,設定ファイルのコピーとUSERの書き換え,Firewalldの設定を終えていざ接続…!お,繋がった!が,アプリが起動しない…?デスクトップ画面は動くのに,ターミナルやブラウザなどのアプリは表示されない…ホストでアプリが起動してしまいます.
似たような症状は見つけられず(ググり力が試されているのか)VNCの解説記事などに書いてある設定変更を試してみまくった結果,起動不能やエラーの発生と悪化しました.systemctl start vncserver@:[ディスプレイ番号]vncserver :[ディスプレイ番号] で起動されるプロセスが一致しないのは異常だったんですね…

そんな中,VNC Server構築方法が書いてある記事を発見し,救われました!GNOMEに原因があったみたい?です.
記事中にあるように,/etc/gdm/custom.confWayland~コメントアウトするだけです!SELinuxのくだりはしなくて大丈夫でした.

他の設定ファイルを書き換える必要はありません.デフォルトでおkです.また,/usr/lib/systemd/system/vncsever@.serviceのコメント通り,/etc/systemd/systemにコピーしたもののを書き換えて使います.なお,ディスプレイ番号ごとに設定ファイル名を作成する必要はないです.参考(RedHat公式サイト)

これで,クライアントでアプリの起動が可能かつ,ホストの画面には影響しないようにできました.
今度はディスプレイ番号1に繋げない,なんてことも発生しています.ホストにおいて先ほどと同じようにアプリが表示されないこともありましたが,ログアウトすることで対処できました.おそらく,ディスプレイのセッションが2つ起動していることが関係しているのだと思われます.ps -C Xorgps -ef | grep Xorgで確認できます.kill -9 [pid]で消しても,vnc繋げたら2つに戻っちゃうのでどうしたものか…クライアントの終了方法をログアウトにしてるんですけど,それがいけなかったり…?うーん…

日本語入力の切り替えショートカットが設定不可

winではcapslock英数キーで日本語と英数の切り替えができていたので,同じようにしたかった.
キーの割り当てをしたいのにcapslockキーが割り当てられない…なんでやねん。
半角全角キーは使えるんだけどな…謎.


とりあえずここまで.