From d9451f49f36f6851f2d10cc2abe31e3fdfd84ab6 Mon Sep 17 00:00:00 2001
From: Matt Crees <mattc@stackhpc.com>
Date: Mon, 24 Apr 2023 17:11:00 +0100
Subject: [PATCH] Add support for checking Octavia cert expiration

Adds a flag ``kolla-ansible octavia-certificates --check-expiry <days>``
to the ``octavia-certificates`` command to check if the certificates
will expire within a given number of days.

Change-Id: I869b8afd85fe282d823ecf3593aa22f94a61b2a0
---
 .../octavia-certificates/defaults/main.yml    |  3 +
 .../tasks/check_expiry.yml                    | 24 ++++++
 .../roles/octavia-certificates/tasks/main.yml | 77 ++++++++++---------
 doc/source/reference/networking/octavia.rst   | 10 +++
 ...k-certificate-expiry-9a80a68cf31cbba4.yaml |  7 ++
 tests/run.yml                                 |  2 +
 tests/test-octavia.sh                         |  9 +++
 tools/kolla-ansible                           | 12 ++-
 8 files changed, 108 insertions(+), 36 deletions(-)
 create mode 100644 ansible/roles/octavia-certificates/tasks/check_expiry.yml
 create mode 100644 releasenotes/notes/octavia-check-certificate-expiry-9a80a68cf31cbba4.yaml

diff --git a/ansible/roles/octavia-certificates/defaults/main.yml b/ansible/roles/octavia-certificates/defaults/main.yml
index 67fe9085a..2061dbe43 100644
--- a/ansible/roles/octavia-certificates/defaults/main.yml
+++ b/ansible/roles/octavia-certificates/defaults/main.yml
@@ -43,3 +43,6 @@ octavia_certs_client_req_organizational_unit: "{{ octavia_certs_organizational_u
 # NOTE(yoctozepto): This should ideally be per controller, i.e. controller
 # generates its key&CSR and this CA signs it.
 octavia_certs_client_req_common_name: client.example.org
+
+# Used with command `kolla-ansible octavia-certificates --check-expiry <days>`.
+octavia_certs_check_expiry: "no"
diff --git a/ansible/roles/octavia-certificates/tasks/check_expiry.yml b/ansible/roles/octavia-certificates/tasks/check_expiry.yml
new file mode 100644
index 000000000..66ed8e4b0
--- /dev/null
+++ b/ansible/roles/octavia-certificates/tasks/check_expiry.yml
@@ -0,0 +1,24 @@
+---
+- name: Gather information on certificates
+  community.crypto.x509_certificate_info:
+    path: "{{ node_custom_config }}/octavia/{{ item }}"
+    valid_at:
+      point_1: "+{{ octavia_certs_expiry_limit | int }}d"
+  register: cert_info
+  delegate_to: localhost
+  with_items:
+    - "server_ca.cert.pem"
+    - "client_ca.cert.pem"
+    - "client.cert-and-key.pem"
+
+- name: Check whether certificates are valid within {{ octavia_certs_expiry_limit }} days
+  assert:
+    that:
+      - item.valid_at.point_1
+    fail_msg: "{{ item.item }} will expire within {{ octavia_certs_expiry_limit }} days, on {{ item.not_after }}"
+    success_msg: "{{ item.item }} will not expire within {{ octavia_certs_expiry_limit }} days. It expires on {{ item.not_after }}"
+    quiet: True
+  loop: "{{ cert_info.results }}"
+  loop_control:
+    label: "{{ item.item }}"
+  delegate_to: localhost
diff --git a/ansible/roles/octavia-certificates/tasks/main.yml b/ansible/roles/octavia-certificates/tasks/main.yml
index ed58ec436..9ba737b2b 100644
--- a/ansible/roles/octavia-certificates/tasks/main.yml
+++ b/ansible/roles/octavia-certificates/tasks/main.yml
@@ -7,38 +7,45 @@
 # Kolla Ansible prepares and controls the Client CA certificate and key.
 # Client CA is used to generate certificates for Octavia controllers.
 
-- name: Ensure server_ca and client_ca directories exist
-  file:
-    path: "{{ octavia_certs_work_dir }}/{{ item }}"
-    state: "directory"
-    mode: 0770
-  loop:
-    - server_ca
-    - client_ca
-
-- name: Copy openssl.cnf
-  copy:
-    src: "{{ octavia_certs_openssl_cnf_path }}"
-    dest: "{{ octavia_certs_work_dir }}/openssl.cnf"
-
-- import_tasks: server_ca.yml
-
-- import_tasks: client_ca.yml
-
-- import_tasks: client_cert.yml
-
-- name: Ensure {{ node_custom_config }}/octavia directory exists
-  file:
-    path: "{{ node_custom_config }}/octavia"
-    state: "directory"
-    mode: 0770
-
-- name: Copy the to-be-deployed keys and certs to {{ node_custom_config }}/octavia
-  copy:
-    src: "{{ octavia_certs_work_dir }}/{{ item.src }}"
-    dest: "{{ node_custom_config }}/octavia/{{ item.dest }}"
-  with_items:
-    - { src: "server_ca/server_ca.cert.pem", dest: "server_ca.cert.pem" }
-    - { src: "server_ca/server_ca.key.pem", dest: "server_ca.key.pem" }
-    - { src: "client_ca/client_ca.cert.pem", dest: "client_ca.cert.pem" }
-    - { src: "client_ca/client.cert-and-key.pem", dest: "client.cert-and-key.pem" }
+- name: Check if any certificates are going to expire
+  include_tasks: check_expiry.yml
+  when: octavia_certs_check_expiry | bool
+
+- block:
+    - name: Ensure server_ca and client_ca directories exist
+      file:
+        path: "{{ octavia_certs_work_dir }}/{{ item }}"
+        state: "directory"
+        mode: 0770
+      loop:
+        - server_ca
+        - client_ca
+
+    - name: Copy openssl.cnf
+      copy:
+        src: "{{ octavia_certs_openssl_cnf_path }}"
+        dest: "{{ octavia_certs_work_dir }}/openssl.cnf"
+
+    - import_tasks: server_ca.yml
+
+    - import_tasks: client_ca.yml
+
+    - import_tasks: client_cert.yml
+
+    - name: Ensure {{ node_custom_config }}/octavia directory exists
+      file:
+        path: "{{ node_custom_config }}/octavia"
+        state: "directory"
+        mode: 0770
+
+    - name: Copy the to-be-deployed keys and certs to {{ node_custom_config }}/octavia
+      copy:
+        src: "{{ octavia_certs_work_dir }}/{{ item.src }}"
+        dest: "{{ node_custom_config }}/octavia/{{ item.dest }}"
+      with_items:
+        - { src: "server_ca/server_ca.cert.pem", dest: "server_ca.cert.pem" }
+        - { src: "server_ca/server_ca.key.pem", dest: "server_ca.key.pem" }
+        - { src: "client_ca/client_ca.cert.pem", dest: "client_ca.cert.pem" }
+        - { src: "client_ca/client.cert-and-key.pem", dest: "client.cert-and-key.pem" }
+
+  when: not octavia_certs_check_expiry | bool
diff --git a/doc/source/reference/networking/octavia.rst b/doc/source/reference/networking/octavia.rst
index 735f5a180..72d2a0443 100644
--- a/doc/source/reference/networking/octavia.rst
+++ b/doc/source/reference/networking/octavia.rst
@@ -75,6 +75,16 @@ used to encrypt the CA key:
 
 .. _octavia-network:
 
+Monitoring certificate expiry
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can use the following command to check if any of the certificates will
+expire within a given number of days:
+
+.. code-block:: console
+
+   kolla-ansible octavia-certificates --check-expiry <days>
+
 Networking
 ----------
 
diff --git a/releasenotes/notes/octavia-check-certificate-expiry-9a80a68cf31cbba4.yaml b/releasenotes/notes/octavia-check-certificate-expiry-9a80a68cf31cbba4.yaml
new file mode 100644
index 000000000..46f832abd
--- /dev/null
+++ b/releasenotes/notes/octavia-check-certificate-expiry-9a80a68cf31cbba4.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    The flag ``--check-expiry`` has been added to the ``octavia-certificates``
+    command. ``kolla-ansible octavia-certificates --check-expiry <days>`` will
+    check if the Octavia certificates are set to expire within a given number
+    of days.
diff --git a/tests/run.yml b/tests/run.yml
index b89b8a261..47c4e8ae2 100644
--- a/tests/run.yml
+++ b/tests/run.yml
@@ -491,6 +491,8 @@
             executable: /bin/bash
             chdir: "{{ kolla_ansible_src_dir }}"
           when: scenario == "octavia"
