Add keepalived role

Sem-Ver: feature
Change-Id: Ibb54fa4cefe2393eb86a9f1646d108448a589a7e
diff --git a/doc/source/roles/keepalived/index.rst b/doc/source/roles/keepalived/index.rst
new file mode 100644
index 0000000..de5844e
--- /dev/null
+++ b/doc/source/roles/keepalived/index.rst
@@ -0,0 +1,10 @@
+.. Copyright (C) 2022 VEXXHOST, Inc.
+.. SPDX-License-Identifier: Apache-2.0
+
+``keepalived``
+================
+
+.. toctree::
+   :maxdepth: 2
+
+   defaults/main
\ No newline at end of file
diff --git a/playbooks/generate_workspace.yml b/playbooks/generate_workspace.yml
index 9dec792..02f4fd4 100644
--- a/playbooks/generate_workspace.yml
+++ b/playbooks/generate_workspace.yml
@@ -134,6 +134,39 @@
         content: "{{ kubernetes | to_nice_yaml(indent=2, width=180) }}"
         dest: "{{ _kubernetes_path }}"
 
+- name: Generate Keepalived configuration for workspace
+  hosts: localhost
+  gather_facts: false
+  vars:
+    _keepalived_path: "{{ workspace_path }}/group_vars/all/keepalived.yml"
+  tasks:
+    - name: Ensure the Keeaplived configuration file exists
+      ansible.builtin.file:
+        path: "{{ _keepalived_path }}"
+        state: touch
+
+    - name: Load the current Keepalived configuration into a variable
+      ansible.builtin.include_vars:
+        file: "{{ _keepalived_path }}"
+        name: keepalived
+
+    - name: Generate Keepalived values for missing variables
+      ansible.builtin.set_fact:
+        keepalived: "{{ keepalived | default({}) | combine({item.key: item.value}) }}"
+      # NOTE(mnaser): We don't want to override existing Keepalived configurations,
+      #               so we generate a stub one if and only if it doesn't exist
+      when: item.key not in keepalived
+      # NOTE(mnaser): This is absolutely hideous but there's no clean way of
+      #               doing this using `with_fileglob` or `with_filetree`
+      with_dict:
+        keepalived_interface: ens4
+        keepalived_vip: 10.96.250.10
+
+    - name: Write new Keepalived configuration file to disk
+      ansible.builtin.copy:
+        content: "{{ keepalived | to_nice_yaml(indent=2, width=180) }}"
+        dest: "{{ _keepalived_path }}"
+
 - name: Generate endpoints for workspace
   hosts: localhost
   gather_facts: false
diff --git a/playbooks/openstack.yml b/playbooks/openstack.yml
index e005549..019d33d 100644
--- a/playbooks/openstack.yml
+++ b/playbooks/openstack.yml
@@ -48,6 +48,10 @@
       tags:
         - cert-manager
 
+    - role: keepalived
+      tags:
+        - keepalived
+
     - role: percona_xtradb_cluster
       tags:
         - percona-xtradb-cluster
