jupyterlab-wav extensionをjupyterlab 3.xへ対応させた上でWaveSurfer.jsで波形・スペクトログラムが描画できるようにした

この記事では以前にこのポストで作成したjupyterlab-wavのExtensionを WaveSurfer.jsを用いて波形+スペクトログラムが描画できるように今年の2月ごろに改良したのでその過程について説明します。 (この文章自体3月に書いたまま放置していたため内容として古くなっている部分があるかもしれません)。

jupyterlab 3.x系への対応

Jupyter blogでも紹介されているようにJupyterlab 3.0が2021年の年始にリリースされました。 jupyterlab 3.x系ではこのポストでも触れたDebuggerが導入されたなどの話もありますが、 extension開発者にとって大きな変更点としてJupyterLab extensions can now be distributed as prebuilt extensions, which do not require a user to rebuild JupyterLab or have Node.js installed. とあるように、prebuilt extensionをPyPIなどに登録することによってpipやcondaでextensionをインストールできるようになったことが挙げられます。 これにより、npmやjupyterによるローカルでのビルドなどが不要となったためユーザーはより気軽にExtensionを導入できるようになりました (後述のように従来通りnpmjsからインストールすることも可能です)。

そんな訳でjupyterlab-wavもpipでインストールできるようになりました。

$ pip install jupyterlab-wav

で簡単に導入できます。

WaveSurfer.jsの導入

以前のポストで作成したjupyterlab-wavはMIME Rendererのテンプレートをそのまま使っただけのものでしたが、 今回の更新にあたりWaveSurfer.jsを用いて波形・スペクトログラムが描画できるようになりました。 また、wavファイルだけでなくmp3ファイルとflacファイルも再生できるようになりました。

screenshot

マルチチャンネルファイルのスペクトログラムが描画できないなど、まだまだ不具合などは残っていますが最低限使えるような状態にはなっています。

jupyterlab extensionの2.x系から3.x系への更新作業

2.x系から3.x系へとextensionを更新する際には Extension Migration Guideを見ながら作業を行いました。

手動でpackage.jsonのバージョンを上げる場合

下記の@jupyterlab/applicationのバージョンを^3.0.0へと変更すれば良いとのこと。

  "dependencies": {
          "@jupyterlab/application": "^3.0.0",

しかしjupyterlab 3.0には前述のようにextensionをPyPIconda-forgeにアップロードすることでpipcondaでextensionがインストールできるようになり、 そのためのパッケージングの仕組みが提供されているため、下記手順に沿って対応した方が良いと思われます。

upgradeスクリプトを用いた更新

Jupyterlab 3.0はupgrade用のスクリプトを公開しているのでこれを用いてバージョンを上げたいと思います。 まずpipでjupyter-packagingcookiecutterをインストールします。

$ pip install jupyterlab -U
$ pip install jupyter-packaging cookiecutter

上記を実行した上でextensionのルートディレクトリで下記コマンドを実行すると対話的に各種項目を設定することができます。 python_name [wrist_jupyterlab_wav]:の部分はpythonパッケージ名となり、ここではデフォルトのまま実行していましたが、最終的には各種設定を手動でjupyterlab_wavに書き直しています。

$ python -m jupyterlab.upgrade_extension .
author_name [wrist <stoicheia1986@gmail.com>]:
python_name [wrist_jupyterlab_wav]:
labextension_name [@wrist/jupyterlab-wav]:
project_short_description [A JupyterLab extension for rendering wav files.]:
has_server_extension [n]:
has_binder [n]:
repository [https://github.com/wrist/jupyterlab-wav]:
overwrite scripts in package.json? [n]:
overwrite ".gitignore"? [n]:
overwrite "README.md"? [n]:
overwrite "tsconfig.json"? [n]:
overwrite "src/index.ts"? [n]:
overwrite "style/index.css"? [n]:
** package.json scripts must be updated manually
** skipped _temp_extension/.gitignore
** skipped _temp_extension/README.md
** skipped _temp_extension/tsconfig.json
** skipped _temp_extension/src/index.ts
** skipped _temp_extension/style/index.css
** Remove _temp_extensions directory when finished
(

上記を実行すると、package.jsonが下記のように上書きされます。

(base) jovyan@188a4999b58c:/mnt/work/python/tmp/jupyterlab-wav$ git diff
diff --git a/package.json b/package.json
index 0feeaed..cf1620b 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,11 @@
   "description": "A JupyterLab extension for rendering wav files.",
   "author": "wrist <stoicheia1986@gmail.com>",
   "homepage": "https://github.com/wrist/jupyterlab-wav",
-  "repository": {"type": "git", "url": "https://github.com/wrist/jupyterlab-wav"},
-  "license" : "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/wrist/jupyterlab-wav"
+  },
+  "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "style": "style/index.css",
@@ -16,27 +19,38 @@
   ],
   "files": [
     "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
-    "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
+    "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
+    "style/index.js"
   ],
   "jupyterlab": {
-    "mimeExtension": true
+    "mimeExtension": true,
+    "outputDir": "wrist_jupyterlab_wav/labextension"
   },
   "scripts": {
-    "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
     "build": "tsc",
-    "prepare": "npm run clean && npm run build",
-    "watch": "tsc -w",
+    "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
+    "extension:disable": "jupyter labextension disable jupyterlab-wav",
+    "extension:enable": "jupyter labextension enable jupyterlab-wav",
     "extension:install": "jupyter labextension install jupyterlab-wav",
     "extension:uninstall": "jupyter labextension uninstall  jupyterlab-wav",
-    "extension:enable": "jupyter labextension enable jupyterlab-wav",
-    "extension:disable": "jupyter labextension disable jupyterlab-wav"
+    "prepare": "npm run clean && npm run build",
+    "watch": "tsc -w"
   },
   "dependencies": {
-    "@jupyterlab/rendermime-interfaces": "^2.0.0",
-    "@lumino/widgets": "^1.5.0"
+    "@jupyterlab/rendermime-interfaces": "^3.0.2",
+    "@lumino/widgets": "^1.16.1"
   },
   "devDependencies": {
-    "rimraf": "^2.6.3",
-    "typescript": "~3.7.0"
-  }
-}
+    "@jupyterlab/builder": "^3.0.0",
+    "@typescript-eslint/eslint-plugin": "^4.8.1",
+    "@typescript-eslint/parser": "^4.8.1",
+    "eslint": "^7.14.0",
+    "eslint-config-prettier": "^6.15.0",
+    "eslint-plugin-prettier": "^3.1.4",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.1.1",
+    "rimraf": "^3.0.2",
+    "typescript": "~4.1.3"
+  },
+  "styleModule": "style/index.js"
+}

