ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

Ubuntu 22.04.1 ある日突然 Dummy Output になった問題

ある日突然音声出力が Dummy Output に

apt full-upgradeしたら 突然Soundが Dummy Output となって音声がでなくなる現象に遭遇しました.
PluseAudio絡みの問題かと思い、色々調べてみますが一向に解決せず。

askubuntu.com

apt upgradeで誤ったカーネルがインストールされてしまっていた

よくよく調べてみると、音声がでなくなるだけでなくいろいろ起動しなくなっている様子。

  • ディスプレイが自動でOFFにならない
  • VirtualBoxがエラーで起動しない

これはPluseAudio絡みではなくカーネルの問題ではないか・・・?、とカーネルのバージョンを調べたら・・

ん??

$ uname -r 
5.15.0-1025-oracle

なんて知らないバージョンが勝手にinstallされて使われてました.
どうやら最近のupdateで起こったバグのようです.

askubuntu.com

ネットワークなんかも動かなくなっているケースがあるようですね..

issueも上がっていました.

bugs.launchpad.net

ということでGrub2で正しいカーネルを指定して再起動し、知らないバージョンのカーネルイメージを削除して無事解決しました.

$ sudo apt remove linux-image-5.15.0-1021-intel-iotg linux-image-5.15.0-1025-oracle linux-modules-5.15.0-1021-intel-iotg linux-modules-5.15.0-1025-oracle linux-modules-5.15.0-56-lowlatency linux-image-5.15.0-56-lowlatency

参考サイト

VirtualBox の Guest OS 上でビルドすると internal compiler error が多発する現象 (Ubuntu 22.04.1)

Guest OS 上でビルド中 internal compiler error が発生してビルドできない・・・

ある日突然仮想マシン上の Guest OSでビルドすると internal compiler error が多発する現象に遭遇.

apportのログを見ると、ビルド中に Segmentation Fault が発生してビルドが中断している模様.
しかし、コードに問題があるわけではなく発生箇所は毎回変化し、ビルドを再開すると、しばらくビルドが進んでまた internal compiler error が発生.

これ、 通常であればコンパイラバグ、ハードウェアの問題かカーネルバグを疑うべきところなんですが・・・

  • 今回は、エラーが毎回違う場所で発生しているので、コンパイラのバグの可能性は薄い.
  • Guest OS でのみ発生しており、Host OS 側は何の問題もありませんから、マシンのハードウェアの可能性は薄い.
  • Guest OS のメモリ不足も疑うべきですが、Guest OS へのメモリ割当ては16Gと十分でこれも可能性は薄い.

となると・・残るはVirtualBoxが怪しいということに・・

Ubuntu公式リポジトリVirtualBoxとホスト側カーネルの不整合が原因

VirtualBoxUbuntu 22.04.1 で似たような問題が起きてないか漁っていると・・・
以下に同じ現象がpostされているのを発見.

askubuntu.com

この現象、 ホスト側カーネルlinux-image-5.15.0-47-generic だと発生し、一つ前のカーネル linux-image-5.15.0-46-generic では発生しないそうで.
カーネルバージョンとvirtualboxのバージョンの組み合わせで発生する問題なのだとか・・・.

公式リポジトリのパッケージの組み合わせで発生しちゃうので、apt-get dist-update (apt full-upgrade) したらある日突然に、新規で環境を作る場合はいきなりこの現象に遭遇することになるわけです.
これは結構・・心臓に悪いですね・・・.

公式リポジトリVirtualBoxOracle提供のものに変更すれば解決.

解決にはOracleが提供するVirtualBoxの最新版に変更すればよいようです.

Ubuntu公式リポジトリ由来の virtualbox と関連パッケージをすべて削除して、Oracle版のもののみをインストールします.
aptの依存関係による自動削除に任せるとUbuntu公式由来の依存パッケージが残ってしまうことがあるので、packageの依存関係を確認して、削除するパッケージを各々個別に指定して確実に削除する必要があります .
Ubuntu公式リポジトリ由来のパッケージが残っていると、ゲストOS起動時にエラーになったりCrashしたりしますので注意.