diff --git a/releasenotes/notes/keepalived-add-role-1b2ad22c86e253ba.yaml b/releasenotes/notes/keepalived-add-role-1b2ad22c86e253ba.yaml
new file mode 100644
index 0000000..9634c55
--- /dev/null
+++ b/releasenotes/notes/keepalived-add-role-1b2ad22c86e253ba.yaml
@@ -0,0 +1,3 @@
+---
+features:
+  - Add role for keepalived in openstack namespace
diff --git a/roles/keepalived/defaults/main.yml b/roles/keepalived/defaults/main.yml
new file mode 100644
index 0000000..5b0e8d9
--- /dev/null
+++ b/roles/keepalived/defaults/main.yml
@@ -0,0 +1,49 @@
+---
+# .. vim: foldmarker=[[[,]]]:foldmethod=marker
+
+# .. Copyright (C) 2022 VEXXHOST, Inc.
+# .. SPDX-License-Identifier: Apache-2.0
+
+# Default variables
+# =================
+
+# .. contents:: Sections
+#    :local:
+
+
+# .. envvar:: keepalived_password [[[
+#
+# Keepalived password
+keepalived_password: "{{ undef(hint='You must specify a Keepalived password') }}"
+
+                                                                   # ]]]
+# .. envvar:: keepalived_vip [[[
+#
+# Keepalived virtual IP address
+keepalived_vip: "{{ undef(hint='You must specify a Keepalived virtual IP address') }}"
+
+                                                                   # ]]]
+# .. envvar:: keepalived_interface [[[
+#
+# Keepalived virtual IP interface
+keepalived_interface: "{{ undef(hint='You must specify a Keepalived virtual IP interface') }}"
+
+                                                                   # ]]]
+# .. envvar:: keepalived_image_repository [[[
+#
+# Keepalived container image repository location
+keepalived_image_repository: "{{ atmosphere_image_repository | default('us-docker.pkg.dev/vexxhost-infra/openstack') }}"
+
+                                                                   # ]]]
+# .. envvar:: keepalived_image_tag [[[
+#
+# Keepalived container image tag
+keepalived_image_tag: 2.0.19
+
+                                                                   # ]]]
+# .. envvar:: keepalived_vrid [[[
+#
+# Keepalived virtual router id
+keepalived_vrid: 51
+
+                                                                   # ]]]
diff --git a/roles/keepalived/meta/main.yml b/roles/keepalived/meta/main.yml
new file mode 100644
index 0000000..39b6ca5
--- /dev/null
+++ b/roles/keepalived/meta/main.yml
@@ -0,0 +1,23 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+galaxy_info:
+  author: VEXXHOST, Inc.
+  description: Ansible role for keepalived
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
diff --git a/roles/keepalived/tasks/main.yml b/roles/keepalived/tasks/main.yml
new file mode 100644
index 0000000..c97d66b
--- /dev/null
+++ b/roles/keepalived/tasks/main.yml
@@ -0,0 +1,200 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+- name: Create Secret
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      apiVersion: v1
+      kind: Secret
+      metadata:
+        name: keepalived-etc
+        namespace: openstack
+      stringData:
+        keepalived.conf: |
+          global_defs {
+            default_interface {{ keepalived_interface }}
+          }
+
+          vrrp_instance VI_1 {
+            interface {{ keepalived_interface }}
+
+            state BACKUP
+            virtual_router_id {{ keepalived_vrid }}
+            priority 150
+            nopreempt
+
+            virtual_ipaddress {
+              {{ keepalived_vip }}
+            }
+
+            authentication {
+              auth_type PASS
+              auth_pass {{ keepalived_password }}
+            }
+          }
+
+- name: Create ConfigMap
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      apiVersion: v1
+      kind: ConfigMap
+      metadata:
+        name: keepalived-bin
+        namespace: openstack
+      data:
+        wait-for-ip.sh: |
+          #!/bin/sh -x
+
+          while true; do
+              ip -4 addr list dev {{ keepalived_interface }} | grep {{ keepalived_interface }}
+
+              # We detected an IP address
+              if [ $? -eq 0 ]; then
+                  break
+              fi
+
+              sleep 1
+          done
+
+- name: Create Role
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      apiVersion: rbac.authorization.k8s.io/v1
+      kind: Role
+      metadata:
+        name: keepalived
+        namespace: openstack
+      rules:
+        - apiGroups:
+            - ""
+          resources:
+            - pods
+          verbs:
+            - list
+            - get
+
+- name: Create ServiceAccount
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      apiVersion: v1
+      automountServiceAccountToken: true
+      kind: ServiceAccount
+      metadata:
+        name: keepalived
+        namespace: openstack
+
+- name: Create ServiceAccount
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      apiVersion: rbac.authorization.k8s.io/v1
+      kind: RoleBinding
+      metadata:
+        name: keepalived
+        namespace: openstack
+      roleRef:
+        apiGroup: rbac.authorization.k8s.io
+        kind: Role
+        name: keepalived
+      subjects:
+        - kind: ServiceAccount
+          name: keepalived
+          namespace: openstack
+
+- name: Create DaemonSet
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      apiVersion: apps/v1
+      kind: DaemonSet
+      metadata:
+        name: keepalived
+        namespace: openstack
+      spec:
+        selector:
+          matchLabels:
+            application: keepalived
+        template:
+          metadata:
+            labels:
+              application: keepalived
+          spec:
+            automountServiceAccountToken: true
+            initContainers:
+              - name: init
+                image: "{{ keepalived_image_repository }}/kubernetes-entrypoint:latest"
+                env:
+                  - name: NAMESPACE
+                    valueFrom:
+                      fieldRef:
+                        apiVersion: v1
+                        fieldPath: metadata.namespace
+                  - name: POD_NAME
+                    valueFrom:
+                      fieldRef:
+                        apiVersion: v1
+                        fieldPath: metadata.name
+                  - name: DEPENDENCY_POD_JSON
+                    value: '[{"labels":{"application":"neutron","component":"neutron-ovs-agent"},"requireSameNode":true}]'
+              - name: wait-for-ip
+                image: "{{ keepalived_image_repository }}/keepalived:{{ keepalived_image_tag }}"
+                command:
+                  - /bin/wait-for-ip.sh
+                volumeMounts:
+                  - mountPath: /bin/wait-for-ip.sh
+                    mountPropagation: None
+                    name: keepalived-bin
+                    readOnly: true
+                    subPath: wait-for-ip.sh
+            containers:
+              - name: keepalived
+                image: "{{ keepalived_image_repository }}/keepalived:{{ keepalived_image_tag }}"
+                command:
+                  - keepalived
+                  - -f
+                  - /etc/keepalived/keepalived.conf
+                  - --dont-fork
+                  - --log-console
+                  - --log-detail
+                  - --dump-conf
+                securityContext:
+                  allowPrivilegeEscalation: true
+                  capabilities:
+                    add:
+                      - NET_ADMIN
+                      - NET_BROADCAST
+                      - NET_RAW
+                volumeMounts:
+                  - mountPath: /etc/keepalived
+                    mountPropagation: None
+                    name: keepalived-etc
+                    readOnly: true
+            hostNetwork: true
+            nodeSelector:
+              openstack-control-plane: enabled
+            serviceAccountName: keepalived
+            volumes:
+              - name: keepalived-etc
+                secret:
+                  optional: false
+                  secretName: keepalived-etc
+              - configMap:
+                  defaultMode: 0755
+                  name: keepalived-bin
+                  optional: false
+                name: keepalived-bin