各種依存ライブラリのバージョンが更新されていることが分かります。 @jupyterlab/builderdevDependencyに追加されているが、これはfederated extensionとしてextensionをビルドするために必要となるものです。 これはwebpackのような依存を内部に隠蔽し、pythonパッケージの一部として配布可能なassetを生成するものとのことです。 extension開発においては直接@jupyterlab/builderを対話的に操作することはないが、その代わりにjupyter labextension buildコマンドを用いることができます。 このコマンドはビルドスクリプトjlpm run buildの一部として自動的に実行されます。

また、python -m jupyterlab.upgrade_extension .の実行によりパッケージングに必要となるsetup.pypyproject.tomlなどが生成されています。 実際に実行したところ、下記のファイル/ディレクトリ群が追加されていました。

.eslintignore
.eslintrc.js
.github/
.prettierignore
.prettierrc
LICENSE
MANIFEST.in
_temp_extension/
install.json
pyproject.toml
setup.py
style/base.css
style/index.js
wrist_jupyterlab_wav/

eslintは構文チェック、prettierはフォーマッタであり、それらの設定ファイルが追加されています。 LICENSEは自動的に追加されていたが中身は3条項BSDライセンスでした。元々存在していたpackage.jsonにはLicenseをMITと記載していましたが、これを受けてBSD3-clauseに修正しています。

.github以下にはgithub actionsで使用するworkflow定義のymlファイルが格納されています。mainブランチに対して動作するため、今回はmasterに対して発動するように修正しています。

また、実際にgithubにpushしてactionsのタブを見るとbuild時にエラーが出ていました、これはpackage.jsonscriptseslint:checkが存在していないためでした。 ここでは下記のscirptsを追加した上で、実際にeslintを実行して指摘箇所の修正を行いました。

   "eslint": "eslint . --ext .ts,.tsx --fix",
   "eslint:check": "eslint . --ext .ts,.tsx",

ここに限らずmigrateの過程でpackage.jsonにscriptが全て追加されておらず、build:prodのscriptがないために生じたエラーなどにも遭遇しましたが、 最終的にはhttps://github.com/jupyterlab/extension-cookiecutter-tsのリポジトリのpackage.jsonに記載のscriptを追加することで解決しています。

extensionのローカルインストール

過去に書いた記事では下記のようにビルドを実行していましたが、

$ jlpm install
$ jlpm run build
$ jupyter labextension install . --no-build
$ jupyter lab

テンプレートディレクトリのREADME.mdに記載されている下記手順のように実行することでローカルでのテスト用にextensionをjupyterlabにリンクできます。

