From 2c58388ac336456369b5ee277cb6896f58b07c44 Mon Sep 17 00:00:00 2001
From: Will Szumski <will@stackhpc.com>
Date: Thu, 7 Mar 2024 10:56:53 +0000
Subject: [PATCH] Add seed service destroy

This can be useful when using a seed VM that is not deployed by kayobe,
and can therefore not use ``kayobe seed deprovision``, especially when
iterating on kayobe-config during the inital stages of a deployment, or
for development, where you want to re-run the playbooks from a clean-ish
state.

Change-Id: I43a9c2a57fcfe2c9d84f39903aac7c258f9a897f
---
 .../roles/deploy-containers/defaults/main.yml |  3 ++
 .../tasks/deploy-container.yml                | 38 ++++++++++++++
 .../roles/deploy-containers/tasks/deploy.yml  | 50 ++++++-------------
 .../tasks/destroy-container.yml               | 15 ++++++
 .../roles/deploy-containers/tasks/destroy.yml |  6 +++
 .../roles/deploy-containers/tasks/main.yml    | 16 +-----
 ansible/seed-deploy-containers.yml            |  2 +
 doc/source/administration/seed.rst            | 23 +++++++++
 .../reference/seed-custom-containers.rst      | 25 ++++++++--
 kayobe/cli/commands.py                        | 44 ++++++++++++++--
 kayobe/tests/unit/cli/test_commands.py        |  3 ++
 ...seed-service-destroy-1bdf79990d050e68.yaml |  6 +++
 setup.cfg                                     |  3 ++
 13 files changed, 177 insertions(+), 57 deletions(-)
 create mode 100644 ansible/roles/deploy-containers/tasks/deploy-container.yml
 create mode 100644 ansible/roles/deploy-containers/tasks/destroy-container.yml
 create mode 100644 ansible/roles/deploy-containers/tasks/destroy.yml
 create mode 100644 releasenotes/notes/adds-seed-service-destroy-1bdf79990d050e68.yaml

diff --git a/ansible/roles/deploy-containers/defaults/main.yml b/ansible/roles/deploy-containers/defaults/main.yml
index 9130164e..0838be95 100644
--- a/ansible/roles/deploy-containers/defaults/main.yml
+++ b/ansible/roles/deploy-containers/defaults/main.yml
@@ -1,4 +1,7 @@
 ---
+# Action to perform: One of: "deploy", "destroy".
+deploy_containers_action: "deploy"
+
 deploy_containers_defaults:
   comparisons:
     image: strict
diff --git a/ansible/roles/deploy-containers/tasks/deploy-container.yml b/ansible/roles/deploy-containers/tasks/deploy-container.yml
new file mode 100644
index 00000000..0c5ccc08
--- /dev/null
+++ b/ansible/roles/deploy-containers/tasks/deploy-container.yml
@@ -0,0 +1,38 @@
+---
+- name: "[{{ container_name }}] Ensure we have latest image"
+  docker_image:
+    name: "{{ container_config.image }}"
+    tag: "{{ container_config.tag | default(omit) }}"
+    source: pull
+
+- name: "[{{ container_name }}] Include tasks file for pre task(s)"
+  include_tasks: "{{ container_config.pre }}"
+  when: container_config.pre is defined
+
+- name: "[{{ container_name }}] Start container"
+  docker_container:
+    capabilities: "{{ container_config.capabilities | default(omit) }}"
+    command: "{{ container_config.command | default(omit) }}"
+    comparisons: "{{ container_config.comparisons | default(deploy_containers_defaults.comparisons) }}"
+    detach: "{{ container_config.detach | default(deploy_containers_defaults.detach) }}"
+    env: "{{ container_config.env | default(omit) }}"
+    name: "{{ container_name }}"
+    network_mode: "{{ container_config.network_mode | default(deploy_containers_defaults.network_mode) }}"
+    image: "{{ container_config.image }}:{{ container_config.tag | default('latest') }}"
+    init: "{{ container_config.init | default(deploy_containers_defaults.init) }}"
+    ipc_mode: "{{ container_config.ipc_mode | default(omit) }}"
+    pid_mode: "{{ container_config.pid_mode | default(omit) }}"
+    ports: "{{ container_config.ports | default(omit) }}"
+    privileged: "{{ container_config.privileged | default(omit) }}"
+    restart_policy: "{{ container_config.restart_policy | default(deploy_containers_defaults.restart_policy) }}"
+    shm_size: "{{ container_config.shm_size | default(omit) }}"
+    sysctls: "{{ container_config.sysctls | default(omit) }}"
+    timeout: "{{ deploy_containers_docker_api_timeout }}"
+    ulimits: "{{ container_config.ulimits | default(omit) }}"
+    user: "{{ container_config.user | default(omit) }}"
+    volumes: "{{ container_config.volumes | default(omit) }}"
+  become: true
+
+- name: "[{{ container_name }}] Include tasks file for post task(s)"
+  include_tasks: "{{ container_config.post }}"
+  when: container_config.post is defined
diff --git a/ansible/roles/deploy-containers/tasks/deploy.yml b/ansible/roles/deploy-containers/tasks/deploy.yml
index d146f21a..79796b4b 100644
--- a/ansible/roles/deploy-containers/tasks/deploy.yml
+++ b/ansible/roles/deploy-containers/tasks/deploy.yml
@@ -1,37 +1,17 @@
 ---
-- name: "[{{ container_name }}] Ensure we have latest image"
-  docker_image:
-    name: "{{ container_config.image }}"
-    tag: "{{ container_config.tag | default(omit) }}"
-    source: pull
+- name: Login to docker registry
+  docker_login:
+    registry_url: "{{ kolla_docker_registry or omit }}"
+    username: "{{ kolla_docker_registry_username }}"
+    password: "{{ kolla_docker_registry_password }}"
+    reauthorize: yes
+  when:
+    - deploy_containers_registry_attempt_login | bool
+  become: true
 
