kWatanabe 記事一覧へ

kWatanabe の 技術帖

某企業でOSや仮想化の研究をやっているインフラ系エンジニア。オンプレとクラウドのコラボレーションなど、興味ある技術を綴る。

オムロン製UPS「BY50S」を Debian 11 で使う

  • オムロンUPSBY50S」は比較的安価で使いやすい製品だが、同梱の Linux 向けの自動シャットダウンツールは RHEL 系しかサポートしていない。
  • BY50S向け自動シャットダウンツールの「SimpleShutdownSoftware」を Debian 11 (bullseye) で使えるようにした。

BY50S は DebianLinuxをサポートしていない

オムロン社製UPSの「BY50S」は、実売価格18000円ほどで品質も必要十分なので、自宅サーバや録画機器などで使いやすい。

BY50S 向けの自動シャットダウンツールとしては、エージェントとマネージャで複数のサーバを統合管理できる「PowerAct」と、UPSと直接会話するデーモンで単体のサーバのシャットダウンだけを行う「SimpleShutdownSoftware」が提供されている。

しかし、いずれも対応OSが WindowsRHELLinuxmacos で、DebianLinux はサポート外となっている。

socialsolution.omron.com

一方、 kWatanabe の自宅サーバDebian で稼働している。そこで、「SimpleShutodownSoftware」を Debian で使うための手順を整理した。

Debian 11 で SimpleShutdownSoftware を使う

検証環境

  • Debian 11.4 (bullseye) amd64
  • Simple Shutdown Software 2.41

手順

必要なパッケージを導入する。

# apt install wget unzip libusb-dev build-essentials

オムロン社のWebページから「Simple Shutdown Software Ver 2.41(Linux版)」を入手し、 /usr/local/src に配置する。

# cd /usr/local/src
# wget https://socialsolution.omron.com/jp/ja/products_service/ups/support/download/soft/sss/SimpleSoftwareVer241_Linux_X64.zip
# unzip SimpleSoftwareVer241_Linux_X64.zip
# mv SimpleSoftwareVer241_Linux_X64 SimpleShutdownSoftware

Release/master 配下に RHEL7 向けのバイナリが保存されている。これが動作する環境ならば、そのまま利用してもよい。リビルドする場合には、以下のように Source ディレクトリに移動してビルドする。

# cd ./SimpleShutdownSoftware/Source
# make -j`nproc`
# cd ../Release
# ls master
AgentManager*       Shutdown.cfg*    SimpleShutdownDaemon*  UsbPort-Libusb.so.1.0.0*  my_kvm.sh*  ssdDaemon*
S99SimpleShutdown*  SimpleShutdown*  UsbPort-HID.so.1.0.0*  config.sh*                omronctl*   ssdService*

バイナリの配置と設定ファイルの生成のために、インストールスクリプトを実行する。

# ./install.sh
Do you agree this license? [ y/n ] y

設定スクリプトを用いて、ファイルをしかるべき値に設定する。我が家の場合は /srv/scripts/emergency/emergency-shutdown.sh を叩くと、全てのサーバがクリーンにシャットダウンできる仕組みを整えているので、これをキックする設定を行う。

# /usr/lib/ssd/master/config.sh
...
********************************************************************************
[Reconfirm shutdown parameter of the marine (Master Agent)].
********************************************************************************
1. Port Selection Mode:               Semiautomatic Mode(Only select all USB device)
2. Select USB communication mode :    Libusb
3. AC fail Delay time (Sec):          5
4. External Command Line:             /srv/scripts/emergency/emergency-shutdown.sh
5. External Command needs time (Sec): 60
6. OS Shutdown needs time (Sec):      60
7. Send Message to login users:       Enable
8. System closing mode :              Shutdown
9. Virtual Server shutdown mode :     Shutdown by Linux System
********************************************************************************
Select Number:1

これで、一見インストールされデーモンが起動するようにみえるが、自動起動がうまく設定されておらず、システムを再起動すると起動に失敗する。そのため、Debian 11 の環境に合わせて処置する。

まず、インストールスクリプトで導入された壊れている rc ファイルを削除する。

# find /etc/rc?.d | grep SimpleShutdown | xargs rm -v
'/etc/rc1.d/S99SimpleShutdown' を削除しました
'/etc/rc2.d/S99SimpleShutdown' を削除しました
'/etc/rc3.d/S99SimpleShutdown' を削除しました
'/etc/rc5.d/S99SimpleShutdown' を削除しました

削除した rc ファイルに変わる systemd ユニットファイルを作成する。

# vim /etc/systemd/system/SimpleShutdown.service
[Unit]
Description=Simple Shutdown Software

[Service]
Type=forking
ExecStart=/usr/lib/ssd/master/ssdService
KillSignal=SIGUSR1
Restart=on-abort
RestartSec=3

[Install]
WantedBy=multi-user.target

作成したユニットファイルを読み込ませて、システムを再起動する。

# systemctl daemon-reload
# systemctl enable SimpleShutdown.service
# reboot

再起動後、デーモンが起動していることを確認する。