# Oracleのリポジトリと公開鍵を追加
$ sudo echo "deb [arch=amd64 signed-by=/usr/share/keyrings/oracle-virtualbox-2016.gpg] https://download.virtualbox.org/virtualbox/debian jammy contrib" > /etc/apt/sources.list.d/virtualbox.list
$ wget -O- https://www.virtualbox.org/download/oracle_vbox_2016.asc | sudo gpg --dearmor --yes --output /usr/share/keyrings/oracle-virtualbox-2016.gpg
# パッケージ情報更新
$ sudo apt update

# 公式リポジトリ由来のvirtualboxと関連パッケージを明示的に削除.
$ sudo apt purge virtualbox virtualbox-dkms virtualbox-ext-pack virtualbox-qt
# Oracleのvirtualboxをインストール
$ sudo apt install virtualbox-6.1

Oracle版の VirtualBox を立ち上げて Guest OS が問題なく起動できれば無事完了です.
これで、ビルド時に internal compiler error が頻発する現象も解消されました.

参考サイト

These articles have been very helpful for me. thanks!!

埋め込みpythonでのみモジュールのModuleLoadErrorが起こる問題

埋め込みpythonでのみモジュールがModuleLoadErrorを起こす!?

C++アプリケーションへpythonコードを埋め込んで使用していると、pythonコード内でimportしているモジュールの中に、ModuleLoadErrorを起こして正しく動作しなくなるものがあります.
debugpyも例外ではなく、エラーが出てC++アプリへ組み込んだ状態でのpythonコードのデバッグができません・・・

しかし、問題を起こすpythonコードをpythonインタプリタから単独で実行したときは何の問題もなく動作するんですよね.
これはハマりました.

pythonモジュール側のシンボルが解決できないのが原因

どうも _ctypes が存在しないとかなので、pythonランタイム側のシンボルが見えていないようですが・・・

原因の可能性の一つは、libffi問題.
zv-louis.hatenablog.com
こちらは過去の記事にて解決済み.
事実、python インタプリタで実行した場合は動作しています.
明らかに別の問題です・・・

色々探してると、この問題に関する記事がありました.

stackoverflow.com

C/C++の拡張モジュールが、pythonモジュール pythonXX.so 内のシンボルを参照している場合に、実行時にシンボル参照を解決できないと発生する問題、とのこと.

いわれてみれば、現象が発生するモジュールはすべてC/C++拡張タイプのモジュールであるという特徴があります.
これは盲点でした.

確かに、C/C++拡張モジュールを作るとき、pythonXX.so を参照させるようにする必要がありました.
当然、実行時のシンボル解決には pythonXX.so のシンボルが見えていなくてはなりません.
ところが、拡張モジュールはどこにデプロイされてどこからロードされるかが不定なライブラリですから、事前にシンボル解決で参照する依存先をパスで指定することなどはできず、シンボル解決はもっぱら 参照可能なロード済みシンボル に依存せざるを得ないのでした.

組み込み先のシンボルを可視化する

--export-dynamicを指定してリンクする

pythonコードを組み込むモジュールのリンク時にリンカオプション -export-dynamic 指定をする.
これは、ロード済みシンボルを後でロードするモジュールのシンボル解決に使用可能にするオプション.

この件については、以下にリンカオプション例が示されています.
ちゃんと読まなきゃダメですね.

docs.python.org

python組み込み先が共有ライブラリの場合の注意点

pythonコードを組み込んだモジュールが共有ライブラリの場合、別の問題があります.

組み込んだ共有ライブラリをロードするときにdlopenを使う場合、--export-dynamic のリンク指定は無視されて、dlopenのロードオプションが適用されます.
ほとんどの実行環境では dlopen の RTLD_LAZY, RTLD_LOCAL いずれかがデフォルトになっているために、pythonモジュールがロードされるタイミングで必要なシンボルがすでにロードされて参照可能となっておらず、モジュールで必要なシンボルを解決できないことがあるので注意が必要です.

確実にロード済みシンボルを参照できるようにするには、 dlopenを使用するときに RTLD_NOWRTLD_GLOBAL オプションを明示的に指定する必要があります.

    // hoge.soへpythonXX.soをリンクして組み込んでいる場合
    void* hmod = nullptr;
    std::string mod_path("hoge.so");
    hmod = dlopen(mod_path.c_str(), RTLD_NOW | RTLD_GLOBAL);