$ pip install -e .
$ jupyter labextension develop . --overwrite
$ jlpm run build

jlpmによるビルドが終了した後にjupyterlabを立ち上げるとextensionが有効化されているはずです。

jupyterlab-wavの修正

今回の作業のついでにjupyterlab-wavを改良しています。 前述のように波形可視化ライブラリであるwavesurfer.jsを追加し、これを用いて描画を試みていますが、 下記のように最初にjlpmでパッケージをいくつか追加しています。

$ jlpm add wavesurfer.js
$ jlpm add @types/wavesurfer.js
$ jlpm add colormap

react-widgetの導入

以前のMIME Extensionのテンプレートを改造しただけのプロジェクトではIRenderMime.IRendererを実装するためのクラスを作り、 ファイルをjupyterlabで開いた際に呼ばれるrenderModelメソッドを定義してaudio要素にsrc属性を設定していただけでしたが、 今回の修正ではreact-widgetを下記コードのように導入しています。

import { ReactWidget } from '@jupyterlab/apputils';
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';

import React from 'react';
import AudioComponent from './AudioComponent';

const CLASS_NAME = 'mimerenderer-wav';

/**
 * A widget for rendering wav.
 */
export class WavWidget extends ReactWidget implements IRenderMime.IRenderer {
  constructor(options: IRenderMime.IRendererOptions) {
    super();
    this._mimeType = options.mimeType;
    this._src = '';
    this.addClass(CLASS_NAME);
  }

  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
    const data = model.data[this._mimeType] as string;
    this._src = `data:${this._mimeType};base64,${data}`;

    this.update();

    return Promise.resolve();
  }

  render() {
    return <AudioComponent src={this._src} />;
  }

  private _src: string;
  private _mimeType: string;
}

jupyterlab上でファイルを開いた際にrenderModelメソッドが呼ばれる点は同じですが、 この際にthis.update()を実行することでReactWidgetをextendsする際に必須となるrenderメソッドを明示的に呼び出しています。 renderメソッドは<AudioComponent ...>のJSXタグを返しますが、このAudioComponentAudioComponent.tsx内で定義されたReactコンポーネントとなります。 Reactコンポーネント自体はjupyterlabと独立にReact単体のエコシステム上で別途動作検証やデバッグができるため、 ReactWidgetの導入によりextension開発が容易になりました。

extensionの公開方法

パッケージングの方法についてはextension tutorialのpackagingの部分に、 アップロードの方法についてはextension tutorialのpublishに関する部分に説明があります。

PyPIへのアップロード

自動生成されたsetup.pyを用いてパッケージのビルドを行います。

$ python setup.py build sdist
$ python setup.py build bdist_wheel

ビルドの成果物がdist以下に保存されるので、これらをPyPIへとアップロードします。 ここではtwineを使ってアップロードを行います。 予めTestPyPIにアップロードし問題ないことを確認した上でPyPIへとアップロードします。

