diff --git a/ansible/cluster_info.yml b/ansible/cluster_info.yml index b82228d..6e9b213 100644 --- a/ansible/cluster_info.yml +++ b/ansible/cluster_info.yml @@ -20,7 +20,7 @@ validate_certs: "{{ ontap_validate_certs }}" use_rest: always gather_subset: - - cluster/node_info + - cluster/nodes fields: - "version" register: cluster_result @@ -29,12 +29,8 @@ - name: Display cluster version ansible.builtin.debug: msg: >- - Cluster: {{ cluster_result.ontap_info['cluster/node_info'] - | dict2items | first - | json_query('value.cluster_name') | default('unknown') }} - — ONTAP {{ cluster_result.ontap_info['cluster/node_info'] - | dict2items | first - | json_query('value.version') | default('unknown') }} + Cluster node: {{ cluster_result.ontap_info['cluster/nodes']['records'][0].name | default('unknown') }} + — ONTAP {{ cluster_result.ontap_info['cluster/nodes']['records'][0].version.full | default('unknown') }} # -- Step 2: List nodes with serial numbers ------------------------- - name: Get node details @@ -46,7 +42,7 @@ validate_certs: "{{ ontap_validate_certs }}" use_rest: always gather_subset: - - cluster/node_info + - cluster/nodes fields: - "name" - "serial_number" @@ -55,7 +51,7 @@ - name: Display node list ansible.builtin.debug: - msg: "Node: {{ item.value.name }} — serial: {{ item.value.serial_number | default('N/A') }}" - loop: "{{ nodes_result.ontap_info['cluster/node_info'] | dict2items }}" + msg: "Node: {{ item.name }} — serial: {{ item.serial_number | default('N/A') }}" + loop: "{{ nodes_result.ontap_info['cluster/nodes']['records'] }}" loop_control: - label: "{{ item.value.name }}" + label: "{{ item.name }}" diff --git a/ansible/cluster_setup.yml b/ansible/cluster_setup.yml new file mode 100644 index 0000000..3fdc872 --- /dev/null +++ b/ansible/cluster_setup.yml @@ -0,0 +1,222 @@ +--- +# cluster_setup_basic.yml — Basic ONTAP cluster setup via REST API. +# +# Mirrors the cluster_setup_basic workflow: +# 1. Discover available nodes (retry 3x/30s) +# 2. Discover local node (has management_interfaces, retry 3x/30s) +# 3. Discover partner node (exclude local uuid, retry 3x/30s) +# 4. POST /api/cluster with discovered node info +# 5. Poll job until complete +# +# Usage: +# ansible-playbook -i inventory/hosts.yml cluster_setup_basic.yml +# +# Override variables: +# ansible-playbook -i inventory/hosts.yml cluster_setup_basic.yml \ +# -e cluster_name=my_cluster -e cluster_pass=MySecret123 \ +# -e partner_mgmt_ip=10.10.10.2 +# +- name: "Basic ONTAP Cluster Setup" + hosts: ontap + gather_facts: false + connection: local + module_defaults: + group/netapp.ontap.netapp_ontap: + hostname: "{{ ontap_hostname }}" + username: "{{ ontap_username }}" + password: "{{ ontap_password }}" + https: "{{ ontap_https }}" + validate_certs: "{{ ontap_validate_certs }}" + use_rest: always + + vars: + # -- Cluster configuration ------------------------------------------ + cluster_name: "" + cluster_pass: "" + cluster_mgmt_ip: "" + cluster_netmask: "" + cluster_gateway: "" + partner_mgmt_ip: "" + + # -- REST API defaults ---------------------------------------------- + api_url_base: "https://{{ ontap_hostname }}/api" + api_auth: "{{ ontap_username }}:{{ ontap_password }}" + node_query_fields: "name,model,state,ha,version,serial_number,membership,cluster_interfaces,management_interfaces,metrocluster,disaggregated,san_optimized" + + tasks: + # -- Pre-flight checks ---------------------------------------------- + - name: "Validate required variables" + ansible.builtin.assert: + that: + - cluster_name | length > 0 + - cluster_pass | length > 0 + - cluster_mgmt_ip | length > 0 + - cluster_netmask | length > 0 + - cluster_gateway | length > 0 + - partner_mgmt_ip | length > 0 + fail_msg: >- + Required variables: cluster_name, cluster_pass, cluster_mgmt_ip, + cluster_netmask, cluster_gateway, partner_mgmt_ip + no_log: false + + # -- Step 1: Discover available nodes (reusable pre-check) ---------- + - name: "Discover available nodes (retry 3x / 30s)" + ansible.builtin.uri: + url: "{{ api_url_base }}/cluster/nodes?fields={{ node_query_fields }}&membership=available&return_timeout=120" + method: GET + url_username: "{{ ontap_username }}" + url_password: "{{ ontap_password }}" + force_basic_auth: true + validate_certs: "{{ ontap_validate_certs }}" + headers: + Accept: "application/json" + status_code: 200 + timeout: 150 + register: discover_nodes + retries: 3 + delay: 30 + until: discover_nodes is succeeded and (discover_nodes.json.num_records | default(0)) > 0 + no_log: false + + - name: "Show discovered nodes" + ansible.builtin.debug: + msg: "Discovered {{ discover_nodes.json.num_records }} available node(s)" + no_log: false + + # -- Step 2: Discover local node (has management_interfaces) -------- + - name: "Discover local node (retry 3x / 30s)" + ansible.builtin.uri: + url: "{{ api_url_base }}/cluster/nodes?fields={{ node_query_fields }}&membership=available&management_interfaces=!null&return_timeout=120" + method: GET + url_username: "{{ ontap_username }}" + url_password: "{{ ontap_password }}" + force_basic_auth: true + validate_certs: "{{ ontap_validate_certs }}" + headers: + Accept: "application/json" + status_code: 200 + timeout: 150 + register: discover_local + retries: 3 + delay: 30 + until: discover_local is succeeded and (discover_local.json.num_records | default(0)) > 0 + no_log: false + + - name: "Show discovered local node" + ansible.builtin.debug: + msg: >- + Local node: {{ discover_local.json.records[0].name }} + (uuid: {{ discover_local.json.records[0].uuid }}, + cluster_if: {{ discover_local.json.records[0].cluster_interfaces[0].ip.address }}) + no_log: false + + # -- Step 3: Discover partner node (exclude local uuid) ------------- + - name: "Discover partner node (retry 3x / 30s)" + ansible.builtin.uri: + url: "{{ api_url_base }}/cluster/nodes?fields={{ node_query_fields }}&membership=available&uuid=!{{ discover_local.json.records[0].uuid }}&return_timeout=120" + method: GET + url_username: "{{ ontap_username }}" + url_password: "{{ ontap_password }}" + force_basic_auth: true + validate_certs: "{{ ontap_validate_certs }}" + headers: + Accept: "application/json" + status_code: 200 + timeout: 150 + register: discover_partner + retries: 3 + delay: 30 + until: discover_partner is succeeded and (discover_partner.json.num_records | default(0)) > 0 + no_log: false + + - name: "Show discovered partner node" + ansible.builtin.debug: + msg: >- + Partner node: {{ discover_partner.json.records[0].name }} + (uuid: {{ discover_partner.json.records[0].uuid }}, + cluster_if: {{ discover_partner.json.records[0].cluster_interfaces[0].ip.address }}) + no_log: false + + # -- Step 4: Create cluster (POST /api/cluster) --------------------- + - name: "Create cluster '{{ cluster_name }}'" + ansible.builtin.uri: + url: "{{ api_url_base }}/cluster?keep_precluster_config=true" + method: POST + url_username: "{{ ontap_username }}" + url_password: "{{ ontap_password }}" + force_basic_auth: true + validate_certs: "{{ ontap_validate_certs }}" + headers: + Accept: "application/json" + Content-Type: "application/json" + body_format: json + body: + name: "{{ cluster_name }}" + password: "{{ cluster_pass }}" + management_interface: + ip: + address: "{{ cluster_mgmt_ip }}" + netmask: "{{ cluster_netmask }}" + gateway: "{{ cluster_gateway }}" + nodes: + - name: "{{ cluster_name }}-01" + management_interface: + ip: + address: "{{ ontap_hostname }}" + cluster_interface: + ip: + address: "{{ discover_local.json.records[0].cluster_interfaces[0].ip.address }}" + - name: "{{ cluster_name }}-02" + management_interface: + ip: + address: "{{ partner_mgmt_ip }}" + cluster_interface: + ip: + address: "{{ discover_partner.json.records[0].cluster_interfaces[0].ip.address }}" + name_servers: {} + ntp_servers: {} + dns_domains: {} + configuration_backup: {} + status_code: 202 + timeout: 60 + register: create_cluster + no_log: false + + - name: "Show create-cluster job UUID" + ansible.builtin.debug: + msg: "Job UUID: {{ create_cluster.json.job.uuid }}" + no_log: false + + # -- Step 5: Poll job until complete -------------------------------- + - name: "Poll cluster-create job until complete" + ansible.builtin.uri: + url: "{{ api_url_base }}/cluster/jobs/{{ create_cluster.json.job.uuid }}?fields=code,description,end_time,error,_links,message,start_time,state,svm,uuid,arguments&return_timeout=120" + method: GET + url_username: "{{ ontap_username }}" + url_password: "{{ cluster_pass }}" + force_basic_auth: true + validate_certs: "{{ ontap_validate_certs }}" + headers: + Accept: "application/json" + status_code: 200 + timeout: 150 + register: job_status + retries: 30 + delay: 20 + until: job_status.json.state in ["success", "failure"] + no_log: false + + - name: "Fail if cluster-create job failed" + ansible.builtin.fail: + msg: "Cluster create job failed: {{ job_status.json.message | default(job_status.json.error | default('unknown error')) }}" + when: job_status.json.state == "failure" + no_log: false + + # -- Summary -------------------------------------------------------- + - name: "Print setup summary" + ansible.builtin.debug: + msg: >- + Cluster '{{ cluster_name }}' created successfully. + Cluster mgmt: {{ cluster_mgmt_ip }}/{{ cluster_netmask }} gw {{ cluster_gateway }}. + Node 1 (local): {{ ontap_hostname }} cluster-if {{ discover_local.json.records[0].cluster_interfaces[0].ip.address }}. + Node 2 (partner): {{ partner_mgmt_ip }} cluster-if {{ discover_partner.json.records[0].cluster_interfaces[0].ip.address }}.