-- name: "[{{ container_name }}] Include tasks file for pre task(s)"
-  include_tasks: "{{ container_config.pre }}"
-  when: container_config.pre is defined
-
-- name: "[{{ container_name }}] Start container"
-  docker_container:
-    capabilities: "{{ container_config.capabilities | default(omit) }}"
-    command: "{{ container_config.command | default(omit) }}"
-    comparisons: "{{ container_config.comparisons | default(deploy_containers_defaults.comparisons) }}"
-    detach: "{{ container_config.detach | default(deploy_containers_defaults.detach) }}"
-    env: "{{ container_config.env | default(omit) }}"
-    name: "{{ container_name }}"
-    network_mode: "{{ container_config.network_mode | default(deploy_containers_defaults.network_mode) }}"
-    image: "{{ container_config.image }}:{{ container_config.tag | default('latest') }}"
-    init: "{{ container_config.init | default(deploy_containers_defaults.init) }}"
-    ipc_mode: "{{ container_config.ipc_mode | default(omit) }}"
-    pid_mode: "{{ container_config.pid_mode | default(omit) }}"
-    ports: "{{ container_config.ports | default(omit) }}"
-    privileged: "{{ container_config.privileged | default(omit) }}"
-    restart_policy: "{{ container_config.restart_policy | default(deploy_containers_defaults.restart_policy) }}"
-    shm_size: "{{ container_config.shm_size | default(omit) }}"
-    sysctls: "{{ container_config.sysctls | default(omit) }}"
-    timeout: "{{ deploy_containers_docker_api_timeout }}"
-    ulimits: "{{ container_config.ulimits | default(omit) }}"
-    user: "{{ container_config.user | default(omit) }}"
-    volumes: "{{ container_config.volumes | default(omit) }}"
-
-- name: "[{{ container_name }}] Include tasks file for post task(s)"
-  include_tasks: "{{ container_config.post }}"
-  when: container_config.post is defined
+- name: Deploy containers (loop)
+  include_tasks: deploy-container.yml
+  vars:
+    container_name: "{{ item.key }}"
+    container_config: "{{ item.value }}"
+  with_dict: "{{ seed_containers }}"
diff --git a/ansible/roles/deploy-containers/tasks/destroy-container.yml b/ansible/roles/deploy-containers/tasks/destroy-container.yml
new file mode 100644
index 00000000..692ce2a6
--- /dev/null
+++ b/ansible/roles/deploy-containers/tasks/destroy-container.yml
@@ -0,0 +1,15 @@
+---
+
+- name: "[{{ container_name }}] Include tasks file for pre destroy task(s)"
+  include_tasks: "{{ container_config.pre_destroy }}"
+  when: container_config.pre_destroy is defined
+
+- name: "[{{ container_name }}] Delete container"
+  docker_container:
+    name: "{{ container_name }}"
+    state: absent
+  become: true
+
+- name: "[{{ container_name }}] Include tasks file for post destroy task(s)"
+  include_tasks: "{{ container_config.post_destroy }}"
+  when: container_config.post_destroy is defined
diff --git a/ansible/roles/deploy-containers/tasks/destroy.yml b/ansible/roles/deploy-containers/tasks/destroy.yml
new file mode 100644
index 00000000..acf0d25b
--- /dev/null
+++ b/ansible/roles/deploy-containers/tasks/destroy.yml
@@ -0,0 +1,6 @@
+- name: Destroy containers (loop)
+  include_tasks: destroy-container.yml
+  vars:
+    container_name: "{{ item.key }}"
+    container_config: "{{ item.value }}"
+  with_dict: "{{ seed_containers }}"
\ No newline at end of file
diff --git a/ansible/roles/deploy-containers/tasks/main.yml b/ansible/roles/deploy-containers/tasks/main.yml
index a16e6d8d..bcbde162 100644
--- a/ansible/roles/deploy-containers/tasks/main.yml
+++ b/ansible/roles/deploy-containers/tasks/main.yml
@@ -1,16 +1,2 @@
 ---
-- name: Login to docker registry
-  docker_login:
-    registry_url: "{{ kolla_docker_registry or omit }}"
-    username: "{{ kolla_docker_registry_username }}"
-    password: "{{ kolla_docker_registry_password }}"
-    reauthorize: yes
-  when:
-    - deploy_containers_registry_attempt_login | bool
-
-- name: Deploy containers (loop)
-  include_tasks: deploy.yml
-  vars:
-    container_name: "{{ item.key }}"
-    container_config: "{{ item.value }}"
-  with_dict: "{{ seed_containers }}"
+- include_tasks: "{{ deploy_containers_action }}.yml"
diff --git a/ansible/seed-deploy-containers.yml b/ansible/seed-deploy-containers.yml
index 23eb241c..585487af 100644
--- a/ansible/seed-deploy-containers.yml
+++ b/ansible/seed-deploy-containers.yml
@@ -3,5 +3,7 @@
   hosts: seed
   tags:
     - seed-deploy-containers
+  vars:
+    deploy_containers_action: "{{ kayobe_action }}"
   roles:
     - role: deploy-containers
diff --git a/doc/source/administration/seed.rst b/doc/source/administration/seed.rst
index ff8fab51..673e1215 100644
--- a/doc/source/administration/seed.rst
+++ b/doc/source/administration/seed.rst
@@ -13,6 +13,29 @@ To deprovision the seed VM::
 
     (kayobe) $ kayobe seed vm deprovision
 