$ pip install twine
$ twine upload --repository-url https://test.pypi.org/legacy/ dist/*
$ twine upload dist/*

npmjsへのアップロード

extensionはPyPIやconda-forgeにアップロードするだけでなく、従来のようにnpmへとアップロードすることもできます。 これはnpmパッケージとして配布することでユーザーがJupyterLab 1.xや2.xと同様にextensionを明示的にコンパイルして追加することが可能となるためであったり、 他のextensionからサービスとして利用されるextensionを公開したい場合はJavaScriptパッケージをpublishする必要があるためとのことです。

以前の記事ではnpmを使ってアップロードしていましたが、ここではjlpmを使ってアップロードを試みました。

$ jlpm publish --access=public

作成中に遭遇した問題

  • 複数の音源ファイルを開いた場合に一つのタブに描画されてしまう
    • 描画対象のdivをidで指定していたためであり、ReactのuseRefを用いた参照に切り替えることによって解決
  • 開発用の手順が失敗する
    • jupyter labextension develop . --overwriteに失敗したが、これはPyPIに公開するパッケージディレクトリ(jupyterlab_wav)の下に__init__.py_version.pyがないためでした
    • このテンプレートのファイルを加工して使うことで成功するようになりました

既知の問題

  • Multi channelのaudioファイルが正しく描画されない
    • 波形はsplitChannelsオプションを使うことで分割して描画されるようになりました
    • スペクトログラムは要対応
  • タブの再描画などの際に以前に再生した音声が止まらないまま残ってしまう
    • 再生中にタブを閉じると再生され続ける
      • ReactWidgetの解放時に確実に止めるための何かが必要ではないかと思われる

今後改良したい点

  • FFT長選択のUIをつける
  • Jupyterlabから設定を行えるようにする
  • キーボードショートカットをつける(スペースで再生など)
  • timelineの単位の指定(サンプルなど)
  • colormapの制御

Debian 10 busterを初代Mac mini (A1176; 2006)にインストール

経緯

前回の記事を書いた後にUbuntu 18.04がインストールされていたMacBook Pro 2009のトラックパッドがバッテリー膨張による圧力によって割れるという惨事が発生してしまった。 このMacBook Proは以前に一度ファンとバッテリーを自力で換装したことがあるものであり、もう限界と判断して押入れに眠っていたMac mini 2006(大学の先輩から2万円で譲ってもらったもの)を復活させることを決意したのであった。

白昼夢の惨劇

ディストリビューションの選択

当初はarch linuxを入れようかと思い、x86_64のISOイメージをUSBに焼いてインストールしようとしたが、このタイミングでこのマシンのCPUがx86_64ではなくi686、つまり32bit CPUであるということが発覚した。 2020年にもなり32bit CPUのマシンを稼働させることに躊躇いもあったが、調べたところUbuntuが32bit対応の廃止を表明している一方でDebianは32bit対応のイメージを今でも配布しているということもありインストールすることを決めた。

マシンのスペック

ここに載っている1.66GHzのCPUを積んでいるものである。

  • CPU
    • 1.66GHz Core Duo
  • メモリ
    • 2GB(DDR2; 667MHz)
  • ディスク容量
    • 60GB

ちなみにこの記事によればファームウェアをアップデートした場合CPUの換装や4GBまでのメモリの増設が可能になるらしい。

想定使用用途

  • ファイルサーバ
    • 外付けHDDにもともと保存していたデータに対してsambaでアクセスしたいため
  • VPNサーバ
    • 移行元マシンで稼働していたSoftEtherの設定を引き継ぐ

実際の作業

全般的に自分用のメモ書きである。

DebianのISOをUSBに焼く

ここではdebian-10.6.0-i386-netinst.isoを公式から落としてきてUSBメモリに書き込んだ。 この作業は今回使用するMac miniではない別のマシンで行っている。

$ df -hか何かでUSBメモリのデバイスのパスを確認する(ここでは`/dev/disk4`)
$ diskutil umountDisk /dev/disk4
$ sudo dd if=debian-10.6.0-i386-netinst.iso of=/dev/rdisk4  # /dev/rdisk4, とrを付けると早く書き込まれるらしい

Mac mini上での作業

rEFIndのインストール

mac miniのosx側で予めrEFIndをインストールしておく。 rEFIndに同梱のインストールスクリプトを実行した上で再起動すると起動時にrEFIndが立ち上がるはずである。 ちなみにmac miniに入っていたosxのバージョンはLepardであった。

mac mini側のディスクユーティリティでosxのパーティションサイズを縮小しておく

HFS+フォーマットのosxがインストールされた領域のサイズを20GB程度に縮小した。 残りの領域についてはこの時点でフォーマットなどは実施しない(GUIインストーラの実行時にやってくれるため)。

GUIインストーラによるDebianのインストール

rEFIndが使えるようになるとUSBメモリに焼いたbootableイメージからインストーラを起動することが可能となる。 GUIインストーラで提示された項目を選んでいくだけで基本的には問題なくインストールは完了した。 パーティションは予めosx側で分割した残りの空き領域に対しインストーラが自動的にroot, var, tmp, swap, homeを分割してくれたのでその通りにした。LVMは使っていない。ファイルシステムはext4になっている。 デスクトップ環境はメモリが2GBしかなく必要かどうか悩んだがxfce4とした。 SSHサーバもインストーラの選択肢で選べたためインストールしておく。 よって基本的にこれ以降の作業はSSH越しの作業である。


ルーターの固定IP割当設定

以降でローカルLAN内でIPアドレスを固定化するためにルーターの割当設定を行っておく。 Debian上でip addrでMACアドレスを表示させ、有線LANのNICとWiFiアダプタに対して固定IP割当を設定。


Debian起動直後における設定

sudoグループへの所属

インストール時に登録したユーザーがデフォルトではsudoを使えないため、一度suでrootユーザになった上でsudoグループに所属させる。 $は一般ユーザ、#はrootユーザでの実行を意味する。

$ su
# gpasswd -a ${username} sudo
# exit
SSHの設定

鍵認証による接続を行うために公開鍵の登録をした後に/etc/ssh/sshd_configを編集する。 公開鍵はクライアント側からssh-copy-id -i id_rsa.pub ${ipaddr}で転送しても良いが、GitHubに登録してある公開鍵を使う場合はwget https://github.com/${username}.keys -qO - >> ~/.ssh/authorized_keysauthorized_keysに追記しても良い。

次にsudo vi /etc/ssh/sshd_configに下記項目を設定する。

Port ポート番号  # 変更したい場合
PubkeyAuthentication yes
PermitRootLogin no
PermitEmptyPasswords no
PasswordAuthentication no

設定後、sudo systemctl restart sshdでsshdを再起動する。

このタイミングで他のターミナルからsshしてパスワードログインができなくなっていることを確認する。 (元々接続していたターミナルでexitしてしまうと再度接続できなくなってしまったときに困る)。

その後に、クライアント側で~/.ssh/configを下記のように編集しておくとssh hostnameで接続できるようになるため便利である。

Host hostname
    HostName ${ipaddr}
    Port ポート番号
    preferredauthentications publickey
    IdentityFile "/path/to/id_rsa"
ホームディレクトリ配下の日本語ディレクトリの英語化

セットアップ時に日本語を選択するとホームディレクトリ配下の各種ディレクトリが日本語になってしまっている場合がある。 その場合はLC_ALL=C xdg-user-dirs-gtk-update --forceを実行して英語ディレクトリを作成する。 日本語ディレクトリはそのまま残ってしまうため不要であれば消す。

aptで色々入れる

とりあえず最低限のパッケージと前回の記事で書いたように最新版のsambaを使うためのppaを追加する。 最初にインストールするsoftware-properties-commonはdebianでadd-apt-repositoryを使うために必要である。

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install -y software-properties-common
$ sudo add-apt-repository ppa:linux-schools/samba-latest
$ sudo apt dist-upgrade
$ sudo apt install -y git vim zsh tmux curl htop
$ sudo apt install -y samba
$ sudo apt install -y build-essential autotools-dev libtool automake autoconf autogen

しかしリポジトリにppaを追加した後にupdateを掛けたところ無効である旨が表示されたためDebianでは使用不可能であった。


シェル環境周りの設定

tmuxの設定

.tmux.confを作成しC-tをprefixキーとするために下記のような.tmux.conf~以下に配置した。

unbind C-b
set-option -g prefix C-t
set-window-option -g mode-keys vi
bind C-t send-prefix
シェルをzshに変える
$ chsh  # /bin/zshを指定

再ログインすると色々zshの設定に関する対話スクリプトが走るが、blankファイルだけ生成して脱出した。 その上で下記のような設定を.zshrcに記述した。 内容自体は他マシンにおける設定の使い回しであるため各自好きに変更すると良いと思う。

setopt autopushd
setopt auto_cd

WORDCHARS='*?_-.[]~=&;!#$%^(){}<>'

compctl -M 'm:{a-z}={A-Z}'

setopt IGNOREEOF

umask 002

HISTFILE=$HOME/.zsh-history
HISTSIZE=10000
SAVEHIST=10000

setopt hist_ignore_dups # ignore duplication command history
setopt extended_history
setopt share_history
function history-all { history -E 1 }

# bind
bindkey -e
bindkey '^P' history-beginning-search-backward
bindkey '^N' history-beginning-search-forward

setopt no_check_jobs
setopt no_hup

setopt print_eight_bit

export LSCOLORS=gxfxcxdxbxegedadagacad
zstyle ':completion:*:default' list-colors $LSCOLORS

alias ls="ls -vG"
alias la='ls -aF'
alias ll='ls -h -laF'

alias mv='mv -v'
alias cp='cp -v'
alias rm='rm -vi'
alias grep='grep --color'

alias du='du -h'
alias df='df -h'

autoload -U compinit
compinit -u

autoload colors
colors

# VCS settings
# http://liosk.blog103.fc2.com/blog-entry-209.html
autoload -Uz vcs_info
precmd() {
    psvar=()
    LANG=en_US.UTF-8 vcs_info
    psvar[1]=$vcs_info_msg_0_
}

PROMPT="%{${fg[green]}%}[%n@%m %*] %{${fg[magenta]}%}%(!.#.>) %{${reset_color}%}"
PROMPT2="%{${fg[cyan]}%}%_> %{${reset_color}%}"
SPROMPT="%{${fg[red]}%}correct: %R -> %r %{${reset_color}%}"
RPROMPT="%{${fg[green]}%}[%{${fg[red]}%}%~%{${fg[white]}%}%1v%{${fg[green]}%}]%{${reset_color}%}"

ちなみに最近はプロンプトにstarshipを使っているが、このマシンでインストールしようとしても失敗したので独自に設定している。


ファイルサーバ(samba)関連の設定

fstabの設定

外付けHDDのUUIDを調べてシステム起動時に自動的にマウントされるように設定する。

$ sudo blkid -o list  # UUIDを調べる
$ sudo mkdir /mnt/hdd /mnt/hdd2  # マウントポイントを作成
$ sudo vim /etc/fstab

fstabには下記のような感じで書く。${uuidN}の部分は上記で調べたものに置き換えること。

UUID="${uuid1}" /mnt/hdd hfsplus nofail,defaults,force 0 0
UUID="${uuid2}" /mnt/hdd2 ext4 nofail,defaults 0 0

記述後、再起動して無事にマウントされているかを確認する。 nofailの記述は仮にマウントできなかった場合であってもエラーとして扱わないための指定である。 (実際、hfsplusフォーマットのディスクのマウントはsudo apt install hfsprogsが必要であったためこの記述を付けていない場合にemergency modeに突入してしまった。)

UUIDは外付けHDDに固有であるためか以前と変わっていなかったため、引き継ぎ元のマシンのfstabの追記内容をそのまま使い回すことができた。

/etc/smb.confの設定およびavahi-daemonの設定をする

詳細は割愛するが前回の記事を参考のこと。 ここもsmb.conf内のinterface名を変えたぐらいで基本的に元の設定ファイルをそのまま使い回すことができた。

sambaアクセス用のユーザーを追加する

guest okにしていてもTimeMachineバックアップの時には書き込み権限を持ったユーザーが登録されている必要があったので下記で設定しておく。

sudo smbpasswd -a ${username}


VPNサーバーの設定

SoftEtherの32bit対応版をダウンロードしてビルドする

ここから条件に合うものを落としてきてmakeする。makeの際にライセンスの同意などを求められる。 ビルドした後は/usr/localに移動しておく。

$ wget https://jp.softether-download.com/files/softether/v4.34-9745-rtm-2020.04.05-tree/Linux/SoftEther_VPN_Server/32bit_-_Intel_x86/softether-vpnserver-v4.34-9745-rtm-2020.04.05-linux-x86-32bit.tar.gz
$ tar zxvf softether-vpnserver-v4.34-9745-rtm-2020.04.05-linux-x86-32bit.tar.gz
$ cd vpnserver
$ make
$ cd ..
$ mv vpnserver /usr/local/
systemdの設定

/etc/systemd/system/vpnserver.serviceとして下記ファイルを作成する。

[Unit]
Description=SoftEther VPN Server
After=network.target network-online.target

[Service]
ExecStart=/usr/local/vpnserver/vpnserver start
ExecStop=/usr/local/vpnserver/vpnserver stop
Type=forking
RestartSec=3s

[Install]
WantedBy=multi-user.target
bridgeとtapデバイスの作成

下記のようなスクリプトを作成しsudoで実行する。

#!/bin/bash
# vim:fileencoding=utf-8

ip_addr="192.168.xxx.xxx/xx"
gateway_addr="192.168.xxx.xxx"
interface="enp1s0"

bridge_name="br0"
tap_device_name="tap_softether"

echo "create bridge interface"
/usr/bin/nmcli connection add type bridge ifname ${bridge_name}
echo "disable STP"
/usr/bin/nmcli connection modify bridge-${bridge_name} bridge.stp no
echo "modify bridge"
/usr/bin/nmcli connection modify bridge-${bridge_name} ipv4.method manual ipv4.addresses ${ip_addr} ipv4.gateway ${gateway_addr}
echo "connect ${interface} to bridge"
/usr/bin/nmcli connection add type bridge-slave ifname ${interface} master bridge-${bridge_name}
echo "connect tap to bridge"
/usr/bin/nmcli connection add type bridge-slave ifname ${tap_device_name} master bridge-${bridge_name}
echo "up tap device"
/usr/bin/nmcli connection up bridge-${bridge_name}

上記を実行すると下記のような出力が出る。

create bridge interface
接続 'bridge-br0' (0aa0ae96-6428-4f7e-bc1c-fa6f22f98cea) が正常に追加されました。
disable STP
modify bridge
connect enp1s0 to bridge
接続 'bridge-slave-enp1s0' (ec87d050-f6ae-4ca8-adbf-7b1f0e4eb1b6) が正常に追加されました。
connect tap to bridge
接続 'bridge-slave-tap_softether' (f262c26c-2799-4c9b-b149-9d1b3c44f8ce) が正常に追加されました。
up tap device
接続が正常にアクティベートされました (master waiting for slaves) (D-Bus アクティブパス: /org/freedesktop/NetworkManager/ActiveConnection/3)

この後に再起動を行い、ip addrでIPアドレスが割り振られていたり、ip link showでbr0のSTATEがUpになっていれば問題ないと思われる。

なおsudo apt install bridge-utilsを実行すると /sbin/brctlが使えるようになるが、今回は直接は不要であった。 また上記スクリプトではnmcliを使っているがipコマンドでもどうやら作成できるらしい。

VPNサーバーの設定の読み込み

移行元で予めvpncmdのConfigGetを実行し保存しておく。 これは

$ /usr/local/vpnserver/vpncmd
  > * 1. VPN Server または VPN Bridgeの管理 を実行
  > * 何も指定せずにEnterを押すことでローカルのサーバーに接続
  > * 更に何も指定せずにEnterを押すことでサーバー管理モードに入る
  > * 管理パスワードを入力
  VPN Server> ConfigGet 保存先パス

と実行することで保存が可能である。この設定ファイルを引き継ぎ先のマシンで読み込ませることを考える。

まず、sudo systemctl enable vpnserverを実行し前述のsystemd用の設定によるサービスを有効化する。 続いてsudo systemctl start vpnserverでVPNサーバーを起動する。

この上で/usr/local/vpnserver/vpncmdを実行し、移行元での保存時と同様に1を指定の上で無指定でEnterを2回実行することでサーバー管理モードに入る。 ここでConfigSetを実行すると設定ファイルのパスを尋ねられるので、入力することで設定の読み込みが可能である。 ここによればこの時点で元マシンの環境は復元できるとのことである。

ルーターで通信に必要なポートを開放する

50, 51, 500, 4500番ポートの通信をホストマシンのIPアドレスに対して許可するようにルーターの設定を施した。

接続できない原因を探す

元マシンでの設定が正しければ上記までの設定で接続できるようになるはずだが案の定接続できない。 そこで/usr/local/vpnserver以下に存在するsecure_logserver_logといったログ見て原因を推察する。 ここではserver_log以下に存在するlogにおける

2020-11-21 xx:xx:xx.xxx L2TP PPP セッション [xxx.xxx.xxx.xxx:xxxx]:
  DHCP サーバーからの IP アドレスの取得に失敗しました
  PPP の通信を受諾するためには DHCP サーバーが必要です
  仮想 HUB  Ethernet セグメント上で DHCP サーバーが正しく動作しているかどうか確認してください
  DHCP サーバーを用意することができない場合は仮想 HUB  SecureNAT 機能を用いることもできます

という記述からSecureNatが有効化されていないことが分かったため、vpncmdを実行し仮想ハブの管理からSecureNatEnableを実行したところ、無事に接続されるようになった。

SecureNATの利用を使用しない設定への変更

前節の設定のままだとVPNでSecureNAT機能によりIPアドレスが割り振られるが、このままだとホストマシンにSSHアクセスできなかったため元マシンの設定を見返したところ/etc/network/interfacesを明示的に指定していたことが分かった。 よってvpncmdSecureNATDisableを実行した上で再度SecureNatを停止した上で下記のような設定を/etc/network/interfacesに追記した。

auto enp1s0
iface enp1s0 inet manual

auto br0
iface br0 inet static
  address 192.168.xxx.xxx
  netmask 255.255.xxx.xxx
  gateway 192.168.xxx.xxx
  bridge_ports enp1s0 tap_softether
  dns-nameservers 192.168.xxx.xxx

NetworkManagerは/etc/NetworkManager/NetworkManager.confにおいて下記のような設定

[ifupdown]
managed=false

となっている場合、/etc/network/interfaceによって管理されているデバイスはNetworkManagerで制御を行わないとのことである。 この上でsudo systemctl restart networkingを行うとSecureNATがdisableとなっている場合でも接続が可能となった。

しかし、この状態で再起動を行うとやはり接続できない状態に戻ってしまう。 調べたところsudo systemctl restart networkingを行った後は下記状態

$ /sbin/brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.0016cba5df15       no              enp1s0
                                                        tap_softether

となっているが、マシンの再起動直後は

$ /sbin/brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.0016cba5df15       no              enp1s0

という状態になっていることが分かった。 つまりbr0に対して起動直後は何らかの理由でtap_softetherのtapデバイスがbr0に接続されていないことが原因となっていることが考えられ、 実際に/sbin/brctl addif br0 tap_softetherを実行することで明示的に追加を行った場合はVPN接続が可能となった。

これを受けて/etc/systemd/system/vpnserver.serviceに下記のようにExecStartPostを追加した。

[Service]
ExecStart=/usr/local/vpnserver/vpnserver start
ExecStartPost=/bin/sleep 10 ; /sbin/brctl addif br0 tap_softether

sleepの秒数の後のセミコロンは10とセミコロンの間にスペースを空けないとエラーが生じるため注意が必要である。 これによりマシン再起動後も手動での操作なしにVPN接続が可能な状態となった。


まとめ

古いMac miniにDebianを入れてファイルサーバ・VPNサーバとして復活させる手順のメモを記した。 稼働させたばかりであり、そもそも古いマシンであるため、いつまで持つか不明であるがこのまましばらく運用してみようと思う。

Ubuntu 18.04のSambaサーバー上にTimeMachineのバックアップ環境を構築

普段使用しているMacBook Proのバタフライキーボードにおいて特定のキーが繰り返し入力されたりバッテリーが膨張しつつある問題が発生したため修理に出す前にTimeMachineを用いてバックアップを取ろうと思ったが、調べたところ最近のSambaでは特定の条件が揃えばネットワーク越しにTimeMachineを用いてバックアップを取ることが可能であることが分かったため、その際に設定した環境構築のメモを下記に記す。

手順

  1. ppaを追加し新しめのバージョンのsambaを入れる
  2. /etc/samba/smbd.confを編集する
  3. avahi-daemonの設定をする
  4. sambaのdaemonをrestartする

1.ppaを追加し新しめのバージョンのsambaを入れる

2020/10/31現在、aptでインストールされるsambaはバージョン4.7.6であるが、 バージョン4.8以降でなければTimeMachineバックアップ用の設定ができないのでリポジトリを追加した上で新しいバージョンのsambaをインストールする。 このQAのAnswerを参考に下記のようにppaを追加する。

$ sudo add-apt-repository ppa:linux-schools/samba-latest
$ sudo apt-get dist-upgrade
$ sudo apt-get install samba

バージョンは下記のように確認可能である。

$ smbd -V
> Version 4.10.18

2. /etc/samba/smbd.confを編集する

様々なサイトを参考に下記のような項目を追加した(が不要な設定もあるかもしれない)。 pathは適宜共有したいディレクトリに書き換えること。またfruit:time machine max sizeの項目は適切なサイズを設定しておかないとTimeMachineはディスクをフルに使おうとしてしまうので注意が必要である。 正確な設定項目についてはマニュアルを見たほうが良い。

[global]

# Special configuration for Apple's Time Machine
vfs objects = catia fruit streams_xattr 
fruit:model = MacPro
fruit:advertise_fullsync = true
fruit:aapl = yes

[TimeMachine]
  comment = Backup for Mac Computers
  path = /path/to/share
  writable = yes
  browsable = yes
  guest ok = yes
  fruit:time machine = yes
  fruit:time machine max size = 600G
  durable handles = yes
  kernel oplocks = no
  kernel share modes = no
  posix locking = no

3. avahi-daemonの設定をする

上記設定を施したのちにsambaのデーモンを再起動した時点で使えるかと思ったがどうやら別途avahi-daemon(mDNS)の設定をしなければならなかったことが分かった。 この記事を参考に下記のような設定を記述したファイルを/etc/avahi/services/timemachine.serviceとして作成した。 なおここではavahi-daemon自体はaptで別途インストール済みであることを想定している。

<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
 <name replace-wildcards="yes">%h</name>
 <service>
   <type>_smb._tcp</type>
   <port>445</port>
 </service>
 <service>
   <type>_adisk._tcp</type>
   <txt-record>dk0=adVN=TimeMachine,adVF=0x82</txt-record>
 </service>
</service-group>

ここで<txt-record>dk0=adVN=TimeMachine,adVF=0x82</txt-record>TimeMachineとなっている部分は/etc/samba/smb.confの共有名と共通にしておく必要がある。

4. sambaのdaemonをrestartしMacOSからTimeMachineを実行

3までを実施した後で、sudo systemctl restart avahi-daemonおよびsudo systemctl restart smbdを実行しavahi-daemonとsmbdをrestartさせると MacOS(手元のマシンはCatalina 10.15.7)のシステム環境設定にあるTimeMachineのディスクを選択の項目から設定したディスクが探せるようになっているはずである。 保存先として設定するとIDとパスワードを入力するダイアログが出るため書き込み権限のあるユーザーIDを打ち込むとバックアップが進行するようになった。

参考にさせていただいたサイト

  • https://askubuntu.com/questions/1166875/unable-to-built-samba-4-10-6-from-source
    • ppaからのsambaをインストールする際の参考にしました
  • https://ideal-reality.com/computer/server/time-machine-samba/
    • 設定の参考にしました
  • https://qiita.com/upsilon/items/c368726845f89cb0ffe9
    • avahiの設定を参考にしました