+          environment:
+            KOLLA_ANSIBLE_VENV_PATH: "{{ kolla_ansible_venv_path }}"
 
         - name: Run test-masakari.sh script
           script:
diff --git a/tests/test-octavia.sh b/tests/test-octavia.sh
index 4ba3457a2..7f61093ce 100644
--- a/tests/test-octavia.sh
+++ b/tests/test-octavia.sh
@@ -8,6 +8,12 @@ set -o errexit
 # Enable unbuffered output for Ansible in Jenkins.
 export PYTHONUNBUFFERED=1
 
+function check_certificate_expiry {
+    RAW_INVENTORY=/etc/kolla/inventory
+    source $KOLLA_ANSIBLE_VENV_PATH/bin/activate
+    kolla-ansible octavia-certificates --check-expiry 7
+    deactivate
+}
 
 function register_amphora_image {
     amphora_url=https://tarballs.opendev.org/openstack/octavia/test-images/test-only-amphora-x64-haproxy-ubuntu-focal.qcow2
@@ -79,6 +85,9 @@ function test_octavia {
 }
 
 function test_octavia_logged {
+    # Check if any certs expire within a week.
+    check_certificate_expiry
+
     . /etc/kolla/admin-openrc.sh
     . ~/openstackclient-venv/bin/activate
     test_octavia
diff --git a/tools/kolla-ansible b/tools/kolla-ansible
index 9fce6e824..278a2dbf2 100755
--- a/tools/kolla-ansible
+++ b/tools/kolla-ansible
@@ -197,6 +197,7 @@ Commands:
     stop                 Stop Kolla containers
     certificates         Generate self-signed certificate for TLS *For Development Only*
     octavia-certificates Generate certificates for octavia deployment
+                             --check-expiry <days> to check if certificates expire within that many days
     upgrade              Upgrades existing OpenStack Environment
     upgrade-bifrost      Upgrades an existing bifrost container
     genconfig            Generate configuration files for enabled OpenStack services
@@ -263,7 +264,7 @@ function version {
 check_environment_coherence
 
 SHORT_OPTS="hi:p:t:k:e:CD:v"
-LONG_OPTS="help,version,inventory:,playbook:,skip-tags:,tags:,key:,extra:,check,diff,verbose,configdir:,passwords:,limit:,forks:,vault-id:,ask-vault-pass,vault-password-file:,yes-i-really-really-mean-it,include-images,include-dev:,full,incremental"
+LONG_OPTS="help,version,inventory:,playbook:,skip-tags:,tags:,key:,extra:,check,diff,verbose,configdir:,passwords:,limit:,forks:,vault-id:,ask-vault-pass,vault-password-file:,yes-i-really-really-mean-it,include-images,include-dev:,full,incremental,check-expiry:"
 
 RAW_ARGS="$*"
 ARGS=$(getopt -o "${SHORT_OPTS}" -l "${LONG_OPTS}" --name "$0" -- "$@") || { usage >&2; exit 2; }
@@ -281,6 +282,7 @@ DANGER_CONFIRM=
 INCLUDE_IMAGES=
 INCLUDE_DEV=
 BACKUP_TYPE="full"
+OCTAVIA_CERTS_EXPIRY=
 # Serial is not recommended and disabled by default. Users can enable it by
 # configuring ANSIBLE_SERIAL variable.
 ANSIBLE_SERIAL=${ANSIBLE_SERIAL:-0}
@@ -398,6 +400,11 @@ while [ "$#" -gt 0 ]; do
             shift 1
             ;;
 
+    (--check-expiry)
+            OCTAVIA_CERTS_EXPIRY="$2"
+            shift 2
+            ;;
+
     (--version)
             version
             exit 0
@@ -532,6 +539,9 @@ EOF
 (octavia-certificates)
         ACTION="Generate octavia Certificates"
         PLAYBOOK="${BASEDIR}/ansible/octavia-certificates.yml"
+        if [[ ! -z "${OCTAVIA_CERTS_EXPIRY}" ]]; then
+            EXTRA_OPTS="$EXTRA_OPTS -e octavia_certs_check_expiry=yes -e octavia_certs_expiry_limit=${OCTAVIA_CERTS_EXPIRY}"
+        fi
         ;;
 (genconfig)
         ACTION="Generate configuration files for enabled OpenStack services"
-- 
GitLab