リファレンス

debugpyを使ったリモートデバッグ

直接起動できないpythonコードのデバッグに便利

インタプリタやデバッガで直接開始できないpythonコードをデバッグするには、 多くの場合リモートデバッグを使いますね.

pythonでは debugpy というモジュールを使います. github.com

組み込みpythonのdebugに使用できるので便利です.

Step1. 対象コードにdebugpyモジュールのコードを埋め込む

デバッグ対象のコードにデバッグサーバを立ち上げて、接続待機するコードを埋め込みます.

    import debugpy

    # port : 受付port
    port = 5678
    debugpy.listen(port)
    # addressとportを指定する場合はtupleでまとめて指定
    # debugpy.listen((address, port))

    # デバッガが接続されるまで待機
    debugpy.wait_for_client()

たったこれだけ.

Step.2 launch設定

Visual Studio Codeのlaunch設定に, pythonのリモートデバッグ設定を追加します.

// リモートデバッグ用
{
    "name": "debugpy remote debug (Attach)",
    "type": "python",
    "request": "attach",
    "connect": {
        "host": "localhost", // debugpy.listen している接続先ホスト
        "port": 5678           // debugpy.listenしているポート
    },
},

Step.3 接続とアタッチ

debugpy実行側のpythonコードを実行すると、wait_for_client のところで接続待機するので、デバッガを起動してアタッチします.

初回起動時などは、サーバ側が立ち上がるまで数秒かかることがあるので注意.
接続エラーが出た場合は、2〜3秒ほど待ってから再度接続を試みると良いでしょう.

Windowsの場合は特殊な準備が必要

Windowsで使用する場合は、サーバ起動前にdebugpyにインタプリタ環境へのパスを設定してやらないと接続に失敗します.

以下にissueがありました.

github.com

こんな感じです.

# Only windows.
# debugpyにインタプリタパスを通知する
python_path = Path(sys.prefix).joinpath('python.exe')
debugpy.configure(python=str(python_path))

debugpy.listen(address_or_port)
debugpy.wait_for_client()

関数にしておくと便利

まとめて関数化しておくと便利です

def debug_wait_for_attach(listen_to):
    if os.name == 'nt':
        # for windows.
        python_path = Path(sys.prefix).joinpath('python.exe')
        debugpy.configure(python=str(python_path))
    debugpy.listen(listen_to)
    debugpy.wait_for_client()

リファレンス

C/C++アプリケーションへPythonを埋め込む

アプリケーションの一部をPythonで作る

pythonの公式Docには組み込み可能なpythonパッケージの記述があります.
これによると、Windows版にはアプリと一緒に配布できるpython環境があるそうです.

ドキュメントを見るととても容易にできるようなので、試してみました.

(準備) 通常のパッケージをインストール

組み込みpythonのパッケージには、ビルドに使用するヘッダやリンクライブラリ(.lib)が付属していません。 このため、通常パッケージを使ってビルドして、デプロイ時にembededdable版を配置するようにします.

以下からembeddable版と対応する通常版をDownloadしてインストールしておきます.

Python Releases for Windows | Python.org

テスト用最小コード

以下、テスト用の最小コードです.
いくつか注意点があります.

#define PY_SSIZE_T_CLEAN

// デバッグ版へのリンクを避けるため _DEBUG を一時的に無効化.
#ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
#else
    #include <Python.h>
#endif

// 使用する実行環境のPythonインタプリタのパス
// ここで指定するインタプリタパスを利用してpythonモジュールの探索先など実行環境のパスが決まる.
// ここにvenvで作った仮想環境のpythonインタプリタパスを指定した場合、
// 仮想環境下でランタイムが動作する
#define PYTHON_EXE_PATH u8"Path to python"

int main(int argc, char** argv)
{
    int result = 0; 
    // このポインタはFinalizeが完了するまで保持する
    wchar_t* program = Py_DecodeLocale(PYTHON_EXE_PATH, NULL);
    if (program == nullptr) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
    }
    else {
 
        // 使用したい実行環境のpythonインタプリタのパスを指定
        Py_SetProgramName(program);

        // 初期化
        Py_Initialize();

        // Python実行環境の参照パスを出力してみる
        PyRun_SimpleString("import sys");
        PyRun_SimpleString("print(sys.path)\n");
        PyRun_SimpleString("print(sys.prefix)\n");
        PyRun_SimpleString("print(sys.exec_prefix)\n");

        constexpr int ERROR_RET      = 1;
        constexpr int SUCCESS_RET  = 0;

        // 環境終了処理
        int finalize_result = Py_FinalizeEx();

        int result = (finalize_result < 0) ? ERROR_RET : SUCCESS_RET;
        PyMem_RawFree(program);
    }
    retrurn result; 
}

(注意点1) embeddableにはDebug版が付属しない

Windowsの場合Python.h内に pragma があり、自動的にヘッダに対応する環境のpythonXX.libが参照されるようになっています.
ただし、マクロ _DEBUG が定義されているとデバッグ版 pythonXX_d.lib のほうがリンクされてしまいます.

embeddable python環境にはデバッグ版dllは付属していないので、 デバッグ版へのリンクを避けるため _DEBUG を一時的に無効化します

// デバッグ版へのリンクを避けるため _DEBUG を一時的に無効化.
#ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
#else
    #include <Python.h>
#endif

(注意点2) Py_SetProgramNameにはインタプリタのパスを指定する

Python/C API Reference Manual — Python 3.10.5 documentation
Py_SetProgramNameの使い方について、公式Docをみると以下の説明がありました.

This function should be called before Py_Initialize() is called for the first time, if it is called at all. It tells the interpreter the value of the argv[0] argument to the main() function of the program (converted to wide characters). This is used by Py_GetPath() and some other functions below to find the Python run-time libraries relative to the interpreter executable.

あれ?

文章の後半では「インタプリタ実行ファイルからの相対パスからpythonのランタイムライブラリを見つける」とあるのに、 文章の前半では、「(C/C++のmain関数の)引数のargv[0] を入れる」との記述があり、前半と後半の文章の内容に微妙な食い違いがあります.

不思議に思って実際にpythonソースコードを追ってみたところ・・・
どうやら「argv[0] を入れろ」との記述は、 argv[0] に入るのがpythonインタプリタ実行ファイルパスであることを想定しているためのようです.

確かに、pythonファイル(.py)の実行だけを想定すれば、 argv[0]には常に python 実行ファイルへのパスが入ることになりますね.
しかし、pythonランタイムをC/C++アプリケーションに埋め込む場合は、この前提が当てはまりません.
公式Docの説明は、文章の後半は正確な内容ですが前半には表現に誤りを含んでいると解釈するのが良さそうです.

ここは 使用する実行環境のインタプリタ実行ファイルのパス と読み替えて、
使用したい実行環境に配置されているインタプリタ実行ファイル(python.exe もしくは python)へのパスを指定します.

    // **使用したい実行環境のpythonインタプリタ実行ファイルのパス** を指定するっぽい
    //「プログラムファイルパス」は単純にmainの引数argv[0]を入力するというわけではなさそう.
    Py_SetProgramName(program);

実際、ProgramNameにインタプリタ絶対パスを指定してみると、組み込んだ環境のモジュール参照パス sys.path が、指定したインタプリタのある実行環境のものに変化します.
インタプリタパスはvenvで作られた仮想環境のものであっても良く、仮想環境のインタプリタパスを指定した場合は、仮想環境を参照するようにsysのモジュール参照パスが初期化されます.

システムのpythonがインストールされた環境でも、venvなど独立した仮想環境を作ってアプリに組み込めるのは利便性が高まりますね.

結構使えそう

Initializeが完了すれば、あとはPythonの対話モードを起動した状態に似ています.
pythonの対話モードを起動した状態でC/C++コードを使ってコードを流し込んでいく感じです.

    // Python実行環境のモジュール参照パスを出力
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("print(sys.path)\n");
    PyRun_SimpleString("print(sys.prefix)\n");
    PyRun_SimpleString("print(sys.exec_prefix)\n");

かなり手軽に利用できます.
python機械学習/深層学習フレームワークで学習したモデルをアプリに組み込むときの有効な選択肢になりそうですね.

リファレンス

家庭内LANでIPv6のグローバルIP(GUA)とLAN内専用の固定IP(ULA)の両方を使う

