Skip to content

Instantly share code, notes, and snippets.

@clemenko
Created July 9, 2026 14:50
Show Gist options
  • Select an option

  • Save clemenko/97da6371bdceb9544405376d2084fcb9 to your computer and use it in GitHub Desktop.

Select an option

Save clemenko/97da6371bdceb9544405376d2084fcb9 to your computer and use it in GitHub Desktop.
PX-CSI Generic Install

PX-CSI anywhere

This is a quick guide for adding Portworx CSI ( PX-CSI 26.1.0 ) on ANY k8s.

Based on https://docs.harvesterhci.io/v1.8/advanced/csidriver/.

Please also check out https://dzver.rfed.io for the last versions of all these components.

tl:dr - Add multipathd. Add PX CSI, that points to an FA.

If you are on an air gap please read the airgap.md file on collecting all the bits and transfering them.

add multipathd

On ALL nodes ensure multipathd and iscsi is there.

for Rocky

dnf install -y nfs-utils cryptsetup iscsi-initiator-utils device-mapper-multipath; systemctl enable --now iscsid;

If you are missing those packages. fix it. lol

cat << EOF >> /etc/multipath.conf
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
    }
    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
    }
}
blacklist_exceptions {
    property "(SCSI_IDENT_|ID_WWN)"
}
blacklist {
    devnode "^pxd[0-9]*"
    devnode "^pxd*"
    device {
        vendor "VMware"
        product "Virtual disk"
    }
    device {
        vendor "IET"
        product "VIRTUAL-DISK"
    }
}
EOF

add portworx stuff

Here we are going to install the CSI. Note the API token for a "storage admin" user. Here are the docs : https://docs.portworx.com/portworx-enterprise/platform/kubernetes/flasharray/install/install-flasharray/install-flasharray-cd-da

# get latest version of PX-CSI
PX_CSI_VER=$(curl -sL https://dzver.rfed.io/json | jq -r .portworx)

# create namespace
kubectl create ns portworx

# create and add secret
cat << EOF > pure.json 
{
    "FlashArrays": [
        {
            "MgmtEndPoint": "192.168.1.11",
            "APIToken": "934f95b6-6d1d-ee91-d210-6ed9bce13ad1",
            "NFSEndPoint": "192.168.1.8"
        }
    ]
}
EOF
kubectl create secret generic px-pure-secret -n portworx --from-file=pure.json=pure.json

# apply operator yaml
kubectl apply -f 'https://install.portworx.com/'$PX_CSI_VER'?comp=pxoperator&oem=px-csi&kbver=1.35.6&ns=portworx'

# add annotation of "portworx.io/health-check: "skip" " for running on a single node

#  If you want nvme-tcp change the value: "NVMEOF-TCP"

# or FA NFS : https://docs.portworx.com/portworx-csi/provision-storage/dynamic-provisioning/flasharray-file-services

kubectl apply -n portworx  -f - << EOF 
kind: StorageCluster
apiVersion: core.libopenstorage.org/v1
metadata:
  name: px-cluster
  namespace: portworx
  annotations:
    portworx.io/misc-args: "--oem px-csi"
    #portworx.io/health-check: "skip"
spec:
  image: portworx/px-pure-csi-driver:$PX_CSI_VER
  imagePullPolicy: IfNotPresent
  csi:
    enabled: true
  monitoring:
    telemetry:
      enabled: false
    prometheus:
      enabled: false
      exportMetrics: false
  env:
  - name: PURE_FLASHARRAY_SAN_TYPE
    value: "ISCSI"
EOF

or a simple pvc test

kubectl apply -n portworx  -f - << EOF 
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: demopvc
spec:
  storageClassName: px-fa-direct-access
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1234Mi
EOF

Success.

optional storage class

from https://docs.portworx.com/portworx-csi/reference/storage-class#storageclass

kubectl apply -n portworx  -f - << EOF 
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: px-fb-direct-access-nfsv4-hardquota
mountOptions:
- nfsvers=4.1
- tcp
allowVolumeExpansion: false
parameters:
  backend: pure_file
  pure_export_rules: '*(rw)'
provisioner: pxd.portworx.com
reclaimPolicy: Delete
volumeBindingMode: Immediate
EOF

# or 

kubectl apply -n portworx  -f - << EOF
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: px-fa-direct-access
allowVolumeExpansion: true
parameters:
  backend: pure_block #pure_fa_file
provisioner: pxd.portworx.com
reclaimPolicy: Delete
volumeBindingMode: Immediate
EOF

Hauler All the Pure/Portworx Things

This guide will show how we can easily air gap all the Pure and Portworx bits for Harvester. It is heavily based on https://docs.portworx.com/portworx-csi/install/airgapped-install#configure-portworx-version-manifest. We are going to use a tool from Rancher Gov called Hauler. Currently we are not using all the features of Hauler. This guide is using the FlashArray as the target storage device. There a few things we will need to connect. An API token for the pureuser, the ip address, a linux host with internet access, and a linux host with access to the array.

Or watch the video : https://youtu.be/SJHFvABdvUA

install hauler - INTERNET side

We are going to use the linux server with internet access. We will collect and tar the bits we need. Using Hauler as the swiss army knife for collecting and serving files across an air gap. https://docs.hauler.dev/docs/intro

curl -sfL https://get.hauler.dev | bash

generate hauler yaml and sync

Ww are going to generate the hauler manifest yaml. A good idea is to save that to version control for use repeatability.

mkdir -p /opt/pure; cd /opt/pure

PX_CSI_VER=$(curl -sL https://dzver.rfed.io/json | jq -r .portworx)

cat << EOF > /opt/pure/airgap.yaml
apiVersion: content.hauler.cattle.io/v1
kind: Files
metadata:
  name: pure-files
spec:
  files:
    - path: https://install.portworx.com/$PX_CSI_VER/version?kbver=1.35.4
      name: versions.yaml
    - path: https://install.portworx.com/$PX_CSI_VER?comp=pxoperator&oem=px-csi&kbver=1.35.4&ns=portworx
      name: operator.yaml
    - path: https://raw.githubusercontent.com/clemenko/px-harvester/refs/heads/main/readme.md
      name: px_harvester.md
    - path: https://raw.githubusercontent.com/clemenko/px-harvester/refs/heads/main/StorageCluster_example.yaml
    - path: https://raw.githubusercontent.com/clemenko/px-harvester/refs/heads/main/airgap_readme.md
    - path: https://mirrors.portworx.com/packages/px-cli/latest/px-v1.4.0.linux.amd64.tar.gz
---
apiVersion: content.hauler.cattle.io/v1
kind: Charts
metadata:
  name: portworx-charts
spec:
  charts:
    - name: portworx
      repoURL: http://charts.portworx.io/ 
---
apiVersion: content.hauler.cattle.io/v1
kind: Images
metadata:
  name: portworx-images
  annotations:
    hauler.dev/platform: linux/amd64
spec:       
  images:
EOF

for i in $(curl -s https://install.portworx.com/$PX_CSI_VER/images); do echo "    - name: "$i >> /opt/pure/airgap.yaml ; done

Now we can sync and create the local hauler store. This can take a minute or two depending on the images.

hauler store sync -f /opt/pure/airgap.yaml

Add local pdfs if needed.

hauler store add file user_guides_for_vsphere_plugin.pdf
hauler store add file Dark_site_FA_Install_guide.pdf

Validate the local store

hauler store info

Add the Hauler binary to the /opt/pure directory.

rsync -avP /usr/local/bin/hauler /opt/pure/hauler

compress

tar -cf /opt/pxcsi_airgap_$(date '+%m_%d_%y').tar $(ls)


Move the Tar

This will highly depend on your network and security levels. Diode, DVD, BluRay, or even Thumbdrive are all options. Just get the tarball over to the air-gapped side.


Unpack and Serve - Air Gap side

Once you have the tar on the air gapped side we need to uncompress it on the linux host.

mkdir -p /opt/pure/cert
tar -vxf pxcsi_airgap_$(date '+%m_%d_%y').tar -C /opt/pure
cd /opt/pure

This is a step that may not be necessary. If you want to push the images to an internal registry you can use the command:

hauler store sync --filename <file-name> --platform <platform> --key <cosign-public-key> --registry <registry-url>

Docs : https://docs.hauler.dev/docs/hauler-usage/store/sync

serve all the things

Hauler makes it fairly easy serve out the files and even the images if needed. We can take advantage of systemd and create 2 "services". The registry service will create it's own self signed cert.

# we need the ip of this host
export HAULER_IP=192.168.1.216
openssl req -x509 -newkey rsa:4096 -keyout /opt/pure/cert/key.pem -out /opt/pure/cert/cert.pem -sha256 -days 3650 -nodes -subj "/C=US/ST=Maryland/L=Edgewater/O=PureStorage/OU=FluxDepartment/CN=$HAULER_IP"

# create systemd files
cat << EOF > /etc/systemd/system/hauler-fileserver.service
# /etc/systemd/system/hauler-fileserver.service
[Unit]
Description=Hauler Serve FileServer Service

[Service]
Environment="HOME=/opt/pure/"
ExecStart=/usr/local/bin/hauler store serve fileserver -s /opt/pure/store
WorkingDirectory=/opt/pure/
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

cat << EOF > /etc/systemd/system/hauler-registry.service
# /etc/systemd/system/hauler-registry.service
[Unit]
Description=Hauler Serve Registry Service

[Service]
Environment="HOME=/opt/pure/"
ExecStart=/usr/local/bin/hauler store serve registry -s /opt/pure/store --tls-cert /opt/pure/cert/cert.pem --tls-key /opt/pure/cert/key.pem 
WorkingDirectory=/opt/pure/
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

#reload daemon
systemctl daemon-reload

# start fileserver
systemctl enable --now hauler-fileserver.service

# start reg
systemctl enable --now hauler-registry.service

We can now navigate to the IP:8080 to see the files on the webserver. And check port 5000 for the registry. If it is not working we can run ss -tln to see if ports 8080, and 5000 are open. If not we can run journalctl -xefu hauler-registry or journalctl -xefu hauler-fileserver to see why.

Review the readme.md for the install instructions. We will have to change a few things like imagePullPolicy: IfNotPresent to imagePullPolicy: IfNotPresent in all the yamls. We will also have to change the image locations to something local. Either Hauler or an internal registry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment