chore: add rook_ceph_cluster role
diff --git a/playbooks/openstack.yml b/playbooks/openstack.yml
index 8bec7c2..db8eda5 100644
--- a/playbooks/openstack.yml
+++ b/playbooks/openstack.yml
@@ -80,6 +80,11 @@
       tags:
         - rook-ceph
 
+    - role: rook_ceph_cluster
+      when: atmosphere_ceph_enabled | default(true)
+      tags:
+        - rook-ceph-cluster
+
     - role: ceph_provisioners
       when: atmosphere_ceph_enabled | default(true)
       tags:
diff --git a/roles/defaults/defaults/main.yml b/roles/defaults/defaults/main.yml
index 936bb16..f89c8bd 100644
--- a/roles/defaults/defaults/main.yml
+++ b/roles/defaults/defaults/main.yml
@@ -13,6 +13,7 @@
   cert_manager_cli: quay.io/jetstack/cert-manager-ctl:v1.7.1
   cert_manager_controller: quay.io/jetstack/cert-manager-controller:v1.7.1
   cert_manager_webhook: quay.io/jetstack/cert-manager-webhook:v1.7.1
+  ceph: quay.io/ceph/ceph:v16.2.11
   cilium_node: quay.io/cilium/cilium:v1.10.7@sha256:e23f55e80e1988db083397987a89967aa204ad6fc32da243b9160fbcea29b0ca
   cilium_operator: quay.io/cilium/operator-generic:v1.10.7@sha256:d0b491d8d8cb45862ed7f0410f65e7c141832f0f95262643fa5ff1edfcddcafe
   cinder_api: quay.io/vexxhost/cinder:zed
