[stable/zed] Add Portworx CSI support (#1441)

\n\nCloses #1440
diff --git a/doc/source/deploy/csi.rst b/doc/source/deploy/csi.rst
index 7fbb398..f56fb5b 100644
--- a/doc/source/deploy/csi.rst
+++ b/doc/source/deploy/csi.rst
@@ -6,6 +6,14 @@
 your Kubernetes cluster that Atmosphere runs on. You will need to follow the
 steps below to enable specific CSI drivers based on your storage requirements.
 
+.. admonition:: Storing secrets securely
+    :class: tip
+
+    When configuring CSI drivers, it is important to store sensitive
+    information securely. You can use Ansible Vault to encrypt your inventory
+    file and store it in a secure location. For more information on how to
+    use Ansible Vault, refer to the `Ansible documentation <https://docs.ansible.com/ansible/latest/user_guide/vault.html>`_.
+
 ********
 Ceph RBD
 ********
@@ -69,6 +77,30 @@
 documentation to help you determine the values to use.
 
 ********
+Portworx
+********
+
+If you are using a Pure Storage array for your block storage, you can use the
+Portworx CSI driver to integrate it with your Kubernetes cluster.  Portworx
+automatically enables a custom license when integrated with Pure Storage
+arrays (FA/FB edition).
+
+To configure the Portworx CSI driver, update your Ansible inventory as follows:
+
+.. code-block:: yaml
+
+    csi_driver: portworx
+    portworx_pure_flasharray_san_type: <FILL IN> # FC or ISCSI
+    portworx_pure_json:
+      FlaskBlades: []
+      FlashArrays:
+        - MgmtEndPoint: <FILL IN>
+          APIToken: <FILL IN>
+
+For more information about how the ``portworx_pure_json`` variable is used,
+you can refer to the `Pure Storage FlashArray and FlashBlade JSON file reference <https://docs.portworx.com/portworx-enterprise/reference/pure-reference/pure-json-reference>`_.
+
+********
 StorPool
 ********
 
diff --git a/playbooks/csi.yml b/playbooks/csi.yml
index a0590fe..0b86a74 100644
--- a/playbooks/csi.yml
+++ b/playbooks/csi.yml
@@ -12,7 +12,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-- hosts: controllers[0]
+- hosts: controllers
   gather_facts: false
   become: true
   roles:
diff --git a/roles/csi/meta/main.yml b/roles/csi/meta/main.yml
index 8c37342..c2dce15 100644
--- a/roles/csi/meta/main.yml
+++ b/roles/csi/meta/main.yml
@@ -36,6 +36,8 @@
     when: csi_driver == "rbd"
   - role: powerstore_csi
     when: csi_driver == "powerstore"
+  - role: portworx
+    when: csi_driver == "portworx"
   - role: storpool_csi
     when: csi_driver == "storpool"
   - role: ibm_block_csi_driver
diff --git a/roles/multipathd/handlers/main.yml b/roles/multipathd/handlers/main.yml
index 818a509..e94180b 100644
--- a/roles/multipathd/handlers/main.yml
+++ b/roles/multipathd/handlers/main.yml
@@ -16,3 +16,7 @@
   ansible.builtin.service:
     name: multipathd
     state: reloaded
+
+- name: Reconfigure "multipathd"
+  ansible.builtin.shell:
+    cmd: multipathd -k'reconfigure'
diff --git a/roles/multipathd/tasks/main.yml b/roles/multipathd/tasks/main.yml
index be14d2a..edfff2c 100644
--- a/roles/multipathd/tasks/main.yml
+++ b/roles/multipathd/tasks/main.yml
@@ -12,6 +12,18 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+- name: Add backports PPA
+  ansible.builtin.apt_repository:
+    repo: ppa:vexxhost/backports
+
+- name: Install the multipathd package
+  ansible.builtin.package:
+    name: multipath-tools
+    state: latest
+  notify:
+    - Reload "multipathd"
+    - Reconfigure "multipathd"
+
 - name: Install the configuration file
   ansible.builtin.template:
     src: multipath.conf.j2
@@ -21,3 +33,4 @@
     mode: "0644"
   notify:
     - Reload "multipathd"
+    - Reconfigure "multipathd"
diff --git a/roles/multipathd/templates/multipath.conf.j2 b/roles/multipathd/templates/multipath.conf.j2
index 6eb0c5f..adc44ac 100644
--- a/roles/multipathd/templates/multipath.conf.j2
+++ b/roles/multipathd/templates/multipath.conf.j2
@@ -2,4 +2,47 @@
 
 defaults {
     user_friendly_names {{ multipathd_user_friendly_names | bool | ternary('yes', 'no') }}
+    find_multipaths yes
+}
+
+devices {
+    device {
+        vendor                      "NVME"
+        product                     "Pure Storage FlashArray"
+        path_selector               "queue-length 0"
+        path_grouping_policy        group_by_prio
+        prio                        ana
+        failback                    immediate
+        fast_io_fail_tmo            10
+        user_friendly_names         no
+        no_path_retry               0
+        features                    0
+        dev_loss_tmo                60
+        find_multipaths             yes
+    }
+    device {
+        vendor                   "PURE"
+        product                  "FlashArray"
+        path_selector            "service-time 0"
+        hardware_handler         "1 alua"
+        path_grouping_policy     group_by_prio
+        prio                     alua
+        failback                 immediate
+        path_checker             tur
+        fast_io_fail_tmo         10
+        user_friendly_names      no
+        no_path_retry            0
+        features                 0
+        dev_loss_tmo             600
+        find_multipaths          yes
+    }
+}
+
+blacklist {
+      devnode "^pxd[0-9]*"
+      devnode "^pxd*"
+      device {
+        vendor "VMware"
+        product "Virtual disk"
+      }
 }
diff --git a/roles/portworx/defaults/main.yml b/roles/portworx/defaults/main.yml
new file mode 100644
index 0000000..27d09bd
--- /dev/null
+++ b/roles/portworx/defaults/main.yml
@@ -0,0 +1,16 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# portworx_pure_flasharray_san_type:
+# portworx_pure_json:
diff --git a/roles/portworx/meta/main.yml b/roles/portworx/meta/main.yml
new file mode 100644
index 0000000..e35bb38
--- /dev/null
+++ b/roles/portworx/meta/main.yml
@@ -0,0 +1,33 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+galaxy_info:
+  author: VEXXHOST, Inc.
+  description: Ansible role for Portworx
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  standalone: false
+  platforms:
+    - name: EL
+      versions:
+        - "8"
+        - "9"
+    - name: Ubuntu
+      versions:
+        - focal
+        - jammy
+
+dependencies:
+  - role: defaults
+  - role: multipathd
diff --git a/roles/portworx/tasks/main.yml b/roles/portworx/tasks/main.yml
new file mode 100644
index 0000000..3b3138a
--- /dev/null
+++ b/roles/portworx/tasks/main.yml
@@ -0,0 +1,63 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+- name: Detect if InitiatorName is set
+  ansible.builtin.slurp:
+    src: /etc/iscsi/initiatorname.iscsi
+  register: portworx_iscsi_initiatorname
+
+- name: Configure InitiatorName
+  when: "'InitiatorName' not in portworx_iscsi_initiatorname.content | b64decode"
+  block:
+    - name: Generate a new InitiatorName
+      ansible.builtin.shell:
+        cmd: iscsi-iname
+      register: portworx_iscsi_iname
+
+    - name: Write the new InitiatorName
+      ansible.builtin.copy:
+        content: "InitiatorName={{ portworx_iscsi_iname.stdout }}"
+        dest: /etc/iscsi/initiatorname.iscsi
+        owner: root
+        group: root
+        mode: "0644"
+
+- name: Install Portworx
+  run_once: true
+  kubernetes.core.k8s:
+    state: present
+    template:
+      - portworx.yml
+      - config.yml
+
+- name: Wait till the CRDs are created
+  run_once: true
+  kubernetes.core.k8s_info:
+    api_version: apiextensions.k8s.io/v1
+    kind: CustomResourceDefinition
+    name: storageclusters.core.libopenstorage.org
+  # NOTE(mnaser): Portworx operator creates the CRDs for the cluster
+  #               so we need to make sure they're created before we proceed.
+  retries: 60
+  delay: 5
+  register: _result
+  until: _result.resources | length > 0
+
+- name: Create Portworx Storage Cluster
+  run_once: true
+  kubernetes.core.k8s:
+    state: present
+    template:
+      - storage_cluster.yml
+      - storage_class.yml
diff --git a/roles/portworx/templates/config.yml b/roles/portworx/templates/config.yml
new file mode 100644
index 0000000..5f8f604
--- /dev/null
+++ b/roles/portworx/templates/config.yml
@@ -0,0 +1,8 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: px-pure-secret
+  namespace: portworx
+stringData:
+  pure.json: '{{ portworx_pure_json | to_json }}'
diff --git a/roles/portworx/templates/portworx.yml b/roles/portworx/templates/portworx.yml
new file mode 100644
index 0000000..65bb35c
--- /dev/null
+++ b/roles/portworx/templates/portworx.yml
@@ -0,0 +1,80 @@
+# SOURCE: https://install.portworx.com/?comp=pxoperator
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: portworx-operator
+  namespace: kube-system
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: portworx-operator
+rules:
+  - apiGroups: ["*"]
+    resources: ["*"]
+    verbs: ["*"]
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: portworx-operator
+subjects:
+- kind: ServiceAccount
+  name: portworx-operator
+  namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: portworx-operator
+  apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: portworx-operator
+  namespace: kube-system
+spec:
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  replicas: 1
+  selector:
+    matchLabels:
+      name: portworx-operator
+  template:
+    metadata:
+      labels:
+        name: portworx-operator
+    spec:
+      containers:
+      - name: portworx-operator
+        imagePullPolicy: Always
+        image: portworx/px-operator:23.10.5
+        command:
+        - /operator
+        - --verbose
+        - --driver=portworx
+        - --leader-elect=true
+        env:
+        - name: OPERATOR_NAME
+          value: portworx-operator
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "name"
+                    operator: In
+                    values:
+                    - portworx-operator
+              topologyKey: "kubernetes.io/hostname"
+      serviceAccountName: portworx-operator
+      # NOTE(mnaser): Add this to keep running on control plane
+      nodeSelector:
+        openstack-control-plane: enabled
diff --git a/roles/portworx/templates/storage_class.yml b/roles/portworx/templates/storage_class.yml
new file mode 100644
index 0000000..c935e24
--- /dev/null
+++ b/roles/portworx/templates/storage_class.yml
@@ -0,0 +1,10 @@
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: general
+  annotations:
+    storageclass.kubernetes.io/is-default-class: "true"
+provisioner: pxd.portworx.com
+parameters:
+  backend: pure_block
+allowVolumeExpansion: true
diff --git a/roles/portworx/templates/storage_cluster.yml b/roles/portworx/templates/storage_cluster.yml
new file mode 100644
index 0000000..ae3f5a5
--- /dev/null
+++ b/roles/portworx/templates/storage_cluster.yml
@@ -0,0 +1,53 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: portworx
+---
+kind: StorageCluster
+apiVersion: core.libopenstorage.org/v1
+metadata:
+  name: px-cluster-567cacc6-e39c-49da-9d35-2bafacfcf18c
+  namespace: portworx
+  annotations:
+    portworx.io/install-source: "https://install.portworx.com/?operator=true&mc=false&kbver=1.28.0&ns=portworx&oem=esse&user=2487eaa2-e557-4f68-8e87-9b56c0a4498f&b=true&iop=6&s=%22size%3D150%22&pureSanType=FC&ce=pure&c=px-cluster-567cacc6-e39c-49da-9d35-2bafacfcf18c&stork=true&csi=true&mon=true&tel=true&st=k8s&promop=true"
+    portworx.io/misc-args: "--oem esse"
+    portworx.io/disable-storage-class: "true"
+    portworx.io/pvc-controller-secure-port: "20257"
+spec:
+  image: portworx/oci-monitor:3.1.1
+  imagePullPolicy: Always
+  kvdb:
+    internal: true
+  cloudStorage:
+    deviceSpecs:
+    - size=150
+  secretsProvider: k8s
+  stork:
+    enabled: true
+    args:
+      webhook-controller: "true"
+  autopilot:
+    enabled: true
+  runtimeOptions:
+    default-io-profile: "6"
+  csi:
+    enabled: true
+  monitoring:
+    telemetry:
+      enabled: true
+    prometheus:
+      enabled: false
+      exportMetrics: true
+  env:
+  - name: PURE_FLASHARRAY_SAN_TYPE
+    value: "{{ portworx_pure_flasharray_san_type }}"
+  placement:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: openstack-control-plane
+            operator: In
+            values:
+            - "enabled"
diff --git a/roles/powerstore_csi/tasks/main.yml b/roles/powerstore_csi/tasks/main.yml
index ec85eb5..5caec12 100644
--- a/roles/powerstore_csi/tasks/main.yml
+++ b/roles/powerstore_csi/tasks/main.yml
@@ -1,11 +1,13 @@
 ---
 - name: Clone PowerStore CSI from GitHub
+  run_once: true
   ansible.builtin.git:
     repo: https://github.com/dell/csi-powerstore.git
     dest: /var/lib/csi-powerstore
     version: v2.3.0
 
 - name: Create Secret
+  run_once: true
   kubernetes.core.k8s:
     state: present
     definition:
@@ -18,6 +20,7 @@
         config: "{{ powerstore_csi_config | to_yaml }}"
 
 - name: Create StorageClass
+  run_once: true
   kubernetes.core.k8s:
     state: present
     definition:
@@ -33,6 +36,7 @@
       volumeBindingMode: Immediate
 
 - name: Deploy Helm chart
+  run_once: true
   kubernetes.core.helm:
     name: csi-powerstore
     chart_ref: /var/lib/csi-powerstore/helm/csi-powerstore