tag:crieit.net,2005:https://crieit.net/users/advanceboy/feed advanceboyの投稿 - Crieit Crieitでユーザーadvanceboyによる最近の投稿 2024-02-27T00:48:40+09:00 https://crieit.net/users/advanceboy/feed tag:crieit.net,2005:PublicArticle/18777 2024-02-27T00:46:48+09:00 2024-02-27T00:48:40+09:00 https://crieit.net/posts/Raspberry-Pi-5-Ubuntu-24-04-LTS-SSH Raspberry Pi 5 に Ubuntu 24.04 LTS を入れて SSH するまで <p>ついに日本でも、 Raspberry Pi 5 の技適対応モデルが発売された。</p> <p>Raspberry Pi にインストールする OS と言えば、まずは <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.com/software/">Raspberry Pi OS (旧 Raspbian)</a> だが、同じ Debian ベースの <a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/download/raspberry-pi">Ubuntu</a> も Raspberry Pi 向けの公式イメージを出している。</p> <p>但し、 Raspberry Pi 5 に対応しているのは、 Ubuntu 23.10 (コードネーム: mantic) 以降のみとなる。</p> <p>現在リリース済みの 23.10 は「非LTS」バージョンとなり、サポートが 9ヶ月 (2024年6月まで) と短い。<br /> 常用するなら、サポートが 5年 (ESM なら 10年) と長い LTS バージョンが望ましいが、残念ながら 22.04 LTS Raspberry Pi 5 に非対応だ。</p> <p>LTS バージョンの次回リリースは 24.04 LTS (コードネーム: noble / 2024年4月リリース予定) で、現在絶賛開発中だ。</p> <p>そんな開発中の 24.04 LTS も、デイリースナップショットが手に入る。<br /> そいつを使って<strong>ディスプレイやキーボードが無い状態</strong>で、インストールから SSH 接続までやってみよう。</p> <p>Raspberry Pi 5 をターゲットに書いているが、 Raspberry Pi 3, 4, Raspberry Pi Zero 2 W でも同じ手順でできるはずだ。</p> <p>なお、この記事は 24.04 LTS がリリースされたら、それに併せて内容に書き換えていくつもり。</p> <h2 id="TL;DR"><a href="#TL%3BDR">TL;DR</a></h2> <ul> <li>microSD カードに Ubuntu の OS イメージを焼いたあと、 RasPi に挿す<strong>前</strong>に初回起動時の設定を書き換える</li> <li>cloud-init の挙動を理解しよう</li> <li>ディスプレイなし & 無線LAN Only だと少し工夫がいる</li> <li>今回の記事の内容は、<a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/?p=1404">Raspberry Pi に Ubuntu を入れて SSH でログインするまでの A to B | Aqua Ware つぶやきブログ</a> の二番煎じ <ul> <li>Ubuntu 24.04 LTS に合わせて内容はアップデートされている</li> </ul></li> </ul> <h2 id="準備するもの"><a href="#%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B%E3%82%82%E3%81%AE">準備するもの</a></h2> <ul> <li>Raspberry Pi 5 <ul> <li>Raspberry Pi 3, 4 でも同じ手順でいけるはず</li> </ul></li> <li>4GB 以上の micorSD カード <ul> <li>IOPS が高い、「アプリケーションパフォーマンスクラス」が A2 のものが良いだろう。値段や速度を考えると、実用上は 64GB 以上になるだろうか。</li> <li>私は、<a target="_blank" rel="nofollow noopener" href="https://amzn.to/42J1AeI">ARCANITE 64GB microSDXCカード 【A2】、UHS-I U3、V30、4K、C10</a> 値段の割にパフォーマンスが良かったため、これを使った。</li> <li>後述するが、最終的には NVMe SSD にした方が断然良い</li> </ul></li> <li>microSD を書き込める PC (Win, Linux, mac)</li> <li>インターネットに繋がる Wi-Fi または イーサネットケーブル</li> </ul> <p>以下もあると便利だが、今回の手順では無くても問題ない。</p> <ul> <li>micro-HDMI で繋がるディスプレイ 又は 変換コネクタ</li> <li>RasPi に繋げる USB キーボード</li> </ul> <h2 id="イメージファイルの取得"><a href="#%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%8F%96%E5%BE%97">イメージファイルの取得</a></h2> <p>正式リリース前の Raspberry Pi 向けデイリースナップショットイメージは、以下のいずれかから手に入る。</p> <p>今回は、サーバーOS的に SSH して使う事が目的なので、<strong>後者のサーバー版</strong>を利用する。</p> <ul> <li>Raspberry Pi 向けデスクトップ版イメージ <ul> <li><a target="_blank" rel="nofollow noopener" href="https://cdimage.ubuntu.com/daily-preinstalled/current/">https://cdimage.ubuntu.com/daily-preinstalled/current/</a></li> </ul></li> <li>Raspberry Pi や、汎用 64-bit ARM, RISC-V 向けサーバー版イメージ <ul> <li><a target="_blank" rel="nofollow noopener" href="https://cdimage.ubuntu.com/ubuntu-server/daily-preinstalled/current/">https://cdimage.ubuntu.com/ubuntu-server/daily-preinstalled/current/</a></li> </ul></li> </ul> <p>リンク先から、 <code>noble-preinstalled-server-arm64+raspi.img.xz</code> を選択してダウンロードしよう。</p> <p>24.04 LTS が正式リリースされれば、 <a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/download/raspberry-pi">Install Ubuntu on a Raspberry Pi | Ubuntu</a> のページからダウンロードできるようになるはずだ。</p> <h2 id="ブータブル SD の焼き込み"><a href="#%E3%83%96%E3%83%BC%E3%82%BF%E3%83%96%E3%83%AB+SD+%E3%81%AE%E7%84%BC%E3%81%8D%E8%BE%BC%E3%81%BF">ブータブル SD の焼き込み</a></h2> <p>ダウンロードしたイメージファイルを、ブート可能な状態で microSD に書き込む。<br /> ダウンロードしたファイルをただ SD カードに保存するのではなく、専用のツールで書き込む必要があるぞ。</p> <p>方法は色々あるのだが、今回は個人的にオススメな <a target="_blank" rel="nofollow noopener" href="https://www.balena.io/etcher/">balenaEtcher</a> を使った方法で紹介しよう。</p> <ol> <li>microSD を SDカードスロットに挿す</li> <li>balenaEtcher を DL & 起動する。 <ul> <li>DLする balenaEtcher は Portable 版で良い</li> </ul></li> <li>前項でダウンロードするした <code>***.img.xz</code> を選択して、書き込みを開始する <ul> <li><code>***.img.xz</code> は、 <code>***.img</code> ファイルを <code>.xz</code> 形式で圧縮したものなのだが、手動で解凍せず直接 balenaEtcher で選択してしまって問題ない。 balenaEtcher は書き込み時にオンザフライで解凍する。</li> </ul></li> </ol> <p>なお、リリース済みの Ubuntu (や Raspberry Pi OS など) であれば、一般的に <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> というツールを使った方法がオススメされるのだが、DLが遅いからか、全体的に時間がかかるので私はあまりオススメしない。</p> <h2 id="system-boot パーティション内の cloud-init 等の設定を書き換える"><a href="#system-boot+%E3%83%91%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B7%E3%83%A7%E3%83%B3%E5%86%85%E3%81%AE+cloud-init+%E7%AD%89%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B">system-boot パーティション内の cloud-init 等の設定を書き換える</a></h2> <p>作成した micorSD を早速 RasPi に挿す <strong>…前に</strong> 、 初回セットアップ設定を書き換える。</p> <p>一度書き込んだ microSD を再び PC に挿し直すと、 <code>"system-boot"</code> という 500MB 位の FAT32 パーティションが開けるはずだ。</p> <p>ここには、 cloud-init という仕組みで初回起動時の設定を定義するファイルがいくつか含まれている。<br /> これらを書き換えていく。</p> <p>書き換え後のファイルは <a target="_blank" rel="nofollow noopener" href="https://gist.github.com/advanceboy/c9b020fe09ababd390c0e95d74dfde7e">こちらの Gist</a> に置いてあるので、結果だけ欲しい人はここから DL をば。</p> <h3 id="cloud-init の network-config"><a href="#cloud-init+%E3%81%AE+network-config">cloud-init の network-config</a></h3> <p>以下のように書き換える。<br /> (元のファイルからの差分を <code>diff</code> のフォーマットで表示している。 以降同様。)</p> <pre><code class="diff">--- network-config.org +++ network-config @@ -22,23 +22,11 @@ ethernets: eth0: dhcp4: true optional: true -# wifis: -# wlan0: -# dhcp4: true -# optional: true -# access-points: -# myhomewifi: -# password: "S3kr1t" -# myworkwifi: -# password: "correct battery horse staple" -# workssid: -# auth: -# key-management: eap -# method: peap -# identity: "[email protected]" -# password: "passw0rd" -# ca-certificate: /etc/my_ca.pem - -# regulatory-domain: GB + wifis: + wlan0: + dhcp4: true + access-points: + "SSID-of-myhomewifi": + password: "password-of-myhomewifi" </code></pre> <p>Ubuntu では Netplan を使ってネットワークが管理されるため、 Netplan の形式で記述する。</p> <p>規定値では IPv4 の有線イーサネットのみ、 DHCP 自動取得で接続されるようになっている。<br /> 必要に応じて、上述のように Wi-Fi で接続する SSID 等の情報を追記したり、逆に有線LANを使わないなら <code>ethernet:</code> プロパティごと削除してしまってもよいだろう。</p> <p>SSID やパスワードは、各自の環境のものに置き換えて書き換えてほしい。</p> <p>コメントアウトされている wifi の記述例では、 <code>wifis.wlan0.optional</code> を <code>true</code> に設定しているが、もし Wi-Fi 単独でネットワークに接続するつもりならこの設定は行わないようにしよう。<br /> さもないと、OS起動の度に2分も余計に時間かかる羽目になる。<br /> これは、 <code>systemd-networkd-wait-online.service</code> デーモンが、(optional true を指定していない) 有効なネットワークの接続をタイムアウトするまで待機してしまうためだ。</p> <p>network-config 自体のリファレンスはこちら。<br /> <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/23.4.1/reference/network-config.html">https://cloudinit.readthedocs.io/en/23.4.1/reference/network-config.html</a></p> <h3 id="cloud-init の user-data"><a href="#cloud-init+%E3%81%AE+user-data">cloud-init の user-data</a></h3> <p>こちらは書き換え箇所が多いので、何箇所かに分けて説明していく。</p> <p>まず最初はユーザー名まわり。</p> <pre><code class="diff">--- user-data.org +++ user-data @@ -17,13 +17,17 @@ # Some additional examples are provided in comments below the default # configuration. +# Override the `default_user` configuration from `/etc/cloud/cloud.cfg`. The `user` dictionary keys supported for the default_user are the same as the `users` schema. +user: + name: newusername + # On first boot, set the (default) ubuntu user's password to "ubuntu" and # expire user passwords chpasswd: expire: true users: - - name: ubuntu - password: ubuntu + - name: newusername + password: newpassword type: text ## Set the system's hostname. Please note that, unless you have a local DNS </code></pre> <p>規定では、 cloud-init パッケージの <code>/etc/cloud/cloud.cfg</code> ファイルの定義によって、 "ubuntu" というユーザーが作成される。</p> <p>しかし、 既知のユーザー名をそのまま使うのはセキュリティ的にも若干不安があるし、 作成後のユーザー名を書き換えるのは若干面倒なので、作成されるデフォルトのユーザー名自体を、他の名前 (上記例だと "newusername") に書き換えてしまおう。</p> <p>また、 <code>chpasswd</code> による、初回アクセス時のパスワード変更対象とするユーザー名も、併せて変更しておく。</p> <p>--</p> <p>次にホスト名とキーボード周りの設定。</p> <pre><code class="diff">@@ -31,14 +35,14 @@ ## setting the hostname here will not make the machine reachable by this name. ## You may also wish to install avahi-daemon (see the "packages:" key below) ## to make your machine reachable by the .local domain -#hostname: ubuntu +hostname: ubuntu-raspi5 ## Set up the keyboard layout. See localectl(1), in particular the various ## list-x11-* sub-commands, to determine the available models, layouts, ## variants, and options -#keyboard: -# model: pc105 -# layout: gb +keyboard: + model: pc105 + layout: jp # variant: # options: ctrl:nocaps </code></pre> <p>ここはまぁ、見た通りなので追加での説明はいらないかな。</p> <p>--</p> <p>お次は SSH まわり。</p> <pre><code class="diff">@@ -53,6 +57,8 @@ #ssh_import_id: #- lp:my_launchpad_username #- gh:my_github_username +ssh_authorized_keys: +- ssh-rsa AAAA<中略>== rsa-key-of-user1 ## Add users and groups to the system, and import keys with the ssh-import-id ## utility </code></pre> <p><code>ssh_authorized_keys</code> に直接指定するか、<code>ssh_import_id:</code> のところで自分の GitHub のユーザー名を指定してインポートすることにより、 デフォルトのユーザー (上記例だと "newusername") に登録する公開鍵を指定する。</p> <p>SSH に登録するパスワードログインは規定で禁止されているので、この設定をしておかないと SSH による遠隔からのアクセスができない。</p> <p>--</p> <p>タイムゾーンや起動後の処理まわり。</p> <pre><code class="diff">@@ -82,8 +88,13 @@ #- python3-gpiozero #- [python3-serial, 3.5-1] +# locale and timezone +timezone: Asia/Tokyo + ## Write arbitrary files to the file-system (including binaries!) -#write_files: +write_files: +- path: /etc/cloud/cloud-init.disabled + defer: true #- path: /etc/default/console-setup # content: | # # Consult the console-setup(5) manual page. </code></pre> <p>とりあえずタイムゾーンは日本標準時にしておこう。</p> <p>次に、 <code>write_files:</code> のほうだが、これは次回以降のブート時に cloud-init が余計に起動しないようにするための設定だ。</p> <p>cloud-init の設定の大半は、 初回ブート時にただ一度だけ実行されるのだが、 cloud-init サービス自体は基本的に毎回常に起動して、サービスとして常駐しっぱなしになっている。<br /> <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/topics/modules.html">Modules</a> で、 <code>Module frequency: per always</code> となっているモジュールの実行などは、ブートする度に毎回実行される。<br /> そうすると、場合によっては cloud-init の実行ログがログイン画面を覆ってしまうことが毎回の起動ごとに発生して鬱陶しい。<br /> このため、 <code>/etc/cloud/cloud-init.disabled</code> という <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/howto/disable_cloud_init.html#method-1-text-file">空ファイルを作成しておく</a> ことで、 次回以降の起動時に cloud-init を無効にしている。</p> <p><code>defer: true</code> が指定されている部分についてだが、これは cloud-init の初期化処理が大きく分けて Network (<code>cloud_init_modules</code>), Config (<code>cloud_config_modules</code>), Final (<code>cloud_final_modules</code>) という <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/explanation/boot.html">3ステージ に分かれて</a> いることに関係する。<br /> <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#write-files"><code>write_files:</code> モジュール</a> は Ubuntu の既定だと Network ステージに実行されてしまい、このタイミングで <code>cloud-init.disabled</code> ファイルが作成されてしまうと、 Config や Final ステージの処理が実行されなくなってしまう。<br /> このため、 <code>defer</code> オプションを true にして Final ステージで <code>cloud-init.disabled</code> ファイルが作成されるよう調整しているのだ。</p> <p>なお、どのモジュールがどのステージで実行されるかは、 <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/reference/base_config_reference.html#example"><code>/etc/cloud/cloud.cfg</code></a> で定義されている。<br /> (各ディストリビューション毎の <a target="_blank" rel="nofollow noopener" href="https://git.launchpad.net/ubuntu/+source/cloud-init/tree/config/cloud.cfg.tmpl?h=ubuntu/noble"><code>cloud.cfg</code> のテンプレートソースはこちら</a>)</p> <hr /> <p>最後に、コマンドの実行。<br /> 以下の <code>192.168.0.2</code> の部分は、 ssh で RasPi に接続するゲスト PC の IPアドレス に書き換えてくれ。</p> <pre><code class="diff">@@ -106,7 +117,9 @@ # permissions: '0644' ## Run arbitrary commands at rc.local like time -#runcmd: +runcmd: +# after wlan0 is connected, write the arp entry to target machine on background job +- [ sh, -c, 'sleep 2; ping -c 4 192.168.0.2' ] #- [ ls, -l, / ] #- [ sh, -xc, "echo $(date) ': hello world!'" ] #- [ wget, "http://ubuntu.com", -O, /run/mydir/index.html ] </code></pre> <p>このコマンドは、仮に ssh ゲストの IP アドレスが <code>192.168.0.2</code> であると仮定して、そこへ向かって Ping を打っている。<br /> なぜなら、 ゲストPC から RaspPi の IP アドレスを ARP コマンドで確実に調べられるようにするためだ。</p> <p>RasPi に繋げるディスプレイがないことを前提とすると、 SSH するために DHCP によって RasPi に割り当てられた IP アドレスをどうやって調べるのか、と言う問題に直面する。</p> <p>RasPi の MAC アドレスの先頭 24bit は、 Raspberry 財団の所持する OUI から割り当てられるはずなので、以下のいずれかになるはずだ。</p> <pre><code>28:CD:C1 2C:CF:67 B8:27:EB D8:3A:DD DC:A6:32 E4:5F:01 </code></pre> <p>通常、 RasPi がオンラインになったときに、 ARPリクエストがブロードキャストされるため、 同一ネットワーク内の PC の ARP テーブルに RasPi のレコードが追加され、 以下のような arp コマンドで対象の IP アドレスを調べることができるようになる。。。 <strong>多くの場合は。</strong></p> <pre><code class="powershell">while(1) { arp -a | ?{ $_ -match '\s(28-cd-c1|2c-cf-67|b8-27-eb|d8-3a-dd|dc-a6-32|e4-5f-01)' }; Start-Sleep -Seconds 1} </code></pre> <p>ところが、 RasPi が Wi-Fi で繋いでいる先が (単なるアクセスポイントではなく) ルーターだったりすると、 RasPi が 同じサブネット内に ARPリクエスト をブロードキャストしたのにも関わらず、 ルーターが無駄に気を利かせて ARPリクエスト を代替して投げてしまい、 同じサブネットの別の端末の ARP テーブルには、 RasPi の MAC アドレスのレコードが記録されないことがある。</p> <p>そこで、 RasPi 側から SSH のアクセス元となる PC を指定して ping を投げることで、 その PC の ARP テーブルに確実に RasPi が載るようにしている。</p> <p>--</p> <p>ここまで書き換えたら、 SD カードを取り外して RasPi に挿し、いよいよ起動する。</p> <h3 id="cloud-init 周りの補足"><a href="#cloud-init+%E5%91%A8%E3%82%8A%E3%81%AE%E8%A3%9C%E8%B6%B3">cloud-init 周りの補足</a></h3> <p>system-boot パーティション内の中身について、もう少し細かい仕組み周りを深堀りして補足したい。<br /> 興味が無ければ、読み飛ばしてしまって次の章に進んでもらって問題ない。</p> <p>system-boot パーティション直下にある <code>README</code> というファイルを開くと、このパーティションに存在するファイルの概要が簡単に書いてある。</p> <pre><code class="text:README">================== The Boot Partition ================== This file belongs to the boot partition of the Ubuntu Server for Raspberry Pi. ... </code></pre> <p>このパーティションには、Raspberry Pi に於ける BIOS の代替となる <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.com/documentation/computers/config_txt.html">config.txt</a> といった H/W 設定用のファイルや、 cloud-init の構成ファイルなどが含まれていることがわかる。</p> <p>このうち、<strong>cloud-init</strong> は Infrastructure as Code (IaC) の一種だ。<br /> その名の通り、 AWS などのクラウド上の仮想マシンの初期設定を念頭においた仕組みだが、それに留まらず、一般的な Linux インスタンスの初期設定に活用される。<br /> 設定内容は、YAML形式の設定ファイルを使用して記述し、クラウドやディストリビューションの違いをある程度吸収して初期設定を行うことができる。<br /> Ubuntu の OS のイメージ内には、事前に cloud-init のサービス(デーモン)が組み込まれており、OS の起動時に何らかの方法で指定された設定ファイルを読み取って、初回設定処理が実行される仕組みとなっている。</p> <p>Raspberry Pi 用の Ubuntu Server イメージでは、この cloud-init 用の設定ファイルが上記の <code>system-boot</code> パーティションから取得される。<br /> イメージ内のデフォルト値の定義と、<code>system-boot</code> パーティション内のユーザー定義が組み合わされ、初期設定が進められるワケだ。</p> <p>たとえば、今回 microSD に書き込んだイメージをそのまま書き換えずに Ubuntu を起動すると、ユーザー認証が以下のように構成される。</p> <ol> <li><code>ubuntu</code> というユーザーID</li> <li><code>ubuntu</code> というパスワード</li> <li>初回起動時にパスワードの変更を求める</li> <li>SSH サーバーの起動</li> </ol> <p>このうち、 (1), (4) は、OSに組み込まれた <code>/etc/cloud/cloud.cfg</code> の定義によるもの。<br /> そして、 (2), (3) は <code>system-boot</code> パーティション内の <code>user-data</code> ファイル内の以下の部分の定義によるモノだ。</p> <pre><code class="yaml:user-data">#cloud-config chpasswd: expire: true users: - name: ubuntu password: ubuntu type: text </code></pre> <p>このデフォルトの構成だと流石にセキュリティ的にアレなので、書き換えを行っている… というのが、前項の設定の真相となる。</p> <h2 id="Raspberry Pi に割り当てられた IP アドレスを調べられるようにして起動"><a href="#Raspberry+Pi+%E3%81%AB%E5%89%B2%E3%82%8A%E5%BD%93%E3%81%A6%E3%82%89%E3%82%8C%E3%81%9F+IP+%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%89%E3%82%8C%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%A6%E8%B5%B7%E5%8B%95">Raspberry Pi に割り当てられた IP アドレスを調べられるようにして起動</a></h2> <p>こんな感じでひととおりファイルを書き換えたら、いよいよ RasPi を起動する。</p> <p>…と、その前に、 ssh でつなぐ予定のゲスト端末で、管理者権限で以下のコマンドを実行し、端末内の「arp テーブル」をリセットする。</p> <pre><code class="powershell">arp -d * </code></pre> <p>その後、ようやくだが RasPi に microSD を挿して起動させよう。<br /> 初回ブート時の設定を適用しながら起動してくれる。</p> <p>ssh でつなぐ予定のゲスト端末の PowerShell で、以下のような arp コマンドのループを実行し、ゲストPC から RasPi の IPアドレスを調べよう。<br /> 初回起動され安定しただろうところ(SDカードの I/O 性能によるが 1~5分?)で、 RasPi の IP アドレスがヒットしてくるようになるはずだ。</p> <pre><code class="powershell">while(1) { arp -a | ?{ $_ -match '\s(28-cd-c1|2c-cf-67|b8-27-eb|d8-3a-dd|dc-a6-32|e4-5f-01)' }; Start-Sleep -Seconds 1} </code></pre> <p>公開鍵の登録が適切に行えれば、その IP アドレスに対し SSH できるはずだ。</p> <p>初回アクセス時は、以下のようなパスワード変更を求められる。</p> <pre><code>WARNING: Your password has expired. You must change your password now and login again! Changing password for newusername. </code></pre> <p>古いパスワード ("newpassword") と変更後のパスワードを改めて入れる。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>一般的な1列端子の microSD で最速な UHS-I SDR104 のバスインターフェースに Raspberry Pi 5 が対応したことや、最近の安価な microSD の I/O 速度もだいぶ向上したのも相まって、 RasPi 4B が登場した頃と比べると、体感できる I/O 性能が大きく向上している。<br /> おかげで、起動やパッケージのインストール、更新などが快適になった。</p> <p>だいぶ高価になってしまった価格的な面からも、 Raspberry Pi の 非Zero/Pico シリーズは「Linuxが動くマイコン」から「小型のLinuxマシン」という面がより強くなったと感じる。<br /> (それはどうなんだという気もするけど。)</p> <p>更に RasPi 5 では PCI Express (PCIe) にも対応しているため、 NVMe で繋いだ SSD からブートさせると、更に桁違いの高速化が臨める。</p> <p>近いうちに、そちらも試してみようと思う。</p> <p>…ただ、どうせ PCIe に対応するなら、microSD Express に対応してくれれば、 NVMe 用 HAT とか買い足さんで済むから良かったのにな~…と、思わんでもない。<br /> しかし、2019年にSD7.1の規格として登場してから4年も経った2024年現在その製品が売られているとこを見たことないので、まぁ無理な相談か。</p> advanceboy tag:crieit.net,2005:PublicArticle/18735 2024-02-09T02:11:50+09:00 2024-02-09T09:29:30+09:00 https://crieit.net/posts/JavaScript-TOTP JavaScript で TOTP を計算する <p>TOTP: Time-Based One-Time Password Algorithm とは、二要素認証でよく見る <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ja&gl=US">Google Authenticator</a> 等で QRコード を読み込ませたりして、一定時間毎に替わる 6桁 くらいの数字を入力するアレだ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://tex2e.github.io/rfc-translater/html/rfc6238.html">RFC 6238</a> に詳しい仕様が書かれている。<br /> コイツを TOTP をブラウザ上で生成したい。</p> <p>幸い近年の Web API には、バイナリ操作やクリプトまわりの機能が出揃っているので、わりかし簡単に実装できそうだ。<br /> …とまぁ、そんな愚生が思いつくような話など既に、偉大なる先人たちによる多くのサンプルが残されている。</p> <p>せっかく自分で実装するなら、最新の ES 仕様や Web API を活用して簡潔に、それでいて汎用的な仕様で書いてみよう。</p> <h2 id="実装"><a href="#%E5%AE%9F%E8%A3%85">実装</a></h2> <pre><code class="javascript">// @ts-check const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const base32AlphabetValuesMap = new Map([ ...Array.from(base32Alphabet, (key, index) => /** @type {[string, number]} */([key, index])), ]); /** @type { (strBase32: string) => Uint8Array } */ function decodeBase32(strBase32) { const strTrimmed = strBase32.replace(/=*$/, ""); const result = new Uint8Array(strTrimmed.length * 5 >>> 3); let dataBuffer = 0; let dataBufferBitLength = 0; let byteOffset = 0; for (const encoding of strTrimmed) { const value = base32AlphabetValuesMap.get(encoding); if (typeof value === "undefined") throw new Error("Invalid base32 string"); dataBuffer <<= 5; dataBuffer |= value; dataBufferBitLength += 5; if (dataBufferBitLength >= 8) { dataBufferBitLength -= 8; result[byteOffset++] = dataBuffer >>> dataBufferBitLength; } } if (dataBufferBitLength >= 5) throw new Error("Invalid base32 string"); if ((dataBuffer << (4 - dataBufferBitLength) & 0xf) !== 0) throw new Error("Invalid base32 string"); return result; } const hashAlgorithmNameMap = new Map([ ["SHA1", "SHA-1"], ["SHA256", "SHA-256"], ["SHA512", "SHA-512"], ]); /** @type { (secretUint8Array: Uint8Array, unixTimeMilliseconds: number, digits?: 6|7|8, period?: number, algorithm?: "SHA1"|"SHA256"|"SHA512") => Promise<string> } */ async function generateTOTP(secretUint8Array, unixTimeMilliseconds, digits = 6, period = 30, algorithm = "SHA1") { // unixTimeMilliseconds to step binary const stepsBuffer = new ArrayBuffer(8); const dv = new DataView(stepsBuffer); dv.setBigUint64(0, BigInt(Math.floor(unixTimeMilliseconds / 1000 / period))); // calc Hash const hashAlgorithmName = hashAlgorithmNameMap.get(algorithm); if (!hashAlgorithmName) throw new Error(`invalid algorithm: ${algorithm}`); const cryptKey = await crypto.subtle.importKey("raw", secretUint8Array, { name: "HMAC", hash: hashAlgorithmName }, false, ["sign"]); const hash = new Uint8Array(await crypto.subtle.sign("HMAC", cryptKey, stepsBuffer)); // Truncate const offset4Bit = hash.at(-1) & 0xf; const binary = ( (hash[offset4Bit] & 0x7f) << 24 | hash[offset4Bit + 1] << 16 | hash[offset4Bit + 2] << 8 | hash[offset4Bit + 3] ); // stringify const otp = binary % Math.pow(10, digits); return otp.toString().padStart(digits, "0"); } </code></pre> <p>実行例</p> <pre><code class="javascript">// 実行例 await generateTOTP(decodeBase32('XXXXXXXXXXXXXXXX'), Date.now(), 8, 30, "SHA1"); // 実行例2 => "94287082" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8) // 実行例2.2 => "94287082" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA1") // 実行例3 => "32247374" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA256") // 実行例4 => "69342147" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA512") </code></pre> <p>ハッシュアルゴリズムや、表示桁数、表示期間と言ったパラメータを変化させて、TOTP を出力できる。</p> <h2 id="応用例"><a href="#%E5%BF%9C%E7%94%A8%E4%BE%8B">応用例</a></h2> <p>例えば、前回の QRコード リーダーと組み合わせれば、 TOTP の管理を Web アプリ側で完結させるような実装もできる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/?p=2028">D&DしたQRコード画像をブラウザ上でデコードする | Aqua Ware つぶやきブログ</a></p> <p>ちゃんと多要素認証の一要素として所有者認証を満たせるよう、実装にはかなり気をつける必要はあるだろうが。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://tex2e.github.io/rfc-translater/html/rfc6238.html">RFC 6238</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format · google/google-authenticator Wiki</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kerupani129/items/4f3b44b2e00d32731ca4">[JavaScript] Unicode 文字列やバイナリデータを Base32 エンコードおよびデコードする #JavaScript - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kerupani129/items/4780fb1eea160c7a00bd">JavaScript で HOTP および TOTP を計算する #JavaScript - Qiita</a></li> </ul> advanceboy tag:crieit.net,2005:PublicArticle/18734 2024-02-08T01:37:05+09:00 2024-02-08T01:37:05+09:00 https://crieit.net/posts/D-D-QR D&DしたQRコード画像をブラウザ上でデコードする <p>ブラウザ上にドラッグ&ドロップ (D&D) されたQRコードの画像を、読み込んでデコードするUIを作りたい。</p> <p>Pure JavaScript で動作する、QR コードデコーダー(パーサー) <a target="_blank" rel="nofollow noopener" href="https://github.com/cozmo/jsQR">cozmo/jsQR</a> が有名なので、これを使ってみよう。</p> <p>jsQR は、 png や jpg といった画像コンテナの解凍機能も、 HTML や DOM, Web 周りに特化した機能も一切無いため、読み込ませた画像は何らかの方法で <code>Uint8ClampedArray</code> な RAW 画像に変換して渡す必要がある。</p> <p>ちょっと遠回りにはなるが、</p> <ol> <li>D&D されたファイルの <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/File_API">File API</a> を読み取る</li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/FileReader">FileReader API</a> を使って、ファイルを DataURI として読み込ませ、 img タグに表示させる</li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas">OffscreenCanvas</a> と <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D">OffscreenCanvasRenderingContext2D</a> を経由して <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/ImageData">ImageData</a> を作成する</li> <li>jsQR に処理を投げる</li> </ol> <p>といった処理になるだろうか。</p> <p>早速実装してみよう。</p> <h2 id="実装"><a href="#%E5%AE%9F%E8%A3%85">実装</a></h2> <pre><code class="html"><div id="divDrop" style="margin: 10px; padding: 10px; background-color: lightgray; border: 4px dashed gray; border-radius: 12px;"> <div>Drag and drop an image file here</div> <div>or <input id="iptFile" type="file" accept="image/*"></div> <div id="divPreviewContainer"></div> </div> <div style="margin: 10px; padding: 10px;"> <input type="text" id="iptResult" style="width: 600px"> <div id="divErrorOut" style="display: none; margin: 4px; padding: 4px; background-color: pink; border: 1px solid red; border-radius: 4px;"></div> </div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js"></script> <script type="text/javascript"> // @ts-check (() => { "use strict"; const divDrop = /** @type {HTMLDivElement} */(document.getElementById("divDrop")); const divPreviewContainer = /** @type {HTMLDivElement} */(document.getElementById("divPreviewContainer")); const divErrorOut = /** @type {HTMLDivElement} */(document.getElementById("divErrorOut")); const iptFile = /** @type {HTMLInputElement} */(document.getElementById("iptFile")); const iptResult = /** @type {HTMLInputElement} */(document.getElementById("iptResult")); /** @type { (file: File) => Promise<any> } */ async function decodeQrCode(file) { divErrorOut.style.display = "none"; try { // read ile const fileReader = new FileReader(); const fileReadAsync = new Promise((resolve, reject) => { fileReader.onload = ev => resolve(ev.target?.result); fileReader.onerror = ev => reject(ev); }); fileReader.readAsDataURL(file); /** @type {string} */ const dataUrl = await fileReadAsync; // load as image divPreviewContainer.innerHTML = ""; const imgPreview = document.createElement("img"); const imgLoadAsync = new Promise(resolve => imgPreview.onload = ev => resolve(ev)) imgPreview.setAttribute("src", dataUrl); divPreviewContainer.append(imgPreview); await imgLoadAsync; const { naturalWidth, naturalHeight } = imgPreview; // convert image to raw binary var canvas = new OffscreenCanvas(naturalWidth, naturalHeight); var ctx = canvas.getContext("2d"); if (!ctx) throw "failure to getContext"; ctx.drawImage(imgPreview, 0, 0); const imageData = ctx.getImageData(0, 0, naturalWidth, naturalHeight); // decode with jsQR const code = jsQR(imageData.data, naturalWidth, naturalHeight); if (code) { iptResult.value = code.data; } else { iptResult.value = ""; throw "decode QR error"; } } catch (err) { divErrorOut.style.display = "block"; divErrorOut.textContent = `${err}`; } } // Handling D&D divDrop.addEventListener("dragover", ev => { ev.preventDefault(); }); divDrop.addEventListener("drop", ev => { ev.preventDefault(); let imageFiles; if (ev.dataTransfer && 0 < (imageFiles = [...ev.dataTransfer.files].filter(f => f.type.startsWith("image/"))).length) { const dt = new DataTransfer(); imageFiles.forEach(f => dt.items.add(f)); iptFile.files = dt.files; decodeQrCode(imageFiles[0]); } }); iptFile.addEventListener("change", ev => iptFile.files && 0 < iptFile.files.length ? decodeQrCode(iptFile.files[0]) : undefined) })(); </script> </code></pre> <p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="GReBXOM" data-user="advanceboy" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a target="_blank" rel="nofollow noopener" href="https://codepen.io/advanceboy/pen/GReBXOM"> decode D&Ded QR images</a> by advanceboy (<a target="_blank" rel="nofollow noopener" href="https://codepen.io/advanceboy">@advanceboy</a>) on <a target="_blank" rel="nofollow noopener" href="https://codepen.io">CodePen</a>.</span> </p> <p>いったん img タグで画像を表示させるワンクッションを置いているおかげで、 png, jpg のみならず svg 等の画像もデコードできるようになっている。</p> <p>Edge や Chrome ブラウザであれば、表示されている画像をそのまま D&D してきてのデコードもできるぞ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/02/decode-drag-and-droped-qr-image-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/02/decode-drag-and-droped-qr-image-01-1024x564.png" alt="" /></a></p> <p>jsQR の処理は、 JavaScript のスレッドで行われるので、処理が重くなるとブラウザが固まってしまう。<br /> このため、理想的には Web Worker に処理を渡してしまうほうがよさそうではある。</p> <p>ただ、1000x1000ピクセル程度の画像であれば、近年のデバイスなら一瞬でデコードできるので、実際のところはメインスレッド側で処理してしまって問題ないだろう。</p> advanceboy tag:crieit.net,2005:PublicArticle/18733 2024-02-08T01:34:00+09:00 2024-02-08T01:35:45+09:00 https://crieit.net/posts/URL-65c3b0f86a9e2 ブラウザのタイトルや URL をコピーするブックマークレット【スマホ&阿部寛対応】 <p>ブラウザで表示しているページのタイトルと URL を一括でコピーしたり、 markdown などのマークアップ言語でフォーマットされたものを、クリップボードにコピーしたくなることは無いだろうか?</p> <p>意外にも、主要なブラウザには表示ページのタイトル名すら簡単にコピーする方法がない。</p> <p>いくつかのブラウザ拡張機能(アドオン)には、上述の機能を提供しているものがいくつもある。<br /> しかし、これだけのために拡張機能を入れるのもなんだかなと。<br /> 場合によっては、ポリシー等様々な理由で拡張機能を入れることを制限されている場合もあるだろうし。</p> <p>そこで、表示中ページのタイトルと URL を任意の種類にフォーマットしてクリップボードへコピーしてくれるブックマークレットを作ってみた。</p> <p>ブックマークレットとした事により、拡張機能の使えないスマートフォンの Chrome や Safari といった、とても不便なブラウザでも利用できる。</p> <h2 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h2> <p><a target="_blank" rel="nofollow noopener" href='javascript:(()=>{let e="FRAMESET"===top.document.body.tagName?top.frames[0].document:document,t=t=>e.getElementById(t),i=e.createElement("div"),l="modalBMId_key";i.innerHTML=%60<div id=${l}m style="position:fixed;width:100%;height:100%;top:0;text-align:center;background:rgba(0,0,0,.5);padding:16px 20px;z-index:999"><div style="display:inline-block;background:#fff"><div style="padding:8px"><div id=${l}b></div><div><label for=${l}i style="color:#000">copied:</label><input id=${l}i style="width:400px" readonly/></div></div></div></div>%60;let r=t(l+"m");r||(e.body.prepend(i.firstChild),Object.entries({TitleOnly:"<span>{</span><span>{</span>title<span>}</span><span>}</span>",Text:"<span>{</span><span>{</span>title<span>}</span><span>}</span>\n<span>{</span><span>{</span>url<span>}</span><span>}</span>",Markdown:String.raw%60[<span>{</span><span>{</span>title/\\/\\/(?=[\[\]])/\<span>}</span><span>}</span>](<span>{</span><span>{</span>url/\)/%2529<span>}</span><span>}</span>)%60,HTML:String.raw%60<a href="<span>{</span><span>{</span>url/&/&amp;/"/&quot;<span>}</span><span>}</span>"><span>{</span><span>{</span>title/</&lt;<span>}</span><span>}</span></a>%60,Textile:String.raw%60"<span>{</span><span>{</span>title/&/&amp;/"/&quot;/\]/&#93;<span>}</span><span>}</span>":<span>{</span><span>{</span>url/\]/%255D<span>}</span><span>}</span>%60,AsciiDoc:String.raw%60link:++<span>{</span><span>{</span>url<span>}</span><span>}</span>++[<span>{</span><span>{</span>title/\[/&#91;/\]/&#93;<span>}</span><span>}</span>]%60,Jira:String.raw%60[<span>{</span><span>{</span>title/&/&amp;/\[/&#91;/\]/&#93;/\|/&#124;<span>}</span><span>}</span>|<span>{</span><span>{</span>url/\[/%255B/\]/%255D/\|/%257C<span>}</span><span>}</span>]%60,LaTeX:String.raw%60\href<span>{</span><span>{</span>{url/\\/\backslash/(?=[&%$#_{}])/\<span>}</span><span>}</span>}<span>{</span><span>{</span>{title/\\/\backslash/(?=[&%$#_{}])/\<span>}</span><span>}</span>}%60}).map(([i,r])=>{let a=e.createElement("button");a.style.color="#000",a.textContent="copy - "+i,a.onclick=()=>{let i=r.replace(/<span>{</span><span>{</span>((title|url)(?:\/(.*?)\/(.*?))?(?:\/(.*?)\/(.*?))?(?:\/(.*?)\/(.*?))?(?:\/(.*?)\/(.*?))?)<span>}</span><span>}</span>/ig,function(t,i,l){let r="title"==l.toLowerCase()?e.title:location.href;for(let a=3;a<10;a+=2){let d=arguments[a];if(d&&"string"==typeof d)r=r.replace(RegExp(d,"g"),arguments[a+1]??"");else break}return r});navigator.clipboard.writeText(i),t(l+"i").value=i},t(l+"b").append(a)}),r=t(l+"m"),e.addEventListener("click",e=>{e.target==r&&r.remove()}))})();'>ブラウザのタイトルや URL をコピーするブックマークレット </a></p> <p>上記のリンクを(Firefox なら右クリックから直接、 Edge や Chrome なら ブックマークバーに D&D する等の方法で)ブックマークとして保存し、任意のページで呼び出すと、下記のようなダイアログが表示される。<br /> お好きなボタンをクリックすれば、クリップボードにフォーマット済みのリンクが格納されるという算段だ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-01-1024x82.png" alt="" /></a></p> <p>暗転したところをクリックすれば、ダイアログはクローズされる。</p> <p>最低限のコードにするため、 CSS は表示中のページのものを使っており、開いたページによってダイアログのデザインが変わるのは仕様となっている。</p> <p>Firefox, Chrome, Edge いずれでも動作することは確認している。<br /> Edge の場合ツールバーの「お気に入り☆」ボタンからではブックマークレットが動かないようなので、ブックマークバーから実行する必要があるようだ。(v121 現在)</p> <p>もちろん、「阿部 寛のホームページ」にも対応している。<br /> このようにフレームセットを使っている場合は、最初のフレームにダイアログを表示させる形で、対応している。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-02-1024x180.png" alt="" /></a></p> <h2 id="カスタマイズ方法"><a href="#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E6%96%B9%E6%B3%95">カスタマイズ方法</a></h2> <p>既定で markdown, textile, LaTeX などいくつかのテンプレートを用意している。</p> <p>もし追加したいものがあれば、後述のソースコードの <code>Object.entries({</code>の後ろに、 JSON キーにボタンのタイトルを、 JSON 値に変換のテンプレートを指定したものを追加し、 各自 minify して使ってもらえれば OK だ。(ライセンスは CC-0)</p> <p>ソースコード中に <code>%5B</code>, <code>%5D</code> などといった URL エンコードと解釈できてしまう部分があるため、ブックマークの編集画面の URL に直接貼り付ける場合は、 <code>%255B</code>, <code>%255D</code> などとエスケープしておこう。</p> <p>なお、テンプレートの定義方法だが、 <code><span>{</span><span>{</span><span>}</span><span>}</span></code> で括った部分に <code>title</code> (ページタイトル) もしくは <code>url</code> と、 0~8 の偶数個の <code>/</code> を記述し、置換対象を表す正規表現と、置換後の文字列を交互に記述する。</p> <p>例えば、</p> <pre><code class="js">String.raw`<span>{</span><span>{</span>url/\[/%5B/\]/%5D/\|/%7C<span>}</span><span>}</span>` </code></pre> <p>なら、以下と等価となる。</p> <pre><code class="js">url.replace(RegExp(String.raw`\[`, "g"), "%5B") .replace(RegExp(String.raw`\]`, "g"), "%5D") .replace(RegExp(String.raw`\|`, "g"), "%7C"); </code></pre> <p>雑な説明だがわかって。</p> <gist src="https://gist.github.com/advanceboy/a960ec38442165a7df93408d775b03eb.js"></gist> advanceboy tag:crieit.net,2005:PublicArticle/18667 2023-12-01T01:13:10+09:00 2023-12-01T01:13:10+09:00 https://crieit.net/posts/PowerShell-6568b49675a81 後ほどミュートを解除しておく PowerShell スクリプト <p>本記事は <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2023/shell">シェルスクリプト&PowerShell Advent Calendar 2023</a> の1日目の記事だ。<br /> 如何にハードルを下げるかを考え、しょーもない内容となることを意識している…と言い訳しておこう。</p> <h2 id="何故ミュートを解除したいのか"><a href="#%E4%BD%95%E6%95%85%E3%83%9F%E3%83%A5%E3%83%BC%E3%83%88%E3%82%92%E8%A7%A3%E9%99%A4%E3%81%97%E3%81%9F%E3%81%84%E3%81%AE%E3%81%8B">何故ミュートを解除したいのか</a></h2> <p>さて、一時的に PC をミュートにしておきたい事がある。</p> <p>コロナ禍を経て、 Cisco や Polycom といったテレビ会議システムに替わって、 Zoom などのWeb会議ソフトが普及した。<br /> しかし、近年出社を強要されるのが一般化してくると、結局以前のように会議室に集まって、離れた拠点の相手とビデオ会議を行うことになる。<br /> このとき、全員各々のPCでビデオ会議には参加するものの、音声は卓上マイク&スピーカー使う事があり、ハウリングを避ける為に卓上マイクを使う人以外はスピーカー・マイクをミュートにするだろう。</p> <p>ところが、会議終了後ミュートしっぱなしにしてしまい、通知等に気づきにくくなることがあるので、一定時間後にミュートを簡単に解除できるようにしておきたい。</p> <p>それだけのために、スクリプト経由で MMDevice API 呼ぶのも仰々しいなぁと思っていた所、よく考えたらキーボードにあるミュート状態をトグルキーを仮想的に押せばいいじゃないかと気づいた。</p> <p>ということで、早速 PowerShell でキーボードのマルチメディアキーを押すスクリプトを書いてみる。</p> <h2 id="早速コード例"><a href="#%E6%97%A9%E9%80%9F%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8B">早速コード例</a></h2> <pre><code class="powershell">#Requires -Version 5 $addTypeOptions = if ($PSVersionTable.PSVersion -lt [version]'6.0') { @{ Language = 'CSharpVersion3' }; } else { @{}; }; $typeKeyboard = Add-Type -PassThru @addTypeOptions -TypeDefinition @' using System; using System.Runtime.InteropServices; namespace D8B35E79FC964E69854B61A2C9FB3C08 { public static class Keyboard { [StructLayout(LayoutKind.Sequential)] struct MOUSEINPUT { public Int32 dx; public Int32 dy; public UInt32 mouseData; public UInt32 dwFlags; public UInt32 time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] struct KEYBDINPUT { public UInt16 wVk; public UInt16 wScan; public UInt32 dwFlags; public UInt32 time; public IntPtr dwExtraInfo; } // Define the unused MOUSE structure due to the UNION_INPUT structure size determination. [StructLayout(LayoutKind.Explicit)] struct UNION_INPUT { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; } [StructLayout(LayoutKind.Sequential)] struct INPUT { public int type; public UNION_INPUT ui; } [DllImport("user32.dll", SetLastError = true)] extern static uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); public static void TypeKey(UInt16 vk) { var inputs = new INPUT[2]; inputs[1].type = inputs[0].type = 1; // INPUT_KEYBOARD inputs[1].ui.ki.wVk = inputs[0].ui.ki.wVk = vk; inputs[0].ui.ki.dwFlags = 0x0000; // KEYEVENTF_KEYDOWN inputs[1].ui.ki.dwFlags = 0x0002; // KEYEVENTF_KEYUP var uSent = SendInput(2, inputs, Marshal.SizeOf(inputs[0])); if (uSent != 2) { throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); } } } } '@ | Where-Object Name -EQ Keyboard; $typeKeyboard::TypeKey(<# VK_VOLUME_MUTE #> 0xAD); </code></pre> <p>これを <code>Push-MuteKey.ps1</code> 等の名前で保存しておいて、</p> <pre><code>Start-Sleep -Seconds ([datetime]'11:01' - [datetime]::Now).TotalSeconds; .\Push-MuteKey.ps1; </code></pre> <p>などと指定して実行すれば、指定時間にミュートキーが押されてミュートが解除される。</p> <p>Win32 API C# 経由で叩く場合、 32bit か 64bit どちらのプロセスでも問題なく動くよう気をつける必要があったり、 PowerShell のバージョン (5 か 7以降か) でコンパイル時の挙動が異なるので、そこらへんも気をつけて実装した。<br /> うーん、 Win32 API 叩くために色々構造体を定義する必要もあって、結局仰々しいなぁ。。。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2023/shell">シェルスクリプト&PowerShell Advent Calendar 2023</a> は、執筆者募集中です!<br /> こんな割としょうも無い内容で構わないので、久々になんか書いてみませんか!?</p> advanceboy tag:crieit.net,2005:PublicArticle/18640 2023-11-03T22:58:23+09:00 2023-11-03T22:58:23+09:00 https://crieit.net/posts/Go-ZUIKI 電Go!! の ZUIKI コンは製造時期による違いがある? <p><a target="_blank" rel="nofollow noopener" href="https://www.zuiki.co.jp/mascon/">電車でGO!!専用ワンハンドルコントローラー - 瑞起 ZUIKI</a> (以下、 ZUIKIコン) には、 <a target="_blank" rel="nofollow noopener" href="https://amzn.to/49hrvNp">型番 : ZKNS-001 の通常版</a> と、 <a target="_blank" rel="nofollow noopener" href="https://amzn.to/3SlNxIV">型番:‎ZKNS-002 の EXCLUSIVE EDITION</a> の二種類ある。</p> <p>ただどうも、それ以外に製造時期による違いもあるようだ。</p> <p>その違いのせいで、 <a target="_blank" rel="nofollow noopener" href="https://www.jreast.co.jp/simulator/">JR東日本トレインシミュレータ</a> で ZUIKIコン 使うツールがな機能しなくなって困ったので、その対処をした話。</p> <h2 id="きっかけ"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">きっかけ</a></h2> <p>元々、2021年の発売当初から通常版の ZUIKIコン を購入しており、公式で対応している <a target="_blank" rel="nofollow noopener" href="https://amzn.to/3MnYg1K">電車でGo!! Switch 版</a> や ソニックパワードの 鉄道にっぽん! シリーズ <a target="_blank" rel="nofollow noopener" href="https://amzn.to/3FIY9Kc">Real Pro など</a> に加え、 <a target="_blank" rel="nofollow noopener" href="https://steamcommunity.com/app/2111630/discussions/0/3360272431836180307/">Switch版電車でGOワンハンドルマスコンで操作できるようにしてみました - Steam コミュニティー</a> で紹介されてるツールを使って <a target="_blank" rel="nofollow noopener" href="https://www.jreast.co.jp/simulator/">JR東トレシム</a> で使っていたりしていた。</p> <p>程よい重量感や操作感で臨場感が爆上がり… というか、無いと面白みが8割減レベルに気に入っており、最近 EXCLUSIVE EDITION を買い増した。</p> <p>…ところが買い増しした ZKNS-002 は、Switch 版や 鉄道にっぽん! シリーズでは問題なく動くのに、上記ツールを使って JR東トレシム しようとすると以下のようなエラーが出て動かない。<br /> ```text/plain<br /> Traceback (most recent call last):<br /> File "main.py", line 24, in <module><br /> File "SwitchDenGo.py", line 32, in loadStatus<br /> pygame.error: Invalid joystick button<br /> [33064] Failed to execute script 'main' due to unhandled exception!</p> <pre><code><br />存在しない joystick 番号を指定されてエラーになってる。 …つまりボタン配置が違う? しかしそもそも、この ZUIKIコン は汎用 USB コントローラ として認識されるモノだし、Windows OS 側のデバイスの設定を見ても、どちらも同じ "One Handle MasCon for Nintendo Switch" と認識され、全くボタン配置で認識されている。 なんでボタン配置がズレるのか、訳がわからないよ。 [![](https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-11-old-237x300.png)](https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-11-old.png)[![](https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-21-new-237x300.png)](https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-21-new.png) [上記 Steam コミュニティー](https://steamcommunity.com/app/2111630/discussions/0/3360272431836180307/) でも、同じような問題に遭遇している人が何人かいる。 しかも、 通常版 ZKNS-001 か EXCLUSIVE EDITION ZKNS-002 かを問わず発生するようだ。 つまり、型番違いによる仕様変更が原因ではない…? ## 意外なる原因 せっかく、エラーが出るデバイスと出ないデバイスが揃っているので、もう少し深く掘って調べてみる。 このツール [mipsparc/JRESim_Dengo](https://github.com/mipsparc/JRESim_Dengo/blob/v1.0/SwitchDenGo.py) のソースを取って Python のデバックをしてみたところ、やはりというか [pygame](https://www.pygame.org/news) というライブラリでコントローラーを操作するあたりでエラーがでている。 ```text/plain 031: # ○ボタン 032: if self.joy.get_button(15): # <- ここでエラー 033: self.buttons.append("SW_CIRCLE") 034: # HOMEボタン 035: if self.joy.get_button(5): 036: self.buttons.append("SW_HOME") </code></pre> <p>試しに、 <a target="_blank" rel="nofollow noopener" href="https://github.com/pygame/pygame/blob/2.1.2/examples/joystick.py">pygame のジョイスティック実行例コード joystick.py</a> を実行して、 pygame 側でどのように認識されているか確認してみる。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-12-old.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-12-old.png" alt="" /></a><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-22-new.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-22-new.png" alt="" /></a></p> <p>んん゙っ!?<br /> ZKNS-002 のほうは "One Handle MasCon for Nintendo Switch Exclusive Edition" として認識されるものの、<br /> <strong>発売日に買った方の ZKNS-001 が "Nintendo Switch Pro Controller" と認識されて、ボタンの認識軸数が大きく変わっている。</strong></p> <p><a target="_blank" rel="nofollow noopener" href="https://www.pygame.org/news">pygame</a> は、内部的に <a target="_blank" rel="nofollow noopener" href="https://github.com/libsdl-org/SDL">SDL2 (libsdl-org/SDL)</a> というライブラリでジョイスティック操作をしている。<br /> その SDL では、既知の様々なコントローラに様々な特殊対応が為されているので、そこらへんが影響していそうだ。</p> <p>こういったものは、 USB の VID (ベンダーID) と PID (プロダクトID) で場合分けしているものと相場が決まっている。<br /> ということで、デバイスマネージャーのプロパティ(ハードウェアID)から VID と PID を確認してみる。</p> <ul> <li>発売日購入の通常版 ZKNS-001: <ul> <li><code>HID\VID_0F0D&PID_00C1&REV_0106</code></li> </ul></li> <li>最近買った EXCLUSIVE EDITION ZKNS-002: <ul> <li><code>HID\VID_33DD&PID_0002&REV_0111</code></li> </ul></li> </ul> <p>VID ちゃうやんけ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.usb.org/developers">https://www.usb.org/developers</a> で USB ベンダーID を検索してみると…<br /> 0x0F0D (3853): HORI CO., LTD.<br /> 0x33DD (13277): ZUIKI Inc.</p> <p>?? HORI ??</p> <p>とりあえず、上記の VID と PID をもとに、 SDL2 (SDL 2.0.18) でどのように動作が定義されているか調べてみる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/libsdl-org/SDL/blob/release-2.0.18/src/joystick/controller_type.h#L576">SDL/src/joystick/controller_type.h at release-2.0.18 · libsdl-org/SDL</a></p> <pre><code class="c"> { MAKE_CONTROLLER_ID( 0x0f0d, 0x00c1 ), k_eControllerType_SwitchInputOnlyController, NULL }, // HORIPAD for Nintendo Switch </code></pre> <p><strong><a target="_blank" rel="nofollow noopener" href="https://amzn.to/3Qkb0rj">ホリパッド for Nintendo Switch</a> って書いてあるぞ。</strong></p> <p>更にここら辺 (<a target="_blank" rel="nofollow noopener" href="https://github.com/libsdl-org/SDL/blob/release-2.0.18/src/joystick/SDL_joystick.c#L2062-L2069">SDL/src/joystick/SDL_joystick.c</a>) の処理を経て、 VID: 0x0F0D & PID: 0x00C1 の組み合わせのものが "Nintendo Switch Pro Controller" と認識されているようだ。</p> <p>なお、 VID: 0x33DD & PID: 0x0002 については、特に SDL 側での特殊処理は定義されていなそうだった。<br /> このため、 USB H/W 側で定義されている "One Handle MasCon for Nintendo Switch Exclusive Edition" の名前がそのまま pygame での認識名として表示されていたのだろう。</p> <p>うーん、製造当初は適当に(Switch に対応した)ホリパッドを偽装してたのを、後のロットではちゃんと VID や PID とって設定したってコトかしら…?<br /> ライセンス品としてどうなんだソレ。</p> <p>とりあえず、以下の2点が今回の問題が発生していることがわかった。</p> <ol> <li><strong>製造時期?によって、ホリパッドを偽装しているものとそうでないものがある</strong></li> <li><strong>ホリパッドは SDL 側に特殊対応があり、偽装の有無でライブラリからとれるキー配置が異なっている</strong></li> </ol> <h2 id="JRESim_Dengo の修正"><a href="#JRESim_Dengo+%E3%81%AE%E4%BF%AE%E6%AD%A3">JRESim_Dengo の修正</a></h2> <p>原因がわかったところで、 <a target="_blank" rel="nofollow noopener" href="https://github.com/mipsparc/JRESim_Dengo/">JRESim_Dengo</a> の方をどちらの ZUIKIコン にも対応させたい。</p> <p>とはいえ、 VID や PID で動作し分けるにしても、私が持っている2台とは別の VID や PID を持つものもありそうだ。<br /> (製造時期の違いからか同じ不具合に遭遇していた、通常版の ZKNS-001 とか)</p> <p>SDL が動作を書き換えなければ、 USB から出ている信号は同じボタン配置で出ているっぽい為、コントローラーの軸数やボタン数で SDL 側での書き換えが行われたか判別すればよさそうだ。</p> <pre><code class="diff">--- ./SwitchDenGo_old.py 2022-10-03 18:55:53 +0900 +++ ./SwitchDenGo.py 2023-10-08 05:51:32 +0900 @@ -7,36 +7,64 @@ def __init__(self): pygame.init() self.joy = pygame.joystick.Joystick(0) + self.ctrl_nums = (self.joy.get_numaxes(), self.joy.get_numballs(), self.joy.get_numbuttons(), self.joy.get_numhats()) + if self.ctrl_nums not in [(6, 0, 16, 0), (4, 0, 14, 1)]: + raise Exception("サポートされていないコントローラです") self.joy.init() def loadStatus(self): self.brake_knotch = 0 self.accel_knotch = 0 self.buttons = [] pygame.event.get() - # Xボタン - if self.joy.get_button(2): - self.buttons.append("SW_X") - # Yボタン - if self.joy.get_button(3): - self.buttons.append("SW_Y") - # Aボタン - if self.joy.get_button(0): - self.buttons.append("SW_A") - # Bボタン - if self.joy.get_button(1): - self.buttons.append("SW_B") - # ○ボタン - if self.joy.get_button(15): - self.buttons.append("SW_CIRCLE") - # HOMEボタン - if self.joy.get_button(5): - self.buttons.append("SW_HOME") - - knotch_level = self.joy.get_axis(1) + # ロンチ版 + if self.ctrl_nums == (6, 0, 16, 0): + # Xボタン + if self.joy.get_button(2): + self.buttons.append("SW_X") + # Yボタン + if self.joy.get_button(3): + self.buttons.append("SW_Y") + # Aボタン + if self.joy.get_button(0): + self.buttons.append("SW_A") + # Bボタン + if self.joy.get_button(1): + self.buttons.append("SW_B") + # ○ボタン + if self.joy.get_button(15): + self.buttons.append("SW_CIRCLE") + # HOMEボタン + if self.joy.get_button(5): + self.buttons.append("SW_HOME") + + knotch_level = self.joy.get_axis(1) + elif self.ctrl_nums == (4, 0, 14, 1): + # Xボタン + if self.joy.get_button(3): + self.buttons.append("SW_X") + # Yボタン + if self.joy.get_button(0): + self.buttons.append("SW_Y") + # Aボタン + if self.joy.get_button(2): + self.buttons.append("SW_A") + # Bボタン + if self.joy.get_button(1): + self.buttons.append("SW_B") + # ○ボタン + if self.joy.get_button(13): + self.buttons.append("SW_CIRCLE") + # HOMEボタン + if self.joy.get_button(12): + self.buttons.append("SW_HOME") + + knotch_level = self.joy.get_axis(1) + else: + raise Exception("サポートされていないコントローラです") if knotch_level > 0.95: self.accel_knotch = 5 elif knotch_level > 0.75: </code></pre> <p>こんな感じの修正を入れてビルドしたものを以下のリリースで展開した。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/advanceboy/JRESim_Dengo/releases/tag/v1.0p1">Release JRESim_Dengo v1.0p1 · advanceboy/JRESim_Dengo</a></p> <p>もし、最近 ZUIKIコン を JREトレシム で使おうと思って困っている人がいればどうぞ。</p> <p>元の作者の方にもプルリク投げとくか。</p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <p>実装のために調査した、各環境・デバイス毎の、キー割り当ては以下の通りだった。</p> <h3 id="DirectInput からの取得 (ロンチ版, 最近の Exclusive Edition 共通):"><a href="#DirectInput+%E3%81%8B%E3%82%89%E3%81%AE%E5%8F%96%E5%BE%97+%28%E3%83%AD%E3%83%B3%E3%83%81%E7%89%88%2C+%E6%9C%80%E8%BF%91%E3%81%AE+Exclusive+Edition+%E5%85%B1%E9%80%9A%29%3A">DirectInput からの取得 (ロンチ版, 最近の Exclusive Edition 共通):</a></h3> <p>```text/plain<br /> index 1軸: マスコン<br /> Button 1: Y<br /> Button 2: B<br /> Button 3: A<br /> Button 4: X<br /> Button 5: L<br /> Button 6: R<br /> Button 7: ZL, EB<br /> Button 8: ZR<br /> Button 9: -<br /> Button 10: +<br /> Button 11: N/A<br /> Button 12: N/A<br /> Button 13: HOME<br /> Button 14: Capture<br /> PoVハット 1: 方向キー</p> <pre><code><br />### pygame 経由の、ロンチ版 ```text/plain index 1軸: マスコン index 4軸: ZL index 5軸: ZR Button 0: A Button 1: B Button 2: X Button 3: Y Button 4: - Button 5: Home Button 6: + Button 7: N/A Button 8: N/A Button 9: L Button 10: R Button 11: Up Button 12: Left Button 13: Down Button 14: Right Button 15: Capture </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-12-old.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-12-old.png" alt="" /></a></p> <pre><code class="python"># ハードウェアID: 'HID\VID_0F0D&PID_00C1&REV_0106' joy = pygame.joystick.Joystick(0) joy.get_guid() # -&gt; '030000000d0f0000c100000006016800' joy.get_name() # -&gt; 'Nintendo Switch Pro Controller' joy.get_numaxes() # -&gt; 6 joy.get_numballs() # -&gt; 0 joy.get_numbuttons() # -&gt; 16 joy.get_numhats() # -&gt; 0 </code></pre> <h3 id="pygame 経由の、 Exclusive Edition"><a href="#pygame+%E7%B5%8C%E7%94%B1%E3%81%AE%E3%80%81+Exclusive+Edition">pygame 経由の、 Exclusive Edition</a></h3> <p>```text/plain<br /> index 1軸: マスコン<br /> Button 0: Y<br /> Button 1: B<br /> Button 2: A<br /> Button 3: X<br /> Button 4: L<br /> Button 5: R<br /> Button 6: ZL<br /> Button 7: ZR<br /> Button 8: -<br /> Button 9: +<br /> Button 10: N/A<br /> Button 11: N/A<br /> Button 12: Home<br /> Button 13: Capture<br /> PoVハット 1: 方向キー</p> <pre><code>[![](https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-22-new.png)](https://aquasoftware.net/blog/wp-content/uploads/2023/11/zuiki-dengo-controller-22-new.png) ```python # ハードウェアID: 'HID\VID_33DD&PID_0002&REV_0111' joy = pygame.joystick.Joystick(0) joy.get_guid() # -&gt; '03000000dd3300000200000000000000' joy.get_name() # -&gt; 'One Handle MasCon for Nintendo Switch Exclusive Edition' joy.get_numaxes() # -&gt; 4 joy.get_numballs() # -&gt; 0 joy.get_numbuttons() # -&gt; 14 joy.get_numhats() # -&gt; 1 </code></pre> advanceboy tag:crieit.net,2005:PublicArticle/18636 2023-11-01T01:41:31+09:00 2023-11-01T01:41:31+09:00 https://crieit.net/posts/PC-Manager PC Manager をオフラインやプロキシ環境下でインストールする <p>システム最適化ソフト「Microsoft PC Manager」が、 v3.1.3 で日本語に対応したらしい。<br /> → <a target="_blank" rel="nofollow noopener" href="https://forest.watch.impress.co.jp/docs/news/1542944.html">Microsoft純正のシステム最適化アプリが日本語に対応、「PC Manager」v3.1 - 窓の杜</a></p> <p>元々中国向けに提供されていたものが、英語、それ以外の言語と順次展開が拡大されおり、今回めでたく日本語対応したようだ。<br /> …あれ? こないだまで最新ベータ版は v3.2 だったような…?<br /> まぁいいか。</p> <p>Windows 11 だと何かとメモリを食いつぶされて、 16~32GB 程度のメモリでは VM や WSL の起動に失敗することが多々あるので、 Windows 98 や Me の時代を思い出すようなメモリクリーンアップ機能は、今の時代でも重宝される。</p> <p>さて、この Microsoft PC Manager だが <a target="_blank" rel="nofollow noopener" href="https://pcmanager.microsoft.com/ja-JP">Microsoft の公式ページ</a> からインストーラをダウンロードして実行しても、オフライン環境やプロキシ環境下だと「インストール ファイルをダウンロードできませんでした。後でもう一度お試しください。」等と表示されて、インストールに失敗する。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/11/install-pc-manager-offline-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/11/install-pc-manager-offline-01.png" alt="" /></a></p> <p>インストーラ (<code>MSPCManagerSetup.exe</code>) が実行時に実プログラムをダウンロードする必要があるからだ。</p> <p>どこかにオフライン版のインストーラはないものか…<br /> と調べていたら、 FANDOM の Wiki にオフラインインストーラのリンクの作り方が書いてあった。</p> <blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://microsoft.fandom.com/wiki/Microsoft_PC_Manager#Online/offline/upgrade_packages">Microsoft PC Manager | Microsoft Wiki | Fandom</a><br /> <code>aka.ms/PCManager[ChannelNo.]</code></p> </blockquote> <p>2023年11月現在、 前述の公式サイトからオンラインインストーラをダウンロードしようとすると、 以下のリンク先 (に認証用のシグネチャのパラメータが付いたもの) へリダイレクトされてダウンロードされる。<br /> <code>https://pcmdistributestorage.blob.core.windows.net/mvp/500000/56784/MSPCManagerSetup.exe</code></p> <p>このことから、最新版系統は Channel 500000 のようだ。</p> <p>つまり、以下のリンク先からダウンロードすれば、 v3.1 系統のオフライン版インストーラが手に入るようだ。</p> <p><code>https://aka.ms/PCManagerOFL500000</code></p> <p>しっかりマイクロソフトによるデジタル署名がついたインストーラが手に入るので、ウイルスなどが仕込まれたファイルであるリスクは低そうだ。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/11/install-pc-manager-offline-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/11/install-pc-manager-offline-02.png" alt="" /></a></p> advanceboy tag:crieit.net,2005:PublicArticle/18630 2023-10-29T13:16:05+09:00 2023-11-17T00:11:51+09:00 https://crieit.net/posts/Clipchamp 個人用 Clipchamp から組織ユーザーへプロジェクトを無理やり移行する <p>この記事では、 個人アカウント用 Clipchamp から組織(法人)ユーザーの Clipchamp へプロジェクトを移行する方法、およびその応用で、個人用から別の個人用ユーザーに Clipchamp プロジェクトを移行(コピー)する方法を説明する。</p> <hr /> <p>去る7月31日に、 <a target="_blank" rel="nofollow noopener" href="https://www.microsoft.com/ja-jp/microsoft-365/clipchamp-for-work">動画編集アプリ Clipchamp</a> が組織アカウントに対応し、ビジネス向け Microsoft 365 に加わることが発表された。<br /> → <a target="_blank" rel="nofollow noopener" href="https://forest.watch.impress.co.jp/docs/news/1520514.html">動画編集アプリ「Clipchamp」、ビジネス向け「Microsoft 365」にも導入開始 - 窓の杜</a></p> <p>これにより、 Microsoft での買収以降どのプランが商用利用可能なのか曖昧だった Clipchamp が、組織アカウント上で安心して商用利用できる事になる。<br /> <a target="_blank" rel="nofollow noopener" href="https://techcommunity.microsoft.com/t5/microsoft-365-blog/new-create-videos-and-images-at-work-with-clipchamp-and-designer/ba-p/3981574">北米時間の 2023年10月15日 に Clipchamp が商用ユーザーで利用可能になったことが発表</a> され、 Microsoft 365 の対象プランのライセンスを持つ全てのユーザーに対し Clipchamp が一般公開された。<br /> → <a target="_blank" rel="nofollow noopener" href="https://forest.watch.impress.co.jp/docs/news/1547562.html">動画編集アプリ「Clipchamp」が法人「Microsoft 365」に ~上位版・単体プランも提供へ - 窓の杜</a></p> <p>さて、 Clipchamp が組織アカウントに対応したことで、旧来の個人アカウント用 Clipchamp アカウントで作成していたプロジェクトを、組織アカウントに移行したくなる場合もあるだろう。</p> <p>しかし FAQ などをみると、</p> <p><a target="_blank" rel="nofollow noopener" href="https://support.microsoft.com/ja-jp/topic/how-to-access-microsoft-clipchamp-with-your-work-account-8122e9d2-b517-4230-8398-64cdaca9bef2#bkmk_move_projects">職場アカウントで Microsoft Clipchamp にアクセスする方法 - Microsoft サポート</a></p> <blockquote> <h4 id="個人アカウント用に Clipchamp で作成されたプロジェクトを作業バージョンに移動できますか?"><a href="#%E5%80%8B%E4%BA%BA%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E7%94%A8%E3%81%AB+Clipchamp+%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%95%E3%82%8C%E3%81%9F%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%A5%AD%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%81%AB%E7%A7%BB%E5%8B%95%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%81%8B%3F">個人アカウント用に Clipchamp で作成されたプロジェクトを作業バージョンに移動できますか?</a></h4> <p>いいえ。</p> </blockquote> <p>無慈悲。</p> <p><a target="_blank" rel="nofollow noopener" href="https://clipchamp.com/ja/clipchamp-for-work/">仕事向けの Clipchamp - clipchamp.com</a></p> <blockquote> <h4 id="個人用アカウントの Clipchamp で作成したプロジェクトを職場アカウントに移行できますか?"><a href="#%E5%80%8B%E4%BA%BA%E7%94%A8%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE+Clipchamp+%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E8%81%B7%E5%A0%B4%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AB%E7%A7%BB%E8%A1%8C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%81%8B%3F">個人用アカウントの Clipchamp で作成したプロジェクトを職場アカウントに移行できますか?</a></h4> <p>現時点では、個人用アカウントで作成したプロジェクトを職場アカウントに移行することはできません。</p> </blockquote> <p>こちらは、もうちょいマイルドだ。</p> <p>将来的にやる気があるのかはわからんが、少なくとも現時点では、個人アカウントのデータの組織アカウントへの移行は、公式的にはできないらしい。</p> <p>ところで、実際に組織アカウントで Clipchamp を使ってみると、プロジェクトファイルが OneDrive for Bussiness の <code><ルート>\動画\Clipchamp\<プロジェクト名>\</code> あたりの、 <code>*.clipchamp</code> という、 JSON が実体のファイルとして保存されていることに気づく。</p> <p>個人用 Clipchamp の動作を思い出してみると、画像や音声などのアセットはローカルにキャッシュや保存されているものが参照されているだけで、別 PC でプロジェクトを開こうとすると、改めてアセットファイルを紐づける必要があった。</p> <p>このため、 個人用 Clipchamp のプロジェクトも、この JSON ファイル程度の情報しかサーバーで管理されていないのではと推測できる。</p> <p>つまり、何らかの方法でこの JSON ファイル相当の情報を個人用 Clipchamp から抜き出せれば、組織アカウントに移行できるかもしれない。</p> <p>試行錯誤してみたところ、どうやら上手くいったようなので、その方法を紹介しよう。</p> <ul> <li>対象者: <ul> <li>最低限の JSON の読み書きができる</li> <li>ブラウザの開発者ツールを多少触ったことがある</li> </ul></li> <li>対象環境: <ul> <li>Microsoft Store Clipchamp アプリ 又は Chromium 系ブラウザ <ul> <li>以降の説明は Clipchamp アプリ 又は Edge を例に説明</li> </ul></li> <li>何らかのテキストエディタ <ul> <li>vscode あたりがオススメ</li> </ul></li> </ul></li> </ul> <p>ちなみに Clipchamp は Firefox 非対応だ。<br /> Web 標準に仇なすクソがよ。</p> <h3 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h3> <p>まず、組織アカウントで一度は何らかの Clipchamp のプロジェクトを作成しておく。(但しそのプロジェクトは以降の手順では使わない)</p> <h3 id="個人アカウントからプロジェクトデータの取り出し"><a href="#%E5%80%8B%E4%BA%BA%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%8B%E3%82%89%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97">個人アカウントからプロジェクトデータの取り出し</a></h3> <p>移行元の個人アカウントを使い、 Clipchamp アプリか Edge で、 Clipchamp サービスを開く。</p> <p>ウィンドウ内の何らかのコンテンツにフォーカスをあわせた状態で、 <code>Ctrl+Shift+I</code> または <code>F12</code> キーを押し、開発者ツールを起動する。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-01.png" alt="" /></a></p> <p>Clipchamp 側ではエクスポートしたいプロジェクトを開き、開発者ツール側でネットワークタブを開く。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-02.png" alt="" /></a></p> <p><code>Ctrl+F</code> でネットワークタブの検索窓を開いて、 <strong>URL に</strong> <code>/projects/</code> が含まれているものを探す。<br /> 例えば下のスクショの場合、検索結果の 2つ目, 3つ目のレコードは <code>/projects/</code> が含まれているのが URL ではないので該当しない。<br /> 該当するものが複数ある場合は、ヘッダーの要求方法 (METHOD) が <code>GET</code> となっているモノを探す。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-03.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-03.png" alt="" /></a></p> <p>URL 一覧からアクティブになっているレコードを右クリックし、「編集して再送信する」をクリックする。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-04.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-04.png" alt="" /></a></p> <p>すると、開発者ツールウィンドウの下半分にネットワークコンソールが開くので、 <code>Send</code> ボタンをクリックする。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-05.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-05.png" alt="" /></a></p> <p>すると、更にその下に再送信結果が表示されるので、 <code>Download</code> をクリック。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-06.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-06.png" alt="" /></a></p> <p>既定のダウンロード先に <code>.json</code> ファイルがダウンロードされているはずなので、任意のテキストエディタで開く。 (vscode がオススメ)</p> <p>JSON の <code>projectState</code> プロパティの中身だけを繰り出して、 <code>.clipchamp</code> の拡張子で保存する。<br /> 具体的には、 vscode の場合 <code>Shift+Alt+F</code> で JSON を整形し、 <code>{ "projectState": {</code> の部分まで削除したのち <code>Ctrl+Shift+\</code> で次の対応する括弧に移動して、その閉じ括弧 <code>}</code> より後ろを削除するとよいだろう。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-11.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-11.png" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-12.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-12.png" alt="" /></a></p> <p>以上で、移行に用いる <code>.clipchamp</code> ファイルのエクスポートは完了だ。</p> <h3 id="組織アカウントへの取り込み"><a href="#%E7%B5%84%E7%B9%94%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%B8%E3%81%AE%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF">組織アカウントへの取り込み</a></h3> <p>OneDrive for Bussiness の <code><ルート>\動画\Clipchamp\</code> あたりにプロジェクトフォルダを作成して、そこに <code>.clipchamp</code> ファイルを移動する。</p> <p>OneDrive for Business の WebUI 上から上記ファイルを開いて、 Clipchamp のウェブエディタを起動する。</p> <p>もし、「予期しないエラーが見つかりました ... Error: Project schema is an unsupported version」 というエラーが発生した場合、<br /> 上記 <code>.clipchamp</code> ファイルの JSON の <code>$.schemaVersion</code> のところを、 <code>1.3.3</code> くらいに書き換えて、改めて開く。<br /> (組織アカウントで予め作製しておいた Clipchamp のプロジェクトの <code>.clipchamp</code> を開いてみて、それと同じ <code>schemaVersion</code> の値にしておくのが良いだろう。)</p> <p>Clipchamp の編集画面が読み込まれたら、不足しているアセットを「アセットを探す」をクリックしてたダイアログ上で探して取り込む。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-22.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-22.png" alt="" /></a></p> <p>アセットが一箇所にまとまっているなら、「フォルダーを選択」でアセットが入っているフォルダを選択すれば、そのフォルダに含まれているファイルが再帰的に検索され、プロジェクトで使われているアセットが自動的に読み込まれるはずだ。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-23.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-23.png" alt="" /></a></p> <p>読み込めないアセットが全て紐付けられたか、あるいはどうして見つからないファイルについてはタイムラインから削除すれば、「アセットを探す」の表示が消え動画のプレビューができるようになるはずだ。</p> <p>なお、 2023年10月現在、組織アカウントの Clipchamp では内蔵ストックアセットが使えないようだ。 これも <a target="_blank" rel="nofollow noopener" href="https://www.microsoft.com/ja-jp/microsoft-365/roadmap?filters=&searchterms=Clipchamp%2Ccontent">10月以降順次ロールアウトされる</a> らしいので、移行元プロジェクトでストックアセットを使っている場合、そのあとに移行した方が良さそうだ。</p> <hr /> <h3 id="キャッシュからアセットの取り出し"><a href="#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%8B%E3%82%89%E3%82%A2%E3%82%BB%E3%83%83%E3%83%88%E3%81%AE%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97">キャッシュからアセットの取り出し</a></h3> <p>更に突っ込んで、一部ファイルがローカルのどこにあるかわからなくなっている場合、キャッシュなどからリソースをファイルとして取り出せる場合もある。</p> <p>但しこの方法を使うと、移行元のプランで使える一部のストックアセットなども取り出せてしまうが、それはライセンス的に微妙なので要注意。</p> <p>読み込めていないアセットの ID を、 プロジェクトの JSON から 表示名をキーにして探す。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-31.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-31.png" alt="" /></a></p> <p>移行元の Clipchamp のプロジェクトを開発者ツールを開いた状態で開き、先ほどのプロジェクトデータの取り出しと同じ要領で、今度はネットワークタブの検索窓を開いて、 <strong>URL に</strong> <code>/local-cache/{id}</code> が含まれている要求方法 (METHOD) が <code>GET</code> となっているレコードを探し、同じくアクティブレコードを右クリックで「編集して再送信する」をクリックする。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-32.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-32.png" alt="" /></a></p> <p>開発者ツールウィンドウの下半分にネットワークコンソールが開かれるが、 <strong>まずは <code>Headers</code> を開き <code>Range</code> のチェックを外してから、</strong> <code>Send</code> ボタンをクリックする。 そして、再送信結果が表示されたら <code>Download</code> をクリック。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-33.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-33.png" alt="" /></a></p> <p>保存される拡張子は必ずしも正しく設定されないので、 動画なら <code>.mp4</code> や <code>.webm</code>, <code>.gif</code> など、 画像なら <code>.png</code>, <code>.jpg</code>, <code>.svg</code> などにファイル名を書き換えよう。</p> <p>このファイルを、移行先の「アセットを探す」で指定すれば、移行先に取り込めるはずだ。</p> <hr /> <h3 id="別の個人アカウントへプロジェクトの取り込み"><a href="#%E5%88%A5%E3%81%AE%E5%80%8B%E4%BA%BA%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%B8%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF">別の個人アカウントへプロジェクトの取り込み</a></h3> <p>組織アカウントと異なり、個人アカウントでは <code>.clipchamp</code> ファイルが OneDrive 等の見える所に存在しないので、別の方法で取り込む必要がある。<br /> Clipchamp は、かなり綺麗な URI 設計となっているようで、取り出し方法を応用すれば、個人アカウントを跨いでプロジェクトの移行もできそうだ。</p> <p>移行先のアカウントで Clipchamp を開き、適当な名前で新しいプロジェクトを作成したのち、開発者ツールを開く。 タイトル名を変えるなど何らかの変更を加え、いったん編集した場所からフォーカスを外す。</p> <p>この状態で開発者ツールのネットワークタブの検索窓を開いて、 <strong>URL に</strong> <code>/projects/</code> が含まれているもののうち、今度はヘッダーの要求方法 (METHOD) が <strong><code>PUT</code></strong> となっているモノを探す。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-51.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-51.png" alt="" /></a></p> <p>ペイロードタブを開いて、 <code>"id"</code> プロパティの値をいったんどこかにコピペしておこう。<br /> ちなみにこの値は、 <code>/projects/</code> の URL に含まれている ID と同じ値のはずだ。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-52.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-52.png" alt="" /></a></p> <p>今度は、インポート対象となるエクスポート済みの <code>.clipchamp</code> ファイルを任意のテキストエディタで開く。 そして、<strong>ルート直下の <code>"id"</code> プロパティ</strong>の値を、前述のコピペしといた <code>"id"</code> プロパティの値に書き換える。<br /> <code>"id"</code> プロパティ自体は JSON の中にいくつもあるが、書き換えるのは<strong>ルート直下のひとつだけにすることに要注意</strong>だ。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-53.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-53.png" alt="" /></a></p> <p>更に、 <code>.clipchamp</code> ファイルの中身全体を <code>{ "projectState":</code> と <code>}</code> でくくって、 <code>.json</code> ファイルとして保存する。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-54.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-54.png" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-55.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-55.png" alt="" /></a></p> <p>移行先の Clipchamp の開発者ツールに戻り、先ほどの PUT のレコードが選択されている状態で、アクティブレコードを右クリックして「編集して再送信する」をクリックする。 開発者ツールウィンドウの下半分に開いたネットワークコンソールの、今度は <code>Body</code> を開き、そこに、先ほどの書き換えた <code>.json</code> ファイルの中身をペーストして、 <code>Send</code> ボタンをクリックする。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-56.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/10/clipchamp-migrate-personal-to-business-56.png" alt="" /></a></p> <p>リクエストが成功して何らかのレスポンスが返ってきたら、いったん Clipchamp アプリ(またはブラウザタブ)をすぐに閉じる。</p> <p>その後、改めて Clipchamp アプリまたは、ブラウザの新しいタブで Clipchamp を開き、プロジェクト一覧を見ると、先ほど作成した新しいプロジェクトが移行したプロジェクトに置き換わっているはずだ。<br /> もしそのプロジェクトを開いた際にエラーが出る場合は、 <code>PUT</code> する JSON の書き換えや指定方法が誤っているので、一旦プロジェクトを削除後、もう一度最初の手順からやり直そう。</p> <hr /> <h3 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h3> <p>だいぶ無理矢理な方法だが、上記のような方法で 個人用アカウント同士や、 個人アカウントと組織アカウントの間でプロジェクトを移行できた。<br /> ただ、 Clipchamp の仕様変更で同じ方法が使えなくなくなっている可能性がある点にはご了承願いたい。</p> advanceboy tag:crieit.net,2005:PublicArticle/18588 2023-10-03T09:07:43+09:00 2023-10-03T23:51:18+09:00 https://crieit.net/posts/Pester-v3-v5-PowerShell Pester v3 & v5 互換のテストコードを書く (PowerShell) <p>PowerShell のテストフレームワークである Pester は、単体テストや自動テストなどを手軽に書けるので、スクリプトやコードの品質を保つのに役立つ。</p> <p>しかし、 Pester は v3 から v5 にかけてかなり大きな破壊的変更がある。</p> <p>この記事では、 Pester v3 と v5 の両方に互換があるテストコードの書き方について紹介したい。</p> <h2 id="何故 v3 と v5 で互換を取りたいか"><a href="#%E4%BD%95%E6%95%85+v3+%E3%81%A8+v5+%E3%81%A7%E4%BA%92%E6%8F%9B%E3%82%92%E5%8F%96%E3%82%8A%E3%81%9F%E3%81%84%E3%81%8B">何故 v3 と v5 で互換を取りたいか</a></h2> <p>Windows 10 や 11 では、デフォルトで Windows PowerShell 5.1 がインストールされており、更にそこには Pester モジュールの v3.4.0 もインストールされている。<br /> Windows 上の PowerShell モジュールディレクトリはシステム全体で共通のため、たとえ別途 PowerShell 7.3 LTS などをインストールしていたとしても、既定では Pester v3.4.0 が読み込まれるわけだ。</p> <p>一方、 Linux 等のそれ以外のシステムで PowerShell をインストールした場合は、 Pester が自動的にインストールされることはないため、 Pester モジュールを追加でインストールすることになる。<br /> このとき、普通は最新版の v5 が入るだろう。</p> <p>Windows PowerShell の Pester を v5 に更新させたり、 Linux 等のシステムでインストールする Pester モジュールを v3.4 に抑えさせたりできればよいが、まぁなかなかそうも行かない時がある。</p> <p>そんなのっぴきならん状況だと、多少苦労してでもテストコード側を Pester v3 と v5 どちらにも互換を取ってしまったほうが手っ取り早いかも…なんて状況が地球上の何処かには存在するかもしれない。</p> <h2 id="テストコードの書き方"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9">テストコードの書き方</a></h2> <p>v3 と v4 の間の破壊的変更で一番デカいのは、 <code>Should</code> 構文の変更で互換切りを行っている部分だ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/breaking-changes-in-v5">https://pester.dev/docs/migrations/breaking-changes-in-v5</a></p> <blockquote> <ul> <li>Legacy syntax <code>Should Be</code> (without <code>-</code>) is removed, see <a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/v3-to-v4">Migrating from Pester v3 to v4</a></li> </ul> </blockquote> <p>構文自体の変更自体は v4 で行われたものだが、このときは以前の構文も互換性が残されていた。<br /> ところが、 v5 で早速この互換が切られてしまっている。</p> <p>コレのせいで、ほぼすべてのテストコードが、そのままでは v3 と v5 の間で互換を取ることが不可能になっている。<br /> 何考えてんだ……</p> <p>位置指定パラメータからスイッチパラメータに変わっているため、このどちらにも渡せるようにするためには <a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_splatting">Splatting 表記</a> に頼ることにする。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/pester/Pester/wiki/Should-v3">v3 で使える <code>Should</code></a> のオペレーターそれぞれについて、 v3 以下なら位置指定パラメータで渡す方法で、 v4 以上ならスイッチパラメータで渡す方法で、それぞれ <code>$BeForCompat</code> のような形で <code>$*ForCompat</code>, <code>$Not*ForCompat</code> の形式の変数をテストコード開始時に定義する。<br /> そして、その変数を <code>Should</code> で <code>@BeForCompat</code> のように Splatting 表記にて使用する。</p> <p>また、 v4 では <code>Contain</code>/<code>ContainExactly</code> オペレーターが <a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/v3-to-v4#update-to-new-names"><code>-FileContentMatch</code>/<code>-FileContentMatchExactly</code></a> にリネームされていたり、<br /> v5 では <code>*.Tests.ps1</code> <a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/v4-to-v5#put-setup-in-beforeall">ファイル先頭や、 <code>Context</code>, <code>Describe</code> 直下にセットアップコードが記載できなくなっている</a>ので、 全て <code>BeforeAll</code> ブロック内に記述する必要がある。</p> <p>これらのマイグレーションをまとめると、 v3/v5 互換の Pester テストコードは以下のようになる。<br /> (<code>v3-to-v5-Migrations.Tests.ps1</code> というファイル名で保存されているものとする)</p> <pre><code class="powershell">#Requires -Version 5 #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="3.0" } trap { break; } $GlobalBeforeAll = { # Write here the code that needs to be executed at the start of the test $shouldParams = 'Be','BeExactly','BeGreaterThan','BeLessThan','BeLike','BeLikeExactly','BeNullOrEmpty','BeOfType','Exist','Match','MatchExactly','Throw'; if ((Get-Module Pester).Version -ge [version]'4.0') { $shouldParams | ForEach-Object { Set-Variable "${_}ForCompat" @{ $_ = $true }; Set-Variable "Not${_}ForCompat" @{ Not = $true; $_ = $true }; }; $FileContentMatchForCompat = @{ FileContentMatch = $true }; $FileContentMatchExactlyForCompat = @{ FileContentMatchExactly = $true }; $NotFileContentMatchForCompat = @{ Not = $true; FileContentMatch = $true }; $NotFileContentMatchExactlyForCompat = @{ Not = $true; FileContentMatchExactly = $true }; } else { $shouldParams | ForEach-Object { Set-Variable "${_}ForCompat" @($_); Set-Variable "Not${_}ForCompat" @('Not', $_); }; $FileContentMatchForCompat = @('Contain'); $FileContentMatchExactlyForCompat = @('ContainExactly'); $NotFileContentMatchForCompat = @('Not','Contain'); $NotFileContentMatchExactlyForCompat = @('Not','ContainExactly'); } } if ((Get-Module Pester).Version -ge [version]'5.0') { BeforeAll $GlobalBeforeAll; } else { . $GlobalBeforeAll; } Describe 'Pester v3, v5 compatibility notation test' { BeforeAll { # DON'T use $MyInvocation.MyCommand.Path $filePath = '.\v3-to-v5-Migrations.Tests.ps1'; } It "Pattern: (<A>, <B>)" -TestCases @( @{A=1; B=2}; @{A=3; B=4}; ) { param ($A, $B); ($A / $A) | Should @BeForCompat 1; ($B * $A) | Should @NotBeForCompat 0; $filePath | Should @FileContentMatchExactlyForCompat 'BeforeAll'; } } </code></pre> <pre><code class="console">PS > # Pester 3.4.0 の場合 PS > Get-Module Pester ModuleType Version PreRelease Name ---------- ------- ---------- ---- Script 3.4.0 Pester PS > Invoke-Pester Describing Pester v3, v5 compatibility notation test [+] Pattern: (1, 2) 74ms [+] Pattern: (3, 4) 5ms Tests completed in 53ms Passed: 2 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0 PS > </code></pre> <pre><code class="console">PS > # Pester 5.5.0 の場合 PS /> Get-Module Pester ModuleType Version PreRelease Name ---------- ------- ---------- ---- Script 5.5.0 Pester PS > Invoke-Pester Starting discovery in 1 files. Discovery found 2 tests in 21ms. Running tests. [+] .../v3-to-v5-Migrations.Tests.ps1 115ms (82ms|13ms) Tests completed in 117ms Tests Passed: 2, Failed: 0, Skipped: 0 NotRun: 0 PS > </code></pre> <p>うーん、面倒くさいね。</p> <h2 id="Tips"><a href="#Tips">Tips</a></h2> <p>こんなアホなことはせずに、Pester v3 なら v3 のコードで、 v5 なら v5 のコードで記述する場合、せめてテストコードの先頭で <a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_requires"><code>#Requires</code> 構文</a> を書いて、どのバージョンの Pester をターゲットにしているかしっかり明示しよう。</p> <pre><code class="powershell"># Pester v3, v4 をターゲットにする場合 #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="3.0"; MaximumVersion="4.99" } (Get-Module Pester).Version; Describe 'desc' { It 'it' { 1 | Should Be 1; }; }; </code></pre> <pre><code class="powershell"># Pester v5 以降をターゲットにする場合 #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.0" } BeforeAll { (Get-Module Pester).Version; }; Describe 'desc' { It 'it' { 1 | Should -Be 1; }; }; </code></pre> <p>本来 <code>#Requires</code> 構文には、必要なモジュールが現在のセッションにない場合自動でインポートする機能があり、例えば複数のバージョンのモジュールがサイドバイサイドでインストールされていた場合だと、条件に一致するバージョンのものがインポートされる便利な機能がある。<br /> しかし Pester の場合は、必ずしも良い感じには機能してくれるとは限らない。</p> <p>テストコードが読み込まれる前 <code>Invoke-Pester</code> した時点で Pester モジュールのインポートが走ってしまうことや、複数のバージョンのモジュールがインポートされた状態になると、いずれか一つのモジュールが条件を満たせば <code>#Requires</code> の検証をパスしてしまうのに、インポートされたもののうち最も古いバージョンのモジュールで実行されてしまうような挙動をとるためだ。</p> <p>それでも、 Pester モジュールがひとつしかインストールされていないような多くの状況では、バージョンを明示しておくことでエラー原因がわかりやすくはなるはずだ。</p> advanceboy tag:crieit.net,2005:PublicArticle/18573 2023-09-23T02:32:08+09:00 2023-09-25T00:26:04+09:00 https://crieit.net/posts/NEC-IX-VLAN-VLAN NEC IXシリーズルーターでポートVLANとタグVLANをVLANスイッチ的に併用する <p>この記事では、 GE0, GE1 の 2つの "デバイス" を持つ NEC の IX2105 ルーターを例に、1カ所のスイッチングハブ用のデバイスの中で ポートVLAN と タグVLAN を併用し、スイッチングハブのトランクポートやアクセスポートのように使う方法について説明する。</p> <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>国内の中規模オフィス向けのルーターとして、 Yamaha や Cisco がよく知られているが、 NEC も <a target="_blank" rel="nofollow noopener" href="https://jpn.nec.com/univerge/ix/Info/ix2106.html">UNIVERGE IXシリーズ</a> を提供している。</p> <p>Yamaha の RTXシリーズ と価格帯が近いものの、 NEC の IXシリーズ には、 Yamaha と比べてもわりと柔軟な構成を行える利点がある。</p> <p>一方で Yamaha と比べた場合、 NEC の IXシリーズ の設定方法を調べるのは少し…いや、だいぶ難しい。<br /> 書籍や Web 記事、公式マニュアルなど、ドキュメントや設定例の全てに於いて、 Yamaha と比べて著しく情報が少ないためだ。</p> <p>そんな IXシリーズ を使って、ポートVLAN と タグVLAN を併用する方法について紹介しよう。<br /> (この併用も、Yamaha の RTX で同一ポート内で実現するのは難しい機能のひとつだ)</p> <p>なお、同じ UNIVERGE でも LTE や Wi-Fi などのワイヤレス機能を持った WAシリーズ では、 VLAN 周りのコマンド体系が異なるため、このページの方法は使えない。</p> <p>設定例として挙げている IX2105 は <a target="_blank" rel="nofollow noopener" href="https://www.necplatforms.co.jp/product/ix/product/ix2105.html">2019年9月30日をもって販売を終了</a> しているが、マイナーバージョンこそ <a target="_blank" rel="nofollow noopener" href="https://www.nw-meister.jp/service/resources/common/products/ix/relnotes/ix2105-release-10.2.42.txt">Ver.10.2 までに留まるものの不具合修正は続いて</a>いる。<br /> (例えば後継の IX2106 だと 10.2系 は <a target="_blank" rel="nofollow noopener" href="https://www.nw-meister.jp/service/resources/common/products/ix/relnotes/ix2106-release-10.2.26.txt">Ver.10.2.26</a> までで終了し、最新版は <a target="_blank" rel="nofollow noopener" href="https://www.nw-meister.jp/service/resources/common/products/ix/relnotes/ix2106-release-10.8.21.txt">Ver.10.8.21</a> となっている)<br /> 中古なら<a target="_blank" rel="nofollow noopener" href="https://auctions.yahoo.co.jp/search/search?ei=utf-8&p=IX2105">ヤフオク等で二束三文で手に入る</a>し、最新 FW (ソフトウェア) も NEC に "UNIVERGE IXシリーズ ソフトウェアダウンロードサイトへの接続申請書" を出せば手に入るので、練習機としてはもってこいだ。</p> <h2 id="ゴール"><a href="#%E3%82%B4%E3%83%BC%E3%83%AB">ゴール</a></h2> <p>以下のような構成を行う。</p> <ul> <li>既定ではスイッチングハブとなっている、 <code>GE1</code> デバイスの4つの物理ポートを、以下の3つに分割する <ul> <li>タグ付き通信を行う(即ちトランクポートとなる)1番ポート <ul> <li>但し、管理VLAN 的に使うため、 untag のフレームも受け付ける</li> </ul></li> <li>VLAN100 の通信を untag で行う(即ちアクセスポートとなる)2番ポート</li> <li>VLAN200 の通信を untag で行う(即ちアクセスポートとなる)3, 4番ポート</li> </ul></li> <li>各 VLAN それぞれで DHCP サーバによる異なるセグメントの IP アドレスの割り当てを行う</li> <li><code>GE0</code> デバイスでは NAT (NAPT) を行い、 <code>GE1</code> デバイスの各セグメントからのルーティングも受け付ける</li> </ul> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-00.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-00-1024x576.png" alt="" /></a></p> <p>先に最終的な構成を載せておくと、以下のようになる。</p> <pre><code class="text">ip ufs-cache enable ip dhcp enable ip access-list web-http-acl permit ip src any dest 192.168.99.1/32 ! ! ! ! ! ブリッジの有効化 bridge irb enable ! ! ! ! GE0 から DNS サーバーを自動取得し DNSプロキシー を実行 proxy-dns ip enable proxy-dns interface GigaEthernet0.0 priority 254 ! Webコンソールを管理ポートの 192.168.99.1 として開始 http-server ip access-list web-http-acl http-server ip enable ! ! ! ! 各サブネットごとに、自身をDNSサーバーと通知するDHCPプロファイルを作成 ip dhcp profile dhcp-profile-99 dns-server 192.168.99.1 ! ip dhcp profile dhcp-profile-120 dns-server 192.168.120.1 ! ip dhcp profile dhcp-profile-130 dns-server 192.168.130.1 ! device GigaEthernet0 ! GE1 ポートに ポートベース VLAN グループを設定 device GigaEthernet1 vlan-group 2 port 2 vlan-group 3 port 3 4 ! GE0 は WAN 側として、 IPアドレスを DHCP から自動取得し、 NAPT を有効化 interface GigaEthernet0.0 description WAN1 ip address dhcp receive-default ip napt enable ip napt hairpinning no shutdown ! GE0 の グループ N/A (ポート1番) のアドレスと DHCP の設定 interface GigaEthernet1.0 description LAN1 ip address 192.168.99.1/24 ip dhcp binding dhcp-profile-99 no shutdown ! GE0 の グループ N/A (ポート1番) の VLANID 222 の VLAN タグ付きの設定 (BVI20とブリッジ) interface GigaEthernet1.2 description VLAN222 encapsulation dot1q 222 tpid 8100 auto-connect no ip address bridge-group 200 no shutdown ! GE0 の グループ N/A (ポート1番) の VLANID 333 の VLAN タグ付きの設定 (BVI30とブリッジ) interface GigaEthernet1.3 description VLAN333 encapsulation dot1q 333 tpid 8100 auto-connect no ip address bridge-group 300 no shutdown ! GE0 の グループ 2 (ポート2番) の untag の設定 (BVI20とブリッジ) interface GigaEthernet1:2.0 no ip address bridge-group 200 no shutdown ! GE0 の グループ 3 (ポート3番&4番) の untag の設定 (BVI30とブリッジ) interface GigaEthernet1:3.0 no ip address bridge-group 300 no shutdown ! VLANID 222 向けのブリッジインタフェースのアドレスと DHCP の設定 interface BVI20 ip address 192.168.120.1/24 ip dhcp binding dhcp-profile-120 bridge-group 200 no shutdown ! VLANID 333 向けのブリッジインタフェースのアドレスと DHCP の設定 interface BVI30 ip address 192.168.130.1/24 ip dhcp binding dhcp-profile-130 bridge-group 300 no shutdown ! interface Loopback0.0 no ip address ! interface Null0.0 no ip address ! </code></pre> <p>メモ:</p> <p>なお、このページのコンフィグ例では、 IX2105 や IX2106, IX2107 のように GigaEthernet1 デバイスがスイッチングハブであることを前提としている。<br /> IX2215 や IX2235 のように GigaEthernet2 デバイスなどがスイッチングハブとなっている場合、適宜読み替えて設定して欲しい。</p> <h2 id="前提知識"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98">前提知識</a></h2> <p>ひとつひとつ、細かく解説していこう。</p> <p>まず前提として理解しておかなくてはならないのは、 IXシリーズの設定の考え方に於いて <strong>タグVLAN と ポートVLAN は全く異なる機能</strong> であり、独立した設定となっている、という点だ。</p> <h3 id="タグVLAN"><a href="#%E3%82%BF%E3%82%B0VLAN">タグVLAN</a></h3> <p><strong>タグVLAN</strong> は、</p> <ol> <li>ある GigaEthernet1.0 といった <strong>"基本インタフェース"</strong> に対して、 GigaEthernet1<strong>.2</strong> のような <strong>"サブインタフェース"</strong> を作成し… <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li> <li><strong>各サブインタフェースに VLAN ID を割り当て</strong>ると…</li> <li><strong>基本インタフェース側</strong>では IEEE 802.1Q <strong>VLANタギング でカプセル化</strong>されたフレームが流れる</li> </ol> <p>という仕組みだ。</p> <p>すなわち、基本インタフェース GigaEthernet1<strong>.0</strong> 内にて、各サブインタフェース GigaEthernet1<strong>.1</strong>, GigaEthernet1<strong>.2</strong> が、 VLANタギング によってトンネリングされていると見做せる。</p> <p>基本インタフェースも各サブインタフェースも、それぞれに IP アドレスを割り当てれば、各々がルーティングの対象となる。</p> <p>メモ:</p> <p>IXルーター上でトンネリングを行う仕組みとしては、似たようなもので他にも Tunnelインタフェース が存在する。</p> <p>Tunnelインタフェース では、カプセル化後が (L3 の) IP パケットとなるようなモノを対象とするため、カプセル化後のパケットはルーティングされる。</p> <p>一方、サブインタフェースでは、 VLANタギング や PPPoE といった、カプセル化後が (L2 の) Ethernet フレームとなるようなモノを対象としているため、カプセル化後のフレームは親となる基本インタフェース(と後述するブリッジ先)にしか流れない、といった仕組みの違いがある。</p> <h3 id="ポートVLAN"><a href="#%E3%83%9D%E3%83%BC%E3%83%88VLAN">ポートVLAN</a></h3> <p>一方で <strong>ポートVLAN</strong> は、</p> <ol> <li>スイッチハブ機能を持つデバイス (IX2105 なら GigaEthernet1) を</li> <li>複数のサブネット (グローバルキャストドメイン) に分割&グループ化し</li> <li>GigaEthernet1<strong>:2</strong>.0, GigaEthernet1<strong>:3</strong>.0 のような「ポートベース VLAN グループ番号」が異なる独立したインタフェースを作成する <sup id="fnref2:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li> </ol> <p>という仕組みだ。</p> <p>すなわち、 グループなし GigaEthernet1.0 や、ポートベース VLAN でグループ化された GigaEthernet1<strong>:2</strong>.0, GigaEthernet1<strong>:3</strong>.0 などは、それぞれ L2 (レイヤー2) 的に互いに独立していると見做せる。</p> <p>もちろん、各ポートVLANグループにそれぞれ IP アドレスを割り当てれば、各々がルーティングの対象となる。</p> <h3 id="ブリッジ"><a href="#%E3%83%96%E3%83%AA%E3%83%83%E3%82%B8">ブリッジ</a></h3> <p>独立した タグVLAN と ポートVLAN を紐づけるためには、複数のインタフェースをひとつのサブネット(グローバルキャストドメイン)にまとめる <strong>ブリッジ機能</strong> を活用する。</p> <p>インタフェースにブリッジ機能を割り当てると、そのインタフェースでは L3 のネットワーク層の情報が機能しなくなるため、IPアドレスや DHCP の設定など L3 に関する機能が使用できなくなる。 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p> <p>このため、ブリッジしたインタフェース上でルーティングや IPアドレスの設定などを行う場合、 <strong>BVIインタフェース</strong> という仮想インタフェースを作成してブリッジに加え、これに IPアドレス などの L3 に関する機能を割り当てる必要がある。</p> <blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-01.png" alt="" /></a></p> </blockquote> <h2 id="具体的なコンフィグの解説"><a href="#%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AA%E3%82%B3%E3%83%B3%E3%83%95%E3%82%A3%E3%82%B0%E3%81%AE%E8%A7%A3%E8%AA%AC">具体的なコンフィグの解説</a></h2> <p>まず、 GE0 を NAPT する WAN側、 GE1 を DHCP と Webコンソールを有効にした LAN側 と見立てた、以下のようなコンフィグを入れてある状態とする。</p> <pre><code class="text">ip ufs-cache enable ip dhcp enable ip access-list web-http-acl permit ip src any dest 192.168.99.1/32 ! proxy-dns ip enable proxy-dns interface GigaEthernet0.0 priority 254 ! http-server ip access-list web-http-acl http-server ip enable ! ip dhcp profile dhcp-profile-99 dns-server 192.168.99.1 ! interface GigaEthernet0.0 description WAN1 ip address dhcp receive-default ip napt enable ip napt hairpinning no shutdown ! interface GigaEthernet1.0 description LAN1 ip address 192.168.99.1/24 ip dhcp binding dhcp-profile-99 no shutdown ! </code></pre> <h3 id="タグVLANの構成"><a href="#%E3%82%BF%E3%82%B0VLAN%E3%81%AE%E6%A7%8B%E6%88%90">タグVLANの構成</a></h3> <p>タグVLAN のトランクポートを扱うには、前述の「サブインタフェース」を作成する必要がある。</p> <p>以下のように、 GigaEthernet1.0 に対応する各サブインタフェースにも IP アドレスや DHCP の設定を行うと、 GE1 の各ポートでは</p> <ul> <li>タグなし: 192.168.99.1/24 の基本インタフェース</li> <li>VLAN ID 222: 192.168.120.1/24 のサブインタフェース番号2</li> <li>VLAN ID 333: 192.168.130.1/24 のサブインタフェース番号3</li> </ul> <p>に割当たるようになる。</p> <p><code>encapsulation dot1q</code> コマンドを実行した場合は、その反映には端末の再起動が必要な点には注意。</p> <pre><code class="text">ip dhcp profile dhcp-profile-120 dns-server 192.168.120.1 ! ip dhcp profile dhcp-profile-130 dns-server 192.168.130.1 ! interface GigaEthernet1.2 description VLAN222 encapsulation dot1q 222 tpid 8100 auto-connect ip address 192.168.120.1/24 ip dhcp binding dhcp-profile-120 no shutdown ! interface GigaEthernet1.3 description VLAN333 encapsulation dot1q 333 tpid 8100 auto-connect ip address 192.168.130.1/24 ip dhcp binding dhcp-profile-130 no shutdown </code></pre> <p><code>encapsulation dot1q</code> コマンドで tpid の値を省略してもデフォルト値 0x8100 が設定されるのだが、省略したか否かにかかわらず <code>show running-config</code> や <code>show startup-config</code> のダンプ結果には表示されるため、最初からコマンドに含めている。</p> <p>上記の例では、説明のために敢えて サブインタフェース番号 2 に VLAN ID 222、等と異なる番号を割り当てているが、実際に設定するときはある程度揃えておいた方が分かりやすいだろう。<br /> サブインタフェース番号の上限は 32 等と少なめ(機種による)なので、 VLAN ID と完全に一致させるのはなかなか難しいだろうが。</p> <h2 id="ポートVLANとブリッジの追加構成"><a href="#%E3%83%9D%E3%83%BC%E3%83%88VLAN%E3%81%A8%E3%83%96%E3%83%AA%E3%83%83%E3%82%B8%E3%81%AE%E8%BF%BD%E5%8A%A0%E6%A7%8B%E6%88%90">ポートVLANとブリッジの追加構成</a></h2> <p>前項の タグVLAN の構成だけでは、 GE1 のどの番号のポートもトランクポートへのアクセスとなるため、 untagged なフレームを VLAN 用のサブネットにスイッチングできない。</p> <p>GE1 の 2番~4番ポートをアクセスポートのように扱うために、これらのポートにポートVLANのグループ設定を構成し、トランクポートとネットワークを分割する以下のコードを実行する。</p> <pre><code class="text">device GigaEthernet1 vlan-group 2 port 2 vlan-group 3 port 3 4 </code></pre> <p>"ポートベース VLAN グループ番号" 2 に、 GE1 の 2番ポートを、 グループ番号 3 に、 GE1 の 2番 & 4番 ポートを、それぞれ割り当てている。</p> <p>なお、グループ番号は 1 からスイッチングハブスロット上のポート数以下の整数値 (IX2105 なら 4) を設定できる。</p> <p>GE1 の 1番ポートをグループ化の対象としないことにより、1番ポートには GigaEthernet0.0 の タグVLAN トランクポートのフレームが引き続き流れ続ける。</p> <p>一方で、単にポートVLANグループに分割しただけでは、2番~4番ポートに untagged のフレームはスイッチングされてこない。</p> <p>そこで、 untagged なフレームが流れている タグVLAN の各サブインタフェースと、 <strong>ブリッジ</strong> させてしまおう。</p> <pre><code class="text">configure bridge irb enable ! ! interface GigaEthernet1.2 description VLAN222 encapsulation dot1q 222 tpid 8100 no ip address no ip dhcp binding dhcp-profile-120 bridge-group 200 no shutdown ! interface GigaEthernet1:2.0 no ip address bridge-group 200 no shutdown ! interface BVI20 ip address 192.168.120.1/24 ip dhcp binding dhcp-profile-120 bridge-group 200 no shutdown ! ! interface GigaEthernet1.3 description VLAN333 encapsulation dot1q 333 tpid 8100 no ip address no ip dhcp binding dhcp-profile-130 bridge-group 300 no shutdown ! interface GigaEthernet1:3.0 no ip address bridge-group 300 no shutdown ! interface BVI30 ip address 192.168.130.1/24 ip dhcp binding dhcp-profile-130 bridge-group 300 no shutdown ! </code></pre> <p>まず、ブリッジ機能を有効にするために <code>bridge irb enable</code> コマンドの実行が必要だ。<br /> (このコマンドはグローバルコンフィグモードで実行する必要があるため、手前で <code>configure</code> コマンドを実行している)</p> <p>ここで タグVLAN の サブインタフェース と、 グループ化したポートを ブリッジするのだが、 前述の通りブリッジ機能を使うと サブインタフェースに設定していた L3 の IP 関連の構成が機能しなくなる。<br /> このため、 VLAN 毎に BVI インタフェースを作成してブリッジに加えたうえで、そちらに <code>ip address</code> や <code>ip dhcp binding</code> の構成を移植する。</p> <p>なお、タグVLAN で使用している "サブインタフェース番号" GigaEthernet1<strong>.2</strong> と、ポートベース VLAN で使用している "グループ番号" GigaEthernet1<strong>:2</strong>.0 (、そして BVI インタフェース番号 BVI<strong>20</strong> や bridge-group <strong>200</strong>、 VLANID VLAN<strong>222</strong>) をある程度数字揃えてブリッジしているが、必ずしもこれら番号は揃えておく必要は無い。<br /> 見やすさやわかりやすさを重視しつつ、指定できる範囲内で設定するのが良いだろう。</p> <h2 id="動作確認"><a href="#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">動作確認</a></h2> <p>ここまでの設定が完了すると、4つのサブネット間でルーティングされ、GE1 デバイスの各ポートでは以下のような組み合わせで VLAN のイーサネットフレームが流れるようになる。</p> <p><a href="https://crieit.now.sh/upload_images/65977e5c680bdc1e3b81a85057afc4fb650f11784d39a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/65977e5c680bdc1e3b81a85057afc4fb650f11784d39a.png?mw=700" alt="image.png" /></a></p> <p>試しに、GE1 の 2番ポート に繋いだ PC (192.168.120.2) から、 GE1 の 1番ポート に繋いだ PC (192.168.99.2) に、 ping を打ってみた所を、 GE1 の 1番ポート に繋いだ PC 側でパケットキャプチャしてみる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-11.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-11.png" alt="" /></a></p> <p>192.168.120.1/24 のサブネットで ARP のブロードキャストされたフレームが、送信元 PC に繋いだ NIC の MACアドレス (Buffalo_f0:1X:XX) より <strong>VLANID 222 のタグ付き</strong>で 1番ポート へ届いていることがわかる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-12.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/09/use-port-and-tag-vlans-together-on-nec-ix-12.png" alt="" /></a></p> <p>一方、 ICMP の通信はルーティングされた上で 192.168.99.2/24 のサブネットへ届いているため、 IXルーター の MACアドレス (NECPlatf_4f:XX:XX) より<strong>タグなし</strong>で 1番ポート へ届いていることがわかる。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>上記の設定では、VLANグループ間でスイッチングはされないが、サブネット同士で自由にルーティングされる。このため、L3スイッチで VLAN を分割しただけの場合とは異なり、 VLAN 間でも IPレイヤー の通信は行われる。実運用で VLAN 間の通信を制限したい場合は、適宜フィルタなどを追加して使う事になるだろう。<br /> また、パスワードが設定されていないので、実運用では設定しておこう。</p> <p>以上のように、VLAN スイッチなどでは単純にできるような設定でも、ルーターで実現するにはまぁまぁ大変であることがわかると思う。</p> <p>とはいえ、ブリッジなどを駆使すれば、このような複雑な構成であってもルーターだけで構成できるのが IXルーター の強みとも言える。</p> <p>上記の例では使用しなかったが、 GigaEthernet1<strong>:2****.3</strong> のように、"ポートベース VLAN グループ番号" に "サブインタフェース番号" をつけることもできるため、ポートベース VLAN グループに割り当てたポートに VLAN タグがついたフレームを流すことも可能だ。 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.labohyt.net/blog/lan/post-7008">NEC IX2207 の VLAN 設定備忘録 | hyt adversaria</a></p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://www.support.nec.co.jp/View.aspx?id=3170102588">IX2000シリーズ取扱説明書</a> の 【3 基本操作と各種説明 → モードについて → インタフェース表示の意味】 あたりを参照 <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a> <a href="#fnref2:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://www.support.nec.co.jp/View.aspx?id=3170102598">IX2000/IX3000 シリーズ 機能説明書</a> の【■2.9 ブリッジの設定】より <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:3" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://www.support.nec.co.jp/View.aspx?id=3170102600">UNIVERGE IXシリーズ 設定事例集</a> の 【19.2 ポートベース VLAN とタグ VLAN の併用】 あたりを参照 <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> advanceboy tag:crieit.net,2005:PublicArticle/18551 2023-08-19T01:00:36+09:00 2023-08-29T08:59:27+09:00 https://crieit.net/posts/Keepass2Android-Offline Keepass2Android は Offline 版がオススメ <p>パスワード管理ソフトの中でも非常に有名なものの一つが KeePass シリーズだ。</p> <p>複数の端末での同期や、多要素認証などのセキュリティ向上設定には少し知識が必要なものの、 LastPass や 1Password などのクラウド型のソリューションとは異なり、有料プランの課金やサービス提供企業側のヘマによるセキュリティ侵害などのリスクが低いのが特徴だ。</p> <p>Andoroid 向けの古参 KeePass 互換のパスワード管理ソフトとして、 <a target="_blank" rel="nofollow noopener" href="https://github.com/PhilippC/keepass2android">Keepass2Android</a> が存在する。</p> <p>この Keepass2Android には、同じ作者によって提供される以下の2つのバージョンがある。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=keepass2android.keepass2android">Keepass2Android Password Safe</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=keepass2android.keepass2android_nonet">Keepass2Android Offline</a></li> </ul> <p>一見すると、インストール数の多い Keepass2Android Password Safe (以下、 非オフライン版) の方が良さそうに見えるかもしれないが、実際には <strong>Keepass2Android Offline をオススメ</strong>する。</p> <h2 id="Offline版 と 非オフライン版 の違い"><a href="#Offline%E7%89%88+%E3%81%A8+%E9%9D%9E%E3%82%AA%E3%83%95%E3%83%A9%E3%82%A4%E3%83%B3%E7%89%88+%E3%81%AE%E9%81%95%E3%81%84">Offline版 と 非オフライン版 の違い</a></h2> <p>そもそも、 Offline版 と 非オフライン版 の違いは、アプリが<strong>直接</strong>インターネットにアクセスしてデータベースファイルを取得する機能の有無だ。</p> <p>しかし、 Offline版 ではデータベースファイルのクラウド同期ができない…かというと<strong>そんなことはない</strong>。<br /> Android には、 "Storage Access Framework" (SAF) という機能が備わっており、アプリがクラウドストレージと直接通信する必要なく、システムのファイルピッカーを介して、 Google Drive や OneDrive のアプリを開き、クラウドストレージとの同期ができる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-01.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-01-135x300.jpg" alt="" /></a> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-02.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-02-135x300.jpg" alt="" /></a></p> <p>このため、 FTP や WebDAV などの一部の特殊な場所を除いて、ほとんどのクラウドストレージを Offline版 でも問題なく利用できる。<br /> 開くだけでなく、データベースを編集して保存すれば、もちろんクラウド側にも変更が反映される。</p> <p>すなわち、ほとんどの場合は Offline版 で十分なのだ。</p> <p>非オフライン が、アプリ内部から直接 Google ドライブや OneDrive, Dropbox 等を開けるのは、 SAF が一般的ではなかった過去の名残という面が大きい。</p> <h2 id="なぜ Offline版 が良いのか"><a href="#%E3%81%AA%E3%81%9C+Offline%E7%89%88+%E3%81%8C%E8%89%AF%E3%81%84%E3%81%AE%E3%81%8B">なぜ Offline版 が良いのか</a></h2> <p>上記のような前提を踏まえ、そのなかで何故 Offline版 が良いのかというと、アプリが <strong>「ネットワークへのフルアクセス」の権限を持っていない</strong> からだ。</p> <p>パスワード管理ソフトのリスクのひとつに、開発者の PC がクラッキングされて、ソフト側にキーロガーや他の脆弱性が仕込まれる可能性がある。<br /> (以前、 <a target="_blank" rel="nofollow noopener" href="https://gigazine.net/news/20230301-lastpass-incident/">LastPass でやられた</a>のがまさにそれ)</p> <p>しかし、万が一にもそのような脆弱性を仕込まれても、そもそもネットワークアクセス権限がなければ、奪われた情報をどこかに送信されることもない。<br /> すなわち、より安全になる…というワケだ。</p> <h2 id="そのほか"><a href="#%E3%81%9D%E3%81%AE%E3%81%BB%E3%81%8B">そのほか</a></h2> <p>Keepass2Android 以外の Android 向け KeePass 互換ソフトで最近人気が出てきているのが <a target="_blank" rel="nofollow noopener" href="https://www.keepassdx.com/">KeePassDX</a> だ。<br /> 元々は <a target="_blank" rel="nofollow noopener" href="https://www.keepassdroid.com/">KeePassDroid</a> のフォークだったものだが、 UI はだいぶモダナイズされている。<br /> 権限回りもだいぶ尖っていて、「バイブレーションの制御」しか存在しない。</p> <p>個人的には、ロック解除後の UI は KeePassDX の方が好みだが、生体認証を用いたクイック解除まわりの挙動は Keepass2Android の方が使いやすいので、もっぱら後者を使っている。</p> <p>利用者の多さだけでなく、 GitHub 上の OSS 開発者コミュニティの活発さからもアプリの安全性が推察できるので、ぜひ利便性と安全性のバランスが取れた、お好みの KeePass クライアントを探してみると良いだろう。</p> advanceboy tag:crieit.net,2005:PublicArticle/18505 2023-07-06T01:13:51+09:00 2023-07-06T01:13:51+09:00 https://crieit.net/posts/Ubuntu-Main Ubuntu で Main リポジトリ以外のインストール済みパッケージを一覧にする <p>以下のコマンドを使用すると、 Ubuntu にインストールされているパッケージのうち、Main リポジトリ以外(Restricted, Universe, Multiverse)に属するものを一覧で表示できる。</p> <pre><code>dpkg-query --show --showformat='${binary:Package}\n' | xargs -IX bash -c "apt-cache policy X | sed -n --regexp-extended '1h;/.*(\/restricted|\/universe|\/multiverse) .*/{x;p;x;p;Q}'" </code></pre> <p>以上!</p> <p>…とすると、この記事だけでは少し物足りない気がするので、使い方についても少し説明しておく。</p> <h2 id="なぜ Main リポジトリ以外のものを把握したいのか?"><a href="#%E3%81%AA%E3%81%9C+Main+%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E4%BB%A5%E5%A4%96%E3%81%AE%E3%82%82%E3%81%AE%E3%82%92%E6%8A%8A%E6%8F%A1%E3%81%97%E3%81%9F%E3%81%84%E3%81%AE%E3%81%8B%EF%BC%9F">なぜ Main リポジトリ以外のものを把握したいのか?</a></h2> <p>パッケージの取得元リポジトリのカテゴリを把握する理由は、Canonical による Ubuntu Pro のサポート範囲が関係しているからだ。</p> <p><code>apt</code> コマンドを使用して、Ubuntu の標準リポジトリから取得される各パッケージは、以下の4つのカテゴリに分類される。 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p> <ul> <li>Main <ul> <li>Ubuntu チームによってサポートされる、フリーソフトウェアを収容するコンポーネントで、 2,300 以上のパッケージを含む。</li> <li>Ubuntu インストール時にデフォルトでインストールされるパッケージの大半はこれ。</li> </ul></li> <li>Restricted <ul> <li>ドライバーなど、利便性の面でデフォルトでインストールされたほうが望ましいものの、プロプライエタリであるソフトウェアを含むコンポーネント。</li> </ul></li> <li>Universe <ul> <li>Ubuntu コミュニティによって管理されている、 Linux 界の多くのオープンソースソフトウェアを収容するコンポーネントで、 23,000 以上のパッケージを含む。</li> </ul></li> <li>Multiverse <ul> <li>フリーソフトの要件を満たさないような、特殊なライセンス要件をもつソフトウェアを収容するコンポーネント。</li> <li>GPU 関連 (CUDA 等) や, VirtualBox などが含まれている。</li> </ul></li> </ul> <p>これらのうち、Restricted と Multiverse リポジトリのパッケージについては、それぞれのソフトの提供元によってのみメンテナンスされ、Canonical によるサポートは行われない。</p> <p>一方で、Main と Universe リポジトリのパッケージについては、セキュリティ修正の提供期間や、電話/オンラインチケットサポート範囲が、 Ubuntu Pro の有無やライセンスの種類ごとに以下のような内訳となっている。 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p> <blockquote> <div class="table-responsive"><table> <thead> <tr> <th>Security patching</th> <th>Ubuntu LTS</th> <th>Ubuntu Pro (Infra-only)</th> <th>Ubuntu Pro</th> </tr> </thead> <tbody> <tr> <td>Over 2,300 packages in Ubuntu <strong>Main</strong> repository</td> <td>5 years</td> <td>10 years</td> <td>10 years</td> </tr> <tr> <td>Over 23,000 packages in Ubuntu <strong>Universe</strong> repository</td> <td>Best effort</td> <td>Best effort</td> <td>10 years</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/support">Optional phone/ticket support</a></td> <td>No</td> <td>Yes</td> <td>Yes</td> </tr> </tbody> </table></div> </blockquote> <p>5年のLTSの期間中、Main リポジトリのパッケージを主に使用する場合、(ライブパッチ等の他のUbuntu Proのサービスが不要であれば)無料の範囲内で十分だ。</p> <p>Main リポジトリのパッケージを主に使用しながら、10年の延長サポートを望む場合は、"Ubuntu Pro (Infra-only)" を契約することをおすすめする。</p> <p>また、Universe リポジトリのパッケージを積極的に活用したい場合は、"Ubuntu Pro" を契約しておくと、より安心だろう。</p> <h2 id="Ubuntu の Universe リポジトリのパッケージの更新基準"><a href="#Ubuntu+%E3%81%AE+Universe+%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E6%9B%B4%E6%96%B0%E5%9F%BA%E6%BA%96">Ubuntu の Universe リポジトリのパッケージの更新基準</a></h2> <p>ちなみに、Universe repository の "Best effort" と "10 years" の違いがいまいちわかりにくいが…</p> <p>例えば、サポート中の Ubuntu 22.04 LTS であっても、 Universe リポジトリの imagemagick パッケージを更新しようとすると、 <code>Ubuntu Pro with 'esm-apps'</code> の有効化が必要と広告が表示される。</p> <pre><code>$ cat /etc/os-release PRETTY_NAME="Ubuntu 22.04.2 LTS" NAME="Ubuntu" VERSION_ID="22.04" VERSION="22.04.2 LTS (Jammy Jellyfish)" VERSION_CODENAME=jammy ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=jammy $ $ sudo apt update $ sudo apt install imagemagick $ $ sudo apt upgrade Reading package lists... Done Building dependency tree... Done Reading state information... Done Calculating upgrade... Done Get more security updates through Ubuntu Pro with 'esm-apps' enabled: imagemagick ansible libopenexr25 libmagickcore-6.q16-6-extra libmagickwand-6.q16-6 imagemagick-6.q16 libmagickcore-6.q16-6 imagemagick-6-common Learn more about Ubuntu Pro at https://ubuntu.com/pro 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. $ $ apt-cache policy imagemagick imagemagick: Installed: 8:6.9.11.60+dfsg-1.3ubuntu0.22.04.3 Candidate: 8:6.9.11.60+dfsg-1.3ubuntu0.22.04.3 Version table: *** 8:6.9.11.60+dfsg-1.3ubuntu0.22.04.3 500 500 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages 500 http://archive.ubuntu.com/ubuntu jammy-security/universe amd64 Packages 100 /var/lib/dpkg/status 8:6.9.11.60+dfsg-1.3build2 500 500 http://archive.ubuntu.com/ubuntu jammy/universe amd64 Packages </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/1452299/im-getting-the-message-the-following-security-updates-require-ubuntu-pro-with/1453309#1453309">package management - I'm getting the message: "The following security updates require Ubuntu Pro with 'esm-apps' enabled" when updating Ubuntu 22.04 - Ask Ubuntu</a></p> <p>上記の回答によると、 Ubuntu の Universe リポジトリのアップストリームパッチの取り込みは引き続き行っているものの、 Canonical のセキュリティチームによるパッチは Ubuntu Pro の 'esm-apps' に限って行うようになっている。</p> <p>ここでいう「アップストリームパッチ」とはなんぞや?</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/ImageMagick/ImageMagick6/tree/6.9.12-90">ImageMagick/ImageMagick6 at 6.9.12-90</a></p> <p>ImageMagick の元の開発元の GitHub を見れば分かる通り、より高いバージョンのパッケージもメンテナンスリリースされ続けているが、これがそのまま取り込まれているわけではなさそうだ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://packages.debian.org/bullseye/imagemagick">Debian -- bullseye の imagemagick パッケージに関する詳細</a></p> <p>一方で、Ubuntu 22.04 LTS の元となっている Debian 11 bullseye のパッケージを見ると、 2023年7月 現在は</p> <pre><code>imagemagick (8:6.9.11.60+dfsg-1.3+deb11u1) </code></pre> <p>と、 Ubuntu 側の非ESMのバージョンと、似たようなバージョンにとどまっている。</p> <p>すなわち、ココで言う「アップストリーム」とは、 Debian のパッケージ管理の方を指しているのだと思われる。</p> <p>おそらく、Universe リポジトリのパッケージについては以下のようになっているのだろう。</p> <ul> <li>ベースとなっている Debian でリリースされているバージョンは Best effort で LTS へ取り込み</li> <li>それ以外の Canonical 側で当てたセキュリティパッチは Ubuntu Pro のみに提供</li> </ul> <p>だとすれば、Ubuntu Pro の esm-apps を有効にしていなくても、ベースとなった Debian レベルの修正はされると考えてよさそうだ。</p> <h3 id="実際にソースコードを見てみる"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%82%8B">実際にソースコードを見てみる</a></h3> <p>実際に、 <a target="_blank" rel="nofollow noopener" href="http://ftp.debian.org/debian/dists/bullseye/main/source/Sources.gz">Debian 11 bullseye</a>, <a target="_blank" rel="nofollow noopener" href="http://archive.ubuntu.com/ubuntu/dists/jammy-security/universe/source/Sources.gz">Ubuntu 22.04 LTS</a>, <a target="_blank" rel="nofollow noopener" href="https://esm.ubuntu.com/apps/ubuntu/dists/jammy-apps-security/main/source/Sources.gz">Ubuntu 22.04 ESM</a> それぞれに於ける2023年7月現在最新版のソースを、 <code>apt source</code> コマンドで入手してみて比べてみると…</p> <div class="table-responsive"><table> <thead> <tr> <th>ディストリビューション</th> <th><a target="_blank" rel="nofollow noopener" href="https://debian-handbook.info/browse/ja-JP/stable/sect.source-package-structure.html">.dsc (Debian Source Control) ファイル</a></th> <th>ソースファイル</th> </tr> </thead> <tbody> <tr> <td>Debian 11 bullseye</td> <td><a target="_blank" rel="nofollow noopener" href="http://ftp.debian.org/debian/pool/main/i/imagemagick/imagemagick_6.9.11.60+dfsg-1.3+deb11u1.dsc">imagemagick_6.9.11.60+dfsg-1.3+deb11u1.dsc</a></td> <td>imagemagick_6.9.11.60+dfsg.orig.tar.xzimagemagick_6.9.11.60+dfsg-1.3+deb11u1.debian.tar.xz</td> </tr> <tr> <td>Ubuntu 22.04 LTS</td> <td><a target="_blank" rel="nofollow noopener" href="http://archive.ubuntu.com/ubuntu/pool/universe/i/imagemagick/imagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3.dsc">imagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3.dsc</a></td> <td>imagemagick_6.9.11.60+dfsg.orig.tar.xzimagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3.debian.tar.xz</td> </tr> <tr> <td>Ubuntu 22.04 esm-apps</td> <td><a target="_blank" rel="nofollow noopener" href="https://esm.ubuntu.com/apps/ubuntu/pool/main/i/imagemagick/imagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3+esm2.dsc">imagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3+esm2.dsc</a> <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></td> <td>imagemagick_6.9.11.60+dfsg.orig.tar.xzimagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3+esm2.debian.tar.xz</td> </tr> </tbody> </table></div> <p>このうち、 <code>imagemagick_6.9.11.60+dfsg.orig.tar.xz</code> についてはハッシュ値まで同じファイルなので、相違点は以下のファイルとなる。</p> <ul> <li><code>imagemagick_6.9.11.60+dfsg-1.3+deb11u1.debian.tar.xz</code></li> <li><code>imagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3.debian.tar.xz</code></li> <li><code>imagemagick_6.9.11.60+dfsg-1.3ubuntu0.22.04.3+esm2.debian.tar.xz</code></li> </ul> <p>これらのアーカイブファイルの中身に対して WinMerge の3方向マージなどを使い、changelog やパッチファイルなどの比較をしてみよう。<br /> すると、いずれも <code>6.9.11.60+dfsg-1.3</code> までのパッチがあたっているのに加えて、それぞれ以下のセキュリティフィックスを含んでいることがわかる。</p> <ul> <li>Debian 11 と Ubuntu 22.04 LTS <ul> <li>CVE-2022-44267</li> <li>CVE-2022-44268</li> </ul></li> <li>Ubuntu 22.04 esm-apps <ul> <li>CVE-2022-44267</li> <li>CVE-2022-44268</li> <li>CVE-2021-3574</li> <li>CVE-2021-3610</li> <li>CVE-2021-4219</li> <li>CVE-2021-20241</li> <li>CVE-2021-20243</li> <li>CVE-2021-20244</li> <li>CVE-2021-20245</li> <li>CVE-2021-20246</li> <li>CVE-2021-20309</li> <li>CVE-2021-20312</li> <li>CVE-2021-20313</li> <li>CVE-2021-39212</li> <li>CVE-2022-1114</li> <li>CVE-2022-28463</li> <li>CVE-2022-32545</li> <li>CVE-2022-32546</li> <li>CVE-2022-32547</li> <li>CVE-2023-1289</li> <li>CVE-2023-1906</li> <li>CVE-2023-3195</li> <li>CVE-2023-3428</li> <li>CVE-2023-34151</li> </ul></li> </ul> <p>前述の、 <code>Debian 11</code> ≒ <code>Ubuntu 22.04 LTS</code> ≦ <code>Ubuntu 22.04 esm-apps</code> だろうという予想は当たっていそうだ。</p> <p>ちなみに、つい先日2023年6月にリリースされたばかりの <a target="_blank" rel="nofollow noopener" href="http://ftp.debian.org/debian/dists/bookworm/main/source/Sources.gz">Debian 12 bookworm</a> では、<a target="_blank" rel="nofollow noopener" href="http://ftp.debian.org/debian/pool/main/i/imagemagick/imagemagick_6.9.11.60+dfsg-1.6.dsc">imagemagick_6.9.11.60+dfsg-1.6.dsc</a> という少し dfsg のバージョンが高いパッケージがリリースされている。<br /> その changelog を見てみると、<code>6.9.11.60+dfsg-1.3</code> までのパッチに加えて、以下のセキュリティフィックスが含まれていた。</p> <ul> <li>Debian 12 <ul> <li>CVE-2022-44267</li> <li>CVE-2022-44268</li> <li>CVE-2021-3574</li> <li>CVE-2021-4219</li> <li>CVE-2021-20241</li> <li>CVE-2021-20243</li> <li>CVE-2021-20244</li> <li>CVE-2021-20245</li> <li>CVE-2021-20246</li> <li>CVE-2021-20309</li> <li>CVE-2021-20312</li> <li>CVE-2021-20313</li> <li>CVE-2021-39212</li> <li>CVE-2022-1114</li> <li>CVE-2022-28463</li> <li>CVE-2022-32545</li> <li>CVE-2022-32546</li> <li>CVE-2022-32547</li> </ul></li> </ul> <p>Ubuntu Pro の esm-apps のほうが、Debian 12 に含まれているパッチに加え、更に多くのセキュリティフィックスを含んでいることがわかる。</p> <p>… Debian 12 bookworm のパッケージでも、ImageMagick 6.9.12系 や ImageMagick 7系へのパッケージの更新はしていないのね。</p> <hr /> <p>参考:</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://gihyo.jp/admin/clip/01/ubuntu-topics/202210/07">「Ubuntu Pro」のワークステーション・データセンター向けベータリリース | gihyo.jp</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://pc.watch.impress.co.jp/docs/column/ubuntu/1512815.html">【Ubuntu日和】【第30回】DebianとUbuntu、CentOSとRHELから学ぶ、Upstreamとの関係 - PC Watch</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://help.ubuntu.com/community/Repositories">Repositories - Community Help Wiki</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/pro">Ubuntu Pro | Ubuntu</a> <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:3" role="doc-endnote"> <p>ESM向けの配信ページはパスワード保護がかかっているが、Ubuntu Pro を有効すると作成される<a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/a/1454416"><code>/etc/apt/auth.conf.d/</code>内のファイルに記載されたユーザー名(<code>bearer</code>)とパスワードで認証できる</a>。 <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> advanceboy tag:crieit.net,2005:PublicArticle/18494 2023-06-28T01:34:29+09:00 2023-06-28T01:34:29+09:00 https://crieit.net/posts/SIKAI-CASE 超コスパ左手デバイス SIKAI CASE を安全に設定する <p>世の中には、「片手キーボード」とか「左手デバイス」と呼ばれるデバイスが存在する。</p> <p>ペンを使ったお絵かきや、マウス中心の作業を行う際に使われるデバイスで、必要なショートカットキーなどを登録しておくことで、複雑な操作を簡略化して入力を行えるものだ。</p> <p>こういった商品は、世の中にいろいろあるのだが、有名な商品はまぁ高い。</p> <p>自分で好きなようにショートカットキーを登録できる汎用的なものだと、2万3万は余裕で飛ぶようなものばかりだ。</p> <p>こういったジャンルには、いわゆる中華デバイスなどとも呼ばれる、機能を割り切った安価なデバイスが売られていることが多く、この「左手デバイス」も例外ではない。</p> <p>そんな、中華左手デバイスの中で、デバイスとしては比較的評判が悪くない <a target="_blank" rel="nofollow noopener" href="https://amzn.to/3ONzcTM">SIKAI CASE 片手キーボード</a> を、少し安全に使う方法について、少し考えてみようと思う。</p> <p><a target="_blank" rel="nofollow noopener" href="https://amzn.to/3ONzcTM"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-00-1024x760.jpg" alt="" /></a></p> <p>なお、この製品自体がどんなものなのかは、ここらへんの解説動画あたりが参考になるので、そちらをどうぞ。<br /> (私自身も、この動画を見て買った)</p> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/2ghCYdc171s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe></div> <h2 id="「安全に使う」とは?"><a href="#%E3%80%8C%E5%AE%89%E5%85%A8%E3%81%AB%E4%BD%BF%E3%81%86%E3%80%8D%E3%81%A8%E3%81%AF%EF%BC%9F">「安全に使う」とは?</a></h2> <p>この SIKAI CASE の片手キーボードの難点として挙げられるポイントの筆頭が、「設定ツール」に関するものだ。</p> <p>まず、 UI あまりよろしくなく、だいぶ使いにくい。</p> <p>そして、一番の難点は <strong>得体の知れない Google ドライブから、設定ツールをダウンロードさせる</strong> ところだろう。<br /> 正直、こんな怪しいツールをメインのマシンで動かしたくはない。</p> <p>「設定ツールすら信用できないのに、問題の片手デバイスを USB で繋ぐ事を許容するのは、矛盾してるのでは?」と思われるかもしれない。</p> <p>しかし、だ。</p> <p>設定ツールで書き込まれた設定内容は、片手デバイス内部に保存され、他の Windows や Androidb, macOS でも使える仕組みになっている。<br /> このとき、汎用 HID デバイス(一般的なマウスやキーボード)として振る舞い、ドライバーも OS 組み込みのものが使われる。</p> <p>そのため、仮にデバイス側に悪意があっても、過電流でPCを物理的にぶっ壊すか、入力されたキーでなんかするくらいしかできず、現実的にはあまり大きなリスクにはならないと考えられる。</p> <p>つまり、なんとかキー割り当ての設定さえしてしまえれば、値段のわりに安全 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> に使えるのでは、というワケだ。</p> <h2 id="仮想マシンの中から設定"><a href="#%E4%BB%AE%E6%83%B3%E3%83%9E%E3%82%B7%E3%83%B3%E3%81%AE%E4%B8%AD%E3%81%8B%E3%82%89%E8%A8%AD%E5%AE%9A">仮想マシンの中から設定</a></h2> <p>さて、前項を話をまとめると、<br /> 『得体の知れない設定ツールによって「自分のPCの中身」を読み書きさせるのを避けたい』<br /> ということになる。</p> <p>そこで、自分の PC の中に分離された環境の仮想マシンを立て、その中でツールを動かしてしまえば、問題を解決できそうだ。</p> <p>ツールを逆コンパイルしてざっとの動作を見た感じ、 <a target="_blank" rel="nofollow noopener" href="https://www.nuget.org/packages/hidlibrary">hidlibrary</a> を使って、以下のような条件に一致する USB デバイスを探して、HID レポートを送りつけて設定を書き換えているようだ。</p> <ul> <li>Vendor ID: <code>0x1189</code> (Trisat Industrial Co., Ltd.)</li> <li>Product ID: <code>0x8890</code>, <code>0x8810</code>,<code>0x8830</code>-<code>0x8834</code>, <code>0x8840</code></li> <li><code>"mi_00"</code> や <code>"mi_01"</code> といったデバイスパスを持つ</li> </ul> <p>このため、仮想マシンゲストOSに USB デバイスをマウントしてしまえば、ゲストOS からキーの設定ができそうだ。</p> <p>ということで、 VirtualBox と Extension Pack を用いて、 USB デバイスのマウントを行い、ゲスト OS の中から設定する方法をとる。</p> <p>なお、 Windows 10/11 の Pro エディションなら、 Hyper-V と RemoteFX USB Redirect の組み合わせでも同じ事ができると思われる。</p> <h3 id="とりあえず VirtualBox に Windows を入れる"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A+VirtualBox+%E3%81%AB+Windows+%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B">とりあえず VirtualBox に Windows を入れる</a></h3> <p>ここの手順はざっくりとだけ説明する。<br /> 詳しくは、 "VirtualBox Windows インストール" とかで検索してくれ。</p> <p>以下のファイルをそれぞれダウンロードし、</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.virtualbox.org/wiki/Downloads">VirtualBox</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.virtualbox.org/wiki/Downloads">VirtualBox Extension Pack</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.microsoft.com/ja-jp/evalcenter/evaluate-windows-11-enterprise">Windows 10 または 11 の評価版 の ISO</a></li> </ul> <p>VirtualBox をインストール & Extension Pack のインポートを実施する。</p> <p>VirtualBox を立ち上げて仮想マシンを新規作成し、 ISO に評価版のものを指定し、 Windows をインストールさせる。<br /> (評価版だと、 "Unattended Install" ができないので、手動でインストールウィザードを進めよう。)</p> <h3 id="仮想マシンのゲストに USB 機器をマウントして設定"><a href="#%E4%BB%AE%E6%83%B3%E3%83%9E%E3%82%B7%E3%83%B3%E3%81%AE%E3%82%B2%E3%82%B9%E3%83%88%E3%81%AB+USB+%E6%A9%9F%E5%99%A8%E3%82%92%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88%E3%81%97%E3%81%A6%E8%A8%AD%E5%AE%9A">仮想マシンのゲストに USB 機器をマウントして設定</a></h3> <p>仮想マシンのゲストOSのインストールが終わったら、ゲストOS内で設定ツールをダウンロードする。</p> <p>設定ツールを起動させても、以下のようにデバイスが未接続だと表示されるはずだ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-01.png" alt="" /></a></p> <p>ゲストOS に USB デバイスをマウントするために、ホストPC側に左手デバイスを USB で接続し、VirtualBox の「デバイス」オプションから、 USB -> wch.cn などと書いてある(左手デバイスによって異なる)ベンダーID 1189 のデバイスを選択する。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-02.png" alt="" /></a></p> <p>すると、ゲスト側の設定ツールが反応し、接続状態に切り替わる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-03.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/06/left-hand-device-SIKAI-CASE-03.png" alt="" /></a></p> <p>この状態になれば、設定ツールによる書き込みが可能になる。</p> <p>設定するキーやジョグダイヤルを選択し、1キーずつ設定をダウンロードさせていこう。</p> <p>全て設定が終わったら、ゲストOSから USB のマウントを停止させる。</p> <p>ゲストOS 自体もシャットダウンさせてしまって良いだろう。</p> <h3 id="動作確認"><a href="#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">動作確認</a></h3> <p>ホスト PC から 左手デバイスを一旦取り外し、実際に使いたいマシンに改めて接続する。</p> <p>すると、先ほど設定した内容は片手デバイスの中に保存されているため、設定したとおりに機能するはずだ。</p> <h2 id="その他"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96">その他</a></h2> <p>説明としては以上だが、この SIKAI CASE 左手デバイスについて気になる点をいくつか。</p> <ul> <li>Bluetooth版 (無線版) には技適マークが無い <ul> <li>無線を使っているかにかかわらず、国内で通電させたら厳密には法律違反となるので注意</li> </ul></li> <li>レイヤー機能が Bluetooth版 (無線版) にしか存在しない <ul> <li>キーの組み合わせを3パターンも登録できるレイヤー機能が便利なのだが、Bluetooth版にしか存在しない</li> <li>レイヤー機能の無い有線版を買うか、電波法違反を承知で無線版を有線接続して使うか、の2択になる。</li> <li>もちろん、後者は違法なのでオススメしない。</li> </ul></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>あくまで「値段の割に」であり、その安全性を保証しているわけではないので、使用は各自の自己責任で。 <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> advanceboy tag:crieit.net,2005:PublicArticle/18441 2023-05-29T09:25:32+09:00 2023-06-04T01:27:32+09:00 https://crieit.net/posts/GeForce-GPU-35-RINNA-AI ミドルGPUで35億の女(RINNA AI)のローカル実行 ことはじめ <p>RINNA社が <a target="_blank" rel="nofollow noopener" href="https://prtimes.jp/main/html/rd/p/000000042.000070041.html">5月17日</a> に、 <a target="_blank" rel="nofollow noopener" href="https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-sft">rinna/japanese-gpt-neox-3.6b-instruction-sft</a> という、対話言語 AI モデルを公開したと、プレスリリースを出した。<br /> その後さらに <a target="_blank" rel="nofollow noopener" href="https://rinna.co.jp/news/2023/05/20220531.html">5月31日</a>、人間の評価による強化学習したモデル <a target="_blank" rel="nofollow noopener" href="https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-ppo">rinna/japanese-gpt-neox-3.6b-instruction-ppo</a> を追加リリースしている。</p> <p>この AI モデルは、ご家庭にあるミドルレンジ GPU でもサクサク動く性能だったので、 AI 系に明るくない人にも導入できるように、詳しい導入手順を紹介しようと思う。<br /> <a href="https://crieit.now.sh/upload_images/4df8d093a01c34ee29df245fd53b00c06475320f4437d.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4df8d093a01c34ee29df245fd53b00c06475320f4437d.gif?mw=700" alt="rinna-3.6b-chat-00-00-split.gif" /></a></p> <p>こう言うのって、登場して直ぐ情報がホットなうちに記事にすることに価値があって、こんな 1~2週間経ってから記事を書いたって誰にも届かないんだろうけど、とりあえず書いてみる。</p> <p>この界隈の情報はコンテクストが高いというか、あんまりイチから導入方法を説明している記事って目にしないので、多少は需要があるかなと。</p> <p>はじめに断っておくが、 Chat-GPT や 新しいBing 等と比べると数段落ちる回答精度となる。<br /> ただ、個人がお手軽に購入できる程度の GPU を使って、完全にローカルで動かす事ができるという点が、一種の浪漫だ。</p> <h3 id="前提条件"><a href="#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6">前提条件</a></h3> <p>以下のスペックを持った PC。</p> <ul> <li>OS は Windows 10 または Windows 11</li> <li>GeForce RTX 3060 以上、 VRAM 12GB 以上が載った NVIDIA の GPU <ul> <li>GPU ドライバはインストール済み</li> <li>具体的には 2023年5月現在 以下のいずれかモデル <ul> <li>GeForce RTX 3060 (12 GB)</li> <li>GeForce RTX 3080 (12 GB)</li> <li>GeForce RTX 3090</li> <li>GeForce RTX 3090 Ti</li> <li>GeForce RTX 4060 Ti (16 GB)</li> <li>GeForce RTX 4070</li> <li>GeForce RTX 4070 Ti</li> <li>GeForce RTX 4080</li> <li>GeForce RTX 4090</li> </ul></li> <li>3070 Ti, 3060 Ti, 3060 (8GB) だと VRAM が足りないので、多分動かすとエラーになる</li> </ul></li> </ul> <h2><strong>導入手順</strong></h2> <h3 id="Python のインストール"><a href="#Python+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Python のインストール</a></h3> <p>Python と呼ばれるプログラミング言語(の、「ランタイム」と呼ばれるもの)をインストールする。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.python.org/downloads/windows/">https://www.python.org/downloads/windows/</a></p> <p>こちらのページから、 Python Releases for Windows の Latest Python 3 Release のバージョン (以下のスクショ だと 3.11.3) を確認し、下のリストからそのバージョンの "Windows Installer (64-bit)" をダウンロード。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-01-300x225.png" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-02-300x225.png" alt="" /></a></p> <p>ダウンロードしたインストーラーを実行し、デフォルトの "Use admin privileges when installing py.exe" にチェックが入っていることを確認して、インストールを実行。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-03.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-03-300x185.jpg" alt="" /></a></p> <h3 id="Python 仮想環境の作成"><a href="#Python+%E4%BB%AE%E6%83%B3%E7%92%B0%E5%A2%83%E3%81%AE%E4%BD%9C%E6%88%90">Python 仮想環境の作成</a></h3> <p>先ほどインストールした Python に、追加で必要なツールをダウンロードする環境(仮想環境と呼ばれるもの)を作成する。</p> <p>まず、これから環境を作成していく元のフォルダをエクスプローラで開き、アドレスバーに <code>powershell</code> と入力する。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-04.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-01-04-300x201.png" alt="" /></a></p> <p>すると、黒い画面が立ち上がるので、以下のコマンドを実行。</p> <pre><code>py -3 -m venv rinna-japanese-gpt-chat </code></pre> <p>すると、 <code>rinna-japanese-gpt-chat</code> という名前のフォルダ作成されているはず。</p> <h3 id="vscode のインストール"><a href="#vscode+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">vscode のインストール</a></h3> <p>Visual Studio Code (vscode) という、プログラム用のテキストエディタ(コードエディタ)をインストールする。</p> <p><a target="_blank" rel="nofollow noopener" href="https://code.visualstudio.com/">https://code.visualstudio.com/</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-01-300x225.png" alt="" /></a></p> <p>Download for Windows からインストーラをダウンロードし実行する。</p> <p>途中、インストールタスクの選択で、「エクスプローラーのディレクトリ コンテキスト メニューに [Code で開く] アクションを追加する」を追加しておく。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-02-300x233.png" alt="" /></a></p> <h3 id="スクリプトファイルのダウンロード"><a href="#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89">スクリプトファイルのダウンロード</a></h3> <p>実行するスクリプトファイルをダウンロード。</p> <p>以下のページの右上らへんにある、「Download ZIP」をクリックしてダウンロードした ZIP を解凍し、中にある <code>rinna_gradio_chat.py</code> を、 先ほど作成された <code>rinna-japanese-gpt-chat</code> という名前のフォルダの中に置く。</p> <p><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/advanceboy/717fde162a6f9ccb592f04898f0aacc1">https://gist.github.com/advanceboy/717fde162a6f9ccb592f04898f0aacc1</a></p> <h3 id="vscode の環境のセットアップ"><a href="#vscode+%E3%81%AE%E7%92%B0%E5%A2%83%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97">vscode の環境のセットアップ</a></h3> <p>後半の作業を簡単にするため、 vscode の環境を整える。</p> <p>先ほど作成された <code>rinna-japanese-gpt-chat</code> という名前のフォルダを右クリックして(Windows 11 の場合は、更に「その他のオプションを表示を」クリックして)、 「Code で開く」 を選択する。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-03.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-03-300x225.png" alt="" /></a></p> <p>すると、 vscode が立ち上がる (フォルダを信用しますか?的な Trust 何ちゃらの画面が出たら、 yes を選択しておく)。</p> <p><code>Ctrl + Shift + E</code> キーを押すと開く vscode 上の Explorer ペインで、先ほどダウンロードした <code>.py</code> ファイルを選択する。<br /> すると、 "Do you want to install the recommented 'Python' Extension..." と聞かれるので、 <code>Install</code> を選択。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-04.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-04-300x106.png" alt="" /></a></p> <p>インストールが終わったら、再び vscode 上の Explorer ペインで <code>.py</code> ファイルを選択して <code>Ctrl+@</code> キーを押し、 PowerShell ターミナルを表示させる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-05.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-03-05-300x145.png" alt="" /></a></p> <p>このとき、表示される TERMINAL ウィンドウでは <code>(rinna-japanese-gpt-chat) PS</code> と先頭に表示されるようになっているはずだが、もしそれが表示されない場合は、このターミナル上で <code>.\Scripts\Activate.ps1</code> と入力しておく。</p> <h3 id="Python の依存パッケージ (CUDA 対応 CUDA) の確認とインストール"><a href="#Python+%E3%81%AE%E4%BE%9D%E5%AD%98%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8+%28CUDA+%E5%AF%BE%E5%BF%9C+CUDA%29+%E3%81%AE%E7%A2%BA%E8%AA%8D%E3%81%A8%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Python の依存パッケージ (CUDA 対応 CUDA) の確認とインストール</a></h3> <p>Python で AI を実行するため、 AI 関連ライブラリーを、先ほど作成した Python の仮想環境内にインストールする。</p> <p>まず、 PyTorch という機械学習ライブラリのページに飛ぶ。<br /> <a target="_blank" rel="nofollow noopener" href="https://pytorch.org/get-started/locally/">https://pytorch.org/get-started/locally/</a></p> <p>以下のような組み合わせの選択肢を選び、表示されたコマンドを前述の vscode のターミナル上( <code>(rinna-japanese-gpt-chat) PS</code> と先頭に表示される状態)で実行する。</p> <ul> <li>PyTorch Build: Stable</li> <li>Your OS: Windows</li> <li>Package: Pip</li> <li>Language: Python</li> <li>Compute Platform: CUDA の最新版</li> </ul> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-04-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-04-01-300x210.png" alt="" /></a></p> <p>例えば、 CUDA 11.8 の環境をインストールする場合は、以下のようなコマンドになると思われる。</p> <pre><code>pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 </code></pre> <p>その後、残りの依存ライブラリもインストールしておく。</p> <pre><code>pip3 install ipython sentencepiece transformers accelerate gradio </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-04-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-04-02-300x60.png" alt="" /></a></p> <p><code>pip3 install</code> の実行順を逆にすると、 CUDA に対応していない torch がインストールされてしまい、後々エラーになってしまうので注意。</p> <h3 id="CUDA ツールキットのインストール"><a href="#CUDA+%E3%83%84%E3%83%BC%E3%83%AB%E3%82%AD%E3%83%83%E3%83%88%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">CUDA ツールキットのインストール</a></h3> <p>最後に、 CUDA と呼ばれる NVIDIA の GPU 上で AI を効率よく実行するためのツールキットをインストールする。</p> <p>以下の CUDA Toolkit Archive のページを開き、 前項で "Compute Platform" で選択したバージョンの CUDA Toolkit をダウンロードし、インストールする。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.nvidia.com/cuda-toolkit-archive">https://developer.nvidia.com/cuda-toolkit-archive</a></p> <p>最新版ではなく、前項の PyTorch ライブラリのダウンロード時に選択したバージョンの CUDA をダウンロードする必要がある点に注意が必要だ。</p> <p>例えば、 2023年5月現在、 CUDA Toolkit の最新版は 12.1.1 だが、 前述の PyTorch ライブラリが対応する CUDA が 11.8 までなので、 CUDA Toolkit 11.8.0 をダウンロード&インストールする。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-05-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/05/rinna-3.6b-chat-05-01-300x224.png" alt="" /></a></p> <h2><strong>実行手順</strong></h2> <p>vscode のターミナル上( <code>(rinna-japanese-gpt-chat) PS</code> と先頭に表示される状態)で、以下のように実行する。</p> <pre><code>python .\rinna_gradio_chat.py </code></pre> <p>しばらくすると、ターミナルにローカルで立ち上がったサーバーの URL <a target="_blank" rel="nofollow noopener" href="http://127.0.0.1:7860">http://127.0.0.1:7860</a> が表示されるので、 Ctrl キーを押しながらクリックして、ブラウザでアクセスする。</p> <p><a href="https://crieit.now.sh/upload_images/4df8d093a01c34ee29df245fd53b00c06475320f4437d.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4df8d093a01c34ee29df245fd53b00c06475320f4437d.gif?mw=700" alt="rinna-3.6b-chat-00-00-split.gif" /></a></p> <p>あとはお好きなようにいじくり倒すだけだ。</p> <p>前述の動画は生成部分も含めて等倍速だが、ミドルレンジの GPU でもそれなりの速度で生成できていることがわかる。</p> <p>停止させたい場合は、ターミナル上で <code>Ctrl + C</code> キーを押そう。</p> <h2><strong>補足</strong></h2> <h3 id="生成速度"><a href="#%E7%94%9F%E6%88%90%E9%80%9F%E5%BA%A6">生成速度</a></h3> <p>ChatGPT などでは元々生成速度が速いので気になりにくいが、 英語と比べて日本語テキストの生成は遅く、同じ情報量で生成時間も利用料金も数倍かかってしまう。<br /> その理由を雑に説明すると、「トークン」と呼ばれる学習・生成の単位が、英語では単語単位となる一方で、日本語は文字単位となっているためだ。</p> <p>一方、りんなさんこと japanese-gpt-neox-3.6b では、単語間にスペースが存在しない日本語でも、ある程度単語ごとにトークン化されてから処理されるので、生成も文字単位では無く単語単位となり、処理効率が大幅に向上しているのだろうと考えられる。<br /> ChatGPT などが存在しても、国産ないし国内向けの AI モデルが必要とされる理由の一つに、こういった背景があるようだ。</p> <h3 id="対応GPU"><a href="#%E5%AF%BE%E5%BF%9CGPU">対応GPU</a></h3> <p>対応GPUを RTX30 世代以降に限っているのは、VRAM の利用を節約するために bfloat16 量子化して動かしているのだが、この bloat16 での効率的な計算に対応しているのが Ampere 以降世代の GPU となるためだ。</p> <p>もし、 RTX 10, 20 の GPU で動かしてみたい場合、</p> <p><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/advanceboy/717fde162a6f9ccb592f04898f0aacc1#file-rinna_gradio_chat-py-L35">https://gist.github.com/advanceboy/717fde162a6f9ccb592f04898f0aacc1#file-rinna_gradio_chat-py-L35</a></p> <pre><code class="python">torch_dtype = torch.bfloat16 </code></pre> <p>この部分を、搭載 VRAM に応じて <code>torch_dtype = torch.float16</code> や <code>torch_dtype = torch.float32</code> に書き換えれば上手く動くかもしれない。</p> <h3 id="CUI版"><a href="#CUI%E7%89%88">CUI版</a></h3> <p>わざわざブラウザ立ちあげず CUI 上でチャットしたい場合は、上記のスクリプトの替わりに以下のスクリプトを使うと良いだろう。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">rinna_chat_streaming.pyrinna/japanese-gpt-neox-3.6b-instruction-sft を使ったチャット UI のサンプル実装です。 transformers.TextIteratorStreamer API を利用して、 ChatGPT のように生成したテキストを少しずつ表示し、ユーザー体験を向上させています。<a target="_blank" rel="nofollow noopener" href="https://t.co/tXtoN952un">https://t.co/tXtoN952un</a></p>— ながいの as a サービス (@longer_n) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/longer_n/status/1659595062496612354?ref_src=twsrc%5Etfw">May 19, 2023</a></blockquote> advanceboy tag:crieit.net,2005:PublicArticle/18426 2023-05-01T01:32:11+09:00 2023-05-01T01:32:11+09:00 https://crieit.net/posts/YAML YAML の細かい仕様の話 <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">グワーッ!YAML 1.2 って、 yes,no,on,off 系が Boolean リテラルではなくなる破壊的変更あるん!?Docker Compose v2.17.0 で内部パーサが go-yaml/yaml.v3 に更新され、真偽値を yes, no で書いてた compose.yml ファイルが軒並み must be a boolean エラー出まくってて死。<a target="_blank" rel="nofollow noopener" href="https://t.co/PVpdIWFVgA">https://t.co/PVpdIWFVgA</a></p>— ながいの as a サービス (@longer_n) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/longer_n/status/1646154607201075207?ref_src=twsrc%5Etfw">April 12, 2023</a></blockquote> <p>趣味アカウントのほうでバズった… ってほどでは無いけど、割と反響があったので、 YAML に関する話をいくつか。</p> <h2 id="YAML 1.2 では Boolean として扱うのは true, false だけ"><a href="#YAML+1.2+%E3%81%A7%E3%81%AF+Boolean+%E3%81%A8%E3%81%97%E3%81%A6%E6%89%B1%E3%81%86%E3%81%AE%E3%81%AF+true%2C+false+%E3%81%A0%E3%81%91">YAML 1.2 では Boolean として扱うのは true, false だけ</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://yaml.org/type/bool.html">https://yaml.org/type/bool.html</a></p> <p>YAML 1.1 では 以下の文字が Boolean として認識されるのが一般的だ。<br /> <code>y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF</code></p> <p>厳密には、 <code>!!bool</code> タグを使って Boolean 型を表現する場合の表記方法だが、基本的には多くのパーサーではタグを書かなくても Boolean 型として扱われる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://yaml.org/spec/1.2.0/#id2602744">https://yaml.org/spec/1.2.0/#id2602744</a></p> <p>しかし YAML 1.2 以降ではこれが、 <code>true</code>, <code>false</code> のみが Boolean として認識されるという、だいぶデカい破壊的変更が入っている。</p> <p>ここで、 1.2.0 の主たる更新点を見てみると…<br /> <a target="_blank" rel="nofollow noopener" href="https://yaml.org/spec/1.2.2/ext/changes/#changes-in-version-12-revision-120-2009-07-21">https://yaml.org/spec/1.2.2/ext/changes/#changes-in-version-12-revision-120-2009-07-21</a></p> <blockquote> <h4 id="Changes in version 1.2 (revision 1.2.0) (2009-07-21)"><a href="#Changes+in+version+1.2+%28revision+1.2.0%29+%282009-07-21%29">Changes in version 1.2 (revision 1.2.0) (2009-07-21)</a></h4> <p>[...]</p> <ul> <li>Only true and false strings are parsed as booleans (including True and TRUE); y, yes, on, and their negative counterparts are parsed as strings.</li> <li>Underlines _ cannot be used within numerical values.</li> <li>Octal values need a 0o prefix; e.g. 010 is now parsed with the value 10 rather than 8.</li> <li>The binary and sexagesimal integer formats have been dropped.</li> <li>The !!pairs, !!omap, !!set, !!timestamp and !!binary types have been dropped.</li> <li>The merge</li> </ul> </blockquote> <p>参考日本語訳:</p> <blockquote> <ul> <li>true と false の文字列のみがブーリアン(TrueとTRUEを含む)として解析される。 y, yes, on およびそれらの否定語は文字列として解析される。</li> <li>アンダーライン _ は、数値の中では使用できません。</li> <li>8進数の値には0oのプレフィックスが必要です。例えば010は8ではなく10という値で解析されるようになりました。</li> <li>二進法と十進法の整数形式は廃止されました。</li> <li>ペア、オマップ、セット、タイムスタンプ、バイナリ型は削除されました。</li> <li>merge</li> </ul> </blockquote> <p>うーん、現代の <a target="_blank" rel="nofollow noopener" href="https://semver.org/lang/ja/">セマンティック バージョニング</a> だったら、絶対にマイナー場ジョンアップじゃダメなヤツ。</p> <p>こういった仕様のため、 <code>yes</code>, <code>no</code> と書いた際にどう扱われるかは、パーサーの実装による。</p> <p>例えば、 PyYAML などは Boolean リテラルとして扱われるが、 go-yaml/yaml.v3 では文字列として扱われる。</p> <p>内部パーサーの更新でこの解釈が変わってしまったために発生したのが、冒頭の問題だ。</p> <p>Ansible などでは、真偽値を yes, no と書く慣習があるせいで、もうめちゃくちゃあっちこっちで <code>yes</code>, <code>no</code> って書いてるわ…</p> <p>なお、冒頭の Docker Compose については、 Docker Compose version v2.17.3 で <code>yes</code> 等を書いてもエラーにせずワーニングだけ出すように修正された。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/docker/compose/issues/10465">[BUG] Error "must be a boolean" with <code>yes</code> scalar in Docker Compose v2.17.0 · Issue #10465 · docker/compose</a></p> <hr /> <h2 id="&quot;&gt;&quot; は全ての改行をスペースに置き換えるわけではない"><a href="#%26quot%3B%26gt%3B%26quot%3B+%E3%81%AF%E5%85%A8%E3%81%A6%E3%81%AE%E6%94%B9%E8%A1%8C%E3%82%92%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%AB%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B%E3%82%8F%E3%81%91%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%84">">" は全ての改行をスペースに置き換えるわけではない</a></h2> <p>ブロックスタイルの文字列表記で、 <code>">"</code> インジゲータを置くと、次の行から途中の改行が半角スペースに置き換えられると説明されがちだ。</p> <pre><code class="yaml">foo: > trimmed line </code></pre> <p>しかし、半角スペースに置き換えられる条件はもう少し複雑だ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://yaml.org/spec/1.2.2/#line-folding">https://yaml.org/spec/1.2.2/#line-folding</a></p> <p>この Line Folding と言うのがこれに当たるのだが、これは長い行を折りたたんで読みやすくするための機能だ。</p> <p>折りたたまれた部分(改行)が半角スペース (0x20) に変換されるものだが、</p> <ul> <li>複数の改行が続く場合は最初の改行が破棄され、残りは保持される</li> <li>先頭の空白を含むテキスト行(つまりインデントされた行)は、行の折り返しは適用されない。</li> </ul> <p>という仕様がある。</p> <p>このため、以下のように動作する。</p> <p>YAML 入力:</p> <pre><code class="yaml">foo: > trimmed line breaking but blank line and nested line are are retained. </code></pre> <p>JSON 出力:</p> <pre><code class="json">{ "foo": "trimmed line breaking\nbut blank line and\n nested\n line\nare are retained.\n" } </code></pre> <p>つまり、「改行を消す」というよりは、「次の行のインデントの高さがブロックの先頭と同じ場合のみ、行の折りたたみと見なして改行を削除する」という動作となる。</p> <hr /> <h2 id="マージキー (&lt;&lt;) によるマージは 1回 しか記述できない。"><a href="#%E3%83%9E%E3%83%BC%E3%82%B8%E3%82%AD%E3%83%BC+%28%26lt%3B%26lt%3B%29+%E3%81%AB%E3%82%88%E3%82%8B%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%AF+1%E5%9B%9E+%E3%81%97%E3%81%8B%E8%A8%98%E8%BF%B0%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%80%82">マージキー (<<) によるマージは 1回 しか記述できない。</a></h2> <p>複数のマップをマージしたい場合は、 シーケンス(配列)によって指定する。</p> <p><a target="_blank" rel="nofollow noopener" href="https://yaml.org/type/merge.html">https://yaml.org/type/merge.html</a></p> <p>YAML 入力:</p> <pre><code class="yaml">- - &ANCHER1 { '0': 0, a: 0, b: 0 } - &ANCHER2 { b: 99, c: 99 } - &ANCHER3 { c: 255, d: 255 } #- # Eror # '0': 1 # &lt;&lt; : *ANCHER1 # &lt;&lt; : *ANCHER2 # &lt;&lt; : *ANCHER3 # a: 1 - # Single map b: 1 << : *ANCHER2 - # Override multiple maps '0': 1 << : - *ANCHER1 - *ANCHER2 - *ANCHER3 a: 1 </code></pre> <p>JSON 出力:</p> <pre><code class="json">[ [ { "a": 0, "b": 0 }, { "b": 99, "c": 99 }, { "c": 255, "d": 255 } ], { "b": 1, "c": 99 }, { "0": 1, "a": 1, "b": 0, "c": 99, "d": 255 } ] </code></pre> <p>シーケンスで複数のマップを指定した場合で、キーが重複している物は、シーケンス内では順序が前のものが常に優先される。<br /> 一方で、マージキーで指定したモノよりは、マージ先のものが優先となる。</p> <p>ただ、パーサーによっては マージキー (<code><<</code>) を複数書く誤った書き方に対応しているモノがあり、<br /> 実際、 Docker Compose v2.17.0 で、内部パーサーが <code>go-yaml/v3</code> に切り替わったときに、誤った書き方が対応から未対応に変わって、以下のようなエラーが出てハマった。</p> <pre><code>mapping key "<<" already defined at line nnn </code></pre> <p>[[BUG]using multiple yaml alias not working. mapping key "</p> <p>もちろんこれは、マージキーを複数書く方が悪いので、 YAML の方を直す必要がある。</p> <p>例: https://go.dev/play/p/jos7jc38loK<br /> --></p> advanceboy tag:crieit.net,2005:PublicArticle/18421 2023-04-01T01:43:53+09:00 2023-04-01T17:00:53+09:00 https://crieit.net/posts/Windows-MBR-SSD-GPT [Windows] MBR パーティションの SSD を GPT へコピーする <p>突然だが、超久々に自作 PC を組んだ。<br /> 前回は Windows 7 登場の 2009年に、 第1世代 Core i5 750 で組んだ時なので、約14年ぶりとなる。</p> <p>ハードは一度、中古でもらった 第3世代 Core i7 3770 に置換えているのだが、その際もストレージや Windows 7 Pro のライセンスはそのまま流用していてる。<br /> その後も、ストレージをクローンしたり「初期状態に戻す」で再インストールしながら、脈々と使い続けているものとなる。</p> <p>このため今回も、 旧PCに入っている SATA SSD に入っている Windows 10 を、新PC の NVMe SSD にコピーして、その状態で起動させてから(ライセンスにハード構成覚えさせてから)、 Windows 11 に上げようと考えた。</p> <p>ところが、 NVMe SSD で起動するのに結構苦戦したので、その解決策のメモ。</p> <h2 id="とりあえず最初に試したこと"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E6%9C%80%E5%88%9D%E3%81%AB%E8%A9%A6%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">とりあえず最初に試したこと</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://www.ventoy.net/en/index.html">Ventoy</a> をインストールしたブータブル USB メモリに GParted live を入れ、 GParted を使い、</p> <ul> <li>新NVMe SSD をパーティションテーブル GPT で初期化</li> <li>旧SATA SSD から 新NVMe SSD へ雑にパーティションをコピー</li> </ul> <p>を行った。</p> <p>この状態で試したところ、 ブルースクリーンすら表示されず、 UEFI から何もブートできない。<br /> USB メモリ内の Windows ISO からブートして、回復コンソールからスタートアップ修復などを試しても駄目だった。</p> <h2 id="根本的な原因"><a href="#%E6%A0%B9%E6%9C%AC%E7%9A%84%E3%81%AA%E5%8E%9F%E5%9B%A0">根本的な原因</a></h2> <p>原因は言わずもがな、パーティション弄った際のお馴染みのトラブルである、ブートレコードまわりの問題だ。<br /> 今回は、パーティションテーブルの違い (MBR と GPT) もあって、少々ややこしいことになっていた。</p> <p>この PC の系譜の元祖を組んだ 2009年 当時も UEFI は存在はしていたものの、当時サポートしていた M/B (マザボ) なんぞ殆どなく、そこから脈々とクローンしている旧 PC の SATA SSD のパーティションテーブルは MBR のままだ。</p> <p>一応、 M/B には CSM Support という BIOS 互換モードの設定があるので、 新SSD も MBR でパーティション作り直し、 セキュアブートを無効にすれば起動はできるはず?<br /> …とは思ったのだが…、</p> <p>なんと最近の CPU, チップセット は Legacy BIOS のブートに対応しない方針らしい。</p> <p><a target="_blank" rel="nofollow noopener" href="https://blog.tsukumo.co.jp/tokyo/2020/09/legacy_boot.html">【サポート情報】最新世代のマザーボードでのLegacy BOOTには注意しよう - ツクモ東京地区 店舗BLOG</a></p> <p>あまりに長いこと自作マシンから距離を置いていたせいで全然知らなかった…<br /> パーティションテーブルが MBR なら Legacy BIOS ブート、 GPT なら UEFI ブートにしようみたいなところで知識が止まっていたぞ。</p> <p>MBR でのブート対応しない Windows 11 に今後更新することを想定すれば、 GPT に変更しておくべきだろう。<br /> どうせ今回は 旧SATA SSD から 新NVMe SSD へのパーティションのクローンもしなくてはならないので、 MBR から GPT への切替をしながらクローンを進める方向で考える。</p> <p>ところが、 色々調べても MiniTool, EaseUS, AOMEI の広告記事の機械翻訳といったゴミ情報ばかりでなかなか情報にたどり着けず。<br /> ChatGPT や "新しい Bing" に聞いてみても、残念ながらあんまり役に立つ回答が得られなかったし。</p> <h2 id="兎にも角にもクリーンインストール"><a href="#%E5%85%8E%E3%81%AB%E3%82%82%E8%A7%92%E3%81%AB%E3%82%82%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">兎にも角にもクリーンインストール</a></h2> <p>UEFI ベースのデバイスに GUIDパーティションテーブル (GPT) の ファイルシステムで Windows を起動させる場合、 EFI システムパーティション (ESP) というものが必要になる。<br /> こいつは MBR では存在しなかったものなので、別途作ってやらなければならない。</p> <p>また、 Windows 側でパーティション管理しやすくするため、 Microsoft 予約パーティション (MSR) というものも必要らしい。</p> <p><a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/windows-hardware/manufacture/desktop/configure-uefigpt-based-hard-drive-partitions?view=windows-11">UEFI/GPT ベースのハード ドライブ パーティション | Microsoft Learn</a></p> <p>ちまちまパーティションを作るのは面倒なので、 新NVMe SSD の中身を綺麗さっぱり削除し、 Windows 10 のクリーンインストール → 旧SATA SSD の Windows パーティションで上書き ……という方法を取る。</p> <ol> <li>Ventoy に Windows 10 インストーラ ISO イメージを書き込んでブート</li> <li>手順に従って、 Windows 10 を新規インストール <ul> <li>ココで入れた Windows はすぐ消すので、プロダクトキーは入れずに進めて良い</li> </ul></li> <li>起動するパーティションを覚えさせるため、インストール完了後一度 Windows を起動させる</li> <li>再び Ventory に入れた GParted をブート</li> <li>旧SATA SSD の Windows パーティションを、 新NVMe SSD の Windows パーティションにコピー<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/04/mbr-partition-table-to-gpt-02.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/04/mbr-partition-table-to-gpt-02.jpg" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/04/mbr-partition-table-to-gpt-01.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/04/mbr-partition-table-to-gpt-01.jpg" alt="" /></a> <ul> <li>コピーの方向逆にするとデータが全部飛ぶので要注意</li> </ul></li> </ol> <p>うまく行けば、この状態で再起動すれば問題なく Windows が起動するだろう。</p> <h2 id="それでもブルースクリーンが出る場合"><a href="#%E3%81%9D%E3%82%8C%E3%81%A7%E3%82%82%E3%83%96%E3%83%AB%E3%83%BC%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%81%8C%E5%87%BA%E3%82%8B%E5%A0%B4%E5%90%88">それでもブルースクリーンが出る場合</a></h2> <p>少なくとも、上記の手順で ESP はブートするはずなので、悪くてもブルースクリーンぐらいは出るはずだ。</p> <p>ここでもしエラーコード <code>0xc000000e</code> などが出ている場合、 ESP 内の Boot Configuration Data (BCD) や M/B の NVRAM にあるブートオプションが、次に起動する Windows パーティションを適切に指せていない可能性がある。</p> <p>この場合、回復コンソール上で <code>echo list volume | diskpart</code> と実行して Windows が入っているとみられるパーティションのドライブレターを確認し、 (ラベルやサイズで判断する)</p> <p>```text/plain<br /> X:\Sources>echo list volume | diskpart</p> <p>Microsoft DiskPart バージョン 10.0.19041.964</p> <p>Copyright (C) Microsoft Corporation.</p> <p>Volume ### Ltr Label Fs Type Size Status Info</p> <hr /> <p>Volume 0 C Windows NTFS Partition 461 GB Healthy<br /> Volume 1 BOOT FAT32 Partition 499 MB Healthy<br /> Volume 2 Recovery NTFS Partition 4881 MB Healthy Hidden</p> <pre><code><br />その結果例えばドライブレターが `C` であれば以下のように実行し、 「ブート ファイルは正常に作成されました。」 と結果が出れば OK なはずだ。 </code></pre> <p>bcdboot C:\Windows /l ja-JP<br /> ```</p> <h2 id="終わりに"><a href="#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB">終わりに</a></h2> <p>以上が、 Windows メディアや OSS の無料のツールだけを使って、 MBR のディスクから GPT のディスクにパーティションをクローンする方法だ。</p> <p>もう流石に、今後 MBR を触ることはないかな…</p> <h2 id="余談1 (Ventoy)"><a href="#%E4%BD%99%E8%AB%871+%28Ventoy%29">余談1 (Ventoy)</a></h2> <p>本文中にも出てくるが、 <a target="_blank" rel="nofollow noopener" href="https://www.ventoy.net/en/index.html">Ventoy: A New Bootable USB Solution</a> が便利すぎる。</p> <p>PC を構成しているとどうしても、パーティション弄るツールやインストールメディアなど、複数のブータブルメディアを使う羽目になってくる。<br /> 昔は種類ごとに CD/DVD を焼いたり、 USBメモリを焼き込んだりしていたもので、使い分ける際もいちいち本体に入れ直さなくてはならないなど、面倒この上なかった。</p> <p>Ventoy をインストールした USBメモリ を一つ用意しておけば、そのメモリの exFAT パーティションに好きな iso ファイルを複数放り込んで、簡単にブート仕分けることができる。</p> <p>ブートするツールを更新する際も、 iso ファイルを入れ替えれば良いので、最新のツールに更新する敷居も下がってとても良い。</p> <h2 id="余談2 (光ディスクの劣化)"><a href="#%E4%BD%99%E8%AB%872+%28%E5%85%89%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E3%81%AE%E5%8A%A3%E5%8C%96%29">余談2 (光ディスクの劣化)</a></h2> <p>Ventoy を使いだす前、昔焼いた ブータブルCD/DVD 使ってパーティション弄ろうとしていた。<br /> しかし、流石に5年以上前にしかも安物のディスクに焼いたものだったので、ディスクのエラー率高くて時折ブートに失敗してしまう。</p> <p>普通の色素系ディスクだとこれくらいの年月で駄目になってくるのねー、と実感してしまった。</p> <p>長期とっときたいデータはやはり、選別や再エンコードなどして M-DISC とかに焼いとかないと駄目だなー</p> advanceboy tag:crieit.net,2005:PublicArticle/18387 2023-02-14T12:41:19+09:00 2023-02-15T00:14:13+09:00 https://crieit.net/posts/PowerShell-63eb02df4b3e2 PowerShell で中身がランダムな大きめのファイルを高速に作る <p>色々テストをしていると、ユニークなランダムの中身で、任意サイズのファイルが欲しくなることがままある。</p> <p>デバイスを全てファイルとして扱う Unix や Linux なら、 <code>/dev/urandom</code> 擬似デバイスを使って <code>dd if=/dev/urandom of=out.bin bs=1M count=64</code> みたいに任意サイズでファイルをコピーして作れば良い。</p> <p>しかし API ベースの Windows だとそういうわけにいかないので、何らかのスクリプトから API を呼んでやる必要がある。</p> <p>呼ばせるスクリプトは、 Windows に標準で搭載されていて追加でインストール不要な、毎度おなじみ PowerShell が良いだろう。<br /> 作りたいランダムなファイルのサイズが小さければ、 PowerShell 上で以下のような雑なコードを実行すればよい。</p> <pre><code class="powershell">$Size = 16KB; [byte[]]$bin = &{foreach($i in 1..$Size) { Get-Random -Maximum 255 <span>}</span><span>}</span>; [System.IO.File]::WriteAllBytes((Join-Path (Get-Location) .\out.bin), $bin); </code></pre> <p>しかし、 MiB オーダーを越えてくると、上記のコードでは速度が実用的ではなくなる。</p> <p>と言うことで、出力速度も気にしながら、大きめなランダムなファイルを作成するコードを考えてみる。</p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <pre><code class="powershell">function Make-RandomFile ([Parameter(Mandatory=$true)][string]$OutPath, [Parameter(Mandatory=$true)][long]$FileSize, [int]$ChunkSize = 16MB) { $bin1 = [byte[]]::new($ChunkSize); $bin2 = [byte[]]::new($ChunkSize); $r = [System.Random]::new(); [long]$progress = 0; [int]$nextLen = 0; $fs = [System.IO.File]::Create([System.IO.Path]::Combine($PWD, $OutPath)); try { do { $task = $fs.WriteAsync($bin1, 0, $nextLen); $r.NextBytes($bin2); $task.Wait(); $bin1, $bin2 = $bin2, $bin1; $progress += $nextLen; $nextLen = [System.Math]::Min([long]$ChunkSize, $FileSize - $progress); } while ($nextLen -gt 0); } finally { $fs.Dispose(); } } </code></pre> <p>上記のような関数をコンソールに貼り付けて定義し、あとは以下のように実行すれば良い。</p> <pre><code class="powershell">Make-RandomFile .\out.bin -FileSize 1GB; </code></pre> <h2 id="解説"><a href="#%E8%A7%A3%E8%AA%AC">解説</a></h2> <p>大きなファイルを作る際、最初に挙げたような雑なコードだと、一旦全てのデータをメモリ (RAM) に置いてしまうため、メモリ不足になったり動作が遅くなったりしかねない。<br /> そこで、適当なチャンクサイズ (上記コードだと標準では 16MiB) 毎にランダムなデータを作って、それを書き込んでいる。</p> <p>また、ランダムな配列を作るのも、その配列をファイルに書き込むのも、それぞれ時間がかかるため、それらを同期的に逐次実行すると効率が悪い。<br /> そこでバッファを2つ用意して、片方のバッファでファイルを非同期的に書き込んでいる間に、もう一方のバッファでランダムなデータを生成することで、効率を上げている。</p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <p>ちなみに、 .NET 5 以前の <a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/dotnet/api/system.random">System.Random</a> の擬似乱数生成器は、あまり乱数精度が高くないと言われているので、セキュリティ的に精度の高いランダム性であることが重要な場合は、より乱数精度の高い別の API を使った方が良いかもしれない。</p> <p>とりあえず、ちょっとしたテストファイルでランダムなデータが欲しいだけなら、 System.Random で十分だと思うが。</p> <p>また、ストレージの速度が十分に速い状況でより速く実行させたいなら、 Windows 10/11 標準の Windows PowerShell 5.1 より <a target="_blank" rel="nofollow noopener" href="https://www.microsoft.com/store/apps/9MZ1SNWT0N5D">PowerShell 7.2 以降</a>で実行したほうが、圧倒的に速いはずだ。 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>Windows PowerShell 5.1 では .NET Framework 4.8 が、 PowerShell 7.2 LTS では .NET 6 が、それぞれ使われているが、 その .NET 6 以降では、擬似乱数生成器が性能の良いものに切り替わった為 (<a target="_blank" rel="nofollow noopener" href="https://andantesoft.hatenablog.com/entry/2021/03/28/231632">.NET 6 (Preview) における System.Random の実装変更 - 屋根裏工房改</a>) <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> advanceboy tag:crieit.net,2005:PublicArticle/18380 2023-01-31T23:59:28+09:00 2023-02-01T00:00:20+09:00 https://crieit.net/posts/Git-OneDrive Git と OneDrive 等のクラウドストレージを併用する <p>ブログに書きたいネタとか雑多なメモを管理する際、Git によるバージョン管理と、クラウドストレージによる同期を併用したくなる。</p> <p>具体的には、こんなことがしたい:</p> <ul> <li>コミットに至らないメモ書きの段階では、クラウドストレージで同期させたい <ul> <li>ついでにそのメモ書きは、タブレット等の複数の端末から更新したい</li> </ul></li> <li>ブログで記事を公開してからの差分を Git で管理</li> </ul> <p>プライベートのリモートリポジトリ代わりに、ベアリポジトリを OneDrive に共有する話は見たことある (<a target="_blank" rel="nofollow noopener" href="https://www.mussyu1204.com/wordpress/it/?p=249">これ</a> とか <a target="_blank" rel="nofollow noopener" href="https://note.com/ume_white/n/n825adeb8e04d">これ</a>) のだが、私がやりたいこととはちょっと違うんだよな。<br /> そもそも、 Microsoft 買収後に GitHub で無制限にプライベートリポジトリ作れるようになったので、 Git リポジトリの共有自体は GitHub とかで良いので。</p> <p>以前、ローカルリポジトリをまるごと OneDrive で共有させてみたのだが、複数の端末で編集すると同期の競合が発生しまくってしまった。<br /> 特に <code>.\.git</code> ディレクトリ内でコンフリクトすると後処理が面倒くさすぎる。</p> <p>どうにかならんもんか。</p> <h2><code>.\.git</code> ディレクトリだけ同期除外</h2> <p>先に答えを言ってしまうと、クラウドストレージでの同期で特定のディレクトリの同期を除外させれば良い。</p> <p><code>.\.git</code> ディレクトリ内のコンフリクトが面倒なら、 <code>.\.git</code> ディレクトリだけ同期を除外させればいじゃない… という精神だ。</p> <p>ただ、 OneDrive では一筋縄ではいかなかったので、少々トリッキーな方法を採っている。</p> <p>そのやり方を、 Windows 上の OneDrive を例に紹介する。</p> <h3 id="1つ目の端末"><a href="#1%E3%81%A4%E7%9B%AE%E3%81%AE%E7%AB%AF%E6%9C%AB">1つ目の端末</a></h3> <p>既に OneDrive で管理しているフォルダを、 Git 管理させる方法。</p> <ol> <li>同期させたいディレクトリのルート直下に <code>.\.git</code> フォルダを手動で新規作成する<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-01.png" alt="" /></a></li> <li>OneDrive の設定から、 アカウント → フォルダの選択 から、 <code>.\.git</code> フォルダーを同期の対象外にする<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-02-244x300.png" alt="" /></a> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-03.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-03-300x272.png" alt="" /></a> <ul> <li>ローカルから <code>.\.git</code> フォルダが消える</li> </ul></li> <li><code>git init</code> する <ul> <li><code>.\.git</code> 隠しフォルダが再作成される</li> </ul></li> <li>適当に Git のコミットしたり、 remote の追加・プッシュしたりする</li> </ol> <p>この結果、 Git のリポジトリやステージングのデータを除く、ワーキングツリーのファイルだけをクラウドストレージで同期できる。</p> <p>難点なのが、 OneDrive 上で「同期の問題」として以下のエラーが出続けることだ。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-04.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-04.png" alt="" /></a></p> <blockquote> <p>この名前のファイルまたはフォルダーが、同じ場所にあります。<br /> 両方のバージョンを残すには、この PCまたはオンラインのどちらかでこのアイテムの名前を変更してください。両方のアイテムが同じ場合は、この PCにあるバージョンを削除するとオンラインのバージョンをダウンロードできます。</p> </blockquote> <p>なお、エラーが出ていても同期はされる。</p> <h3 id="2つ目の端末"><a href="#2%E3%81%A4%E7%9B%AE%E3%81%AE%E7%AB%AF%E6%9C%AB">2つ目の端末</a></h3> <p>上記と同様、 OneDrive でワーキングツリーを同期させつつ、 <code>.\.git</code> ディレクトリは同期させないフォルダを、別のマシンに構築する方法。</p> <ol> <li>一旦、対象ディレクトリを「このデバイス上で常に保持する」を実行して、ファイルをローカルにおいておく</li> <li>OneDrive の設定から、 アカウント → フォルダの選択 から、 <code>.\.git</code> フォルダーを同期の対象外にする <ul> <li>1つ目の端末の設定で、既に空の <code>.\.git</code> フォルダが存在しているはず</li> <li>設定後ローカルから <code>.\.git</code> フォルダが消える</li> </ul></li> <li>同期を一時停止</li> <li><p>clone の替わりに fetch と reset を使い、ワーキングツリーはそのままリポジトリだけ更新する</p> <pre><code class="shell">$ git init $ git remote add origin [email protected]:username/reponame.git $ git fetch origin master $ git reset origin/master </code></pre></li> <li><p>同期を再開させる</p></li> </ol> <h2 id="リポジトリの同期"><a href="#%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E5%90%8C%E6%9C%9F">リポジトリの同期</a></h2> <p>片方で push した後もう一方で pull しようとしても、先に OneDrive の同期によってワーキングツリーのファイルが更新されてしまっているので、 pull がエラーになってしまう。</p> <p>こればかりはどうしようもないので、2つ目の端末のセットアップと同様、以下のように <strong>ワークツリーを維持したまま、</strong> リポジトリとステージングを (<code>--mixed</code> オプション相当で) リセットしてやれば良い。</p> <pre><code>$ git fetch origin master $ git reset origin/master </code></pre> <p>こんな感じで、期待していた環境が整った。</p> <p>Android からは Jota+ あたりを使って OneDrive 上のファイルを直接更新したりしながら活用している。</p> <h2 id="【参考】: OneDrive で特定のファイル名パターンのファイルをアップロードから除外する方法"><a href="#%E3%80%90%E5%8F%82%E8%80%83%E3%80%91%3A+OneDrive+%E3%81%A7%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%8B%E3%82%89%E9%99%A4%E5%A4%96%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95">【参考】: OneDrive で特定のファイル名パターンのファイルをアップロードから除外する方法</a></h2> <p>ちなみに、グループポリシーやレジストリをいじることで、「特定の種類のファイルをアップロードから除外」する事ができる (<a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/sharepoint/use-group-policy#exclude-specific-kinds-of-files-from-being-uploaded">OneDrive ポリシーを使用して同期設定を制御する - SharePoint in Microsoft 365 | Microsoft Learn</a>)。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-05.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-05.png" alt="" /></a></p> <p>が、残念ながらフォルダに対してはこの機能が働かないので、上記の用途を満たさない。</p> <p>参考までに、やり方だけ下記に示しておく。</p> <h3 id="レジストリを弄る場合:"><a href="#%E3%83%AC%E3%82%B8%E3%82%B9%E3%83%88%E3%83%AA%E3%82%92%E5%BC%84%E3%82%8B%E5%A0%B4%E5%90%88%3A">レジストリを弄る場合:</a></h3> <ol> <li><code>HKLM\SOFTWARE\Policies\Microsoft\OneDrive\</code> の下に <code>EnableODIgnoreListFromGPO</code> というキーを作る<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-06.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/01/onedrive-git-together-06.png" alt="" /></a></li> <li>その中に「除外したいファイル名」を名前と値の両方に持った、文字列値(複数可)を入れる</li> <li>PC を再起動する</li> </ol> <h3 id="グループポリシーを使う場合:"><a href="#%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88%3A">グループポリシーを使う場合:</a></h3> <ol> <li>「グループ ポリシーの編集」を使える状態にする <ul> <li>Windows の Pro エディション以上なら標準では言っている。 Home エディションなら、ググって調べて。</li> </ul></li> <li><code>C:\Users\N2633\AppData\Local\Microsoft\OneDrive\<ビルド番号>\adm\</code> 内の以下のファイルをコピーする <ul> <li><code>.\OneDrive.admx</code> を <code>C:\Windows\PolicyDefinitions\</code> へ</li> <li><code>.\ja\OneDrive.adml</code> <code>を C:\Windows\PolicyDefinitions\ja-JP\</code> へ</li> </ul></li> <li>Win+R で <code>gpedit.msc</code> を入力し「ローカル グループ ポリシー エディター」を立ち上げる</li> <li>[コンピューターの構成] → [管理用テンプレート] → [OneDrive] → [特定の種類のファイルをアップロードから除外] を選択</li> <li>構成を有効にして、キーワード一覧に除外したいファイル名(アスタリスク可)を設定する</li> </ol> advanceboy tag:crieit.net,2005:PublicArticle/18357 2022-12-29T16:47:37+09:00 2022-12-29T16:47:37+09:00 https://crieit.net/posts/Docker-run-bash-ash Docker run の起動時に任意コードを実行後 bash や ash を終了しない <p>この記事は、 <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2022/docker">Docker Advent Calendar 2022</a> の 23日目の記事だ。<br /> 空いていたので埋めちゃうよ。</p> <p>この記事では、 bash や ash で任意のコードの実行後、ターミナルを終了せずに入力待ちにする方法について紹介する。<br /> 特に、 <code>docker run</code> の実行後に、その環境を維持したまま入力待ちにすることを考える。</p> <p>例えば、 Windows コマンドプロンプトや PowerShell であれば、 <code>CMD /K ***</code> オプションや、 <code>-NoExit -Command ***</code> オプションで実現できるような内容だ。</p> <p>本来なら、 docker build にてその任意コードの実行後の内容をイメージにするべきだが、 わざわざ build するまでもないとか、 build できない事情などもあるかもしれない。</p> <p>ということで、 bash の場合と、 alpine などで使われる BusyBox ash それぞれについて、 <code>docker run</code> 実行時に任意コード実行後、ターミナルの入力待ちにする方法を紹介する。</p> <h2 id="bash の場合"><a href="#bash+%E3%81%AE%E5%A0%B4%E5%90%88">bash の場合</a></h2> <p>bash の場合、 <code>--rcfile</code> オプションにて、起動時に実行するコマンドを指定できる。</p> <p>ただし、 <code>--rcfile</code> はファイルを指定する必要があるため、 替わりに<strong>プロセス置換</strong>で実行コードを与えてやる方法をとる。</p> <pre><code class="console">user@hostmachine:~$ docker run --rm -it debian:bullseye bash -c "bash --rcfile <(echo 'ls && export '\''FOO=B A R'\'' && MY_TIME=\$(date)')" bin dev home lib64 mnt proc run srv tmp var boot etc lib media opt root sbin sys usr root@container:/# echo $FOO : $MY_TIME B A R : Thu Dec 22 15:00:00 UTC 2022 root@container:/# </code></pre> <p>プロセス置換はコンテナ内で実行される必要があるため、一旦 <code>bash -c</code> にてコンテナ内で bash 実行させ、その中で改めて <code>--rcfile</code> オプションを指定した bash を起動する流れとなる。</p> <p>実際に実行したいコマンドは、 echo で文字列として書き出す。<br /> 上記例では、 <code>ls && export 'FOO=B A R' && MY_TIME=$(date)</code> と言う文字列を echo させている。</p> <p>引用符が二重三重になっていて、エスケープが非常に難しくなっているので注意。</p> <h2 id="BusyBox ash"><a href="#BusyBox+ash">BusyBox ash</a></h2> <p>alpine 3.15 以降に含まれる BusyBox の ash であれば、意外にもプロセス置換が使える。</p> <p>しかし、 <code>--rcfile</code> に相当するオプションは残念ながら無い。<br /> 替わりに、 ash には <code>ENV</code> という環境変数に記載されたファイルを ash 起動時に実行する機能がある。</p> <p>これを使おう。</p> <pre><code class="console">user@hostmachine:~$ docker run --rm -it alpine:3.15 ash -c "ash -c 'export ENV=\$1;ash' -s <(echo 'ls && export '\''FOO=B A R'\'' && MY_TIME=\$(date)')" bin etc lib mnt proc run srv tmp var dev home media opt root sbin sys usr / # echo $FOO : $MY_TIME B A R : Thu Dec 22 15:00:00 UTC 2022 / # </code></pre> <p><code>ash</code> の <code>-c</code> のコマンドに対して引数を与える <code>-s</code> オプションを使ってプロセス置換のファイルを与え、 それを <code>$1</code> 経由で <code>ENV</code> 環境変数にセット。 その状態で再度 ash を起動させれば、 bash と同様のことが行える。</p> <p>ENV 環境変数に直接プロセス置換のファイルを指定せず、わざわざ一旦引数を経由させているのは、 入力を受け付ける ash プロセスが動いている間、 プロセス置換のファイルにアクセス可能にする必要があるためだ。<br /> 例えば、 <code>export ENV=<(echo 'command');ash</code> と実行しても、 ash 実行の段階ではプロセス置換のファイルが閉じられているので、コマンドは実行されない。</p> <h2 id="もうちょっと複雑な例"><a href="#%E3%82%82%E3%81%86%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E8%A4%87%E9%9B%91%E3%81%AA%E4%BE%8B">もうちょっと複雑な例</a></h2> <p>起動時に apt パッケージマネージャーのリポジトリを書き換える方法(Ubuntu):</p> <pre><code>docker run --rm -it ubuntu:22.04 bash -c "bash --rcfile <(echo 'sed -i -E '\''s%^(deb(-src|)\s+)https?://(archive|security)\.ubuntu\.com/ubuntu/%\1http://srv2.ftp.ne.jp/Linux/packages/ubuntu/archive/%'\'' /etc/apt/sources.list && apt update && FooBar=`date -uIs`')" </code></pre> <p>起動時に apk パッケージマネージャーのリポジトリを書き換える方法(Alpine):</p> <pre><code>docker run --rm -it alpine:3.15 ash -c "ash -c 'export ENV=\$1;ash' -s <(echo 'sed -i -E '\''s%^https?://dl-cdn\.alpinelinux\.org/alpine/%https://ftp.udx.icscoe.jp/Linux/alpine/%'\'' /etc/apk/repositories && apk update && FooBar=`date -uIs`')" </code></pre> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/74094552/how-not-to-terminate-after-carried-out-commands-in-bash">https://stackoverflow.com/questions/74094552/how-not-to-terminate-after-carried-out-commands-in-bash</a></p> advanceboy tag:crieit.net,2005:PublicArticle/18356 2022-12-28T23:48:38+09:00 2022-12-28T23:48:38+09:00 https://crieit.net/posts/GNS3-VyOS-63ac5746e74c7 GNS3 に VyOS 仮想ルーターを追加する長い道のり④ 応用編 <p>本記事は <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2022/nw-engineering-are-core-output">【アットホームな現場です】🎄★☆ネットワーク系エンジニア★☆アレコレアウトプット★☆🎄 Advent Calendar 2022</a> 25日目の記事だ。<br /> ごめんなさい、色々書いてたら結局分量が多くなって期限内に書き切れなかった。</p> <p><a target="_blank" rel="nofollow noopener" href="https://gns3.com/">GNS3</a> という OSS のネットワークエミュレータを、 Cisco の IOS などの取得なしに、無料のライセンス内で利用できるようにしようという話。</p> <p>今回は最終回の応用編。<br /> 以前の投稿はこちら。<br /> <a href="https://crieit.net/posts/GNS3-VyOS">https://crieit.net/posts/GNS3-VyOS</a> (① 環境導入編)<br /> <a href="https://crieit.net/posts/GNS3-VyOS-63ac561ed5a75">https://crieit.net/posts/GNS3-VyOS-63ac561ed5a75</a> (② イメージ準備編)<br /> <a href="https://crieit.net/posts/GNS3-VyOS-63ac5693838af">https://crieit.net/posts/GNS3-VyOS-63ac5693838af</a> (③ 実践編)</p> <h2 id="目指すゴール"><a href="#%E7%9B%AE%E6%8C%87%E3%81%99%E3%82%B4%E3%83%BC%E3%83%AB">目指すゴール</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-00.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-00.png" alt="" /></a><br /> 今回は、上記の図のように、 ルーターを 2台 置いて、 ルーターを跨いだ端末間 (<code>tmp-net-tools-1</code>, <code>-2</code>) で通信ができるように構成していく。</p> <h2 id="相互通信するコンテナの準備"><a href="#%E7%9B%B8%E4%BA%92%E9%80%9A%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E6%BA%96%E5%82%99">相互通信するコンテナの準備</a></h2> <p>ルーターを跨いだ通信を実際に行うことになるコンテナを準備する。</p> <p>通信の確認を行う最低限のモジュール(パッケージ)をインストールしたイメージを用意する。</p> <p>(リスクを自分で評価しつつ)第三者が用意したイメージを使っても良いのだが、今回は自分でビルドしてみよう。</p> <ol> <li>まず、 VM管理ツールや ssh で GNS3 VM の管理画面に入る</li> <li>メニューから Shell を選択する<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-trouble-01-03.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-trouble-01-03.png" alt="" /></a></li> <li><p>以下のコマンドで、 <code>tmp-net-tools</code> という名前をつけた Docker イメージをビルドする<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-01.png" alt="" /></a></p> <ul> <li> <br /> <code>shell gns3@gns3vm:~$ docker build -t tmp-net-tools - FROM alpine:latest RUN apk add --no-cache curl iperf3 net-tools EOF</code></li> </ul></li> <li><p>GNS3 (GUI) の Preferences を開き、 Docker コンテナテンプレートで上記のビルド済み <code>tmp-net-tools</code> でテンプレートを作成する。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-02.png" alt="" /></a></p> <ul> <li>Console type は telnet で OK.</li> </ul></li> </ol> <p>ここでポイントとなるのは、 GNS3 で動かすコンテナは基本的に(明示的に接続市内限り)外部ネットワークに繋がっていないが、 GNS3 VM で Docker をビルドする際は、外部ネットワークに繋がると言う点だ。<br /> このため、コンテナ内にて追加で必要なパッケージがある場合、このように予めコンテナの build 段階でインストールしておく必要がある。</p> <h2 id="ネットワークの構成 1"><a href="#%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AE%E6%A7%8B%E6%88%90+1">ネットワークの構成 1</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-00.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-00.png" alt="" /></a><br /> コンテナテンプレートの準備ができたら、ペタペタとデバイスを貼って結線していく。<br /> 2つの VyOS とも、 eth0 を端末のコンテナ側に、 eth1 をルーター同士を繋ぐ Switch に繋いでいこう。</p> <p>本来 VyControl は、 1サービスで複数の VyOS を管理できるはずなのだが、 試した感じどうも管理対象の切り替えでエラーになってしまうので、 素直に VyOS と VyControl を 1対1 で作成している。<br /> ただ、そうするとホストPCからそれぞれの VyControl にアクセスする際に同じ IP アドレスとなって Cookie が衝突するため、片方にログインするともう一方からログアウトされてしまう問題があるので注意。</p> <p>また、 vycontrol-host や tmp-net-tools の各コンテナにて、 Edit config からの <code>/etc/network/interfaces</code> の編集で、 eth0 を DHCP から IP アドレスを取得するよう設定することを忘れずに。</p> <p>VyOS は前回の設定に加えて eth1 の IP アドレスを静的に決めるため、それぞれ以下のように設定する。</p> <p><code>VyOS1.3.2-1</code>:</p> <pre><code class="shell">vyos@vyos:~$ configure vyos@vyos# set interfaces ethernet eth0 address '192.168.11.254/24' vyos@vyos# set interfaces ethernet eth1 address '192.168.1.1/24' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.11.0/24' default-router '192.168.11.254' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.11.0/24' name-server '192.168.11.254' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.11.0/24' range 0 start '192.168.11.17' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.11.0/24' range 0 stop '192.168.11.126' vyos@vyos# set service https api keys id my_id key 'my_secret_key' vyos@vyos# set service https certificates system-generated-certificate lifetime '65535' vyos@vyos# set service https virtual-host vyos1 listen-address '192.168.11.254' vyos@vyos# set service https virtual-host vyos1 listen-port '6443' vyos@vyos# set service https virtual-host vyos1 server-name 'vyos1.example.com' vyos@vyos# commit vyos@vyos# save vyos@vyos# exit </code></pre> <p><code>VyOS1.3.2-2</code>:</p> <pre><code class="shell">vyos@vyos:~$ configure vyos@vyos# set interfaces ethernet eth0 address '192.168.21.254/24' vyos@vyos# set interfaces ethernet eth1 address '192.168.1.2/24' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.21.0/24' default-router '192.168.21.254' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.21.0/24' name-server '192.168.21.254' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.21.0/24' range 0 start '192.168.21.17' vyos@vyos# set service dhcp-server shared-network-name 'subnet01' subnet '192.168.21.0/24' range 0 stop '192.168.21.126' vyos@vyos# set service https api keys id my_id key 'my_secret_key' vyos@vyos# set service https certificates system-generated-certificate lifetime '65535' vyos@vyos# set service https virtual-host vyos1 listen-address '192.168.21.254' vyos@vyos# set service https virtual-host vyos1 listen-port '6443' vyos@vyos# set service https virtual-host vyos1 server-name 'vyos1.example.com' vyos@vyos# commit vyos@vyos# save vyos@vyos# exit </code></pre> <p>ちなみに、 VyOS の仮想デバイスは、 一通り設定を完了させてから、仮想デバイスを複製できる。<br /> VyOS のインストールなどの作業が 1回 で済む点は便利なのだが、 コピー後 MAC アドレスが変更になるため、仮想デバイス内のインターフェース名が、 eth0, eth1, eth2 から eth3, eth4, eth4 へと名前を変えてしまうことが多く、 GNS3 UI 上の NIC のインターフェース名と、 仮想デバイス内の名前が一致しなくなってややこしい事になるので、ご注意を。</p> <h2 id="ネットワークの構成 2"><a href="#%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AE%E6%A7%8B%E6%88%90+2">ネットワークの構成 2</a></h2> <p>さて、ここで各端末 (<code>tmp-net-tools-1</code>, <code>-2</code>) が DHCP サーバーに割り当てられた IP アドレスを使って、お互いに ping を打っても、当然届かず失敗する。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-03.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-03.png" alt="" /></a></p> <p>ここで VyControl を使って、 それぞれの VyOS に静的ルーティングを定義する。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-05.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-05.png" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-06.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-06.png" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-07.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-07.png" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-08.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-08.png" alt="" /></a></p> <p>その後改めてお互いに ping を打つと、無事疎通ができるようになった。<br /> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-04.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-04.png" alt="" /></a></p> <h2 id="外部との接続"><a href="#%E5%A4%96%E9%83%A8%E3%81%A8%E3%81%AE%E6%8E%A5%E7%B6%9A">外部との接続</a></h2> <p>さらなる応用として、 "Cloud" デバイスを使って外部機器との接続もできる。</p> <p>まず、適当な物理NIC (USB LAN アダプタ) を、仮想マシン管理ツール側で GNS3 VM のネットワークアダプタに割り当てる。<br /> その後、 GNS3 内で "Cloud" デバイスをその物理NIC に割り当ててやれば、物理ルーターと仮想ルーターとの間で、論理的なネットワークの接続を構成できる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-09.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2022/12/gns3-applied-09.png" alt="" /></a></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>…とまあ、こんなところが GNS3 の基本的な使い方となる。<br /> 慣れてきたら、不完全な VyControl なんか使わず、 直接 VyOS に CUI コマンドで設定を定義してやると良いかもしれない。</p> <p>長々となる解説となったが、誰かの役に立てば幸いだ。</p> advanceboy