プライベートLAN内専用の固定IPv6アドレスがほしい

IPv6対応の家庭用ルータが普及してきた

市販の家庭用ルータを使う我が家のネットワークでは、LAN内はIPv4固定IPアドレスを各マシンに割り当ててsshしたりプライベートなサーバ立てたりして使ってたんですが、昨今では、プロバイダからIPv4のアドレスだけでなくIPv6プレフィックスが当然のように割り当てられるようになりました.

いよいよIPv6対応が当然の前提とされるようになると、アプリなどでIPv6アドレスでのテストなどもしなきゃダメなので、IPv6の固定アドレスなんて欲しくなります.

IPv6のリンクローカルアドレスはどうにも使いづらい

通常、プロバイダの接続サービスと家庭用ルータがIPv6に対応していて、マシン側のIPv6が有効になっていれば、プロバイダからグローバルネットワーク識別子の払い出しを受けたルータからのRA(Router Advertisement; ルータ広告)によって、ローカルネットワーク内の各マシンでグローバルスコープのGUA(グローバルユニキャストアドレス)が自動で構成されます.
それと同時にローカルネットワーク内専用のリンクローカルアドレス(fe80::/64)が各マシンで自動的に生成されて、固定IPのように使えることになってるのですが、リンクローカルアドレスを通信で使うには、IPv6アドレスに加えてScopeIDなんてのが必要だったりします.
このScopeID、困ったことにツールやアプリごとに指定する方法が異なっている上に場合によっては対応していなかったり、どうにもこうにも使いづらいんですよね.

自動構成のGUAと固定のULAを同時に持たせれば・・?

幸いIPv6は一つのインタフェースに対してアドレスを複数持つことが出来る仕組みなので、自動構成によって割り当てられるグローバルIP(GUA)を持たせつつIPv4のプライベートアドレスっぽく使えるULA(ユニークローカルアドレス)を各マシンに固定IPとして割り当てて使うことは可能なはずです.

しかしながら、LinuxのNetworkManagerやmacOSでは、ネットワーク設定などでIPv6アドレスを 手動設定 とすると、ルータからのRAによるGUAの自動構成が行われなくなるため、ローカルネットワークの外とIPv6での通信が行えなくなり、自動構成のGUAを使うのか手動割当のULAかいずれかを選択する、といった状況になってしまいます.
さすがにこれでは本末転倒です.

そこで、もうちょっとなんとかできないかな〜、と色々調べてみました.

あとから追加でULAを割り当てる方法なら意外に簡単

GUAの自動構成はとりあえずそのままにして、ULAを自動構成させるにはルータ側でULAを自動構成させるための設定ができないといけませんが、さすがにそんな機能、安価な家庭用ルータに備わってる訳ありません.

しかし、あとから各マシンでコマンドを使ってULAを追加割当てする方法であれば、比較的に容易に実現できそうです.

ここではLinuxmacOSの手順を示します.

(準備) ULAのサブネットプレフィックスを準備する

ULAはプライベートネットワーク内での使用が想定されているとはいえ、グローバルスコープのUnicastAddressとして定義されてるものですから、SubnetPrefix部分はちゃんと RFC4193 に従ってEUI-64識別子を生成しなければなりません.

ただ、このEUI-64識別子の生成は手計算では到底無理なのでツールを使います.

こちらに生成用のpythonコードが公開されてますので、これを使いました.
www.jdoodle.com

ネットワーク内の代表マシンを適当に一つ選び、そのマシンが使用するネットワークインタフェースのMACアドレスから識別子を生成します.

$ ./ula_generator.py
Input MAC address: 12:34:56:78:ab:cd  # ←ここにMACアドレスを指定する

ULA Prefix        -> fd98:c3f9:eb41::/48
First Subnet      -> fd98:c3f9:eb41::/64
Last Subnet       -> fd98:c3f9:eb41:ffff::/64
First IPv6 Address-> fd98:c3f9:eb41::1/64

ULA Prefixが得られたので、サブネット識別部を 1 (0001) とし、fd98:c3f9:eb41:1::/64 をサブネットプレフィックスとします.

(Step1) とりあえず手動で割り当てて試してみる

