NanoPi NEOでFreeBSDする

freebsd@generic:~ % uname -a
FreeBSD generic 14.0-STABLE FreeBSD 14.0-STABLE #0 stable/14-n267114-72c3d91294c4: Thu Apr  4 08:28:31 UTC 2024     root@releng1.nyi.freebsd.org:/usr/obj/usr/src/arm.armv7/sys/GENERIC arm

するためのあれこれ。

適当なマシンで、以下のとおり準備する。うっかりSTABLEを使ったが、まあ、なんでもいいだろう。

# pkg install u-boot-nanopi_neo-2024.01
# dd if=FreeBSD-14.0-STABLE-arm-armv7-GENERICSD-20240404-72c3d91294c4-267114.img.xz of=/dev/da0 bs=524288
# dd if=/usr/local/share/u-boot/u-boot-nanopi_neo/u-boot-sunxi-with-spl.bin of=/dev/da0 conv=notrunc,sync bs=1k seek=8

このイメージはデフォルトでSSHが有効で、ユーザ名freebsd/パスワードfreebsdでログインできる。
さすがにrootユーザでのSSHログインはできないので、suとrootユーザの初期パスワードrootを使ってrootになる。

あとはパスワードを変更して(忘れたままインターネットに接続するようなアホはいないな?)、pkgでやりたい放題すればよい。

ちなみに、

WARNING: 32-bit kernels are deprecated and may be removed in FreeBSD 15.0.

とのことなので、15.0になったら64bitにしようね。

FreeBSDでOMRON UPSを監視する

OMRONのUPSを使っているが、長年監視を放置していた。いい加減監視したいと思って設定したので、記録を残しておく。
まず、OpenBSD/NetBSDな人は以下を参照のこと。

オムロン製UPSをOpenBSD/NetBSDで使うためのメモ (GitHub Gist)

本記事は上記を参考に、FreeBSD 14.0向けに設定してみた記録になる。

やることは以下。

  • nutをインストールする
  • UPSのVendor IDとProduct IDを調べる
  • nutユーザがデバイスを使えるようにdevdを設定する
  • nutを設定する(※本稿では全部は扱わない)

諸事情あってproduct IDはすべて伏せている。参考にして設定する奇特な人は、XXXXのような部分は適宜読み替えてほしい。

nutをインストールする

pkgでいっぱつ。

# pkg install nut

UPS関係だから名前にUPSが入ってるだろうと思って pkg search ups しても出てこない。
nutという名前は覚えて帰ろう。

UPSのVendor IDとProduct IDを調べる

なんでもいいので調べる。
ここでは dmesgugenX.X を調べたあと、usbconfigdump_device_desc を使って調べた。

# usbconfig -d ugenX.X dump_device_desc
ugen0.3:  at usbus0, cfg=0 md=HOST spd=LOW (1.5Mbps) pwr=ON (100mA)
(...snip...)
  idVendor = 0x0590 
  idProduct = 0xXXXX 

nutユーザがデバイスを使えるようにdevdを設定する

nutはnutユーザで動作するので、通常 root:wheel/dev/ugenX.X (の指す /dev/usb/X.X) を触れない。
devd を設定して、OMRON UPSだったらnutユーザにプレゼントしよう。
設定後は service devd restart をお忘れなく。

/usr/local/etc/devd/nut-usb-omron.conf:

notify 100 {
  match "system" "USB";
  match "subsystem" "INTERFACE";
  match "type" "ATTACH";
  match "vendor" "0x0590";
  match "product" "0xXXXX";
        action "chgrp nut /dev/$cdev; chmod g+rw /dev/$cdev";
};

/usr/local/etc/devd/nut-usb.conf を参考に書いたが、subsystemはDEVICEではなくINTERFACEになるようだ。
うまくいかないなら devd -d を実行して確認するといい。

nutを設定する

こんな感じ。
driverは参考資料では blazer_usb だが、maintenance-onlyだよ、というので nutdrv_qx に変えた。

/usr/local/etc/nut/ups.conf:

[XXXXX]
        driver = nutdrv_qx
        port = auto
        desc = "OMRON XXXXX"
        vendorid = 0590
        productid = XXXX
        subdriver = ippon

