Ansible + PXE + Kickstart で ESXi Server 上にVMを作成し、OSをインストールする

無償版 ESXi Server 上にVMを作成し、OSをインストールするまでを自動化できないか試行錯誤した結果をまとめます。

## 目標

* 無償版 ESXi Server に Ansible から指定する構成で VM を作成できるようにする
* VMを起動すると PXE boot、Kickstart によりOSを自動でインストールできるようにする

## 構成

* ESXi Server (192.168.33.50)
    * VMware vSphere Hypervisor 7.0
* Ansible client (192.168.33.11)
    * CentOS8
    * Ansible 2.8 + vsphere_guest モジュール
* PXE / Kickstart Server (192.168.33.10)
    * CentOS8
    * tftp-server
    * dhcp
    * httpd
    * syslinux

### Ansible client の構成

無償版ESXi ServerをAnsibleから操作する場合、Ansible2.9でobsoleteとなった vsphere_guest を選択するしかないようです。（公式モジュールの vmware_guest を使用したら以下のエラーが出て使えませんでした。）

> Failed to create virtual machine due to product versioning restrictions: Current license or ESXi version prohibits execution of the requested operation.

※ vsphere_guest と vmware_guest の比較については下記記事を参照してください：

* [Ansible: vSphere Guest モジュールについてのまとめ - Qiita](https://qiita.com/hirofumihida/items/e793b8119154e2970001)

仕方がないので、virtualenv で Ansible2.8 環境をつくることにします。

### PXE / Kickstart Server の構成

PXE, Kickstart によるOSインストールの流れは以下の通りです：

1. VMの電源を入れる
2. VMがdhcp serverに通信する
3. dhcp serverがIPアドレスとPXE ServerのIPアドレスを返す
4. tftp server に PXE boot file を取りに行く
5. http server に Kickstart file, OS イメージを取りに行く
6. OSの自動インストール

4.の処理では、まず、MACアドレスに対応する名前(16進数が小文字で表記されハイフンで区切られたもの)のブート構成ファイルを検索する仕様になっています。たとえば、MACアドレスが88:99:AA:BB:CC:DDの場合は、01-88-99-aa-bb-cc-dd (01-{MAC-ADDRESS}) というファイルを検索します。見つからない場合は default という名前のファイルを検索します。

したがって、ESXi Server にVMを作成するときは、MACアドレスを手動で設定し、そのMACアドレスに対応した PXE boot file を配置するとよさそうです。

## 資材

以下のISOイメージを用意します。

* [VMware-VMvisor-Installer-7.0.0-15843807.x86_64.iso](https://my.vmware.com/jp/web/vmware/evalcenter?p=free-esxi7)
* [CentOS-8.2.2004-x86_64-dvd1.iso](http://isoredirect.centos.org/centos/8/isos/x86_64/)

## Ansible client のインストール

pysphere は Python2系のようなので、Python2をインストールします。Ansibleは2.8系の最新版である 2.8.12 をインストールします。

```
sudo dnf -y install python2
sudo pip2 install virtualenv
python2 -m virtualenv ansible-py2
source ansible-py2/bin/activate
pip install ansible==2.8.12
pip install pysphere
ansible -i localhost, all -m ping -c local
```

最後の `ansible` コマンドで pong が返れば成功です。

## PXE / Kickstart Server のインストール

dhcp server, tftp server, http server をインストールし、各種資材を配置していきます。

### dhcp server のインストール

以下の設計でインストール・設定します。

| パラメータ | 値 |
|----------|----|
| subnet | 192.168.33.0/24 |
| range | 192.168.33.100 - 192.168.33.199 |
| pxe server | 192.168.33.10 |
| boot | BIOS |

```
sudo dnf -y install dhcp-server
sudo vi /etc/dhcp/dhcpd.conf
sudo systemctl enable --now dhcpd
sudo firewall-cmd --add-service=dhcp --permanent 
sudo firewall-cmd --reload 
```

```/etc/dhcp/dhcpd.conf
option space pxelinux;
option pxelinux.magic code 208 = string;
option pxelinux.configfile code 209 = text;
option pxelinux.pathprefix code 210 = text;
option pxelinux.reboottime code 211 = unsigned integer 32;
option architecture-type code 93 = unsigned integer 16;

authoritative;
default-lease-time 3600;
max-lease-time 43200;

subnet 192.168.33.0 netmask 255.255.255.0 {
        range 192.168.33.100 192.168.33.199;
        option subnet-mask 255.255.255.0;
        option broadcast-address 192.168.0.255;

        class "pxeclients" {
                match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
                next-server 192.168.33.10; # pxe server
                filename "pxelinux.0";
        }
}
```

### tftp server のインストール

```
sudo dnf -y install tftp-server
sudo systemctl enable --now tftp
sudo firewall-cmd --add-service=tftp --permanent
sudo firewall-cmd --reload 
```

### http server のインストール

Kickstart file は `/var/www/html/pub/kickstart` に配置することとします。

```
sudo dnf -y install httpd 
sudo systemctl enable --now httpd 
sudo firewall-cmd --add-service=http --permanent 
sudo firewall-cmd --reload 
```

### tftp server および http server に PXE boot file, OSイメージの資材を配置

tftp server のコンテンツは `/var/lib/tftpboot/` に、CentOS のメディアは `/var/www/html/pub/centos8/` に配置します。
資材は syslinux パッケージ、CentOS8 のメディアから取得します。

```
sudo dnf -y install syslinux
sudo cp /usr/share/syslinux/pxelinux.0 /var/lib/tftpboot/ 
sudo cp /usr/share/syslinux/{menu.c32,vesamenu.c32,ldlinux.c32,libcom32.c32,libutil.c32} /var/lib/tftpboot/ 
sudo mkdir -p /var/www/html/pub/centos8
sudo mount -t iso9660 -o loop,ro ~/CentOS-8.2.2004-x86_64-dvd1.iso /var/www/html/pub/centos8
```

PXE boot file は後程 Ansible で作成します。

### Kickstart file の配置

Kickstart file の書きかたについては、Red Hat のマニュアルが詳しいです。詳細はここでは割愛します。

* [パート V. キックスタートの参照 Red Hat Enterprise Linux 8 | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/performing_an_advanced_rhel_installation/kickstart_references)

## Ansible playbook の作成

環境が用意できたので、いよいよ Ansible playbook を書きます。
処理したい内容は以下の通りです：

1. PXE boot file を MACアドレスごとに生成する
2. Kickstart file を VMごとに生成する
3. ESXi にVMを作成し、起動する

### inventory

以下のようにVMごとにホスト名とMACアドレス、NWの設定をセットにして、リスト形式で定義します。

```inventory/esxi/group_vars/all.yml
---
## createvms (ESXi Server)
esx_hostname: esxi
esx_ipaddr: 192.168.11.50
esx_user: root
esx_password: <<PASSWORD>>
esx_datastore_name: datastore1

## create VMs
vms:
  - hostname: server-a
    notes: server-a
    macaddr: 52:54:00:98:09:69
    disksize: 10
    nic1_network: "2032_ProviApp"
    nic2_network: "VM Network"
    memory: 2048
    cpus: 1
    osid: centos8_64Guest
    network:
      - "--bootproto=dhcp --device=ens192 --ipv6=auto"
      - "--bootproto=static --device=ens224 --ip=192.168.11.20 --netmask=255.255.255.0 --gateway=192.168.11.1 --ipv6=auto --activate"
  - hostname: server-b
    notes: server-b
    macaddr: 52:54:00:98:09:70
    disksize: 10
    nic1_network: "2032_ProviApp"
    nic2_network: "VM Network"
    memory: 2048
    cpus: 1
    osid: centos8_64Guest
    network:
      - "--bootproto=dhcp --device=enp0s3 --ipv6=auto"
      - "--bootproto=static --device=enp0s8 --ip=192.168.11.21 --netmask=255.255.255.0 --gateway=192.168.11.1 --ipv6=auto --activate"

```

```inventory/esxi/group_vars/pxeserver.yml
---
## pxe boot files
pxe_basedir: "/var/lib/tftpboot"
pxe_ks_basedir: "{{ pxe_basedir }}/pxelinux.cfg"
pxe_img_basedir: "{{ pxe_basedir }}/images/CentOS8"
pxe_label: "Kickstart Install CentOS8 x64 with local Repository"
pxe_kernel: "images/CentOS8/vmlinuz"
pxe_initrd: "images/CentOS8/initrd.img"
pxe_server: "192.168.10.1"

## Kickstart files
ks_basedir: "/var/www/html/pub/kickstart"
ks_server: "{{ dhcpd_next_server }}"
```

### PXE boot file を MACアドレスごとに生成する

Ansible playbook は以下のように書きます。

```pxeserver.yml
---
- hosts: pxeserver
  become: yes
  gather_facts: no
  tasks:
    - name: create PXE boot files
      template:
        src: pxelinux.cfg.j2
        dest: "{{ pxe_ks_basedir }}/01-{{ item.macaddr | regex_replace(':','-') }}"
      with_items:
        - "{{ vms }}"
```

```pxelinux.cfg.j2
default 1
PROMPT 0
TIMEOUT 30
ONTIMEOUT local

menu title ######## PXE Boot Menu ##########

label 1
  menu label ^1) {{ pxe_label }}
  kernel {{ pxe_kernel }}
  append initrd={{ pxe_initrd }} inst.ks=http://{{ pxe_server }}/pub/kickstart/{{ item.hostname }}.cfg
```

生成するファイルは「01-{MACアドレス}」という命名規則ですが、「-」区切りなので、インベントリに「:」区切りで定義しても置換するようにしています。

### Kickstart file を VMごとに生成する

Ansible playbook は以下のように書きます。（先ほどの pxeserver.yml に続けて書きます）

```pxeserver.yml
    - name: create Kickstart directory
      file:
        path: "{{ ks_basedir }}"
        state: directory
        owner: "root"
        group: "root"
        mode: "0755"
        recurse: "yes"
    - name: create Kickstart files
      template:
        src: ks.cfg.j2
        dest: "{{ ks_basedir }}/{{ item.hostname }}.cfg"
      with_items:
        - "{{ vms }}"
```

```ks.cfg.j2
# System authorization information
auth --enableshadow --passalgo=sha512

# Use CDROM installation media
# cdrom
# # Use network installation
url --url=http://{{ ks_server }}/pub/centos8/

# Use text install
text
# Run the Setup Agent on first boot
firstboot --enable
ignoredisk --only-use=sda
# Keyboard layouts
keyboard --vckeymap=jp --xlayouts='jp'
# System language
lang ja_JP.UTF-8

# Network information
{% for network in item.network %}
network {{ network }}
{% endfor %}
network  --hostname={{ item.hostname }}
(以下略)
```

### ESXi にVMを作成し、起動する

vsphere_guest モジュールを使用します。使いかたは公式ドキュメントを参照してください。

* [vsphere_guest – Create/delete/manage a guest VM through VMware vSphere &mdash; Ansible Documentation](https://docs.ansible.com/ansible/2.8/modules/vsphere_guest_module.html)

```createvms.yml
---
- hosts: 127.0.0.1
  connection: local
  user: root
  sudo: false
  gather_facts: false
  serial: 1
  tasks:
    - name: Deploy guest VMs
      vsphere_guest:
        vcenter_hostname: "{{ esx_ipaddr }}"
        validate_certs: no
        username: "{{ esx_user }}"
        password: "{{ esx_password }}"
        guest: "{{ item.hostname }}"
        state: powered_off
        vm_hw_version: vmx-08
        vm_extra_config:
          vcpu.hotadd: yes
          mem.hotadd:  yes
          notes: "{{ item.notes }}"
        vm_disk:
          disk1:
            size_gb: "{{ item.disksize }}"
            type: thin
            datastore: "{{ esx_datastore_name }}"
        vm_nic:
          nic1:
            type: vmxnet3
            network: "{{ item.nic1_network }}"
            network_type: standard
            mac_address: "{{ item.macaddr | regex_replace('-',':') }}"
          nic2:
            type: vmxnet3
            network: "{{ item.nic2_network }}"
            network_type: standard
        vm_hardware:
          memory_mb: "{{ item.memory }}"
          num_cpus: "{{ item.cpus }}"
          osid: "{{ item.osid }}"
          scsi: paravirtual
        esxi:
          datacenter: ha-datacenter
          hostname: "{{ esx_hostname  }}"
      with_items:
        - "{{ vms }}"
```

PXE boot file と違って、MACアドレスの指定は「:」区切りでないとエラーになりますので、インベントリに「-」区切りで定義しても置換するようにしています。
また、`osid` については、以下のページを参考にしてください。

* [Enum - VirtualMachineGuestOsIdentifier(vim.vm.GuestOsDescriptor.GuestOsIdentifier)](https://vdc-download.vmware.com/vmwb-repository/dcr-public/da47f910-60ac-438b-8b9b-6122f4d14524/16b7274a-bf8b-4b4c-a05e-746f2aa93c8c/doc/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html)

## Ansible playbook の実行

pxeserver.yml で PXE boot file および Kickstart file を作成し、createvms.yml で ESXi Server にVMを作成し、電源を入れます。

```
ansible-playbook -i inventory/esxi/inventory.ini pxeserver.yml
ansible-playbook -i inventory/esxi/inventory.ini createvms.yml
```

うまくいくと、VMが作成され、電源が入った後、OSが自動でインストールされます。


## 参考

* [Ansible: vSphere Guest モジュールについてのまとめ - Qiita](https://qiita.com/hirofumihida/items/e793b8119154e2970001)
* [[Ansible] CentOS 8 に Ansible をインストールする（Python 3 + venv + pip） - てくなべ (tekunabe)](https://tekunabe.hatenablog.jp/entry/2019/10/06/ansible_centos8_python3_pip)
* [パート V. キックスタートの参照 Red Hat Enterprise Linux 8 | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/performing_an_advanced_rhel_installation/kickstart_references)
* [vsphere_guest – Create/delete/manage a guest VM through VMware vSphere &mdash; Ansible Documentation](https://docs.ansible.com/ansible/2.8/modules/vsphere_guest_module.html)
* [Enum - VirtualMachineGuestOsIdentifier(vim.vm.GuestOsDescriptor.GuestOsIdentifier)](https://vdc-download.vmware.com/vmwb-repository/dcr-public/da47f910-60ac-438b-8b9b-6122f4d14524/16b7274a-bf8b-4b4c-a05e-746f2aa93c8c/doc/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html)