えんでぃの技術ブログ

えんでぃの技術ブログ

ネットワークエンジニアの視点で、IT系のお役立ち情報を提供する技術ブログです。

sudo pip を実行してはいけない理由

python-logo

お伝えしたいこと

sudo pipを実行すべきではないという話を書きます。

詳しくは書きませんが、ローカルのVMsudo pip uninstallを実行した結果、状況次第ではおかしなことになりました。
検証の結果、わかったことをかいつまんでお伝えします。

※元々別記事に書いてあった内容ですが、記事を分けました

sudo pip install が NG である理由

sudo pip installは実行しないでください。
Fedora Wikiでも、以下のように力強く書かれていました。

Never ever ever ever use pip or pip3 with sudo. Use pip --user or Python virtual environments instead.

Fedora Wiki

実行してはいけない理由は2つあります。

  • システムを破壊する懸念がある
  • 知らぬ間にvenvの外側を破壊するケースがある

詳細はこの後に続きます。

システムを破壊する懸念

pipには、--system--userの2種類があります。

pip install --systemは、システム全体にPythonパッケージをインストールするというオプションです。
デフォルトのインストール先は/usr/local/配下です。
具体的には/usr/local/bin/や、/usr/local/lib/pythonX.Y/site-packages/配下です。

pip install --userは、ユーザーごとに個別にPythonパッケージをインストールするという意味です。
デフォルトのインストール先は、~/.local配下です。
具体的には~/.local/bin/や、~/.local/lib/pythonX.Y/site-packages/配下です。

pip listを実行すると、実は--system--userの両方をマージした結果が出力されます。
pip list --userを実行すると、--userの結果のみが表示されます。

pip listの仕組みを正確に理解しているわけではありませんが、pipは恐らくPythonのライブラリ検索パス上にPythonパッケージ (※) が含まれる場合に表示される作りになっています。
(※) あるルールを満たすディレクトリのこと

例えば、sudo dnf install ansibleのようにpip以外の仕組みで導入された場合も、pip listで表示されます。
そして、sudo pip uninstall ansibleを実行することで関連ファイルを削除することができます。
もしこのような方法で削除すると、おかしなことになります。

dnf list ansibleでインストール済みと表示されますが、pip listにはansibleと表示されません。
そしてansibleのコマンド実行に失敗します。
ライブラリや実行ファイルなどが削除されているため、当然です。
このような状況になったら、sudo dnf reinstall ansibleなどで再インストールして復旧できますが、混乱の度合いは計り知れません。

※dnfとpipではインストール済みか否かを判断する基準が異なると思われます。dnfは「dnf removeを実行したか否か」で判断するのに対し、pipは「パッケージの実体がファイルとして存在するか」で判断しているように見えました

このように、sudo pipは影響が大きいです。
その上、他のインストール手段と競合しうる危険性をはらんでいます。
原則として、sudo pipは使うべきではないと思います。

sudoさえつけなければ、間違えてpip uninstall ansibleを実行しても、権限不足でシステムのファイル削除は失敗してくれます。

知らぬ間にvenvの外側を破壊するケースがある

sudoをつけるとPATHの再読込が発生することで、仮想環境の外に出てしまいます。
更に、管理者権限でpip installを実行すると、--userをつけたときのような~/.local/配下ではなく、/usr/local/配下にインストールしてしまいます。

仮想環境の中であっても、sudo pip installだけは実行しないようにお気をつけください。
このコマンドを実行したら警告が出力されることからも、公式でも非推奨の操作です。

もちろん、sudo pip uninstallも原則として避けてください。
間違えてsudo pip installしてしまったパッケージを消す時だけ使うこともあるかもしれませんが...。

まとめ

sudo pipを封印すべき理由について書きました。

関連記事

endy-tech.hatenablog.jp

PythonをVS Codeで書くための環境設定

python-logo-no-wordmark

お伝えしたいこと

本記事では、VS CodePythonを動かすための環境設定を紹介します。
最低限必要なのは、以下の2点です。

上記セットアップによってできるようになることを、以下のセクションで補足します。

最後に補足として、応用的な設定を一部紹介しています。

途中でVS Codeの操作方法について記述している箇所がありますが、ショートカットキーはLinux基準となっています。
OSの違いによって読み替えが必要となりますが、そこはご了承ください。

Python拡張機能のインストール

Python拡張機能をインストールします。

Pylance拡張機能も自動的にインストールされると思います。
この拡張機能は無効化しないことを推奨します。
詳細は#Language Serverについてにて補足します。

拡張機能のインストール手順は、VS Codeの基本操作になるので割愛します。

初期設定

Python拡張機能の初期設定として、Pythonインタプリタを指定します。
/usr/bin/pythonなど、代表的なパスにある場合は自動検知してくれるので、Linuxの方は初期設定不要だと思います。

一方、後からインストールしたバージョン違いのPythonを使いたい場合は、個別に指定する必要があります。
Ctrl+Shift+P (View > Command Palette)を押してから、出てきた検索ウィンドウにPython: Select Interpreterと入力することで設定できます。

select_python_interpreter1 loading=

.pyファイルを開いている間は、左下の隅っこをクリックすることでも設定できます。

select_python_interpreter2

初期設定はこの時点で完了です。

Python拡張機能の基本機能

Python拡張機能を入れることで、追加設定不要で使える機能を紹介します。

Ctrl+F5で実行

.pyファイルを開いているときにCtrl+F5 (Run > Run without debugging)を押すだけでPythonを直ちに実行してくれます。
サンプルコードを書いてCtrl+F5を押すだけでサクサク実行できます。

学習効率に関わる部分なので、シンプルですが重要な機能です。

IntelliSense機能

VS Code のIntelliSense機能が使えるようになります (公式ドキュメント)。
IntelliSense機能は、いわゆる「コード補完」機能です。
裏でPython Language Server というコンポーネントと連携することで、以下のサービスを提供します。

機能名 機能詳細
コード補完 例えば、print()関数のprまで入力すると、入力候補を表示する
説明の表示 print()関数の基本機能、取りうる引数、返り値、型情報などを表示する

クラス、関数、プロパティ、メソッドの仕様を事細かに暗記せずとも、この機能を頼りながら効率的にプログラムを書くことができます。
学習に伴う暗記、調査のストレスを減らし、作業と学習の効率を大幅に上げてくれる必須機能です。

IntelliSense機能は、関数名などを途中まで入力してからCtrl+Spaceを押すことで呼び出すことができます。
Ctrl+Spaceを複数回押すと、コード補完の表示の詳細度を切り替わります。

以下は、listのsortメソッドの説明をIntelliSenseで表示した画像です。
Language Server はPylanceを使用しています。

completion_pylance

(参考) Language Serverについて

Language Server とは、プログラミング言語による開発をサポートするための機能を提供するサーバです。
VS Codeなどのエディタは、Language Server Protocol を介してLanguage Server と通信することで、サポート機能を享受することができます。

Python拡張機能の場合、デフォルトでPylanceというLanguage Serverが使われます。
Pylanceは上述のIntelliSense機能だけでなく、Syntax Highlighting (ソースコードの色分け) やエラーレポートなどの機能も提供します。
詳細はPylance拡張機能の公式ページを参照してください。

PylanceはPylance拡張機能がインストールされていれば、オフラインで利用可能です。

Python拡張機能python.languageServerという設定にてLanguage Serverを変更することもできます。
しかし、公式に推奨されていて、なおかつ現状機能として最も優れているのはPylanceなので、デフォルトのままPylanceを使い続けることをお勧めします。

python.languageServerのデフォルト値はDefaultとなっていますが、この設定値は以下の挙動になります。

  • Pylance拡張機能が有効である場合は、Pylanceを使う
  • Pylance拡張機能が無効、または未インストールである場合は、JediLSPを使う

Pylanceとほぼ同等の使い勝手を持つLanguage ServerとしてMicrosoftもありますが、Microsoftは将来的にPylanceに統合されると公式で宣言されています (開発者ブログより)。

(以下、追記内容)

Pylanceを使っていて、Intellisenseの説明が足りないなど不便を感じた場合は、Microsoftもお試しください。
例えば2021年7月時点でSocketライブラリのメソッドを調べたところ、socket.gethostnameなどの説明がPylanceではほぼ表示されませんでした。
Microsoftの場合はしっかり表示されました。
もしPylanceで不便を感じることがあれば、当面の間はMicrosoftを使い、後々Pylanceに移行するのも良いと思います。

Language ServerをMicrosoftに変更するには、File > Preferences > Settings (Ctrl+,)にてpython.languageServerを検索し、設定値をMicrosoftに変更します。
その後VS Codeを再起動することで反映します。

実際に使ってみる

簡易なコードを例に、実際に使ってみます。

まず、Python開発用のフォルダを作っておきます (projects/python/hello_world/など)
Ctrl+K,Ctrl+O (File > Open Folder)でこのフォルダを開きます。

Ctrl+Shift+E(または左上のアイコン) からエクスプローラを開き、新規ファイルを作成します。
ファイル名は適当に、app.pyというファイル名にしておきます。

create_file

app.pyに以下のコードを書き、Ctrl+Sで保存します。

print('hello world')

Ctrl+F5(Run > Run without Debugging)で実行します。
すると、画面下部にターミナルが開いてPythonが実行されます。
出力が残って気になる場合は、Ctrl+Dでターミナルを閉じます (これはbashのショートカットキーです)
または、Ctrl+Shift+@ (View > Terminal)でターミナルの表示/非表示を切替できます。

Pythonの学習初期段階においてはサンプルコードを書いて動かすことの繰り返しになりますが、上記のワークフローであれば効率的に作業できるのではないかと思います。

(参考) Python拡張機能の応用機能

注意:私自身まだPython初心者であり、このセクションに記載の内容は確証が持てていません。その前提で流し読みください。

Python拡張機能には、#Python拡張機能の基本機能 の他にも色々な機能があります。
例えば、Linting、Debugging、Testingなどです。

これらの機能はPythonモジュールで実装されています。
Python拡張機能は、これらのモジュールを参照し、VS Code上で実行できるようにしてくれるだけです。

これらの機能をVS Codeと連携させるには、対応するLint、デバッグ、テスト用ライブラリを追加インストールする必要があります。

これらのライブラリは似たような機能を持っているものが複数存在するので、どれを使ったら良いか迷います。
従って、最初は敢えて何も導入しないのも全然アリだと思います。
私も、現時点でどの組み合わせが候補として上がるのか、何が違うのか全く理解できていません。
そのうち、上記のような機能が欲しくなったときに改めて調べて理解したいと思います。

Linting

Lintとは

現時点ではTesting, Debugging 機能を使いこなせていませんが、Lint機能だけは欲しくなったので少し調べました。
Lint機能により、Pythonの文法エラーやコーディング規約に習っていない書き方を検知してVS Codeがエラーや警告を表示してくれるようになります。
これらの警告を解決するように修正することで、ソースコードの品質を一定ラインに保ちやすくなります。

VS Codeで警告やエラーメッセージを見たい場合は、View > Problems (Ctrl+Shift+M)でウィンドウを表示します。