サブネットプレフィックスができたら、まずは手動でULAを追加割り当てしてみます.
とりあえずこれでGUAとULAとリンクローカルアドレスの3タイプのIPv6アドレスを持つ状態にすることが出来ます.

(Linuxの場合) ip コマンドを使う

Linux系OSは ipコマンド で割り当てます.

# (コマンド)
$ sudo  ip -6 addr add {IPv6Addr}/{PrefixLength} dev {IntarfeceName}

# (例: eno1 に fd98:c3f9:eb41:1::2/64 を割り当て)
$ sudo ip -6 addr add fd98:c3f9:eb41:1::2/64 dev eno1

コマンドを実行したら、 ifconfig コマンドでネットワークの設定を確認します.

$ ifconfig eno1
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.2  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:db31  prefixlen 64  scopeid 0x0<global>
        inet6 fd98:c3f9:eb41:1::2  prefixlen 64  scopeid 0x0<global>  ★追加されている
        inet6 fe80::7404:eeb4:6b91:34b8  prefixlen 64  scopeid 0x20<link>
        inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:b06f  prefixlen 64  scopeid 0x0<global>
        ether 70:85:c2:7f:bb:7d  txqueuelen 1000  (イーサネット)
        RX packets 134402  bytes 68324237 (68.3 MB)
        RX errors 0  dropped 22346  overruns 0  frame 0
        TX packets 87596  bytes 20566693 (20.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 16  memory 0xa3200000-a3220000  

さらに、routeコマンドで静的ルーティングテーブルを確認します.

$ route -6
カーネルIPv6 経路テーブル
Destination                    Next Hop                   Flag Met Ref Use If
ip6-localhost/128              [::]                       U    256 2     0 lo
2405:xxxx:xxxx:xxxx::/64       [::]                       U    100 1     0 eno1
fd98:c3f9:eb41:1::/64          [::]                       U    256 3     0 eno1  ★追加されている
fe80::/64                      [::]                       U    100 2     0 eno1
[::]/0                         _gateway                   UG   20100 13     0 eno1
   :
(省略)
   :

(macOSの場合) ifconfig を使う

macOSでは ipコマンドは標準で組み込まれてないようです.
Homebrewを使って iproute2mac をインストールすれば ip コマンドが使えるのですが、 まずは標準の ifconfig で行うことにしました.

(コマンド)
$ sudo ifconfig {IntarfeceName} inet6 add {IPv6Addr} prefixlen {PrefixLength}

(例: en0 に fd98:c3f9:eb41:1::4/64 を割り当て)
$ sudo ifconfig en0 inet6 add fd98:c3f9:eb41:1::4 prefixlen 64

コマンド実行後、 ifconfig でインタフェースのアドレスを確認します.

$ ifconfig 
       :
    (省略)
       :
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 88:e9:fe:4e:3d:19 
    inet6 fe80::8a6:a219:8c03:1cb6%en0 prefixlen 64 secured scopeid 0x4 
    inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:27ae prefixlen 64 autoconf secured 
    inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:7a3d prefixlen 64 autoconf temporary 
    inet6 fd98:c3f9:eb41:1::4 prefixlen 64  ★追加されている
    inet 192.168.1.4 netmask 0xffffff00 broadcast 192.168.1.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active

さらに、 netstat -rn で静的ルーティングテーブルを確認.

$ netstat -rn
Internet6:
Destination              Gateway                         Flags           Netif Expire
default                  fe80::8a6:a219:8c03:1cb6%en0    UGcg              en0       
::1                      ::1                             UHL               lo0       
     :  
   (省略)
     :
fd98:c3f9:eb41:1::/64    link#4                          UC                en0     ★追加されている
fd98:c3f9:eb41:1::4      88:e9:fe:4e:3d:19               UHL               lo0     ★追加されている

(Step2) マシンの起動時にコマンドで自動付与するように構成する

コマンドで付加したULAはマシンをシャットダウンするたびに消えてしまうので、このままでは毎回起動するたびにログインしてコマンドで付与しなければいけません.
ならば、マシンの起動時に自動的にULAを付加できるようになれば本格的に運用できそうです.

(Linuxの場合) NetworkManagerのフックスクリプトを使う

デスクトップ版のLinuxでは、Network関係の設定は通常 NetworkManager が行っているようですね.
NetworkManager には dispatcher の仕組みがあって、 /etc/NetworkManager/dispatcher.d/スクリプトや実行ファイルを配置すると、ネットワークデバイスの起動や状態変更のタイミングで呼び出してくれたりするので、これを使います.

以下のスクリプトを配置しました.
ファイルの所有者はrootにしておく必要があります.

#!/usr/bin/env bash

set -e

#
#  $1 = interface name. 
#  $2 = interface state. 
#

# $2をみて interface の 'up'と'down'のタイミングででコマンドを実行.

if [ "$2" = "connectivity-change" ]; then
    exit 0;
fi
 
if [ -z "$1" ]; then
    echo "$0: called with no interface" 1>&2
    exit 1;
fi

IP_CMD=$(which ip)

# インタフェース名
IFACE_NAME=eno1
# 追加で割当てるIPv6のULAとプレフィックス長
IP6_ULA=fd98:c3f9:eb41:1::2/64

if [ -n "${IP6_ULA}" ]; then 
    # 対象interfaceの呼び出しに絞り込み 
    if [ "$1" = "${IFACE_NAME}" ]; then
        # 'up' と 'down' の通知をトリガーに処理する
        case "$2" in
            up)
                # インタフェースの起動. ipを追加割当
                ${IP_CMD} -6 addr add ${IP6_ULA} dev $1 
                ;;
            down)
                # インタフェースのシャットダウン. ip割当を解除
                ${IP_CMD} -6 addr del ${IP6_ULA} dev $1
                ;;
            *)
                # Do nothing
                ;;
        esac
    fi