diff --git a/roles/rook_ceph_cluster/defaults/main.yml b/roles/rook_ceph_cluster/defaults/main.yml
new file mode 100644
index 0000000..7fee383
--- /dev/null
+++ b/roles/rook_ceph_cluster/defaults/main.yml
@@ -0,0 +1,30 @@
+# Copyright (c) 2023 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.
+
+rook_ceph_cluster_name: ceph
+
+rook_ceph_cluster_helm_release_name: "{{ rook_ceph_cluster_name }}"
+rook_ceph_cluster_helm_chart_path: "{{ role_path }}/../../charts/rook-ceph-cluster/"
+rook_ceph_cluster_helm_chart_ref: /usr/local/src/rook-ceph-cluster
+
+rook_ceph_cluster_helm_release_namespace: openstack
+rook_ceph_cluster_helm_values: {}
+
+# List of annotations to apply to the Ingress
+rook_ceph_cluster_radosgw_annotations: {}
+
+rook_ceph_cluster_mon_group: controllers
+
+rook_ceph_cluster_spec: {}
+rook_ceph_cluster_radosgw_spec: {}
diff --git a/roles/rook_ceph_cluster/meta/main.yml b/roles/rook_ceph_cluster/meta/main.yml
new file mode 100644
index 0000000..c5bb5e8
--- /dev/null
+++ b/roles/rook_ceph_cluster/meta/main.yml
@@ -0,0 +1,34 @@
+# 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 Rook Ceph Cluster
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  standalone: false
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
+
+dependencies:
+  - role: defaults
+  - role: openstacksdk
+  - role: openstack_helm_endpoints
+    openstack_helm_endpoints_list: ["rook_ceph_cluster"]
+  - role: upload_helm_chart
+    vars:
+      upload_helm_chart_src: "{{ rook_ceph_cluster_helm_chart_path }}"
+      upload_helm_chart_dest: "{{ rook_ceph_cluster_helm_chart_ref }}"
diff --git a/roles/rook_ceph_cluster/tasks/main.yml b/roles/rook_ceph_cluster/tasks/main.yml
new file mode 100644
index 0000000..ee3c06b
--- /dev/null
+++ b/roles/rook_ceph_cluster/tasks/main.yml
@@ -0,0 +1,125 @@
+# Copyright (c) 2023 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: Collect "ceph quorum_status" output from a monitor
+  run_once: true
+  delegate_to: "{{ groups[rook_ceph_cluster_mon_group][0] }}"
+  ansible.builtin.command: ceph quorum_status -f json
+  changed_when: false
+  register: _rook_ceph_cluster_quorum_status_data
+
+- name: Retrieve keyring for client.admin
+  run_once: true
+  delegate_to: "{{ groups[rook_ceph_cluster_mon_group][0] }}"
+  vexxhost.atmosphere.ceph_key:
+    name: client.admin
+    state: info
+    output_format: json
+  register: _rook_ceph_cluster_admin_auth_data
+
+- name: Retrieve keyring for monitors
+  run_once: true
+  delegate_to: "{{ groups[rook_ceph_cluster_mon_group][0] }}"
+  vexxhost.atmosphere.ceph_key:
+    name: mon.
+    state: info
+    output_format: json
+  register: _rook_ceph_cluster_mon_auth_data
+
+- name: Create Ceph cluster resource
+  run_once: true
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      - apiVersion: v1
+        kind: Secret
+        metadata:
+          name: rook-ceph-mon
+          namespace: "{{ rook_ceph_cluster_helm_release_namespace }}"
+        stringData:
+          cluster-name: "{{ rook_ceph_cluster_name }}"
+          fsid: "{{ _rook_ceph_cluster_quorum_status.monmap.fsid }}"
+          admin-secret: "{{ _rook_ceph_cluster_admin_auth.key }}"
+          mon-secret: "{{ _rook_ceph_cluster_mon_auth.key }}"
+
+      - apiVersion: v1
+        kind: ConfigMap
+        metadata:
+          name: rook-ceph-mon-endpoints
+          namespace: "{{ rook_ceph_cluster_helm_release_namespace }}"
+        data:
+          data: "{{ _rook_ceph_cluster_leader_name }}={{ _rook_ceph_cluster_leader_addr }}"
+          maxMonId: "0"
+          mapping: "{}"
+  vars:
+    _rook_ceph_cluster_quorum_status: "{{ _rook_ceph_cluster_quorum_status_data.stdout | from_json }}"
+    _rook_ceph_cluster_admin_auth: "{{ _rook_ceph_cluster_admin_auth_data.stdout | from_json | first }}"
+    _rook_ceph_cluster_mon_auth: "{{ _rook_ceph_cluster_mon_auth_data.stdout | from_json | first }}"
+    _rook_ceph_cluster_leader_name: "{{ _rook_ceph_cluster_quorum_status.quorum_leader_name }}"
+    _rook_ceph_cluster_leader_mon: "{{ (_rook_ceph_cluster_quorum_status.monmap.mons | selectattr('name', 'equalto', _rook_ceph_cluster_leader_name) | list | first) }}"
+    _rook_ceph_cluster_leader_addr: "{{ _rook_ceph_cluster_leader_mon.public_addr.split('/')[0] }}"
+
+- name: Deploy Helm chart
+  run_once: true
+  kubernetes.core.helm:
+    name: "{{ rook_ceph_cluster_helm_release_name }}"
+    chart_ref: "{{ rook_ceph_cluster_helm_chart_ref }}"
+    release_namespace: "{{ rook_ceph_cluster_helm_release_namespace }}"
+    create_namespace: true
+    kubeconfig: /etc/kubernetes/admin.conf
+    values: "{{ _rook_ceph_cluster_helm_values | combine(rook_ceph_cluster_helm_values, recursive=True) }}"
+
+- name: Create OpenStack user
+  openstack.cloud.identity_user:
+    cloud: atmosphere
+    name: "{{ openstack_helm_endpoints.identity.auth.rgw.username }}"
+    password: "{{ openstack_helm_endpoints.identity.auth.rgw.password }}"
+    domain: service
+
+- name: Grant access to "service" project
+  openstack.cloud.role_assignment:
+    cloud: atmosphere
+    domain: service
+    user: "{{ openstack_helm_endpoints.identity.auth.rgw.username }}"
+    project: service
+    role: admin
+
+- name: Create OpenStack service
+  openstack.cloud.catalog_service:
+    cloud: atmosphere
+    name: swift
+    service_type: object-store
+    description: OpenStack Object Storage
+
+- name: Create OpenStack endpoints
+  openstack.cloud.endpoint:
+    cloud: atmosphere
+    service: swift
+    endpoint_interface: "{{ item.interface }}"
+    url: "{{ item.url }}"
+    region: "{{ openstack_helm_endpoints.identity.auth.rgw.region_name }}"
+  loop:
+    - interface: public
+      url: "https://{{ openstack_helm_endpoints.rook_ceph_cluster.host_fqdn_override.public.host }}/swift/v1/%(tenant_id)s"
+    - interface: internal
+      url: "http://rook-ceph-rgw-ceph.openstack.svc.cluster.local/swift/v1/%(tenant_id)s"
+
+- name: Create Ingress
+  ansible.builtin.include_role:
+    name: openstack_helm_ingress
+  vars:
+    openstack_helm_ingress_endpoint: rook_ceph_cluster
+    openstack_helm_ingress_service_name: rook-ceph-rgw-ceph
+    openstack_helm_ingress_service_port: 80
+    openstack_helm_ingress_annotations: "{{ _rook_ceph_cluster_radosgw_annotations | combine(rook_ceph_cluster_radosgw_annotations, recursive=True) }}"
diff --git a/roles/rook_ceph_cluster/vars/main.yml b/roles/rook_ceph_cluster/vars/main.yml
new file mode 100644
index 0000000..1ed5894
--- /dev/null
+++ b/roles/rook_ceph_cluster/vars/main.yml
@@ -0,0 +1,71 @@
+# Copyright (c) 2023 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.
+
+_rook_ceph_cluster_spec:
+  cephVersion:
+    image: "{{ atmosphere_images['ceph'] | vexxhost.atmosphere.docker_image('ref') }}"
+  external:
+    enable: true
+
+_rook_ceph_cluster_radosgw_spec:
+  preservePoolsOnDelete: true
+  metadataPool:
+    failureDomain: host
+    replicated:
+      size: 3
+  dataPool:
+    failureDomain: host
+    replicated:
+      size: 3
+  gateway:
+    port: 80
+    instances: 3
+    placement:
+      nodeAffinity:
+        requiredDuringSchedulingIgnoredDuringExecution:
+          nodeSelectorTerms:
+            - matchExpressions:
+                - key: openstack-control-plane
+                  operator: In
+                  values: ["enabled"]
+
+_rook_ceph_cluster_helm_values:
+  clusterName: "{{ rook_ceph_cluster_name }}"
+  configOverride: |
+    [client]
+    rgw keystone api version = 3
+    rgw keystone url =  http://keystone-api.openstack.svc.cluster.local:5000
+    rgw keystone admin user = "{{ openstack_helm_endpoints.identity.auth.rgw.username }}"
+    rgw keystone admin password = "{{ openstack_helm_endpoints.identity.auth.rgw.password }}"
+    rgw_keystone admin domain = service
+    rgw_keystone admin project = service
+    rgw keystone implicit tenants = true
+    rgw keystone accepted roles = member,admin
+    rgw_keystone accepted admin roles = admin
+    rgw keystone token cache size = 0
+    rgw s3 auth use keystone = true
+    rgw swift account in url = true
+    rgw swift versioning enabled = true
+  cephClusterSpec: "{{ _rook_ceph_cluster_spec | combine(rook_ceph_cluster_spec, recursive=True) }}"
+  cephBlockPools: []
+  cephFileSystems: []
+  cephObjectStores:
+    - name: "{{ rook_ceph_cluster_name }}"
+      spec: "{{ _rook_ceph_cluster_radosgw_spec | combine(rook_ceph_cluster_radosgw_spec, recursive=True) }}"
+      storageClass:
+        enabled: false
+
+_rook_ceph_cluster_radosgw_annotations:
+  nginx.ingress.kubernetes.io/proxy-body-size: "0"
+  nginx.ingress.kubernetes.io/proxy-request-buffering: "off"