[Ansible]Proxmoxでコンソールキーを入力する方法

# はじめに

Proxmoxは仮想化プラットフォームでKVMをベースとした仮想化やLXCのコンテナ仮想化を利用できるOSSです。

APIもかなり充実していてVMの構築・起動からコンソール操作など細かいところに対応しています。
Ansibleからの操作も、[community.generalコレクション](https://docs.ansible.com/ansible/latest/collections/community/general/index.html)にて対応モジュールが多数用意されています。

しかし、VM自動構築でkickstartコマンドを実行しようと思っていたのですが、2023/9時点ではコンソールへのキー入力を行うモジュールはありませんでした。

そのため、URIモジュールとtemplateで無理やり対応してみました。
あまり賢い方法ではないですが、応用の幅が広そうなのでその際のやり方を残します。

# 環境

- Ansible: v8.2.0
- Proxmox: v7.4-16

# 実装

Playbookとしては単純でURIモジュールで[sendkey](https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/qemu/{vmid}/sendkey)APIに1文字ずつコードを送っています。
ただ、Proxmoxでsendkeyを送る際に`!`→`shift-1`のように記号文字をエスケープする必要があります。

エスケープ処理はAnsible実行前にやっておくこともできますが汎用的にしたかったため、Ansible側でエスケープ処理をするようにしました。

エスケープ処理のパターンは以下KVM用の変換ツールを参考にさせてもらっています。

https://github.com/mvidner/sendkeys

## パラメータ

Ansibleに渡すパラメータの例です。
`__send_command`に送信したいコマンドを入力します。

```yaml
__proxmox_host: 192.168.1.1 #<PVEホスト名>
__vm_node: node1 #<PVEノード名> 
__vmid: 100 #<VM ID>
__validate_certs: false #<証明書確認するか>
__proxmox_user: admin #<PVEユーザ>
__proxmox_api_token_id: XXXX #<トークンID>
__proxmox_api_token_secret: XXX #<トークンシークレット>
__send_command: "linux inst.ks=cdrom:/ks.cfg" #<VMコンソールに送信するコマンド>
```

## Playbook

OSインストールのインストール画面を一時中断するためにESCキー、コマンド入力後にEnterキーを押す必要があるのですが1文字で表現できなかったのでPlaybook側でハードコードしています。

時々、送信失敗するので`retries`をつけるのとAnsibleログにAPIシークレットが表示されてしまうので`no_log`設定しています。

```yaml
- name: Send kickstart command
  ansible.builtin.uri:
    url: "https://{{ __proxmox_host }}:8006/api2/json/nodes/{{ __vm_node }}/qemu/{{ __vmid }}/sendkey"
    validate_certs: "{{ __validate_certs }}"
    method: PUT
    return_content: false
    body_format: json
    body: >-
      {
        "key": "{{ item }}"
      }
    headers:
      Content-Type: application/json
      Authorization: >-
        PVEAPIToken={{
          __proxmox_user
        }}!{{
          __proxmox_api_token_id
        }}={{
          __proxmox_api_token_secret
        }}
  no_log: true
  loop: "{{ ['esc'] + lookup('template', 'kickstart_command.j2').split() + ['ret'] }}"
  retries: 5
  delay: 1
```

## Template

Jinja Templateでコマンドを1文字ずつエスケープしています。

詳細な流れは以下の通り

1. コマンド1文字ごとに改行コード(`\n`)を入れる
2. `regex_replace`でエスケープ処理(改行コードが入っているので実質1文字ずつ評価される)
3. Playbook側で作成した1文字ごと改行コードが入った文字列を`split()`でリストに変換

```jinja
{{
  __send_command |
  join('\n') |
  regex_replace(' ', 'spc') |
  regex_replace('\!', 'shift-1') |
  regex_replace('\"', 'shift-apostrophe') |
  regex_replace('\#', 'shift-3') |
  regex_replace('\$', 'shift-4') |
  regex_replace('\%', 'shift-5') |
  regex_replace('\&', 'shift-7') |
  regex_replace('\'', 'apostrophe') |
  regex_replace('\(', 'shift-9') |
  regex_replace('\)', 'shift-0') |
  regex_replace('\*', 'shift-8') |
  regex_replace('\+', 'shift-equal') |
  regex_replace('\,', 'comma') |
  regex_replace('\-', 'minus') |
  regex_replace('\.', 'dot') |
  regex_replace('\/', 'slash') |
  regex_replace('\:', 'shift-semicolon') |
  regex_replace('\;', 'semicolon') |
  regex_replace('\<', 'shift-comma') |
  regex_replace('\=', 'equal') |
  regex_replace('\>', 'shift-dot') |
  regex_replace('\?', 'shift-slash') |
  regex_replace('\@', 'shift-2') |
  regex_replace('\[', 'bracket_left') |
  regex_replace('\]', 'bracket_right') |
  regex_replace('\^', 'shift-6') |
  regex_replace('\_', 'shift-minus') |
  regex_replace('\`', 'grave_accent') |
  regex_replace('\{', 'shift-bracket_left') |
  regex_replace('\|', 'shift-backslash') |
  regex_replace('\}', 'shift-bracket_right') |
  regex_replace('\~', 'shift-grave_accent') |
  regex_replace('([A-Z])', 'shift-\\1') | lower |
  regex_replace('\\\\', 'backslash')
}}
```

# 結果

例として`linux inst.ks=cdrom:/ks.cfg`を入れてみると以下のような結果になります。
きちんと記号部分のみエスケープされているのがわかります。

```shell
ok: [localhost] => {
    "msg": [
        "esc",
        "l",
        "i",
        "n",
        "u",
        "x",
        "spc",
        "i",
        "n",
        "s",
        "t",
        "dot",
        "k",
        "s",
        "equal",
        "c",
        "d",
        "r",
        "o",
        "m",
        "shift-semicolon",
        "slash",
        "k",
        "s",
        "dot",
        "c",
        "f",
        "g",
        "ret"
    ]
}
```

# さいごに

Templateを使うことで簡単な文字列エスケープを実装して、Proxmoxのsendkeyを実行できるようにしてみました。

かなり脳筋実装ではありますが、カスタムモジュールやフィルタを作らなくてもある程度は何とかなることが分かりました。
実装の幅が広がったのはとても良かったです。


vSphereであれば、今回作ったsendkeyを行える[community.vmware.vmware_guest_sendkeyモジュール](https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_sendkey_module.html)あるのでProxmoxとしても同様のモジュールはあってもよさそうです。

ひとまず、[Issue](https://github.com/ansible-collections/community.general/issues/7206)は送ったので時間が空いているときにでも作ってみたいと思います。

# 参考リンク

https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/qemu/{vmid}/sendkey

https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_sendkey_module.html