fi

exit 0;

(macOSの場合) launchd を使う

macOSの場合、非ログイン状態でNetworkManagerのようにネットワークデバイス起動/切断時のフックを行う仕組みはちょっと見当たりませんでした.
なにか良い方法があればと思いますが、まずは設定だけでも・・・と、launchdを使って起動時にコマンドを実行させるようにしました.

ただし、コマンドの実行はNetworkデバイスのSetupが完了した後でなければ正しくルーティングテーブルが更新されません.
そこで、launchd のスクリプト起動タイミングには RunAtLoad ではなく WatchPaths を使い、マシンの起動後にネットワークサービスが DNS情報/etc/resolv.conf を変更したタイミングで実行させるようにします.
(※ 今回macOSでは起動後のip付加のみ. downイベント相当のフックに使えそうなものはちょっと見当たらず・・要検討...)

以下の設定ファイルを所有者をrootにして /Library/LaunchDaemons/ に配置します.
呼び出すスクリプトファイルは、 /Users/hoge/bin/reg_ipv6_ula.bash としました.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<!-- jp.or.regv6ula.plist. -->
<!-- /Library/LaunchDaemons/ に配置 -->

<plist version="1.0">
<dict>
    <key>Label</key>
    <string>reg_ipv6_ula</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/env</string>
        <string>bash</string>
        <string>/Users/hoge/bin/reg_ipv6_ula.bash</string>
        <string>add</string>
    </array>
    <key>Disabled</key>
    <false/>
    <key>WatchPaths</key>
    <array>
        <string>/etc/resolv.conf</string>
    </array>
    <key>KeepAlive</key>
    <false/>
    <key>StandardErrorPath</key>
    <string>/Users/hoge/log/reg_ula_err.log</string>
</dict>
</plist>

呼び出すスクリプトファイル(reg_ipv6_ula.bash)の内容は以下の通り.

#!/usr/bin/env bash

#
# 登録/削除用Script
#

IFCFG_CMD=$(which ifconfig)

# インタフェース名
IFACE_NAME=en0
# 割り当てるULA
IPV6_ULA=fd98:c3f9:eb41:1::4

if [ -n "$1" ]; then
    case "$1" in
    remove)
        echo "unregister ULA.. ${IPV6_ULA}"
        ${IFCFG_CMD} ${IFACE_NAME} inet6 remove ${IPV6_ULA} prefixlen 64
        echo "Done."
        ;;
    add)
        echo "register ULA.. ${IPV6_ULA}"
        ${IFCFG_CMD} ${IFACE_NAME} inet6 add ${IPV6_ULA} prefixlen 64
        echo "Done."
        ;;
    *)
        ;;
    esac
fi

(参考) ところで・・Windowsではどうするのだろう?

Windowsの場合はネットワークのプロパティからIPv6手動割当て とした場合でも、手動割り当てしたULAに加えてGUAもちゃんと自動構成されるようでした.
この動作だと「ULAだけ使う」という選択肢はなさそうですが、こちらは設定が簡単で良いですね.

参考サイト

pyenvでインストールしたpython環境でデバッグができなくて困った件

あれ?デバッガ起動できない・・?

pyenv、 複数のpythonバージョンをあれこれ切り替えることが出来て便利ですね.
システムにインストールされてない特定のバージョン環境で動作するスクリプトを書く必要があるときは、いつも使ってるおなじみツールです.
pyenv-virtualenv の仮想環境モジュールと組み合わせるとさらに利便性が高まりますね.

github.com

github.com

さて、例にもれず特定バージョン向けスクリプトを書くことがありまして・・・
そろそろ動作を見てみようかな、とpyenvでインストールしたpython環境を使ってデバッグをしようとしたところ、以下のエラーに遭遇.

/usr/bin/env /home/hoge/.pyenv/versions/3.8.6/bin/python /home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 36261 -- /home/hoge/work/presnip/pyscr_dev/prj/main.py 
Traceback (most recent call last):
  File "/home/hoge/.pyenv/versions/3.8.6/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/home/hoge/.pyenv/versions/3.8.6/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher/__main__.py", line 97, in <module>
    main()
  File "/home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher/__main__.py", line 24, in main
    from debugpy.launcher import debuggee
  File "/home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher/../../debugpy/launcher/debuggee.py", line 8, in <module>
    import ctypes
  File "/home/hoge/.pyenv/versions/3.8.6/lib/python3.8/ctypes/__init__.py", line 7, in <module>
    from _ctypes import Union, Structure, Array
ModuleNotFoundError: No module named '_ctypes'

んー?
モジュールが無いぞ!・・・と怒られてデバッガが起動してくれません.

ctypeモジュールが見当たらない!?

ModuleNotFoundError: No module named '_ctypes'

ctypesモジュールが見つからないよ!とあるのですが、ctypesって標準でインストールされるはず・・・
んなアホな・・・と versions以下にあるC拡張モジュールのライブラリディレクトリ(3.8.6/lib/python3.8/lib-dynload) を見に行くと・・・

あれまー、ほんとに無い.

あるはずの _ctypes.cpython-38-x86_64-linux-gnu.so が見当たりません.
どうやらビルドされてないようです.

ビルド環境にlibffiが無いとctypesモジュールはビルドされない

いろいろ探し回ったところ、どうもこれっぽいなと思われる記事を発見.

pyenvのpython環境インストールは、インストール環境でpython環境を一式ビルドする方式なんですが、 ビルド環境でlibffiがみつからない場合は ctypeモジュールのビルドをSkipする んだそうで...

stackoverflow.com

いやこれ、python環境でも結構重要な位置づけのモジュールじゃなかったっけ!?
無しでもビルドできちゃうって・・これでいいんだろうか・・・?

$ sudo apt install libffi-dev

$ pyenv install 3.8.6
pyenv install 3.8.6
pyenv: /home/hoge/.pyenv/versions/3.8.6 already exists
continue with installation? (y/N) y

ということで、Ubuntu環境へ libffi の開発用パッケージ libffi-dev をインストールして、pyenvの環境を再インストール(再ビルド)したところ・・・

無事デバッガが動作するようになりました.

めでたく一件落着なんですが、ちょっとこれはメッセージから原因までパッっと分かりづらかった・・・
少なくとも、インストール時に警告ぐらい出してくれてもいいんでないかな・・・

参考情報

Red Hat Enterprise Linux の場合

RedHatの場合は、libffi-devel パッケージですね.
依存関係の都合上、他の開発パッケージ導入時に自動的に入ってるみたいです.

$ sudo yum install libffi-devel

macOSの場合

macOSの場合はbrewでinstallすれば良いみたいです.
こちらは libffi Fomula.
こちらも結構いろんなモジュールから依存関係があるみたいで、どうやら llvm 入れたときに一緒に入ったようでした.

$ brew install libffi