diff --git a/.gitignore b/.gitignore index 3ba3d3aa87fd9439eea51fbb690aed9905ca3f53..04e0e490102e9fd5cf976c482913ce61e231f5bb 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ ansible/.ansible/ *.retry ansible_facts/ ansible/secrets.yml -ansible/inventory/hosts.local.yml # Automation folder - contains kubeconfig files generated by ansible automation/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47ea210e86836156414729aca8fa56c8f1000a35..424cde01c2322f67ff4e462f3138375c5159af85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,8 +45,8 @@ To contribute code, follow these steps: 1. **Clone the Repository**: ```bash - git clone https://labs.etsi.org/rep/oop/code/integration.git - cd integration + git clone git@gitlab.i2cat.net:areas/software-networks/operator-platform/oop/op-automation.git + cd i2CAT_OP_AUTOMATION ``` 2. **Create a Feature Branch**: diff --git a/README.md b/README.md index 6cd10e4dc0c5094e9b86252b66e4b1fe5700b43a..4af249b4a2e5dd83c61a1d59938e5b299fdbb4e8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ Repository scope: - deploy two-host federation setups - fetch kubeconfigs back to local machine +This downstream repository keeps i2CAT-specific inventory and local defaults on +top of the public-safe upstream automation. + ## Start here - `docs/getting-started.md`: environment, inventory, secrets, first deployment diff --git a/ansible/inventory/group_vars/all/srm.yml b/ansible/inventory/group_vars/all/srm.yml index 64fc1ef4d355121a4453d9d611658b09b52f7948..2d78cbc67ded50ca27653ce20c9c03269387aac3 100644 --- a/ansible/inventory/group_vars/all/srm.yml +++ b/ansible/inventory/group_vars/all/srm.yml @@ -12,7 +12,7 @@ oop_namespace: oop # Service NodePort srm_nodeport: 32415 -# SRM image +# SRM image (ETSI registry) srm_controller_image_repository: "labs.etsi.org:5050/oop/code/service-resource-manager" srm_controller_image_tag: "1.1" diff --git a/ansible/inventory/hosts.local.yml b/ansible/inventory/hosts.local.yml new file mode 100644 index 0000000000000000000000000000000000000000..147b20bc2fc4b44380bcc3c25ba431c77d6a800f --- /dev/null +++ b/ansible/inventory/hosts.local.yml @@ -0,0 +1,63 @@ +--- +all: + hosts: + openop_1: + ansible_host: 192.168.123.188 + ansible_connection: ssh + ansible_user: ubuntu + ansible_ssh_private_key_file: "~/.ssh/keys/openop-dev-vm.key" + ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ConnectTimeout=300 -o ServerAliveInterval=30 -o IdentitiesOnly=yes' + ansible_python_interpreter: /usr/bin/python3 + deployment_mode: remote + host_ip: 192.168.123.188 + kubeconfig_output_dir: "/home/ubuntu/kind-cluster-config" + kubeconfig_filename: operator-platform-external-kubeconfig.yaml + cluster_name: dev-all-in-one + + openop_2: + ansible_host: 192.168.123.178 + ansible_connection: ssh + ansible_user: ubuntu + ansible_ssh_private_key_file: "~/.ssh/keys/openop-dev-vm.key" + ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ConnectTimeout=300 -o ServerAliveInterval=30 -o IdentitiesOnly=yes' + ansible_python_interpreter: /usr/bin/python3 + deployment_mode: remote + host_ip: 192.168.123.178 + kubeconfig_output_dir: "/home/ubuntu/kind-cluster-config" + kubeconfig_filename: op2-kubeconfig.yaml + cluster_name: node-2 + api_server_port: 6444 + oeg_partner_api_root: "http://192.168.123.155:30989" + oeg_avail_zone_notif_link: "http://192.168.123.178:32263/oeg/1.0.0/availability-zones/notifications" + + openop_3: + ansible_host: 192.168.123.155 + ansible_connection: ssh + ansible_user: ubuntu + ansible_ssh_private_key_file: "~/.ssh/keys/openop-dev-vm.key" + ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ConnectTimeout=300 -o ServerAliveInterval=30 -o IdentitiesOnly=yes' + ansible_python_interpreter: /usr/bin/python3 + deployment_mode: remote + host_ip: 192.168.123.155 + kubeconfig_output_dir: "/home/ubuntu/kind-cluster-config" + kubeconfig_filename: op1-kubeconfig.yaml + cluster_name: node-1 + api_server_port: 6443 + oeg_partner_api_root: "http://192.168.123.178:30989" + oeg_avail_zone_notif_link: "http://192.168.123.155:32263/oeg/1.0.0/availability-zones/notifications" + + children: + openop_dev: + hosts: + openop_1: + op1_nodes: + hosts: + openop_3: + op2_nodes: + hosts: + openop_2: + k8s_clusters: + children: + openop_dev: + op1_nodes: + op2_nodes: diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml index 71e2526eb916d663729886bd84db6d48bb2b71ab..5c71095ae6ef103f5e9e61142642a1c8016f83f4 100644 --- a/ansible/inventory/hosts.yml +++ b/ansible/inventory/hosts.yml @@ -16,10 +16,29 @@ all: ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ConnectTimeout=300 -o ServerAliveInterval=30 -o IdentitiesOnly=yes' ansible_python_interpreter: /usr/bin/python3 deployment_mode: remote - host_ip: 192.0.2.10 - # kubeconfig_output_dir: "/home/ubuntu/kind-cluster-config" - # kubeconfig_filename: operator-platform-external-kubeconfig.yaml - # cluster_name: example-all-in-one + host_ip: 192.168.123.214 + cluster_name: sunrise-remote + api_server_port: 16443 + kubeconfig_output_dir: "/home/ubuntu/.kube" + kubeconfig_filename: "config" + oop_deployment_profile: lite2edge + + openop_kul: + ansible_host: 193.190.168.50 + ansible_connection: ssh + ansible_user: i2cat + ansible_ssh_private_key_file: "~/.ssh/keys/sn-keypair" + ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ConnectTimeout=300 -o ServerAliveInterval=30 -o IdentitiesOnly=yes' + ansible_python_interpreter: /usr/bin/python3 + deployment_mode: remote + host_ip: 193.190.168.50 + cluster_name: op2-remote + api_server_port: 16443 + kubeconfig_output_dir: /home/i2cat/.kube + kubeconfig_filename: "config" + oop_deployment_profile: kubernetes + deploy_prometheus: false + skip_prerequisites: true example_op_1: ansible_host: 198.51.100.11 @@ -69,7 +88,11 @@ all: children: openop_dev: hosts: - example_op_1: + openop_1: + existing_cluster_nodes: + hosts: + openop_sunrise: + openop_kul: op1_nodes: hosts: example_op_3: diff --git a/ansible/playbooks/infrastructure/prerequisites/setup-docker.yml b/ansible/playbooks/infrastructure/prerequisites/setup-docker.yml index 626edf7cfcebb290e00ff8e707ebfa5f1ce673be..cf2ba4fa5ea03ad5e92e3b7b086dbaeac7a2222f 100644 --- a/ansible/playbooks/infrastructure/prerequisites/setup-docker.yml +++ b/ansible/playbooks/infrastructure/prerequisites/setup-docker.yml @@ -4,7 +4,7 @@ # Usage: ansible-playbook playbooks/infrastructure/prerequisites/setup-docker.yml - name: Install Docker - hosts: k8s_clusters + hosts: "{{ target_hosts | default('k8s_clusters') }}" gather_facts: true become: true diff --git a/ansible/playbooks/infrastructure/prerequisites/setup-kubectl.yml b/ansible/playbooks/infrastructure/prerequisites/setup-kubectl.yml index 606693cd73fa7fd0da071296559dd97c91b3c027..b02f8433195b830d80c8d853c6ab67c1815677a0 100644 --- a/ansible/playbooks/infrastructure/prerequisites/setup-kubectl.yml +++ b/ansible/playbooks/infrastructure/prerequisites/setup-kubectl.yml @@ -4,7 +4,7 @@ # Usage: ansible-playbook playbooks/infrastructure/prerequisites/setup-kubectl.yml - name: Install kubectl - hosts: k8s_clusters + hosts: "{{ target_hosts | default('k8s_clusters') }}" gather_facts: true become: true diff --git a/ansible/playbooks/infrastructure/prerequisites/setup-python-libs.yml b/ansible/playbooks/infrastructure/prerequisites/setup-python-libs.yml index 0cf2d857b6b7a6d9fa02e7f7fe66790f4f30dfec..fbc23ac39d4f978bf4ef8b5f3d8dce242402f541 100644 --- a/ansible/playbooks/infrastructure/prerequisites/setup-python-libs.yml +++ b/ansible/playbooks/infrastructure/prerequisites/setup-python-libs.yml @@ -4,7 +4,7 @@ # Usage: ansible-playbook playbooks/infrastructure/prerequisites/setup-python-libs.yml - name: Install Python Libraries - hosts: k8s_clusters + hosts: "{{ target_hosts | default('k8s_clusters') }}" gather_facts: true become: true diff --git a/ansible/playbooks/scenarios/README.md b/ansible/playbooks/scenarios/README.md index c6fb8a765a4ab04a72b04cca371049ac091ce223..3a85626e849fb40c5cd9eea952fe4e3f679d88aa 100644 --- a/ansible/playbooks/scenarios/README.md +++ b/ansible/playbooks/scenarios/README.md @@ -27,6 +27,15 @@ ansible-playbook -i inventory/hosts.local.yml playbooks/scenarios/dual_oop/deplo ansible-playbook -i inventory/hosts.local.yml playbooks/scenarios/dual_oop/deploy.yml --limit example_op_2 ``` +### `full_oop_existing_cluster` + +One complete Operator Platform on a host that already has a running cluster. + +```bash +ansible-playbook playbooks/scenarios/full_oop_existing_cluster/deploy.yml --limit openop_sunrise +ansible-playbook playbooks/scenarios/full_oop_existing_cluster/deploy.yml --limit openop_kul +``` + ## Undeploy Each scenario provides: @@ -49,6 +58,7 @@ ansible-playbook -i inventory/hosts.local.yml playbooks/scenarios/dual_oop/quick ## Inventory expectations - `k8s_clusters`: shared host group for common prerequisite playbooks +- `existing_cluster_nodes`: hosts with an already running Kubernetes cluster - `op1_nodes`: first host for `dual_oop` - `op2_nodes`: second host for `dual_oop` - `openop_dev`: optional host group for one-host `full_oop` @@ -59,3 +69,4 @@ ansible-playbook -i inventory/hosts.local.yml playbooks/scenarios/dual_oop/quick - `inventory/hosts.yml` is a sanitized example; keep site values in `inventory/hosts.local.yml`. - `secrets.yml` is optional and loaded only when present. - For remote deployments, kubeconfigs are fetched to `~/kind-cluster-configs/`. +- `full_oop_existing_cluster` expects `cluster_name`, `api_server_port`, `kubeconfig_output_dir`, and `kubeconfig_filename` in inventory. diff --git a/ansible/playbooks/scenarios/dual_oop/deploy.yml b/ansible/playbooks/scenarios/dual_oop/deploy.yml index ca3614d3ca642aeef50cc949ee1900aa776531a2..07e23a46cead609536cbf86d9e629d0d5722a96d 100644 --- a/ansible/playbooks/scenarios/dual_oop/deploy.yml +++ b/ansible/playbooks/scenarios/dual_oop/deploy.yml @@ -23,6 +23,11 @@ vars: # OP1-specific configuration + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" kind_cluster_name: "op1" cluster_name: "{{ kind_cluster_name }}" worker_nodes: 0 @@ -166,6 +171,11 @@ vars: # OP2-specific configuration + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" kind_cluster_name: "op2" cluster_name: "{{ kind_cluster_name }}" worker_nodes: 0 diff --git a/ansible/playbooks/scenarios/dual_oop/graceful_undeploy.yml b/ansible/playbooks/scenarios/dual_oop/graceful_undeploy.yml index 1d5557660829150622619af2710ae709f61e1d7d..aab8c16f70a64a8f8843faa50752dcf74867df66 100644 --- a/ansible/playbooks/scenarios/dual_oop/graceful_undeploy.yml +++ b/ansible/playbooks/scenarios/dual_oop/graceful_undeploy.yml @@ -13,6 +13,11 @@ vars: # OP1-specific configuration + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" kind_cluster_name: "op1" cluster_name: "{{ kind_cluster_name }}" kubeconfig_filename: "op1-kubeconfig.yaml" @@ -131,6 +136,11 @@ vars: # OP2-specific configuration + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" kind_cluster_name: "op2" cluster_name: "{{ kind_cluster_name }}" kubeconfig_filename: "op2-kubeconfig.yaml" diff --git a/ansible/playbooks/scenarios/full_oop/deploy.yml b/ansible/playbooks/scenarios/full_oop/deploy.yml index 659b446346cb891b0986a0acfbc55951505a01cc..09940a05d8a189635a7faa872d01d4aad5e86218 100644 --- a/ansible/playbooks/scenarios/full_oop/deploy.yml +++ b/ansible/playbooks/scenarios/full_oop/deploy.yml @@ -18,13 +18,17 @@ hosts: k8s_clusters gather_facts: true vars_files: - - ../../../inventory/profiles/{{ oop_deployment_profile | default('kubernetes') }}.yml + - ../../../secrets.yml + - ../../../inventory/profiles/{{ oop_deployment_profile | default('lite2edge') }}.yml vars: # Scenario-specific configuration - worker_nodes: 0 + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" prepare_oop_storage: true - federation_remote_enabled: false edgecloud_platform: "{{ oop_profile_edgecloud_platform }}" srm_edge_cloud_adapter_name: "{{ oop_profile_srm_edge_cloud_adapter_name }}" srm_adapter_base_url: "{{ oop_profile_srm_adapter_base_url }}" @@ -128,7 +132,7 @@ ✓ OEG Controller ✓ Federation Manager (1 instance) ✓ Homer Dashboard - ✓ Edge profile: {{ oop_profile_name }} + {% if edgecloud_platform == 'lite2edge' %}✓ Lite2Edge Platform{% else %}⊘ Lite2Edge Platform (skipped){% endif %} Access Points: Kubeconfig: {{ kind_config_dir }}/{{ kubeconfig_filename }} @@ -139,5 +143,5 @@ Next Steps: 1. Verify all pods: kubectl get pods -A 2. Access Homer dashboard for service overview - 3. Check Federation Manager: kubectl get pods -n federation-manager + 3. Check Federation Manager: kubectl get pods -n {{ oop_federation_manager_namespace }} ========================================== diff --git a/ansible/playbooks/scenarios/full_oop/graceful_undeploy.yml b/ansible/playbooks/scenarios/full_oop/graceful_undeploy.yml index 924a65f5e36f2494d7b08fde731d40814edc0046..f363bb3a64bfc89a2f81782491073184e4ece83e 100644 --- a/ansible/playbooks/scenarios/full_oop/graceful_undeploy.yml +++ b/ansible/playbooks/scenarios/full_oop/graceful_undeploy.yml @@ -8,6 +8,11 @@ gather_facts: true vars: + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" # Set all components to absent state lite2edge_state: absent homer_state: absent diff --git a/ansible/playbooks/scenarios/full_oop_existing_cluster/deploy.yml b/ansible/playbooks/scenarios/full_oop_existing_cluster/deploy.yml new file mode 100644 index 0000000000000000000000000000000000000000..69f9ff50c04708ca16d1a2863ca89f70e3e2187e --- /dev/null +++ b/ansible/playbooks/scenarios/full_oop_existing_cluster/deploy.yml @@ -0,0 +1,174 @@ +--- +# Playbook: Full OOP Existing Cluster Deployment +# Description: Deploys complete Operator Platform with a single Federation Manager on an already running Kubernetes cluster +# Usage: ansible-playbook playbooks/scenarios/full_oop_existing_cluster/deploy.yml + +# ========================================== +# User-Space Python Bootstrap +# ========================================== +- name: Bootstrap user-space Ansible Python for existing clusters + hosts: existing_cluster_nodes + gather_facts: false + + vars: + ansible_user_python_venv: "/home/{{ ansible_user }}/.ansible-venv" + + tasks: + - name: Create user-space Python venv and install Ansible dependencies + ansible.builtin.raw: >- + python3 -m venv {{ ansible_user_python_venv }} && + {{ ansible_user_python_venv }}/bin/pip install --upgrade pip && + {{ ansible_user_python_venv }}/bin/pip install kubernetes PyYAML requests + + - name: Reset SSH connection so later tasks can use bootstrapped interpreter + ansible.builtin.meta: reset_connection + +# ========================================== +# Full OOP Deployment On Existing Cluster +# ========================================== +- name: Deploy Full OOP on Existing Cluster + hosts: existing_cluster_nodes + gather_facts: true + vars_files: + - ../../../secrets.yml + - ../../../inventory/profiles/{{ oop_deployment_profile | default('lite2edge') }}.yml + + vars: + ansible_python_interpreter: "/home/{{ ansible_user }}/.ansible-venv/bin/python" + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" + prepare_oop_storage: true + k8s_distribution: existing + k8s_platform: existing + edgecloud_platform: "{{ oop_profile_edgecloud_platform }}" + srm_edge_cloud_adapter_name: "{{ oop_profile_srm_edge_cloud_adapter_name }}" + srm_adapter_base_url: "{{ oop_profile_srm_adapter_base_url }}" + federation_manager_ecp_host: "{{ oop_profile_federation_manager_ecp_host }}" + federation_manager_ecp_port: "{{ oop_profile_federation_manager_ecp_port }}" + federation_manager_ecp_client_name: "{{ oop_profile_federation_manager_ecp_client_name }}" + + pre_tasks: + - name: Display deployment information + ansible.builtin.debug: + msg: | + ========================================== + Full OOP Existing Cluster Deployment + ========================================== + Scenario: Single operator platform on running cluster + Deployment Profile: {{ oop_profile_name }} + Federation Managers: 1 (local only) + Target Host: {{ inventory_hostname }} + Ansible Python: {{ ansible_python_interpreter }} + + Cluster Access: + Cluster Name: {{ cluster_name }} + Kubeconfig: {{ kubeconfig_output_dir }}/{{ kubeconfig_filename }} + API Server Port: {{ api_server_port }} + ========================================== + + - name: Set kubeconfig facts from inventory + ansible.builtin.set_fact: + kubeconfig_dir: "{{ kubeconfig_output_dir }}" + kind_config_dir: "{{ kubeconfig_output_dir }}" + kubeconfig_path: "{{ kubeconfig_output_dir }}/{{ kubeconfig_filename }}" + + - name: Get control-plane InternalIPs for Prometheus endpoint discovery + ansible.builtin.shell: >- + kubectl --kubeconfig {{ kubeconfig_path }} get nodes + --selector='node-role.kubernetes.io/control-plane' + -o jsonpath='{range .items[*]}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\n"}{end}{end}' + register: prometheus_control_plane_ips_result + changed_when: false + + - name: Get master-labeled InternalIPs for Prometheus endpoint discovery fallback + ansible.builtin.shell: >- + kubectl --kubeconfig {{ kubeconfig_path }} get nodes + --selector='node-role.kubernetes.io/master' + -o jsonpath='{range .items[*]}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\n"}{end}{end}' + register: prometheus_master_ips_result + changed_when: false + + - name: Get worker InternalIPs for Prometheus endpoint discovery + ansible.builtin.shell: >- + kubectl --kubeconfig {{ kubeconfig_path }} get nodes + --selector='!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master' + -o jsonpath='{range .items[*]}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\n"}{end}{end}' + register: prometheus_worker_ips_result + changed_when: false + + - name: Set Prometheus node IPs from existing cluster + ansible.builtin.set_fact: + prometheus_control_plane_ips: >- + {{ + (prometheus_control_plane_ips_result.stdout_lines | reject('equalto', '') | list) + if (prometheus_control_plane_ips_result.stdout_lines | reject('equalto', '') | list | length > 0) + else (prometheus_master_ips_result.stdout_lines | reject('equalto', '') | list) + }} + prometheus_worker_ips: "{{ prometheus_worker_ips_result.stdout_lines | reject('equalto', '') | list }}" + + roles: + - role: helm + tags: [infrastructure, helm] + + - role: prometheus + when: deploy_prometheus | default(true) | bool + tags: [infrastructure, monitoring, prometheus] + + - role: node-feature-discovery + tags: [infrastructure, nfd] + + - role: zot + tags: [oop, zot, registry] + + - role: artefact-manager + tags: [oop, artefact-manager] + + - role: srm + tags: [oop, srm] + + - role: oeg + tags: [oop, oeg] + + - role: federation-manager + tags: [oop, federation-manager] + vars: + federation_manager_remote: false + + - role: homer + tags: [oop, homer, dashboard] + + - role: lite2edge + when: edgecloud_platform == 'lite2edge' + tags: [edge, lite2edge] + + post_tasks: + - name: Display deployment summary + ansible.builtin.debug: + msg: | + ========================================== + Full OOP Existing Cluster Deployment Complete + ========================================== + + Components Deployed: + ✓ Existing K8s Cluster ({{ cluster_name }}) + {% if deploy_prometheus | default(true) | bool %}✓ Prometheus Monitoring{% else %}⊘ Prometheus Monitoring (skipped){% endif %} + ✓ Node Feature Discovery + ✓ Zot Registry + ✓ Artefact Manager + ✓ SRM Controller + ✓ OEG Controller + ✓ Federation Manager (1 instance) + ✓ Homer Dashboard + {% if edgecloud_platform == 'lite2edge' %}✓ Lite2Edge Platform{% else %}⊘ Lite2Edge Platform (skipped){% endif %} + + Access Points: + Kubeconfig: {{ kubeconfig_path }} + {% if deploy_prometheus | default(true) | bool %}Prometheus: http://{{ host_ip | default('localhost') }}:30090 + Grafana: http://{{ host_ip | default('localhost') }}:30091 + {% endif %}Homer: http://{{ host_ip | default('localhost') }}:{{ homer_nodeport }} + + Existing cluster preserved. + ========================================== diff --git a/ansible/playbooks/scenarios/full_oop_existing_cluster/graceful_undeploy.yml b/ansible/playbooks/scenarios/full_oop_existing_cluster/graceful_undeploy.yml new file mode 100644 index 0000000000000000000000000000000000000000..dc4758fe9eb1f0542f4930f2653e537f575b696e --- /dev/null +++ b/ansible/playbooks/scenarios/full_oop_existing_cluster/graceful_undeploy.yml @@ -0,0 +1,122 @@ +--- +# Playbook: Graceful Full OOP Existing Cluster Undeployment Scenario +# Description: Removes Full OOP components from an existing cluster without deleting the cluster +# Usage: ansible-playbook playbooks/scenarios/full_oop_existing_cluster/graceful_undeploy.yml + +- name: Bootstrap user-space Ansible Python for existing clusters + hosts: existing_cluster_nodes + gather_facts: false + + vars: + ansible_user_python_venv: "/home/{{ ansible_user }}/.ansible-venv" + + tasks: + - name: Create user-space Python venv and install Ansible dependencies + ansible.builtin.raw: >- + python3 -m venv {{ ansible_user_python_venv }} && + {{ ansible_user_python_venv }}/bin/pip install --upgrade pip && + {{ ansible_user_python_venv }}/bin/pip install kubernetes PyYAML requests + + - name: Reset SSH connection so later tasks can use bootstrapped interpreter + ansible.builtin.meta: reset_connection + +- name: Graceful Undeploy Full OOP from Existing Cluster + hosts: existing_cluster_nodes + gather_facts: true + + vars: + ansible_python_interpreter: "/home/{{ ansible_user }}/.ansible-venv/bin/python" + oop_core_namespace: oop + oop_federation_manager_namespace: "{{ oop_core_namespace }}" + oop_artefact_manager_namespace: "{{ oop_core_namespace }}" + oop_zot_namespace: "{{ oop_core_namespace }}" + oop_homer_namespace: "{{ oop_core_namespace }}" + lite2edge_state: absent + homer_state: absent + federation_manager_state: absent + oeg_state: absent + srm_state: absent + artefact_manager_state: absent + i2edge_state: absent + zot_state: absent + node_feature_discovery_state: absent + prometheus_state: absent + helm_state: absent + k8s_distribution: existing + k8s_platform: existing + + vars_prompt: + - name: confirm_deletion + prompt: "Are you sure you want to undeploy the Full OOP components from the existing cluster? The cluster itself will be kept. (yes/no)" + default: "no" + private: false + + pre_tasks: + - name: Display undeployment information + ansible.builtin.debug: + msg: | + ========================================== + Graceful Full OOP Existing Cluster Undeployment + ========================================== + Target Host: {{ inventory_hostname }} + Cluster Name: {{ cluster_name }} + + This removes platform components only. + Existing Kubernetes cluster stays running. + ========================================== + + - name: Exit if deletion not confirmed + ansible.builtin.meta: end_play + when: confirm_deletion | lower not in ['yes', 'y'] + + - name: Set kubeconfig facts from inventory + ansible.builtin.set_fact: + kubeconfig_dir: "{{ kubeconfig_output_dir }}" + kind_config_dir: "{{ kubeconfig_output_dir }}" + kubeconfig_path: "{{ kubeconfig_output_dir }}/{{ kubeconfig_filename }}" + + roles: + - role: lite2edge + tags: [edge, lite2edge] + + - role: homer + tags: [oop, homer, dashboard] + + - role: federation-manager + tags: [oop, federation-manager] + + - role: oeg + tags: [oop, oeg] + + - role: srm + tags: [oop, srm] + + - role: artefact-manager + tags: [oop, artefact-manager] + + - role: i2edge + tags: [oop, i2edge] + + - role: zot + tags: [oop, zot, registry] + + - role: node-feature-discovery + tags: [infrastructure, nfd] + + - role: prometheus + when: deploy_prometheus | default(true) | bool + tags: [infrastructure, monitoring, prometheus] + + - role: helm + tags: [infrastructure, helm] + + post_tasks: + - name: Display undeployment summary + ansible.builtin.debug: + msg: | + ========================================== + Full OOP Existing Cluster Undeployment Complete + ========================================== + Removed platform components from {{ cluster_name }}. + Existing Kubernetes cluster preserved. + ========================================== diff --git a/ansible/roles/artefact-manager/defaults/main.yml b/ansible/roles/artefact-manager/defaults/main.yml index f2c2a0ec38bcadc91c0419cbb9a7bc3b05318333..b410fe965c8a226a3b707d5280761bfd5cd0c241 100644 --- a/ansible/roles/artefact-manager/defaults/main.yml +++ b/ansible/roles/artefact-manager/defaults/main.yml @@ -23,4 +23,4 @@ artefact_manager_image_pull_policy: "Always" # ========================================== # Namespace # ========================================== -artefact_manager_namespace: "artefact-manager" +artefact_manager_namespace: "{{ oop_artefact_manager_namespace | default('artefact-manager') }}" diff --git a/ansible/roles/artefact-manager/templates/artefact-manager.yaml.j2 b/ansible/roles/artefact-manager/templates/artefact-manager.yaml.j2 index 05083b3339e20d6b78cfd1d224077f259dba9157..6f19e195587e2f5d8b96e777d08724bab0480cab 100644 --- a/ansible/roles/artefact-manager/templates/artefact-manager.yaml.j2 +++ b/ansible/roles/artefact-manager/templates/artefact-manager.yaml.j2 @@ -15,11 +15,11 @@ data: insecure = true [[registry]] - location = "zot.zot.svc.cluster.local:5000" + location = "zot.{{ oop_zot_namespace | default('zot') }}.svc.cluster.local:5000" insecure = true [[registry]] - location = "zot.zot.svc.cluster.local:5000" + location = "zot.{{ oop_zot_namespace | default('zot') }}.svc.cluster.local:5000" insecure = true --- apiVersion: apps/v1 diff --git a/ansible/roles/federation-manager/defaults/main.yml b/ansible/roles/federation-manager/defaults/main.yml index 59016aa119aedc7a04db0ac26308ab908b6cbd2e..5953634ac33a779cc455f8d927d2ce060a0554e3 100644 --- a/ansible/roles/federation-manager/defaults/main.yml +++ b/ansible/roles/federation-manager/defaults/main.yml @@ -15,7 +15,7 @@ federation_manager_variant: "local" federation_manager_state: present # Namespace (override per deployment) -federation_manager_namespace: "federation-manager" +federation_manager_namespace: "{{ oop_federation_manager_namespace | default('federation-manager') }}" # Kubeconfig # K8s-distro-agnostic kubeconfig path @@ -58,8 +58,18 @@ federation_manager_keycloak_realm: "federation" federation_manager_mongodb_image: "mongo" federation_manager_mongodb_tag: "6.0" federation_manager_mongodb_nodeport: 30017 -federation_manager_mongodb_data_dir: "/tmp/db" +federation_manager_mongodb_persistence_enabled: true +federation_manager_mongodb_persistence_size: 1Gi +federation_manager_mongodb_persistence_access_mode: ReadWriteOnce +federation_manager_mongodb_persistence_storage_class: manual +federation_manager_mongodb_persistence_create_pv: true +federation_manager_mongodb_persistence_use_default_storage_class: true +federation_manager_mongodb_persistence_node_name: "" +federation_manager_mongodb_persistence_auto_configure: true +federation_manager_mongodb_preserve_data: true +federation_manager_mongodb_data_dir: "/mnt/data/federation-manager-mongodb-{{ federation_manager_variant }}" federation_manager_mongodb_cleanup_data: true +federation_manager_mongodb_init_cleanup_lock: true # Federation Manager MongoDB settings federation_manager_mongodb_host: "mongodb" @@ -78,7 +88,7 @@ federation_manager_op_platform_caps: "homeRouting" # ========================================== # Service Resource Manager Integration # ========================================== -federation_manager_srm_host: "srm.oop.svc.cluster.local" +federation_manager_srm_host: "srm.{{ oop_srm_namespace | default(oop_namespace | default('oop')) }}.svc.cluster.local" federation_manager_srm_port: 8080 # ========================================== @@ -92,7 +102,7 @@ federation_manager_ecp_flavour_id: "default" # ========================================== # Artefact Manager Integration # ========================================== -federation_manager_am_host: "artefact-manager.artefact-manager.svc.cluster.local" +federation_manager_am_host: "artefact-manager.{{ oop_artefact_manager_namespace | default('artefact-manager') }}.svc.cluster.local" federation_manager_am_port: 8000 federation_manager_am_enabled: "false" # Destination registry for artefact-manager copies. diff --git a/ansible/roles/federation-manager/tasks/deploy.yml b/ansible/roles/federation-manager/tasks/deploy.yml index c60d7b978954f4a8989525265a82b9e8fea07074..78f827e7abbb5121e28340aca42e8b5a24c02fdf 100644 --- a/ansible/roles/federation-manager/tasks/deploy.yml +++ b/ansible/roles/federation-manager/tasks/deploy.yml @@ -2,20 +2,6 @@ # Federation Manager deployment tasks # Deploys MongoDB, Keycloak, and Federation Manager -# ========================================== -# Registry Authentication -# ========================================== -- name: Log in to private GitLab registry (if token is provided) - community.docker.docker_login: - registry_url: "{{ docker_registry_host }}" - username: "{{ docker_registry_username }}" - password: "{{ docker_registry_password }}" - when: - - federation_manager_image_pull_secret_enabled | default(false) - - docker_registry_host | default('') | length > 0 - - docker_registry_username | default('') | length > 0 - - docker_registry_password | default('') | length > 0 - # ========================================== # Namespace Setup # ========================================== @@ -48,6 +34,189 @@ - docker_registry_username | default('') | length > 0 - docker_registry_password | default('') | length > 0 +- name: Get available storage classes + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + kubeconfig: "{{ federation_manager_kubeconfig }}" + register: federation_manager_storage_classes + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + +- name: Detect dynamic storage classes + ansible.builtin.set_fact: + federation_manager_dynamic_storage_classes: >- + {{ (federation_manager_storage_classes.resources | default([])) + | rejectattr('provisioner', 'equalto', 'kubernetes.io/no-provisioner') + | map(attribute='metadata.name') + | list }} + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + +- name: Detect local storage classes + ansible.builtin.set_fact: + federation_manager_local_storage_classes: >- + {{ (federation_manager_storage_classes.resources | default([])) + | selectattr('provisioner', 'equalto', 'kubernetes.io/no-provisioner') + | map(attribute='metadata.name') + | list }} + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + +- name: Detect default storage class + ansible.builtin.shell: | + kubectl get storageclass -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' | head -n 1 + args: + executable: /bin/bash + register: federation_manager_default_storage_class_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ federation_manager_kubeconfig }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + +- name: Set default storage class fact + ansible.builtin.set_fact: + federation_manager_default_storage_class: "{{ federation_manager_default_storage_class_result.stdout | trim }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + +- name: Select default dynamic storage class + ansible.builtin.set_fact: + federation_manager_mongodb_persistence_use_default_storage_class: true + federation_manager_mongodb_persistence_storage_class: "{{ federation_manager_default_storage_class }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + - federation_manager_dynamic_storage_classes | length > 0 + - federation_manager_default_storage_class | length > 0 + +- name: Select non-default dynamic storage class + ansible.builtin.set_fact: + federation_manager_mongodb_persistence_use_default_storage_class: false + federation_manager_mongodb_persistence_storage_class: "{{ federation_manager_dynamic_storage_classes[0] }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + - federation_manager_dynamic_storage_classes | length > 0 + - federation_manager_default_storage_class | length == 0 + +- name: Get schedulable worker node name + ansible.builtin.command: > + kubectl get nodes + -l '!node-role.kubernetes.io/control-plane' + --field-selector spec.unschedulable!=true + -o jsonpath='{.items[0].metadata.name}' + register: federation_manager_worker_node_name_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ federation_manager_kubeconfig }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + - federation_manager_dynamic_storage_classes | length == 0 + +- name: Get schedulable node name fallback + ansible.builtin.command: > + kubectl get nodes + --field-selector spec.unschedulable!=true + -o jsonpath='{.items[0].metadata.name}' + register: federation_manager_node_name_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ federation_manager_kubeconfig }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + - federation_manager_dynamic_storage_classes | length == 0 + - federation_manager_worker_node_name_result.stdout | default('') | length == 0 + +- name: Select node for local storage + ansible.builtin.set_fact: + federation_manager_mongodb_persistence_node_name: >- + {{ (federation_manager_worker_node_name_result.stdout | default('') | length > 0) + | ternary(federation_manager_worker_node_name_result.stdout, federation_manager_node_name_result.stdout | default('')) }} + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + - federation_manager_dynamic_storage_classes | length == 0 + +- name: Select local storage class + ansible.builtin.set_fact: + federation_manager_mongodb_persistence_use_default_storage_class: false + federation_manager_mongodb_persistence_create_pv: true + federation_manager_mongodb_persistence_storage_class: >- + {{ (federation_manager_local_storage_classes | length > 0) | ternary(federation_manager_local_storage_classes[0], 'local-storage') }} + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_auto_configure + - federation_manager_dynamic_storage_classes | length == 0 + - federation_manager_mongodb_persistence_node_name | length > 0 + +- name: Get existing MongoDB deployment availability + ansible.builtin.command: > + kubectl get deployment mongodb + --namespace {{ federation_manager_namespace }} + -o jsonpath='{.status.availableReplicas}' + register: federation_manager_mongodb_available_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ federation_manager_kubeconfig }}" + +- name: Get existing MongoDB PV details + ansible.builtin.command: > + kubectl get pv mongodb-{{ federation_manager_variant }} + -o jsonpath='{.spec.hostPath.path}|{.spec.storageClassName}|{range .spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[*]}{.}{end}|{.status.phase}' + register: federation_manager_mongodb_pv_details_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ federation_manager_kubeconfig }}" + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_create_pv + - not federation_manager_mongodb_persistence_use_default_storage_class + +- name: Determine if MongoDB storage reset is required + ansible.builtin.set_fact: + federation_manager_mongodb_reset_required: >- + {{ + federation_manager_mongodb_pv_details_result.rc == 0 and ( + (federation_manager_mongodb_pv_details_result.stdout.split('|') | last) in ['Released', 'Failed'] or + (federation_manager_mongodb_pv_details_result.stdout.split('|') | first) != federation_manager_mongodb_data_dir or + (federation_manager_mongodb_pv_details_result.stdout.split('|'))[1] != federation_manager_mongodb_persistence_storage_class or + ( + federation_manager_mongodb_persistence_node_name | length > 0 and + (federation_manager_mongodb_pv_details_result.stdout.split('|'))[2] != federation_manager_mongodb_persistence_node_name + ) + ) and (federation_manager_mongodb_available_result.stdout | default('0') | trim) != '1' + }} + when: + - federation_manager_mongodb_persistence_enabled + - federation_manager_mongodb_persistence_create_pv + - not federation_manager_mongodb_persistence_use_default_storage_class + +- name: Fail when existing Federation Manager MongoDB storage conflicts with desired config + ansible.builtin.fail: + msg: >- + Existing MongoDB storage for Federation Manager conflicts with desired configuration. + Current PV details={{ federation_manager_mongodb_pv_details_result.stdout | default('not found') | quote }}. + Desired hostPath={{ federation_manager_mongodb_data_dir | quote }}, + desired storageClass={{ federation_manager_mongodb_persistence_storage_class | quote }}, + desired node={{ federation_manager_mongodb_persistence_node_name | default('') | quote }}. + Run the graceful undeploy/cleanup flow before redeploying. + when: + - federation_manager_mongodb_reset_required | default(false) + # ========================================== # MongoDB Deployment # ========================================== diff --git a/ansible/roles/federation-manager/tasks/undeploy.yml b/ansible/roles/federation-manager/tasks/undeploy.yml index 30ccea89bb23211effa2c56516294561d0273ed0..732ef47dcb145b8eb799fc0a6ed281b6e8d7a0fe 100644 --- a/ansible/roles/federation-manager/tasks/undeploy.yml +++ b/ansible/roles/federation-manager/tasks/undeploy.yml @@ -84,6 +84,15 @@ name: mongodb kubeconfig: "{{ federation_manager_kubeconfig }}" +- name: Get Federation Manager namespace state + kubernetes.core.k8s_info: + api_version: v1 + kind: Namespace + name: "{{ federation_manager_namespace }}" + kubeconfig: "{{ federation_manager_kubeconfig }}" + register: federation_manager_namespace_info + when: federation_manager_mongodb_cleanup_data + - name: Create MongoDB hostPath cleanup DaemonSet kubernetes.core.k8s: state: present @@ -127,7 +136,10 @@ path: "{{ federation_manager_mongodb_data_dir }}" type: DirectoryOrCreate kubeconfig: "{{ federation_manager_kubeconfig }}" - when: federation_manager_mongodb_cleanup_data + when: + - federation_manager_mongodb_cleanup_data + - federation_manager_namespace_info.resources | length > 0 + - federation_manager_namespace_info.resources[0].status.phase != 'Terminating' - name: Wait for MongoDB cleanup DaemonSet ansible.builtin.command: > @@ -137,7 +149,10 @@ changed_when: false environment: KUBECONFIG: "{{ federation_manager_kubeconfig }}" - when: federation_manager_mongodb_cleanup_data + when: + - federation_manager_mongodb_cleanup_data + - federation_manager_namespace_info.resources | length > 0 + - federation_manager_namespace_info.resources[0].status.phase != 'Terminating' - name: Delete MongoDB cleanup DaemonSet kubernetes.core.k8s: @@ -147,7 +162,10 @@ namespace: "{{ federation_manager_namespace }}" name: fm-mongodb-cleanup kubeconfig: "{{ federation_manager_kubeconfig }}" - when: federation_manager_mongodb_cleanup_data + when: + - federation_manager_mongodb_cleanup_data + - federation_manager_namespace_info.resources | length > 0 + - federation_manager_namespace_info.resources[0].status.phase != 'Terminating' - name: Delete MongoDB PVC kubernetes.core.k8s: @@ -166,12 +184,6 @@ name: "mongodb-{{ federation_manager_variant }}" kubeconfig: "{{ federation_manager_kubeconfig }}" -- name: Remove MongoDB data directory on host - ansible.builtin.file: - path: "{{ federation_manager_mongodb_data_dir }}" - state: absent - when: federation_manager_mongodb_cleanup_data - - name: Delete image pull secret kubernetes.core.k8s: state: absent diff --git a/ansible/roles/federation-manager/templates/mongo-db.yaml.j2 b/ansible/roles/federation-manager/templates/mongo-db.yaml.j2 index 7e0ca2889148ec3cb8578df270bee39ad61ae4fe..a0cdd56f0112f8fc5cd2f9a3a3028578a54e5b46 100644 --- a/ansible/roles/federation-manager/templates/mongo-db.yaml.j2 +++ b/ansible/roles/federation-manager/templates/mongo-db.yaml.j2 @@ -1,18 +1,31 @@ --- +{% if federation_manager_mongodb_persistence_enabled and federation_manager_mongodb_persistence_create_pv and not federation_manager_mongodb_persistence_use_default_storage_class %} kind: PersistentVolume apiVersion: v1 metadata: name: mongodb-{{ federation_manager_variant }} spec: capacity: - storage: 1Gi + storage: {{ federation_manager_mongodb_persistence_size }} hostPath: path: {{ federation_manager_mongodb_data_dir }} + type: DirectoryOrCreate accessModes: - - ReadWriteOnce + - {{ federation_manager_mongodb_persistence_access_mode }} persistentVolumeReclaimPolicy: Retain - storageClassName: manual + storageClassName: {{ federation_manager_mongodb_persistence_storage_class }} +{% if federation_manager_mongodb_persistence_node_name | length > 0 %} + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - {{ federation_manager_mongodb_persistence_node_name }} +{% endif %} --- +{% endif %} kind: PersistentVolumeClaim apiVersion: v1 metadata: @@ -20,12 +33,14 @@ metadata: namespace: {{ federation_manager_namespace }} spec: accessModes: - - ReadWriteOnce + - {{ federation_manager_mongodb_persistence_access_mode }} resources: requests: - storage: 1Gi + storage: {{ federation_manager_mongodb_persistence_size }} +{% if not federation_manager_mongodb_persistence_use_default_storage_class %} volumeName: mongodb-{{ federation_manager_variant }} - storageClassName: manual + storageClassName: {{ federation_manager_mongodb_persistence_storage_class }} +{% endif %} --- kind: Deployment apiVersion: apps/v1 @@ -46,6 +61,18 @@ spec: - name: storage persistentVolumeClaim: claimName: mongodb +{% if federation_manager_mongodb_init_cleanup_lock %} + initContainers: + - name: mongo-clean-lock + image: '{{ federation_manager_mongodb_image }}:{{ federation_manager_mongodb_tag }}' + command: + - /bin/sh + - -c + - rm -f /data/db/mongod.lock /data/db/WiredTiger.lock + volumeMounts: + - name: storage + mountPath: /data/db +{% endif %} containers: - name: mongodb image: '{{ federation_manager_mongodb_image }}:{{ federation_manager_mongodb_tag }}' diff --git a/ansible/roles/homer/defaults/main.yml b/ansible/roles/homer/defaults/main.yml index d900d8e09dfeac47ce0711a2b0f9cd91501853f9..df3b6358bc7ba5468691b0ade77ffc3214829c31 100644 --- a/ansible/roles/homer/defaults/main.yml +++ b/ansible/roles/homer/defaults/main.yml @@ -22,7 +22,7 @@ homer_nodeport: 30088 # ========================================== # Namespace # ========================================== -homer_namespace: "homer" +homer_namespace: "{{ oop_homer_namespace | default('homer') }}" # ========================================== # Dashboard Content diff --git a/ansible/roles/lite2edge/defaults/main.yml b/ansible/roles/lite2edge/defaults/main.yml index f6aed6f900ae85e847118e050cd5030e8efb1ef6..ade634d9386e0703d9a63254b63e070fab4939cf 100644 --- a/ansible/roles/lite2edge/defaults/main.yml +++ b/ansible/roles/lite2edge/defaults/main.yml @@ -15,7 +15,7 @@ lite2edge_image_pull_secret: registry-pull-secret lite2edge_registry_email: "" # Image Configuration -lite2edge_image: "" +lite2edge_image: "gitlab.i2cat.net:5050/areas/software-networks/operator-platform/lite2edge:latest" lite2edge_image_pull_policy: IfNotPresent # State (present/absent) diff --git a/ansible/roles/lite2edge/tasks/undeploy.yml b/ansible/roles/lite2edge/tasks/undeploy.yml index deb036105787d50a72a2cfd0896179876a469abd..c9ce93d36a1651a6ccb52f5eface888345e5abbd 100644 --- a/ansible/roles/lite2edge/tasks/undeploy.yml +++ b/ansible/roles/lite2edge/tasks/undeploy.yml @@ -8,10 +8,6 @@ kubeconfig: "{{ lite2edge_kubeconfig }}" register: namespace_check -- name: End play if lite2edge not deployed - ansible.builtin.meta: end_host - when: namespace_check.resources | length == 0 - - name: Display undeploy information ansible.builtin.debug: msg: | @@ -22,6 +18,7 @@ - lite2edge application and resources - Namespace: {{ lite2edge_namespace }} ========================================== + when: namespace_check.resources | length > 0 - name: Delete lite2edge resources kubernetes.core.k8s: @@ -31,6 +28,7 @@ namespace: "{{ lite2edge_namespace }}" wait: true wait_timeout: 60 + when: namespace_check.resources | length > 0 - name: Delete lite2edge namespace kubernetes.core.k8s: @@ -41,7 +39,9 @@ kubeconfig: "{{ lite2edge_kubeconfig }}" wait: true wait_timeout: 60 + when: namespace_check.resources | length > 0 - name: Display undeploy result ansible.builtin.debug: msg: "✓ lite2edge resources deleted" + when: namespace_check.resources | length > 0 diff --git a/ansible/roles/oeg/defaults/main.yml b/ansible/roles/oeg/defaults/main.yml index 4dd58acd973767ffb37764ab650dfecd1397b8e7..f685e243bf52c437d9293ccdc1de4d4fbc8d806d 100644 --- a/ansible/roles/oeg/defaults/main.yml +++ b/ansible/roles/oeg/defaults/main.yml @@ -51,9 +51,9 @@ oeg_mongodb_wipe_ssh_user: ubuntu # OEG Environment Variables oeg_mongo_uri: "mongodb://{{ oeg_mongodb_name }}:{{ oeg_mongodb_service_port }}" oeg_srm_host: "http://srm:8080/srm/1.0.0" -oeg_federation_manager_host: "http://federation-manager.federation-manager.svc.cluster.local:8989/operatorplatform/federation/v1" +oeg_federation_manager_host: "http://federation-manager.{{ oop_federation_manager_namespace | default('federation-manager') }}.svc.cluster.local:8989/operatorplatform/federation/v1" oeg_partner_api_root: "http://federation-manager.federation-manager-remote.svc.cluster.local:8989" -oeg_token_endpoint: "http://keycloak.federation-manager.svc.cluster.local:8080/realms/federation/protocol/openid-connect/token" +oeg_token_endpoint: "http://keycloak.{{ oop_federation_manager_namespace | default('federation-manager') }}.svc.cluster.local:8080/realms/federation/protocol/openid-connect/token" oeg_avail_zone_notif_link: "http://oeg.{{ oeg_namespace }}.svc.cluster.local/oeg/1.0.0/availability-zones/notifications" # Resource limits diff --git a/ansible/roles/oeg/tasks/deploy.yml b/ansible/roles/oeg/tasks/deploy.yml index 4a19b71aaa69d513533d753819e624dd833e6445..23f7c0a3de236f332e2f0aefb3951c6fe90c55b5 100644 --- a/ansible/roles/oeg/tasks/deploy.yml +++ b/ansible/roles/oeg/tasks/deploy.yml @@ -313,7 +313,16 @@ - name: Set desired MongoDB PVC storage class ansible.builtin.set_fact: oeg_mongodb_pvc_storage_class_desired: >- - {{ (oeg_mongodb_persistence_use_default_storage_class | bool) | ternary('', oeg_mongodb_persistence_storage_class) }} + {{ + (oeg_mongodb_persistence_create_pv | bool) + | ternary( + oeg_mongodb_persistence_storage_class, + ( + (oeg_mongodb_persistence_use_default_storage_class | bool) + | ternary(oeg_default_storage_class | default(''), oeg_mongodb_persistence_storage_class) + ) + ) + }} when: oeg_mongodb_persistence_enabled - name: Get existing MongoDB PVC @@ -327,33 +336,32 @@ failed_when: false when: oeg_mongodb_persistence_enabled -- name: Force delete MongoDB PVC when storage class changes - ansible.builtin.shell: | - current_sc="$(kubectl -n {{ oeg_namespace }} get pvc mongodb-oeg-pvc -o jsonpath='{.spec.storageClassName}' 2>/dev/null || true)" - if [ -n "$current_sc" ] && [ "$current_sc" != "{{ oeg_mongodb_pvc_storage_class_desired }}" ]; then - kubectl -n {{ oeg_namespace }} patch pvc mongodb-oeg-pvc --type=json -p='[{"op":"remove","path":"/metadata/finalizers"}]' || true - kubectl -n {{ oeg_namespace }} delete pvc mongodb-oeg-pvc --ignore-not-found - fi - register: oeg_force_delete_pvc - changed_when: oeg_force_delete_pvc.rc == 0 - failed_when: false +- name: Fail when existing OEG MongoDB PVC conflicts with desired storage config + ansible.builtin.fail: + msg: >- + Existing PVC {{ oeg_namespace }}/mongodb-oeg-pvc conflicts with desired storage configuration. + Current storageClass={{ oeg_mongodb_pvc_info.resources[0].spec.storageClassName | default('') | quote }}, + current volumeName={{ oeg_mongodb_pvc_info.resources[0].spec.volumeName | default('') | quote }}, + desired storageClass={{ oeg_mongodb_pvc_storage_class_desired | quote }}. + Run the graceful undeploy/cleanup flow before redeploying. when: - oeg_mongodb_persistence_enabled + - oeg_mongodb_pvc_info.resources is defined + - oeg_mongodb_pvc_info.resources | length > 0 + - (oeg_mongodb_pvc_info.resources[0].spec.storageClassName | default('')) != oeg_mongodb_pvc_storage_class_desired -- name: Delete MongoDB PVC if storage class changed - kubernetes.core.k8s: - state: absent - api_version: v1 - kind: PersistentVolumeClaim - namespace: "{{ oeg_namespace }}" - name: mongodb-oeg-pvc - kubeconfig: "{{ oeg_kubeconfig }}" +- name: Fail when OEG MongoDB PVC is pending with stale static binding + ansible.builtin.fail: + msg: >- + PVC {{ oeg_namespace }}/mongodb-oeg-pvc is pending with stale volumeName={{ oeg_mongodb_pvc_info.resources[0].spec.volumeName | default('') | quote }} + under storageClass={{ oeg_mongodb_pvc_info.resources[0].spec.storageClassName | default('') | quote }}. + This requires graceful undeploy cleanup before redeploy. when: - oeg_mongodb_persistence_enabled - - not oeg_mongodb_preserve_data | default(true) - oeg_mongodb_pvc_info.resources is defined - oeg_mongodb_pvc_info.resources | length > 0 - - (oeg_mongodb_pvc_info.resources[0].spec.storageClassName | default('')) != oeg_mongodb_pvc_storage_class_desired + - (oeg_mongodb_pvc_info.resources[0].status.phase | default('')) == 'Pending' + - (oeg_mongodb_pvc_info.resources[0].spec.volumeName | default('')) | length > 0 - name: Create MongoDB PersistentVolume for OEG kubernetes.core.k8s: @@ -383,7 +391,6 @@ when: - oeg_mongodb_persistence_enabled - oeg_mongodb_persistence_create_pv - - not oeg_mongodb_persistence_use_default_storage_class - oeg_mongodb_persistence_node_name | length > 0 - name: Create MongoDB PersistentVolumeClaim for OEG @@ -401,11 +408,12 @@ resources: requests: storage: "{{ oeg_mongodb_persistence_size }}" + volumeName: mongodb-oeg-pv storageClassName: "{{ oeg_mongodb_persistence_storage_class }}" kubeconfig: "{{ oeg_kubeconfig }}" when: - oeg_mongodb_persistence_enabled - - not oeg_mongodb_persistence_use_default_storage_class + - oeg_mongodb_persistence_create_pv - name: Create MongoDB PersistentVolumeClaim for OEG (default storage class) kubernetes.core.k8s: @@ -425,7 +433,7 @@ kubeconfig: "{{ oeg_kubeconfig }}" when: - oeg_mongodb_persistence_enabled - - oeg_mongodb_persistence_use_default_storage_class + - not oeg_mongodb_persistence_create_pv - name: Deploy MongoDB for OEG kubernetes.core.k8s: diff --git a/ansible/roles/srm/defaults/main.yml b/ansible/roles/srm/defaults/main.yml index faf5a071e8fc68341e0ed35f6266d3456f840427..c933520febb2da89594aeffbb9b5446ed8f30ec5 100644 --- a/ansible/roles/srm/defaults/main.yml +++ b/ansible/roles/srm/defaults/main.yml @@ -62,7 +62,7 @@ srm_mongodb_preserve_data: true # Preserve data on undeploy (do not delete # SRM Environment Variables srm_emp_storage_uri: "mongodb://{{ srm_mongodb_name }}:{{ srm_mongodb_service_port }}" -srm_artifact_manager_address: "http://artefact-manager.artefact-manager.svc.cluster.local:8000" +srm_artifact_manager_address: "http://artefact-manager.{{ oop_artefact_manager_namespace | default('artefact-manager') }}.svc.cluster.local:8000" srm_kubernetes_master_ip: "kubernetes.default.svc.cluster.local" srm_kubernetes_master_port: "443" srm_kubernetes_username: admin diff --git a/ansible/roles/zot/defaults/main.yml b/ansible/roles/zot/defaults/main.yml index 145027ffc0a59d2ac38d2961a43cd26b402abfad..da98c96d2c779009059af36ffcaa4da68bd3764d 100644 --- a/ansible/roles/zot/defaults/main.yml +++ b/ansible/roles/zot/defaults/main.yml @@ -17,7 +17,7 @@ zot_helm_repo_name: project-zot zot_helm_repo_url: http://zotregistry.dev/helm-charts zot_chart: project-zot/zot zot_release_name: zot -zot_namespace: zot +zot_namespace: "{{ oop_zot_namespace | default('zot') }}" zot_install_timeout: 300 # ========================================== @@ -26,5 +26,13 @@ zot_install_timeout: 300 zot_service_type: NodePort zot_http_nodeport: 30050 zot_persistence_enabled: true -zot_storage_class: "standard" # defaults to standard/local-path in kind +zot_persistence_access_mode: ReadWriteOnce +zot_storage_class: "standard" # defaults to standard/local-path on clusters with dynamic provisioning zot_persistence_size: 10Gi +zot_persistence_host_path: /mnt/data/zot +zot_persistence_create_pv: true +zot_persistence_use_default_storage_class: true +zot_persistence_node_name: "" +zot_persistence_auto_configure: true +zot_pvc_create: true +zot_existing_pvc_name: "{{ zot_release_name }}-pvc-{{ zot_release_name }}-0" diff --git a/ansible/roles/zot/tasks/install.yml b/ansible/roles/zot/tasks/install.yml index 422f34fdea7367b76d9dd2cb818d44a699c2f6a3..6310bae995ae0ed5271aec2fb830d9cfbd72ee01 100644 --- a/ansible/roles/zot/tasks/install.yml +++ b/ansible/roles/zot/tasks/install.yml @@ -25,6 +25,245 @@ name: "{{ zot_namespace }}" kubeconfig: "{{ zot_kubeconfig }}" +- name: Get available storage classes + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + kubeconfig: "{{ zot_kubeconfig }}" + register: zot_storage_classes + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + +- name: Detect dynamic storage classes + ansible.builtin.set_fact: + zot_dynamic_storage_classes: >- + {{ (zot_storage_classes.resources | default([])) + | rejectattr('provisioner', 'equalto', 'kubernetes.io/no-provisioner') + | map(attribute='metadata.name') + | list }} + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + +- name: Detect local storage classes + ansible.builtin.set_fact: + zot_local_storage_classes: >- + {{ (zot_storage_classes.resources | default([])) + | selectattr('provisioner', 'equalto', 'kubernetes.io/no-provisioner') + | map(attribute='metadata.name') + | list }} + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + +- name: Detect default storage class + ansible.builtin.shell: | + kubectl get storageclass -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' | head -n 1 + args: + executable: /bin/bash + register: zot_default_storage_class_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ zot_kubeconfig }}" + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + +- name: Set default storage class fact + ansible.builtin.set_fact: + zot_default_storage_class: "{{ zot_default_storage_class_result.stdout | trim }}" + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + +- name: Select default dynamic storage class + ansible.builtin.set_fact: + zot_persistence_use_default_storage_class: true + zot_pvc_create: true + zot_storage_class: "{{ zot_default_storage_class }}" + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + - zot_dynamic_storage_classes | length > 0 + - zot_default_storage_class | length > 0 + +- name: Select non-default dynamic storage class + ansible.builtin.set_fact: + zot_persistence_use_default_storage_class: false + zot_pvc_create: true + zot_storage_class: "{{ zot_dynamic_storage_classes[0] }}" + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + - zot_dynamic_storage_classes | length > 0 + - zot_default_storage_class | length == 0 + +- name: Get schedulable worker node name + ansible.builtin.command: > + kubectl get nodes + -l '!node-role.kubernetes.io/control-plane' + --field-selector spec.unschedulable!=true + -o jsonpath='{.items[0].metadata.name}' + register: zot_worker_node_name_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ zot_kubeconfig }}" + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + - zot_dynamic_storage_classes | length == 0 + +- name: Get schedulable node name fallback + ansible.builtin.command: > + kubectl get nodes + --field-selector spec.unschedulable!=true + -o jsonpath='{.items[0].metadata.name}' + register: zot_node_name_result + changed_when: false + failed_when: false + environment: + KUBECONFIG: "{{ zot_kubeconfig }}" + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + - zot_dynamic_storage_classes | length == 0 + - zot_worker_node_name_result.stdout | default('') | length == 0 + +- name: Select node for local storage + ansible.builtin.set_fact: + zot_persistence_node_name: >- + {{ (zot_worker_node_name_result.stdout | default('') | length > 0) + | ternary(zot_worker_node_name_result.stdout, zot_node_name_result.stdout | default('')) }} + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + - zot_dynamic_storage_classes | length == 0 + +- name: Select local storage class + ansible.builtin.set_fact: + zot_persistence_use_default_storage_class: false + zot_persistence_create_pv: true + zot_pvc_create: false + zot_storage_class: >- + {{ (zot_local_storage_classes | length > 0) | ternary(zot_local_storage_classes[0], 'local-storage') }} + when: + - zot_persistence_enabled + - zot_persistence_auto_configure + - zot_dynamic_storage_classes | length == 0 + - zot_persistence_node_name | length > 0 + +- name: Get existing Zot PV + kubernetes.core.k8s_info: + api_version: v1 + kind: PersistentVolume + name: zot-pv + kubeconfig: "{{ zot_kubeconfig }}" + register: zot_pv_info + failed_when: false + when: + - zot_persistence_enabled + - zot_persistence_create_pv + - not zot_pvc_create + +- name: Set desired Zot PVC storage class + ansible.builtin.set_fact: + zot_pvc_storage_class_desired: >- + {{ (zot_persistence_use_default_storage_class | bool) | ternary('', zot_storage_class) }} + when: zot_persistence_enabled + +- name: Get existing Zot PVC + kubernetes.core.k8s_info: + api_version: v1 + kind: PersistentVolumeClaim + namespace: "{{ zot_namespace }}" + name: "{{ zot_existing_pvc_name }}" + kubeconfig: "{{ zot_kubeconfig }}" + register: zot_pvc_info + failed_when: false + when: zot_persistence_enabled + +- name: Fail when existing Zot PVC conflicts with desired storage config + ansible.builtin.fail: + msg: >- + Existing Zot PVC conflicts with desired storage configuration. + Current storageClass={{ zot_pvc_info.resources[0].spec.storageClassName | default('') | quote }}. + Desired storageClass={{ zot_pvc_storage_class_desired | quote }}. + Run the graceful undeploy/cleanup flow before redeploying. + when: zot_persistence_enabled + and zot_pvc_info.resources is defined + and zot_pvc_info.resources | length > 0 + and (zot_pvc_info.resources[0].spec.storageClassName | default('')) != zot_pvc_storage_class_desired + +- name: Delete released Zot PV + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: PersistentVolume + name: zot-pv + kubeconfig: "{{ zot_kubeconfig }}" + when: + - zot_persistence_enabled + - zot_persistence_create_pv + - not zot_pvc_create + - zot_pv_info.resources is defined + - zot_pv_info.resources | length > 0 + - zot_pv_info.resources[0].status.phase in ['Released', 'Failed'] + +- name: Create Zot PersistentVolume + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: PersistentVolume + metadata: + name: zot-pv + spec: + capacity: + storage: "{{ zot_persistence_size }}" + accessModes: + - "{{ zot_persistence_access_mode }}" + storageClassName: "{{ zot_storage_class }}" + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - "{{ zot_persistence_node_name }}" + hostPath: + path: "{{ zot_persistence_host_path }}" + kubeconfig: "{{ zot_kubeconfig }}" + when: + - zot_persistence_enabled + - zot_persistence_create_pv + - not zot_pvc_create + - zot_persistence_node_name | length > 0 + +- name: Create Zot PersistentVolumeClaim + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: "{{ zot_existing_pvc_name }}" + namespace: "{{ zot_namespace }}" + spec: + accessModes: + - "{{ zot_persistence_access_mode }}" + resources: + requests: + storage: "{{ zot_persistence_size }}" + storageClassName: "{{ zot_storage_class }}" + kubeconfig: "{{ zot_kubeconfig }}" + when: + - zot_persistence_enabled + - not zot_pvc_create + - name: Create Zot values file from template ansible.builtin.template: src: zot-values.yaml.j2 diff --git a/ansible/roles/zot/tasks/undeploy.yml b/ansible/roles/zot/tasks/undeploy.yml index 44dd2fd3ae631584defd12942f5ce3178d3c5f78..495e3c77cd5e7381b633489dec2d7df0951c7c9b 100644 --- a/ansible/roles/zot/tasks/undeploy.yml +++ b/ansible/roles/zot/tasks/undeploy.yml @@ -26,6 +26,44 @@ msg: "✓ Zot uninstalled successfully" when: zot_uninstall is changed +- name: Get Zot PVC info + kubernetes.core.k8s_info: + api_version: v1 + kind: PersistentVolumeClaim + namespace: "{{ zot_namespace }}" + name: "{{ zot_existing_pvc_name }}" + kubeconfig: "{{ zot_kubeconfig }}" + register: zot_pvc_info + failed_when: false + +- name: Remove finalizers from Zot PVC during cleanup + ansible.builtin.command: > + kubectl -n {{ zot_namespace }} patch pvc {{ zot_existing_pvc_name }} + --type=json + -p=[{"op":"remove","path":"/metadata/finalizers"}] + changed_when: true + failed_when: false + environment: + KUBECONFIG: "{{ zot_kubeconfig }}" + when: + - zot_pvc_info.resources is defined + - zot_pvc_info.resources | length > 0 + +- name: Delete Zot PVC during cleanup + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: PersistentVolumeClaim + namespace: "{{ zot_namespace }}" + name: "{{ zot_existing_pvc_name }}" + kubeconfig: "{{ zot_kubeconfig }}" + wait: yes + wait_timeout: 60 + failed_when: false + when: + - zot_pvc_info.resources is defined + - zot_pvc_info.resources | length > 0 + # ========================================== # Remove Namespace and PVCs # ========================================== diff --git a/ansible/roles/zot/templates/zot-values.yaml.j2 b/ansible/roles/zot/templates/zot-values.yaml.j2 index 534a641dbb149bc7c185900f109e7d7b5e953d1a..640e61519c2529ee68401d8ee6e6d25716e90f11 100644 --- a/ansible/roles/zot/templates/zot-values.yaml.j2 +++ b/ansible/roles/zot/templates/zot-values.yaml.j2 @@ -17,9 +17,14 @@ service: persistence: {{ zot_persistence_enabled | lower }} pvc: - create: true + create: {{ zot_pvc_create | lower }} + name: {{ 'null' if zot_pvc_create else (zot_existing_pvc_name | to_json) }} + accessModes: ["{{ zot_persistence_access_mode }}"] storage: {{ zot_persistence_size }} - storageClassName: {{ zot_storage_class }} + storageClassName: {{ 'null' if zot_persistence_use_default_storage_class else (zot_storage_class | to_json) }} + +strategy: + type: {% if zot_persistence_enabled and not zot_pvc_create %}Recreate{% else %}RollingUpdate{% endif %} mountConfig: true configFiles: diff --git a/mkdocs.yml b/mkdocs.yml index 6308ca467a88b10ed7a8007cb31d0a7bd6c629f4..cd4aee71c05f6e5fdb3a4eaa92c916af98f28dce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: Operator Platform Automation -site_url: https://labs.etsi.org/rep/oop/code/integration +site_url: https://op-automation-b0b7e6.pages.i2cat.net site_dir: public -repo_url: https://labs.etsi.org/rep/oop/code/integration -repo_name: oop/code/integration +repo_url: https://gitlab.i2cat.net/areas/software-networks/operator-platform/oop/op-automation +repo_name: areas/software-networks/operator-platform/oop/op-automation copyright: Copyright © 2025 Software Networks theme: