From 0a919817719676d379bd4fd2cef38bacb1dbe9e3 Mon Sep 17 00:00:00 2001 From: rajeshP524 Date: Thu, 2 Jul 2026 18:35:35 +0530 Subject: [PATCH 1/2] Add nic mapping in OVN for octavia --- ansible/roles/values-prep/templates/service_values.yaml.j2 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ansible/roles/values-prep/templates/service_values.yaml.j2 b/ansible/roles/values-prep/templates/service_values.yaml.j2 index 7ab7f0a..897cddd 100644 --- a/ansible/roles/values-prep/templates/service_values.yaml.j2 +++ b/ansible/roles/values-prep/templates/service_values.yaml.j2 @@ -32,10 +32,12 @@ data: ovnController: external-ids: enable-chassis-as-gateway: true - ovn-bridge: octbr + ovn-bridge: br-int ovn-encap-type: geneve system-id: random networkAttachment: tenant + nicMappings: + octavia: octbr resources: {} tls: {} From ce682f19263439a4ec66117b8ddbb1f6ef358e2a Mon Sep 17 00:00:00 2001 From: rajeshP524 Date: Tue, 22 Sep 2026 22:34:42 +0530 Subject: [PATCH 2/2] Add support for Ceph RGW --- ansible/main.yml | 7 ++ ansible/roles/ceph-rgw-prep/defaults/main.yml | 17 +++ .../tasks/includes/ensure_endpoint.yml | 30 +++++ .../tasks/includes/export_tls.yml | 17 +++ .../tasks/includes/keystone_prep.yml | 115 ++++++++++++++++++ .../ceph-rgw-prep/tasks/includes/tls_prep.yml | 98 +++++++++++++++ ansible/roles/ceph-rgw-prep/tasks/main.yml | 16 +++ .../templates/certificate_ceph_rgw.yaml.j2 | 15 +++ .../templates/dnsdata_ceph_rgw.yaml.j2 | 17 +++ ansible/roles/ceph-rgw/defaults/main.yml | 32 +++++ .../roles/ceph-rgw/tasks/includes/apply.yml | 103 ++++++++++++++++ .../ceph-rgw/tasks/includes/load_prep.yml | 23 ++++ .../ceph-rgw/tasks/includes/placement.yml | 14 +++ ansible/roles/ceph-rgw/tasks/main.yml | 9 ++ .../roles/ceph-rgw/templates/rgw_spec.yaml.j2 | 36 ++++++ ansible/roles/ceph/defaults/main.yml | 9 ++ .../ceph/tasks/includes/detect_iface.yml | 22 ++++ ansible/roles/ceph/tasks/main.yml | 107 ++++++++++++---- .../templates/service_values.yaml.j2 | 2 +- 19 files changed, 661 insertions(+), 28 deletions(-) create mode 100644 ansible/roles/ceph-rgw-prep/defaults/main.yml create mode 100644 ansible/roles/ceph-rgw-prep/tasks/includes/ensure_endpoint.yml create mode 100644 ansible/roles/ceph-rgw-prep/tasks/includes/export_tls.yml create mode 100644 ansible/roles/ceph-rgw-prep/tasks/includes/keystone_prep.yml create mode 100644 ansible/roles/ceph-rgw-prep/tasks/includes/tls_prep.yml create mode 100644 ansible/roles/ceph-rgw-prep/tasks/main.yml create mode 100644 ansible/roles/ceph-rgw-prep/templates/certificate_ceph_rgw.yaml.j2 create mode 100644 ansible/roles/ceph-rgw-prep/templates/dnsdata_ceph_rgw.yaml.j2 create mode 100644 ansible/roles/ceph-rgw/defaults/main.yml create mode 100644 ansible/roles/ceph-rgw/tasks/includes/apply.yml create mode 100644 ansible/roles/ceph-rgw/tasks/includes/load_prep.yml create mode 100644 ansible/roles/ceph-rgw/tasks/includes/placement.yml create mode 100644 ansible/roles/ceph-rgw/tasks/main.yml create mode 100644 ansible/roles/ceph-rgw/templates/rgw_spec.yaml.j2 create mode 100644 ansible/roles/ceph/tasks/includes/detect_iface.yml diff --git a/ansible/main.yml b/ansible/main.yml index 70cbac1..0aaafc7 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -30,5 +30,12 @@ - lvms - common-osp - controlplane + - { role: ceph-rgw-prep, when: ceph_backend | default(false) | bool } - dataplane - post + +- name: Deploy Ceph RGW for object storage + hosts: ceph_admin + gather_facts: false + roles: + - { role: ceph-rgw, when: ceph_backend | default(false) | bool } diff --git a/ansible/roles/ceph-rgw-prep/defaults/main.yml b/ansible/roles/ceph-rgw-prep/defaults/main.yml new file mode 100644 index 0000000..608aa90 --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/defaults/main.yml @@ -0,0 +1,17 @@ +--- +openstack_namespace: openstack +openstackclient_pod: openstackclient +osp_secret_name: osp-secret +osp_swift_password_key: SwiftPassword +keystone_region: regionOne +rgw_storage_vip: 172.20.0.24 +rgw_external_vip: 172.19.0.24 +rgw_frontend_port: 8080 +rgw_dns_zone: ceph.local +rgw_internal_hostname: ceph-rgw-internal-vip.ceph.local +rgw_external_hostname: ceph-rgw-external-vip.ceph.local +rgw_dnsdata_name: ceph-storage +rgw_certificate_name: cert-ceph-rgw +rgw_certificate_secret: cert-ceph-rgw +rgw_certificate_issuer: rootca-public +rgw_certificate_duration: 43800h0m0s diff --git a/ansible/roles/ceph-rgw-prep/tasks/includes/ensure_endpoint.yml b/ansible/roles/ceph-rgw-prep/tasks/includes/ensure_endpoint.yml new file mode 100644 index 0000000..dded39c --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/tasks/includes/ensure_endpoint.yml @@ -0,0 +1,30 @@ +--- +- name: "Get object-store {{ rgw_endpoint_interface }} endpoint" + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack endpoint list \ + --service object-store \ + --interface {{ rgw_endpoint_interface }} \ + -f value -c ID -c URL + register: rgw_endpoint_list + changed_when: false + failed_when: false + +- name: "Create object-store {{ rgw_endpoint_interface }} endpoint" + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack endpoint create \ + --region {{ keystone_region }} \ + object-store {{ rgw_endpoint_interface }} \ + '{{ rgw_endpoint_url }}' + when: rgw_endpoint_list.stdout_lines | length == 0 + +- name: "Update object-store {{ rgw_endpoint_interface }} endpoint URL" + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack endpoint set \ + --url '{{ rgw_endpoint_url }}' \ + {{ rgw_endpoint_list.stdout_lines[0].split()[0] }} + when: + - rgw_endpoint_list.stdout_lines | length > 0 + - (rgw_endpoint_list.stdout_lines[0].split(None, 1)[1] | default('')) != rgw_endpoint_url diff --git a/ansible/roles/ceph-rgw-prep/tasks/includes/export_tls.yml b/ansible/roles/ceph-rgw-prep/tasks/includes/export_tls.yml new file mode 100644 index 0000000..6fceda8 --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/tasks/includes/export_tls.yml @@ -0,0 +1,17 @@ +--- +- name: Read RGW TLS cert and key from Certificate secret + no_log: true + shell: | + set -euo pipefail + crt=$(oc -n {{ openstack_namespace }} get secret {{ rgw_certificate_secret }} \ + -o jsonpath='{.data.tls\.crt}' | base64 -d) + key=$(oc -n {{ openstack_namespace }} get secret {{ rgw_certificate_secret }} \ + -o jsonpath='{.data.tls\.key}' | base64 -d) + printf '%s\n%s\n' "$crt" "$key" + register: rgw_tls_pem_raw + changed_when: false + +- name: Store concatenated TLS PEM on localhost for Stage 6 + no_log: true + set_fact: + rgw_ssl_pem: "{{ rgw_tls_pem_raw.stdout }}" diff --git a/ansible/roles/ceph-rgw-prep/tasks/includes/keystone_prep.yml b/ansible/roles/ceph-rgw-prep/tasks/includes/keystone_prep.yml new file mode 100644 index 0000000..abc303d --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/tasks/includes/keystone_prep.yml @@ -0,0 +1,115 @@ +--- +- name: Wait for openstackclient pod + shell: | + oc wait -n {{ openstack_namespace }} pod/{{ openstackclient_pod }} \ + --for=condition=Ready --timeout=300s + changed_when: false + +- name: Fetch SwiftPassword from osp-secret + no_log: true + shell: | + oc get secret {{ osp_secret_name }} -n {{ openstack_namespace }} \ + -o jsonpath='{.data.{{ osp_swift_password_key }}}' | base64 -d + register: osp_swift_password_raw + changed_when: false + failed_when: osp_swift_password_raw.rc != 0 or osp_swift_password_raw.stdout | length == 0 + +- name: Set swift password fact + no_log: true + set_fact: + rgw_swift_password: "{{ osp_swift_password_raw.stdout }}" + +- name: Fetch Keystone internal endpoint URL + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack endpoint list --service identity --interface internal -f value -c URL + register: keystone_internal_urls + changed_when: false + failed_when: keystone_internal_urls.rc != 0 or keystone_internal_urls.stdout_lines | length == 0 + +- name: Fetch keystone-internal LoadBalancer IP for Ceph reachability + shell: | + oc -n {{ openstack_namespace }} get svc keystone-internal \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' + register: keystone_lb_ip + changed_when: false + failed_when: false + +- name: Set Keystone URL and RGW endpoint URL facts + set_fact: + rgw_keystone_url: >- + {{ + ('https://' ~ (keystone_lb_ip.stdout | trim) ~ ':5000') + if (keystone_lb_ip.stdout | default('') | trim | length > 0) + else (keystone_internal_urls.stdout_lines[0] | trim) + }} + rgw_endpoint_public_url: "https://{{ rgw_external_hostname }}:{{ rgw_frontend_port }}/swift/v1/AUTH_%(tenant_id)s" + rgw_endpoint_internal_url: "https://{{ rgw_internal_hostname }}:{{ rgw_frontend_port }}/swift/v1/AUTH_%(tenant_id)s" + +- name: Check object-store service + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack service show object-store + register: rgw_object_store_service + changed_when: false + failed_when: false + +- name: Create object-store service + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack service create \ + --name swift \ + --description "OpenStack Object Storage" \ + object-store + when: rgw_object_store_service.rc != 0 + +- name: Ensure swift user + no_log: true + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack user create --project service \ + --password {{ rgw_swift_password | quote }} \ + --or-show swift + changed_when: false + +- name: Ensure swiftoperator role + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack role create --or-show swiftoperator + changed_when: false + +- name: Ensure ResellerAdmin role + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack role create --or-show ResellerAdmin + changed_when: false + +- name: Ensure swift member role on service project + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack role add --user swift --project service member + changed_when: false + +- name: Ensure swift admin role on service project + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack role add --user swift --project service admin + changed_when: false + +- name: Ensure admin swiftoperator role on admin project + shell: | + oc rsh -n {{ openstack_namespace }} {{ openstackclient_pod }} \ + openstack role add --project admin --user admin swiftoperator + changed_when: false + +- name: Ensure object-store public endpoint + include_tasks: ensure_endpoint.yml + vars: + rgw_endpoint_interface: public + rgw_endpoint_url: "{{ rgw_endpoint_public_url }}" + +- name: Ensure object-store internal endpoint + include_tasks: ensure_endpoint.yml + vars: + rgw_endpoint_interface: internal + rgw_endpoint_url: "{{ rgw_endpoint_internal_url }}" diff --git a/ansible/roles/ceph-rgw-prep/tasks/includes/tls_prep.yml b/ansible/roles/ceph-rgw-prep/tasks/includes/tls_prep.yml new file mode 100644 index 0000000..d54a956 --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/tasks/includes/tls_prep.yml @@ -0,0 +1,98 @@ +--- +- name: Render DNSData for RGW VIPs + template: + src: dnsdata_ceph_rgw.yaml.j2 + dest: /tmp/jetbrew-ceph-rgw-dnsdata.yaml + mode: "0644" + +- name: Apply DNSData for RGW VIPs + shell: | + oc apply -f /tmp/jetbrew-ceph-rgw-dnsdata.yaml + +- name: Wait for DNSData Ready + shell: | + oc wait -n {{ openstack_namespace }} dnsdata/{{ rgw_dnsdata_name }} \ + --for=jsonpath='{.status.conditions[?(@.type=="Ready")].status}'=True \ + --timeout=300s + changed_when: false + +- name: Get dnsmasq-dns ClusterIP + shell: | + oc -n {{ openstack_namespace }} get svc dnsmasq-dns -o jsonpath='{.spec.clusterIP}' + register: rgw_dnsmasq_cluster_ip + changed_when: false + failed_when: rgw_dnsmasq_cluster_ip.stdout | length == 0 + +- name: Ensure CoreDNS forwards ceph.local to dnsmasq-dns + shell: | + python3 - <<'PY' + import json + import subprocess + import sys + + zone = "{{ rgw_dns_zone }}" + upstream = "{{ rgw_dnsmasq_cluster_ip.stdout | trim }}:53" + desired = { + "name": "ceph", + "zones": [zone], + "forwardPlugin": {"policy": "Random", "upstreams": [upstream]}, + } + + raw = subprocess.check_output( + ["oc", "-n", "openshift-dns", "get", "dns.operator/default", "-o", "json"], + text=True, + ) + dns = json.loads(raw) + servers = list(dns.get("spec", {}).get("servers") or []) + changed = False + found = False + for i, server in enumerate(servers): + if zone in (server.get("zones") or []) or server.get("name") == "ceph": + found = True + if server != desired: + servers[i] = desired + changed = True + break + if not found: + servers.append(desired) + changed = True + + if not changed: + print("unchanged") + sys.exit(0) + + patch = {"spec": {"servers": servers}} + subprocess.run( + ["oc", "-n", "openshift-dns", "patch", "dns.operator/default", "--type=merge", "-p", json.dumps(patch)], + check=True, + ) + print("updated") + PY + register: rgw_coredns_forward + changed_when: "'updated' in rgw_coredns_forward.stdout" + +- name: Render Certificate for RGW hostnames + template: + src: certificate_ceph_rgw.yaml.j2 + dest: /tmp/jetbrew-ceph-rgw-certificate.yaml + mode: "0644" + +- name: Apply Certificate for RGW hostnames + shell: | + oc apply -f /tmp/jetbrew-ceph-rgw-certificate.yaml + +- name: Wait for RGW Certificate Ready + shell: | + oc wait -n {{ openstack_namespace }} certificate/{{ rgw_certificate_name }} \ + --for=condition=Ready --timeout=300s + changed_when: false + +- name: Wait for RGW TLS secret + shell: | + oc -n {{ openstack_namespace }} get secret {{ rgw_certificate_secret }} \ + -o jsonpath='{.data.tls\.crt}' | grep -q . + register: rgw_tls_secret + retries: 30 + delay: 5 + until: rgw_tls_secret.rc == 0 + changed_when: false diff --git a/ansible/roles/ceph-rgw-prep/tasks/main.yml b/ansible/roles/ceph-rgw-prep/tasks/main.yml new file mode 100644 index 0000000..65efd1b --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: Configure DNS and TLS for external RGW + include_tasks: includes/tls_prep.yml + +- name: Ensure external RGW VIP hostname in /etc/hosts + ansible.builtin.lineinfile: + path: /etc/hosts + regexp: '.*\b{{ rgw_external_hostname | regex_escape }}\b.*' + line: "{{ rgw_external_vip }} {{ rgw_external_hostname }}" + state: present + +- name: Export TLS materials for Ceph RGW deploy + include_tasks: includes/export_tls.yml + +- name: Prepare Keystone for external RGW + include_tasks: includes/keystone_prep.yml diff --git a/ansible/roles/ceph-rgw-prep/templates/certificate_ceph_rgw.yaml.j2 b/ansible/roles/ceph-rgw-prep/templates/certificate_ceph_rgw.yaml.j2 new file mode 100644 index 0000000..9742242 --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/templates/certificate_ceph_rgw.yaml.j2 @@ -0,0 +1,15 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ rgw_certificate_name }} + namespace: {{ openstack_namespace }} +spec: + duration: {{ rgw_certificate_duration }} + issuerRef: + group: cert-manager.io + kind: Issuer + name: {{ rgw_certificate_issuer }} + secretName: {{ rgw_certificate_secret }} + dnsNames: + - {{ rgw_internal_hostname }} + - {{ rgw_external_hostname }} diff --git a/ansible/roles/ceph-rgw-prep/templates/dnsdata_ceph_rgw.yaml.j2 b/ansible/roles/ceph-rgw-prep/templates/dnsdata_ceph_rgw.yaml.j2 new file mode 100644 index 0000000..0102ca0 --- /dev/null +++ b/ansible/roles/ceph-rgw-prep/templates/dnsdata_ceph_rgw.yaml.j2 @@ -0,0 +1,17 @@ +apiVersion: network.openstack.org/v1beta1 +kind: DNSData +metadata: + labels: + component: ceph-storage + service: ceph + name: {{ rgw_dnsdata_name }} + namespace: {{ openstack_namespace }} +spec: + dnsDataLabelSelectorValue: dnsdata + hosts: + - hostnames: + - {{ rgw_internal_hostname }} + ip: {{ rgw_storage_vip }} + - hostnames: + - {{ rgw_external_hostname }} + ip: {{ rgw_external_vip }} diff --git a/ansible/roles/ceph-rgw/defaults/main.yml b/ansible/roles/ceph-rgw/defaults/main.yml new file mode 100644 index 0000000..6fac33c --- /dev/null +++ b/ansible/roles/ceph-rgw/defaults/main.yml @@ -0,0 +1,32 @@ +--- +rgw_storage_vip: 172.20.0.24 +rgw_external_vip: 172.19.0.24 +rgw_frontend_port: 8080 +rgw_backend_port: 8082 +rgw_realm: default +rgw_zone: default +rgw_service_id: rgw +rgw_ingress_service_id: rgw.default + +# Align with mon/mgr +rgw_placement_count: 3 + +storage_network: 172.20.0.0/16 +external_network: 172.19.0.0/16 +storage_network_prefix: "{{ storage_network.split('/')[1] }}" +external_network_prefix: "{{ external_network.split('/')[1] }}" + +rgw_remote_workdir: /tmp/jetbrew-rgw +rgw_spec_file: "{{ rgw_remote_workdir }}/rgw_spec.yaml" + +# Ingress needs one host slot per VIP +rgw_ingress_monitor_port: 8999 +rgw_ingress_count: 2 + +rgw_keystone_verify_ssl: false +rgw_keystone_api_version: 3 +rgw_keystone_accepted_roles: "member, Member, admin" +rgw_keystone_accepted_admin_roles: "ResellerAdmin, swiftoperator" +rgw_keystone_admin_domain: default +rgw_keystone_admin_project: service +rgw_keystone_admin_user: swift diff --git a/ansible/roles/ceph-rgw/tasks/includes/apply.yml b/ansible/roles/ceph-rgw/tasks/includes/apply.yml new file mode 100644 index 0000000..cca4f7d --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/includes/apply.yml @@ -0,0 +1,103 @@ +--- +# Keystone config + orch apply RGW/ingress + wait healthy. + +- name: Render RGW + ingress orch spec + no_log: true + template: + src: rgw_spec.yaml.j2 + dest: "{{ rgw_spec_file }}" + mode: "0600" + +- name: Configure RGW Keystone settings + no_log: true + shell: | + set -euo pipefail + c() { cephadm shell -- ceph config set global "$@"; } + c rgw_keystone_url "{{ rgw_keystone_url }}" + c rgw_keystone_verify_ssl {{ rgw_keystone_verify_ssl | lower }} + c rgw_keystone_api_version {{ rgw_keystone_api_version }} + c rgw_keystone_accepted_roles "{{ rgw_keystone_accepted_roles }}" + c rgw_keystone_accepted_admin_roles "{{ rgw_keystone_accepted_admin_roles }}" + c rgw_keystone_admin_domain {{ rgw_keystone_admin_domain }} + c rgw_keystone_admin_project {{ rgw_keystone_admin_project }} + c rgw_keystone_admin_user {{ rgw_keystone_admin_user }} + c rgw_keystone_admin_password "{{ rgw_swift_password }}" + c rgw_keystone_implicit_tenants true + c rgw_s3_auth_use_keystone true + c rgw_swift_versioning_enabled true + c rgw_swift_enforce_content_length true + c rgw_swift_account_in_url true + c rgw_trust_forwarded_https true + c rgw_max_attr_name_len 128 + c rgw_max_attrs_num_in_req 90 + c rgw_max_attr_size 1024 + changed_when: true + +- name: Apply RGW + ingress orch spec + shell: | + cephadm shell -m {{ rgw_spec_file }} -- \ + ceph orch apply -i /mnt/{{ rgw_spec_file | basename }} + register: rgw_orch_apply + changed_when: true + +- name: Wait for RGW running + shell: | + set -euo pipefail + status=$(cephadm shell -- ceph orch ls --service_name rgw.{{ rgw_service_id }} 2>/dev/null | awk '/^rgw\./ {print $3; exit}') + echo "$status" + running=${status%%/*} + size=${status##*/} + test -n "$running" && test "$running" = "$size" && test "$size" -gt 0 + register: rgw_ls + retries: 30 + delay: 10 + until: rgw_ls.rc == 0 + changed_when: false + +- name: Wait for ingress running + shell: | + set -euo pipefail + status=$(cephadm shell -- ceph orch ls --service_name ingress.{{ rgw_ingress_service_id }} 2>/dev/null | awk '/^ingress\./ {print $3; exit}') + echo "$status" + running=${status%%/*} + size=${status##*/} + test -n "$running" && test "$running" = "$size" && test "$size" -gt 0 + register: rgw_ingress_ls + retries: 30 + delay: 10 + until: rgw_ingress_ls.rc == 0 + changed_when: false + +# Fresh apply can leave beast SSL unbound while orch still reports N/N (VIP 503). +# Redeploy once now so SSL loads, then confirm with a VIP probe. +- name: Redeploy RGW after orch counts are healthy + shell: | + cephadm shell -- ceph orch redeploy rgw.{{ rgw_service_id }} + changed_when: true + +- name: Wait for RGW running after redeploy + shell: | + set -euo pipefail + status=$(cephadm shell -- ceph orch ls --service_name rgw.{{ rgw_service_id }} 2>/dev/null | awk '/^rgw\./ {print $3; exit}') + echo "$status" + running=${status%%/*} + size=${status##*/} + test -n "$running" && test "$running" = "$size" && test "$size" -gt 0 + register: rgw_ls_after_redeploy + retries: 30 + delay: 10 + until: rgw_ls_after_redeploy.rc == 0 + changed_when: false + +- name: Wait for storage VIP HTTPS + shell: | + set -euo pipefail + code=$(curl -sk -o /dev/null -w "%{http_code}" --connect-timeout 5 \ + https://{{ rgw_storage_vip }}:{{ rgw_frontend_port }}/ || true) + echo "$code" + test "$code" = "200" + register: rgw_vip_ready + retries: 30 + delay: 10 + until: rgw_vip_ready.rc == 0 + changed_when: false diff --git a/ansible/roles/ceph-rgw/tasks/includes/load_prep.yml b/ansible/roles/ceph-rgw/tasks/includes/load_prep.yml new file mode 100644 index 0000000..f133e2b --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/includes/load_prep.yml @@ -0,0 +1,23 @@ +--- +# Load Keystone, Swift password, and TLS PEM from localhost (ceph-rgw-prep). + +- name: Fail when ceph-rgw-prep outputs are missing on localhost + fail: + msg: "ceph-rgw-prep must run on localhost first (need Keystone URL, Swift password, TLS PEM)" + when: >- + (hostvars['localhost'].rgw_keystone_url | default('') | length == 0) or + (hostvars['localhost'].rgw_swift_password | default('') | length == 0) or + (hostvars['localhost'].rgw_ssl_pem | default('') | length == 0) + +- name: Load prep facts from localhost + no_log: true + set_fact: + rgw_keystone_url: "{{ hostvars['localhost'].rgw_keystone_url }}" + rgw_swift_password: "{{ hostvars['localhost'].rgw_swift_password }}" + rgw_ssl_pem: "{{ hostvars['localhost'].rgw_ssl_pem }}" + +- name: Ensure workdir on Ceph admin + file: + path: "{{ rgw_remote_workdir }}" + state: directory + mode: "0700" diff --git a/ansible/roles/ceph-rgw/tasks/includes/placement.yml b/ansible/roles/ceph-rgw/tasks/includes/placement.yml new file mode 100644 index 0000000..d2e2c51 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/includes/placement.yml @@ -0,0 +1,14 @@ +--- +- name: Get mon-labeled Ceph hosts + shell: | + cephadm shell -- ceph orch host ls 2>/dev/null \ + | grep mon \ + | awk '{print $1}' \ + | head -n {{ rgw_placement_count | int }} + register: rgw_placement + changed_when: false + failed_when: rgw_placement.stdout_lines | length == 0 + +- name: Set RGW placement hosts + set_fact: + rgw_placement_hosts: "{{ rgw_placement.stdout_lines }}" diff --git a/ansible/roles/ceph-rgw/tasks/main.yml b/ansible/roles/ceph-rgw/tasks/main.yml new file mode 100644 index 0000000..1268e30 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: Load prep facts and TLS PEM + include_tasks: includes/load_prep.yml + +- name: Select RGW placement hosts + include_tasks: includes/placement.yml + +- name: Configure Keystone and apply RGW orch + include_tasks: includes/apply.yml diff --git a/ansible/roles/ceph-rgw/templates/rgw_spec.yaml.j2 b/ansible/roles/ceph-rgw/templates/rgw_spec.yaml.j2 new file mode 100644 index 0000000..15e06b7 --- /dev/null +++ b/ansible/roles/ceph-rgw/templates/rgw_spec.yaml.j2 @@ -0,0 +1,36 @@ +--- +service_type: rgw +service_id: {{ rgw_service_id }} +service_name: rgw.{{ rgw_service_id }} +placement: + hosts: +{% for h in rgw_placement_hosts %} + - {{ h }} +{% endfor %} +networks: +- {{ storage_network }} +spec: + rgw_frontend_port: {{ rgw_backend_port }} + rgw_realm: {{ rgw_realm }} + rgw_zone: {{ rgw_zone }} + ssl: true + rgw_frontend_ssl_certificate: | +{{ rgw_ssl_pem | indent(4, first=True) }} +--- +service_type: ingress +service_id: {{ rgw_ingress_service_id }} +service_name: ingress.{{ rgw_ingress_service_id }} +placement: + count: {{ rgw_ingress_count }} +spec: + backend_service: rgw.{{ rgw_service_id }} + frontend_port: {{ rgw_frontend_port }} + monitor_port: {{ rgw_ingress_monitor_port }} + virtual_ips_list: + - {{ rgw_storage_vip }}/{{ storage_network_prefix }} + - {{ rgw_external_vip }}/{{ external_network_prefix }} + virtual_interface_networks: + - {{ storage_network }} + - {{ external_network }} + ssl_cert: | +{{ rgw_ssl_pem | indent(4, first=True) }} diff --git a/ansible/roles/ceph/defaults/main.yml b/ansible/roles/ceph/defaults/main.yml index 00a27ee..4315680 100644 --- a/ansible/roles/ceph/defaults/main.yml +++ b/ansible/roles/ceph/defaults/main.yml @@ -10,7 +10,16 @@ ceph_health_check_delay: 60 # Network configuration storage_network_vlan_id: 20 storage_mgmt_network_vlan_id: 21 +external_network_vlan_id: 19 +internalapi_network_vlan_id: 17 storage_network: 172.20.0.0/16 storage_mgmt_network: 172.21.0.0/16 +external_network: 172.19.0.0/16 +internalapi_network: 172.17.0.0/16 ip_offset: 10 + +# 1-based indexes into ocp inventory node.mac[] storage_network_nic_num: 4 +storage_mgmt_network_nic_num: "{{ storage_network_nic_num }}" +external_network_nic_num: 4 +internalapi_network_nic_num: 3 diff --git a/ansible/roles/ceph/tasks/includes/detect_iface.yml b/ansible/roles/ceph/tasks/includes/detect_iface.yml new file mode 100644 index 0000000..9e494cf --- /dev/null +++ b/ansible/roles/ceph/tasks/includes/detect_iface.yml @@ -0,0 +1,22 @@ +--- +- name: "Get MAC for nic {{ nic_num }} ({{ iface_fact }})" + set_fact: + _detect_mac: "{{ current_node_info.mac[nic_num | int - 1] }}" + +- name: "Detect interface name for {{ iface_fact }} (MAC {{ _detect_mac }})" + shell: | + for iface in $(ls /sys/class/net/); do + if [ -f /sys/class/net/$iface/address ]; then + mac=$(cat /sys/class/net/$iface/address) + if [ "$mac" = "{{ _detect_mac }}" ]; then + echo $iface + break + fi + fi + done + register: _detected_iface + failed_when: _detected_iface.stdout == "" + +- name: "Set {{ iface_fact }}" + set_fact: + "{{ iface_fact }}": "{{ _detected_iface.stdout }}" diff --git a/ansible/roles/ceph/tasks/main.yml b/ansible/roles/ceph/tasks/main.yml index bb27e92..1a6fa3b 100644 --- a/ansible/roles/ceph/tasks/main.yml +++ b/ansible/roles/ceph/tasks/main.yml @@ -17,48 +17,57 @@ set_fact: current_node_info: "{{ (hostvars['localhost']['ocp_inventory']['content'] | from_json).nodes | selectattr('name', 'equalto', inventory_hostname) | first }}" -- name: Get storage MAC address for interface detection - set_fact: - storage_mac: "{{ current_node_info.mac[storage_network_nic_num | int - 1] }}" - -- name: Detect storage interface name from MAC address - shell: | - for iface in $(ls /sys/class/net/); do - if [ -f /sys/class/net/$iface/address ]; then - mac=$(cat /sys/class/net/$iface/address) - if [ "$mac" = "{{ storage_mac }}" ]; then - echo $iface - break - fi - fi - done - register: detected_storage_iface - failed_when: detected_storage_iface.stdout == "" - -- name: Set storage interface variables - set_fact: - storage_iface: "{{ detected_storage_iface.stdout }}" - storage_mgmt_iface: "{{ detected_storage_iface.stdout }}" +- name: Detect storage interface from storage_network_nic_num + include_tasks: includes/detect_iface.yml + vars: + nic_num: "{{ storage_network_nic_num }}" + iface_fact: storage_iface + +- name: Detect storage management interface from storage_mgmt_network_nic_num + include_tasks: includes/detect_iface.yml + vars: + nic_num: "{{ storage_mgmt_network_nic_num }}" + iface_fact: storage_mgmt_iface + +- name: Detect external interface from external_network_nic_num + include_tasks: includes/detect_iface.yml + vars: + nic_num: "{{ external_network_nic_num }}" + iface_fact: external_iface + +- name: Detect internalapi interface from internalapi_network_nic_num + include_tasks: includes/detect_iface.yml + vars: + nic_num: "{{ internalapi_network_nic_num }}" + iface_fact: internalapi_iface - name: Set host index set_fact: host_index: "{{ groups['ceph_nodes'].index(inventory_hostname) }}" -- name: Calculate storage and storage management network IPs and prefixes +- name: Calculate storage, storage management, external, and internalapi IPs and prefixes set_fact: storage_ip: "{{ storage_network.split('/')[0].split('.')[:-1] | join('.') }}.{{ ip_offset + host_index | int }}" storage_mgmt_ip: "{{ storage_mgmt_network.split('/')[0].split('.')[:-1] | join('.') }}.{{ ip_offset + host_index | int }}" + external_ip: "{{ external_network.split('/')[0].split('.')[:-1] | join('.') }}.{{ ip_offset + host_index | int }}" + internalapi_ip: "{{ internalapi_network.split('/')[0].split('.')[:-1] | join('.') }}.{{ ip_offset + host_index | int }}" storage_network_prefix: "{{ storage_network.split('/')[1] }}" storage_mgmt_network_prefix: "{{ storage_mgmt_network.split('/')[1] }}" + external_network_prefix: "{{ external_network.split('/')[1] }}" + internalapi_network_prefix: "{{ internalapi_network.split('/')[1] }}" -- name: Display assigned storage IPs +- name: Display assigned interfaces and IPs debug: msg: | Host: {{ inventory_hostname }} (index {{ host_index }}) - Storage IP: {{ storage_ip }} - Storage Mgmt IP: {{ storage_mgmt_ip }} + Storage iface: {{ storage_iface }} (nic {{ storage_network_nic_num }}) -> {{ storage_ip }} + Storage mgmt iface: {{ storage_mgmt_iface }} (nic {{ storage_mgmt_network_nic_num }}) -> {{ storage_mgmt_ip }} + External iface: {{ external_iface }} (nic {{ external_network_nic_num }}) -> {{ external_ip }} + Internalapi iface: {{ internalapi_iface }} (nic {{ internalapi_network_nic_num }}) -> {{ internalapi_ip }} Storage Network: {{ storage_network }} Storage Mgmt Network: {{ storage_mgmt_network }} + External Network: {{ external_network }} + Internalapi Network: {{ internalapi_network }} - name: Ensure NetworkManager is running systemd: @@ -70,6 +79,8 @@ shell: | ip link set dev {{ storage_iface }} up ip link set dev {{ storage_mgmt_iface }} up + ip link set dev {{ external_iface }} up + ip link set dev {{ internalapi_iface }} up - name: Create storage VLAN interface shell: | @@ -93,16 +104,58 @@ ipv4.method manual ignore_errors: yes -- name: Bring up storage and storage management VLAN interfaces +- name: Create external VLAN interface + shell: | + nmcli connection add type vlan \ + con-name {{ external_iface }}.{{ external_network_vlan_id }} \ + ifname {{ external_iface }}.{{ external_network_vlan_id }} \ + dev {{ external_iface }} \ + id {{ external_network_vlan_id }} \ + ip4 {{ external_ip }}/{{ external_network_prefix }} \ + ipv4.method manual + ignore_errors: yes + +- name: Create internalapi VLAN interface + shell: | + nmcli connection add type vlan \ + con-name {{ internalapi_iface }}.{{ internalapi_network_vlan_id }} \ + ifname {{ internalapi_iface }}.{{ internalapi_network_vlan_id }} \ + dev {{ internalapi_iface }} \ + id {{ internalapi_network_vlan_id }} \ + ip4 {{ internalapi_ip }}/{{ internalapi_network_prefix }} \ + ipv4.method manual + ignore_errors: yes + +- name: Bring up storage, storage management, external, and internalapi VLAN interfaces shell: | nmcli connection up {{ storage_iface }}.{{ storage_network_vlan_id }} nmcli connection up {{ storage_mgmt_iface }}.{{ storage_mgmt_network_vlan_id }} + nmcli connection up {{ external_iface }}.{{ external_network_vlan_id }} + nmcli connection up {{ internalapi_iface }}.{{ internalapi_network_vlan_id }} + +- name: Verify external VLAN has expected address + shell: | + ip -4 -o addr show dev {{ external_iface }}.{{ external_network_vlan_id }} | awk '{print $4}' | grep -q "^{{ external_ip }}/" + register: external_vlan_addr_check + retries: 3 + delay: 2 + until: external_vlan_addr_check.rc == 0 + +- name: Verify internalapi VLAN has expected address + shell: | + ip -4 -o addr show dev {{ internalapi_iface }}.{{ internalapi_network_vlan_id }} | awk '{print $4}' | grep -q "^{{ internalapi_ip }}/" + register: internalapi_vlan_addr_check + retries: 3 + delay: 2 + until: internalapi_vlan_addr_check.rc == 0 - name: Store host information for cluster spec set_fact: host_info: hostname: "{{ inventory_hostname }}" storage_ip: "{{ storage_ip }}" + external_ip: "{{ external_ip }}" + internalapi_ip: "{{ internalapi_ip }}" host_index: "{{ host_index }}" # Admin-only tasks (runs on admin node) diff --git a/ansible/roles/values-prep/templates/service_values.yaml.j2 b/ansible/roles/values-prep/templates/service_values.yaml.j2 index 897cddd..82efae4 100644 --- a/ansible/roles/values-prep/templates/service_values.yaml.j2 +++ b/ansible/roles/values-prep/templates/service_values.yaml.j2 @@ -59,7 +59,7 @@ data: default: replicas: 1 swift: - enabled: true + enabled: {{ false if (ceph_backend | default(false) | bool) else true }} telemetry: enabled: {{ telemetry | default(true) }} template: