AnsibleでKVMの仮想ゲストを構築し、IP付与させる

# 概要
Ansibleを使用することで、さまざまな作業が自動化できる。
しかし、IPがついていない機器に対してリモート作業を行うのは難しい。
ここでは、KVMのホスト上からAnsibleでIPを付与するまでを自動化させ、リモートでSSHできるまでの手法を記載する。
なお、ユーザーと公開鍵は事前にvirtcloneするマシンの中に含まれているものとする。

## 構成
- ansibleサーバ1台
- ansible実行先仮想ホスト
- 仮想ホストで構築されるVM
検証環境は、Almalinux９で構築する。

# 実行手順

以下のようなAnsible構成である。
- roles
    - kvm_create_vm
        - vars
            - main.yml
        - task
            - main.yml
    - kvm_configure_vm
        - vars
            - main.yml
        - task
            - main.yml
- inventory.yml
- create-yml
- secrets.yml

各ファイルの構成は以下の通り。
### inventory.yml
仮想ゲストのIPと、仮想ホストのIPをいれる。ゲストのオプションは、初めてのホストにsshするためにつける。
```
[kvm_host]
仮想ホストのIP
[vm_ip]
仮想ゲストのIP ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
```
### secrets.yml
ansible vaultで暗号化したユーザー名とパスワードを入れてある。
```
vault_user: "linuxのユーザ名"
vault_pass: "ユーザーのパスワード"
```

### create-yml
各々のタスクを実行する。それぞれ、inventory.ymlと、secrets.ymlを指定している。

```
---

- name: play create vm
  hosts: kvm_host
  vars_files:
    - secrets.yml
  roles:
    - { role: kvm_create_vm, become: yes }
 
- name: play configure vm
  hosts: vm_ip
  vars_files:
    - secrets.yml
  roles:
    - {role: kvm_configure_vm, become: yes}
```

### kvm-create/task/main.yml
interfaceの指定の部分は、ここではシェルで指定しているが、libvirtのNIC付与コマンドを後程指定してやること。
実は、ここの中で、ボリュームの作成等で対話型の内容が出てくるので、`expectモジュール`を使用して、想定する返答をここに記載している。自分のいるフォルダを指定して、コマンドが終わったら別のフォルダに移動することでコマンドを実行させないようにしている。

```
---

- name: Clone_a_KVM-irtual-machine
  command: virt-clone --original template-vmguest --name {{ vmname }} --auto-clone
  become: yes
   
- name: Create_Network_Interfaces
  command: /opt/kickstart/setup/kvm/attach-nic.sh {{ vmname }} {{ item.bridge }} {{ item.ip }}
  loop: "{{ bridge_ip_mapping }}"
  become: yes
 
- name: CPUmax
  command: virsh setvcpus {{ vmname }} {{ cpus }} --config --maximum
- name: Memorymax
  command: virsh setmaxmem {{ vmname }} {{ memorys }} --config
- name: CPUcurrent
  command: virsh setvcpus {{ vmname }} {{ cpus }} --config
- name: Memorycurrent
  command: virsh setmem {{ vmname }} {{ memorys }} --config
 
- name: Volume_add
  command: virsh vol-resize /var/lib/libvirt/images/{{ vmname }}.img {{ VolumeSize }}GiB
  when: VolumeSize != 'default'


- name: Run an interactive command using expect
  expect:
    command: sudo parted /var/lib/libvirt/images/{{ vmname }}.img print
    responses:
      "Fix": fix
  when: VolumeSize != 'default'
  become: yes


- name: test-resize
  command: sudo parted /var/lib/libvirt/images/{{ vmname }}.img resizepart 3 100%
  when: VolumeSize != 'default'
 
- name: kpartx
  command: sudo kpartx -av /var/lib/libvirt/images/{{ vmname }}.img
  when: VolumeSize != 'default'
 
- name: mount
  command: sudo mount /dev/mapper/loop0p3 /mnt
  when: VolumeSize != 'default'
 
- name: xfs_growfs
  command: sudo xfs_growfs /mnt
  when: VolumeSize != 'default'
 
- name: umount
  command: sudo umount /mnt
  when: VolumeSize != 'default'
- name: kpartxd
  command: sudo kpartx -d /var/lib/libvirt/images/{{ vmname }}.img
  when: VolumeSize != 'default'
 
 # to use expect task
- name: utf8-define
  command: sudo localectl set-locale LANG=ja_JP.UTF-8

- name: start vm
  virt:
   name: "{{ vmname }}"
   state: running
- name: Wait for 40 seconds to ensure the VM is up
  pause:
   seconds: 40
- name: Connect to VM and enter username after escape character prompt
  expect:
    command: sudo virsh console {{ vmname }}
    responses:
      'Connected to domain': '{{vault_user}}'
      'パスワード': '{{vault_pass}}'
      'Password:': '{{vault_pass}}'
      '~': 'sudo systemd-machine-id-setup && cd /tmp'
      'UUID': 'sudo reboot'
      '/tmp': 'sudo reboot'
    timeout: 50
  ignore_errors: yes
 
- name: Wait for 40 seconds to ensure the VM is up
  pause:
   seconds: 40
- name: make_firewall
  expect:
    command: sudo virsh console {{ vmname }}
    responses:
      'Connected to domain': '{{vault_user}}'
      'パスワード': '{{vault_pass}}'
      'Password:': '{{vault_pass}}'
      '~': 'sudo firewall-cmd --zone=trusted --add-source=xx.xx.x.x/8 && sudo firewall-cmd --runtime-to-permanent && cd /tmp'
    timeout: 10
  ignore_errors: yes

- name: Connect to VM and make mgmt-ip
  expect:
    command: sudo virsh console {{ vmname }}
    responses:
      'Connected to domain': '{{vault_user}}'
      'パスワード': '{{vault_pass}}'
      'Password:': '{{vault_pass}}'
      'tmp': 'sudo nmcli con add type 802-3-ethernet ifname {{management_interfaces}} con-name {{management_interfaces}} autoconnect no ipv4.method manual ipv4.address "{{management_ip}}" ipv4.gateway {{management_gw}} ipv4.dns c.c.c.c ipv4.dns-options timeout:1,attempts:1,single-request-reopen ipv6.method ignore && cd /var'
      'var': 'sudo nmcli c up enp1s0f1 && cd /home'
    timeout: 10
  ignore_errors: yes

```
### kvm-create/vars/main.yml
IPアドレスを指定すると、ブリッジのMACアドレスを書き換えてくれる。
また、インターフェースがethになるか、enp1s0f0になるか等は事前に調べておく必要がある。
```
vmname: test-vmname-1

bridge_ip_mapping:
  - bridge: bridgename
    ip: 2.2.2.2
  - bridge: bridgename
    ip: 3.3.3.3
cpus: 2
memorys: 8GiB
# if VolumuSize not change "VolumeSize: default"
# if you change volume size 60G, -> VolumeSize: 60
VolumeSize: default
# I couldn't for the life of me determine the interface from the mac address.
management_interfaces: enp1s0f1
management_ip: 2.2.2.2/x
management_gw: 2.2.2.254
```

### kvm_configure_vm/tasks/main.yml
ここで、KVMで新しく作成した仮想マシンをhostにしてタスクが実行されていく。
最初のcreateのタスクで、ゲートウェイをあらかじめ設定してしまっているので、ルーティングの情報を書き換えるようなコマンドを実行している。
また、インターフェースに関連したタスクは、後述のvars/main.ymlに記載したインターフェースの数だけループさせている。

```
---

- name: Set hostname
  ansible.builtin.hostname:
    name: "{{hostname}}"

- name: Add hostname entry to /etc/hosts
  ansible.builtin.lineinfile:
    path: /etc/hosts
    line: "127.0.1.1 {{ ansible_fqdn }} {{ ansible_hostname }}"
    state: present
    create: yes

- name: Update hostname in Zabbix agent configuration
  ansible.builtin.replace:
    path: /etc/zabbix/zabbix_agent2.conf
    regexp: '^Hostname=.*'
    replace: 'Hostname={{ ansible_hostname }}'


- name: Add Ethernet connection
  nmcli:
    conn_name: "{{item.name}}"
    ifname: "{{item.name}}"
    type: ethernet
    autoconnect: true
    state: present
    ip4: "{{ item.ip | default(omit) }}"
    gw4: "{{ item.gateway | default(omit) }}"
    dns4: "{{item.dns | default(omit)}}"
  loop: "{{interfaces}}"

- name: fix_maneged_routing
  ansible.builtin.command:
    cmd: nmcli connection modify "{{management_interface}}" +ipv4.routes "x.x.x.x/y {{management_gw}}"

- name: restart_interfaces
  ansible.builtin.command:
    cmd: sudo nmcli connection up "{{item.name}}"
  loop: "{{interfaces}}"
- name: restart_mgmt_interface
  ansible.builtin.command:
    cmd: sudo nmcli connection up "{{management_interface}}"


# if you already installed,skip this task 
- name: install_shell from git
  ansible.builtin.command:
    cmd: sudo git clone https://gitlab.stream.co.jp/infra/alma9-setup.git /opt/kickstart/setup
  ignore_errors: yes

# Loop over as many interfaces as there are interfaces.
- name: make_firewalld_zone
  ansible.builtin.command:
    cmd: sudo /opt/kickstart/setup/basic/firewalld-zone.sh "{{item.firewallzone}}" "{{item.name}}"
  loop: "{{interfaces}}"

- name: modify_firewalld
  ansible.builtin.command:
    cmd: sudo nmcli con mod "{{item.name}}" connection.zone "{{item.firewallzone}}"-"{{item.name}}"
  loop: "{{interfaces}}"
- name: restart_interface
  ansible.builtin.command:
    cmd: sudo nmcli connection up "{{item.name}}"
  loop: "{{interfaces}}" 
```

### kvm_configure_vm/vars/main.yml

複数作る場合は、インターフェースのnameの数だけ作成していく。
また、管理用のNWに対しては、ループから外すためにインターフェースの変数を別途指定してやる。

```
hostname: test-vmname-1

# modify else mgmtip_interfce
interfaces:
  - name: enp1s0f0
    ip: 3.3.3.3/24
    gateway: 2.2.2.2
    dns:
      - 1.1.1.1
      - b.b.b.b
      - a.a.a.a
    firewallzone: external
  - name: enp1s0f2
    ip: 1.1.1.1
    firewallzone: internal
#
management_gw: 4.4.4.4
management_interface: enp1s0f1
```

# 実行
以下のようにコマンドを実行
- become-pass
    - sudo用のパスワードを実行するために。
- vault-pass
    - secrets.ymlに指定した内容を解凍する。
- -iで、インベントリファイルを指定。
- --private-keyで、秘密鍵を指定する。
```
sudo ansible-playbook -v kvm-create-vm.yml -u ユーザ名 --ask-become-pass -i inventory.yml --private-key="/home/jst-dito/.ssh/honban_id_rsa" --ask-vault-pass

```