SONiCにアプリケーションを追加する方法

# はじめに

WindowsでもMacでもiOS、Androidその他いろいろなOSで、標準で提供されるアプリケーションだけしか使っていないという人はあまりいらっしゃらないと思います。が、ホワイトボックススイッチ用OSであるSONiCでは、その性質上標準で組み込まれている機能の範囲で使われる方が大半と思います。

それでも「この機能を組み込みたい」というケースはあると思います。今回は、その方法についていくつか紹介します。

# その1 起動後に組み込む

SONiCはDebianをベースにして作られていますので、`apt-get`や`apt`がが使えます。
Ansibleなどを利用するのであれば、これだけできれば十分です。
最初は`sudo apt update`をお忘れなく。

```shell-session:htopを組み込む例
admin@sonic:~$ sudo apt update
Get:1 https://download.docker.com/linux/debian bullseye InRelease [43.3 kB]
Get:2 http://debian-archive.trafficmanager.net/debian bullseye InRelease [116 kB]                 
Get:3 https://download.docker.com/linux/debian bullseye/stable amd64 Packages [18.3 kB]
Get:4 http://debian-archive.trafficmanager.net/debian bullseye-updates InRelease [44.1 kB]
Get:5 http://debian-archive.trafficmanager.net/debian bullseye-backports InRelease [49.0 kB]
Get:6 http://debian-archive.trafficmanager.net/debian-security bullseye-security InRelease [48.4 kB]
Get:7 http://debian-archive.trafficmanager.net/debian bullseye/contrib Sources [51.4 kB]
Get:8 http://debian-archive.trafficmanager.net/debian bullseye/non-free Sources [98.3 kB]
Get:9 http://debian-archive.trafficmanager.net/debian bullseye/main Sources [11.4 MB]
Get:10 http://debian-archive.trafficmanager.net/debian bullseye/non-free amd64 Packages [122 kB]
Get:11 http://debian-archive.trafficmanager.net/debian bullseye/contrib amd64 Packages [60.9 kB]
Get:12 http://debian-archive.trafficmanager.net/debian bullseye/main amd64 Packages [11.1 MB]
Get:13 http://debian-archive.trafficmanager.net/debian bullseye-updates/main Sources [4,812 B]
Get:14 http://debian-archive.trafficmanager.net/debian bullseye-updates/main amd64 Packages [14.6 kB]
Get:15 http://debian-archive.trafficmanager.net/debian bullseye-backports/contrib amd64 Packages [4,396 B]
Get:16 http://debian-archive.trafficmanager.net/debian bullseye-backports/non-free amd64 Packages [14.3 kB]
Get:17 http://debian-archive.trafficmanager.net/debian bullseye-backports/main amd64 Packages [371 kB]
Get:18 http://debian-archive.trafficmanager.net/debian-security bullseye-security/non-free Sources [558 B]
Get:19 http://debian-archive.trafficmanager.net/debian-security bullseye-security/main Sources [272 kB]
Get:20 http://debian-archive.trafficmanager.net/debian-security bullseye-security/main amd64 Packages [264 kB]
Get:21 http://debian-archive.trafficmanager.net/debian-security bullseye-security/non-free amd64 Packages [457 B]
Fetched 24.1 MB in 3s (8,513 kB/s)                    
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
10 packages can be upgraded. Run 'apt list --upgradable' to see them.
admin@sonic:~$ sudo apt install htop
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Suggested packages:
  lm-sensors lsof strace
The following NEW packages will be installed:
  htop
0 upgraded, 1 newly installed, 0 to remove and 10 not upgraded.
Need to get 127 kB of archives.
After this operation, 328 kB of additional disk space will be used.
Get:1 http://debian-archive.trafficmanager.net/debian bullseye/main amd64 htop amd64 3.0.5-7 [127 kB]
Fetched 127 kB in 0s (283 kB/s)
debconf: delaying package configuration, since apt-utils is not installed
Selecting previously unselected package htop.
(Reading database ... 38304 files and directories currently installed.)
Preparing to unpack .../htop_3.0.5-7_amd64.deb ...
Unpacking htop (3.0.5-7) ...
Setting up htop (3.0.5-7) ...
admin@sonic:~$ 
```

入れてしまえばこっちのもの。`pip`でも`npm`でもご自由に。

# その2 ビルド時に組み込む

## バイナリパッケージ編

さきほどの`apt-get`, `apt`によるインストールをビルド時に実行させることができます。