+Destroying all services on the seed
+===================================
+
+.. warning::
+
+   This step will destroy all containers, container images, and volumes that were deployed by
+   Kayobe and Kolla. To destroy volumes and images associated with
+   :ref:`custom containers <configuration-seed-custom-containers>`, you must configure the
+   ``post_destroy`` and ``pre_destroy`` hooks to do the clean up manually as Kayobe will not
+   automatically clean these up. It is generally only advised to run this command when
+   you have no important data on the system.
+
+To destroy the seed services::
+
+    (kayobe) $ kayobe seed service destroy --yes-i-really-really-mean-it
+
+This can optionally be used with a tag::
+
+    (kayobe) $ kayobe seed service destroy --yes-i-really-really-mean-it -kt none -t docker-registry
+
+Care must be taken to set both kayobe and kolla tags to avoid accidentally
+destroying other services.
+
 Updating Packages
 =================
 
diff --git a/doc/source/configuration/reference/seed-custom-containers.rst b/doc/source/configuration/reference/seed-custom-containers.rst
index 5b3e03cd..b4f3035a 100644
--- a/doc/source/configuration/reference/seed-custom-containers.rst
+++ b/doc/source/configuration/reference/seed-custom-containers.rst
@@ -20,10 +20,27 @@ For example, to deploy a squid container image:
        image: "stackhpc/squid:3.5.20-1"
        pre: "{{ kayobe_env_config_path }}/containers/squid/pre.yml"
        post: "{{ kayobe_env_config_path }}/containers/squid/post.yml"
-
-Please notice the *optional* pre and post Ansible task files - those need to
-be created in ``kayobe-config`` path and will be run before and after
-particular container deployment.
+       pre_destroy: "{{ kayobe_env_config_path }}/containers/squid/pre_destroy.yml"
+       post_destroy: "{{ kayobe_env_config_path }}/containers/squid/post_destroy.yml"
+
+Please notice the *optional* pre, post, pre_destroy, and post_destroy Ansible task
+files - those need to be created in ``kayobe-config`` path. The table below describes
+when they will run:
+
+.. list-table:: Container hooks
+   :widths: 25 75
+   :header-rows: 1
+
+   * - Hook
+     - Trigger point
+   * - pre
+     - Before container deployment
+   * - post
+     - After container deployment
+   * - pre_destroy
+     - Before container is destroyed
+   * - post_destroy
+     - After container is destroyed
 
 Possible options for container deployment:
 
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 7b059b9b..adeed542 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -730,7 +730,8 @@ class SeedServiceDeploy(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         self.app.LOG.debug("Deploying seed services")
         playbooks = _build_playbook_list(
             "seed-deploy-containers")
-        self.run_kayobe_playbooks(parsed_args, playbooks)
+        extra_vars = {"kayobe_action": "deploy"}
+        self.run_kayobe_playbooks(parsed_args, playbooks, extra_vars=extra_vars)
         self.generate_kolla_ansible_config(parsed_args, service_config=False,
                                            bifrost_config=True)
 
@@ -739,8 +740,44 @@ class SeedServiceDeploy(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
             "seed-credentials",
             "seed-introspection-rules",
             "dell-switch-bmp")
-        self.run_kayobe_playbooks(parsed_args, playbooks)
+        self.run_kayobe_playbooks(parsed_args, playbooks, extra_vars=extra_vars)
+
+class SeedServiceDestroy(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
+                        Command):
+    """Destroy the seed services.
+
+    * Destroys user defined containers
+    * Destroys kolla deployed containers
+    * Destroys docker registry
+    """
+
+    def take_action(self, parsed_args):
+        if not parsed_args.yes_i_really_really_mean_it:
+            self.app.LOG.error("This will permanently destroy all services "
+                               "and data. Specify "
+                               "--yes-i-really-really-mean-it to confirm that "
+                               "you understand this.")
+            sys.exit(1)
+        self.app.LOG.debug("Destroying seed services")
+        self.generate_kolla_ansible_config(parsed_args, service_config=False,
+                                           bifrost_config=False)
+        extra_args = ["--yes-i-really-really-mean-it"]
+        self.run_kolla_ansible_seed(parsed_args, "destroy", extra_args=extra_args)
 
+        extra_vars = {"kayobe_action": "destroy"}
+        playbooks = _build_playbook_list(
+            "seed-deploy-containers",
+            "docker-registry")
+        self.run_kayobe_playbooks(parsed_args, playbooks, extra_vars=extra_vars)
+
+    def get_parser(self, prog_name):
+        parser = super(SeedServiceDestroy, self).get_parser(prog_name)
+        group = parser.add_argument_group("Services")
+        group.add_argument("--yes-i-really-really-mean-it",
+                           action='store_true',
+                           help="confirm that you understand that this will "
+                                "permanently destroy all services and data.")
+        return parser
 
 class SeedServiceUpgrade(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
                          Command):
@@ -762,7 +799,8 @@ class SeedServiceUpgrade(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         self.app.LOG.debug("Upgrading seed services")
         playbooks = _build_playbook_list(
             "seed-deploy-containers")
-        self.run_kayobe_playbooks(parsed_args, playbooks)
+        extra_vars = {"kayobe_action": "deploy"}
+        self.run_kayobe_playbooks(parsed_args, playbooks, extra_vars=extra_vars)
         self.generate_kolla_ansible_config(parsed_args, service_config=False,
                                            bifrost_config=True)
 
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index 83739c67..56c902e9 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -810,6 +810,7 @@ class TestCase(unittest.TestCase):
             mock.call(
                 mock.ANY,
                 [utils.get_data_files_path("ansible", "seed-deploy-containers.yml")],  # noqa
+                extra_vars={'kayobe_action': 'deploy'}
             ),
             mock.call(
                 mock.ANY,
@@ -834,6 +835,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path(
                         "ansible", "dell-switch-bmp.yml"),
                 ],
+                extra_vars={'kayobe_action': 'deploy'}
             ),
         ]
         self.assertListEqual(expected_calls, mock_run.call_args_list)
