Add OpenStack iSCSI Multipath Playbooks and Jobs

Change-Id: Id29b17c9ef799bd5ae0560eaedfada6dd55a3b3d
diff --git a/playbooks/setup-openstack-client/post.yaml b/playbooks/setup-openstack-client/post.yaml
new file mode 100644
index 0000000..d2525ac
--- /dev/null
+++ b/playbooks/setup-openstack-client/post.yaml
@@ -0,0 +1,8 @@
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+  - name: Deprovision OpenStack Credentials
+    ansible.builtin.file:
+      path: "{{ os_creds_dir }}"
+      state: absent
diff --git a/playbooks/setup-openstack-client/pre.yaml b/playbooks/setup-openstack-client/pre.yaml
new file mode 100644
index 0000000..2534c6e
--- /dev/null
+++ b/playbooks/setup-openstack-client/pre.yaml
@@ -0,0 +1,48 @@
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    # TODO: To be removed once DNS starts working again.
+    - name: Provision systemd-resovled Configuration
+      no_log: true
+      become: true
+      ansible.builtin.copy:
+        content: "{{ RESOLVED_DATA['v'] }}"
+        dest: /etc/systemd/resolved.conf
+
+    - name: Restart systemd-resolved
+      become: true
+      ansible.builtin.service:
+        name: systemd-resolved
+        state: restarted
+
+    - name: Ensure OpenStack Credentials Directory Exists
+      ansible.builtin.file:
+        path: "{{ os_creds_dir }}"
+        state: directory
+        mode: '0700'
+        recurse: true
+
+    - name: Provision OpenStack Credentials
+      no_log: true
+      ansible.builtin.copy:
+        content: "{{ OPENSTACK_DATA['v'] }}"
+        dest: "{{ os_creds_path }}"
+        mode: '0600'
+
+    - name: Install python3-pip
+      become: true
+      ansible.builtin.apt:
+        name: python3-pip
+        update_cache: yes
+
+    - name: Install virtualenv
+      become: true
+      ansible.builtin.pip:
+        name: virtualenv
+        executable: pip3
+
+    - name: Create an OpenStack CLI Venv
+      ansible.builtin.pip:
+        name: python-openstackclient
+        virtualenv: "{{ os_venv }}"
diff --git a/playbooks/setup-openstack-client/vars.yaml b/playbooks/setup-openstack-client/vars.yaml
new file mode 100644
index 0000000..3b859dd
--- /dev/null
+++ b/playbooks/setup-openstack-client/vars.yaml
@@ -0,0 +1,2 @@
+os_creds_dir: "{{ ansible_env.HOME }}/.config/openstack"
+os_creds_path: "{{ os_creds_dir }}/clouds.yaml"
diff --git a/playbooks/setup-openstack-iscsi-multipath-storpool/post.yaml b/playbooks/setup-openstack-iscsi-multipath-storpool/post.yaml
new file mode 100644
index 0000000..e0164d7
--- /dev/null
+++ b/playbooks/setup-openstack-iscsi-multipath-storpool/post.yaml
@@ -0,0 +1,51 @@
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Remove the StorPool Ports from the StorPool Node
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - server
+          - remove
+          - port
+          - "{{ storpool_node }}"
+          - "{{ item }}"
+      loop:
+        - "{{ port_sp_api }}"
+        - "{{ port_sp0 }}"
+        - "{{ port_sp1 }}"
+
+    - name: Delete the StorPool Ports
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "delete", "{{ item }}" ]
+      loop:
+        - "{{ port_sp_api }}"
+        - "{{ port_sp0 }}"
+        - "{{ port_sp1 }}"
+
+    - name: Delete the StorPool Networks
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "network", "delete", "{{ item }}" ]
+      loop:
+        - "{{ network_sp_api }}"
+        - "{{ network_sp0 }}"
+        - "{{ network_sp1 }}"
+
+    - name: Remove the StorPool Volume to the StorPool Node
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - server
+          - remove
+          - volume
+          - "{{ storpool_node }}"
+          - "{{ storpool_volume }}"
+
+    - name: Delete the StorPool Volume
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "volume", "delete", "{{ storpool_volume }}" ]
diff --git a/playbooks/setup-openstack-iscsi-multipath-storpool/pre.yaml b/playbooks/setup-openstack-iscsi-multipath-storpool/pre.yaml
new file mode 100644
index 0000000..8034c12
--- /dev/null
+++ b/playbooks/setup-openstack-iscsi-multipath-storpool/pre.yaml
@@ -0,0 +1,380 @@
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Create the StorPool Networks
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - network
+          - create
+          - "{{ item }}"
+      loop:
+        - "{{ network_sp_api }}"
+        - "{{ network_sp0 }}"
+        - "{{ network_sp1 }}"
+
+    - name: Create the StorPool Subnets
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - subnet
+          - create
+          - --subnet-range
+          - "{{ item.ip_range }}"
+          - --network
+          - "{{ item.network }}"
+          - "{{ item.subnet }}"
+      loop:
+        - { ip_range: "{{ network_ip_sp_api }}", network: "{{ network_sp_api }}", subnet: "{{ subnet_sp_api }}" }
+        - { ip_range: "{{ network_ip_sp0 }}", network: "{{ network_sp0 }}", subnet: "{{ subnet_sp0 }}" }
+        - { ip_range: "{{ network_ip_sp1 }}", network: "{{ network_sp1 }}", subnet: "{{ subnet_sp1 }}" }
+
+    - name: Create the StorPool Ports
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - port
+          - create
+          - --network
+          - "{{ item.network }}"
+          - --fixed-ip
+          - "subnet={{ item.subnet }},ip-address={{ item.ip }}"
+          - "{{ item.port }}"
+      loop:
+        - { network: "{{ network_sp_api }}", subnet: "{{ subnet_sp_api }}", ip: "{{ ip_sp_api }}", port: "{{ port_sp_api }}" }
+        - { network: "{{ network_sp0 }}", subnet: "{{ subnet_sp0 }}", ip: "{{ ip_sp0 }}", port: "{{ port_sp0 }}" }
+        - { network: "{{ network_sp1 }}", subnet: "{{ subnet_sp1 }}", ip: "{{ ip_sp1 }}", port: "{{ port_sp1 }}" }
+
+    - name: Attach the StorPool Ports to the StorPool Node
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - server
+          - add
+          - port
+          - "{{ storpool_node }}"
+          - "{{ item }}"
+      loop:
+        - "{{ port_sp_api }}"
+        - "{{ port_sp0 }}"
+        - "{{ port_sp1 }}"
+
+    - name: Get Information About port_sp_api
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_sp_api }}" ]
+      register: port_sp_api_info
+
+    - name: Get MAC Address of port_sp_api
+      ansible.builtin.set_fact:
+        port_sp_api_mac: "{{ (port_sp_api_info.stdout | from_json).mac_address }}"
+
+    - name: Get Information About port_sp0
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_sp0 }}" ]
+      register: port_sp0_info
+
+    - name: Get MAC Address of port_sp0
+      ansible.builtin.set_fact:
+        port_sp0_mac: "{{ (port_sp0_info.stdout | from_json).mac_address }}"
+
+    - name: Get Information About port_sp1
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_sp1 }}" ]
+      register: port_sp1_info
+
+    - name: Get MAC Address of port_sp1
+      ansible.builtin.set_fact:
+        port_sp1_mac: "{{ (port_sp1_info.stdout | from_json).mac_address }}"
+
+    - name: Create the StorPool Volume
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - volume
+          - create
+          - --size
+          - "230"
+          - "{{ storpool_volume }}"
+
+    - name: Attach the StorPool Volume to the StorPool Node
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - server
+          - add
+          - volume
+          - "{{ storpool_node }}"
+          - "{{ storpool_volume }}"
+
+- hosts: localhost
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Create a Temporary Directory
+      ansible.builtin.tempfile:
+        state: directory
+        prefix: multipath-iscsi-storpool.
+      register: tmpdir
+
+    - set_fact:
+        tmpdir: "{{ tmpdir.path }}"
+
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Download Required Information from Previous Stage to Ansible Controller
+      ansible.builtin.fetch:
+        src: "~/ansivars.yaml"
+        dest: "{{ hostvars.localhost.tmpdir }}"
+
+    - name: Load Required Information from Previous Stage
+      ansible.builtin.include_vars:
+        file: "{{ hostvars.localhost.tmpdir }}/undercloud-client/home/ubuntu/ansivars.yaml"
+
+- hosts: lab-sp-a1
+  vars_files:
+    - vars.yaml
+  tasks:
+    - set_fact:
+        port_sp_api_mac: "{{ hostvars['undercloud-client']['port_sp_api_mac'] }}"
+        sp_sp0_mac: "{{ hostvars['undercloud-client']['port_sp0_mac'] }}"
+        sp_sp1_mac: "{{ hostvars['undercloud-client']['port_sp1_mac'] }}"
+        sp_iscsi0_mac: "{{ hostvars['undercloud-client']['port_node_1_iscsi0_mac'] }}"
+        sp_iscsi1_mac: "{{ hostvars['undercloud-client']['port_node_1_iscsi1_mac'] }}"
+        ip_node_1_iscsi0: "{{ hostvars['undercloud-client']['ip_node_1_iscsi0'] }}/24"
+        ip_node_1_iscsi1: "{{ hostvars['undercloud-client']['ip_node_1_iscsi1'] }}/24"
+        ip_sp_api: "{{ ip_sp_api }}/24"
+        ip_sp0: "{{ ip_sp0 }}/24"
+        ip_sp1: "{{ ip_sp1 }}/24"
+
+    - set_fact:
+        storpool_netplan: "{{ hostvars['localhost'].STORPOOL_NETPLAN.v }}"
+      no_log: true
+
+    - name: Provision the Netplan Template
+      no_log: true
+      become: true
+      ansible.builtin.copy:
+        content: "{{ storpool_netplan }}"
+        dest: /etc/netplan/60-storpool.yaml
+
+    - name: Generate Netplan
+      become: true
+      ansible.builtin.command:
+        argv: ["netplan", "generate"]
+
+    - name: Restart Neplan
+      become: true
+      ansible.builtin.command:
+        argv: ["netplan", "apply"]
+
+    - name: Create Service to Apply Interface Configuration on Boot
+      no_log: true
+      become: true
+      ansible.builtin.copy:
+        content: "{{ hostvars['localhost'].STORPOOL_NETPLAN_SERVICE.v }}"
+        dest: /etc/systemd/system/netplan-fix.service
+
+    - name: Enable Service to Apply Interface Configuration on Boot
+      no_log: true
+      become: true
+      ansible.builtin.command:
+        argv: [ "systemctl", "enable", "netplan-fix" ]
+
+    - name: Provision StorPool Deployment Configuration
+      no_log: true
+      ansible.builtin.lineinfile:
+        path: /home/ubuntu/.ssh/authorized_keys
+        line: "{{ STORPOOL_DEPLOY_KEY_PUB['v'] }}"
+        insertafter: EOF
+
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Ensure StorPool Deployment Configuration Directory Exists
+      ansible.builtin.file:
+        path: /home/ubuntu/.ssh
+        state: directory
+        mode: '0700'
+        recurse: true
+
+    - name: Provision StorPool Deployment Configuration 1
+      no_log: true
+      ansible.builtin.copy:
+        content: "{{ STORPOOL_DEPLOY_KEY['v'] }}"
+        dest: /home/ubuntu/.ssh/id_rsa
+        mode: '0400'
+
+    - name: Provision StorPool Deployment Configuration 2
+      no_log: true
+      ansible.builtin.copy:
+        content: "{{ STORPOOL_DEPLOY_KEY_PUB['v'] }}"
+        dest: /home/ubuntu/.ssh/id_rsa.pub
+
+    - name: Provision StorPool Deployment Configuration 3
+      no_log: true
+      ansible.builtin.lineinfile:
+        line: "{{ item }}"
+        path: /home/ubuntu/.ssh/config
+        insertafter: EOF
+        create: yes
+      loop:
+        - "Host {{ storpool_node_ip }}"
+        - "    StrictHostKeyChecking no"
+
+    - name: Install python3-pip
+      become: true
+      ansible.builtin.apt:
+        name: python3-pip
+        update_cache: yes
+
+    - name: Install ansible
+      become: true
+      ansible.builtin.pip:
+        name: ansible
+        executable: pip3
+
+    - name: Checkout StorPool Ansible
+      ansible.builtin.git:
+        repo: 'https://github.com/storpool/ansible'
+        dest: /home/ubuntu/ansible
+
+    - name: Download Required Ansible Roles
+      ansible.builtin.command:
+        argv: ["ansible-galaxy", "install", "-r", "roles/requirements.yml"]
+      args:
+        chdir: "/home/ubuntu/ansible"
+
+    - name: Provision StorPool Ansible Inventory
+      no_log: true
+      ansible.builtin.copy:
+        content: "{{ STORPOOL_INVENTORY['v'] }}"
+        dest: /home/ubuntu/ansible/inventory.yaml
+
+    - name: Configure the StorPool Ansible Inventory
+      ansible.builtin.replace:
+        path: /home/ubuntu/ansible/inventory.yaml
+        regexp: 'STORPOOL_NODE'
+        replace: "{{ storpool_node_ip }}"
+
+    - set_fact:
+        storpool_node_hostname: "{{ hostvars['lab-sp-a1']['ansible_hostname'] }}"
+
+    - set_fact:
+        storpool_conf: "{{ hostvars['localhost'].STORPOOL_CONF.v }}"
+      no_log: true
+
+    - name: Provision the StorPool Template
+      no_log: true
+      ansible.builtin.copy:
+        content: "{{ storpool_conf }}"
+        dest: /home/ubuntu/ansible/storpool.conf
+
+    - name: Deploy StorPool
+      ansible.builtin.command:
+        argv: ["ansible-playbook", "playbook.yml", "-i", "inventory.yaml", "--skip-tags", "perform-tests"]
+      args:
+        chdir: "/home/ubuntu/ansible"
+
+- hosts: lab-sp-a1
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Wait for the StorPool Cluster to Come Up
+      become: true
+      ansible.builtin.command:
+        argv: ["storpool", "-j", "service", "list"]
+      register: r
+      until: r.rc == 0 and (r.stdout | from_json).data.clusterStatus == "running"
+      delay: 1
+      retries: 120
+
+    - name: Configure StorPool for Multipath iSCSI
+      become: true
+      ansible.builtin.command:
+        argv: "{{ item }}"
+      loop:
+        - [ "storpool", "iscsi", "config", "setBaseName", "iqn.2023-03.storpool.com" ]
+        - [ "storpool", "iscsi", "config", "portalGroup", "pg", "create" ]
+        - [ "storpool", "iscsi", "config", "portalGroup", "pg", "addNet", "192.168.40.100/24" ]
+        - [ "storpool", "iscsi", "config", "portalGroup", "pg", "addNet", "192.168.50.100/24" ]
+        - [ "storpool", "iscsi", "config", "portal", "create", "portalGroup", "pg", "controller", "1", "address", "192.168.40.101" ]
+        - [ "storpool", "iscsi", "config", "portal", "create", "portalGroup", "pg", "controller", "1", "address", "192.168.50.101" ]
+
+- hosts: controller
+  vars_files:
+    - vars.yaml
+  tasks:
+    - set_fact:
+        ip_sp_api: "{{ ip_sp_api }}/24"
+        mac_node_2_iscsi0: "{{ hostvars['undercloud-client']['port_node_2_iscsi0_mac'] }}"
+        mac_node_2_iscsi1: "{{ hostvars['undercloud-client']['port_node_2_iscsi1_mac'] }}"
+        ip_node_2_iscsi0: "{{ hostvars['undercloud-client']['ip_node_2_iscsi0'] }}/24"
+        ip_node_2_iscsi1: "{{ hostvars['undercloud-client']['ip_node_2_iscsi1'] }}/24"
+
+    - set_fact:
+        systemd_networkd_iscsi: "{{ hostvars['localhost'].STORPOOL_NETPLAN.v }}"
+      no_log: true
+
+    - set_fact:
+        iscsi_mac: mac_node_2_iscsi0
+        iscsi_ip: ip_node_2_iscsi0
+
+    - name: Provision the Netplan Template for iSCSI Network 1
+      no_log: true
+      become: true
+      ansible.builtin.copy:
+        content: "{{ systemd_networkd_iscsi }}"
+        dest: /etc/systemd/network/60-storpool-iscsi-1.network
+
+    - set_fact:
+        iscsi_ip: ip_node_2_iscsi1
+        iscsi_mac: mac_node_2_iscsi1
+
+    - name: Provision the Netplan Template for iSCSI Network 2
+      no_log: true
+      become: true
+      ansible.builtin.copy:
+        content: "{{ systemd_networkd_iscsi }}"
+        dest: /etc/systemd/network/61-storpool-iscsi-2.network
+
+    - set_fact:
+        storpool_conf: "{{ hostvars['localhost'].STORPOOL_CONF_ISCSI_NODE.v }}"
+      no_log: true
+
+    - name: Provision the StorPool Template
+      no_log: true
+      ansible.builtin.copy:
+        content: "{{ storpool_conf }}"
+        dest: /etc/storpool.conf
+
+    - name: Restart systemd-networkd to Apply the Network Configuration
+      ansible.builtin.command:
+        argv: [ "systemctl", "restart", "systemd-networkd" ]
+
+    - name: Wait for the Portal Group IP 1 to Become Available
+      ansible.builtin.command:
+        argv: [ "ping", "-c1", "192.168.40.101" ]
+      register: r
+      until: r.rc == 0
+
+    - name: Wait for the Portal Group IP 2 to Become Available
+      ansible.builtin.command:
+        argv: [ "ping", "-c1", "192.168.50.101" ]
+      register: r
+      until: r.rc == 0
diff --git a/playbooks/setup-openstack-iscsi-multipath-storpool/vars.yaml b/playbooks/setup-openstack-iscsi-multipath-storpool/vars.yaml
new file mode 100644
index 0000000..86c46c0
--- /dev/null
+++ b/playbooks/setup-openstack-iscsi-multipath-storpool/vars.yaml
@@ -0,0 +1,25 @@
+storpool_node: "{{ hostvars['lab-sp-a1']['nodepool']['external_id'] }}"
+storpool_node_ip: "{{ hostvars['lab-sp-a1']['nodepool']['interface_ip'] }}"
+undercloud_client_node: "{{ hostvars['undercloud-client']['nodepool']['external_id'] }}"
+
+storpool_volume: "{{ undercloud_client_node }}-storpool-volume"
+
+network_ip_sp_api: "192.168.10.0/24"
+network_ip_sp0: "192.168.20.0/24"
+network_ip_sp1: "192.168.30.0/24"
+
+ip_sp_api: "192.168.10.3"
+ip_sp0: "192.168.20.3"
+ip_sp1: "192.168.30.3"
+
+network_sp_api: "{{ undercloud_client_node }}-network-sp-api"
+network_sp0: "{{ undercloud_client_node }}-network-sp0"
+network_sp1: "{{ undercloud_client_node }}-network-sp1"
+
+subnet_sp_api: "{{ undercloud_client_node }}-subnet-sp-api"
+subnet_sp0: "{{ undercloud_client_node }}-subnet-sp0"
+subnet_sp1: "{{ undercloud_client_node }}-subnet-sp1"
+
+port_sp_api: "{{ undercloud_client_node }}-port-sp-api"
+port_sp0: "{{ undercloud_client_node }}-port-sp0"
+port_sp1: "{{ undercloud_client_node }}-port-sp1"
diff --git a/playbooks/setup-openstack-iscsi-multipath/post.yaml b/playbooks/setup-openstack-iscsi-multipath/post.yaml
new file mode 100644
index 0000000..e11f10d
--- /dev/null
+++ b/playbooks/setup-openstack-iscsi-multipath/post.yaml
@@ -0,0 +1,33 @@
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Remove the Multipath iSCSI Ports from the StorPool Node
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "server", "remove", "port", "{{ storpool_node }}", "{{ item }}" ]
+      loop:
+        - "{{ port_node_1_iscsi0 }}"
+        - "{{ port_node_1_iscsi1 }}"
+
+    - name: Remove the Multipath iSCSI Ports from the OpenStack Node
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "server", "remove", "port", "{{ openstack_node }}", "{{ item }}" ]
+      loop:
+        - "{{ port_node_2_iscsi0 }}"
+        - "{{ port_node_2_iscsi1 }}"
+
+    - name: Delete the Multipath iSCSI Ports
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "delete", "{{ item }}" ]
+      loop :
+        - "{{ port_node_1_iscsi0 }}"
+        - "{{ port_node_1_iscsi1 }}"
+        - "{{ port_node_2_iscsi0 }}"
+        - "{{ port_node_2_iscsi1 }}"
+
+    - name: Delete Multipath iSCSI Networks
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "network", "delete", "{{ item }}" ]
+      loop:
+        - "{{ network_1 }}"
+        - "{{ network_2 }}"
diff --git a/playbooks/setup-openstack-iscsi-multipath/pre.yaml b/playbooks/setup-openstack-iscsi-multipath/pre.yaml
new file mode 100644
index 0000000..cb1c099
--- /dev/null
+++ b/playbooks/setup-openstack-iscsi-multipath/pre.yaml
@@ -0,0 +1,111 @@
+- hosts: undercloud-client
+  vars_files:
+    - vars.yaml
+  tasks:
+    - name: Create Multipath iSCSI Networks
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "network", "create", "{{ item }}" ]
+      loop:
+        - "{{ network_1 }}"
+        - "{{ network_2 }}"
+
+    - name: Create Multipath iSCSI Subnets
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - subnet
+          - create
+          - --subnet-range
+          - "{{ item.ip_range }}"
+          - --network
+          - "{{ item.network }}"
+          - "{{ item.subnet }}"
+      loop:
+        - { ip_range: "{{ network_ip_iscsi0 }}", network: "{{ network_1 }}", subnet: "{{ subnet_1 }}" }
+        - { ip_range: "{{ network_ip_iscsi1 }}", network: "{{ network_2 }}", subnet: "{{ subnet_2 }}" }
+
+    # TODO: Support port security
+    - name: Create the Multipath iSCSI Ports
+      ansible.builtin.command:
+        argv:
+          - "{{ os_venv }}/bin/openstack"
+          - --os-cloud
+          - openstack-testing
+          - port
+          - create
+          - --network
+          - "{{ item.network }}"
+          - --fixed-ip
+          - "subnet={{ item.subnet }},ip-address={{ item.ip }}"
+          - --disable-port-security
+          - "{{ item.port }}"
+      loop:
+        - { network: "{{ network_1 }}", subnet: "{{ subnet_1 }}", ip: "{{ ip_node_1_iscsi0 }}", port: "{{ port_node_1_iscsi0 }}" }
+        - { network: "{{ network_2 }}", subnet: "{{ subnet_2 }}", ip: "{{ ip_node_1_iscsi1 }}", port: "{{ port_node_1_iscsi1 }}" }
+        - { network: "{{ network_1 }}", subnet: "{{ subnet_1 }}", ip: "{{ ip_node_2_iscsi0 }}", port: "{{ port_node_2_iscsi0 }}" }
+        - { network: "{{ network_2 }}", subnet: "{{ subnet_2 }}", ip: "{{ ip_node_2_iscsi1 }}", port: "{{ port_node_2_iscsi1 }}" }
+
+    - name: Attach the Multipath iSCSI Ports to the StorPool Node
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "server", "add", "port", "{{ storpool_node }}", "{{ item }}" ]
+      loop:
+        - "{{ port_node_1_iscsi0 }}"
+        - "{{ port_node_1_iscsi1 }}"
+
+    - name: Attach the Multipath iSCSI Ports to the OpenStack Node
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "server", "add", "port", "{{ openstack_node }}", "{{ item }}" ]
+      loop:
+        - "{{ port_node_2_iscsi0 }}"
+        - "{{ port_node_2_iscsi1 }}"
+
+    - name: Get Information About port_node_1_iscsi0
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_node_1_iscsi0 }}" ]
+      register: port_node_1_iscsi0_info
+
+    - name: Get MAC Address of port_node_1_iscsi0
+      ansible.builtin.set_fact:
+        port_node_1_iscsi0_mac: "{{ (port_node_1_iscsi0_info.stdout | from_json).mac_address }}"
+
+    - name: Get Information About port_node_1_iscsi1
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_node_1_iscsi1 }}" ]
+      register: port_node_1_iscsi1_info
+
+    - name: Get MAC Address of port_node_1_iscsi1
+      ansible.builtin.set_fact:
+        port_node_1_iscsi1_mac: "{{ (port_node_1_iscsi1_info.stdout | from_json).mac_address }}"
+
+    - name: Get Information About port_node_2_iscsi0
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_node_2_iscsi0 }}" ]
+      register: port_node_2_iscsi0_info
+
+    - name: Get MAC Address of port_node_2_iscsi0
+      ansible.builtin.set_fact:
+        port_node_2_iscsi0_mac: "{{ (port_node_2_iscsi0_info.stdout | from_json).mac_address }}"
+
+    - name: Get Information About port_node_2_iscsi1
+      ansible.builtin.command:
+        argv: [ "{{ os_venv }}/bin/openstack", "--os-cloud", "openstack-testing", "port", "show", "--format", "json", "{{ port_node_2_iscsi1 }}" ]
+      register: port_node_2_iscsi1_info
+
+    - name: Get MAC Address of port_node_2_iscsi1
+      ansible.builtin.set_fact:
+        port_node_2_iscsi1_mac: "{{ (port_node_2_iscsi1_info.stdout | from_json).mac_address }}"
+
+    - name: Dump Required Information for Next Stage
+      ansible.builtin.copy:
+        content: |
+          port_node_1_iscsi0_mac: {{ port_node_1_iscsi0_mac }}
+          port_node_1_iscsi1_mac: {{ port_node_1_iscsi1_mac }}
+          ip_node_1_iscsi0: {{ ip_node_1_iscsi0 }}
+          ip_node_1_iscsi1: {{ ip_node_1_iscsi1 }}
+          port_node_2_iscsi0_mac: {{ port_node_2_iscsi0_mac }}
+          port_node_2_iscsi1_mac: {{ port_node_2_iscsi1_mac }}
+          ip_node_2_iscsi0: {{ ip_node_2_iscsi0 }}
+          ip_node_2_iscsi1: {{ ip_node_2_iscsi1 }}
+        dest: "~/ansivars.yaml"
diff --git a/playbooks/setup-openstack-iscsi-multipath/vars.yaml b/playbooks/setup-openstack-iscsi-multipath/vars.yaml
new file mode 100644
index 0000000..10cc748
--- /dev/null
+++ b/playbooks/setup-openstack-iscsi-multipath/vars.yaml
@@ -0,0 +1,24 @@
+undercloud_client_node: "{{ hostvars['undercloud-client']['nodepool']['external_id'] }}"
+storpool_node: "{{ hostvars['lab-sp-a1']['nodepool']['external_id'] }}"
+openstack_node: "{{ hostvars['controller']['nodepool']['external_id'] }}"
+
+network_ip_iscsi0: "192.168.40.0/24"
+network_ip_iscsi1: "192.168.50.0/24"
+
+ip_node_1_iscsi0: "192.168.40.3"
+ip_node_1_iscsi1: "192.168.50.3"
+
+ip_node_2_iscsi0: "192.168.40.4"
+ip_node_2_iscsi1: "192.168.50.4"
+
+network_1: "{{ undercloud_client_node }}-network-iscsi-1"
+network_2: "{{ undercloud_client_node }}-network-iscsi-2"
+
+subnet_1: "{{ undercloud_client_node }}-subnet-iscsi-1"
+subnet_2: "{{ undercloud_client_node }}-subnet-iscsi-2"
+
+port_node_1_iscsi0: "{{ undercloud_client_node }}-port-node-1-iscsi-0"
+port_node_1_iscsi1: "{{ undercloud_client_node }}-port-node-1-iscsi-1"
+
+port_node_2_iscsi0: "{{ undercloud_client_node }}-port-node-2-iscsi-0"
+port_node_2_iscsi1: "{{ undercloud_client_node }}-port-node-2-iscsi-1"