変更するファイルは`build_debian.sh`です。中をみていくと、下記のような部分があります。

```sh
## Pre-install the fundamental packages
## Note: gdisk is needed for sgdisk in install.sh
## Note: parted is needed for partprobe in install.sh
## Note: ca-certificates is needed for easy_install
## Note: don't install python-apt by pip, older than Debian repo one
## Note: fdisk and gpg are needed by fwutil
sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install      \
    file                    \
    ifmetric                \
    iproute2                \
    bridge-utils            \
    isc-dhcp-client         \
    sudo                    \
    vim                     \
    tcpdump                 \
```

ここにインストールしたいパッケージ名を追加してビルドするだけです。たとえば`keepalived`を組み込む場合は下記のようにします。(例示しましたが、`keepalived`の場合たんに組み込んだだけではうまく動かないのでご注意を)

```diff
diff --git a/build_debian.sh b/build_debian.sh
index 01a10d78d..a68f7d7a2 100755
--- a/build_debian.sh
+++ b/build_debian.sh
@@ -330,6 +330,7 @@ fi
 ## Note: don't install python-apt by pip, older than Debian repo one
 ## Note: fdisk and gpg are needed by fwutil
 sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install      \
+    keepalived              \
     file                    \
     ifmetric                \
     iproute2                \
```

## Pythonパッケージ編

同じく`build_debian.sh`に書けばいいのですが、いくつかおまじないが必要です。下記は、j2cli(Jinja2テンプレートエンジンのCLI)を組み込む差分です。

```diff
diff --git a/build_debian.sh b/build_debian.sh
index 01a10d78d..a68f7d7a2 100755
--- a/build_debian.sh
+++ b/build_debian.sh
@@ -526,6 +526,9 @@ sudo https_proxy=$https_proxy LANG=C chroot $FILESYSTEM_ROOT pip3 install 'docke
 # Install scapy
 sudo https_proxy=$https_proxy LANG=C chroot $FILESYSTEM_ROOT pip3 install 'scapy==2.4.4'
 
+# Install j2cli
+sudo https_proxy=$https_proxy LANG=C chroot $FILESYSTEM_ROOT pip3 install 'j2cli'
+
 ## Note: keep pip installed for maintainance purpose
 
 # Install GCC, needed for building/installing some Python packages
```

## 自作アプリ編

自作アプリを組み込むことももちろん可能です。
ここでは、実在しない架空のアプリですがややこしい例を出します。

- アプリケーション名 `etcdconfig`
- 機能: `etcd`からJSON形式の設定を読み込み、SONiCの設定に反映させる
- 使用言語: Differential Datalog (DDlog)

これのどこがややこしいのかと言いますと、

- DDlogコンパイラはSONiCのビルド環境に用意されていない
- DDlogコンパイラの出力はRust言語のソースコードで、これをコンパイルする必要がある
- つまりRustコンパイラも必要だが、これもSONiCのビルド環境に用意されていない

つまり、ビルド環境にも手を入れる必要があるということです。

せっかくですのでSONiCに組み込まれるいろいろと同じように、Debianパッケージ形式で作りたいと思います。

必要な作業を箇条書きにするとこれだけあります。

1. `etcdconfig`本体(ソースコードとビルドルール)を用意する
1. `etcdconfig`Debianパッケージのビルドルールを追加する
1. `etcdconfig`Debianパッケージをビルド対象とする
1. `etcdconfig`DebianパッケージをSONiCに組み込む
1. `etcd-client`をSONiCに組み込む
1. DDlogコンパイラをビルド環境に組み込む
1. Rustコンパイラをビルド環境に組み込む

大変面倒ですね。シンプルにC++で組まれたプログラムの場合は5からあとは不要です。
ではさっそくやってみます。

### `etcdconfig`本体(ソースコードとビルドルール)を用意する

`src/sonic-etcdconfig/`に一式置くものとします。
本体は `src/sonic-etcdconfig/sonic_config.dl` です。

`Makefile`と`debian/*`は下記のような感じです。