@@ -862,6 +864,7 @@ class TestCase(unittest.TestCase):
             mock.call(
                 mock.ANY,
                 [utils.get_data_files_path("ansible", "seed-deploy-containers.yml")],  # noqa
+                extra_vars={'kayobe_action': 'deploy'}
             ),
             mock.call(
                 mock.ANY,
diff --git a/releasenotes/notes/adds-seed-service-destroy-1bdf79990d050e68.yaml b/releasenotes/notes/adds-seed-service-destroy-1bdf79990d050e68.yaml
new file mode 100644
index 00000000..06b7526e
--- /dev/null
+++ b/releasenotes/notes/adds-seed-service-destroy-1bdf79990d050e68.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds the command ``kayobe seed service destroy``. This can be used to clean
+    up all services on the seed host. Caution is advised when using this command
+    as it will delete all of the data on the seed.
diff --git a/setup.cfg b/setup.cfg
index ce9dcb69..df817dc0 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -96,6 +96,7 @@ kayobe.cli=
     seed_hypervisor_host_package_update = kayobe.cli.commands:SeedHypervisorHostPackageUpdate
     seed_hypervisor_host_upgrade = kayobe.cli.commands:SeedHypervisorHostUpgrade
     seed_service_deploy = kayobe.cli.commands:SeedServiceDeploy
+    seed_service_destroy = kayobe.cli.commands:SeedServiceDestroy
     seed_service_upgrade = kayobe.cli.commands:SeedServiceUpgrade
     seed_vm_deprovision = kayobe.cli.commands:SeedVMDeprovision
     seed_vm_provision = kayobe.cli.commands:SeedVMProvision
@@ -221,6 +222,8 @@ kayobe.cli.seed_hypervisor_host_upgrade =
     hooks = kayobe.cli.commands:HookDispatcher
 kayobe.cli.seed_service_deploy =
     hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_service_destroy =
+    hooks = kayobe.cli.commands:HookDispatcher
 kayobe.cli.seed_service_upgrade =
     hooks = kayobe.cli.commands:HookDispatcher
 kayobe.cli.seed_vm_deprovision =
-- 
GitLab