以下にLintが表示するエラーのサンプルを示します。
こちらのエラーでは、無駄なimport文を実行していることと、ファイル末尾に空行が無いことで、flake8というLinterがエラーと警告をそれぞれ出しています。
PEP8などのコーディング規約を理解していなくても、ツールに教えてもらった通りに修正するだけで最低限見た目が綺麗になるのは便利ですね。
「ライトユーザーだから動けばいい」と思う方もいるかもしれませんが、むしろライトユーザーこそツールに任せて楽をするのが良いと私は思います。

lint_errors

導入したツール

正直、どのツールを選べば良いかはよくわかりません。
一旦、みんなが使ってそうなツールを候補に入れました。

候補を選択するにあたり、以下の記事がとても参考になりました。
ありがとうございます。
2021年Python開発リンター導入のベストプラクティス

私の環境で導入したツールは以下の通りです。

ツール名 役割 起動方法
black フォーマッター。
Lintエラーの自動修正
Ctrl+Shift+I
(Format Document)
isort import文のソート Ctrl+Shift+P
python.sortImports
bandit セキュリティ観点の警告 ファイル保存時に自動実行
flake8 スタイル、論理エラー、
複雑度観点の警告
ファイル保存時に自動実行
mypy 型チェック ファイル保存時に自動実行

Python初心者の目線ですが、flake8とblackは特に入れてよかったと感じています。
flake8が頻繁にエラーを出してくれて、blackがいつも良い感じに自動整形してくれます。

一方で、まだ簡単なコードしか書いていないこともあり、bandit、mypyがエラーを出力したり、isortが役に立った場面に遭遇したことはありません。

導入手順

以下のコマンドで必要パッケージをインストールします。
VS Codeで使いたいので、venvではなくユーザー権限でグローバルにインストールしています (※)。

(※) この方法では後から不要パッケージの整理に苦労する懸念がありそうですが、回避方法はあります。
詳細については、Pythonパッケージの管理方法を参照してください。

pip install --user black isort bandit flake8 mypy

VS Code の設定を以下のようにカスタマイズします。

設定ID 設定値 意味
python.linting.flake8Enabled チェックを入れる flake8を有効化する
python.formatting.provider black フォーマッターにblackを使用する
python.linting.mypyEnabled チェックを入れる mypyを有効化する
python.linting.banditEnabled チェックを入れる banditを有効化する

参考にしたサイトでは、更に追加のカスタマイズも紹介されていました。
役に立つ内容なので、ぜひ見に行ってください。
VSCode と連携する方法

まとめ

Pythonを始めるのは面倒です。
教材探し、Pythonインストール、venvの検討、そしてIDEの検討が事前に必要です。

そこで本記事では、VS CodeによるPython開発環境の最低限のセットアップ手順を紹介しました。

本記事によって、Pythonを書き始める前の工程が少しでも短縮されれば幸いです。

Linuxで無線LANが繋がらない時の対処法

wifi-icon

お伝えしたいこと

NUC10を購入してFedoraをインストールしてから約半年間、長らく無線LANが使えない状況でした。
NUCはミニPCとはいえデスクトップなので、私は基本有線で使っています。
無線LANクライアントをずっと無効化したまま使っていました。

その状況が変わり、最近正しく動くようになったので、経緯を共有します。
無線LANが動かずに困っている方に役に立てば良いなと思います。

今回は無線LANの理解を深める前にあっさり直ってしまったため、内容としては薄めです。
1つのケーススタディとして見ていただければと思います。

環境

私の環境を貼っておきます。

項目 概要
PC本体 BXNUC10i5FNH
無線LANクライアント (内蔵) Intel® Wi-Fi 6 AX201 (参考元)
OS Fedora33

発生事象

GUIでの確認

トラブル発生時、GUI上で無線LANを有効化してもSSIDが全く表示されませんでした。

gui_wifi1 loading=

本来であれば、以下のように表示されるはずです。

gui_wifi2

CLIでの確認

CLIでも同様に、SSIDが表示されないことを確認しました。

1行目のコマンドで、無線LANクライアントが有効化されていることを確認しています。
無効の場合は、nmcli radio wifi onで有効化できます。

2行目のコマンドで、SSIDを一覧表示しようとしています。
しかし、どういうわけか何も表示されません。

nmcli radio wifi
# enabled

nmcli device wifi list
# IN-USE  BSSID  SSID  MODE  CHAN  RATE  SIGNAL  BARS  SECURITY

本来では、以下のように表示されるはずです。

nmcli device wifi list
# IN-USE  BSSID              SSID              MODE   CHAN  RATE        SIGNAL  BARS  SECURITY  
#         AA:BB:CC:DD:EE:FF  my-ssid1      Infra  11    405 Mbit/s  100     ▂▄▆█  WPA1 WPA2 
#         11:12:13:14:15:16  my-ssid2      Infra  64    405 Mbit/s  100     ▂▄▆█  WPA1 WPA2 
#         00:11:22:33:44:55  my-ssid3      Infra  9     270 Mbit/s  55      ▂▄__  WPA2 

無線LANが動かない原因

私のケースでは、2つの原因がありました。

  1. 無線LANのデバイスが正しく認識されていなかった
  2. 無線LAN暗号化通信をサポートするパッケージがインストールされていなかった

この2つの原因について、順を追って共有します。

(原因1) 無線LANバイスが正しく認識されない

必要なドライバが足りてなかったことで、無線LANバイスを正しく認識できていなかったようです。
以下に詳細なログを貼ります。

原因1の切り分け

dmesg (Linuxブート中のログ) を見ると、iwlwifiという無線LANクライアントのドライバが無線LANクライアントを正しく検出できず、何度かフォールバックしているように見えます。
※ちなみにiwlwifiは、Intel無線LANに関わるドライバの名称です (iwl = Intel Wireless LAN?)

特に、以下のエラーが悪影響を与えている印象でした。
Direct firmware load for iwlwifi-QuZ-a0-hr-b0-56.ucode failed with error -2
Direct firmware load for iwl-debug-yoyo.bin failed with error -2

しかし最後にAX201という正しいデバイスを認識できているようには見えます。
Detected Intel(R) Wi-Fi 6 AX201 160MHz, REV=0x354

dmesg | grep iwlwifi
# [    5.445530] iwlwifi 0000:00:14.3: enabling device (0000 -> 0002)
# [    5.447086] iwlwifi 0000:00:14.3: Direct firmware load for iwlwifi-QuZ-a0-hr-b0-56.ucode failed with error -2
# [    5.449460] iwlwifi 0000:00:14.3: api flags index 2 larger than supported by driver
# [    5.449471] iwlwifi 0000:00:14.3: TLV_FW_FSEQ_VERSION: FSEQ Version: 65.3.35.22
# [    5.449475] iwlwifi 0000:00:14.3: Found debug destination: EXTERNAL_DRAM
# [    5.449476] iwlwifi 0000:00:14.3: Found debug configuration: 0
# [    5.449698] iwlwifi 0000:00:14.3: loaded firmware version 55.d9698065.0 QuZ-a0-hr-b0-55.ucode op_mode iwlmvm
# [    5.449864] iwlwifi 0000:00:14.3: Direct firmware load for iwl-debug-yoyo.bin failed with error -2
# [    5.628261] iwlwifi 0000:00:14.3: Detected Intel(R) Wi-Fi 6 AX201 160MHz, REV=0x354
# [    5.806506] iwlwifi 0000:00:14.3: base HW address: 04:33:c2:81:04:11
# [    5.821843] iwlwifi 0000:00:14.3 wlp0s20f3: renamed from wlan0

ただ、この話には続きがあります。

lspciコマンドで、Linuxが認識しているPCIバイスを表示できます。
正しくはAX201と表示されるはずですが、なぜかIntel Corporation Wireless-AC 9462と表示されています。
単なる表示上の問題であれば良いのですが、iwlwifiが正しくデバイスを認識できていない可能性も出てきました。

sudo lspci -v | grep -A12 -i 'network controller'
# 00:14.3 Network controller: Intel Corporation Wireless-AC 9462
#   Subsystem: Intel Corporation Device 0074
#   Flags: bus master, fast devsel, latency 0, IRQ 16
#   Memory at 6023114000 (64-bit, non-prefetchable) [size=16K]
#   Capabilities: [c8] Power Management version 3
#   Capabilities: [d0] MSI: Enable- Count=1/1 Maskable- 64bit+
#   Capabilities: [40] Express Root Complex Integrated Endpoint, MSI 00
#   Capabilities: [80] MSI-X: Enable+ Count=16 Masked-
#   Capabilities: [100] Latency Tolerance Reporting
#   Capabilities: [164] Vendor Specific Information: ID=0010 Rev=0 Len=014 <?>
#   Kernel driver in use: iwlwifi
#   Kernel modules: iwlwifi

一方で、正常なときは、以下のようなログになります。

Linux起動時には特にエラーログはなく、フォールバックも発生していません。
読み込まれたファームウェアのファイル名も事象発生時と変わっており、QuZ-a0-hr-b0-59.ucodeと記載があります。
このファイルの実体は/usr/lib/firmware/iwlwifi-QuZ-a0-hr-b0-59.ucodeにあります。

dmesg | grep iwlwifi
# [    4.339129] iwlwifi 0000:00:14.3: enabling device (0000 -> 0002)
# [    4.341962] iwlwifi 0000:00:14.3: api flags index 2 larger than supported by driver
# [    4.341971] iwlwifi 0000:00:14.3: TLV_FW_FSEQ_VERSION: FSEQ Version: 65.3.35.22
# [    4.342152] iwlwifi 0000:00:14.3: loaded firmware version 59.601f3a66.0 QuZ-a0-hr-b0-59.ucode op_mode iwlmvm
# [    4.505557] iwlwifi 0000:00:14.3: Detected Intel(R) Wi-Fi 6 AX201 160MHz, REV=0x354
# [    4.688618] iwlwifi 0000:00:14.3: base HW address: 04:33:c2:81:04:11
# [    4.704444] iwlwifi 0000:00:14.3 wlp0s20f3: renamed from wlan0

Linuxが認識しているデバイス名も、AX201という正しい情報が表示されています。

sudo lspci -v | grep -A12 -i 'network controller'
# 00:14.3 Network controller: Intel Corporation Comet Lake PCH-LP CNVi WiFi
#   Subsystem: Intel Corporation Wi-Fi 6 AX201 160MHz
#   Flags: bus master, fast devsel, latency 0, IRQ 16, IOMMU group 5
#   Memory at 6023114000 (64-bit, non-prefetchable) [size=16K]
#   Capabilities: [c8] Power Management version 3
#   Capabilities: [d0] MSI: Enable- Count=1/1 Maskable- 64bit+
#   Capabilities: [40] Express Root Complex Integrated Endpoint, MSI 00
#   Capabilities: [80] MSI-X: Enable+ Count=16 Masked-
#   Capabilities: [100] Latency Tolerance Reporting
#   Capabilities: [164] Vendor Specific Information: ID=0010 Rev=0 Len=014 <?>
#   Kernel driver in use: iwlwifi
#   Kernel modules: iwlwifi

原因1の解決策

しばらくぶりにsudo dnf upgradeでパッケージを更新したら、上述の正常なログが出るようになりました。
ucodeファイルがそもそも足りていなかったか、iwlwifiがucodeファイルを正しく参照できていなかったか。
いずれにしても、iwlwifi自体に何らかの不具合か互換性不足があり、それがパッケージ更新によって解消されたようです。

時間が解決した、ということですね。

私の環境ではiwl7260-firmwareというrpmパッケージが関係あるようでした。
このパッケージの特定方法は、次のセクションに書きます。

(参考) 必要なrpmファイルの特定の仕方

iwlwifiのドライバが含まれるrpmパッケージは、以下のとおりたくさんあります。
ここでは、以下のrpmパッケージの中に自分が使っている無線LANクライアントに対応したものがあるか確認する方法を紹介します。
今回の方法はrpmを利用するRedHat系かつ、Intel無線LANクライアントを使っている場合のみ使えます。
条件に該当しない方は、適宜手順をアレンジする必要があります。

rpm -qa | grep iwl
# iwl100-firmware-39.31.5.1-119.fc33.noarch
# iwl1000-firmware-39.31.5.1-119.fc33.noarch
# iwl105-firmware-18.168.6.1-119.fc33.noarch
# iwl135-firmware-18.168.6.1-119.fc33.noarch
# iwl2000-firmware-18.168.6.1-119.fc33.noarch
# iwl2030-firmware-18.168.6.1-119.fc33.noarch
# iwl3160-firmware-25.30.13.0-119.fc33.noarch
# iwl3945-firmware-15.32.2.9-119.fc33.noarch
# iwl4965-firmware-228.61.2.24-119.fc33.noarch
# iwl5000-firmware-8.83.5.1_1-119.fc33.noarch
# iwl5150-firmware-8.24.2.2-119.fc33.noarch
# iwl6000-firmware-9.221.4.1-119.fc33.noarch
# iwl6000g2a-firmware-18.168.6.1-119.fc33.noarch
# iwl6000g2b-firmware-18.168.6.1-119.fc33.noarch
# iwl6050-firmware-41.28.5.1-119.fc33.noarch
# iwl7260-firmware-25.30.13.0-119.fc33.noarch

まず、自分が利用している製品の型番を前述のlspci -vで確認します。
lspci -vmmの方が見やすいかもしれません。
今回の場合は、AX201だとわかったとします。

この後すぐできる最も簡単な確認方法は、dnf searchコマンドで検索することです。
パッケージの説明文に型番が書いてあるので、今回はこの方法が使えます。
この結果、iwl7260-firmwareがヒットしました。

dnf search AX201
# iwl7260-firmware.noarch : Firmware for Intel(R) Wireless WiFi Link 726x/8000/9000/AX200/AX201 Series Adapters

上記方法でヒットしないケースもあります。
例えばAX210に対応するドライバも上記パッケージに本当は含まれているのですが、説明文に書いてないのでヒットしません。
そんなときは、次の方法で確認します。

Linux Wireless WikiでサポートされているIntel無線LAN機器の一覧を確認します。
このページでAX201を検索し、対応するドライバが含まれるtar.gzファイルをダウンロードして中身を確認します。
今回は、iwlwifi-Qu-48.13675109.0.tgzをダウンロードします。

すると、中にいくつかのucodeファイルが入っています。
どれでも良いので、一つのucodeファイルの名前をメモしてください。
今回は、iwlwifi-Qu-b0-hr-b0-48.ucodeにします。

続いて、dnf providesコマンドで上記ファイルを含むrpmパッケージを検索します。
dnf providesには、ファイルをフルパスで指定する必要があります。
今回は/usr/lib/firmware/iwlwifi-Qu-b0-hr-b0-48.ucodeが正しい指定方法なのですが、フルパスを知らなくてもワイルドカードで検索できます。
**は、任意の文字列かつ、任意階層分だけフォルダを下った場合もマッチするワイルドカードです。
言い換えると、**は「/を含む任意の文字列」にマッチします。

dnf provides **iwlwifi-Qu-b0-hr-b0-48.ucode
# iwl7260-firmware-1:25.30.13.0-119.fc33.noarch : Firmware for Intel(R) Wireless WiFi Link 726x/8000/9000/AX200/AX201 Series Adapters

以上の確認により、AX201に必要なrpmパッケージはiwl7260-firmwareだとわかりました。

もちろん、私の環境において必要だったQuZ-a0-hr-b0-59.ucodeも今回の検索でヒットします。

dnf provides **QuZ-a0-hr-b0-59.ucode
# iwl7260-firmware-1:25.30.13.0-119.fc33.noarch : Firmware for Intel(R) Wireless WiFi Link 726x/8000/9000/AX200/AX201 Series Adapters

本来必要なucodeファイルは、事象解消時のdmesgから読み取れます。
トラブル発生中に必要なucodeファイルを見抜く方法は、残念ながら私にはわかりませんでした。

# [    4.342152] iwlwifi 0000:00:14.3: loaded firmware version 59.601f3a66.0 QuZ-a0-hr-b0-59.ucode op_mode iwlmvm

(原因2) パッケージが足りていない

原因1を解決してもSSIDが表示されなかったので、更に調査を進めました。
その結果、wpa_supplicantというパッケージが不足していることが原因だとわかりました。

原因2の切り分け

無線LANを有効化したタイミングで、/var/log/messagesに以下のログが発生していました。
恐らく、journalctl -xeu NetworkManagerでも同様のログを確認できると思います。

NetworkManager[950]: <error> [1619738834.7337] device (wlp0s20f3): Couldn't initialize supplicant interface: Failed to D-Bus activate wpa_supplicant service

原因2の解決策

以下のコマンドでwpa_supplicantをインストールすることで直りました。

sudo dnf install wpa_supplicant

このパッケージにより、wpa_supplicant.serviceが作成されます。
このサービスは無線LANを有効化したタイミングで自動起動されるので、有効化する必要はないようです。
sudo systemctl enable --now wpa_supplicant.serviceの実行は、私の環境では不要でした。

以上の対応により原因1と原因2を解消し、無事無線が使えるようになりました。

(参考) wpa_supplicantとは?

wpa_supplicantとは、WEP、WPA、WPA2などの無線LAN暗号化技術における認証クライアントです。
言い換えれば、上述の暗号化技術を用いて無線LAN接続をするのに必要なプログラムです (参考:ArchLinux Wiki)。

初期状態のFedora33には、wpa_supplicantがインストールされていないようです。
これにより、SSIDを表示することすらできなかったようです。

(参考) 起動できないネットワークは無効化すべき

主張

今回の事象のように、無線LANが使えないなどの理由でNetworkManagerのconnectionが起動できないケースはあると思います。
基本的には、connectionが使えないときはその原因を排除して使えるようにするのが良いと思います。
しかしどうしても解決できないときは、OS起動の度にconnection起動を失敗させるのではなく、connectionを初めからdownさせておくべきです。
コマンドでいうと、nmcli radio wifi offなどです。
autoconnect offよりも更にシンプルに、デバイスレベルでwifiを無効にします。

理由

起動に失敗するconnectionを停止しておくべき理由は、OSの起動が遅くなるためです。

LinuxのOSが起動する際、様々なサービスが順番に起動します。
systemctl cat <unit>を実行するとわかりますが、サービス間には依存関係があります。
特に多くのサービスは、ネットワークに依存しています。

つまり、ネットワーク接続が完了しないと、他のサービスの起動が開始されません。

NetworkManager.service については、ネットワーク起動を待つための専用サービスがあります。
それがNetworkManager-wait-online.serviceです。
中身を見てみましょう。
description、実行コマンド、環境変数のみ抜粋しています。

systemctl cat NetworkManager-wait-online
# `nm-online -s` waits until the point when NetworkManager logs
# "startup complete". That is when startup actions are settled and
# devices and profiles reached a conclusive activated or deactivated
# state.

# ExecStart=/usr/bin/nm-online -s -q

# Environment=NM_ONLINE_TIMEOUT=30

内部的にはnm-online -s -qを実行していて、NetworkManagerがstartup completeというログを出力するまで待つようです。
NetworkManagerが30秒以内にログを出さなければ、NetworkManager-wait-onlineタイムアウトする作りです。
NetworkManager-wait-onlineは、NetworkManagerサービスの起動を最大30秒待つことで、他のネットワークに依存するサービスの起動失敗を防ぐ仕組みです。

内部的に実行しているnm-onlineのmanを見てみます。
-sオプションの説明に以下の記述がありました。
autoconnect (connectionの自動起動) に失敗すると、NetworkManagerの起動完了を遅延させるケースがあるようです。
実際、無線LAN自動起動に失敗することでLinuxの起動が遅くなることが確認できました。

For example, by setting a connection profile to autoconnect, such a profile possibly will activate during startup and thus delay startup complete being reached.

どの程度起動が遅くなるのか

では、connection自動起動の失敗により、Linuxの起動がどの程度遅くなるのか。 systemd-analyze blameコマンドで大体の検討がつきます。

systemd-analyze blameを実行すると、各サービスの起動にかかった時間を長い順に表示します。
NetworkManager-wait-onlineの数字が30秒の場合は、何かがおかしいことが多いです。

systemd-analyze blame
# 10.568s dnf-makecache.service
#  9.205s NetworkManager-wait-online.service
#  948ms systemd-udev-settle.service

ただし気をつけていただきたいのは、この表示はLinux起動時間に影響を与えた時間ではありません。
純粋に各プロセスの起動から停止までにかかった時間を表しています。
上記表示は、「NetworkManager-wait-online.serviceによりLinuxの起動が9.205秒遅延した」という意味ではありません。
...今回の場合は、起動に約9秒の影響を与えていますが。

影響を見るには、次のコマンドが適切です。
起動時間のグラフをSVGファイルとして出力します。
GUIで開くことで、画像として開くことができます。

systemd-analyze plot > tmp.svg

グラフは以下のように見えます。
NetworkManager-wait-online.serviceが完了するまで9.2秒待ち続け、その後network-online.targetが起動し、他のネットワークに依存するサービスが起動開始しています。

systemd-analyze

まとめ

Linux無線LANが動作しない原因を2パターン紹介しました。
同じような事象で困っている方の役に立てれば嬉しいです。

無線LANがうまく動作しないときは停止させておくのもぜひ実践してみてください。
Linuxの起動が更に早くなりますよ。
一方、NetworkManager-wait-onlineを無効化するのは、やめておいたほうが良いと思います。

関連記事

Linux パソコン関連の記事をまとめてあります。

endy-tech.hatenablog.jp

メインPCをLinuxにして良かったこと、困ったこと

20200927134745

お伝えしたいこと

メインPCをWindowsMacではなく、Linuxにしてから半年以上経ちました。

実際のところ使っていてどうなのか、思ったことを好き勝手に書きます。
よろしければ、気軽に拾い読みしてください。

私は元々Windowsユーザーで、Macには全く詳しくありません。
従って、WindowsLinuxの対比が多くなります。

また、Linuxが好きになったので、Linuxをひいきしたコメントが目立ちます。
そのあたりはご了承ください。

環境

メインPCの環境を書きます。

まずはハードウェアスペックです。
CPU控えめ、メモリ多め、グラボ無しです。
後は読み飛ばして結構です。

項目 詳細
PC本体 デスクトップ
BXNUC10i5FNH
CPU Intel Core i5-10210U
モバイル用、4コア、最大4.20GHz
RAM 64 GiB
M471A4G43MB1 (32GB, DDR4) x2枚
SSD 1 TiB, M.2 NVMe
WDS100T2B0C

OSとデスクトップ環境は以下のとおりです。

項目 詳細
OS Fedora 33
デスクトップ環境 Cinnamon

Linuxにしてよかったこと

コスパ

Windows 10 は1万7千円ぐらいします。

一方で、Linuxは無料です。

余ったお金をCPU/メモリに充てられます。

検証環境を整えやすい

Windowsで検証をする時、あれこれ工夫が必要で、一筋縄に環境構築できません。
環境構築した後も、たまにソフトウェアを更新しようと思った時に「当時の工夫」を思い出しつつ対応する必要があり、これに中々労力がかかります。
特に心労が...本当に...凄まじいです。

一方で、Linuxの環境構築はWindowsと比べてシンプルかつ簡単です。
パッケージマネージャーでソフトを入れたらおしまい...のことが多いです。
もちろんOSのパッケージマネージャ (dnf, apt) によって手順は異なりますが、ググればすぐに情報が出てきます。
それに、こういったことを調べること自体がLinuxの良い勉強になるところが良い点ですね。

MacWindowsより扱いやすいと思いますが、やはりOSのセキュリティ機能や細かな仕様の差によって苦しむ場面がある印象です。

例1. Python

Linuxだと初めからPython3が入っています。

WindowsはPython3を後からインストールする必要があります。
最近はMicrosoft StoreのPython3が初めから入っていて、これが中々曲者でした。
後からインストールしたPythonよりもMicrosoft StoreのPythonの方がPATHの優先順位が高い。
更に、Microsoft Store版のPythonには不具合があった...と記憶しています。
とにかく面倒でした。。

例2. Ansible

Ansibleに限らず、KVMなどLinuxでしか動作しないソフトには概ね該当すると思います。

LinuxはvenvにAnsibleを入れてすぐ動かせます。
3行のコマンドでセットアップできます。

python3 -m venv ~/venv/ansible
source ~/venv/ansible/bin/activate
pip install ansible

WindowsではAnsibleが動かないので、Linux環境を用意する必要があります。
Linux VMを作るか、WSLを導入するかですね。
その上で、ゲストLinux上で同じ工程を踏む必要があります。
これが中々の手間かなと思います。

コマンドラインが便利

Linuxなら、当然ながらgrepsedawkなどの便利コマンドを色々使えます。

Windowsでもcmdやpowershellに慣れている方はあまり困らないかもしれませんね。
あるいはWSL・Git for Windowsの環境を整えている場合も、それほど不便は感じないかもしれません。

それでも私はcmderやWindows Terminal (WSL) よりも、Linuxターミナルの方が好きです。

ターミナルソフトが充実している

Macも同様かもしれませんが。

Teratermに負けないぐらい、便利なターミナルがたくさんあります。
私のお気に入りはTilixです (過去記事)。
多機能で不具合がなく、ショートカットキーも自由にカスタマイズできるためです。

ソフトウェアのインストールが楽

Linuxならコマンド一発でパッケージインストールできます。

WindowsインストーラをWEBから探してきて、ウィザードをポチポチする必要があります。
ただ、chocolateyなどのツールもあるようなので、このあたりはあまり関係ないのかも...とも思います。

改行コード、文字コード

Linuxにしてから、\r\nShift-JISに悩まされることはなくなりました。

動作が早い

起動時間についてはあまり変わらないかもしれません。
Linuxはデスクトップ環境を動かしていても15秒ほどで起動します。
※もちろん、SSDを搭載していることも大きいです

ただし、Windowsは起動完了となった後も裏でTelemetryやExplolerのインデックス作成、Windows Updateの受信などでディスクI/Oが絶え間なく発生していました。
一方でLinuxに変えてからはそういったことはなく、無駄なディスクI/Oはほぼなくなりました。

余談ですが、Windowsについても、debloat windowsなどのキーワードで検索すると、バックグラウンドプロセスを一括で停止するようなスクリプトを配布しているサイトもあります。
実行は自己責任ですが、確かに多少は早くなります。
Windows Updateで停止したプロセスが復活するので、都度再実行が必要です。
★参考

ちなみに、VirtualBoxやESXiにLinuxデスクトップ環境を入れて動かすと、非常にもっさりしていると思います。
それは仮想環境で動かしているためです。
WindowsVMでも同様に重くなります。
Linuxを物理マシンに直接インストールすれば、とても快適に動作します。

OS更新

Linuxには、Windowsのような強制バージョンアップや強制再起動はありません。
また、再起動ループのようなひどいバグも今のところ引いていません。

もちろん、Linuxでもサポート切れにならない程度に年1程度のアップグレードは必要です。
Fedoraについては、3行程度のコマンドを入れるだけで、トラブルなくアップグレードできました (過去記事)。

レジストリがない

Windowsレジストリって難しくないですか?
私だけでしょうか。

Linuxにはレジストリがないのがありがたいです。

Linuxにして困ったこと

周辺機器の動作

Windowsでは周辺機器を認識しないことで困ったことはあまりないと思います。

一方、私のLinux環境では、イヤホンが認識されなかったり無線LANが使えなかったりしました。
ドライバ関連で問題が出るケースはあります。
どちらも私の環境では解決できましたが、環境次第では解決に時間がかかることもあると思います。

もし解決できなければ、イヤホンが認識されない場合はBluetoothスピーカーで我慢する。
無線LANが使えない場合は有線で我慢する。
...といった回避方法は環境次第で取れると思います。

また、パソコンでゲームしたい方は、グラフィックカードのドライバがLinuxでも問題なく動くか確認したほうが良いかもしれません。

初期設定の難易度

Linuxの初期構築時、Linuxの知識は必要になります。

Linuxのインストールに始まり、デスクトップ環境を導入したり、日本語環境を整えたり...といった具合です。

これらのタスクは面倒ですが、良い経験値になります。
検証環境の踏み台サーバを作ったり、ネットワーク検証用のsyslogサーバを建てたり...。
インフラエンジニアであれば、必ず役に立つノウハウが得られます。

Office

LinuxではMS Officeが動きません。
代替製品を使うことになります。

ExcelPowerpointを日常的に使う方には辛いかもしれません。

私は私生活でOfficeを使う機会が限られているので、ここは妥協しました。
Google SpreadSheetやGoogle Slidesの使い勝手が十分良いので、それほど困りませんでした。

ただクレジットカードやら家計簿など、どうしてもクラウドに置きたくないファイルにはGoogle Appsは使えません。
オフライン環境が必要な場合に限り、FreeOfficeで妥協しています。
使い勝手は60点ぐらい...ですが、機会が少ないので我慢しています。

フリーソフト

Windowsで使えていたフリーソフトも、Linuxには存在しないケースがあります。
ただ、探せば結構見つかります。

以下に代替ソフトの例を書きます。

Windows Linux 備考
ペイント Pinta 軽量なお絵描きツール
Snipping Tool Ctrl + Shift + PrtScreen ソフト不要
メディアプレイヤー Celluloid ショートカットキー充実
軽量
オフィス GSSなど
FreeOffice
OnlyOffice
WPS Office
完璧なものはない

裏を返せば、上記以外でソフトがなくて困ったことはあまりありません。

フォント

LinuxではMSゴシックなどのフォントが使えません。
MS系のフォントはその名の通りMicroSoftの製品に付属するもので、フリーフォントではないためです。

私生活ではあまりないかもしれませんが、Excelなどのローカルのファイルを他の人と共有するときに、互換性に難儀するかもしれません。
ただ家のパソコンで仕事することが無いのであれば、困ることはそうそうないと思います。

自分の作業の都合だけであれば、Noto Sans CJK JPSource Code Proなどを使っておけば生活に困ることはありません。

(おまけ) Fedoraを選んだ理由

RedHatディストリビューションの操作に慣れていたためです。
また、CentOSなどと比較してパッケージの品揃えが圧倒的に良いです。
新しいパッケージの方が使い勝手がよく、互換性もあり、ちゃんと動きます。
パソコン用途ならCentOSよりFedoraの方が良いですよ。

後から海外エンジニアの動画などで何度も聞いたのですが、RedHat系のディストリビューションはよくテストされていて、安定性に優れているとのことです。
たまたまですが、そういった意味でも良い選択でした。

ただ、debの方がrpmと比較してパッケージの品揃えが良いようです。
パソコンでゲームをするなど、検証以外でも多様な用途でパソコンをフル活用したい場合は、Debian系のディストリビューションもありかもしれません。
Linux Mintなど、扱いやすくて人気がありますよね。

(おまけ) Cinnamon を選んだ理由

Windowsから移行して全く違和感がないのがCinnamonでした。
それどころか、Windowsよりデザインや使い勝手が良く、とても気に入っています。
他にもWindowsライクなデスクトップとしてKDEがありますが、設定画面のわかりやすさではCinnamonに軍配が上がりました。

拡張機能 (Desklet) の充実具合は圧倒的にKDEですが。
Cinnamonの場合は拡張機能を一切使わないことになると思います。

他にも以下のような特徴があります。

  • 動作が安定している
  • UIに統一感があってわかりやすい
  • Windowsライクな見た目
  • Windowsライクなショートカットキー (Alt+F4など)。更にカスタマイズも可能
  • 普通に綺麗
  • Windowsより良いところも...
    • File Manager (≒Explorer) にデフォルトでタブがついている
    • Windowsにはないショートカットキーもある
    • ショートカットキーを手軽に設定できる

Mac出身の方は別のデスクトップ環境の方が良いのかもしれません。

最後は結局好みになるので、仮想マシン上に様々なデスクトップ環境をインストールして、あれこれ試す工程は必ず必要になると思います。

まとめ

LinuxをメインPCにして感じたことをあれこれ書きました。
まとめると、「はやい、やすい、うまい簡単」に尽きます。

ゲーミングPCとしての活用は難しそうですが、検証やブラウジングやドキュメント作成に限れば非常に使い勝手が良いです。

Windowsが重くてうまく動かないノートPCなどあれば、試しにLinuxを入れてみてはいかがでしょう?
思いがけず、良いサブPCが手に入るかもしれません。

venvが動作する仕組みを調べてみた

python-logo

お伝えしたいこと

Pythonのvenvがどのような仕組みで動いているか気になったので、少し詳しく調べてみました。
ソースコードは読めないので、いつものようにドキュメント上の仕様を追いかけつつ、実機で検証してみました。

今回は、「興味のある方向け」のマニアックな記事です。
あまり興味のない方は、サマリのみご覧ください。

前提知識として、venvの基本的な使い方の理解が必要です。

サマリ

まずは要点を書きます。
これだけで十分かもしれません。