```diff
diff --git a/src/sonic-etcdconfig/Makefile b/src/sonic-etcdconfig/Makefile
new file mode 100644
index 000000000..84bfe8ea9
--- /dev/null
+++ b/src/sonic-etcdconfig/Makefile
@@ -0,0 +1,30 @@
+INSTALL := /usr/bin/install
+ETCDCONFIGDIR=$(abspath .)
+CONFIGCLIDIR := $(ETCDCONFIGDIR)/sonic_config_ddlog/target/release
+PROG := sonic_config_cli
+TARGET := $(CONFIGCLIDIR)/$(PROG)
+DDLOG := /usr/local/ddlog/bin/ddlog -L /usr/local/ddlog/lib
+
+ETCDCONFIG_VERSION = 1.0.0-0
+MAIN_TARGET = etcdconfig_$(ETCDCONFIG_VERSION)_$(CONFIGURED_ARCH).deb
+
+all: $(TARGET)
+
+$(TARGET): sonic_config.dl
+       $(DDLOG) -i $<
+       cd sonic_config_ddlog; $(HOME)/.cargo/bin/cargo build --release; cd -
+
+check:
+
+clean:
+       $(RM) -r $(ETCDCONFIGDIR)/sonic_config_ddlog
+
+install:
+       $(INSTALL) -D -m 0755 $(TARGET) $(DESTDIR)/usr/bin/$(TARGET)
+
+uninstall:
+       $(RM) $(DESTDIR)/usr/bin/$(PROG)
+
+$(addprefix $(DEST)/, $(MAIN_TARGET)): $(DEST)/% :
+       dpkg-buildpackage -us -uc -b -j$(SONIC_CONFIG_MAKE_JOBS) --admindir $(SONIC_DPKG_ADMINDIR)
+       mv $* $(DEST)/
```

```diff
diff --git a/src/sonic-etcdconfig/debian/changelog b/src/sonic-etcdconfig/debian/changelog
new file mode 100644
index 000000000..ea08a73b0
--- /dev/null
+++ b/src/sonic-etcdconfig/debian/changelog
@@ -0,0 +1,5 @@
+etcdconfig (1.0.0-0) unstable; urgency=low
+
+  * initial release
+
+ -- Masaru OKI <*****@*********>  Mon, 5 Jan 2021 18:49:00 +0900
diff --git a/src/sonic-etcdconfig/debian/compat b/src/sonic-etcdconfig/debian/compat
new file mode 100644
index 000000000..f599e28b8
--- /dev/null
+++ b/src/sonic-etcdconfig/debian/compat
@@ -0,0 +1 @@
+10
diff --git a/src/sonic-etcdconfig/debian/control b/src/sonic-etcdconfig/debian/control
new file mode 100644
index 000000000..34374fc69
--- /dev/null
+++ b/src/sonic-ecdconfig/debian/control
@@ -0,0 +1,10 @@
+Source: etcdconfig
+Maintainer: Masaru OKI <*****@*********>
+Section: net
+Priority: optional
+Build-Depends: dh-exec (>=0.3), debhelper (>= 9)
+Standards-Version: 1.0.0
+
+Package: etcdconfig
+Architecture: amd64
+Description: This package contains Distiributed Configuration Service for SONiC.
diff --git a/src/sonic-etcdconfig/debian/rules b/src/sonic-etcdconfig/debian/rules
new file mode 100755
index 000000000..dd72c9a7b
--- /dev/null
+++ b/src/sonic-etcdconfig/debian/rules
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#export DH_VERBOSE = 1
+
+%:
+       dh $@ 
```

### `etcdconfig`Debianパッケージののビルドルールを追加する

```diff
diff --git a/rules/etcdconfig.mk b/rules/etcdconfig.mk
new file mode 100644
index 000000000..ef51a6a15
--- /dev/null
+++ b/rules/etcdconfig.mk
@@ -0,0 +1,10 @@
+# etcdconfig package
+
+ETCDCONFIG_VERSION = 1.0.0-0
+
+export ETCDCONFIG_VERSION
+export ETCDCONFIG
+
+ETCDCONFIG = etcdconfig_$(ETCDCONFIG_VERSION)_$(CONFIGURED_ARCH).deb
+$(ETCDCONFIG)_SRC_PATH = $(SRC_PATH)/sonic-etcdconfig
+SONIC_DPKG_DEBS += $(ETCDCONFIG)
```

### `etcdconfig`Debianパッケージをビルド対象とする

```diff
diff --git a/slave.mk b/slave.mk
index 54a01aaa1..296f15e67 100644
--- a/slave.mk
+++ b/slave.mk
@@ -1122,6 +1122,7 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \
                 $(SONIC_DEVICE_DATA) \
                 $(IFUPDOWN2) \
                 $(KDUMP_TOOLS) \
+                $(ETCDCONFIG) \
                 $(NTP) \
                 $(LIBPAM_RADIUS) \
                 $(LIBNSS_RADIUS) \
```

