基本的なNetplanのyamlファイルを対話で作成するbashシェルスクリプト

## 0. きっかけ
### 0.1. Netplanのフォーマットはちょいちょい変わり、yamlのインデントルールが厳しい。。
[Ubintu - Configuring network](https://ubuntu.com/server/docs/configuring-networks)
当然、定義はあるのですが、OSバージョンが変わる度にちょっとずつ推奨の書き方が変わっていて、
以前設定したNetplan構文が、次のOSバージョンだと”非推奨です。”と言われることも多々あり。。

## 1. Netplanコンフィグ生成bashスクリプト
### 1.1. とりあえず作っておきました
（minetaro12さんブログ） [NetplanでUbuntuのネットワーク設定をする](https://0sn.net/posts/20240121/ubuntu-netplan/)
> 初期設定の00-installer-config.yamlや50-cloud-init.yamlは直接編集しないようにする。
> 必ずコピーして99-config.yamlのようなファイルで設定をする。

（Qiita） [【Ubuntu】 /etc/netplan/50-cloud-init.yamlを編集するの止めろ](https://qiita.com/yas-nyan/items/9033fb1d1037dcf9dba5)
> netplanは/etc/netplan/*.yamlを全部参照する

以上の前提にて、次の動作をするスクリプトを書きます。
（１）設定するネットワークインターフェース名を選択（入力）する。
（２）```/etc/netplan/99-config-[インターフェース名].yaml```でファイルを作ってしまう。
（３）```netplan apply```で全てのyamlファイルを読み込む。

- **実行例**
```$ sudo bash netplan_config.sh```
<span />

- **ソースコード** ※これもほとんど生成AI出力
```bash:netplan_config.sh (Ver.2025.1.3)
#!/bin/bash

# YAMLファイルのテンプレート関数 #
yaml_template() {
cat << EOL > "${CONFIG_FILE}"
network:
  version: 2
  ethernets:
    ${interface}:
        optional: true
        ${write_ip}
        ${write_gateway}
        ${write_nameservers}
EOL
}

# ネットワークインターフェースを取得する関数 #
get_interfaces() {
  ip link show | awk -F: '/^[0-9]+: [^lo]/ {print $2}' | tr -d ' '
}

# ネットマスクをプレフィックス値に変換する関数 #
netmask_to_cidr() {
    local netmask=$1
    local cidr=0

    # Check if the input is a valid CIDR format
    if [[ $netmask =~ ^[0-9]+$ ]] && [ "$netmask" -ge 0 ] && [ "$netmask" -le 32 ]; then
        echo "$netmask"
        return
    fi

    # Validate IPv4 netmask format
    if [[ $netmask =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        IFS='.' read -r -a octets <<< "$netmask"
        for octet in "${octets[@]}"; do
            # Ensure each octet is between 0 and 255
            if [[ $octet -lt 0 || $octet -gt 255 ]]; then
                echo "Error: Invalid netmask - octet out of range (0-255)."
                return 1
            fi
            # Count the number of 1 bits in the binary representation of the octet
            while [ $octet -gt 0 ]; do
                cidr=$((cidr + (octet & 1)))
                octet=$((octet >> 1))
            done
        done
        echo "$cidr"
    else
        echo "Error: Invalid netmask format."
    fi
}

# ネットワーク設定の編集関数 #
edit_interface() {
  local interface=$1
  echo "$interface の設定を編集します。"

  # IPアドレスを固定するか？ #
  read -p "このインターフェースに固定IPアドレスを設定しますか？ (y/n): " staticip_enable
  if [[ $staticip_enable = [yY] ]]; then
    # IPアドレス #
    local default_ip="192.168.1.100"
    read -p "IPアドレスを入力してください (例: ${default_ip}): " ip_address
    ip_address=${ip_address:-$default_ip}
    if ! [[ $ip_address =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
      echo "無効なIPアドレス形式です: $ip_address"
      return 1
    fi

    # ネットマスク #
    local default_netmask="255.255.255.0"
    read -p "ネットマスクを入力してください (例: ${default_netmask} または $(netmask_to_cidr "$default_netmask")): " netmask
    netmask=${netmask:-$default_netmask}
    cidr=$(netmask_to_cidr "$netmask")
    if [[ $cidr =~ ^Error ]]; then
      echo "無効なネットマスク形式です: $netmask"
      return 1
    fi
    write_ip="dhcp4: false
        addresses:
          [$ip_address/$cidr]"

    # デフォルトゲートウェイ #
    read -p "このインターフェースにデフォルトゲートウェイを設定しますか？ (y/n): " dgw_enable
    if [[ $dgw_enable = [yY] ]]; then
      local default_gateway="192.168.1.254"
      read -p "ゲートウェイアドレスを入力してください (例: ${default_gateway}): " gateway
      gateway=${gateway:-$default_gateway}
      if ! [[ $gateway =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        echo "無効なゲートウェイアドレス形式です: $gateway"
        return 1
      fi
    write_gateway="routes:
          - to: default
            via: $gateway"
    fi

    # DNSサーバアドレス #
    read -p "プライマリDNSサーバアドレスを設定しますか？ (y/n): " dns_enable
    if [[ $dns_enable = [yY] ]]; then
      local default_dns=${gateway:-192.168.1.254}
      read -p "プライマリDNSサーバアドレスを入力してください (例: ${default_dns}): " primary_dns
      primary_dns=${primary_dns:-$default_dns}

    read -p "セカンダリDNSサーバアドレスを設定しますか？ (y/n): " dns_enable
    if [[ $dns_enable = [yY] ]]; then
      local default_dns="8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1"
      read -p "セカンダリDNSサーバアドレスを入力してください (例: ${default_dns}): " secondary_dns
      secondary_dns=", ${secondary_dns:-$default_dns}"
    fi
    write_nameservers="nameservers:
          addresses:
            [$primary_dns$secondary_dns]"
    fi

  else
   # IPアドレスを固定しない場合はDHCPクライアントを有効にする #
    write_ip="dhcp4: true"
  fi

 # ネットワーク設定をYAMLファイルに書き込む(yaml_template関数実行) #
  # /etc/netplan/99-config-{$インターフェース名}.yaml のフルパス #
  CONFIG_FILE="/etc/netplan/99-config-${interface}.yaml"

  echo -e "${interface} 用の設定を \n ${CONFIG_FILE} へ上書きしています..."
  yaml_template
  chmod 600 $CONFIG_FILE

  echo "現在の設定内容:"
  cat "$CONFIG_FILE"
}

# メニュー表示関数 #
show_menu() {
  echo ""
  echo "===== netplan ネットワーク設定メニュー（IPv4） ====="
  echo "1) ネットワークインターフェースを編集する"
  echo "2) 設定を適用する (netplan apply)"
  echo "q) 終了"
  echo -n "選択してください: "
}

# メイン処理関数 #
main() {
  while true; do
    show_menu
    read choice
    case $choice in
      1)
        echo "現在のIPアドレス状態:"
        ip addr show | grep 'inet'
        interfaces=$(get_interfaces)
        echo "利用可能なネットワークインターフェース:"
        echo "$interfaces"
        echo -n "設定するインターフェース名を入力してください（test指定も可能です）: "
        read interface_name
        if [[ $interfaces =~ $interface_name || $interface_name == "test" ]]; then
          edit_interface "$interface_name"
        else
          echo "無効なインターフェース名です。"
        fi
        ;;
      2)
        echo "最終確認: 新たに設定した $CONFIG_FILE は次の通りです。"
        cat "$CONFIG_FILE"
        echo -n "この内容を適用しますか？ (y/n): "
        read apply_choice
        if [[ $apply_choice == [yY] ]]; then
          rm /etc/netplan/99-config-test.yaml 2>/dev/null
          netplan apply &
          wait
           while true; do
            result0=$(ip addr show | grep "$interface_name" | grep -o inet)
             if [[ -n "$result0" || $interface_name == "test" ]]; then
              break
             fi
            echo "$interface_name 情報更新の待機中..."
            sleep 1
             if ip addr show "$interface_name" 2>/dev/null | grep -q "state DOWN"; then
              echo "[注意!]設定は更新しましたが $interface_name はリンクが確立できていません。(ケーブル抜け、接続先ネットワーク無し、等)"
              break
             fi
           done

           while true; do
            result1=$(ip addr show | grep 'inet')
            sleep 1
            result2=$(ip addr show | grep 'inet')
             if [[ "$result1" == "$result2" ]]; then
              break
             fi
            echo "インターフェース情報更新の待機中..."
           done

          echo "設定が適用されました。"
          echo "現在のIPアドレス状態:"
          ip addr show | grep 'inet'
        else
          echo "設定の適用をキャンセルしました。"
          rm /etc/netplan/99-config-test.yaml 2>/dev/null
        fi
        ;;
      q)
        echo "終了します。"
        rm /etc/netplan/99-config-test.yaml 2>/dev/null
        exit 0
        ;;
      *)
        echo "無効な選択です。もう一度入力してください。"
        ;;
    esac
  done
}

# スクリプト実行時の権限チェック #
if [[ $EUID -ne 0 ]]; then
  echo "このスクリプトは root 権限で実行する必要があります。（例: sudo bash `echo "$0"`）"
  exit 1
fi

# Ubuntuのバージョンチェック #
version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f 2 | tr -d '"')
if (( $(echo "$version <= 20.00" | bc -l) )); then
  echo "Ubuntuのバージョンが20.00以前のため、このツールは対応しません。終了します。"
  exit 1
fi

# メイン処理の呼び出し #
main
```
:::note warn
Ver.2025.1.3 の注意点
各変数のリセットが出来ていないので、連続してインターフェースの設定ができません。
１つのインターフェース設定毎に「q)終了」を行ってください。
:::

### 1.2. 動作の様子（Ubuntu22.04上）
![netplan_config.sh_動作の様子.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667967/0567a2ce-8fe8-be80-a356-e0e520ed42d6.gif)

### 1.3. 動作の様子（生成AIプラットフォーム上）
上記の通りシェルスクリプトで作ってみたのですが、結局メンテナンスは必要になりますし、、
おそらく、生成AIのRAGでNetplanのリファレンスと、書かせたいyamlフォーマットを読み込ませた出力を書かせれば良いのではないか？と思いつつ、
（追伸 2025.1.3 [既に実践されている方がいらっしゃいましたね！](https://qiita.com/kewton/items/508baf689cad36aa6d58) [^LLM生成の例]）
とりあえず作ったシェルスクリプトをRAGにいれてシミュレート動作させてみました。

- 事前に「[ナレッジデータストア](https://zenn.dev/makunugi/books/c5b3307e39dde3/viewer/0a0803)」（[miibo](https://miibo.ai/)での呼称）に「netplan_config.sh」を「テキストデータ」として登録しておく。
<span />

- 入力したプロンプト
```
「netplan_config.sh」の動作を、正確にシミュレートして、
都度ユーザー入力は、私からのインプットを待ってください。
最後、yamlの出力は「```pre」～「```」のコードブロックとして囲ってください。
```
- miiboチャットでの動作の様子
![miiboでシミュレートさせた様子.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667967/3ced03a3-ad8b-5a90-8f49-219e4d2e4c82.gif)

<br>
以上です。


 [^LLM生成の例]:[Raspberry PiでUbuntu ServerのIPアドレスをNetPlanで固定する方法（LLMを活用して効率的に設定ファイルを生成） #RaspberryPi - Qiita](https://qiita.com/kewton/items/508baf689cad36aa6d58)