通信テストは、以下のようにしてドライバを直接実行すればいい。read: (... のような出力が定期的に出ればOK。

# /usr/local/libexec/nut/nutdrv_qx -DDDD -a XXXXX

あとはUPSがどうなったらどうしたい、の話になるので、man nut.conf でも実行してnutと仲良くなるといい。

私はドライバ出力を食ってprometheusに出力したかっただけなので、nut自体は使っていない。

FreeBSDのVMでHDD容量を拡張

ESXiなどでディスクサイズを拡張したときどうするか。
再起動すれば認識するのは分かっているが、再起動したくない。

というときは、以下でよい模様。

$ sudo camcontrol reprobe da0

あとはgpart resizeしたりgrowfsしたり、zpool online -eしたりすればよい。

gitlab-runnerにrefusing to work with credential missing host fieldと言われたら

GitLabでCIしていたら特定のホストで動いたときだけ fatal: refusing to work with credential missing host field というエラーが発生した。
あまり情報も整理していないのでメモ程度だが、残さないよりマシと思って書き残すことにした。

結論:

  • GitLabにHTTPSでアクセスしている、かつ、HTTPSでクライアント証明書の提示を要求していて、gitlab-runner側に使う証明書と鍵を設定している。(この時点でたぶん、大多数の人は当て嵌まらないハズ。)
  • クライアント側証明書に使われている署名のハッシュアルゴリズムがSHA-1になっていて、opensslに蹴られていた。

とはいえ、opensslがなんらかの理由でイヤと言えば全部このエラーになってしまう気がするので、調べ方も重要であろう。

gitlab-runnerはいろんな環境変数やらなんやらを設定してgitを叩くので、手動で実行しても同じ問題は再現しづらい。
そこで、gitlab-runnerに細工をして実行することにする。
具体的には、以下を実施した。

  • 環境変数を設定した状態でgitlab-runnerを起動する。GIT_TRACE=1, GIT_CURL_VERBOSE=1あたりが特に有効。詳しくは Git-Internals-Environment-Variables を読まれたし。
  • runnerが作る一時ファイルはすぐに消されてしまうものがあるので、必要ならshell scriptなどで書いた偽のgitを作り、PATHを通しておく。

今回だと、GIT_TRACE=1GIT_CURL_VERBOSE=1 を設定することで、GitLab側のJob出力で以下を得た。

16:23:14.638471 http.c:845              == Info: could not load PEM client certificate from /var/tmp/gitlab_runner/builds/(中略)/ほげほげ.tmp/CI_SERVER_TLS_CERT_FILE, OpenSSL error error:0A00018E:S

このファイルを調べようとしたが、普段は存在しない。実行中だけ存在するものと考えて偽gitにファイルコピーを仕込んだ結果、この証明書はgitlab-runnerに設定しているものと同じと分かった。
また、opensslのエラーコードから、これはどうやらmd too weakというエラーらしいことが分かり、ハッシュアルゴリズムを疑った。
(実際、このエラーはFreeBSD 14なシステム上のgitlab-runnerだけで起きていて、13では起きなかった。opensslのバージョン違いによる差であったのだろう。)

謝辞: manを読んでおきながら読み飛ばして GIT_CURL_VERBOSE に気づいてなかったとき、教えていただいたzunda氏に感謝します。

余談: 今回の問題発生時は最初にGitLabサーバのHTTPSポートに接続してTCP handshakeを行い、直後にgitlab-runner側が切断するという不可解な動作をしていた。
どうやら証明書のチェックはTCP connect後にやっているらしい。
いずれにせよ、切断(先にFIN送出)しているのがgitlab-runner側なので、gitlab-runner側の問題であることは明白だったのだが、少々混乱もした。

FreeBSDでOpenObserveクラスタ構築

elasticsearchとlogstash(とJava)に嫌気が差したので、MinIO+OpenObserveに引っ越した。

  • minioは go install github.com/minio/minio@latestでさくっと入る。
  • OpenObserveのクラスタ構成のためにetcdが必要。こいつはpkgがあるので楽ちん。
  • OpenObserveはソースを拾ってきてビルドする。rust-nightlyが必要。WebUIのコードはFreeBSDでサポートされないCypressが含まれているので、package.jsonをいじって外す。
  • 各サーバに全コンポーネントをつっこむなら簡単。そうでない場合はetcd/minioをhaproxyとかで生きているサービスに飛ばすこと。
  • MinIO

    特に書くことはない。やることはこれだけ。

    go install github.com/minio/minio@latest
    go install github.com/minio/mc@latest
    

    jail上で起動すると、root disk判定が誤判定するので、 MINIO_ROOTDISK_THRESHOLD_SIZE=1 など環境変数を設定しておく。
    もちろん保存先を間違えると大変な目に遭うので注意。

    etcd

    pkg install coreos-etcd34 で入る。
    ちなみにrc scriptがないので、自分で daemon を呼ぶ。

    起こし方は素直にhttps://etcd.io/docs/v3.5/op-guide/clustering/のガイドを読むのがいい。

    一応残しておくと、試した設定はこんな感じ。
    各ホストで調整が必要なURLが多いので注意する。
    quorumを満たしていないと起動しないことと、一度起動できちゃうと無視される設定があるので注意する。
    変だなと思ったら rm -rf /var/db/etcd して起動しなおすほうが早い。

    name: openobserve1
    data-dir: "/var/db/etcd/data"
    wal-dir: "/var/db/etcd/wal"
    strict-reconfig-check: true
    
    initial-cluster-state: new
    #initial-cluster-state: existing
    initial-cluster-token: openobserve-etcd
    initial-cluster: "openobserve1=http://10.0.0.1:2380,openobserve2=http://10.0.0.2:2380"
    initial-advertise-peer-urls: "http://10.0.0.1:2380"
    listen-peer-urls: "http://10.0.0.1:2380"
    advertise-client-urls: "http://10.0.0.1:2379"
    listen-client-urls: "http://10.0.0.1:2379,http://127.0.0.1:2379"
    

    OpenObserve

    ビルドはサーバプロセス(rust)とWebUI(node.js)の2本立て。それぞれビルドする。

    サーバ側はrustup.rsを使ってrustのnightlyを入れておく。pkg install rust-nightlyでも良さそうだが、どうもいつもcargoがコケるのでrustupを使っている。

    rust(とcargo)を入れたらサーバをビルドする。
    そのままだとsimdjsonのビルドが失敗するので、.cargo/config.toml に以下を書き加える。

    [target.x86_64-unknown-freebsd]
    rustflags = ["-C", "target-feature=+sse2,+ssse3,+sse4.1,+sse4.2"]
    

    書き加えたら、cargo build --releaseでビルドする。

    WebUIはnodeとnpmでビルドする。
    ちなみにpkg install node npm したものだが、node18-18.16.0とnpm-node18-9.7.2になっている。
    まずはビルドに必要がなく、FreeBSDがunsupportedなCypressをpackage.jsonからもぎ取っておく。diffだとこんな感じ:

    *** web/package.json.orig       Mon Sep 11 08:34:13 2023
    --- web/package.json    Mon Sep 11 08:37:21 2023
    ***************
    *** 14,21 ****
          "copy": "cd dist && mkdir src && cd src && mkdir assets && cd .. && cd .. && cp -r src/assets/* dist/src/assets/",
          "test:unit": "vitest --environment jsdom --root src/",
          "test:unit:coverage": "vitest --coverage",
    -     "test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
    -     "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
          "build-only": "vite build",
          "build-only-alpha1": "vite build --mode=alpha1",
          "build-only-cloud-prod": "vite build --mode=production",
    --- 14,19 ----
    ***************
    *** 52,59 ****
          "vuex": "^4.0.2"
        },
        "devDependencies": {
    -     "@cypress/vue": "^4.0.0",
    -     "@cypress/webpack-dev-server": "^2.0.0",
          "@quasar/vite-plugin": "^1.0.10",
          "@rushstack/eslint-patch": "^1.1.0",
          "@types/d3-hierarchy": "^3.1.2",
    --- 50,55 ----
    ***************
    *** 71,82 ****
          "@vue/test-utils": "^2.0.2",
          "@vue/tsconfig": "^0.1.3",
          "c8": "^7.11.3",
    -     "cypress": "^10.3.0",
    -     "cypress-localstorage-commands": "^2.2.1",
          "dotenv": "^16.0.3",
          "eslint": "^8.5.0",
          "eslint-config-prettier": "^8.5.0",
    -     "eslint-plugin-cypress": "^2.12.1",
          "eslint-plugin-vue": "^9.5.1",
          "fs-extra": "^11.1.1",
          "happy-dom": "^6.0.4",
    --- 67,75 ----
    

    変更したら、npm inpm run buildでビルドする。
    distに成果物が入るので、ここをnginxなどで公開する。(後述)

    それぞれビルドしたら起動する。
    WebUI側はstatic contentsなので、nginxなどでserveする。
    nginx.confはこんな感じだろうか:

    worker_processes 1;
    events {
        worker_connections 1024;
    }
    http {
        include mime.types;
    
        sendfile on;
        keepalive_timeout 65;
    
        server {
            listen 80;
            server_name openobserve.coco.local;
            location / {
                proxy_pass http://127.0.0.1:5080;
            }
            location /assets {
                alias /openobserve/web/dist/assets;
                index index.html;
            }
            location /web {
                alias /openobserve/web/dist;
                index index.html;
            }
        }
    }
    

    サーバプロセスは環境変数を設定して起こす。
    一覧はこちら: https://openobserve.ai/docs/environment-variables/
    たとえばこんな感じ。
    ZO_LOCAL_MODE_STORAGEはたぶん要らない。ZO_TELEMETRYとZO_PROMETHEUS_ENABLEDはお好みで。
    今回はminio/etcd/openobserveを全サーバにそれぞれ入れたので127.0.0.1になっているが、そこは環境に合わせて適宜調整。

    ZO_LOCAL_MODE="false"
    ZO_LOCAL_MODE_STORAGE="s3"
    ZO_GRPC_ORG_HEADER_KEY="cluster"
    ZO_TELEMETRY="false"
    ZO_PROMETHEUS_ENABLED="true"
    ZO_ETCD_ADDR="http://127.0.0.1:2379"
    ZO_ROOT_USER_EMAIL="user@localhost.local"
    ZO_ROOT_USER_PASSWORD="super-complex-password"
    ZO_S3_ACCESS_KEY=""
    ZO_S3_SECRET_KEY=""
    ZO_S3_REGION_NAME="any"
    ZO_S3_BUCKET_NAME="openobserve"
    ZO_S3_SERVER_URL="http://127.0.0.1:9000"
    ZO_S3_PROVIDER="minio"
    

    これらをexportしたら引数なしでopenobserveを起動すればよい。

    ところでOpenObserveのクラスタ構成はガイドがなくてやや不親切。
    ただ、k8s用helm chartがあるので、こいつを見ながら設定していた。

    https://github.com/openobserve/openobserve-helm-chart/blob/main/values.yaml

    OpenObserveではまったところ

    ちなみにZO_GRPC_ORG_HEADER_KEYの値を設定し忘れて検索するとエラーになる。これが非常に分かりづらい。
    openobserveには以下のログが出ていた。

    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:enter; org_id="default" stream_type=Logs                                                                                
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster; org_id="default"                                                                                               
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::sql] service:search:sql:new; org_id="default"                                                                                          
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] get_file_list; stream_type=Logs time_level=Unset org_id="default" stream_name="default"                                                
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] search->file_list: time_range: Some((1694914712941000, 1695001112941000)), num: 114, offset: 39
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster:grpc_search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster:grpc_search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster:grpc_search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::handler::grpc::request::search] grpc:search:enter; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc] service:search:grpc:search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::sql] service:search:sql:new; org_id="default"
    [2023-09-18T01:38:46Z ERROR openobserve::service::search] search->grpc: node: 2, search err: Status { code: Unauthenticated, message: "No valid auth token", metadata: MetadataMap { headers: {"c
    ontent-type": "application/grpc", "date": "Mon, 18 Sep 2023 01:38:46 GMT"} }, source: None }                             
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc] service:search:grpc:in_wal; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc] service:search:grpc:in_storage; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] service:search:grpc:storage:enter; org_id="default" stream_name="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] search->storage: org default, stream default, load file_list num 39
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] search->storage: org default, stream default, load files 39, scan_size 17390808, compressed_size 1598007
    [2023-09-18T01:38:46Z INFO  tracing::span] service:search:grpc:storage:cache_parquet_files;
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] service:search:wal:enter; org_id="default" stream_name="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] service:search:grpc:wal:get_file_list; org_id="default" stream_name="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] wal->search: load files 2, scan_size 34366
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] service:search:grpc:wal:datafusion; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] search->storage: org default, stream default, load files 39, into memory cache done
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] service:search:grpc:storage:datafusion; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  tracing::span] datafusion::storage::memory::list;
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query took 0.008 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query all took 0.009 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query took 0.011 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query all took 0.011 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] search->grpc: result node: 3, is_querier: true, total: 0, took: 31, files: 36, scan_size: 22
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] search->grpc: result node: 1, is_querier: true, total: 0, took: 41, files: 41, scan_size: 16
    [2023-09-18T01:38:46Z ERROR openobserve::handler::http::request::search] search error: ErrorCode(ServerInternalError("search node error"))
    [2023-09-18T01:38:46Z INFO  actix_web::middleware::logger] 10.33.3.28 "POST /api/default/_search?type=logs HTTP/1.0" 500 94 "141" "http://openobserve1.local/web/logstreams/stream-explore?s
    tream_name=default&stream_type=logs&org_identifier=default" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0" 0.148536
    

    ポイントは以下のログであろう。

    [2023-09-18T01:38:46Z ERROR openobserve::service::search] search->grpc: node: 2, search err: Status { code: Unauthenticated, message: "No valid auth token", metadata: MetadataMap { headers: {"c
    ontent-type": "application/grpc", "date": "Mon, 18 Sep 2023 01:38:46 GMT"} }, source: None }                             
    [2023-09-18T01:38:46Z ERROR openobserve::handler::http::request::search] search error: ErrorCode(ServerInternalError("search node error"))
    

    openobserver-2で検索を要求した場合、openobserve-1側に以下のようなログが残った。

    [2023-09-18T01:39:50Z INFO  openobserve::handler::grpc::auth] Err authenticating Invalid value provided for the HTTP Authorization header
    

MastodonからFedibirdに移行する

Mastodon 3.1.2からFedibirdに乗り換えた。(on FreeBSD 13.2-RELEASE)
久しぶりに地獄を味わった気がする。

とりあえずメモを残しておく。きっと、もっとエレガントな方法があるはずだが、それでも記録は記録なのだ。

  • Rubyは2.xである必要あり。3.xでもナントナクは動くけど、時々 **dict の扱い違いが原因のエラーが出るので危険。
  • ruby27はpkgがあるけど、ruby-gemsはpkgがないのでソースコードから ./configure && make install が必要。
  • idn-ruby gemはinclude pathの都合があるので、 bundle install の前に gem install が必要。
  • db:migrateは気合と根性が必要。かもしれない。
  • tootctl cache clearしないとあるはずのデータ(列)が見えなかったりして大混乱する。

追記は末尾にて。

ruby 2.7

https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.7.tar.gz から拾ってきて、 ./configure && make install すればいいだけ。

idn-ruby

$ pkg install libidn2
$ gem install idn-ruby -v 0.1.2 -- --with-idn-dir=/usr/local

db:migrate

ここが一番しんどかったところ。rake db:migrate してもいろいろコケる。
ここはruby 3.xでやっていたのが悪かったかもしれないが、今となっては確かめる気にもならない。
あと、mastodon側も以前migrateが失敗して誤魔化したことがあるので、一部はソレかもしれない。

ざっくり以下の2手順。

  • db:migrateを気合で通す
  • 余分なテーブルと列を葬る

db:migrateを気合いで通す

PG::DuplicateColumn: ERROR:  column "list_id" of relation "keyword_subscribes" already exists
/home/mastodon/db/migrate/20191025190919_add_list_to_keyword_subscribes.rb:5:in `change'

PG::DuplicateColumn: ERROR:  column "list_id" of relation "account_subscribes" already exists
/home/mastodon/db/migrate/20191026110416_add_list_to_account_subscribes.rb:5:in `change'

PG::DuplicateColumn: ERROR:  column "list_id" of relation "follow_tags" already exists
/home/mastodon/db/migrate/20191026110502_add_list_to_follow_tags.rb:5:in `change'

このへんは既に存在する列の追加なので無視。scriptを空にしてスルーさせる。

NameError: uninitialized constant AddIndexURLToStatuses
Did you mean?  AddIndexUrlToStatuses

これは……なんだろうね?動けばよろしいの精神で直す。

db/migrate/20200312020757_add_index_url_to_statuses.rb:
- class AddIndexUrlToStatuses < ActiveRecord::Migration[5.2]
+ class AddIndexURLToStatuses < ActiveRecord::Migration[5.2]

db/migrate/20200827204602_add_inbox_url_to_conversations.rb:
- class AddInboxUrlToConversations < ActiveRecord::Migration[5.2]
+ class AddInboxURLToConversations < ActiveRecord::Migration[5.2]
No such column: custom_filters.whole_word
/home/mastodon/db/migrate/20230106051103_change_default_of_whole_wotd_in_custom_filter.rb:3:in `change'

これはウチの環境が悪いであろうもの。手動で直す。

(echo 'require("./db/migrate/20180707154237_add_whole_word_to_custom_filter.rb")'; echo "AddWholeWordToCustomFilter.new.change" ) | RAILS_ENV=production bundle exec rails runner -
== 20230221031206 AddWidthHeightToCustomEmoji: migrated (0.0017s) =============

rake aborted!
NameError: uninitialized constant CreateNodeInfo

そんなバカな?これも動けばいいので直す。

db/migrate/20230310214919_create_nodeinfo.rb

- class CreateNode < ActiveRecord::Migration[6.1]
+ class CreateNodeInfo < ActiveRecord::Migration[6.1]

記録してあるのはこのへんまで。これで rake db:migrate は成功するが、まだまだおかしい。

余分なテーブルと列を葬る

db/schema.rb の通りにすればいいんだけど、手動でこれをやるのは困難。特にindexとか。

まずは差分をチェックする。
今のデータベースからschema.rbを作って、diffで見ていく。

$ mv db/schema.rb db/schema.rb.fedibird
$ RAILS_ENV=production bundle exec rake db:schema:dump
$ mv db/schema.rb db/schema.rb.old
$ sdiff --width=240 db/schema.rb.old db/schema.rb.fedibird
$ cp db/schema.rb.fedibird db/schema.rb

fedibird側で追加されているものはdefaultに期待してスルー。
逆にfedibird側でないデータはrestore時にコケるので、1つずつDROP TABLEまたはDROP COLUMNしていく。

余計なものがなくなったら、SQL形式でdumpを作る。

$ pg_dump --data -U mastodon mastodon > /でかいよ/mastodon-db-data-only.sql

ここでDBを完全に消して(別途DROP DATABASE)、db:setupで作り直す。先の手順でdb/schema.rbを戻し忘れていると悲しいので注意。

$ RAILS_ENV=production bundle exec rake db:setup

初期用のデータが入るので消す。ただし、schema_migrationは必要なのでとっておく。

$ pg_dump --data --schema=schema_migrations -U mastodon mastodon > /これは大きくない/schema-migrations.sql
$ cat << EOM | psql -U mastodon mastodon
TRUNCATE accounts CASCADE;
TRUNCATE ar_internal_metadata CASCADE;
TRUNCATE oauth_applications CASCADE;
TRUNCATE schema_migrations;
EOM

まっさらチェック。

$ pg_dump -U mastodon --data --insert mastodon | grep INSERT

データを復元する。ここで、循環参照のあるforeign keyを復元させるため、一時的にmastodonユーザにsuperuser権限が必要。終わったら権限は戻しておく。

$ (echo "set session_replication_role = replica;"; cat /でかいね/mastodon-db-data-only.sql) | psql -U mastodon mastodon

schema_migrationsを上書きする。

$ echo "TRUNCATE schema_migrations;" | psql -U mastodon mastodon
$ psql -U mastodon mastodon < /これは大きくない/schema-migrations.sql

途中、何度か時間や手間のかかる処理があるので、適宜バックアップと取らないと辛いことになる。(なった)

追記分

db:migrate関連の失敗で、過去分のmedia_attachments.shortcodeがNULLになっていた。
これがあると、プロフィールのページが開けなかったりする。
対応するのは 20170105224407_add_shortcode_to_media_attachments.rb
SQLを直接実行して解決した。

UPDATE media_attachments SET shortcode = id WHERE shortcode IS NULL AND remote_url = '';

SambaにINTERNAL_ERRORされた

ついうっかりSambaを更新したら、Firefoxでファイルをダウンロードしたときに失敗するようになってしまった。

ネットワークからの取得は終わっていて.partファイルはちゃんとできている。しかし、その後.partファイルから本来のファイルに差し替えるところで失敗しているように見える。
あとに残るのは .zip.part みたいなファイル(中身はOK)と、0バイトな .zip ファイル。

うんうん唸りながらログを集めていたところ、Sambaがrenameに対してINTERNAL_ERRORを返しているのを見つけた。
前後のログからどうもoplockが怪しい、ということで oplock = no を指定したら解消した。
少なくとも、20回やって毎回失敗していたのが成功して、続けて2件のダウンロードも成功したので、大丈夫であろう。

oplockを有効にすると30%ほどもパフォーマンスが上がるよ!とあるが、失敗するのでは話にならないので、とりあえずこれで。

Wiresharkで見た様子:

log.smbdの様子: (↑とは違うタイミング)

[2023/02/11 12:49:27.186686,  5, pid=63759, effective(1001, 0), real(0, 0), class=locking] ../../lib/dbwrap/dbwrap.c:180(dbwrap_lock_order_unlock)
  dbwrap_lock_order_unlock: release lock order 1 for /var/db/samba4/locking.tdb
[2023/02/11 12:49:27.186922, 10, pid=63759, effective(1001, 0), real(0, 0), class=locking] ../../source3/lib/dbwrap/dbwrap_watch.c:1027(dbwrap_watched_watch_state_destructor_fn)
  dbwrap_watched_watch_state_destructor_fn: Watcher 63759:6 not found
[2023/02/11 12:49:27.187162,  3, pid=63759, effective(1001, 0), real(0, 0), class=smb2] ../../source3/smbd/smb2_server.c:3957(smbd_smb2_request_error_ex)
  smbd_smb2_request_error_ex: smbd_smb2_request_error_ex: idx[5] status[NT_STATUS_INTERNAL_ERROR] || at ../../source3/smbd/smb2_server.c:2053
[2023/02/11 12:49:27.187387, 10, pid=63759, effective(1001, 0), real(0, 0), class=smb2] ../../source3/smbd/smb2_server.c:3849(smbd_smb2_request_done_ex)
  smbd_smb2_request_done_ex: mid [5705] idx[5] status[NT_STATUS_INTERNAL_ERROR] body[8] dyn[yes:1] at ../../source3/smbd/smb2_server.c:4005

bhyveでPXE Bootしたい

簡単だった。

言うまでもないけど、ff:ff:ff:ff:ff:ff は好きなMACアドレスに書き換えてほしい。ほかのパラメータも、ね。

# pkg install bhyve-firmware
# bhyve -AHP -s 0:0,hostbridge -s 1:0,lpc -s 2:0,virtio-net,tap0,mac=ff:ff:ff:ff:ff:ff -c 1 -m 256M -l bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd -l com1,stdio guest

参考はこちら。

ポイントは BHYVE_UEFI.fd は放っておけば IPv4 PXE Boot してくれるということ。
なので、PXE Bootしたいからオプションを探して……と悩む必要はない。起動可能なドライブを接続しなければいいのだ。

Compressorで出力したビデオに日付を付けてPhotosに認識させたい

Compressor でなにも考えずに圧縮したビデオを Photos に入れると、作成日がビデオの日付として使われる。
昔のビデオを加工したときに期待通りに並ばずに腹立たしいので、日付をつけたい。

そんなときは、Job Annotations で Date を付けるとよい。
Job Annotations は Job を選んでいるときに設定できる。

Date の形式は任意の文字列になっていて、どんな文字列も受け付けてくれる。
が、適当な形式では Photos 側が認識してくれない。
いくつか試したところ、少なくとも 2022-07-07T23:59:59+09:00 のような ISO 8601 形式は受けつけてくれることが分かった。

これで、整理していなかったビデオも整理して Photos に入れられる。