### `etcdconfig`DebianパッケージをSONiCに組み込む

```diff
diff --git a/files/build_templates/sonic_debian_extension.j2 b/files/build_templates/sonic_debian_extension.j2
index c5e76d3c4..41b356365 100644
--- a/files/build_templates/sonic_debian_extension.j2
+++ b/files/build_templates/sonic_debian_extension.j2
@@ -914,6 +914,9 @@ sudo cp {{src}} $FILESYSTEM_ROOT/{{dst}}
 {% endif -%}
 {% endfor -%}
 
+# Install etcdconfig
+sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/etcdconfig_*.deb
+
 {% if sonic_asic_platform == "mellanox" %}
 sudo mkdir -p $FILESYSTEM_ROOT/etc/mlnx/
 sudo cp $files_path/$MLNX_SPC_FW_FILE $FILESYSTEM_ROOT/etc/mlnx/fw-SPC.mfa
```

### `etcd-client`をSONiCに組み込む

`etcd`のクライアントは既存のものを使います。これはさきほど紹介したバイナリパッケージの組み込みです。

```diff
diff --git a/build_debian.sh b/build_debian.sh
index 01a10d78d..705076017 100755
--- a/build_debian.sh
+++ b/build_debian.sh
@@ -355,6 +355,7 @@ sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y in
     iptables-persistent     \
     ebtables                \
     logrotate               \
+    etcd-client             \
     curl                    \
     kexec-tools             \
     less                    \
```

### DDlogコンパイラをビルド環境に組み込む

Githubからリリースバイナリを取得します。amd64しか用意されてないので条件をつけていますが、そういえば他の部分は無条件ですね。不完全で申し訳ないです。。

```diff
diff --git a/sonic-slave-bullseye/Dockerfile.j2 b/sonic-slave-bullseye/Dockerfile.j2
index 296905787..7ae7bbf27 100644
--- a/sonic-slave-bullseye/Dockerfile.j2
+++ b/sonic-slave-bullseye/Dockerfile.j2
@@ -610,6 +610,15 @@ RUN apt-get install -y docker-ce=5:20.10.14~3-0~debian-bullseye docker-ce-cli=5:
 RUN echo "DOCKER_OPTS=\"--experimental --storage-driver=vfs {{ DOCKER_EXTRA_OPTS }}\"" >> /etc/default/docker
 RUN update-alternatives --set iptables /usr/sbin/iptables-legacy
 
+# Install differential datalog
+{% if CONFIGURED_ARCH == "amd64" -%}
+RUN wget https://github.com/vmware/differential-datalog/releases/download/v0.35.0/ddlog-v0.35.0-20210120055331-linux.tar.gz \
+ && tar -C /usr/local -xzf ddlog-v0.35.0-20210120055331-linux.tar.gz \
+ && echo 'export PATH=$PATH:/usr/local/ddlog/bin' >> /etc/bash.bashrc \
+ && echo 'export DDLOG_HOME=/usr/local/ddlog' >> /etc/bash.bashrc \
+ && rm ddlog-v0.35.0-20210120055331-linux.tar.gz
+{% endif -%}
+
 # Install m2crypto package, needed by SWI tools
 RUN pip3 install m2crypto==0.36.0
 
```

### Rustコンパイラをビルド環境に組み込む

ほぼ公式で推奨される方法ですが、少し特殊なことをしています。
通常Rustコンパイラは一般ユーザ環境にインストールされるので、ビルドを実行するユーザの環境に入るよう少々細工しています。

```diff
index 2eb7ef18b..02d80e1e7 100644
--- a/sonic-slave-bullseye/Dockerfile.user.j2
+++ b/sonic-slave-bullseye/Dockerfile.user.j2
@@ -31,4 +31,7 @@ RUN chmod go= /var/$user/.ssh -R
 # Add user to sudoers
 RUN echo "$user ALL=(ALL) NOPASSWD:ALL" >>/etc/sudoers
 
+# Install Rust
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | su $user -c "sh -s -- -y" && echo 'export PATH=$PATH:$HOME/.cargo/bin' >> /etc/bash.bashrc
+
 USER $user
```

以上です。

# おわりに

組み込んだバイナリをサービスとして起動するには、既存dockerコンテナの中に組み込むには、dockerコンテナを増やすにはどうすれば、など書き足りないことは多いのですが、あまり膨らませてもしかたないのでひとまずこのくらいにしておこうと思います。

SONiCになにか機能追加したい場合の参考になれば幸いです。