仮想環境ディレクトリ配下の実行ファイル (仮想環境/bin/*) を実行すると、以下の動きになります。

source 仮想環境/bin/activateを実行すると、PATHの先頭に仮想環境/binが追加されます。
これによりパスを指定せずにコマンド実行すると、/usr/binなどのシステム全体の実行ファイルよりも仮想環境の実行ファイルが優先的に実行されます。
例えば仮想環境内でansibleコマンドを実行すると、/usr/bin/ansibleよりも仮想環境/bin/ansibleが優先的に実行されます。
そして仮想環境配下の実行ファイルを実行すると、上述の通り仮想環境と紐づくPythonインタプリタとライブラリが参照されるので、諸々が仮想環境配下で動くようになります。

上記の動作を実装しているのがsite.pyというスクリプトであり、その中でも重要な役割を果たすのがsys.executablesys.prefixといった値です。
以降のセクションでより詳細な仕組みを紹介しますので、興味のある方はご覧ください。

PATHの変更

ドキュメント確認

Python仮想環境に入る時、source 仮想環境/bin/activateコマンドを実行します。
ここで実行しているactivateは仮想環境作成時に自動生成するシェルスクリプトで、内部的には以下の処理を実行しています。

  • PATHの先頭に仮想環境配下のファイル (仮想環境/bin) を追記する
    • 例えば仮想環境に入ったあとにpipコマンドを実行すると、/usr/bin/pipの代わりに仮想環境/bin/pipが呼ばれるようになる
  • 他にも以下の処理をしている
    • 環境変数VIRTUAL_ENVに仮想環境のディレクトリパスを格納
    • 環境変数PS1を書き換えることで、シェルプロンプトの文字列を変更
    • 仮想環境から抜けるためのdeactivateコマンドを定義

実機確認

前提として、予め~/venv/systemにsystemという名前のPython仮想環境を作成しておきます。

$ python3 -m venv ~/venv/system

仮想環境に入る前後でPATHを比較してみます。
仮想環境に入った後は、PATHの先頭に~/venv/system/binが追加されていました。

$ echo $PATH
/home/endy/.local/bin:/home/endy/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin

$ source ~/venv/system/bin/activate

(system)$ echo $PATH
/home/endy/venv/system/bin:/home/endy/.local/bin:/home/endy/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin

では、~/venv/system/bin配下のファイルがどうなっているかというと、Shebangで仮想環境配下のPythonインタプリタを指定していました。
そして、仮想環境配下のPythonは、venvを作成した時のPythonへのシンボリックリンクになっていました。

$ head -1 ~/venv/system/bin/pip
#!/home/stopendy/venv/system/bin/python

$ file /home/stopendy/venv/system/bin/python
/home/stopendy/venv/system/bin/python: symbolic link to /usr/bin/python

結局のところは、シンボリックリンク越しに/usr/bin/pythonが呼ばれる作りになっています。

そうなると「仮想環境のPythonを実行しても、結局/usr/bin/pythonシンボリックリンクされているなら同じ動作になるじゃないか」と思いたくなるところですが、実は「一旦仮想環境配下のPythonインタプリタのパスを指定して実行することで、仮想環境を利用している扱いになる」ような仕組みがsite.pyに実装されています。
具体的には、sys.executableに直接実行されたPythonインタプリタシンボリックリンクのパス (仮想環境/bin/python) が格納され、それによって「venv内である」と識別しています。

次のセクションに詳細を書きます。

Pythonの実行パスによってprefixを書き換える

ドキュメント確認

sys.prefix

前提知識として、sys.prefixsys.exec_prefixの仕様を紹介します。

sys.prefixsys.exec_prefixには、Pythonのインストール先のパス (./configure --prefix=...) がセットされています。
多くの場合、Pythonソースコードからビルドしてインストールした場合は/usr/localPythonrpmなどのパッケージからインストールした場合は/usrがセットされます。

sys.prefixsys.exec_prefixの値は、Pythonスクリプトを実行する度に後述のsite.pyによって書き換えられます。
一方でsys.base_prefixsys.base_exec_prefix (base_prefixと呼びます) も同じデフォルト値 (/usrなど) を持ちますが、site.pyによって書き換えられることはなく常に同じ値を持ち続けます。

site.pyにおいて上記4種類のprefixがどのような意味を持つかは、次のセクションで詳しく説明します。

site.py

sys.pathは、Pythonパッケージの検索パスを示す値です。
そして、sys.pathの値をセットすることで、Pythonパッケージの検索パスを決めているのがsiteです (ソースコード)。
site.pyの処理の中で、Pythonの実行パスから仮想環境を判定し、sys.pathの値を書き換える処理も書いてあります。
つまり、site.pyの動作を理解することがvenvを理解することに繋がります。

site.pyのコメント部分を読むと、以下の処理をしていると記載がありました。
1つ目の処理は前準備で、2つ目の処理が最も重要です。

  1. sys.executable(実行したPythonインタプリタへのファイルパス。仮想環境の場合はシンボリックリンクへのファイルパス) の1つ上のディレクトリにpyvenv.cfgが存在した場合、そのディレクトリのパスがsys.prefixsys.exec_prefixにセットされる
  2. Unix系OSの場合は、sys.prefixsys.exec_prefixの末尾にlib/pythonX.Y/site-packagesを追記したパスをsys.path配列に追加する
  3. pyvenv.cfginclude-system-site-packages = false以外の値が指定されていた場合は、sys.base_prefixsys.base_exec_prefixで示されるシステムのPythonパッケージも検索対象としてsys.pathの末尾に加わる

実機確認

机上で確認したvenvの仕様を実機上でも確認してみます。

仮想環境に入る前

prefixは、全て/usrを指しています。
すなわち、Pythonパッケージの検索パスは/usr/lib/python3.9/site-packages/配下になります。
このことは、sys.pathの値からも読み取れます。

$ python3 -c 'import sys; print(sys.executable)'
/usr/bin/python3'

$ python3 -c 'import sys; print(sys.base_prefix)'
/usr

$ python3 -c 'import sys; print(sys.base_exec_prefix)'
/usr

$ python3 -c 'import sys; print(sys.prefix)'
/usr

$ python3 -c 'import sys; print(sys.exec_prefix)'
/usr

$ python3 -c 'import sys; print(sys.path)'
['', '/usr/lib64/python39.zip', '/usr/lib64/python3.9', '/usr/lib64/python3.9/lib-dynload', '/usr/lib64/python3.9/site-packages', '/usr/lib/python3.9/site-packages']

仮想環境に入った後

まずは、pyvenv.cfgと、仮想環境内のPython実行ファイルの位置関係を確認してみます。
仮想環境を作った時点で、~/venv/system/配下にpyvenv.cfgpythonコマンドへのシンボリックリンクが生成しています。
python実行ファイルの上の階層にpyvenv.cfgが存在する」というPython仮想環境として動作するための条件を満たしています。
つまり、仮想環境配下のPythonを実行すると、sys.prefixsys.exec_prefixが書き換わることになります。

(system)$ file ~/venv/system/pyvenv.cfg ~/venv/system/bin/python3
/home/endy/venv/system/pyvenv.cfg: ASCII text
/home/endy/venv/system/bin/python3: symbolic link to /usr/bin/python3

次に、sys.pathが仮想環境に入ることで変わる様子を確認します。
sys.prefixsys.exec_prefixは仮想環境を反映して/home/endy/venv/systemに変わっています。
これに伴って、Pythonパッケージの検索パス (sys.path) も仮想環境配下に切り替わっています。

一方で、sys.base_prefixsys.base_exec_prefixは特に値が書き換わることのない仕様のため、変わらず/usrにセットされたままです。
これによって仮想環境と実環境のPythonパッケージパスの両方を区別しています。
pyvenv.cfginclude-system-site-packages=trueオプションを使う場合、こういった区別が必要になります。

$ source ~/venv/system/bin/activate

(system)$ python3 -c 'import sys; print(sys.exec_prefix)'
/home/endy/venv/system

(system)$ python3 -c 'import sys; print(sys.executable)'
/home/endy/venv/system/bin/python

(system)$ python3 -c 'import sys; print(sys.base_prefix)'
/usr

(system)$ python3 -c 'import sys; print(sys.base_exec_prefix)'
/usr

(system)$ python3 -c 'import sys; print(sys.prefix)'
/home/endy/venv/system

(system)$ python3 -c 'import sys; print(sys.path)'
['', '/usr/lib64/python39.zip', '/usr/lib64/python3.9', '/usr/lib64/python3.9/lib-dynload', '/home/stopendy/venv/system/lib64/python3.9/site-packages', '/home/stopendy/venv/system/lib/python3.9/site-packages']

更に、仮想環境のpyvenv.cfgについて調べてみましょう。

まず、pyvenv.cfgの中身を見てみます。
include-system-site-packages = falseがデフォルトで記載されているので、この部分を変更しない限りは仮想環境にシステムのパッケージを読み込まないことになります。

(system)$ file ~/venv/system/bin/python3
/home/endy/venv/system/bin/python3: symbolic link to /usr/bin/python3

(system)$ cat ~/venv/system/pyvenv.cfg 
home = /usr/bin
include-system-site-packages = false
version = 3.9.0

pyvenv.cfgのファイル名を変えてみたら仮想環境が認識されなくなり、sys.prefix/usrを指すようになりました。
自動生成しているpyvenv.cfgは、仮想環境の肝なので誤って削除しないよう気をつけましょう。

(system)$ mv ~/venv/system/pyvenv.cfg ~/venv/system/_pyvenv.cfg
(system)$ python3 -c 'import sys; print(sys.prefix)'
/usr

まとめ

Pythonの根幹にはsite.pyが存在し、Python起動時のコマンドラインのパスによってPythonパッケージ検索先 (sys.path) を書き換える仕組みを実装していました。
Python実行ファイル (シンボリックリンク) の一階層上にpyvenv.cfgファイルが存在すること」がsys.pathを書き換える条件です。

venvによって仮想環境を生成すると、これらの条件を満たすようにpyvenv.cfgpython実行ファイルへのシンボリックリンクを生成します。
そして、source 仮想環境/bin/activateを実行すると、PATHが書き換わることで仮想環境のシンボリックリンクを参照するようになります。
これによってsys.executablesys.prefixsys.exec_prefixsys.pathなどが書き換わり、Pythonimportした時のモジュール検索パスが変わっているという原理でした。
本記事では触れていませんが、pip install実行時のインストール先のディレクトリも上記で書き換わった変数を参照しています。

余談ですが、「仮想環境に入る」という操作をPATHの書き換えによって実現していたということは...やはりsudo pipはPATHを書き換えるのでご法度です。
sudoを実行することで、知らず知らずの間に仮想環境の外に出てしまいます。
恐ろしいですね。。

さて、前の記事と併せて、venvの使い方と仕組みについて理解が深まりました。
私自身も含めてですが、これからPythonプログラミングやPython関連ツールの利用を始める方にとってお役に立てれば幸いです。

Pythonパッケージの管理方法

python-logo

お伝えしたいこと

Pythonパッケージ管理で色々悩んだ結果、私が実践している方法を紹介します。
あまりスマートではないかもしれませんが、環境を汚しにくい設計を心がけています。

Python初心者なので、あまり凝ったツールなどは使っていません。
その分構成がシンプルなので、私と同じようなフェーズにいる方にとって役に立つといいなと思って投稿しています。

サマリ

先に要点を書きます。
私が現在実践しているPythonパッケージ管理方法は、以下の通りです。

外部アプリケーションから参照されるパッケージは、以下のように管理する

  • 一般ユーザー権限のpip install~/.local/配下にインストールする
  • 必要なパッケージと理由を書いてrequirements.txtに保存しておく
  • Pythonパッケージ構成をリセットする便利コマンドを作っておく

開発したPythonのテストをするときは、venv配下にパッケージをインストールする。

外部から参照されるPythonパッケージの管理方法

どんな状況か?

例えばVS Code拡張機能を駆使して開発をしているとき、拡張機能をフル活用するためにPythonライブラリをインストールする必要が出てきます。

Python拡張機能を使っているとき、Linter機能をフル活用するために様々なLinterライブラリをインストールすることになります。
(参考) 2021年Python開発リンター導入のベストプラクティス

例えば...

  • black
  • flake8
  • pylint
  • mypy
  • bandit

別の例としてAnsible Language拡張機能を使っている場合は、AnsibleやYAMLのLint機能を持つライブラリをインストールすることでその機能をフル活用できます。
そこで、以下のパッケージが必要となります。

  • ansible
  • ansible-lint
  • yamllint

これらのPythonパッケージをインストールして、VS Codeから参照させたい状況を今回の例として考えてみます。

pip install --userでインストールするのが素直な方法です。
しかし、後述の通りpipdnfyumなどのディストリビューション付属のパッケージマネージャーとは異なり、依存パッケージのゴミを残しやすい欠点があります。

venv配下にパッケージをインストールすれば、不要なvenvごと削除できるので環境をクリーンに保ちやすいです。
しかし、venv配下にこれらのパッケージを入れるのはあまり良くありません。
venv配下のPythonインタープリタや実行ファイルをフルパスで指定することで、venv配下のライブラリを参照できるケースは確かにあります。
しかし、参照元 (VS Codeなど) や参照先 (Pythonパッケージ) の実装により、うまく動かないケースが多々あるのです。

このような状況をどうにかするのが、今回ご紹介する方法です。

ユーザー権限でインストールする

前述のとおり、venvは使いません。
pip install --user ...により、ユーザー権限でインストールします
sudoコマンドなどにより特権を持たせない限り、--userはデフォルトのオプションです。
間違えてもsudo--systemオプションは使わないようにしてください。

このオプションをつけた場合、Pythonパッケージは~/.local/配下にインストールされます。
つまりホームディレクトリ配下にインストールされるため、システムを汚しません。

例えば、Ansible Language拡張機能とansible-lint、yamllintを連携させてフル活用させるために追加のパッケージをインストールしたい場合は、以下のようにコマンド実行します。

pip install ansible-lint yamllint ansible

インストールしたパッケージはrequirements.txt に書いておく

インストールしたパッケージは、requirements.txtに書いておきましょう。
必要な理由もセットでメモしておくと良いと思います。

私の環境では、~/.local/lib/requirements.txtに以下のように記載しています。

# For "Ansible Language" VS Code Extension to work with ansible-lint and yamllint
ansible-lint
yamllint
ansible

requrements.txtを書いておく理由は、後でまっさらな環境にリセットするときに使うためです。

Pythonパッケージ構成をリセットする便利コマンド

自分が望んだPythonパッケージのみがインストールされた状態にリセットする手段を用意しておきましょう。

私の環境では、以下を~/.bashrcに登録しています。
このコマンドを実行すると、いつでも指定したPythonパッケージのみを残して他が全て消えてくれます。

alias pip_reset='pip freeze --user | xargs pip uninstall -y; pip install -r ~/.local/lib/requirements.txt'

引数を受け取らない単純な作りですが、これが結構役に立ちます。
このコマンドによっていつでもパッケージ構成をリセットできるので、ゴミが残ることを気にせず色々なパッケージをインストールできるようになりました。

なお、このコマンドはPythonパッケージの数が多いと処理に時間がかかります。
全てのPythonパッケージをアンインストールし、requirements.txtに書かれたパッケージを全て再インストールするためです。

本来であればrequirements.txtと現状インストールされたパッケージの差分のみをUninstall / Install すれば良いと思うのですが、良い方法が思いつきませんでした。
このコマンド自体、たまにしか実行しないので今は妥協しています。
より効率的なコマンドを思いついたら、後々更新しますね (良い方法をご存知の方はコメントやTwitterなどで教えてください!)

(参考) 単純な pip uninstall ではダメなのか?

pip uninstallコマンドは引数に指定したパッケージのみ削除しますが、依存パッケージは削除してくれません。

例えば、pip install ansible-lintを実行すると、依存関係にあるパッケージを複数インストールします。

# 最初は何もインストールされてない状態
pip list --user

pip install yamllint
# Successfully installed pathspec-0.8.1 yamllint-1.26.1

pip list --user
# Package  Version
# -------- -------
# pathspec 0.8.1
# yamllint 1.26.1

ここで、yamllintをアンインストールします。
これで元の状態に戻ってくれれば良いのですが、依存パッケージのpathspecは消えずに残ってしまいました。

pip uninstall yamllint
# Proceed (y/n)? y
#   Successfully uninstalled yamllint-1.26.1

pip list --user
# Package  Version
# -------- -------
# pathspec 0.8.1

この問題を解決したのが、今回紹介しているコマンドです。
次のセクションでコマンドの意味を簡単に紹介します。

(参考) 便利コマンドの意味

まずは、先ほど紹介したコマンドの前半部分について、挙動を補足します。

pip freeze --user | xargs pip uninstall -y

pip freeze --userは、ユーザー権限でインストールしたPythonパッケージを一覧表示するコマンドです。
xargsにより、各行の値をpip uninstall -yの引数として渡しています。
インストールされたパッケージを明示的にpip uninstallコマンドに全て渡すことで、全パッケージを削除しています。
--userオプションをつけているので、OS全体に予めインストールされているパッケージは処理の対象外になります。

例えば、pip freeze --userの出力が以下の2行だったとします。

pathspec==0.8.1
yamllint==1.26.1

この状態で本セクションの冒頭のコマンドを実行すると、以下の2行のコマンドと同じ意味になります。

pip uninstall -y pathspec==0.8.1
pip uninstall -y yamllint==1.26.1

続いて、後半のコマンドです。
requirements.txt に記載したパッケージを全てインストールします。

pip install -r ~/.local/lib/requirements.txt

2つのコマンドを続けて実行することで、「Pythonパッケージを全て削除して、必要なもののみ入れ直す」動きになります。
これにより、Pythonパッケージ構成をリセットしています。

pip freeze --user | xargs pip uninstall -y; pip install -r ~/.local/lib/requirements.txt

開発したPythonパッケージのテストをするとき

開発したPythonパッケージのテストをするときは、最低限のパッケージ構成で試験をするためにvenvを作って動作確認をするのが良いと思います。

venvを使うことで、最低限のパッケージ構成で動作確認することができます。
またvenv作成は、先ほど紹介したPythonパッケージ構成のリセットコマンドの実行よりも遥かに高速に完了します。

また、venvは事前に複数作った上で、用途に応じて切り替えるような使い方にも向いています。

具体的な操作手順は、以下のようなイメージになります。

# testという名前の仮想環境を作成
$ python -m venv ~/venv/test

# 仮想環境に入る
$ source ~/venv/test/bin/activate

# 好きなパッケージを入れる
(test)$ pip install ansible

# 動作確認する
(test)$ python3 app.py

実践的にはテスト用のライブラリなどを使うと思うのですが、私の知識が不足しており、まだわかりません。
今後良いやり方を覚えていきたいと思います。

まとめ

Pythonパッケージを気軽に導入できるように、自分なりに考えたことを共有しました。

これにより、VS CodePythonを書く時に気兼ねなく好きなパッケージを入れられるようになりました。

私自身PylintやFlake8など、便利そうなパッケージは色々入れて試せるようになりました。
そして時折リセットすることで環境を綺麗に保ちつつ、今後も検証・ハンズオンしていこうと思います。

systemd-resolvedの特徴と、使い方紹介

systemd-light

前の記事

systemd-resolvedがデフォルトのDNSクライアントとなる前後の/etc/nsswitch.confを比較しました。
また、興味のある方向けに詳細まで踏み込んで紹介しました。
忙しい方はサマリだけでもご覧ください。

endy-tech.hatenablog.jp

お伝えしたいこと

Fedora33以降で (/etc/hostsmulticast DNSを除けば) デフォルトのDNSクライアントとなったnss-resolve (systemd-resolved)について概要を紹介します。
従来のDNSであるnss-dns (/etc/resolv.conf)との違いに着目しつつ、systemd-resolvedの基本構成や便利機能 (per-link DNS)、使い方について書きました。

設定ファイルである/etc/systemd/resolved.conf の書きっぷりについては深く触れていませんが、基本動作にてDNSサーバ選出時にどう関わるかのみ紹介しています。
DNSサーバを使い分けない場合は/etc/systemd/resolved.confのみに記載するのがシンプルですが、per-link DNSを使う場合は寧ろNetworkManagerのみでほぼ全てを制御するのが良いと思います。
いずれにしても設定箇所をあまり分散せず、可能な限り一つにまとめるのがわかりやすくて良いでしょう。

systemd-resolved とは

systemd-resolvedは、Linux においてDNSクライアントとして動作します。
多くのLinuxディストリビューションでinitプロセスを管理しているsystemdの1コンポーネントでもあります。

systemd-resolvedの主要な構成要素は、下表の通りです。

要素 説明
systemd-resolved.service systemd に管理されているサービス。
systemctlによって起動・停止などを制御する
/etc/systemd/resolved.conf systemd-resolvedの設定ファイル
/run/systemd/resolve/stub-resolv.conf Fedora33以降では、/etc/resolv.confがこのファイルへのシンボリックリンクになっている。
digなど/etc/resolv.confを固定的に参照するプログラムとの互換性のために、systemd-resolved.service/run/systemd/resolve/stub-resolv.confを自動的に更新する
resolvectl systemd-resolvedコマンドラインツール。
systemd-resolvedの動作を確認したり、リアルタイムに変更したりできる

systemd-resolved のAPI

man systemd-resolved、またはArch Wiki によると、systemd-resolvedを使う方法 (API: Application Programming Interface) は3種類あります。
いずれのAPIもローカル専用で、ネットワーク越しに外部ホストからアクセスすることはできません。

man systemd-resolvedによると、サポートされる機能の多さとしては(1) > (2) >> (3)という関係になっており、systemdの開発者としては(3)よりも(1), (2) のAPIを使うことをより推奨しているようです。
(3) は、古いプログラムへの互換性を保つために残されています。

次のセクションでこれらのAPIがどのような使われ方をするのか、より具体的に紹介します。

# API 説明
1 D-Bus (Desktop Bus) プロセス間通信機能を提供するAPI
2 NSS (Name System Switch) /etc/nsswitch.confを介してアクセスする方式。
GNU C ライブラリによって実装されている
3 Local DNS Stub Listener TCP/UDP127.0.0.53:53 でリッスンしている
/etc/resolv.confを直接参照するプログラムが利用する

(1) D-Bus (Desktop Bus)

プロセス間で各種設定情報や命令を直接やり取りするための仕組みです (IPC: InterProcess Communication = プロセス間通信)
基本的には同一OS内のローカルのやり取りのみをサポートします。
ネットワーク越しに外部ホストとメッセージをやり取りするような使い方は、サポートされていないとのことです。

標準入力や標準出力、シェルなどのユーザー操作を介さずにプロセス間で直接やり取りすることで、ユーザーに意識させず、プロセス間でイイカンジに情報連携するための仕組みと理解しています。
アプリケーション開発者がこういった内部処理を作り込むことで、複数プロセスが矛盾なく連携し、粗結合なアーキテクチャで高度な機能や使い勝手を実現できるのでしょう。

D-Busの概要は、下記サイトにとてもわかりやすく解説されていました。

www.silex.jp

D-Busを利用している身近な例としては、私の知る限り2つあります。
どちらもNetworkManager関連です。

1つ目は、nmcliです。
nmcliで設定する際、裏ではNetworkManager.serviceD-Bus API を叩いて設定変更を反映しているようです。
nmtuiなど、その他のコマンドも恐らく同様です

以下の出力からも何となく察せると思います。

nmcli conn up eno1
# Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/5)

2つ目は、NetworkManagerからsystemd-resolvedへの設定情報連携です。
Fedora33以降の構成で、/etc/nsswitch.conf (resolveが選出され、DNSクライアントとしてsystemd-resolvedが選択される) や、/etc/resolv.conf (システムのDNSサーバとして127.0.0.53を参照する。結果としてsystemd-resolvedがスタブリゾルバとして問い合わせを代行する) だけを見ていると、NetworkManager のDNS設定が挙動に影響しない...と考えたくなるのですが、実は影響を与えます。

それは、NetworkManagerD-Bus APIによってsystemd-resolvedに設定情報を連携しているためです。
man NetworkManager.confに書いてありますが、/etc/NetworkManager/NetworkManager.confsystemd-resolvedという設定項目のデフォルト値がtrueになっています。
これにより、D-Bus API越しの情報連携が有効になります。

この挙動に馴染みのない方は、ご注意を...。
NetworkManagersystemd-resolvedの連携については、記事の後半で具体的な設定手順と共に紹介します。

(2) NSS (Name Service Switch)

DNSクライアントが名前解決をする際、/etc/nsswitch.confhosts:行を参照してどの方式で名前解決するかを決定します。

Fedora33以降の/etc/nsswitch.confは、以下のようになっています。

hosts:      files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] myhostname dns

比較のため、Fedora32 (恐らくRHEL8と同じ) の/etc/nsswitch.confも載せておきます。

hosts:      files mdns4_minimal [NOTFOUND=return] dns myhostname

詳細は前回の記事に譲りますが、多くの場合は/etc/hostsに記載がなければsystemd-resolvedによって外部DNSサーバに問い合わせするのが基本の動きです。

(3) Local DNS Stub Listener

Local DNS Stub Listener とは、TCP/UDPでリッスンしている 127.0.0.53:53のことです。

Fedora33以降では/etc/resolv.conf/run/systemd/resolve/stub-resolv.confへのシンボリックリンクとなっており、/run/systemd/resolve/stub-resolv.confsystemd-resolvedが自動更新するという構造になっています。
結果として、/etc/resolv.confを参照すると、システムのDNSサーバが127.0.0.53、つまりsystemd-resolvedに設定されていることになります。

digコマンドやhostコマンドのように、/etc/nsswitch.confではなく/etc/resolv.confを直接参照するプログラムにも互換性を持たせるため、こういったアクセスの仕方も用意されています。

systemd-resolved の基本機能

前回の記事で紹介しましたが、名前解決に使われるNSS (/etc/nsswitch.confの右側に列挙されているもののこと) には様々なものがあります。
systemd-resolved(=nss-resolve) は、既存のNSSの多くを置き換える機能性を持っています。
systemd-resolvedの場合はキャッシュが効くため、systemdのマニュアルでは従来のfilesmyhostnameよりも推奨されています (→ 前回の記事)。

既存のNSS名 既存NSSの機能 systemd-resolvedでの対応
files /etc/hostsを読み取る デフォルトで有効
mdns4_minimal mDNS機能 (あまり使わない) デフォルト無効。
/etc/systemd/resolved.confを書き換えて有効化できる
dns 外部DNSサーバにクエリを投げる) デフォルト有効。
従来のdns/etc/resolv.confによるシンプルな制御だった。
resolveでは、/etc/systemd/resolved.confでより高度な制御ができる
myhostname 自身のホスト名、*.localhostlocalhostなどの名前解決
(直接は使わないが、この機能を利用するプログラムもある)
デフォルト有効

Fedora33のデフォルトの/etc/nsswitch.conffilesが最初に検索されるため、/etc/hostsの検索にsystemd-resolvedが利用されることはありません。
/etc/nsswitch.confの順序を入れ替えてresolvefilesよりも左に配置することで、systemd-resolved/etc/hostsを読み取るよう設定することも可能です。

このようにsystemd-resolvedは従来のNSSの大半を置き換える機能性を持ちます。
後続のセクションでは、これらの機能の中でも従来のdnsに相当する、外部DNSサーバへの問い合わせの挙動について紹介します。

systemd-resolved と従来型DNSの違い

systemd-resolved (nss-resolve)と従来型DNS (nss-dns)の違いを示します。

設定ファイルの違い

従来のDNS/etc/resolv.confを参照していました。

一方で、systemd-resolved/etc/systemd/resolved.confを参照します。
それぞれの設定ファイルの仕様を見比べると、両者の違いがより詳細に見えてきます。

search (ドメイン自動補完)

まずは、search機能についておさらいします。
例えばsearch に xxx.com を指定していたとします。
ping Aを実行すると、Aが名前解決できなくてもxxx.comを自動補完してA.xxx.compingを実行してくれます。

従来のDNSでは、/etc/resolv.confndotsというオプションによって「ピリオドをいくつまで含んでいるときに自動補完の対象とするか」を制御できました (man resolv.conf)。

systemd-resolvedの場合はndotsを敢えて実装しておらず、単一ラベル (つまりピリオドを含まない) の場合のみドメインを補完します(man systemd-resolved)。
systemd-resolvedndotsを実装していない理由については、man systemd-resolvedで言及されています。

DNSサーバの複数指定

従来のDNSでは、最大3台までDNSサーバを指定できます。
基本的には1台目のみにクエリを投げます。
1台目のDNSサーバへのクエリがタイムアウトしたら、次のDNSサーバに問い合わせます。

systemd-resolvedでは、Routing という仕組みによってDNSサーバが選択されます。
ネットワークのIP Routing とは若干関係ありますが、完全に別物です。
これがper-link DNSに繋がります。
詳細は次のセクションで紹介します。

基本動作

systemd-resolvedは、ネットワークインターフェースごとにDNSサーバを指定できます。
その上で、Routing という仕組みによって、クエリごとにDNSサーバを使い分けます。

具体的には以下の動きになります (man systemd-resolvedPROTOCOLS AND ROUTINGより)
※グレーにした部分はあまり重要ではないので、無視してください
※5. についてはコマンド実行ログは掲載していませんが、私の手元で検証済みです

# 詳細
1 /etc/hostsや自身のホスト名、localhostlocalhost.localdomainXXX.localhostなどは、ネットワークにクエリを出すことなくローカルのみで名前解決する
2 単一ラベル (ピリオドを含まない) 場合は、Unicast DNS (通常のUDP#53のクエリ) は使われず、LLMNR (mDNSのように同一ネットワーク内の名前解決をするサービス) で名前解決する。
ただし、AレコードやAAAAレコードの場合は search domains によってドメイン名が補完され、Unicast DNS でクエリされる
3 *.localという名前はmDNSとして扱われるため、Unicast DNSとしては動作しない
4 複数ラベルA, AAAA, PTRレコードの場合 (ピリオドを含む場合) は、以下のロジックで最適なDNSサーバが選出される (man resolved.confDomains=も併せて参照) (※1)(※2)
  • アクセス先のFQDNが search domains やroute-only domainsと重なる場合は、そのインターフェースと紐づくDNSサーバが優先される
  • 複数の search domains やroute-only domainsに該当する場合は、ラベル数 (※3) の多い search domains と紐づくDNSサーバが優先される (best matching)
  • best matchingDNSサーバが複数あった場合、それらのDNSサーバ全てに対してクエリされる。クエリ結果が複数返ってきた場合は先に受信した方が優先される
  • ~.を含めてsearch domains に全く一致しなかった場合、5 に進む
5 4 のper-link DNSのsearch domains に一致しなかった場合、以下の挙動となる。
  • グローバルなDNSサーバ (/etc/systemd/resolved.confDNS) が設定されていれば、そこに問い合わせる
  • グローバルなDNSサーバが設定されていなかった場合、IPルーティングのデフォルトゲートウェイと紐づくper-link DNSサーバ (resolvectlコマンド出力で+DefaultRouteフラグがついているDNS) に問い合わせる
  • グローバルなDNSサーバ、IPデフォルトゲートウェイのいずれも設定がない場合、全てのper-link DNSとして設定されたDNSサーバに問い合わせ、最初に返ってきた応答を採用する

(※1) search domains に ~. を指定することで、どのFQDNにもマッチする指定が可能です (DNS Default Route)。best matching の観点では、最も優先順位が低いです。~をつけるとRouting Domain の扱いとなり、「DNS Routingの評価対象になるが単一ラベルに対するドメイン補完には利用されない」挙動になります。.はルートドメインを意味し、全てのドメインにマッチします

(※2) グローバルなDNS設定 (/etc/systemd/resolved.confDNSDomains) もper-link DNSと同様に評価されます。グローバルなDNSサーバがbest matching となれば、グローバルなDNSサーバのみにクエリされます。グローバルなDNSサーバとper-link DNSサーバの両方がbest matching となった場合は、どちらにもクエリされます

(※3) 例えば、www.google.comというFQDNにはwwwgooglecomという3つのラベルが含まれます

基本動作の要点

DNS Routing の挙動が非常にややこしいですが、上表の4と5をまとめると、A, AAAA, PTR レコードのクエリ先のDNSサーバは、以下の順序で評価されるようです。
あらゆる状況に対処できるように、systemd-resolvedには細かいルールが設定されていますが、基本的には1か2で確定できるように設計するのがわかりやすくてオススメです。

  1. ~.を含め、search domains に一致したDNS (best matching)。per-link DNS もグローバルなDNSも特に差はなく、同列に評価される
  2. グローバルなDNSが設定されていれば、そこにクエリする
  3. IP Routing の世界でデフォルトルートと紐づくNICにper-link DNSサーバが設定されていれば、そこにクエリする
  4. 全てのper-link DNSサーバにクエリする

設定方法

man resolved.confによると、/etc/systemd/resolved.confではシステム全体でクエリ先のDNSサーバを設定できますが、 search domains によって優先順位付けするような指定方法 (per-link DNS) はできません。

per-link DNSの設定はsystemd-networkd、または外部アプリケーション (例えばD-Bus連携しているNetworkManager) からの同期によって読み込まれます。
次のセクションで具体的な設定例を紹介しますが、設定例は使い慣れた NetworkManager (nmcli)にて紹介します。

どんな時に役に立つか?

Linux端末がVPNに接続している場合、VPN越しにアクセスするDNSサーバと、通常の物理ネットワーク越しにアクセスするDNSサーバで使い分けたいケースがあると思います。
社内向けドメインVPN越しのDNSサーバに、その他のgoogle.comなどのドメインは物理ネットワーク越しのDNSサーバに名前解決をしたいときに、per-link DNSが役に立つと思います。

(参考) 具体例

実際にDNS設定がどうなるか、色々な例で試してみました。

systemd-resolvedで/etc/hostsを認識させる

/etc/nsswitch.confを変更して、systemd-resolvedしか使わないように変更します。
これは検証のための設定変更であり、本来はこのようにすべきではないのでご注意ください。

#hosts:      files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] myhostname dns
hosts:      resolve [!UNAVAIL=return]

また、/etc/hostsに試験用のエントリを追加します。

8.8.8.8     google-hosts

この状態でgoogle-hostspingしたところ、名前解決できました。
確かに、systemd-resolved/etc/hostsを読み込むようです。

ping google-hosts
# PING google-hosts (8.8.8.8) 56(84) bytes of data.
# 64 bytes from google-hosts (8.8.8.8): icmp_seq=1 ttl=116 time=2.88 ms
# (以下略)

ちなみに、自身のホスト名へのpingも通りました (nss-myhostname相当)。

ping pc
# PING pc.internal1 (192.168.100.155) 56(84) bytes of data.
# 64 bytes from pc (192.168.100.155): icmp_seq=1 ttl=64 time=0.252 ms
# (以下略)

変更した/etc/nsswitch.confを元に戻しておきます。

hosts:      files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] myhostname dns

search domains にヒットする方を優先する

こちらの構成で検証します。

dns1

DNS1, DNS2 という2台のDNSサーバを用意し、PCからそれぞれにクエリを投げて名前解決する構成を組みました。
PCには、DNS1と紐づけてendy1.testを、DNS2と紐づけてendy2.testを search domains (ipv4.dns-search) に設定しています。
systemd-resolvedにおいて、問い合わせ先のDNSサーバは search domains に一致するものが優先される仕様です。
rr.endy1.testはDNS1に、rr.endy2.testはDNS2に問い合わせる構成となります。

PCのNetworkManager設定は、以下のようになっています。

# DNS1向け
nmcli connection add \
con-name dnstest1 \
ifname enp7s0 \
type ethernet \
ipv4.method manual \
ipv4.addresses 192.168.100.100/24 \
ipv4.dns 192.168.100.2 \
ipv4.dns-search endy1.test  # ★search domains

nmcli connection up dnstest1

# DNS2向け
nmcli connection add \
con-name dnstest2 \
ifname enp1s0 \
type ethernet \
ipv4.method manual \
ipv4.addresses 192.168.122.100/24 \
ipv4.dns 192.168.122.3 \
ipv4.dns-search endy2.test  # ★search domains

nmcli connection up dnstest2

DNS1には、 endy1.test.zoneendy2.test.zoneには、それぞれ以下の行を記載しました。
rr.endy1.testrr.endy2.testに、それぞれ192.168.100.2 (DNS1のIPアドレス) を紐づけました。
ゾーンファイルを2つ作ったのは、ドメイン名を区別するためです。

rr    A   192.168.100.2

DNS2には、 endy1.test.zoneendy2.test.zoneには、それぞれ以下の行を記載しました。
rr.endy1.testrr.endy2.testに、それぞれ192.168.100.2 (DNS2のIPアドレス) を紐づけました。
DNS1とは異なるアドレスを返すため、どちらにPCからDNS1とDNS2のどちらに問い合わせたかわかるようになっています。

rr    A   192.168.122.3

ここで、PCから2箇所にdigを実行してみます。
endy1.testはDNS1に問い合わせており、endy2.testはDNS2に問い合わせていることがわかります。
この挙動から、 search domains と紐づくDNSサーバに優先して問い合わせることがわかります。

dig rr.endy1.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy1.test.       6779    IN  A   192.168.100.2

dig rr.endy2.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy2.test.       6758    IN  A   192.168.122.3

resolvectl domainsystemd-resolvedが選択するリンクと search domains の関係を確認すると、以下のようになります。

resolvectl domain
# Global:
# Link 2 (enp1s0): endy2.test
# Link 3 (enp7s0): endy1.test

best matching が優先される (longest match)

黄色の部分のみ構成を変更しました。

dns2

rr.endy1.testrr.endy2.test共に、.testドメインに属します。
ただ、上図の構成においては、仕様上以下の動作になります。

  • rr.endy1.testはDNS1とDNS2の両方の search domains に該当するが、best matching の考え方に基づいて2階層分のドメイン名に一致するDNS1が優先される
  • rr.endy2.testは、DNS2の search domains にしか一致しないため、DNS2が優先される

黄色部分の変更差分のNetworkManager設定は、以下のようになります。

sudo nmcli connection modify dnstest2 ipv4.dns-search test
sudo nmcli connection up dnstest2

ここで、PCから2箇所にdigを実行してみます。
endy1.testはDNS1に問い合わせており、endy2.testはDNS2に問い合わせていることがわかります。
この挙動から事前の予想通り、より多くの label (ピリオドで区切られた文字列) に一致するDNSサーバが優先されることがわかりました。

dig rr.endy1.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy1.test.       4800    IN  A   192.168.100.2

dig rr.endy2.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy2.test.       86400   IN  A   192.168.122.3

resolvectl domainsystemd-resolvedが選択するリンクと search domains の関係を確認すると、以下のようになります。

resolvectl domain
# Global:
# Link 2 (enp1s0): test
# Link 3 (enp7s0): endy1.test

best matching なDNSサーバが複数存在する場合、全てのDNSサーバにクエリする

次は以下の黄色部分を変更しました。

dns3

search domains の条件を両方のリンクで揃えました。
search domains のbest matching でも同等な場合は、それらのDNSサーバ全てに問い合わせる仕様です。
そして、最初にDNS応答を受信した経路が採用されます。

NetworkManagerの設定差分は、以下のようになります。

sudo nmcli connection modify dnstest1 ipv4.dns-search endy1.test,endy2.test
sudo nmcli connection up dnstest1

sudo nmcli connection modify dnstest2 ipv4.dns-search endy1.test,endy2.test
sudo nmcli connection up dnstest2

dig で確認します。
DNSキャッシュをクリアしながら実行していると、途中で結果が変わっていることがわかります。
DNS1とDNS2のうち、先に受信した応答が表示されています。
どちらもほぼ同時に応答を受信するため、結果が不定になっています。

dig rr.endy1.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy1.test.       6554    IN  A   192.168.100.2

dig rr.endy2.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy2.test.       86400   IN  A   192.168.100.2

resolvectl flush-caches

dig rr.endy1.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy1.test.       86400   IN  A   192.168.100.2

dig rr.endy2.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy2.test.       86400   IN  A   192.168.122.3

最後に、resolvectlで設定を確認します。
両方の search domains にbest matching となっていることがわかります。

resolvectl domain
# Global:
# Link 2 (enp1s0): endy1.test endy2.test
# Link 3 (enp7s0): endy1.test endy2.test

DNSデフォルトルート

DNSデフォルトルートの挙動を確認します。
DNS1と紐づくリンクに、デフォルトゲートウェイを設定します。
また、 search domains を空欄にすることで一致しないようにします。

dns4

search domains に具体的なキーワードが全く該当しない場合、デフォルトルートが評価されます。
以下の2通りのいずれかの条件に該当するリンクは、デフォルトルートとして扱われます。

(※) .は、ルートドメインなので全てのドメインに一致します。また、ドメイン名の先頭に~を付けると、「ホスト名にドメイン名を補う」効果はなく、「best matchingなDNSサーバの選択」のみに寄与するという意味になります (routing domain)。

即ち、今回の場合は全てのクエリがDNS1を向くようになります。

NetworkManager上は、以下の変更差分となります。

sudo nmcli connection modify dnstest1 ipv4.gateway 192.168.100.1
sudo nmcli connection modify dnstest1 ipv4.dns-search ""
sudo nmcli connection up dnstest1

sudo nmcli connection modify dnstest2 ipv4.dns-search ""
sudo nmcli connection up dnstest2

digを実行してみると、実際に全てのクエリがDNS1を向くようになります。

dig rr.endy1.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy1.test.       86400   IN  A   192.168.100.2

dig rr.endy2.test | sed -ne '/ANSWER SECTION/,/^$/p'
# ;; ANSWER SECTION:
# rr.endy2.test.       86400   IN  A   192.168.100.2

resolvectl domainからは、両リンクで search domains が空っぽであることがわかります。
resolvectl dnsの出力では、実質的にDNS1にしか問い合わせが向いていないことを反映して、DNS1のみが表示されます。
resolvectlからは、IPルーティングのdefault routeが反映されて、DefaultRouteのフラグが片方のリンクで有効 (+) になっていることがわかります。

resolvectl domain
# Global:
# Link 2 (enp1s0):
# Link 3 (enp7s0):

resolvectl dns
# Global:
# Link 2 (enp1s0):
# Link 3 (enp7s0): 192.168.100.2

resolvectl
# Link 2 (enp1s0)
#      Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

# Link 3 (enp7s0)
#          Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
# Current DNS Server: 192.168.100.2                                               

長くなるのでコマンド実行結果は省きますが、いくつか追加で補足します。

DNS2の search domains に~.を追加すると、IP Routing のデフォルトゲートウェイよりも優先されてDNS2のみに問い合わせるようになります。
NetworkManagerの設定コマンドとしては、以下のようになります。

sudo nmcli connection modify dnstest2 ipv4.dns-search "~."
sudo nmcli connection up dnstest2

更にDNS1の search domains に~.を追加すると、DNS1とDNS2の両方にクエリを出すようになりました。
search domains に~.が設定されたリンク同士であれば、ipv4.gatewayの設定有無が優先順位に影響することは無いようです。

現在のDNS設定の確認方法

systemd-resolvedは、/etc/systemd/resolved.confやデフォルトルート、他デーモンからのD-Bus連携によって挙動が変わるため、非常に複雑です。
resolvectlコマンドを使えば、systemd-resolvedの設定情報を確認できるため便利です。

resolvectlをそのまま実行すると、最も詳細な情報が出てきます。
resolvectl domainによって、per-link DNSと関係する search domains を確認できます。
resolvectl dnsによって、各リンクと紐づくDNSサーバを確認できます。

具体的な出力例は、(参考) 具体例を参照してください。

(参考) D-Bus関連コマンド

D-Bus周りでそれっぽいコマンドを実行してみました。
D-Busに対応しているプロセスであれば、CLIGUIだけでなくD-Bus APIでもステータスを確認できます。
--xmlオプションをつければ、XMLで出力することも可能です。

オブジェクトパス (/org/freedesktop/NetworkManager/DnsManager) はインターネットで「NetworkManager D-Bus」で調べても良いですが、bash-completion の働きでタブ補完することでも簡単に確認できました。

D-Bus オブジェクトにはプロパティとメソッドが紐付いて定義されており、コマンドラインで参照できます。
中々使う機会はないのですが、こういったものも存在することを知っておくとたまに便利かもしれません。

# オブジェクトを指定して、メソッドやプロパティの名前や型情報、持っている値を参照できる
gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/DnsManager

# node /org/freedesktop/NetworkManager/DnsManager {
# (中略)
#   interface org.freedesktop.NetworkManager.DnsManager {
#     methods:
#     signals:
#     properties:
#       readonly s Mode = 'systemd-resolved';
#       readonly s RcManager = 'symlink';
#       readonly aa{sv} Configuration = [{'nameservers': <['192.168.1.1']>, 'interface': <'eno1'>, 'priority': <100>, 'vpn': <false>}];
#   };
# };

まとめ

具体例を入れたら非常に長くなってしまいましたが、systemd-resolvedの挙動についてまとめました。

D-Busの働きにより、NetworkManagerの設定変更によりsystemd-resolvedの設定を変更できるので、使い勝手は従来と大きく変わりません。

一方でVPNを利用している場合など、宛先ドメインによってDNSサーバを使い分けたいときにはsystemd-resolvedの per-link DNS 機能が役に立ちそうです。