AnsibleからOpenStackを操作する

## はじめに

本記事は、下記の続きです。

* [install_yamls で作る OpenStack in OpenShift #openshift - Qiita](https://qiita.com/xotaki/items/ca3a4a3cfafa45d84d17)

KVMホストに Ansible をインストールし、上記で構築した OpenStack を操作することが目標です。

## Ansible のインストール

KVMホストに Ansible をインストールします。

```bash
python -m venv venv_ansible
source venv_ansible/bin/activate
vi requirements.txt
```
```plaintext:requirements.txt
openstacksdk
ansible
ansible-core
```
```bash
pip install -r requirements.txt
```

今回はバージョンを指定しませんでしたが、以下のバージョンがインストールされました。
```bash:実行結果例
$ pip freeze | egrep "openstack|ansible"
ansible==8.7.0
ansible-core==2.15.9
openstackclient==4.0.0
openstacksdk==3.0.0
```

Ansible から OpenStack を操作するには、 openstack.cloud という collection が必要なのですが、すでにインストールされています。

```bash:実行結果例
$ ansible-galaxy collection list | grep openstack
openstack.cloud               2.2.0 
```

## 認証情報の設定

前回の記事で構築した OpenStack 環境では、 openstackclient という pod から OpenStack を操作することができます。
openstackclient に接続し、認証情報を `~/.config/openstack/` にコピーします。

```bash
mkdir ~/.config
oc cp openstackclient:.config/openstack ~/.config
```

`clouds.yaml` と `secure.yaml` がコピーされました。

`clouds.yaml` の中を見ると、 `auth_url` が以下のようになっています。

```yaml
            auth_url: https://keystone-public-openstack.apps-crc.testing
```

プロトコルが https なので、rootCA もコピーする必要があります。
`openssl` コマンドで確認すると、 `rootca-public` という rootCA が必要のようです。

```bash:実行結果例
$ openssl s_client -connect keystone-public-openstack.apps-crc.testing:443
CONNECTED(00000003)
depth=1 CN = rootca-public
verify return:1
depth=0 
verify return:1
---
Certificate chain
 0 s:
   i:CN = rootca-public
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Mar 16 08:36:24 2024 GMT; NotAfter: Mar 16 08:36:24 2025 GMT
 1 s:CN = rootca-public
   i:CN = rootca-public
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Mar 16 08:34:29 2024 GMT; NotAfter: Mar 15 08:34:29 2029 GMT
---
Server certificate
(以下略)
```

### rootca-public のインストール

rootCA は `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem` にありますので、確認します。

```bash:実行結果例
$ oc rsh openstackclient cat /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem 
# rootca-public
-----BEGIN CERTIFICATE-----
(中略)
-----END CERTIFICATE-----
# rootca-internal
-----BEGIN CERTIFICATE-----
(中略)
-----END CERTIFICATE-----
# rootca-ovn
-----BEGIN CERTIFICATE-----
(中略)
-----END CERTIFICATE-----
# ACCVRAIZ1
(以下略)
```

今回必要なのは `rootca-public` だけですが、念のため `rootca-public`, `rootca-internal`, `rootca-ovn` のCA認証情報を `/etc/pki/ca-trust/source/anchors/rootca.pem` にコピーし、以下を実行します。

```
sudo update-ca-trust
```

次に、Python のCA認証情報は別ファイルで管理されているので、そちらにも反映します。
反映の手順は下記記事を参考にしました。

* [独自(root)CA のインストール方法 #Python - Qiita](https://qiita.com/msi/items/9cb90271836386dafce3)

```python:local_ca_install.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import certifi

cabundle = certifi.where()
local_rootCA = '/etc/pki/ca-trust/source/anchors/rootca.pem'

print( 'read from {}'.format( local_rootCA ) )
with open( local_rootCA, 'rb' ) as infile:
    myrootca = infile.read()

print( 'append to {}'.format( cabundle ) )
with open( cabundle, 'ab' ) as outfile:
    outfile.write( myrootca )

print( '{} has been imported.'.format( local_rootCA ) )
```
```python
python local_ca_install.py
```

## cirrosイメージ入手

リソースを作成する前に cirrosイメージを入手しておきます。

```
wget https://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img
```

これで準備ができました。

## Ansibleによる OpenStack の各種リソース作成

以下のリソースを作成します。

| 項目 | リソース名 | 備考 |
|-----|-----------|-----|
| project | demo-project | |
| user | demo-user | |
| network | demo-net | |
| subnet | demo-subnet | allocation_pool: 10.0.0.100-10.0.0.250 |
| router | demo-router | public に接続する |
| security group | demo-sg | ssh, icmp を許可 |
| flavor | demo-flavor | |
| keypair | demo-key | 秘密鍵は demo-key-private.pem として保存 |
| image | demo-cirros | |
| volume | demo-volume | demo-cirros の bootable volume |
| server | demo-instance | |

```yaml:all-in-one.yml
---
- hosts: localhost
  tasks:
    - name: admin - create project
      openstack.cloud.project:
        cloud: overcloud
        state: present
        name: demo-project
        description: demo-project
        domain_id: Default
        enabled: True
 
    - name: admin - create user
      openstack.cloud.identity_user:
        cloud: overcloud
        state: present
        name: demo-user
        password: secret
        domain: Default

    - name: admin - grant demo role on the user demo in the project demo-project
      openstack.cloud.role_assignment:
        cloud: overcloud
        user: demo-user
        role: member
        domain: Default
        project: demo-project
 
    - name: set_fact user authentication
      set_fact:
        auth_user:
          auth_url: https://keystone-public-openstack.apps-crc.testing
          password: secret
          project_domain_name: Default
          project_name: demo-project
          user_domain_name: Default
          username: demo-user
 
    - name: user - create network
      openstack.cloud.network:
        auth: "{{ auth_user }}"
        state: present
        name: demo-net

    - name: user - create subnet
      openstack.cloud.subnet:
        auth: "{{ auth_user }}"
        state: present
        name: demo-subnet
        network_name: demo-net
        cidr: 10.0.0.0/24
        dns_nameservers: 192.168.122.1
        enable_dhcp: true
        allocation_pool_start: 10.0.0.100
        allocation_pool_end: 10.0.0.250
 
    - name: user - create router
      openstack.cloud.router:
        auth: "{{ auth_user }}"
        state: present
        name: demo-router
        network: public
        interfaces:
          - demo-subnet
 
    - name: user - create security group
      openstack.cloud.security_group:
        auth: "{{ auth_user }}"
        state: present
        name: demo-sg
 
    - name: user - add security group rule (ssh)
      openstack.cloud.security_group_rule:
        auth: "{{ auth_user }}"
        state: present
        security_group: demo-sg
        protocol: tcp
        port_range_min: 22
        port_range_max: 22
        remote_ip_prefix: 0.0.0.0/0
 
    - name: user - add security group rule (icmp)
      openstack.cloud.security_group_rule:
        auth: "{{ auth_user }}"
        state: present
        security_group: demo-sg
        protocol: icmp
        port_range_min: -1
        port_range_max: -1
        remote_ip_prefix: 0.0.0.0/0
 
    - name: admin - create flavor
      openstack.cloud.compute_flavor:
        cloud: overcloud
        state: present
        name: demo-flavor
        ram: 1024
        vcpus: 1
        disk: 0
 
    - name: user - create keypair
      openstack.cloud.keypair:
        auth: "{{ auth_user }}"
        state: present
        name: demo-key
      register: keypair_info
 
    - name: create private key
      copy:
        content: "{{ keypair_info['keypair']['private_key'] }}"
        dest: ./demo-key-private.pem
        mode: 0600
 
    - name: user - upload cirros image
      openstack.cloud.image:
        auth: "{{ auth_user }}"
        state: present
        name: demo-cirros
        container_format: bare
        disk_format: qcow2
        filename: cirros-0.6.2-x86_64-disk.img
 
    - name: user - create volume
      openstack.cloud.volume:
        auth: "{{ auth_user }}"
        state: present
        volume_type: nfs
        size: 40
        bootable: true
        image: demo-cirros
        display_name: demo-volume
 
    - name: user - create VM
      openstack.cloud.server:
        auth: "{{ auth_user }}"
        name: demo-instance
        boot_volume: demo-volume
        flavor: ce9ed399-b30b-4f92-83a6-917ad1966e72
        key_name: demo-key
        security_groups: demo-sg
        network: demo-net
        floating_ip_pools: public
        state: present
```

※最後のインスタンスを作成するところ、flavorの指定が UUID でないといけないようで、使いにくい……。

では実行します。

```bash
ansible-playbook all-in-one.yml -v
```

openstackclient に接続して確認します。

```bash:実行結果例
$ oc rsh openstackclient 
sh-5.1$ openstack server list --all
+--------------------------------------+---------------+--------+----------------------------------------+--------------------------+-------------+
| ID                                   | Name          | Status | Networks                               | Image                    | Flavor      |
+--------------------------------------+---------------+--------+----------------------------------------+--------------------------+-------------+
| 87a8dbaf-8cbf-4d9f-9a5d-83eac9296615 | demo-instance | ACTIVE | demo-net=10.0.0.147, 192.168.122.208   | N/A (booted from volume) | demo-flavor |
+--------------------------------------+---------------+--------+----------------------------------------+--------------------------+-------------+
sh-5.1$ ping -c 2 192.168.122.208
PING 192.168.122.208 (192.168.122.208) 56(84) bytes of data.
64 bytes from 192.168.122.208: icmp_seq=1 ttl=62 time=1.97 ms
64 bytes from 192.168.122.208: icmp_seq=2 ttl=62 time=1.12 ms

--- 192.168.122.208 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 1.117/1.543/1.970/0.426 ms
sh-5.1$ exit
$ ssh -i demo-key-private.pem cirros@192.168.122.208
The authenticity of host '192.168.122.208 (192.168.122.208)' can't be established.
ED25519 key fingerprint is SHA256:rviyA0kkufqxpfwJZ0Kb0EbbvGlMr5EWMmrsRXZ6x0A.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.122.208' (ED25519) to the list of known hosts.
$ cat /etc/os-release
PRETTY_NAME="CirrOS 0.6.2"
NAME="CirrOS"
VERSION_ID="0.6.2"
ID=cirros
HOME_URL="https://cirros-cloud.net"
BUG_REPORT_URL="https://github.com/cirros-dev/cirros/issues"
```

## Ansibleによる OpenStack の各種リソース削除

詳細は省略しますが、作成のときと逆の順番で削除していきます。

```yaml
---
- hosts: localhost
  tasks:
    - name: set_fact user authentication
      set_fact:
        auth_user:
          auth_url: https://keystone-public-openstack.apps-crc.testing
          password: secret
          project_domain_name: Default
          project_name: demo-project
          user_domain_name: Default
          username: demo-user
 
    - name: user - delete VM
      openstack.cloud.server:
        auth: "{{ auth_user }}"
        state: absent
        name: demo-instance
(以下略)
```