Ubuntu server 22.04 をDHCPサーバ無しで autoinstall する

DHCPサーバを使わずに、Ubuntu server 22.04 を無人インストール(自動インストール)する方法です。Ubuntu 20.04でも動作します。
`mkisofs` で定義ファイルの入った isoイメージを作って定義ファイルを渡す方法です。

# `subiquity` とか `curtin` とか `cloud-init` とか

Ubuntu server 17.10 からインストーラーが `subiquity` になっています[^1] 。

[^1]: https://gihyo.jp/admin/serial/01/ubuntu-recipe/0495

[`subiquity`](https://github.com/canonical/subiquity)  ... Ubuntu server のインストーラー。Ubuntu desktopのインストーラー `ubiquity` のサーバー版。

[`curtin`](https://curtin.readthedocs.io/en/latest/index.html) ... `subiquity` が使っている[^2]ソフトウェア。UIを持たず、OSインストール処理だけを提供するっぽい。
[^2]: https://ubunlog.com/en/subiquity-ubuntu-prepares-a-new-installer-that-we-can-see-in-ubuntu-21-10/

[`cloud-init`](https://cloudinit.readthedocs.io/en/latest/) ... OSインスタンスの各種初期設定を行うソフトウェア。Ubuntu server インストール後の再起動時に、`cloud-init` を使ってアカウントの設定等を行っているっぽい。

# 構成ファイル

公式のリファレンス

https://ubuntu.com/server/docs/install/autoinstall-reference

インストール構成を書いたYAMLファイルをインストーラーに渡すことで無人インストールを実現します。

```yaml
#cloud-config
autoinstall:
  version: 1
  apt:
    disable_components: []
    preserve_sources_list: false
    primary:
        - arches: [i386, amd64]
          uri: "http://archive.ubuntu.com/ubuntu"
        - arches: [default]
          uri: "http://ports.ubuntu.com/ubuntu-ports"
    geoip: true
  drivers:
    install: false

# identity でサーバーのホスト名、初期ユーザーパスワードを設定します。
# 後で出てくる user-data でより詳細に指定するのでスキップします。
#
#  identity:
#    hostname: myhost
#    password: $6$...
#    username: admin

#
# timezone、hostname、ユーザーアカウントを設定します。
# この user-data ブロックの中身は、cloud-init で処理されます。
# なので、cloud-init のスキーマで記述します。
# https://askubuntu.com/questions/1365637/autoinstall-user-datas-users-section-does-not-work-at-all
# 
  user-data:
    timezone: Asia/Tokyo
    hostname: myhost
    users:
      - name: root

        # https://qiita.com/YasuhiroABE/items/637f1046a15938f9d3e9
        # 適当な salt と `openssl -passwd -6` でハッシュを作る。
        # このハッシュは 'rootpass'。
        hashed_passwd: $6$9f4572f53ceafe2d$o0D7PIty0m2a4Hu/mGAA8PbflKS/xKFeh/6ToiWYJTXqcjsPHM2RW1xBVwKuEWw0NwGbsf4.LUL5rg2xM4FK.1

        lock_passwd: false
        ssh_authorized_keys:
          - ssh-rsa AAAA...

      # 一般ユーザー作る場合。
      #
      # - name: admin
      #   primary_group: users
      #   groups: [ adm, sudo ]
      #   passwd: $6$...
      #   lock_passwd: false
      #   shell: /bin/bash
      #   ssh_authorized_keys:
      #     - ssh-rsa AAAA...

  kernel:
    package: linux-generic
  keyboard:
    layout: jp
    toggle: null
    variant: ''
  locale: en_US.UTF-8

  # netplan のスキーマでネットワーク設定を書く。
  network:
    ethernets:
      ens3:
        dhcp4: no
        addresses: [ 192.168.0.2/24 ]
        nameservers:
          addresses: [ 1.1.1.1 ]
        routes:
          - to: default
            via: 192.168.0.1
    version: 2
  ssh:
    allow-pw: true
    install-server: true
  storage:
    config:
    - id: disk-sda
      type: disk
      ptable: gpt
      wipe: superblock
      preserve: false
      name: ''
      grub_device: true
      # `virtio` バスの場合、vda になる。
      match: { path: /dev/vda }

    # GRUB partition
    - { id: partition-0, type: partition, device: disk-sda,
        size: 1G, flag: bios_grub, number: 1, preserve: false, grub_device: false }

    # /boot
    - { id: partition-1, type: partition, device: disk-sda,
        size: 512M, wipe: superblock, flag: '', number: 2, preserve: false, grub_device: false }
    - { type: format, id: format-0, fstype: ext4, volume: partition-1, preserve: false }
    - { type: mount, id: mount-0, path: /boot, device: format-0 }

    # /
    - { id: partition-2, type: partition, device: disk-sda,
        size: -1, wipe: superblock, flag: '', number: 3, preserve: false, grub_device: false }
    - { id: lvm_volgroup-1, type: lvm_volgroup, name: vg0,
        devices: [ partition-2 ], preserve: false }
    - { id: lvm_partition-0, type: lvm_partition, name: lv-0,
        volgroup: lvm_volgroup-1,
        size: 100%, wipe: superblock, preserve: false }
    - { id: format-1, type: format, fstype: ext4, volume: lvm_partition-0, preserve: false }
    - { id: mount-1, type: mount, path: /, device: format-1 }

  late-commands:
    # インストーラの最後に実行するコマンドを書く。

    # root の .hushlogin を作る
    - touch /target/root/.hushlogin

    # デフォルトエディタを vim にする
    # - >-
    #   curtin in-target --target=/target --
    #   update-alternatives --set editor /usr/bin/vim.basic

    # sudo グループを NOPASSWD にする
    # - >-
    #   /bin/sed /target/etc/sudoers -i -E -e 's/^%sudo\s+ALL=\(ALL:ALL\)\s+ALL/%sudo ALL=(ALL:ALL) NOPASSWD: ALL/'
```

1行目は

```yaml
#cloud-config
```

である必要があります。YAMLのコメントだと思って削除してはいけません。

## apt リポジトリ

`apt` ブロックは `curtin` のスキーマで記述します。

https://curtin.readthedocs.io/en/latest/topics/apt_source.html

リポジトリのプライベートミラーを使う場合は、直接 apt source.list の内容を記述します。

```yaml
autoinstall:
  apt:
    sources:
      private.list:
        source: |
          deb http://192.168.0.100/ubuntu/ jammy main restricted universe multiverse
          deb http://192.168.0.100/ubuntu/ jammy-updates main restricted universe multiverse
          deb http://192.168.0.100/ubuntu/ jammy-security main restricted universe multiverse
          deb http://192.168.0.100/ubuntu/ jammy-backports main restricted universe multiverse
```

## プロキシ

Webプロキシが必要な場合は、`proxy` で指定します。

```yaml
autoinstall:
  proxy: http://192.168.0.100:8080
```

## パスワードハッシュ

パスワードハッシュは、適当な salt と `openssl -passwd -6` で生成します。

https://qiita.com/YasuhiroABE/items/637f1046a15938f9d3e9

## カスタム処理

`late-commands` はインストールの最後に実行するカスタムコマンドです。
インストールされたディレクトリツリーは `/target` にマウントされています。

```
curtin in-target --target=/target -- COMMAND
```

みたいに `curtin in-target` で `target` ディレクトリをしてコマンド実行することで加工できます。
単純なファイル操作なら、 `/target/...` のファイルを直接加工することもできます。

## オフラインインストールのワークアラウンド

* インターネットへのリーチャビリティが無い
* LAN内にaptリポジトリが無く apt source として指定することができない

のように、アクセスできる apt リポジトリが無い場合、アクセスタイムアウト待ちでインストールが進みません。

この場合、nameserver を与えないことでワークアラウンドになります。

```yaml
autoinstall:
  network:
    ethernets:
      ens3:
        dhcp4: no
        addresses: [ 192.168.0.2/24 ]

        # [ワークアラウンド]
        # リポジトリへのリーチャビリティが無い場合、ネームサーバーを指定しないことでインストールが進む。
        # nameservers:
        #   addresses: [ 1.1.1.1 ]
        routes:
          - to: default
            via: 192.168.0.1
    version: 2
```

# 定義ファイルをインストーラーに渡す

YAMLファイルをどこから取ってくるか、は起動時のカーネルパラメータ― `ds` (data source) で指定します。
ここは `cloud-init` の仕組みを使っているみたいです。

https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html

`ds=nocloud-net` を指定しておいて、`CIDATA` というボリュームラベルを持つ iso9660 イメージが存在すると、そのボリュームのルートディレクトリからファイルを読んでくれます。

isoイメージの中での定義ファイルは `user-data` というファイル名にします。
もう一つ `meta-data` ファイルが必要ですが、これは空ファイルで大丈夫です。

```shell
$ echo > meta-data
$ ls
meta-data  user-data
$ cat user-data
#cloud-config
autoinstall:
  version: 1
...
$ mkisofs -l -R -J -V CIDATA -o user-data.iso meta-data user-data
I: -input-charset not specified, using utf-8 (detected in locale settings)
Total translation table size: 0
Total rockridge attributes bytes: 331
Total directory bytes: 0
Path table size(bytes): 10
Max brk space used 0
184 extents written (0 MB)

$ ls user-data.iso
user-data.iso
```

# `virt-install` でインストール

適当にqcow2イメージを作ります。

```shell
$ qemu-img create -f qcow2 mydisk.qcow2 100G
```

`virt-install` でインストールします。

```shell
$ virt-install \
    --name=myhost \
    --vcpus=4 \
    --memory=8192 \
    --hvm \
    --virt-type kvm \
    --network bridge=br0,model=virtio \
    --arch=x86_64 \
    --disk path=mydisk.qcow2,bus=virtio \
    --disk path=user-data.iso,format=iso,perms=ro,device=cdrom \
    --location ubuntu-22.04-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
    --extra-args 'autoinstall ds=nocloud-net console=tty0 console=ttyS0' \
    --graphics vnc,listen=0.0.0.0,keymap=ja
```

* `--location` で `kernel`, `initrd` の指定が必要です。
    * ubuntu-22.04-live-server-amd64.iso に `.treeinfo` が含まれていないためです。
    * https://ngyuki.hatenablog.com/category/CentOS
* `--extra-args` (インストール時のカーネルパラメータ) で `autoinstall`, `ds=nocloud-net` を指定します。
* 2つ目の `--disk` で、mkisofs で作った `user-data.iso` を指定します。
* `--graphics` はなくても、`virsh console` でインストール状況を見ることができます。でも、VNCがあると、定義ファイルに不備がありインストーラーが止まったとき、ALT+F2でCLIを起動して、トラブルシュートすることができます。