# systemctl status SimpleShutdown
● SimpleShutdown.service - Simple Shutdown Software
     Loaded: loaded (/etc/systemd/system/SimpleShutdown.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2022-07-23 13:21:08 JST; 14min ago
    Process: 683 ExecStart=/usr/lib/ssd/master/ssdService (code=exited, status=0/SUCCESS)
   Main PID: 699 (ssdService)
      Tasks: 1 (limit: 154419)
     Memory: 2.1M
        CPU: 873ms
     CGroup: /system.slice/SimpleShutdown.service
             └─699 /usr/lib/ssd/master/ssdService

 723 13:21:08 marine systemd[1]: Starting Simple Shutdown Software...
 723 13:21:08 marine systemd[1]: Started Simple Shutdown Software.
 723 13:21:08 marine SimpleShutdown[699]: Shutdown Agent Start.

UPSを接続して、認識していることを確認できたら、UPSの電源を抜いて動作確認する。

# lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 0590:0081 Omron Corp. BY50S
Bus 001 Device 005: ID 046b:ff10 American Megatrends, Inc. Virtual Keyboard and Mouse
Bus 001 Device 004: ID 046b:ffb0 American Megatrends, Inc. Virtual Ethernet
Bus 001 Device 002: ID 046b:ff01 American Megatrends, Inc. Virtual Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

まとめ

  • BY50S 同梱の SimpleShutodownSoftware を Debian 11 で使えるようにする手順を整備した。
  • systemd でデーモンを制御できるようにしたが、 udev などで頑張ることもできるかもしれない。

RockyLinuxとCentOS/RHELで ansible_os_family の挙動が異なる件

  • CentOSRHELで問題無い Ansible Playbook が RockyLinux が動かなかった
  • 原因を調べると ansible_os_family の挙動が異なることが分かった
  • 【追記】この問題を修正するパッチが投稿されている模様

コトのはじまり

お手製の Ansible Playbook を Rocky Linux 8 に適用しようとしたときだった。

内容は大したことなくて、 /etc/yum.conf *1 にプロキシ設定を加えて、パッケージをアップデートするだけのものなのだけど、CentOS8 でも RHEL8 でも動くのに RockyLinux8 では動かない。

...
TASK [basicpack : YUMプロキシ設定の追加] *********************************************************************************************
changed: [centos8.local]
skipping: [rocky8.local]
...

この時、RHEL系なのか、Debian系なのかを振り分けるために when 句で判断してるんだけど、その結果が false になってしまっているようだった。

RockyLinuxは、CentOS と同じように RHEL のクローンのはずなのに、どうしてこういうことが起こるのか。

検証

検証環境

  • RockyLinux 8.5
  • Ansible 2.9.16

when を眺める

該当の task は以下のように実装してある。

- name: YUMプロキシ設定の追加
  lineinfile:
    dest: /etc/yum.conf
    state: present
    regexp: '^proxy'
    line: 'proxy=http://{{ server_yum_proxy}}/'
    create: yes
  when: "ansible_os_family == 'RedHat' and server_yum_proxy is defined"

server_yum_proxy は、環境ごとに切り替わる vars でプロキシサーバのホスト名とポート番号を指定するようにしている。これが定義されていて、且つ、ansible_os_family ( 参考 ) で RedHat 系列であれば*2、プロキシを通すように設定している。

RHELの時はここは RedHat となっていた。CentOS でもずっと同じだったので、てっきり RockyLinux でも同じだと思っていた。

試してみる

動作検証用に以下のような Playbook を用意しているので、これで確認してみる。

- name: OS確認
  debug:
    msg: "{{ ansible_distribution }}{{ ansible_distribution_version }} , {{ ansible_distribution }}{{ ansible_distribution_major_version }} , {{ ansible_distribution }} , {{ ansible_os_family }}"

結果は以下。

TASK [ping : OS確認] **********************************************************************************************************
ok: [rocky8.local] => {
    "msg": "Rocky8.5 , Rocky8 , Rocky , Rocky"
}

なんと! ansible_os_familyRedHat ではなく Rocky になっている。ここは、互換じゃないのか!

対応

というわけで、問題の when 句を以下のように改修した。

  when: "(ansible_os_family == 'RedHat' or ansible_os_family == 'Rocky') and server_yum_proxy is defined"

所感

  • RockyLinux は RHEL 互換だと思っていたのに、そうでもないところがあると分かった。
  • CentOS で稼働するサーバをリプレイスする際にはちゃんと検証しないとダメだ。

追記

どうやら、これは不具合らしく、この問題に対応するパッチをマージする pull request が投稿されていた。

github.com

上記の通り、私の環境は 2.9.16 (Debianのbuster-backports で提供されているもの)でのものなので、最新版では正しく RedHat となるかもしれない。

*1:時代は dnf なのは分かってるんだけど、ついつい使えるから使っちゃう。

*2:今思えば、 /etc/yum.conf の存在を確認して処理するのが正解のように思える。

fastapi-code-generatorで任意のフォーマットのコードを生成する

  • FastAPI 向けのソースコードを OpenAPI Spec から生成する際、実装済み機能のコードが消えてしまわないよう、外だしにする仕組みを考えた

FastAPIとOpenAPI

先日、以下の記事で楽に自作 API を作れる FastAPI が動く環境を整備した。

kwatanabe.hatenablog.jp

さらに調べているうちに OpenAPI Spec から FastAPI 向けのコードジェネレータである「fastapi-code-generator」があることを知った。

OpenAPI ( 別名 Swagger ) は、API 仕様を策定するための規格で、この規格に準じた spec ファイルを作成するとドキュメントの自動生成や、フロントエンド、バックエンドのソースコードの自動生成ができる。

f:id:kWatanabe:20211213004737p:plain

github.com

swagger.io

これはすこぶる便利だが、少し使ってみると困ったことが起こることに気づく。

OpenAPI Spec からのコード生成で困ったこと

fastapi-code-generator は OpenAPI Spec を与えると、ソースコードのスケルトンを生成してくれる。以下のように使う。

$ fastapi-codegen -i <APISpecファイル> -o <出力先ディレクトリ>

その結果、スケルトン本体の main.py と、クラス化したパラメータを集めた models.py が生成される。例えば、/token への POST メソッドを用いる API を定義した OpenAPI Spec を与えると以下のようになる。

from __future__ import annotations
from typing import Optional, Union
from fastapi import FastAPI, Header
from .models import TokenPostRequest

app = FastAPI(
    ...(省略)...
)

@app.post('/token', response_model=TokenPostResponse)
def post_token(body: TokenPostRequest = None) -> TokenPostResponse:
    """
    トークンの取得
    """
    pass

ここの pass のところに実際のAPI処理を記述していくことになるのだけども、実装した後に新たなAPIを追加して fastapi-codegen すると、また pass に逆戻りしてしまう。また、グローバルなスコープに別のライブラリやDBアクセスのコードを足すと、それも消えてしまう。

仕様が変更された場合には致し方ないが、全く無関係な API が追加された場合にも、手作業でコピペする羽目になるのはいただけない。main.py にはインタフェースのみ記述して、実際の処理は別ファイルに外出ししたいし、グローバルなスコープのコードは維持したい。

コードテンプレートを使う

fastapi-code-generator では、-t オプションでコードテンプレートを指定できる。コードテンプレートは jinja2 形式で記述し、生成するコードのフォーマットを任意に定義できる。公式のドキュメントページに使える変数の一覧と、デフォルトテンプレートが記載されている。

koxudaxi.github.io

この仕組みを使えば、グローバルなコードはテンプレートに直書きすればいいし、APIの処理ルーチンは passdo_post_token() みたいなメソッドにし、実体を別ファイルに作成して import すればよい。

何も考えずに使うと失敗する

メソッド名は operation.function_name、引数は operation.snake_case_arguments で取れる。そこで、デフォルトテンプレートの pass に相当する箇所を素直に変更する。

return do_{{operation.function_name}}( {{operation.snake_case_arguments}} )

その結果は以下。

return do_post_token( body: TokenPostRequest = None )

期待しているモノと違う。operation.snake_case_arguments は、型ヒントやデフォルトパラメータまで含まれる様子。リクエストヘッダが付くと更に複雑になる。

return do_post_token(
    authorization: str = Header(..., alias='Authorization'),
    body: TokenPostRequest = None
)

このままでは Syntax Error で動かなくなる。

jinja2の中で引数をパースする

素直に使うと失敗するので、変数名だけ抜き出してくる必要がある。単純に , で split すると、ヘッダの Header(..., が悪さをしてうまく拾えない。力業だけど以下の方法で回避する。

  1. , で split してイテレーションする
  2. 要素に : が含まれるか調べ、無ければ次の要素に移る
  3. 要素に : が含まれるなら : で split して第一要素を変数名として扱う

実装するとこんな感じ。

return do_{{operation.function_name}}(
    {% for arg in operation.snake_case_arguments.split(',') %}
    {% if ':' in arg %}
    {{ arg.split(':')[0] }}={{ arg.split(':')[0] }},
    {% endif %}
    {% endfor %}
)

先の例だとこんな感じになる。

return do_get_token(
    authorization=authorization,
    body=body,
)

実体を作成する

後は、処理ルーチンの実体を実装する。例えば handler.py として以下のように用意する。

from models import *

def do_get_token (**kwargs):
    return TokenPostResponse( token = 'XXXX' )

上記の例では、手抜のために可変長引数をつかっている。もちろん、下記のように真面目に書いてもいい。

from models import *

def do_get_token (authorization, body):
    return TokenPostResponse( token = 'XXXX' )

そしてこれを import するコードをテンプレートの冒頭に追記する。

from handler import *

この from~ と同じように、DBアクセスなどのグローバルなコードがあるのなら、テンプレートの任意の箇所に直接 Python コードを記述する。

検証

検証環境

テンプレート

templ/main.jinja2 を以下のように作成。殆どは、デフォルトのテンプレートのまま。

from __future__ import annotations
from fastapi import FastAPI
{{imports}}
from handler import *

app = FastAPI(
    {% if info %}
    {% for key,value in info.items() %}
    {{ key }} = {{ value }},
    {% endfor %}
    {% endif %}
    )

{% for operation in operations %}
@app.{{operation.type}}('{{operation.snake_case_path}}', response_model={{operation.response}})
def {{operation.function_name}}({{operation.snake_case_arguments}}) -> {{operation.response}}:
    {%- if operation.summary %}
    """
    {{ operation.summary }}
    """
    {%- endif %}
    return do_{{operation.function_name}}(
        {% for arg in operation.snake_case_arguments.split(',') %}
        {% if ':' in arg %}
        {{ arg.split(':')[0] }}={{ arg.split(':')[0] }},
        {% endif %}
        {% endfor %}
    )
{% endfor %}

----- 2021/12/13 21:50 追記 -----

ドキュメントページのサンプルにあるものをそのまま使うと、AttributeError が起こる。理由は FastAPI コンストラクタを定義する以下の箇所。

    {{ key }} = "{{ value }}",

テンプレート指定なしでコードを生成すると、この value はリストとなるところらしく " で囲って str にしてしまうと処理に失敗する。なので、以下が正しい。

    {{ key }} = {{ value }},

----- 追記終わり -----

処理の実態を記述した外部ファイル

from models import *

def do_get_token (**kwargs):
    return TokenPostResponse(token = 'XXXX' )

コードを生成

$ fastapi-codegen -t ./templ -i <OpenAPI Spec> -o .

生成されたコード

main.py

from __future__ import annotations
from typing import Optional, Union
from fastapi import FastAPI, Header
from handler import *
from .models import TokenPostRequest, TokenPostResponse

app = FastAPI(
    ...(省略)...
)

@app.post('/token', response_model=TokenPostResponse)
def get_token(body: TokenPostRequest = None) -> TokenPostResponse:
    """
    トークンの取得
    """
    return do_get_token(
        body=body,
    )

models.py

from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, Field

class TokenPostRequest(BaseModel):
    name: str = Field(..., description='username')
    key: str = Field(..., description='password')

class TokenPostResponse(BaseModel):
    token: str = Field(..., description='token')

動作確認

Gunicorn/Uvicorn で動かして、curl で叩いてみる。

$ curl -s -X POST http://localhost:8000/token
{"token":"XXXX"}

うん。とりあえず動いている感じ。

FastAPI と Uvicorn で REST API 対応のスクリプトを簡単に動かす

f:id:kWatanabe:20211204145741p:plain https://fastapi.tiangolo.comより

  • 私生活の情報をAPI化すれば、世にある監視ツールで快適な生活を送れるのでは考えた
  • プライベート情報を公開するのは怖いので、ローカルでサクッとAPIを作れる環境を用意した

API監視による快適生活

以前、Zabbix で某ワクチン接種サイトの更新を拾って、Slack で飛ばす環境を構築した。

kwatanabe.hatenablog.jp

その後、この環境を育てていって、今は株価や天気を世の中の公開APIから拾って、Slackへ通知する仕組みを確立したのだけど、ふと私生活の情報をAPI化すればもっと捗るのでは無いかと思いついた。

調べてみたところ、FastAPI という素敵なWebフレームワークがあって、これを使うと Python でちょっとしたスクリプトを書く感覚で API が作れると分かったので、試してみることにした。

検証

検証環境

単純に動かす

APIアプリに特化したPythonフレームワークのFastAPIを導入する。 また、FastAPI で推奨となっている ASGI 対応 AP サーバの uvicornを併せて導入する。

fastapi.tiangolo.com

www.uvicorn.org

$ sudo apt install python3 python3-fastapi uvicorn python3-uvloop python3-httptools

ちなみに Debian 10 以前では FastAPI は apt で提供されていないので pip で導入する。

$ sudo python3 -m pip install fastapi

任意の場所にサンプルコードを設置。仮に、/srv/uvicorn とする。

$ sudo mkdir -p /srv/uvicorn
$ sudo vim /srv/uvicorn/main.py

内容はシンプルに、/ を GET すると Hello, World. が JSON で返ってくる API を作る。

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return { "Hello": "World" }

試運転。

$ cd /srv/uvicorn
$ uvicorn main:app --host 0.0.0.0 --port 8000

アクセスしてみる。

$ curl http://localhost:8000/
{"Hello":"World"}

以上おわり、楽チン過ぎる。

Uvicorn を Gunicorn のワーカーとして動かす

Uvicorn のドキュメントによると、 WSGI 対応 AP サーバの Gunicorn のワーカーとして動かすことが推奨されているので、Uvicorn を Gunicorn 配下で動くように変更する。

gunicorn.org

まず、Gunicorn を入れる。

$ sudo apt insatll gunicorn

なお、Debian 10 以前では、Python2 バージョンが入ってしまうので以下のようにする。

$ sudo apt install gunicorn3

Gunicorn と Uvicorn を連携させて、再度試運転。

$ cd /srv/uvicorn
$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

アクセスしてみる。

$ curl http://localhost:8000/
{"Hello":"World"}

いやっほー!

リバースプロキシ配下に置く

実運用では AP サーバ 単体を露出させることは無いだろうし、SSL の終端なども必要だろうということで、リバースプロキシとして nginx をかませる。 静的コンテンツとの区別やAPIバージョンごとにAPサーバを分けたい場合などのため、/api/v1 プレフィックスがついているものは Gunicorn へ飛ばすようにする。

nginx.org

nginx を入れる。

$ sudo apt install nginx

nginx.confserver ディレクティブに以下を追記する。

...
location ^~ /api/v1/ {
    proxy_pass http://GunicornサーバのIPアドレス:8000;
}
...

アプリで FastAPI クラスを作成する際にプレフィックスを指定する。

...
app = FastAPI( root_path = "/api/v1/" )
...

nginx につないでみる。

$ curl http://nginxのIPアドレス/api/v1/
{"Hello":"World"}

完璧すぎる。

CORS に対応させる

調子に乗って、もう一歩。今回のように Zabbix から監視したい程度ならともかく、普通はAPIを素で使うことはあまりないだろうし、大概は何らかのフロントエンドアプリと疎通することになると思うので、CORS へ対処する必要がある。

FastAPI では、オリジンとして許可するドメインFastAPI() クラスに与えることで解決できる。こんな感じ。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI( root_path = "/api/v1/" )

origins = [
    "http://excample.com",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/")
def read_root():
    return { "Hello": "World" }

おわりに

これで FastAPI を使って API アプリを作るための殻ができあがった。あとは好きに API を実装すればいい。

URIとHTTPメソッドによるルーティングはデコレータ (@app.get("/"))、リクエストボディのバリデーションは型ヒント、レスポンスはディクショナリを渡してあげれば (return { "Hello": "World" })、面倒な HTTP 通信やJSONのパースなどは全部やってくれる。

まさにちょっとした Python スクリプトを作る感覚で簡単な API は一瞬で実装できる。いい世の中になった。

KVMでTPMをエミュレートしてWindows11を動かす

  • KVM (Proxmox VE) で TPM 2.0 をエミュレートする環境を構築した
  • KVM (Proxmox VE) で TPM 2.0 をエミュレートしたVMを作り、Windows11を動かしてみた

Windows11のシステム要件

先日、Windows11がリリースされたが、その動作条件の厳しさに一部で話題になっている。

www.microsoft.com

上記の Microsoft のWebサイトから一部引用すると以下の通り。

項目 要件
CPU 1GHz で 2コア以上
Intel なら Coffee Lake 以降
AMD なら Zen+ 以降
RAM 4GB 以上
Storage 64GB 以上
Firmware UEFI / Secureboot 対応
TPM 2.0 以降

CPUの世代は、KabyLake も Zen も 5年前 のものだし、Windows10も継続提供されるので、「新しいOSを使いたければ、新しいマシンを使ってください」という主張はまぁ良しとする。(なんだかんだで動くでしょうし)

ただ、TPM が必須となっている点はちょっと気になるところ。VMTPMのエミュレーションって、可能だったかな。

VMでのTPMエミュレーション

ざっと調べてみただけでも、イマドキのハイパーバイザでは vTPM のサポートに特に問題はなさそう。

QEMU/KVM

QEMU/KVM では、swtpm というモジュールで vTPM をサポートしている。

qemu-project.gitlab.io

Hyper-V

Hyper-V では、Windows Server 2016 以降で vTPM をサポートしている。

docs.microsoft.com

VMware vSphere

VMware vSphere では、vSphere 6.7 以降で vTPM をサポートしている。

docs.vmware.com

検証

QEMU/KVM ( Proxmox VE ) でvTPM をエミュレートできる環境を構築して、Windows11 を動かしてみる。

…つもりだったのだけれども、モタモタしていたら、先日の ProxmoxVE のアップデートで vTPM が標準でサポートされてしまった。哀しみ。

手順

元々やろうとしていたこと

本来は以下のような手順で vTPM 環境を構築するつもりだった。

# swtpm socket --tpmstate dir=/var/run/tpm --tpm2 --ctrl type=unixio,path=/var/run/tpm/sock
  • QEMUの起動オプションに TPM 用のオプションを追加して起動
...
-bios ビルドしたUEFIファーム
-chardev socket,id=tpmc,path=/var/run/tpm/sock
-tpmdev emulator,id=tpm,chardev=tpmc
-device tpm-tis,tpmdev=tpm
...
今やればいいこと

しかし、最近のProxmox VEのアップデートで、vTPMを追加するメニューが追加されたので、今ならここをポチーするだけで終了。ポコペン

f:id:kWatanabe:20211009173605p:plain

一応、注意点としては以下。

  • BIOSOVMF(UEFI) にする
  • マシンは q35 にする
  • TPMバージョンは 2.0 にする

結果

f:id:kWatanabe:20211009181530j:plain
KVM上のWindows11

f:id:kWatanabe:20211009181551j:plain
vTPM2.0も有効

とんだ肩透かしだったけど。まぁ、こんな感じ。

Pythonでブラウザで動く "ぷよぷよっぽいもの" を作る

ぷよぷよeスポーツ×プログラミング

…という教材があるんです。殆ど読んでないんですが、曰く、以下のような代物のようです。

プログラミング学習環境『Monaca Education』において、セガが展開するアクションパズルゲームぷよぷよ』をプログラミング学習できる教材です。 製品版と同じ画像素材を利用して、世界中で使われるコンピュータ言語を使い、プロが使う開発環境で本物のプログラミングをお楽しみください。

https://puyo.sega.jp/program_2020/ より引用

puyo.sega.jp

面白そうじゃないですか。やってみようと思い、著作物利用許諾書に目を通すと、以下の記述がありました。

本利用条件に基づき改変した本プログラム等の著作権著作権法第21条から第28条までに定めるすべての権利を意味します)は、当社に無償にて譲渡及び移転するものとします。また、使用者は、当社が当該本プログラム等を、当社の判断で、商用であるか否かにかかわらず、いかなる方法にて、利用、公開、公表、販売もしくは頒布等を行うことができることを承諾するものとします。

https://puyo.sega.jp/program_2020/dl/puyo-programming-code.pdf より引用

この教材には、ソースコードに加え、画像ファイルなども含まれているようなので、改変した成果物を自由に扱える権利を与えるわけにはいかないのは分かるんですけど。その成果物の権利を逆に召し上げるっていうのはどうなんだ。

なんとなく、ムッとしたので、この教材に頼らずに、「ぷよぷよっぽい何か」を独力で作ってみようと思いました。

教材に頼らず独力で作る

どうやって作るか?

kWatanabe の知るブラウザのプラットフォームと言えば Flash でした。今は HTML5 か。

ただ、kWatanabe は Low-Level な開発を生業にしており、扱える高級言語Python くらいです。ブラウザで走る Python の処理系を探しました。

brython.info

Brython という JavaScript で実装された Python 処理系です。ライセンスは 3条項BSD

brython.jsbrython_stdlib.js という js ファイルをロードして、<canvas><div> で描画領域を用意してあげれば、あとはインラインで Python コードを生で書いていくことができます。

例えば以下のような感じでしょうか。

<!DOCTYPE html>
<html>
    <head>
        <title>Brythonサンプル</title>
        <meta charset="utf-8">
        <script type="text/javascript" src="brython.js"></script>
        <script type="text/javascript" src="brython_stdlib.js"></script>
    </head>
    <body onload="brython()">
        <canvas id="drawarea" width="192" height="384" style="border: 1px solid black;"></canvas>
...
描画先となる HTML5 のオブジェクトを定義する。
...
        <script type="text/python">
from browser import document
from browser import html
...
Python コードを記述する。
...
        </script>
    </body>
</html>

JavaScript と同様、HTMLファイルにソースコードを記述していく*1ので、中身はモロバレです。隠したいなら難読化などの、工夫が必要です。

あと、brython_stdlib.js に実装されていないモジュールは使えません。なので、Python 標準のモジュール以外は基本的に使用できないですが、それでも処理系としては JavaScript と比べてかなり協力です。

kWatanabe のように JavaScript をよく知らない人間や、 JavaScript にアレルギーを持つような人には、選択肢のひとつになるんじゃないでしょうか。

成果物

プレビュー

とりあえず、こんな感じになりました。連鎖で石を消すときの処理が何か怪しい?かも。

f:id:kWatanabe:20210919233648g:plain
Brythonで実装したぷよぷよっぽいもの

デモ

近日中に、期間限定でデモを置く予定です。

操作は以下のとおりです。

キー 効果
W ゲームの開始
A 石を左に動かす
S 押し続けている間、落下速度が増す
D 石を右に動かす
スペース 石を回転させる

ソースコード

GitHubで公開します。ライセンスは GPL Version.2 です。

github.com

所感

  • JavaScript を一切書かなくても、ブラウザのフロントエンドやアプリを作れる方法があることを知った
  • とはいえ、処理系のために数MBの js をロードさせるのは、ユーザフレンドリーではなさそうなので、使いどころは選ぶかも

*1:srcタグでロードすることもできますが、そのファイルを直接開かれると結局同じ。

Zabbixで任意のWebサイトの更新を監視する

  • ZabbixのHTTPエージェントで、Webサイトの更新を監視させる
  • 更新があれば、Slack で通知を投げて貰い、日々のWebサイトの巡回を省力化する

日々のWebサイト巡礼を機械に任せたい

デジタル世界にどっぷり浸かった kWatanabe は、株価情報とか、某ワクチンの予約サイトとか、いくつかのWebサイトの更新を日々確認している。毎日更新されるものなら良いけども、立会外分売の情報のようにそうとは限らないものも多い。

意味の無いルーチンワークはできるだけ機械に任せてしまい、人間様はもっと実になることに時間を使いたい。

Zabbix にWebサイトを監視させる

Zabbix はシステム監視ツールのひとつで、サーバやサービスの負荷状況や障害、その兆候となる事象を監視し、その結果を通知できる。

www.zabbix.com

Zabbix 3.x 以前は、サーバ本体やネットワークなどの機材の監視が主な機能だったが、Zabbix 4.0 以降ではシステムのクラウド化・API化に伴い、監視も通知も Web サービスを意識したものが実装されている。

この中での「HTTPエージェント」と「Slack通知」を使って、手軽にWebサイトの監視システムを構築できる*1

検証

ここでは、最もシンプルに「何でもいいのでWebサイトが更新されていたら Slack へ通知」する場合を考える*2。対象は、現在、日本でホットなWebサイトと思われる自衛隊の某ワクチン接種に関するサイトを監視してみる。

www.mod.go.jp

検証環境

Zabbixのインストール・Slackアカウントの作成は割愛。

Slack にアクセスするための権限を設定

Slack のアカウントから、ディベロッパー用サイトにアクセスする。

api.slack.com

「Create New App」→「From Scratch」を押下し、アプリの名称と連携させるSlack Workspaceを選択する。

アプリが作成されたら、「アプリ名」→「Basic Information」→「Add features and functionality」を押下して、Bots を選択する。

「OAuth & Permissions」→「Scopes」→「Add an Oauth Scope」を押下して、chat: write を追加する。その後、「OAuth Tokens for Your Workspace」の「Bot User OAuth Token」の「Copy」を押下し、テキトーなファイルに貼り付けておく。

Slack に通知用チャンネルを作成

Slack にログインし、Zabbix からの通知を受け取るためのチャンネルを作成する。名前は必ず英数字のみにする。

その後、チャンネルを右クリック「チャンネル詳細を開く」→「インテグレーション」→「App」→「アプリを追加する」を押下して、作成したアプリを登録する。

Slack 通知の設定

Zabbix の画面で「管理」→「メディアタイプ」→「Slack」→一番下の「複製」を押下し、以下を入力する。

  • 名前:Slack以外の任意の名前(ここでは、Slack (Simple)
  • bot_token:Slackからコピーした「Bot User OAuth Token」の値
  • event_opdata:適当な文字列
    • 本来は、取得したデータを挿入するマクロが登録されているが、64KBを超えるとSlackへの投稿に失敗するため、テキトーな値に置き換えておく。

問題なければ「追加」を押下する。

登録されたメディアの右端にある「テスト」リンクを押下し、以下を入力した後に「テスト」ボタンを押下する。

  • channel:追加したSlackチャンネル名
  • event_id:1
  • event_source:1
  • zabbix_url:ZabbixのトップページのURL

ここまでの設定が問題なければ、Slack経由でテスト通知が送られてくるはず。

f:id:kWatanabe:20210810121641p:plain
ZabbixからのSlack通知

f:id:kWatanabe:20210810121752p:plain
ZabbixからのSlack投稿

テストで問題なければ、「管理」→「ユーザ」→利用しているZabbixユーザ→「メディア」で以下を入力し、「追加」を押下する。

  • タイプ:追加したメディア(ここでは、Slack (Simple)
  • 送信先:追加したSlackチャンネル名

Webサイトの監視の設定

Zabbixの画面から、「設定」→「ホスト」→「Zabbix server」→「アイテム」→「アイテムの作成」を選択。

以下のパラメータを入力する。

  • 名前:わかりやすい任意の名前(ここでは、自衛隊ワクチン更新
  • タイプ:HTTPエージェント
  • キー:わかりやすい英数字の名前(ここでは、jgsdf_vaccine
  • URL:監視対象のWebページのアドレス
  • 監視間隔:情報を点検する時間間隔(ここでは、30m)
    • 人力で行ったとしても不自然ではない程度にする。
    • 高頻度での監視は、BANされる可能性もある。また、「前回の監視結果から変化があった」ことを判断基準とするため、短く設定しすぎるとすぐ「解決」状態になってしまい、訳が分からなくなる。
  • データ型:テキスト
  • ヒストリの保存期間:14d
  • 有効:チェックする

f:id:kWatanabe:20210810113525p:plain
アイテムの登録例

設定できたら、「テスト」→「値の取得とテスト」を押下して、「結果」欄にWebサイトのソースが表示されることを確認する。問題なければ「キャンセル」した後、「監視データ取得」と「追加」を押下する。

監視と通知を紐付ける設定

Zabbixの画面から、「設定」→「ホスト」→「Zabbix server」→「トリガー」→「トリガーの作成」を選択。

以下のパラメータを入力し、「追加」を押下する。

  • 名前:わかりやすい任意の名前(ここでは、自衛隊ワクチン更新
  • 深刻度:情報
  • 条件式:「追加」ボタンを押下して、以下を入力
    • アイテム:作成したアイテム(ここでは、Zabbix server: 自衛隊ワクチン更新
    • 関数:change() - 最新値と前回値との差分
    • 結果:<>0

再び、「設定」→「ホスト」→「Zabbix server」→「アイテム」を選択し、作成したアイテムを探し、「トリガー」にトリガー1が表示されており、クリックすると作成したトリガーが表示されることを確認する。

f:id:kWatanabe:20210810124207p:plain
アイテムとトリガーの紐付け

最後に、Zabbixの画面から、「設定」→「アクション」→「トリガーアクション」→「アクションの作成」を押下して、以下を入力し、「追加」を押下する。

  • 「アクション」タブ
    • 名前:わかりやすい任意の名前(ここでは、Report problems to Slack (Simple)
    • 計算のタイプ:Or
    • 実行条件の「追加」を押下して以下を入力
      • タイプ:トリガー
      • オペレータ:等しい
      • トリガー:作成したトリガー(ここでは、Zabbix server: 自衛隊ワクチン更新
    • 有効:チェックする
  • 「実行内容」タブ
    • 実行内容の「追加」を押下して以下を入力
      • ユーザに送信:Slack通知の設定で登録したZabbixユーザ
      • 次のメディアのみ使用:追加したメディア(ここでは、Slack (Simple)

結果

上記の設定が全て完了していれば、Webページの更新されると以下のような通知が Slack にやってくる。

f:id:kWatanabe:20210810131420p:plain
更新通知

これで、人間様はWebサイトに貼りつきになって無駄な時間を浪費することなく、もっと実のあるステキな時間をすごすことができるようになる。

なお、本ブログは全体を通じて無保証です!

*1:本来のZabbixの使い方としては、おおいに間違っている気がしないでもない。

*2:設定を作り込めば、取得した情報を加工して「特定の箇所がこのように変化した場合に通知」なども実現できる。

Majestouch MINILA のキートップを新品に交換する

  • 在宅勤務で持ち帰っていた Majestouch MINILA のキートップを新品にしてみた。
  • 既に8年選手のキーボードが、新品のようによみがえった。

Majestouch MINILA Air専用 Keycap Set 英語配列

今回使ったのは、メーカーが展開している交換用キートップ。カラフルなカスタム向けの商品もありますが、今回購入したのはノーマルの新品キャップ。

商品名には「MINILA Air 専用」とあるが、USB版の「MINILA」にも使える。

f:id:kWatanabe:20210503104257j:plain

www.diatec.co.jp

交換中の様子

f:id:kWatanabe:20210503104736j:plain

キャップを交換した MINILA は、2013年の新発売当時に会社にお願いして買って貰った*1もので、もう8年選手になる。購入後、しばらく別の職場に異動して不在の期間もあったけども、その間もちゃんと保管していてくれた今の職場には感謝している。

f:id:kWatanabe:20210503105049j:plain
交換前

f:id:kWatanabe:20210503105111j:plain
交換後

新品のように綺麗になった。このモデルは、既に絶版なので大切に使っていきたい。

思ったよりよかったので、プライベートで使っている MINILA も交換しようかしら。でも3台あるから、全部交換するとキャップだけで1万円近くになるという・・・。

*1:なお、プライベートでは予約して購入して既に使っていたので、使い勝手の良さはもう分かっており、会社のカネで冒険したわけではない。

LXC非特権コンテナでIntel CPU内蔵GPUを使ってGPGPUする

以前 LXCの非特権コンテナで CUDA する記事を書いた

以前、こんな記事を書いた。

kwatanabe.hatenablog.jp

kWatanabeは、主に waifu2x と ffmpeg を使って動画ファイルを 4K 化するために、LXCゲストでCUDAを動かしている。

github.com

github.com

waifu2xは動画を扱えないので、一度 ffmpegbmp にバラしてから1フレームずつ処理させてるんだけども、そうなるとフレーム同士に依存関係がないので、複数のGPUやCPUを混ぜて並列処理させることができる。

これまで、GeForceとCPUで処理をしていたわけだけど、ふとCPU内蔵GPUも同じように使えるのではと思ったので、試してみた。

検証環境

ホストの設定

GPUの特定と有効化

IntelGPU ドライバは Proxmox VE のカーネルにマージされているので、GeForce のようにドライバを導入する必要はない。

確認する場合は、lspci-v オプションで確認すればよい。まず、PCIのバス番号を調べておいて。

$ lspci | grep VGA
00:02.0 VGA compatible controller: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor Integrated Graphics Controller (rev 06)

バス番号を指定して詳細を確認する。

$ lspci -v -s 00:02.0
00:02.0 VGA compatible controller: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor Integrated Graphics Controller (rev 06) (prog-if 00 [VGA controller])
        Subsystem: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor Integrated Graphics Controller
        Flags: bus master, fast devsel, latency 0, IRQ 44
        Memory at f7400000 (64-bit, non-prefetchable) [size=4M]
        Memory at e8000000 (64-bit, prefetchable) [size=128M]
        I/O ports at f000 [size=64]
        [virtual] Expansion ROM at 000c0000 [disabled] [size=128K]
        Capabilities: <access denied>
        Kernel driver in use: i915
        Kernel modules: i915

見つからないようなら、GeForce のような dGPU のために iGPU が無効化されていないか、BIOS/UEFI などで確認する。

なお、ここで調べたPCIバス番号 ( 上記の場合は 00:02.0 ) は後で必要なのでメモしておく。

GPUをパススルー

GeForce の時と同様に、/dev/dri 配下をゲストに bind マウントする。素のLXC の場合は config ファイル、Proxmox VE の場合は /etc/pve/lxc 配下の設定ファイルに以下を書き加える。

lxc.cgroup.devices.allow: c 226:* rwm
lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir

そして、次がハマりポイント

/dev/dri のファイルは root ユーザ、video グループ、render グループが所有者となっているが、LXCの非特権コンテナの中からは nobodynogroup にリマップされるため、このままで権限不足でアクセスできない。

これを解決するためには以下の2通りの方法がある。

  • LXC がゲストに bind するのをフックして所有者を変更する
  • アクセス権を 660 から 666 に変えてしまう

セキュリティ的にはよくないが、後者の方が簡単。メモしておいた PCI バス番号 ( 今回は 00:02.0 ) を頼りにデバイスファイルのアタリをつける。

$ ls -l /dev/dri/by-path
lrwxrwxrwx 1 root root  8  2月  7 01:06 pci-0000:00:02.0-card -> ../card0
lrwxrwxrwx 1 root root 13  2月  7 01:06 pci-0000:00:02.0-render -> ../renderD128
...

アタリをつけた GPUパーミッションを変更する。

$ sudo chmod 666 /dev/dri/card0 /dev/dri/renderD128

ゲストの設定

まずは、デバイスファイルが bind マウントされており、アクセス権があることを確認する。

$ ls -l /dev/dri        
合計 0
drwxr-xr-x 2 root root         80  2月  7 01:06 by-path/
crw-rw-rw- 1 nobody nogroup 226,   0  2月  7 01:06 card0
crw-rw-rw- 1 nobody nogroup 226, 128  2月  7 01:06 renderD128

問題なければ、実機と同じようにセットアップする。Intel CPU 内蔵 GPU 用の OpenCL ドライバは beignetになる。

$ sudo apt install beignet beignet-opencl-icd opencl-headers clinfo

clinfoOpenCLバイスとして認識できていることを確認する。

$ clinfo -l
Platform #0: Intel Gen OCL Driver
 `-- Device #0: Intel(R) HD Graphics Haswell GT2 Desktop

以上で、LXCの非特権ゲストに Intel CPU 内蔵 GPU をパススルーして、OpenCL できるようになる。

おわりに

  • GeForceをパススルーしてCUDAした以前の記事に引き続き、LXCゲストに Intel CPU 内蔵 GPU をパススルーして OpenCL する方法を整理した
  • バイスファイルのアクセス権の設定を忘れると、デバイスファイルは見えているのに、clinfoからはデバイスが見えないといったことになるので注意
  • waifu2xのように工夫次第で複数、かつ、異種のGPUを使い分けられるワークロードであれば、少しの性能の足しにはなるかもしれない

Azure Spot VM の強制削除イベントをハンドルして安全に終了する

  • Azure や AWS には大幅なディスカウントのある Spot VM インスタンスがある
  • Azure の Spot VM は、インフラの負荷が高まった際、事前通知のうえ強制終了される
  • 事前通知されるイベントを検知する仕組みを作り、安全に終了できるようにした

Azure Spot VM

Azure Virtual Machine には、インフラに余力がある場合にのみ利用でき、従量課金制と比べて大幅にディスカウントされる「Azure Spot VM」というサービスがある。

azure.microsoft.com

ディスカウント幅は、インフラ側の都合で動的に決定される。Microsoft は最大で90% ディスカウントすると主張している。 一方、インフラの余力が無くなった場合には、30秒前に事前通知したうえで、強制的にシャットダウンするとしている。

また、ユーザから「ここまでディスカウントしてくれるなら使ってもいい。そうでないなら、使うのを止めさせてもらう。」といった宣言も可能で、その場合、インフラの余力がまだ残っていても、価格がユーザ指定値以上になった際にシャットダウンされるようになる。

Azure Metadata Service の Scheduled Events

Spot VM に限らず Azure Virtual Machine には、保守のための再デプロイ(ライブマイグレーション)や再起動など、データがロストする可能性があるイベントが予定された際に、それを VM の内部から検知できる仕組みがある。

docs.microsoft.com

以下の様に VM の内部からエンドポイントにアクセスすると、予定されているイベントがあれば JSON で取得できる。

$ curl -H "MetaData:true" "http://169.254.169.254/metadata/scheduledevents?api-version=2019-08-01"
{
    "DocumentIncarnation": {IncarnationID},
    "Events": [
        {
            "EventId": ce5f797a-7369-11eb-ba65-a706ed727702,
            "EventType": "Preempt",
            "ResourceType": "VirtualMachine",
            "Resources": ["SpotVM_TEST"],
            "EventStatus": "Scheduled",
            "NotBefore": Mon, 15 Feb, 2021 18:29:47 GMT,
            "Description": "",
            "EventSource" : "Platform",
        }
    ]
}

もし、Spot VM がインフラ側の余力不足などで強制削除される場合、その30秒前に EventTypePreempt なイベントが通知される。また、Preempt 以外にも以下のイベントが発生する可能性がある。

EventType 概要 ロストするデータ
Freeze 一時停止が予定されている。約15分前に発生 特になし
Reboot 再起動が予定されている。約15分前に発生 オンメモリ
Redeploy ライブマイグレーションが予定されている。約10分前に発生 一時ストレージ
Preempt Spot VMの削除が予定されている。約30秒前に発生 すべて
Terminate ユーザによる削除が予定されている。(発生タイミングは設定による) すべて

エンドポイントをポーリングして、もし、これらのイベントを検知すれば、動かしているプログラムを安全に終了させたり、必要なデータを退避するなどの対応が採れる。

検証環境

検証

Azure Spot VM を作る

具体的な作り方は割愛。閾値を現在価格以下にしてしまうと、そもそも起動できないので、1円上で指定してみた。

検知スクリプトを書く

やっつけだけど、こういうのを書いてみた。とりあえずイベントを検知したら、wall で警告を挙げて、プロセスを pkill するだけのシンプルなもの。

MITライセンスで。

github.com

結果

まだ、発動していないので、絶賛放置中。かなり渋い価格にしたので直ぐ発動すると思ったんだけど。それとも何かミスったか。

まとめ

  • Azure Spot VM の強制終了前に、それを検知し後片付けするスクリプトを書いてみた。
  • これで、Spot VM を安全に使えるはず・・・だが、まだ結果は分からない。

nftablesでシンプルなステートフルファイアウォールを作る

iptables から nftables への移行

Debian 11 (bullseye) の Freeze がはじまったので、自宅サーバ移行検証をしていたところ、ちょっとファイアウォールが気になった。

Debian では、10 (buster) で既に nftables がデフォルトになっており、iptables は nftables の互換レイヤーとして機能している。

wiki.debian.org

wiki.debian.org

今のところ、bullseye でも iptables が無くなるという話はないので、もう1回引っ張っても問題は無いのだが、nftables がデフォルトになった次のバージョンで移行というのはタイミングとしても良いので、ここで移行することにした。

検証環境

想定する構成

目指すはシンプルな2ファイアウォール型のDMZを構成すること。抽象化したモデルとしては以下のような形になる。

f:id:kWatanabe:20210213122100p:plain
シンプルな2ファイアウォールDMZ

なお、我が家はいわゆる「逸般の誤家庭」ではないので、Y社 や C社 のエンタープライズ向けルーターなんてものはない*1。 そのため、実態としては以下のような形になる。

f:id:kWatanabe:20210213123055p:plain
本記事の想定環境

この図の「Linux server」が今回のターゲットになる。 簡単のため、以下 DMZ 側の NICeth-dmz、Trust Zone 側の NICeth-trust と記載する。目指すルールは以下。

  • Trust Zone から DMZ への通信は全て許可
  • DMZ から Trust Zone へのセッション確立済みの通信を許可
  • DMZ から Trust Zone への特定のマシンから、特定のマシンの特定のポートへの通信を許可

nftables で作るステートフルファイアウォール

IP forward の有効化

まずは、eth-dmzeth-trust 間のパケット転送を有効にする。ここは nftables に限った話じゃないので詳細は触れない。

$ sudo vim /etc/sysctl.conf
#net.ipv4.ip_forward=1
↓
net.ipv4.ip_forward=1

適用。

$ sudo sysctl -p

nftables の導入

Debian buster および bullseye では、iptables と nftables の両方が提供されているため、明示的に nftables を導入する。

なお、パッケージは nftables のみでよい。iptablesiptables-persistent のように、管理インタフェースと永続化のための仕組みで分かれていたりはしない。

$ sudo apt install nftables

nftables のルールを書く

nftables は、全ての操作が nft コマンドで完結する。

iptables と同様、テーブル、チェイン、ルールの概念がある。ただし、デフォルトのテーブルやチェインは無い。そのため「nftables はなにもしない」状態から設定を作り込んでいく*2

大まかな流れは以下のようになる。 なお、<> はパラメータ、[]は省略可能な引数を指す。

1. 設定済みのルールをクリア
$ sudo nft flush ruleset
2. テーブルを作成
$ sudo nft add table [<アドレスファミリ>] <テーブル名>

アドレスファミリには以下が設定できる。

  • ip : IPv4
  • ip6 : IPv6
  • inet : IPv4IPv6
  • arp : ARP
  • bridge : ネットワークブリッジ
  • netdev : ネットワークインタフェース

IP層でのステートフルファイアウォールでは inet を使えば事足りる。具体例は以下。

$ sudo nft add table inet FIREWALL
3. テーブルにベースチェインを登録
$ sudo nft add chain [アドレスファミリ] <テーブル名> <ベースチェイン名> \
      { type <タイプ> hook <フック> [device <デバイス>] priority <優先度>; [policy <ポリシ> ;] }

ベースチェインは、初めに呼び出されるチェイン。Cでいう main() みたいなもの*3

「タイプ」は、チェインの用途を指定する。

タイプ 用途とアドレスファミリ 利用できるフック
filter 汎用 (全ファミリ) 全て
nat NAT (ip, ip6) prerouting, input, output, postrouting
route ルーティング (ip, ip6) output

「フック」は、チェインの実行タイミングを指定する。

フック タイミング 利用できるアドレスファミリ
ingress バイスがパケットを受信した際 netdev
prerouting パケット受信後、ルーティング判定の前 ip, ip6, inet, bridge
input ルーティング判定後、宛先が自分自身と判断した際 ip, ip6, inet, bridge, arp
forward ルーティング判定後、宛先が他ホストと判断した際 ip, ip6, inet, bridge
output 自分自身が他ホスト宛のパケットを生成した際 ip, ip6, inet, bridge
postrouting 他ホストへパケットを送信する直前 ip, ip6, inet, bridge

「デバイス」は、NIC 名を指定する。

「プライオリティ」は、同じフックが指定されたベースチェインが複数ある場合の優先度を指定する。値が小さいほど、先に実行される。

「ポリシ」は、チェインの中で取り扱いが決まらなかった際の取り扱いを acceptdrop で指定する。未指定だと accept になる。

具体例は以下の通り。

$ sudo nft add chain inet FIREWALL FILTER { type filter hook prerouting priority 0 \; }
  • type filter (汎用) で
  • hook prerouting (ルーティング判定前) に
  • priority 0 (最優先) で処理するベースチェインで、
  • 合致するルールが無い場合は受け入れる

policy を accept もしくは省略した場合は、確実に不要なパケットを落とすルールを入れることを忘れてはいけない。

4. テーブルにレギュラーチェインを登録
$ sudo nft add chain [アドレスファミリ] <テーブル名> <レギュラーチェイン名>

レギュラーチェインは、他のチェインから明示的に呼び出されるチェイン。呼び出し元のチェインの設定を引き継ぐため、チェイン名の登録のみ。具体例は割愛。

5. チェインにルールを登録
$ sudo nft add rule [<アドレスファミリ>] <テーブル名> <チェイン名> <ステートメント>

当該チェインが呼び出された際に、適用するルールを実行順に登録する。

ステートメント」は、具体的な制御内容を記載するが、網羅すると膨大な数になるため参考ページを記載し、ここでは具体例の記述にとどめる。

wiki.nftables.org

「確立済みの通信を許可する」ための設定例は以下になる。

$ sudo nft add rule inet FIREWALL FILTER \
    ct state related,established accept
  • ct (コネクション) の
  • state (状態) が
  • relatedestablished なものを
  • accept (許可) する。

「特定のマシンから、特定のマシンの特定のポートへのアクセスを許可する」ための設定例は以下になる。

$ sudo nft add rule inet FIREWALL FILTER \
    ip saddr 10.0.0.100 ip daddr 172.16.0.100 tcp dport 443 accept
  • ip (IPv4ヘッダ) の
  • saddr (送信元IPアドレス) が
  • 10.0.0.100 であって
  • ip (IPv4ヘッダ) の
  • daddr (送信先IPアドレス) が
  • 172.16.0.100 であって
  • tcp (TCPヘッダ) の
  • dport (接続先ポート番号) が
  • 443 のものを
  • accept (許可) する。

「(先のステートメントから漏れたパケットで、)特定のNICから入力された全てのパケットを破棄する」ための設定例は以下になる。

$ sudo nft add rule inet FIREWALL FILTER \
    meta iifname eth-dmz drop
  • meta (その他のメタデータ) の
  • iifname (入力インターフェース名) が
  • eth-dmz のものを
  • drop (破棄) する

設定済みのルールを確認

$ sudo nft list ruleset

上記のコマンドにて、現在の設定を出力できる。出力結果の具体例は以下になる。

table inet FIREWALL {
        chain FILTER {
                type filter hook prerouting priority 0; policy accept;
                ct state established,related accept
                ip saddr 10.0.0.100 ip daddr 172.16.0.100 tcp dport https accept
                iifname "eth-dmz" drop
        }
}

ルールの永続化

list ruleset の出力は、そのまま nftables の設定ファイルとして用いることができる。

Debian の場合、nftables サービスが起動する際に /etc/nftables.conf があればそれをロードする。そのため、以下のようにすることで設定を永続化できる。

$ sudo nft list ruleset | sudo tee /etc/nftables.conf
$ sudo systemctl enable nftables
$ sudo systemctl restart nftables

なお、手動で設定ファイルをロードしたい場合は以下のようにする。

$ sudo nft -f <ファイル名>

まとめ

最後に、上記のような設定を一発で行うためのスクリプトを作ったので公開しておく。

ライセンスは、MITライセンスで。

github.com

所感

  • nftables で単純なステートフルファイアウォールを作るための最低限の設定を整理した
  • iptables と比べて、設定ファイルや構文も構造化されており、人間様にわかりやすい
  • なにより、ツールが nft 1本に統一されているのがよい

*1:10GbE を敷くのが精々である。

*2:全て許可でも、全て禁止でも、全て転送でも無い。nftables 以外の仕組みが動いていれば、その設定が有効になる。本文に「nftablesは」とあえて書いた理由はここにある。

*3:_start()じゃないのか、とか面倒なコメントは受け付けない。

RHEL互換ディストリの作り方を手探ってみる2(大晦日ハッカソン)

  • 2020年の大晦日に大晦日ハッカソンなるイベントに参加した
  • CentOS のようなRHEL互換ディストリの作り方を1日で出来る範囲で手探ってみた
  • 1日でできる限られた範囲だけど、得られた知見をここで整理する

はじめに

「大晦日ハッカソンとは何ぞや」については、過去記事を参照。

今回は、そのRHEL*1互換ディストリビューションを自製するためには、どうすればいいのかを手探ってみることにしました。

kwatanabe.hatenablog.jp

RHEL互換ディストリビューションとは

過去記事にも書きましたが、RHEL互換ディストリビューションとは、商用ディストリビューションである RHEL から、RedHatの商標や独占的な権限を有するものを削除し、オープンソースライセンスの下に公開されているもののみで再構成したものです。*2

RHEL互換ディストリビューションを作成するための大まかな手順は以下になります。

  1. RHELサブスクリプションを得る
  2. RHEL のリビルド環境を用意する
  3. RHELソースコード (SRPM) を入手
  4. ソースコードの全てから、RedHat に関わる商標やその他権利を有するモノを仕分け・排除
  5. 上記の結果、RHELとの互換性が崩れてしまうものがあれば、それを補填するモノを開発
  6. リビルドする

以下、実際に大晦日ハッカソンで、kWatanabe が取り組んだ内容を整理していきます。

てさぐれ!RHELもの

RHELサブスクリプションを得る

RHELサブスクリプションを得るには以下の方法があります。

  • RedHatから購入する
  • リセラーを介して調達する
  • 評価版を入手する
  • Red Hat Developers に登録して入手する

また、RHELサブスクリプションは、用途や導入するマシンの種別に応じて、多種多様なものがあります。詳細はサブスクリプションガイドに記載があります。

www.redhat.com

Red Hat Enterprise Linux Developer Suite 以外のサブスクリプションは、RedHatもしくはリセラーから調達する必要があります。Red Hat Enterprise Linux Developer Suiteは、開発用途専用で、サポートもありませんが、Red Hat Developers にエントリすることで無償で入手できます。今回はこのサブスクリプションを利用しました*3

なお、Red Hat Developers には以下からエントリできます。

developers.redhat.com

RHEL のリビルド環境を用意する

バージョン齟齬や変な依存関係で悩みたくなかったため、リビルド対象の RHEL 8.3 をそのまま実機にインストールして準備しました。

RedHatカスタマーポータル*4にログインすれば、RHEL のインストールISOを入手できます。9GBを超え、DVD-R DLでも焼けないサイズのため、BalenaEtcher *5 を使って16GBの USB メモリに焼きました。

インストールが終われば、サブスクリプションを認証し、リビルドに必要なパッケージを導入します。この時、CodeReady Linux Builder リポジトリ*6を一緒に有効化しました。

$ sudo subscription-manager register

$ sudo subscription-manager attach

$ sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms

$ sudo dnf update

$ sudo dnf groupinstall "Development Tools"

$ sudo dnf install rpm-utils rpm-build texinfo

RHELソースコード (SRPM) を入手

ソースコードもまた、RedHatカスタマーポータルから ISO で入手できます。ISO に含まれるものは BaseOSリポジトリとAppStreamリポジトリのみですが、それでも 20GB ほどあります。

これをマシンにコピーし、loop マウントしておきます。

$ sudo mkdir /media/rhel-source

$ sudo vim /etc/fstab
/home/kwatanabe/rhel-8.3-source-dvd.iso /media/rhel-source iso9660 ro,nofail,loop 0 0

$ sudo mount -a

なお、通常は不要ですが、手探るうえで必要になり CentOS のソースも入手しました。 CentOS は ソース ISOを配布していないため、rsyncリポジトリをミラーしました。

$ sudo mkdir -p /media/centos-source/

$ sudo rsync -avzP rsync://vault.centos.org/8.3.2011/BaseOS /media/centos-source/

RedHat に関わる商標やその他権利を有するモノを仕分け・排除

RHELCentOSを比較

環境が整えば、実際に手探っていきます。全パッケージを対象にすると、プロジェクトが立ち上がる位の作業量になってしまうので、BaseOS リポジトリに絞って作業します。

まずは RHEL にあって、CentOS にないものを探しました。もちろん、これはチートです。本来は、削除しないとダメなモノを全部自力で調べなければなりません。

f:id:kWatanabe:20210107120056p:plain
RHELCentOSSRPMを比較

CentOSリポジトリには過去バージョンも含まれていますが、それ以外で RHEL にあって、CentOS にない SRPM は以下の通りでした。

  • libica
  • libzfcphbaapi
  • memstrack
  • openssl-ibmca
  • qclib
  • redhat-indexhtml
  • redhat-logos
  • redhat-release
  • s390utils
諸元から削除された理由を調べる

アタリが付いたら、rpm コマンドを使って中身を調べていきます。まずは、rpm -p -qi を使って諸元を調べました。例えば、libicaだと以下の結果となります。

$ rpm -p -qi libica-3.7.0-2.el8.src.rpm
Name        : libica
Version     : 3.7.0
Release     : 2.el8
Architecture: s390x
Install Date: (not installed)
Group       : System Environment/Libraries
Size        : 552759
License     : CPL
Signature   : RSA/SHA256, 2020年07月21日 17時52分31秒, Key ID 199e2f91fd431d51
Source RPM  : (none)
Build Date  : 2020年07月20日 21時56分29秒
Build Host  : s390-046.build.eng.bos.redhat.com
Relocations : (not relocatable)
Packager    : Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>
Vendor      : Red Hat, Inc.
URL         : https://github.com/opencryptoki/
Summary     : Library for accessing ICA hardware crypto on IBM z Systems
Description :
A library of functions and utilities for accessing ICA hardware crypto on
IBM z Systems.

Architectureや、Description の内容から、これは IBM z 向けのパッケージと分かります。CentOSIBM z をサポートしないため、含まれていないようです。同様に、libzfcphbaapi、openssl-ibmca、qclib、s390utilsもアーキテクチャ違いによるものでした。

中身からCentOSで削除された理由を調べる

x86_64向けやアーキテクチャ非依存にもかかわらず CentOS に含まれていないものは、RedHatの商用や権利関係で削除された可能性があります。rpm -ivhSRPMを展開し、rpmbuild -bpでパッチ群をマージして、実物を確認します。

$ rpm -ivh redhat-indexhtml

$ rpmbuild -bp ~/rpmbuild/SPECS/redhat-indexhtml.spec

$ ls ~/rpmbuild/BUILD/redhat-indexhtml

確認の結果、redhat-indexhtml、redhat-logos は RedHat の商標やロゴが含まれており、NGなものでした。redhat-release は、RHELリポジトリ登録のためのパッケージで RHEL互換ディストリビューション では置き換えが必要なものです。

一方、memstrack は問題があるパッケージには見えませんでした。また、大元のソースコードGitHub で公開されています。

github.com

CentOSリポジトリを見直すと、 BaseOS リポジトリから AppStream リポジトリに移動されていました。理由は不明ですが、削除しなければならないパッケージではなさそうです。

これらの結果から、RHEL 8.3 の BaseOS リポジトリでは以下のものを削除し、オリジナルのものに置き換える必要があると分かりました。

パッケージ 概要
redhat-indexhtml RHELのオフラインマニュアル
redhat-logos RHELのロゴ・商標の画像集
redhat-release RHELリポジトリ情報

互換性が崩れてしまうものがあれば補填するモノを開発

抽出したモノのうち、redhat-logosとredhat-releaseは代替となる画像データや、リポジトリを用意しないとディストリビューションとして完結しなくなりそうです。

redhat-indexhtmlは、無くても良い気がしますが、未確認です。

リビルド

だいたいの流れ

リビルド’作業そのものは、通常のRPMのリビルドと大きな差はありません。dnf builddepで依存パッケージを導入して、RPMをビルドしていきます。

しかし、大きな注意点があります。RHELとの互換性を担保するために、RedHatがビルドに使ったライブラリと同じものを使わなければなりません。これはバージョンや適用されているパッチも完全に一致しなければダメです。

REHLのリポジトリに依存パッケージがあればいいですが、無い場合何が使われているのかを調査し、環境を整える必要があります。ここが、RHEL互換ディストリビューション構築で重要かつ困難な所になります。

依存パッケージを調べる

私は以下の流れで作業を進めました。

  1. dnf builddepを試す → 何事も無く完了したら、rpmbuild -baでビルドする
  2. RedHatのマニュアル「RHEL 8 の導入における検討事項」で、RHEL8.0~8.3で統廃合されたパッケージに該当しないか調べる
  3. FedoraやEPELなど、RHELの upstream に含まれていないか調べる
  4. RPM pbone.net などの RPM 検索サイトなどから調べる
  5. Google 検索する

結果としては、大きく以下の4種類(AND/OR含む)となりました。

もしかしたら、RedHat はビルド環境を RHEL ではなく、upstream の Fedora をベースに構築しているのかもしれません。なお、具体的なパッケージ名は当日ツイートした以下の通りです。

なお、調査に使ったリソースは以下からたどれます。

access.redhat.com

getfedora.org

fedoraproject.org

rpm.pbone.net

ここで終了・おわりに

ここまで絞り込めれば、ビルドが通らなかったパッケージで使われているライブラリのバージョンを調べ、ビルドできるように進めていくところですが、ここで2020年が終わりました。

真に難しいところの1歩手前で時間切れとなりましたが、それでも CentOS という先人の成果をフル活用してもかなりパワーを使ったので、世のRHEL互換ディストリビューションの構築がいかに大変かがうかがい知れます。

それでは。

*1:RedHat Enterprise Linux

*2:単なる再構成ではなく、RHEL とソフトウェア的な互換性が担保されている必要があります。RHELエンタープライズディストリビューションとしてデファクトに近い存在であるので、RHEL と同じ機能・品質を持ち、かつ特定の組織にロックインされないディストリビューションを求める人が大勢いるのです。

*3:RHEL互換ディストリビューションの作成が「開発用途」に含まれるかどうかは不明。限りなく黒に近いグレーな気がしますので、このライセンスで用達したソースを元に再構築したディストリビューションの再配布は止めた方がいいとおもいます。

*4:https://access.redhat.com

*5:https://www.balena.io/etcher/

*6:ドライバやパッケージのビルド環境など、開発用途のツールキットを収録した追加リポジトリCentOSでいうところの PowerTools に相当。

RHEL互換ディストリの作り方を手探ってみる1(大晦日ハッカソン)

  • 2020年はSTAY HOMEのため、帰省できないため何かやりたかった
  • ひとりでもくもく会をやってもいいのだが、折角だから大晦日ハッカソンに参加してみる
  • 自己啓発として、RHEL互換(かも知れない)ディストリの作り方を手探ってみることにした

晦日ハッカソン

2012年より、毎年大晦日に開催されているイベントっぽい何かです。

omisoka-hackathon.connpass.com

内容としては、

以上。

2020年は、コロナ渦対策のSTAY HOMEで、旅行も帰省もできないので。ちょっと参加してみようと思ったのです。

何をやるか

とはいいつつも、何かをこれまで作ってきたわけでもなく、何か作りたいものがあるわけでもない。1日で材料から集めてモノを作っても、中途半端になるに決まってる。

そこで、「何かモノを作るのではなく、知見をためよう。知見であれば中間報告であっても、ブログで公開するなり、会社の勉強会で語るなりすれば、それはれっきとした成果物になるだろう。」と言い訳をして、手を動かして何かを手探っていこうと思った次第です。

ネタ探し

1ヶ月前のとある出来事

ところで、1月ほど前にある出来事があり、業界でちょっとした騒動になりました。

blog.centos.org

超意訳すると、RHEL*1という商用Linuxディストリビューションのクローンとして無償公開されていた CentOS が、突如2021年に開発終了するとアナウンスされたものです*2

元々、CentOS はクローン元の RHEL と同様に2028年まで開発されると予想されていたので、これに期待して構築されたシステムは RHEL の購入、 Oracle Linux などの他のクローンへの移行、Debian などの他のディストリへの移行のいずれかを強いられることになりました。

RHELクローン(互換ディストリビューション)とは

そもそも、RHELクローンとは何でしょうか。

RHELは商用のLinuxディストリビューションですが、製品の多くはオープンソースプロジェクトの成果物のうえに成り立っています。そのため、RHEL はそれ自身もオープンソースとして公開されています*3

そして、RHELEULA*4には、以下の記載があります。

1. License Grant. Subject to the following terms, Red Hat, Inc. ("Red
   Hat") grants to you a perpetual, worldwide license to the Programs
   (most of which include multiple software components) pursuant to
   the GNU General Public License v.2. The license agreement for each
   software component is located in the software component's source
   code and permits you to run, copy, modify, and redistribute the
   software component (subject to certain obligations in some cases),
   both in source code and binary code forms, with the exception of
   (a) certain binary only firmware components and (b) the images
   identified in Section 2 below. The license rights for the binary
   only firmware components are located with the components
   themselves. This EULA pertains solely to the Programs and does not
   limit your rights under, or grant you rights that supersede, the
   license terms of any particular component.
 
2. Intellectual Property Rights. The Programs and each of their
   components are owned by Red Hat and other licensors and are
   protected under copyright law and under other laws as
   applicable. Title to the Programs and any component, or to any
   copy, modification, or merged portion shall remain with Red Hat and
   other licensors, subject to the applicable license. The "Red Hat"
   trademark and the "Shadowman" logo are registered trademarks of Red
   Hat in the U.S. and other countries. This EULA does not permit you
   to distribute the Programs or their components using Red Hat's
   trademarks, regardless of whether the copy has been modified. You
   may make a commercial redistribution of the Programs only if (a)
   permitted under a separate written agreement with Red Hat
   authorizing such commercial redistribution, or (b) you remove and
   replace all occurrences of Red Hat trademarks. Modifications to the
   software may corrupt the Programs. You should read the information
   found at http://www.redhat.com/about/corporate/trademark/ before
   distributing a copy of the Programs.

要するに、オープンソースライセンスの下に公開されているソースコードは、RedHatの商標やロゴ、その他独占的な権限を有するものを全て排除した場合に限り、そのライセンスに抵触しない範囲で再利用して良いことになっています。

そこで、RHELソースコードから RedHat が権利を有するものを削除し、再配布されている Linux ディストリビューションがあります。それが、RHEL クローンと呼ばれるものです。

過去には様々な RHEL クローンがありましたが、2020年現在、プロダクトに耐えうるものは Oracle LinuxAsianux です*5Asianux は有償のため、無償のものは Oracle Linux のみです。なお、このアナウンスを受けて、CentOS に代わる新たな RHEL クローンを作ろうというプロジェクトがいくつか立ち上がっています。

rockylinux.org

www.projectlenix.org

RHELクローンの作り方

さて、やっと本題になりました。大晦日ハッカソンでは、この RHELクローンの作り方を手探っていくことにします。

大まかには以下の流れになるはずです。

  1. RHELサブスクリプションを得る
  2. RHELソースコード (SRPM) を入手
  3. ソースコードの全てから、RedHat に関わる商標やその他権利を有するモノを仕分け・排除
  4. 上記の結果、動かなくなってしまうものがあれば、それを補填するモノを開発
  5. リビルド

SRPMを入手してビルドするだけなら、RHEL を開発で使ったことがある人なら難なくできる作業です。しかし、RedHat が権利を持つものを削除したうえで、RedHatの権利を侵害すること無く同等の機能を補填し、RHEL と同じ動きをするものを作るとなると、簡単にはいきません。

RHEL クローンは、莫大な資金と人的リソースを費やして開発されているものなので、大晦日にちょっと手を出したくらいで何か形になるとは思いません。ひとつでも知見を得られれば、それで成功だと思っています*6

おわりに

  • 2020年の大晦日ハッカソンでは、RHEL クローン(RHEL 互換ディストリビューション)の作り方を手探っていく
  • 目標は、何らかの知見を得ることで、RHEL クローンを作ることでは無い(そもそも無理)
  • 得られた知見は、別途整理して、何らかの形でまとめる

*1:Red Hat Enterprise Linux

*2:より正確には、RHEL のクローンとしての CentOS が開発終了。RHEL のアップストリームとしての CentOS Stream は継続される。

*3: RHELサブスクリプションを持っている人には全てのソースコードを詰め込んだ 20GB強もの ISO ファイルがダウンロードできる

*4:End User License Agreement。https://www.redhat.com/ja/about/agreements

*5: ただし、汎用品として。他にも Red Hawk Linux などの特定用途に特化した製品はあります。

*6:そもそも環境の構築方法を調べるだけで1日終わるかもしれないし…

Proxmox VEのLXCでホストにあるISOをマウントする

  • Proxmox VEのLXCゲストは、ホストに保存されているISOファイルをマウントできない
  • メディアからaptやyumをしたい場合など、ニッチだけど地味に困る
  • ホストに保存されているISOの中身を、LXCゲストに見せる方法を整理する

検証環境

  • Proxmox VE 6.3

Proxmox VEでは、LXCゲストでISOをマウントできない

Proxmox VEは、KVMゲストにISOファイルをマウントさせることはできるが、LXCゲストではWebインタフェースに項目が無く、マウントさせることはできない。

LXCでは、ISOからOSをインストールする必要が無いため、割り切った仕様にしているのだとは思うが、DVDメディアやISOファイルをリポジトリとして yum や apt を使いたい場合に困る*1

ダメな例1

Webインタフェースで設定する代わりに、以下のように/etc/pve/lxc/<VMID>.confに設定を書き込んでも、エラーになりゲストが起動しなくなる。

mp0: /path/to/isofile,mp=/path/to/mountpoint,ro=1 ★ダメな例1★

ダメな例2

Porxmox VE のLXCゲストの設定ファイルには、通常のLXCのオプションをそのまま書くこともできるので、以下のようにloopマウントをやりたくなるが、これもうまくいかない。なお、bindマウントはできる。

lxc.mount.entry = /path/to/isofile path/to/mountpoint iso9660 loop,nofail,ro 0 0 ★ダメな例2★

ISOファイルをLXCゲストに送り込んで、ゲストの中からloopマウントする方法も考えられるが、これは Permission Denied になる。回避するためには、apparmorの設定を緩めたうえで、特権コンテナにし、さらにケーパビリティを調整しないといけない(ケーパビリティに関しては過去記事参照)。

kwatanabe.hatenablog.jp

素のLXCならともかく、Proxmox VE のようなプラットフォームでホストの設定を変えると、不具合の原因になりかねないので避けたい。

解決策

「ダメな例2」でしれっと書いたが、bindマウントはできる。そこで、力業だけどもホストでloopマウントして、そのディレクトリをbindマウントして投影すればよい。

なお、loopマウントのマウントポイントをbindマウントのマウント元に指定しないと、ISOファイルの中身は見えない。横着して、複数のマウントポイントを集めた親ディレクトリをbindマウントしてもISOの中は見えない。

最終的にはホストで以下のようにする。

  • ISOファイルをloopマウント
$ sudo vim /etc/fstab
/path/to/isofile /path/to/mountpoint-host iso9660 ro,loop,nofail 0 0
$ sudo vim /etc/pve/lxc/<VMID>.conf
lxc.mount.entry = /path/to/mountpoint-host path/to/mountpoint-guest none ro,bind,nofail 0 0

これで、ISOファイルの中身がゲストでも中身が見えるようになる。なお、Readonlyとはいえど、ホストとゲストの間で穴を開けることになるので、セキュリティ面では大変よろしくない。使い終わったら、さっさと元に戻すのが吉。

おわりに

  • Proxmox VEでは、LXCゲストにISOファイルをマウントする手段を提供していない
  • ゲストの中からマウントするには、セキュリティ設定をかなり調整しないとダメ
  • ホストでloopマウントしてから、bindマウントすれば間接的にISOファイルをマウントできる
  • セキュリティ面では大変宜しくないため、使い終わったら元に戻すこと

*1:CentOS 7のEverything ISOを使う時とか、ISOの中身をHTTPで公開してPXEサーバを作りたい時とか、ニッチだけど地味に困る

aptリポジトリにおける優先度のルールと確認方法

  • apt には、リポジトリやパッケージが競合する場合に、自動選択する仕組みがある
  • これは優先度に基づいて行われるが、デフォルトの優先度は状況によって異なり「リポジトリを登録したのに使われていない」なんてことがまれに生じる
  • apt のリポジトリ優先度の考え方と、設定の確認・変更方法をまとめる

検証環境

apt における優先度付けのルール

apt の優先度は静的な順序付けではない

実は apt リポジトリの優先度は、静的に順序付けされるものではない。ユーザが設定ファイルで指定しない限り、優先度は動的に設定される。また、優先度の絶対値にも意味がある。

apt_preferencesのmanページを見れば、具体的なルールが記載されている。

manpages.debian.org

デフォルトの優先度

特に指定のない場合、apt は以下の優先度を付与する。

優先度 諸元
1 Releaseファイルに NotAutomatic: yes が指定されているが、ButAutomaticUpgrades: yes が指定されていないリポジトリのパッケージ
100 (1) Releaseファイルに NotAutomatic: yesButAutomaticUpgrades: yes が指定されているリポジトリのパッケージ
(2) 既にシステムにインストール済みのパッケージ
500 他の条件に合致していないパッケージ
990 ターゲットリリースとして指定されたリポジトリのパッケージ

なお、ターゲットリリースとは、/etc/apt/apt.confAPT::Default-Releaseに指定しているリリースか、コマンド実行時に -t オプションで指定したリリースを指す。

絶対値に応じた制御

apt は、リポジトリ同士の相対的な比較とは独立して、絶対値による評価も行う。

  • 優先度が負値のリポジトリからは、パッケージをインストールしない
  • 優先度が1000以上のリポジトリは、たとえパッケージをダウングレードすることになってもインストールする
  • 優先度0は未定義動作

ただし、デフォルトの優先度でこれらの値になることはない。ユーザが /etc/apt/preferences で明示的に優先度を設定して、パッケージやリポジトリブラックリスト化や優先付けができるようになっている。

優先度を手動で設定する方法

リポジトリの優先度をaptに決めさせるのでなく、手動で設定したい場合は /etc/apt/preferencesに設定を追記する。

なお、優先度はリポジトリではなくパッケージごとに設定できる。

Package: <パッケージ>
Pin: release a=<リリース名>
Pin-Priority: <優先度の値>

例えば、testing リポジトリexperimental リポジトリと同様に、「明示的にターゲットリリースにしない限り、インストールもアップグレードもされないようにする」には、以下のようにする。

Package: *
Pin: release a=testing
Pin-Priority: 1

また、一時的にターゲットリリースとして設定するには、-t オプションを指定する。

$ sudo apt install -t testing ...

恒久的にターゲットリリースとして設定するには、/etc/apt/apt.confAPT::Default-Releaseを設定する。

APT::Default-Release "testing";

要するに

これらのルールに基づいて整理すると、以下のようになる。

リポジトリの優先度 指定方法 挙動
1000~ /etc/apt/preferences ダウングレードであっても採用する
991~999 /etc/apt/preferences ダウングレードで無い限り、ターゲットリリースにパッケージがあっても、こちらを採用する
990 -tオプション
/etc/apt/apt.conf
ターゲットリリース。明示的に指定した際に採用する
501~989 /etc/apt/preferences ターゲットリリースを指定したものの、目的のパッケージが無い場合に採用する
500 自動 一般的なリポジトリのデフォルト値
101~499 /etc/apt/preferences 他のリポジトリに目的のパッケージが無い場合に採用する
100 自動 インストール済みのパッケージ
1~99 /etc/apt/preferences
1は自動
他のリポジトリに存在せず、また、まだインストールされていない場合(アップグレードでない場合)に限り採用する
0 使用禁止 未定義動作
負値 /etc/apt/preferences いかなる場合でも採用しない

設定の確認方法

特に何も指定しない場合

現状の設定は apt-cache policyコマンドで確認できる。

$ apt policy
Package files:
 100 /var/lib/dpkg/status
     release a=now
   1 http://deb.debian.org/debian experimental/main amd64 Packages
     release o=Debian,a=experimental,n=experimental,l=Debian,c=main,b=amd64
     origin deb.debian.org
 100 http://deb.debian.org/debian buster-backports/main amd64 Packages
     release o=Debian Backports,a=buster-backports,n=buster-backports,l=Debian Backports,c=main,b=amd64
     origin deb.debian.org
 500 http://ftp.debian.org/debian buster/main amd64 Packages
     release v=10.6,o=Debian,a=stable,n=buster,l=Debian,c=main,b=amd64
     origin ftp.debian.org
...(略)...

この環境の例では、以下の設定となっていることが分かる。

  • 不安定なexplerimentalリポジトリは、インストール時に-t experimentalと明示的に指定しない限り、過去に導入済みであったとしても、自動では導入・更新されない。
  • 将来バージョンからのバックポートであるbuster-backportsは、安定版リポジトリと競合するため、インストール時に-t buster-backportsと明示的に指定しないと導入されないが、一度導入した後は自動更新の対象とされる。
  • 標準の安定版リポジトリは、他のリポジトリがターゲットリリースとして指定されない限り、これを使う。

なお、experimentalリポジトリについては以下の過去記事参照。 kwatanabe.hatenablog.jp

ターゲットリリースを明示的に指定した場合

ここで、-t experimentalを付けて、明示的にexperimentalリポジトリをターゲットリリースにしてみる。

$ apt policy -t experimental
Package files:
 100 /var/lib/dpkg/status
     release a=now
 990 http://deb.debian.org/debian experimental/main amd64 Packages
     release o=Debian,a=experimental,n=experimental,l=Debian,c=main,b=amd64
     origin deb.debian.org
 100 http://deb.debian.org/debian buster-backports/main amd64 Packages
     release o=Debian Backports,a=buster-backports,n=buster-backports,l=Debian Backports,c=main,b=amd64
     origin deb.debian.org
 500 http://ftp.debian.org/debian buster/main amd64 Packages
     release v=10.6,o=Debian,a=stable,n=buster,l=Debian,c=main,b=amd64
     origin ftp.debian.org
...(略)...

experimentalリポジトリの優先度が990になっていることが分かる。

まとめ

  • apt リポジトリの優先度は、インストール状況やパッケージの品質に応じて動的に調整されるようになっている
  • 一時的にリポジトリの優先度を高めたい場合は、-tオプションでターゲットリリースとして指定する
  • リポジトリによっては、インストール済みのパッケージのアップグレードでも、都度ターゲットリリースと指定しないと更新対象とならない場合もある
  • 現状の設定状況は apt-cache policy コマンドで確認できる