feat(octavia): add role
diff --git a/atmosphere/flows.py b/atmosphere/flows.py
index bda7fae..3086da6 100644
--- a/atmosphere/flows.py
+++ b/atmosphere/flows.py
@@ -123,6 +123,9 @@
             name=constants.HELM_RELEASE_NOVA_NAME,
         ),
         openstack_helm.ApplyRabbitmqClusterTask(
+            name=constants.HELM_RELEASE_OCTAVIA_NAME,
+        ),
+        openstack_helm.ApplyRabbitmqClusterTask(
             name=constants.HELM_RELEASE_SENLIN_NAME,
         ),
         openstack_helm.ApplyRabbitmqClusterTask(
diff --git a/atmosphere/tasks/constants.py b/atmosphere/tasks/constants.py
index 30cc587..9cc9fd7 100644
--- a/atmosphere/tasks/constants.py
+++ b/atmosphere/tasks/constants.py
@@ -422,6 +422,8 @@
 
 HELM_RELEASE_NOVA_NAME = "nova"
 
+HELM_RELEASE_OCTAVIA_NAME = "octavia"
+
 HELM_RELEASE_SENLIN_NAME = "senlin"
 
 HELM_RELEASE_HEAT_NAME = "heat"
diff --git a/playbooks/openstack.yml b/playbooks/openstack.yml
index c6e2584..fb422c8 100644
--- a/playbooks/openstack.yml
+++ b/playbooks/openstack.yml
@@ -109,6 +109,10 @@
       tags:
         - openstack-helm-heat
 
+    - role: openstack_helm_octavia
+      tags:
+        - openstack-helm-octavia
+
     - role: openstack_helm_horizon
       tags:
         - openstack-helm-horizon
diff --git a/releasenotes/notes/octavia-init-role-52a84e39a2d222a4.yaml b/releasenotes/notes/octavia-init-role-52a84e39a2d222a4.yaml
new file mode 100644
index 0000000..2f5d537
--- /dev/null
+++ b/releasenotes/notes/octavia-init-role-52a84e39a2d222a4.yaml
@@ -0,0 +1,3 @@
+---
+features:
+  - Add octavia role
diff --git a/roles/openstack_helm_octavia/defaults/main.yml b/roles/openstack_helm_octavia/defaults/main.yml
new file mode 100644
index 0000000..dbdc255
--- /dev/null
+++ b/roles/openstack_helm_octavia/defaults/main.yml
@@ -0,0 +1,67 @@
+---
+# .. vim: foldmarker=[[[,]]]:foldmethod=marker
+
+# .. Copyright (C) 2022 VEXXHOST, Inc.
+# .. SPDX-License-Identifier: Apache-2.0
+
+# Default variables
+# =================
+
+# .. contents:: Sections
+#    :local:
+
+
+# .. envvar:: openstack_helm_octavia_image_repository [[[
+#
+# Image repository location to be prefixed for all images
+openstack_helm_octavia_image_repository: "{{ atmosphere_image_repository | default('quay.io/vexxhost') }}"
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_image_tag [[[
+#
+# Image tag for container
+openstack_helm_octavia_image_tag: zed
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_heat_image_tag [[[
+#
+# Image tag for Heat to be used for jobs running via Helm hooks
+openstack_helm_octavia_heat_image_tag: zed
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_neutron_image_tag [[[
+#
+# Image tag for Neutron to be used for openvswitch_vswitchd
+openstack_helm_octavia_neutron_image_tag: zed
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_values [[[
+#
+# Overrides for Helm chart values
+openstack_helm_octavia_values: {}
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_ingress_annotations [[[
+#
+# Ingress annotations
+openstack_helm_octavia_ingress_annotations: {}
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_heartbeat_key [[[
+#
+# Heartbeat key
+openstack_helm_octavia_heartbeat_key: "{{ undef(hint='You must specify a Octavia heartbeat key') }}"
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_management_subnet_cidr [[[
+#
+# Octavia management subnet (CIDR)
+openstack_helm_octavia_management_subnet_cidr: "172.24.0.0/22"
+
+                                                                   # ]]]
+# .. envvar:: openstack_helm_octavia_amphora_image_url [[[
+#
+# Octavia amphora image url
+openstack_helm_octavia_amphora_image_url: "https://tarballs.opendev.org/openstack/octavia/test-images/test-only-amphora-x64-haproxy-ubuntu-focal.qcow2"
+
+                                                                   # ]]]
diff --git a/roles/openstack_helm_octavia/meta/main.yml b/roles/openstack_helm_octavia/meta/main.yml
new file mode 100644
index 0000000..181e9ad
--- /dev/null
+++ b/roles/openstack_helm_octavia/meta/main.yml
@@ -0,0 +1,27 @@
+# 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 OpenStack Octavia
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
+
+dependencies:
+  - role: openstacksdk
+  - role: openstack_cli
diff --git a/roles/openstack_helm_octavia/tasks/main.yml b/roles/openstack_helm_octavia/tasks/main.yml
new file mode 100644
index 0000000..967da01
--- /dev/null
+++ b/roles/openstack_helm_octavia/tasks/main.yml
@@ -0,0 +1,285 @@
+# 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: Generate OpenStack-Helm endpoints
+  ansible.builtin.include_role:
+    name: openstack_helm_endpoints
+  vars:
+    openstack_helm_endpoints_repo_name: openstack-helm
+    openstack_helm_endpoints_repo_url: https://tarballs.opendev.org/openstack/openstack-helm/
+    openstack_helm_endpoints_chart: octavia
+
+- name: Create management network
+  openstack.cloud.network:
+    cloud: atmosphere
+    # Network settings
+    name: lb-mgmt-net
+  register: _openstack_helm_octavia_management_network
+
+- name: Create management subnet
+  openstack.cloud.subnet:
+    cloud: atmosphere
+    # Subnet settings
+    network_name: lb-mgmt-net
+    name: lb-mgmt-subnet
+    cidr: "{{ openstack_helm_octavia_management_subnet_cidr }}"
+
+- name: Create health manager security group
+  openstack.cloud.security_group:
+    cloud: atmosphere
+    name: lb-health-mgr-sec-grp
+  register: _openstack_helm_octavia_health_manager_sg
+
+- name: Create health manager security group rules
+  openstack.cloud.security_group_rule:
+    cloud: atmosphere
+    security_group: "{{ _openstack_helm_octavia_health_manager_sg.id }}"
+    direction: ingress
+    ethertype: IPv4
+    protocol: tcp
+    port_range_min: "{{ item }}"
+    port_range_max: "{{ item }}"
+  loop:
+    - 5555
+    - 10514
+    - 20514
+
+- name: Create health manager networking ports
+  openstack.cloud.port:
+    cloud: atmosphere
+    name: "octavia-health-manager-port-{{ hostvars[item]['inventory_hostname_short'] }}"
+    device_owner: octavia:health-mgr
+    network: "{{ _openstack_helm_octavia_management_network.id }}"
+    security_groups:
+      - "{{ _openstack_helm_octavia_health_manager_sg.id }}"
+  loop: "{{ groups['controllers'] }}"
+
+- name: Set binding for ports
+  changed_when: false
+  ansible.builtin.shell: |
+    openstack port set \
+      --host {{ hostvars[item]['ansible_fqdn'] }} \
+      octavia-health-manager-port-{{ hostvars[item]['inventory_hostname_short'] }}
+  environment:
+    OS_CLOUD: atmosphere
+  loop: "{{ groups['controllers'] }}"
+
+- name: Get health manager networking ports
+  openstack.cloud.port_info:
+    cloud: atmosphere
+    port: "octavia-health-manager-port-{{ hostvars[item]['ansible_fqdn'] | split('.') | first }}"
+  loop: "{{ groups['controllers'] }}"
+  register: _openstack_helm_octavia_health_manager_ports
+
+- name: Set controller_ip_port_list
+  ansible.builtin.set_fact:
+    _openstack_helm_octavia_controller_ip_port_list: "{{ _openstack_helm_octavia_controller_ip_port_list | d([]) + [item.openstack_ports[0].fixed_ips[0].ip_address + ':5555'] }}"
+  loop: "{{ _openstack_helm_octavia_health_manager_ports.results }}"
+
+- name: Create amphora security group
+  openstack.cloud.security_group:
+    cloud: atmosphere
+    name: lb-mgmt-sec-grp
+  register: _openstack_helm_octavia_amphora_sg
+
+- name: Create amphora security group rules
+  openstack.cloud.security_group_rule:
+    cloud: atmosphere
+    security_group: "{{ _openstack_helm_octavia_amphora_sg.id }}"
+    direction: ingress
+    ethertype: IPv4
+    protocol: tcp
+    port_range_min: "{{ item.0 }}"
+    port_range_max: "{{ item.0 }}"
+    remote_ip_prefix: "{{ item.1.openstack_ports[0].fixed_ips[0].ip_address }}/32"
+  with_nested:
+    - [22, 9443]
+    - "{{ _openstack_helm_octavia_health_manager_ports.results }}"
+
+- name: Create amphora flavor
+  openstack.cloud.compute_flavor:
+    cloud: atmosphere
+    name: "m1.amphora"
+    vcpus: "1"
+    ram: "1024"
+    disk: "2"
+    is_public: false
+  register: _openstack_helm_octavia_amphora_flavor
+
+- name: Download amphora image
+  ansible.builtin.get_url:
+    url: "{{ openstack_helm_octavia_amphora_image_url }}"
+    dest: "/tmp/{{ openstack_helm_octavia_amphora_image_url | basename }}"
+    mode: 0644
+
+- name: Upload images
+  openstack.cloud.image:
+    cloud: atmosphere
+    name: "amphora-x64-haproxy"
+    filename: "/tmp/{{ openstack_helm_octavia_amphora_image_url | basename }}"
+    container_format: "bare"
+    disk_format: "qcow2"
+    tags:
+      - "amphora"
+  register: _openstack_helm_octavia_amphora_image
+
+- name: Check if certificate secret exists
+  kubernetes.core.k8s_info:
+    kind: Secret
+    name: octavia-certs
+    namespace: openstack
+  register: _openstack_helm_octavia_cert_secret
+
+# TODO(mnaser): we should just use cert-manager for this?
+- name: Create certificate secret
+  when: (_openstack_helm_octavia_cert_secret.resources | length) == 0
+  block:
+    - name: Create CA private key
+      community.crypto.openssl_privatekey_pipe:
+        type: RSA
+      register: _openstack_helm_octavia_ca_key
+
+    - name: Create certificate signing request (CSR) for CA
+      community.crypto.openssl_csr_pipe:
+        basic_constraints:
+          - 'CA:TRUE'
+        basic_constraints_critical: true
+        common_name: "octavia"
+        key_usage:
+          - "keyEncipherment"
+          - "digitalSignature"
+          - "keyCertSign"
+        key_usage_critical: true
+        organization_name: "VEXXHOST OpenStack - Octavia"
+        privatekey_content: "{{ _openstack_helm_octavia_ca_key.privatekey }}"
+        use_common_name_for_san: false
+      register: _openstack_helm_octavia_ca_csr
+
+    - name: Create self-signed CA certificate from CSR
+      community.crypto.x509_certificate_pipe:
+        csr_content: "{{ _openstack_helm_octavia_ca_csr.csr }}"
+        entrust_not_after: "+3650d"
+        privatekey_content: "{{ _openstack_helm_octavia_ca_key.privatekey }}"
+        provider: selfsigned
+      register: _openstack_helm_octavia_ca
+
+    - name: Create client private key
+      community.crypto.openssl_privatekey_pipe:
+        type: RSA
+      register: _openstack_helm_octavia_client_ca_key
+
+    - name: Create certificate signing request (CSR) for client
+      community.crypto.openssl_csr_pipe:
+        common_name: "octavia-node"
+        key_usage:
+          - "keyEncipherment"
+          - "digitalSignature"
+          - "keyCertSign"
+        organization_name: "VEXXHOST OpenStack - Octavia"
+        privatekey_content: "{{ _openstack_helm_octavia_client_ca_key.privatekey }}"
+        use_common_name_for_san: false
+      register: _openstack_helm_octavia_client_csr
+
+    - name: Create self-signed client certificate
+      community.crypto.x509_certificate_pipe:
+        csr_content: "{{ _openstack_helm_octavia_client_csr.csr }}"
+        ownca_content: "{{ _openstack_helm_octavia_ca.certificate }}"
+        ownca_privatekey_content: "{{ _openstack_helm_octavia_ca_key.privatekey }}"
+        ownca_not_after: "+3650d"
+        entrust_not_after: "+3650d"
+        privatekey_content: "{{ _openstack_helm_octavia_client_ca_key.privatekey }}"
+        provider: ownca
+      register: _openstack_helm_octavia_client_ca
+
+    - name: Createa kubernetes secret
+      kubernetes.core.k8s:
+        state: present
+        definition:
+          apiVersion: v1
+          kind: Secret
+          metadata:
+            name: octavia-certs
+            namespace: openstack
+          stringData:
+            ca_01.pem: |
+              {{ _openstack_helm_octavia_ca.certificate }}
+            cakey.pem: |
+              {{ _openstack_helm_octavia_ca_key.privatekey }}
+            client.pem: |
+              {{ _openstack_helm_octavia_client_ca.certificate }}
+              {{ _openstack_helm_octavia_client_ca_key.privatekey }}
+
+- name: Create admin compute quotaset
+  openstack.cloud.quota:
+    cloud: atmosphere
+    # NOTE(okozachenko): It uses project name instead of id.
+    name: admin
+    instances: -1
+    cores: -1
+    ram: -1
+
+- name: Deploy Helm chart
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      - apiVersion: v1
+        kind: Secret
+        metadata:
+          name: atmosphere-octavia
+          namespace: openstack
+        stringData:
+          values.yaml: "{{ _openstack_helm_octavia_values | combine(openstack_helm_octavia_values, recursive=True) | to_nice_yaml }}"
+
+      - apiVersion: helm.toolkit.fluxcd.io/v2beta1
+        kind: HelmRelease
+        metadata:
+          name: octavia
+          namespace: openstack
+        spec:
+          interval: 60s
+          chart:
+            spec:
+              chart: octavia
+              version: 0.2.5
+              sourceRef:
+                kind: HelmRepository
+                name: openstack-helm
+          install:
+            disableWait: true
+          upgrade:
+            disableWait: true
+          valuesFrom:
+            - kind: Secret
+              name: atmosphere-octavia
+            - kind: Secret
+              name: percona-xtradb
+              valuesKey: root
+              targetPath: endpoints.oslo_db.auth.admin.password
+            - kind: Secret
+              name: rabbitmq-octavia-default-user
+              valuesKey: username
+              targetPath: endpoints.oslo_messaging.auth.admin.username
+            - kind: Secret
+              name: rabbitmq-octavia-default-user
+              valuesKey: password
+              targetPath: endpoints.oslo_messaging.auth.admin.password
+
+- name: Create Ingress
+  ansible.builtin.include_role:
+    name: openstack_helm_ingress
+  vars:
+    openstack_helm_ingress_endpoint: load_balancer
+    openstack_helm_ingress_service_name: octavia-api
+    openstack_helm_ingress_service_port: 9876
+    openstack_helm_ingress_annotations: "{{ openstack_helm_octavia_ingress_annotations }}"
diff --git a/roles/openstack_helm_octavia/vars/main.yml b/roles/openstack_helm_octavia/vars/main.yml
new file mode 100644
index 0000000..e34a990
--- /dev/null
+++ b/roles/openstack_helm_octavia/vars/main.yml
@@ -0,0 +1,135 @@
+# 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.
+
+_openstack_helm_octavia_values:
+  endpoints: "{{ openstack_helm_endpoints }}"
+  images:
+    tags:
+      bootstrap: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      db_drop: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      db_init: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      dep_check: "{{ openstack_helm_octavia_image_repository }}/kubernetes-entrypoint:latest"
+      ks_endpoints: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      ks_service: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      ks_user: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      rabbit_init: "{{ openstack_helm_octavia_image_repository }}/rabbitmq:3.8.23-management"
+      octavia_api: "{{ openstack_helm_octavia_image_repository }}/octavia:{{ openstack_helm_octavia_image_tag }}"
+      octavia_db_sync: "{{ openstack_helm_octavia_image_repository }}/octavia:{{ openstack_helm_octavia_image_tag }}"
+      octavia_health_manager: "{{ openstack_helm_octavia_image_repository }}/octavia:{{ openstack_helm_octavia_image_tag }}"
+      octavia_health_manager_init: "{{ openstack_helm_octavia_image_repository }}/heat:{{ openstack_helm_octavia_heat_image_tag }}"
+      octavia_housekeeping: "{{ openstack_helm_octavia_image_repository }}/octavia:{{ openstack_helm_octavia_image_tag }}"
+      octavia_worker: "{{ openstack_helm_octavia_image_repository }}/octavia:{{ openstack_helm_octavia_image_tag }}"
+      openvswitch_vswitchd: "{{ openstack_helm_octavia_image_repository }}/neutron:{{ openstack_helm_octavia_neutron_image_tag }}"
+  pod:
+    mounts:
+      octavia_api:
+        octavia_api:
+          volumeMounts:
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/private/cakey.pem"
+              subPath: "cakey.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/ca_01.pem"
+              subPath: "ca_01.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/client.pem"
+              subPath: "client.pem"
+          volumes:
+            - name: octavia-certs
+              secret:
+                secretName: octavia-certs
+      octavia_worker:
+        octavia_worker:
+          volumeMounts:
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/private/cakey.pem"
+              subPath: "cakey.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/ca_01.pem"
+              subPath: "ca_01.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/client.pem"
+              subPath: "client.pem"
+          volumes:
+            - name: octavia-certs
+              secret:
+                secretName: octavia-certs
+      octavia_housekeeping:
+        octavia_housekeeping:
+          volumeMounts:
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/private/cakey.pem"
+              subPath: "cakey.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/ca_01.pem"
+              subPath: "ca_01.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/client.pem"
+              subPath: "client.pem"
+          volumes:
+            - name: octavia-certs
+              secret:
+                secretName: octavia-certs
+      octavia_health_manager:
+        octavia_health_manager:
+          volumeMounts:
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/private/cakey.pem"
+              subPath: "cakey.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/ca_01.pem"
+              subPath: "ca_01.pem"
+            - name: octavia-certs
+              mountPath: "/etc/octavia/certs/client.pem"
+              subPath: "client.pem"
+          volumes:
+            - name: octavia-certs
+              secret:
+                secretName: octavia-certs
+    replicas:
+      api: 3
+      worker: 3
+      housekeeping: 3
+  conf:
+    octavia:
+      DEFAULT:
+        log_config_append: null
+      certificates:
+        ca_private_key_passphrase: null
+        endpoint_type: internalURL
+      cinder:
+        endpoint_type: internalURL
+      controller_worker:
+        amp_boot_network_list: "{{ _openstack_helm_octavia_management_network.id }}"
+        amp_flavor_id: "{{ _openstack_helm_octavia_amphora_flavor.id }}"
+        amp_image_owner_id: "{{ _openstack_helm_octavia_amphora_image.image.owner }}"
+        amp_secgroup_list: "{{ _openstack_helm_octavia_amphora_sg.id }}"
+        amp_ssh_key_name: null
+        workers: 4
+      glance:
+        endpoint_type: internalURL
+      health_manager:
+        controller_ip_port_list: "{{ _openstack_helm_octavia_controller_ip_port_list | join(',') }}"
+        heartbeat_key: "{{ openstack_helm_octavia_heartbeat_key }}"
+      oslo_messaging_notifications:
+        driver: noop
+      neutron:
+        endpoint_type: internalURL
+      nova:
+        endpoint_type: internalURL
+      service_auth:
+        endpoint_type: internalURL
+  manifests:
+    ingress_api: false
+    service_ingress_api: false