Tuesday, May 17, 2022

Rook on Singe Node OpenShift


Recently a lot of customers have been asking about how to configure storage on a Single Node OpenShift (SNO) deployment.   When dealing with a compact or multi-node OpenShift deployment I myself have always relied on using OpenShift Data Foundation (ODF) as the underpinning of my storage requirements after all it provides the ability to do block, object and file all from the same deployment.  However in a SNO deployment ODF does not seem to be an option due to the way the operator has been designed.  However there is a way to at least get some resemblance to ODF without a lot of hassle in a SNO environment.  The following blog demonstrates a non-supported way on how I go about getting the dynamic block storage I need in my SNO cluster using Rook.

Before we begin lets quickly go over the environment of this SNO cluster.   As with any SNO cluster it is a single node acting as both the control plane and worker node.  This particular deployment was based on OpenShift 4.10.11 and deployed using the Assisted Installer at cloud.redhat.com

$ oc get nodes
NAME                            STATUS   ROLES           AGE   VERSION
master-0.sno3.schmaustech.com   Ready    master,worker   3h    v1.23.5+9ce5071

Inside the node via the debug pod we can see that we have an extra 160GB disk available to use toward our Rook deployment:

$ oc debug node/master-0.sno3.schmaustech.com
Starting pod/master-0sno3schmaustechcom-debug ...
To use host binaries, run `chroot /host`
Pod IP: 192.168.0.206
If you don't see a command prompt, try pressing enter.
sh-4.4# chroot /host
sh-4.4# lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0   120G  0 disk 
|-sda1   8:1    0     1M  0 part 
|-sda2   8:2    0   127M  0 part 
|-sda3   8:3    0   384M  0 part /boot
`-sda4   8:4    0 119.5G  0 part /sysroot
sdb      8:16   0   160G  0 disk 
sr0     11:0    1   999M  0 rom more

Now that we have provided the environment background lets go ahead and start to configure Rook on the SNO node.   The first step will be to configure the custom resource definitions that Rook requires before the operator and the Ceph cluster can be deployed.   Since I have not changed the crds.yaml file from its defaults we can consume it directly from the Rook Github repository and apply it to our SNO node:

$ oc create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/crds.yaml 
customresourcedefinition.apiextensions.k8s.io/cephblockpoolradosnamespaces.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephblockpools.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephbucketnotifications.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephbuckettopics.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephclients.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephclusters.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephfilesystemmirrors.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephfilesystems.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephfilesystemsubvolumegroups.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephnfses.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectrealms.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectstores.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectstoreusers.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectzonegroups.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectzones.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephrbdmirrors.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/objectbucketclaims.objectbucket.io created
customresourcedefinition.apiextensions.k8s.io/objectbuckets.objectbucket.io created

With the custom resource definitions applied we can move onto adding in the common resources that are necessary to start the operator and the Ceph cluster.  Again since I am not changing anything in the defaults from the Rook Github repository we can apply directly from the source to the SNO node:
  
$ oc create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/common.yaml 
namespace/rook-ceph created
clusterrole.rbac.authorization.k8s.io/cephfs-csi-nodeplugin created
clusterrole.rbac.authorization.k8s.io/cephfs-external-provisioner-runner created
clusterrole.rbac.authorization.k8s.io/psp:rook created
clusterrole.rbac.authorization.k8s.io/rbd-csi-nodeplugin created
clusterrole.rbac.authorization.k8s.io/rbd-external-provisioner-runner created
clusterrole.rbac.authorization.k8s.io/rook-ceph-cluster-mgmt created
clusterrole.rbac.authorization.k8s.io/rook-ceph-global created
clusterrole.rbac.authorization.k8s.io/rook-ceph-mgr-cluster created
clusterrole.rbac.authorization.k8s.io/rook-ceph-mgr-system created
clusterrole.rbac.authorization.k8s.io/rook-ceph-object-bucket created
clusterrole.rbac.authorization.k8s.io/rook-ceph-osd created
clusterrole.rbac.authorization.k8s.io/rook-ceph-system created
clusterrolebinding.rbac.authorization.k8s.io/cephfs-csi-nodeplugin created
clusterrolebinding.rbac.authorization.k8s.io/cephfs-csi-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/rbd-csi-nodeplugin created
clusterrolebinding.rbac.authorization.k8s.io/rbd-csi-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-global created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-mgr-cluster created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-object-bucket created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-osd created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-system created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-system-psp created
clusterrolebinding.rbac.authorization.k8s.io/rook-csi-cephfs-plugin-sa-psp created
clusterrolebinding.rbac.authorization.k8s.io/rook-csi-cephfs-provisioner-sa-psp created
clusterrolebinding.rbac.authorization.k8s.io/rook-csi-rbd-plugin-sa-psp created
clusterrolebinding.rbac.authorization.k8s.io/rook-csi-rbd-provisioner-sa-psp created
Warning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
podsecuritypolicy.policy/00-rook-privileged created
role.rbac.authorization.k8s.io/cephfs-external-provisioner-cfg created
role.rbac.authorization.k8s.io/rbd-csi-nodeplugin created
role.rbac.authorization.k8s.io/rbd-external-provisioner-cfg created
role.rbac.authorization.k8s.io/rook-ceph-cmd-reporter created
role.rbac.authorization.k8s.io/rook-ceph-mgr created
role.rbac.authorization.k8s.io/rook-ceph-osd created
role.rbac.authorization.k8s.io/rook-ceph-purge-osd created
role.rbac.authorization.k8s.io/rook-ceph-rgw created
role.rbac.authorization.k8s.io/rook-ceph-system created
rolebinding.rbac.authorization.k8s.io/cephfs-csi-provisioner-role-cfg created
rolebinding.rbac.authorization.k8s.io/rbd-csi-nodeplugin-role-cfg created
rolebinding.rbac.authorization.k8s.io/rbd-csi-provisioner-role-cfg created
rolebinding.rbac.authorization.k8s.io/rook-ceph-cluster-mgmt created
rolebinding.rbac.authorization.k8s.io/rook-ceph-cmd-reporter created
rolebinding.rbac.authorization.k8s.io/rook-ceph-cmd-reporter-psp created
rolebinding.rbac.authorization.k8s.io/rook-ceph-default-psp created
rolebinding.rbac.authorization.k8s.io/rook-ceph-mgr created
rolebinding.rbac.authorization.k8s.io/rook-ceph-mgr-psp created
rolebinding.rbac.authorization.k8s.io/rook-ceph-mgr-system created
rolebinding.rbac.authorization.k8s.io/rook-ceph-osd created
rolebinding.rbac.authorization.k8s.io/rook-ceph-osd-psp created
rolebinding.rbac.authorization.k8s.io/rook-ceph-purge-osd created
rolebinding.rbac.authorization.k8s.io/rook-ceph-purge-osd-psp created
rolebinding.rbac.authorization.k8s.io/rook-ceph-rgw created
rolebinding.rbac.authorization.k8s.io/rook-ceph-rgw-psp created
rolebinding.rbac.authorization.k8s.io/rook-ceph-system created
serviceaccount/rook-ceph-cmd-reporter created
serviceaccount/rook-ceph-mgr created
serviceaccount/rook-ceph-osd created
serviceaccount/rook-ceph-purge-osd created
serviceaccount/rook-ceph-rgw created
serviceaccount/rook-ceph-system created
serviceaccount/rook-csi-cephfs-plugin-sa created
serviceaccount/rook-csi-cephfs-provisioner-sa created
serviceaccount/rook-csi-rbd-plugin-sa created
serviceaccount/rook-csi-rbd-provisioner-sa created

Next we need to create the Rook operator.yaml file which will be used to configure the Rook operator:

$ cat << EOF > ~/operator.yaml
kind: SecurityContextConstraints
apiVersion: security.openshift.io/v1
metadata:
  name: rook-ceph
allowPrivilegedContainer: true
allowHostDirVolumePlugin: true
allowHostPID: false
allowHostNetwork: false
allowHostPorts: false
priority:
allowedCapabilities: ["MKNOD"]
allowHostIPC: true
readOnlyRootFilesystem: false
requiredDropCapabilities: []
defaultAddCapabilities: []
runAsUser:
  type: RunAsAny
seLinuxContext:
  type: MustRunAs
fsGroup:
  type: MustRunAs
supplementalGroups:
  type: RunAsAny
volumes:
  - configMap
  - downwardAPI
  - emptyDir
  - hostPath
  - persistentVolumeClaim
  - projected
  - secret
users:
  - system:serviceaccount:rook-ceph:rook-ceph-system 
  - system:serviceaccount:rook-ceph:default 
  - system:serviceaccount:rook-ceph:rook-ceph-mgr 
  - system:serviceaccount:rook-ceph:rook-ceph-osd 
  - system:serviceaccount:rook-ceph:rook-ceph-rgw 
---
kind: SecurityContextConstraints
apiVersion: security.openshift.io/v1
metadata:
  name: rook-ceph-csi
allowPrivilegedContainer: true
allowHostNetwork: true
allowHostDirVolumePlugin: true
priority:
allowedCapabilities: ["SYS_ADMIN"]
allowHostPorts: true
allowHostPID: true
allowHostIPC: true
readOnlyRootFilesystem: false
runAsUser:
  type: RunAsAny
seLinuxContext:
  type: RunAsAny
fsGroup:
  type: RunAsAny
supplementalGroups:
  type: RunAsAny
volumes:
  - configMap
  - projected
  - emptyDir
  - hostPath
users:
  - system:serviceaccount:rook-ceph:rook-csi-rbd-plugin-sa 
  - system:serviceaccount:rook-ceph:rook-csi-rbd-provisioner-sa 
  - system:serviceaccount:rook-ceph:rook-csi-cephfs-plugin-sa 
  - system:serviceaccount:rook-ceph:rook-csi-cephfs-provisioner-sa 
  - system:serviceaccount:rook-ceph:rook-csi-nfs-plugin-sa 
  - system:serviceaccount:rook-ceph:rook-csi-nfs-provisioner-sa 
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: rook-ceph-operator-config
  namespace: rook-ceph 
data:
  ROOK_LOG_LEVEL: "INFO"
  ROOK_CSI_ENABLE_CEPHFS: "true"
  ROOK_CSI_ENABLE_RBD: "true"
  ROOK_CSI_ENABLE_NFS: "false"
  ROOK_CSI_ENABLE_GRPC_METRICS: "false"
  CSI_ENABLE_ENCRYPTION: "false"
  CSI_PROVISIONER_REPLICAS: "2"
  CSI_ENABLE_CEPHFS_SNAPSHOTTER: "true"
  CSI_ENABLE_RBD_SNAPSHOTTER: "true"
  CSI_FORCE_CEPHFS_KERNEL_CLIENT: "true"
  CSI_RBD_FSGROUPPOLICY: "ReadWriteOnceWithFSType"
  CSI_CEPHFS_FSGROUPPOLICY: "ReadWriteOnceWithFSType"
  CSI_NFS_FSGROUPPOLICY: "ReadWriteOnceWithFSType"
  ROOK_CSI_ALLOW_UNSUPPORTED_VERSION: "false"
  CSI_PLUGIN_ENABLE_SELINUX_HOST_MOUNT: "false"
  CSI_PLUGIN_PRIORITY_CLASSNAME: "system-node-critical"
  CSI_PROVISIONER_PRIORITY_CLASSNAME: "system-cluster-critical"
  ROOK_OBC_WATCH_OPERATOR_NAMESPACE: "true"
  ROOK_ENABLE_DISCOVERY_DAEMON: "false"
  CSI_ENABLE_VOLUME_REPLICATION: "false"
  ROOK_CEPH_COMMANDS_TIMEOUT_SECONDS: "15"
  CSI_ENABLE_CSIADDONS: "false"
  CSI_GRPC_TIMEOUT_SECONDS: "150"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rook-ceph-operator
  namespace: rook-ceph 
  labels:
    operator: rook
    storage-backend: ceph
    app.kubernetes.io/name: rook-ceph
    app.kubernetes.io/instance: rook-ceph
    app.kubernetes.io/component: rook-ceph-operator
    app.kubernetes.io/part-of: rook-ceph-operator
spec:
  selector:
    matchLabels:
      app: rook-ceph-operator
  replicas: 1
  template:
    metadata:
      labels:
        app: rook-ceph-operator
    spec:
      serviceAccountName: rook-ceph-system
      containers:
        - name: rook-ceph-operator
          image: rook/ceph:v1.9.3
          args: ["ceph", "operator"]
          securityContext:
            runAsNonRoot: true
            runAsUser: 2016
            runAsGroup: 2016
          volumeMounts:
            - mountPath: /var/lib/rook
              name: rook-config
            - mountPath: /etc/ceph
              name: default-config-dir
            - mountPath: /etc/webhook
              name: webhook-cert
          ports:
            - containerPort: 9443
              name: https-webhook
              protocol: TCP
          env:
            - name: ROOK_CURRENT_NAMESPACE_ONLY
              value: "false"
            - name: ROOK_DISCOVER_DEVICES_INTERVAL
              value: "60m"
            - name: ROOK_HOSTPATH_REQUIRES_PRIVILEGED
              value: "true"
            - name: ROOK_ENABLE_SELINUX_RELABELING
              value: "true"
            - name: ROOK_ENABLE_FSGROUP
              value: "true"
            - name: ROOK_DISABLE_DEVICE_HOTPLUG
              value: "false"
            - name: DISCOVER_DAEMON_UDEV_BLACKLIST
              value: "(?i)dm-[0-9]+,(?i)rbd[0-9]+,(?i)nbd[0-9]+"
            - name: ROOK_ENABLE_MACHINE_DISRUPTION_BUDGET
              value: "false"
            - name: ROOK_UNREACHABLE_NODE_TOLERATION_SECONDS
              value: "5"
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
      volumes:
        - name: rook-config
          emptyDir: {}
        - name: default-config-dir
          emptyDir: {}
        - name: webhook-cert
          emptyDir: {}

EOF

With the Rook operator.yaml saved we can now apply it to the SNO node and after a few minutes validate that the Rook operator is running:

$ oc create -f operator.yaml 
securitycontextconstraints.security.openshift.io/rook-ceph created
securitycontextconstraints.security.openshift.io/rook-ceph-csi created
configmap/rook-ceph-operator-config created
deployment.apps/rook-ceph-operator created

$ oc get pods -n rook-ceph
NAME                                                              READY   STATUS      RESTARTS   AGE
rook-ceph-operator-84bf68d9bd-lv9l9                               1/1     Running     0          1m

Finally we get the heart of this configuration which is the cluster.yaml file.   In this file we need to make some modification since we only have a single node for the Ceph deployment via Rook.   Here are things I modified from the default:
  • osd_pool_default_size needs to be 1
  • mon count needs to be 1 and allowMultiplePerNode needs to be true
  • mgr count needs to be 1 and allowMultiplePerNode needs to be true
  • storage device needs to be set to extra disk available (in my case sdb)
  • osdsPerDevice needs to be 1
  • managePodBudgets and manageMachineDisruptionBudgets both set to false
We also need to keep in mind that this configuration is not a redundant configuration with OSD replication across many nodes so its really just a configuration of convenience to provide dynamic storage for the potential of multiple applications requiring persistent volume claims.  We can proceed by saving out the cluster.yaml with the updates mentioned above:

$ cat << EOF > ~/cluster.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: rook-config-override
  namespace: rook-ceph
data:
  config: |
    [global]
    osd_pool_default_size = 1
---
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph 
spec:
  cephVersion:
    image: quay.io/ceph/ceph:v16.2.7
    allowUnsupported: false
  dataDirHostPath: /var/lib/rook
  skipUpgradeChecks: false
  continueUpgradeAfterChecksEvenIfNotHealthy: false
  waitTimeoutForHealthyOSDInMinutes: 10
  mon:
    count: 1
    allowMultiplePerNode: true
  mgr:
    count: 1
    allowMultiplePerNode: true
    modules:
      - name: pg_autoscaler
        enabled: true
  dashboard:
    enabled: true
    ssl: true
  monitoring:
    enabled: false
  network:
    connections:
      encryption:
        enabled: false
      compression:
        enabled: false
  crashCollector:
    disable: false
  cleanupPolicy:
    confirmation: ""
    sanitizeDisks:
      method: quick
      dataSource: zero
      iteration: 1
    allowUninstallWithVolumes: false
  annotations:
  labels:
  resources:
  removeOSDsIfOutAndSafeToRemove: false
  priorityClassNames:
    mon: system-node-critical
    osd: system-node-critical
    mgr: system-cluster-critical
  storage: 
    useAllNodes: true
    useAllDevices: false
    devices:
    - name: "sdb"
    config:
      osdsPerDevice: "1"
    onlyApplyOSDPlacement: false
  disruptionManagement:
    managePodBudgets: false
    osdMaintenanceTimeout: 30
    pgHealthCheckTimeout: 0
    manageMachineDisruptionBudgets: false
    machineDisruptionBudgetNamespace: openshift-machine-api
  healthCheck:
    daemonHealth:
      mon:
        disabled: false
        interval: 45s
      osd:
        disabled: false
        interval: 60s
      status:
        disabled: false
        interval: 60s
    livenessProbe:
      mon:
        disabled: false
      mgr:
        disabled: false
      osd:
        disabled: false
    startupProbe:
      mon:
        disabled: false
      mgr:
        disabled: false
      osd:
        disabled: false

EOF

Now with the cluster.yaml saved we can apply it to the cluster and let the Rook operator do the work of creating the Ceph cluster on our SNO node:

$ oc create -f cluster.yaml 
configmap/rook-config-override created
cephcluster.ceph.rook.io/rook-ceph created

After a few minutes, depending on the speed of the SNO deployment, we can validate that the Ceph cluster is up and deployed on our SNO node:

$ oc get pods -n rook-ceph
NAME                                                              READY   STATUS      RESTARTS   AGE
csi-cephfsplugin-provisioner-7577bb4d59-kxmq8                     6/6     Running     0          118s
csi-cephfsplugin-x2njd                                            3/3     Running     0          118s
csi-rbdplugin-provisioner-847b498845-7z5qc                        6/6     Running     0          119s
csi-rbdplugin-tlw5d                                               3/3     Running     0          119s
rook-ceph-crashcollector-master-0.sno3.schmaustech.com-858mmbx2   1/1     Running     0          48s
rook-ceph-mgr-a-57fbb7fb47-9rjl5                                  1/1     Running     0          81s
rook-ceph-mon-a-d94d79bb5-l6f8p                                   1/1     Running     0          110s
rook-ceph-operator-84bf68d9bd-qkj6k                               1/1     Running     0          17m
rook-ceph-osd-0-6c98c84f66-96l5q                                  1/1     Running     0          48s
rook-ceph-osd-prepare-master-0.sno3.schmaustech.com-l5s8t         0/1     Completed   0          60s

We can see from the above output that the pods for both the single mon and single osd are up and running along with the additional services for our single node Ceph cluster.   We can further validate that the Ceph cluster is up and running by deploying a Ceph toolbox pod.   For that we will just use the toolbox.yaml from the Rook Github repository.  Once we create the pod we can validate it is running by filtering for the rook-ceph-tools pod in the rook-ceph namespace:

$ oc create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/toolbox.yaml
deployment.apps/rook-ceph-tools created

$ oc get pods -n rook-ceph| grep rook-ceph-tools
rook-ceph-tools-d6d7c985c-6zwc7                                   1/1     Running     0          54s

Now lets use the running toolbox to check on the Ceph cluster by issuing an exec command to it and passing in a ceph status:

$ oc -n rook-ceph exec -it rook-ceph-tools-d6d7c985c-6zwc7 -- ceph status
  cluster:
    id:     c54ad01e-e9f8-48c9-806b-a7d4748eb977
    health: HEALTH_OK
 
  services:
    mon: 1 daemons, quorum a (age 7m)
    mgr: a(active, since 5m)
    osd: 1 osds: 1 up (since 5m), 1 in (since 6m)
 
  data:
    pools:   0 pools, 0 pgs
    objects: 0 objects, 0 B
    usage:   4.8 MiB used, 160 GiB / 160 GiB avail
    pgs: 

Sure enough our Ceph cluster is up and running and in a healthy state.   Lets move on now to confirm we can consume storage from it.   To do that we need to setup a storageclass configuration like the example below which will create a Ceph RBD block storageclass:

$ cat << EOF > ~/storageclass.yaml
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 1
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
    clusterID: rook-ceph
    pool: replicapool
    imageFormat: "2"
    imageFeatures: layering
    csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
    csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
    csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
    csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
    csi.storage.k8s.io/fstype: ext4
reclaimPolicy: Delete
allowVolumeExpansion: true
EOF

Once we have saved the storageclass.yaml file lets go ahead and apply it to the SNO node and then check that the storageclass was created:

$ oc create -f storageclass.yaml
cephblockpool.ceph.rook.io/replicapool created
storageclass.storage.k8s.io/rook-ceph-block created

$ oc get sc
NAME              PROVISIONER                  RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
rook-ceph-block   rook-ceph.rbd.csi.ceph.com   Delete          Immediate           true                   3s
oc get 

Now that we have a storageclass created I like to do one more thing to ensure any outstanding persistent volume claims get fulfilled by the storageclass automatically.  To do this I will patch the storageclass to be the default:

$ oc patch storageclass  rook-ceph-block -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
storageclass.storage.k8s.io/rook-ceph-block patched

$ oc get sc
NAME                        PROVISIONER                  RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
rook-ceph-block (default)   rook-ceph.rbd.csi.ceph.com   Delete          Immediate           true                   8m41s

At this point everything is configured to consume storage.  What I did in the example below was to kick off an installation of Red Hat Advanced Cluster Management on my SNO node because I knew it would need a PV.  Once it completed installation I confirmed by looking at the persistent volumes that indeed one had been created from our storageclass which gets its storage from the Ceph cluster:

$ oc get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                             STORAGECLASS      REASON   AGE
pvc-791551c6-cdc4-4c9e-9692-c9622dbef4e8   10Gi       RWO            Delete           Bound    open-cluster-management/search-redisgraph-pvc-0   rook-ceph-block            45s


Hopefully this was a helpful blog in providing a dynamic almost ODF like experience for a SNO node deployment!

Friday, May 06, 2022

Mirroring Operators into Red Hat Quay

 


When dealing with disconnected spoke clusters that are being deployed by Red Hat Advanced Cluster Management we have to be aware that any operators that we want to install into our disconnected spoke clusters also need to be mirrored into our local Red Hat Quay registry for them to be accessible by the spoke cluster. In this blog we will mirror down the Red Hat Advanced Cluster Manager operator image components because we need the agent images that normally would get started on a spoke cluster so the spoke cluster can properly join the Red Hat Advanced Cluster Manager hub and report in its metrics and status. Note this procedure could be modified to pull in any of the operators that are normally visible in OpenShift's OperatorHub.

Before we get started we need to ensure we have the following tools available to use:  grpcurl, opm and podman.   To install grpcurl we need to retrieve the proper release binary from the following github repository and extract it:

$ wget -q -O - "https://github.com/fullstorydev/grpcurl/releases/download/v1.8.6/grpcurl_1.8.6_linux_x86_64.tar.gz" | sudo tar -C /usr/local/bin/ -xvz
LICENSE
grpcurl
[bschmaus@provisioning ~]$ which grpcurl
/usr/local/bin/grpcurl

Next we need to pull the latest opm binary from the OpenShift mirror (in this case 4.10) and extract it:

$ curl https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/latest-4.10/opm-linux.tar.gz | sudo tar -C /usr/local/bin/ -xvz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0 23.7M    0  135k    0     0   151k      0  0:02:40 --:--:--  0:02:40  151kopm
100 23.7M  100 23.7M    0     0  7685k      0  0:00:03  0:00:03 --:--:-- 7685k
$ which opm
/usr/local/bin/opm

And finally we can use dnf install podman to install podman if it is not already there:

$ sudo dnf install podman
Updating Subscription Management repositories.
Last metadata expiration check: 1:32:12 ago on Fri 06 May 2022 11:22:36 AM CDT.
Package podman-1:3.4.2-9.module+el8.5.0+13852+150547f7.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
$ which podman
/usr/bin/podman 

Now that we have our tools ready we can begin the process of mirroring the Red Hat Advanced Cluster Management operator and its corresponding images.  The first step is to login to the source registry and the target registry.  In my case the source registry is registry.redhat.io and the target registry is my local Quay poc-registry-quay-quay-poc.apps.kni20.schmaustech.com:

$ podman login registry.redhat.io
Username: schmaustech
Password: 
Login Succeeded!

$ podman login poc-registry-quay-quay-poc.apps.kni20.schmaustech.com --tls-verify=false
Username: openshift
Password: 
Login Succeeded!

Next lets determine the list of packages we want to include in our pruned index of operators.  We already know we just want Red Hat Advanced Cluster Management but this will provide the background context on how to get all the available operators in the event one would like to mirror more.  We first need to start a source image index pod so we can extract out the list by executing the following:

$ podman run -p50051:50051 -it registry.redhat.io/redhat/redhat-operator-index:v4.10
WARN[0000] DEPRECATION NOTICE:
Sqlite-based catalogs and their related subcommands are deprecated. Support for
them will be removed in a future release. Please migrate your catalog workflows
to the new file-based catalog format. 
WARN[0000] unable to set termination log path            error="open /dev/termination-log: permission denied"
INFO[0000] Keeping server open for infinite seconds      database=/database/index.db port=50051
INFO[0000] serving registry                              database=/database/index.db port=50051

In another terminal window on the same host where the above podman command was run use grpcurl to extract out a list of operator packages and redirect it into packages.out file:

$ grpcurl -plaintext localhost:50051 api.Registry/ListPackages > packages.out

The packages.out file that was generate will contain a listing of all the different operators that could be potentially mirrored.    If we go ahead and grep out cluster from the packages.out file we can see a few of those listing.   The specific one we are interested in is at the top of the list: advanced-cluster-management.

$ grep cluster packages.out 
  "name": "advanced-cluster-management"
  "name": "cluster-kube-descheduler-operator"
  "name": "cluster-logging"
  "name": "clusterresourceoverride"
  "name": "odf-multicluster-orchestrator"
  "name": "odr-cluster-operator"

Now that we have the exact name of the operator as listed in the operator index from registry.redhat.io we can now use that along with opm to generate our own operator registry index for our Red Hat Quay registry.   To do this we will use the opm index prune command and specify the listing of packages we want.  The output will create a local index:

$ opm index prune --from-index "registry.redhat.io/redhat/redhat-operator-index:v4.10" --packages 'advanced-cluster-management' --tag poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/olm-index/redhat-oprator-index:v4.10
WARN[0000] DEPRECATION NOTICE:
Sqlite-based catalogs and their related subcommands are deprecated. Support for
them will be removed in a future release. Please migrate your catalog workflows
to the new file-based catalog format. 
INFO[0000] pruning the index                             packages="[advanced-cluster-management]"
INFO[0000] Pulling previous image registry.redhat.io/redhat/redhat-operator-index:v4.10 to get metadata  packages="[advanced-cluster-management]"
INFO[0000] running /usr/bin/podman pull registry.redhat.io/redhat/redhat-operator-index:v4.10  packages="[advanced-cluster-management]"
INFO[0022] running /usr/bin/podman pull registry.redhat.io/redhat/redhat-operator-index:v4.10  packages="[advanced-cluster-management]"
INFO[0024] Getting label data from previous image        packages="[advanced-cluster-management]"
INFO[0024] running podman inspect                        packages="[advanced-cluster-management]"
INFO[0024] running podman create                         packages="[advanced-cluster-management]"
INFO[0024] running podman cp                             packages="[advanced-cluster-management]"
INFO[0029] running podman rm                             packages="[advanced-cluster-management]"
INFO[0030] deleting packages                             pkg=3scale-operator
INFO[0030] packages: [3scale-operator]                   pkg=3scale-operator
INFO[0031] deleting packages                             pkg=amq-broker-rhel8
INFO[0031] packages: [amq-broker-rhel8]                  pkg=amq-broker-rhel8
INFO[0031] deleting packages                             pkg=amq-online
INFO[0031] packages: [amq-online]                        pkg=amq-online
INFO[0031] deleting packages                             pkg=amq-streams
INFO[0031] packages: [amq-streams]                       pkg=amq-streams
INFO[0031] deleting packages                             pkg=amq7-interconnect-operator
INFO[0031] packages: [amq7-interconnect-operator]        pkg=amq7-interconnect-operator
INFO[0031] deleting packages                             pkg=ansible-automation-platform-operator
INFO[0031] packages: [ansible-automation-platform-operator]  pkg=ansible-automation-platform-operator
INFO[0032] deleting packages                             pkg=ansible-cloud-addons-operator
INFO[0032] packages: [ansible-cloud-addons-operator]     pkg=ansible-cloud-addons-operator
INFO[0032] deleting packages                             pkg=apicast-operator
INFO[0032] packages: [apicast-operator]                  pkg=apicast-operator
INFO[0032] deleting packages                             pkg=aws-efs-csi-driver-operator
INFO[0032] packages: [aws-efs-csi-driver-operator]       pkg=aws-efs-csi-driver-operator
INFO[0032] deleting packages                             pkg=businessautomation-operator
INFO[0032] packages: [businessautomation-operator]       pkg=businessautomation-operator
INFO[0032] deleting packages                             pkg=cincinnati-operator
INFO[0032] packages: [cincinnati-operator]               pkg=cincinnati-operator
INFO[0032] deleting packages                             pkg=cluster-kube-descheduler-operator
INFO[0032] packages: [cluster-kube-descheduler-operator]  pkg=cluster-kube-descheduler-operator
INFO[0032] deleting packages                             pkg=cluster-logging
INFO[0032] packages: [cluster-logging]                   pkg=cluster-logging
INFO[0032] deleting packages                             pkg=clusterresourceoverride
INFO[0032] packages: [clusterresourceoverride]           pkg=clusterresourceoverride
INFO[0032] deleting packages                             pkg=codeready-workspaces
INFO[0032] packages: [codeready-workspaces]              pkg=codeready-workspaces
INFO[0032] deleting packages                             pkg=codeready-workspaces2
INFO[0032] packages: [codeready-workspaces2]             pkg=codeready-workspaces2
INFO[0032] deleting packages                             pkg=compliance-operator
INFO[0032] packages: [compliance-operator]               pkg=compliance-operator
INFO[0033] deleting packages                             pkg=container-security-operator
INFO[0033] packages: [container-security-operator]       pkg=container-security-operator
INFO[0033] deleting packages                             pkg=costmanagement-metrics-operator
INFO[0033] packages: [costmanagement-metrics-operator]   pkg=costmanagement-metrics-operator
INFO[0033] deleting packages                             pkg=cryostat-operator
INFO[0033] packages: [cryostat-operator]                 pkg=cryostat-operator
INFO[0033] deleting packages                             pkg=datagrid
INFO[0033] packages: [datagrid]                          pkg=datagrid
INFO[0033] deleting packages                             pkg=devworkspace-operator
INFO[0033] packages: [devworkspace-operator]             pkg=devworkspace-operator
INFO[0033] deleting packages                             pkg=dpu-network-operator
INFO[0033] packages: [dpu-network-operator]              pkg=dpu-network-operator
INFO[0033] deleting packages                             pkg=eap
INFO[0033] packages: [eap]                               pkg=eap
INFO[0033] deleting packages                             pkg=elasticsearch-operator
INFO[0033] packages: [elasticsearch-operator]            pkg=elasticsearch-operator
INFO[0033] deleting packages                             pkg=external-dns-operator
INFO[0033] packages: [external-dns-operator]             pkg=external-dns-operator
INFO[0033] deleting packages                             pkg=file-integrity-operator
INFO[0033] packages: [file-integrity-operator]           pkg=file-integrity-operator
INFO[0033] deleting packages                             pkg=fuse-apicurito
INFO[0033] packages: [fuse-apicurito]                    pkg=fuse-apicurito
INFO[0033] deleting packages                             pkg=fuse-console
INFO[0033] packages: [fuse-console]                      pkg=fuse-console
INFO[0033] deleting packages                             pkg=fuse-online
INFO[0033] packages: [fuse-online]                       pkg=fuse-online
INFO[0033] deleting packages                             pkg=gatekeeper-operator-product
INFO[0033] packages: [gatekeeper-operator-product]       pkg=gatekeeper-operator-product
INFO[0033] deleting packages                             pkg=idp-mgmt-operator-product
INFO[0033] packages: [idp-mgmt-operator-product]         pkg=idp-mgmt-operator-product
INFO[0033] deleting packages                             pkg=integration-operator
INFO[0033] packages: [integration-operator]              pkg=integration-operator
INFO[0033] deleting packages                             pkg=jaeger-product
INFO[0033] packages: [jaeger-product]                    pkg=jaeger-product
INFO[0033] deleting packages                             pkg=jws-operator
INFO[0033] packages: [jws-operator]                      pkg=jws-operator
INFO[0033] deleting packages                             pkg=kiali-ossm
INFO[0033] packages: [kiali-ossm]                        pkg=kiali-ossm
INFO[0033] deleting packages                             pkg=klusterlet-product
INFO[0033] packages: [klusterlet-product]                pkg=klusterlet-product
INFO[0033] deleting packages                             pkg=kubernetes-nmstate-operator
INFO[0033] packages: [kubernetes-nmstate-operator]       pkg=kubernetes-nmstate-operator
INFO[0033] deleting packages                             pkg=kubevirt-hyperconverged
INFO[0033] packages: [kubevirt-hyperconverged]           pkg=kubevirt-hyperconverged
INFO[0034] deleting packages                             pkg=local-storage-operator
INFO[0034] packages: [local-storage-operator]            pkg=local-storage-operator
INFO[0034] deleting packages                             pkg=loki-operator
INFO[0034] packages: [loki-operator]                     pkg=loki-operator
INFO[0034] deleting packages                             pkg=mcg-operator
INFO[0034] packages: [mcg-operator]                      pkg=mcg-operator
INFO[0034] deleting packages                             pkg=metallb-operator
INFO[0034] packages: [metallb-operator]                  pkg=metallb-operator
INFO[0034] deleting packages                             pkg=mtc-operator
INFO[0034] packages: [mtc-operator]                      pkg=mtc-operator
INFO[0034] deleting packages                             pkg=mtv-operator
INFO[0034] packages: [mtv-operator]                      pkg=mtv-operator
INFO[0034] deleting packages                             pkg=nfd
INFO[0034] packages: [nfd]                               pkg=nfd
INFO[0034] deleting packages                             pkg=node-healthcheck-operator
INFO[0034] packages: [node-healthcheck-operator]         pkg=node-healthcheck-operator
INFO[0034] deleting packages                             pkg=node-maintenance-operator
INFO[0034] packages: [node-maintenance-operator]         pkg=node-maintenance-operator
INFO[0034] deleting packages                             pkg=numaresources-operator
INFO[0034] packages: [numaresources-operator]            pkg=numaresources-operator
INFO[0034] deleting packages                             pkg=ocs-operator
INFO[0034] packages: [ocs-operator]                      pkg=ocs-operator
INFO[0034] deleting packages                             pkg=odf-csi-addons-operator
INFO[0034] packages: [odf-csi-addons-operator]           pkg=odf-csi-addons-operator
INFO[0034] deleting packages                             pkg=odf-lvm-operator
INFO[0034] packages: [odf-lvm-operator]                  pkg=odf-lvm-operator
INFO[0034] deleting packages                             pkg=odf-multicluster-orchestrator
INFO[0034] packages: [odf-multicluster-orchestrator]     pkg=odf-multicluster-orchestrator
INFO[0034] deleting packages                             pkg=odf-operator
INFO[0034] packages: [odf-operator]                      pkg=odf-operator
INFO[0034] deleting packages                             pkg=odr-cluster-operator
INFO[0034] packages: [odr-cluster-operator]              pkg=odr-cluster-operator
INFO[0034] deleting packages                             pkg=odr-hub-operator
INFO[0034] packages: [odr-hub-operator]                  pkg=odr-hub-operator
INFO[0034] deleting packages                             pkg=openshift-cert-manager-operator
INFO[0034] packages: [openshift-cert-manager-operator]   pkg=openshift-cert-manager-operator
INFO[0034] deleting packages                             pkg=openshift-gitops-operator
INFO[0034] packages: [openshift-gitops-operator]         pkg=openshift-gitops-operator
INFO[0034] deleting packages                             pkg=openshift-pipelines-operator-rh
INFO[0034] packages: [openshift-pipelines-operator-rh]   pkg=openshift-pipelines-operator-rh
INFO[0034] deleting packages                             pkg=openshift-secondary-scheduler-operator
INFO[0034] packages: [openshift-secondary-scheduler-operator]  pkg=openshift-secondary-scheduler-operator
INFO[0034] deleting packages                             pkg=openshift-special-resource-operator
INFO[0034] packages: [openshift-special-resource-operator]  pkg=openshift-special-resource-operator
INFO[0034] deleting packages                             pkg=opentelemetry-product
INFO[0034] packages: [opentelemetry-product]             pkg=opentelemetry-product
INFO[0034] deleting packages                             pkg=performance-addon-operator
INFO[0034] packages: [performance-addon-operator]        pkg=performance-addon-operator
INFO[0034] deleting packages                             pkg=poison-pill-manager
INFO[0034] packages: [poison-pill-manager]               pkg=poison-pill-manager
INFO[0034] deleting packages                             pkg=ptp-operator
INFO[0034] packages: [ptp-operator]                      pkg=ptp-operator
INFO[0034] deleting packages                             pkg=quay-bridge-operator
INFO[0034] packages: [quay-bridge-operator]              pkg=quay-bridge-operator
INFO[0034] deleting packages                             pkg=quay-operator
INFO[0034] packages: [quay-operator]                     pkg=quay-operator
INFO[0034] deleting packages                             pkg=red-hat-camel-k
INFO[0034] packages: [red-hat-camel-k]                   pkg=red-hat-camel-k
INFO[0034] deleting packages                             pkg=redhat-oadp-operator
INFO[0034] packages: [redhat-oadp-operator]              pkg=redhat-oadp-operator
INFO[0034] deleting packages                             pkg=rh-service-binding-operator
INFO[0034] packages: [rh-service-binding-operator]       pkg=rh-service-binding-operator
INFO[0034] deleting packages                             pkg=rhacs-operator
INFO[0034] packages: [rhacs-operator]                    pkg=rhacs-operator
INFO[0034] deleting packages                             pkg=rhpam-kogito-operator
INFO[0034] packages: [rhpam-kogito-operator]             pkg=rhpam-kogito-operator
INFO[0035] deleting packages                             pkg=rhsso-operator
INFO[0035] packages: [rhsso-operator]                    pkg=rhsso-operator
INFO[0035] deleting packages                             pkg=sandboxed-containers-operator
INFO[0035] packages: [sandboxed-containers-operator]     pkg=sandboxed-containers-operator
INFO[0035] deleting packages                             pkg=serverless-operator
INFO[0035] packages: [serverless-operator]               pkg=serverless-operator
INFO[0035] deleting packages                             pkg=service-registry-operator
INFO[0035] packages: [service-registry-operator]         pkg=service-registry-operator
INFO[0035] deleting packages                             pkg=servicemeshoperator
INFO[0035] packages: [servicemeshoperator]               pkg=servicemeshoperator
INFO[0035] deleting packages                             pkg=skupper-operator
INFO[0035] packages: [skupper-operator]                  pkg=skupper-operator
INFO[0035] deleting packages                             pkg=sriov-network-operator
INFO[0035] packages: [sriov-network-operator]            pkg=sriov-network-operator
INFO[0035] deleting packages                             pkg=submariner
INFO[0035] packages: [submariner]                        pkg=submariner
INFO[0035] deleting packages                             pkg=tang-operator
INFO[0035] packages: [tang-operator]                     pkg=tang-operator
INFO[0035] deleting packages                             pkg=vertical-pod-autoscaler
INFO[0035] packages: [vertical-pod-autoscaler]           pkg=vertical-pod-autoscaler
INFO[0035] deleting packages                             pkg=web-terminal
INFO[0035] packages: [web-terminal]                      pkg=web-terminal
INFO[0035] deleting packages                             pkg=windows-machine-config-operator
INFO[0035] packages: [windows-machine-config-operator]   pkg=windows-machine-config-operator
INFO[0035] Generating dockerfile                         packages="[advanced-cluster-management]"
INFO[0035] writing dockerfile: ./index.Dockerfile2850553610  packages="[advanced-cluster-management]"
INFO[0035] running podman build                          packages="[advanced-cluster-management]"
INFO[0035] [podman build --format docker -f ./index.Dockerfile2850553610 -t poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/olm-index/redhat-oprator-index:v4.10 .]  packages="[advanced-cluster-management]" 

With the index created we can now push it up to our Red Hat Quay registry with the podman push command.  It should be noted that in this example we are pushing into the rhacm2 organization and that must exist before attempting the push.

$ podman push poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/olm-index/redhat-oprator-index:v4.10 --tls-verify=false
Getting image source signatures
Copying blob 0d6867937695 done  
Copying blob eeaf5a4136cb done  
Copying blob 9dc1e45bb9ee done  
Copying blob 457de0330aa6 done  
Copying blob 324075f0d95e done  
Copying blob 5b1fa8e3e100 done  
Copying config f6bfd86300 done  
Writing manifest to image destination
Storing signatures

Once we have pushed the index up we can then use the oc adm catalog mirror command to mirror the images:

$ oc adm catalog mirror poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/olm-index/redhat-oprator-index:v4.10 poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2 -a /home/bschmaus/quay-merged-pull-secret.json --insecure

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! DEPRECATION NOTICE:
!!   Sqlite-based catalogs are deprecated. Support for them will be removed in a
!!   future release. Please migrate your catalog workflows to the new file-based
!!   catalog format.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

src image has index label for database path: /database/index.db
using index path mapping: /database/index.db:/tmp/3687994887
wrote database to /tmp/3687994887
using database at: /tmp/3687994887/index.db
poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/
  rhacm2/openshift4-ose-configmap-reloader
    blobs:
      registry.redhat.io/openshift4/ose-configmap-reloader sha256:b77bb434db5a2c43574630adfbe80aa3b36c179ccc20541ae91e2812a3ad9ce2 1.461KiB
      registry.redhat.io/openshift4/ose-configmap-reloader sha256:d8dcf8c7f6920565fec6db5c1479312aad177148dadd89e07f838cd7f44fa074 1.474KiB
      registry.redhat.io/openshift4/ose-configmap-reloader sha256:7d2d8330490119f01d1087adb98a324b3292f4711436cc4f64a8d9cb081fc345 1.479KiB
(...)
uploading: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:cf7ba91a4dc5dbc52d2fd9c09127be351a3d5292f1f2edf5171d2fe79f573850 18.93MiB
uploading: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:83b47d1b1652022425cbee522218c3758b8c59b052d5e845d5f8d897e31609a7 18.93MiB
mounted: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:2a99c93da16827d9a6254f86f495d2c72c62a916f9c398577577221d35d2c790 37.81MiB
mounted: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:d46336f50433ab27336fad8f9b251b2f68a66d376c902dfca23a6851acae502c 37.47MiB
mounted: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:0016483f9a1476d5d57b7a871ed4d3994ba802c643f53f6037d2b28f799f963f 35.94MiB
uploading: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:59f461f3c96d9c3b9d77d9de5fc4df45e4f6965264b2d047d7474d130ca8f9b4 18.93MiB
mounted: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:a9e23b64ace00a199db21d302292b434e9d3956d79319d958ecc19603d00c946 37.79MiB
uploading: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:30ee6976ea1e5387884dd0299fd122b76ec6e1d4fdad01d01302997f2d461edc 18.63MiB
uploading: poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-klusterlet-addon-controller-rhel8 sha256:c4e367c519079053c4297b06640529659c996e21823f2c580e533468b26a2de7 19.07MiB
sha256:acedcbb6483e2b6b51f69900de4f582f48a486114ef6ecaede82f1f549fb4ebf poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-acm-must-gather-rhel8
(...)
sha256:bcf26708e40297fcc5c09657aa540408930e51e7319ec7612fb5529936746cc0 poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-search-api-rhel8:2290f420
sha256:fcfbd48e615e46fe5d33e3059aedbc2a75f4f10489dbe91fa072610dbbe86130 poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-search-api-rhel8:4ad02099
sha256:601f4d74ece9da8888488d303e05220dc9cd9796b07a95cf2001d3a42198b8de poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-search-api-rhel8:2e28486f
sha256:623281445ffba2f86d4d2708e9aaa84b566a461e9bf29f703589cdb27492dcd3 poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/rhacm2-search-api-rhel8:7fa459f6
info: Mirroring completed in 16m46.55s (44.49MB/s)
no digest mapping available for poc-registry-quay-quay-poc.apps.kni20.schmaustech.com/rhacm2/olm-index/redhat-operator-index:v4.10, skip writing to ImageContentSourcePolicy
wrote mirroring manifests to manifests-olm-index/redhat-operator-index-1651068403
deleted dir /tmp/2002258786

This concludes the demonstration of how to mirror specific operators down to ones own instance of Red Hat Quay.

Sunday, February 06, 2022

Enabling vGPU in OpenShift Containerized Virtualization

There is a lot of discussion about using GPUs for AI/ML workloads and while some of those workloads run in containers there are still some use cases where those workloads run in virtual machines.   In OpenShift when using Containerized virtualization one can run virtual machines and use a PCI passhthrough configuration to pass up one of the GPUs into the virtual machine.  This is clearly defined in the documentation here.  However there are some cases where the entire GPU is not needed by the virtual machine and so rather then have wasted cycles we can pass a slice of the GPU into the virtual machine as a vGPU.   In this blog I will demonstrate how to configure and pass up a virtual GPU into a virtual Linux machine.

Before we begin lets make a few assumptions about what has already been configured.   We assume that we have a working OpenShift 4.9 cluster, could be a full cluster, a compact cluster or in my case just a single node cluster (SNO).   We also can assume that Containerized virtualization and Node Feature Discovery operator has been installed via OperatorHub.


Now that we have the basic assumptions out of the way lets begin the process of enabling virtual GPUs.   The very first step is to label the nodes that have a GPU installed:

$ oc get nodes
NAME    STATUS   ROLES           AGE   VERSION
sno2    Ready    master,worker   1d   v1.22.3+e790d7f

$ oc label nodes sno2 hasGpu=true
node/sno2 labeled

With the labeling of the node which will be used later when we deploy the driver we can now create the MachineConfig to enable the IOMMU: 

$ cat << EOF > ~/100-master-kernel-arg-iommu.yaml
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
    machineconfiguration.openshift.io/role: master 
  name: 100-master-iommu 
spec:
  config:
    ignition:
      version: 3.2.0
  kernelArguments:
      - intel_iommu=on
EOF

With the MachineConfig created lets go ahead and apply it to the cluster:

$ oc create -f ~/100-master-kernel-arg-iommu.yaml
machineconfig.machineconfiguration.openshift.io/100-master-iommu created

Wait for the nodes where the machine config is applied to reboot.  Once the nodes have rebooted we can continue onto the next step.

Once the nodes have rebooted we can verify the MachineConfig was applied by running the following:

$ oc get MachineConfig 100-master-iommu
NAME               GENERATEDBYCONTROLLER   IGNITIONVERSION   AGE
100-master-iommu                           3.2.0             6m25s

Now lets go ahead and build the driver container that will apply the NVIDIA driver to the worker nodes that have GPUs in them.   I should note that in order to proceed the NVIDIA GRID drivers need to be obtained from NVIDIA here.  I will be using the following driver in this example to build my container: NVIDIA-Linux-x86_64-470.63-vgpu-kvm.run.  The first step we need to do is determine the driver-toolkit release image our current cluster is using.  We can find that by running the following command:

$ oc adm release info --image-for=driver-toolkit
quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:ce897bc72101dacc82aa593974fa0d8a421a43227b540fbcf1e303ffb1d3f1ea

Next we will take that release image and place it into a Dockerfile in a directory called vgpu: 

$ cat << EOF > ~/vgpu/Dockerfile
FROM quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:ce897bc72101dacc82aa593974fa0d8a421a43227b540fbcf1e303ffb1d3f1ea
ARG NVIDIA_INSTALLER_BINARY
ENV NVIDIA_INSTALLER_BINARY=${NVIDIA_INSTALLER_BINARY:-NVIDIA-Linux-x86_64-470.63-vgpu-kvm.run}

RUN dnf -y install git make sudo gcc \
&& dnf clean all \
&& rm -rf /var/cache/dnf

RUN mkdir -p /root/nvidia
WORKDIR /root/nvidia
ADD ${NVIDIA_INSTALLER_BINARY} .
RUN chmod +x /root/nvidia/${NVIDIA_INSTALLER_BINARY}
ADD entrypoint.sh .
RUN chmod +x /root/nvidia/entrypoint.sh

RUN mkdir -p /root/tmp
EOF

Next create the following entrypoint.sh script and place that in the vgpu directory as well:

$ cat << EOF > ~/vgpu/entrypoint.sh
#!/bin/sh
/usr/sbin/rmmod nvidia
/root/nvidia/${NVIDIA_INSTALLER_BINARY} --kernel-source-path=/usr/src/kernels/$(uname -r) --kernel-install-path=/lib/modules/$(uname -r)/kernel/drivers/video/ --silent --tmpdir /root/tmp/ --no-systemd

/usr/bin/nvidia-vgpud &
/usr/bin/nvidia-vgpu-mgr &

while true; do sleep 15 ; /usr/bin/pgrep nvidia-vgpu-mgr ; if [ $? -ne 0 ] ; then echo "nvidia-vgpu-mgr is not running" && exit 1; fi; done
EOF

Also place the NVIDIA-Linux-x86_64-470.63-vgpu-kvm.run in to the vgpu directory.   Then you should have the following:

$ ls
Dockerfile  entrypoint.sh  NVIDIA-Linux-x86_64-470.63-vgpu-kvm.run


At this point change directory into vgpu and then use the podman build command to build the driver container local:

$ cd ~/vgpu
$ podman build --build-arg NVIDIA_INSTALLER_BINARY=NVIDIA-Linux-x86_64-470.63-vgpu-kvm.run -t ocp-nvidia-vgpu-installer .
STEP 1/11: FROM quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:ce897bc72101dacc82aa593974fa0d8a421a43227b540fbcf1e303ffb1d3f1ea
STEP 2/11: ARG NVIDIA_INSTALLER_BINARY
--> Using cache 13aa17a1fd44bb7afea0a1b884b7005aaa51091e47dfe14987b572db9efab1f2
--> 13aa17a1fd4
STEP 3/11: ENV NVIDIA_INSTALLER_BINARY=${NVIDIA_INSTALLER_BINARY:-NVIDIA-Linux-x86_64-470.63-vgpu-kvm.run}
--> Using cache e818b281ad40c0e78ef4c01a71d73b45b509392100262fddbf542c457d697255
--> e818b281ad4
STEP 4/11: RUN dnf -y install git make sudo gcc && dnf clean all && rm -rf /var/cache/dnf
--> Using cache d6f3687a545589cf096353ad792fb464a6961ff204c49234ced26d996da9f1c8
--> d6f3687a545
STEP 5/11: RUN mkdir -p /root/nvidia
--> Using cache 708b464de69de2443edb5609623478945af6f9498d73bf4d47c577e29811a414
--> 708b464de69
STEP 6/11: WORKDIR /root/nvidia
--> Using cache 6cb724eeb99d21a30f50a3c25954426d4719af84ef43bda7ab0aeab6e7da81a8
--> 6cb724eeb99
STEP 7/11: ADD ${NVIDIA_INSTALLER_BINARY} .
--> Using cache 71dd0491be7e3c20a742cd50efe26f54a5e2f61d4aa8846cd5d7ccd82f27ab45
--> 71dd0491be7
STEP 8/11: RUN chmod +x /root/nvidia/${NVIDIA_INSTALLER_BINARY}
--> Using cache 85d64dc8b702936412fa121aaab3733a60f880aa211e0197f1c8853ddbb617b5
--> 85d64dc8b70
STEP 9/11: ADD entrypoint.sh .
--> Using cache 9d49c87387f926ec39162c5e1c2a7866c1494c1ab8f3912c53ea6eaefe0be254
--> 9d49c87387f
STEP 10/11: RUN chmod +x /root/nvidia/entrypoint.sh
--> Using cache 79d682f8471fc97a60b6507d2cff164b3b9283a1e078d4ddb9f8138741c033b5
--> 79d682f8471
STEP 11/11: RUN mkdir -p /root/tmp
--> Using cache bcbb311e35999cb6c55987049033c5d278ee93d76a97fe9203ce68257a9f8ebd
COMMIT ocp-nvidia-vgpu-installer
--> bcbb311e359
Successfully tagged localhost/ocp-nvidia-vgpu-installer:latest
Successfully tagged localhost/ocp-nvidia-vgpu-nstaller:latest
Successfully tagged quay.io/bschmaus/ocp-nvidia-vgpu-nstaller:latest
bcbb311e35999cb6c55987049033c5d278ee93d76a97fe9203ce68257a9f8ebd

Once the container is build push it to a private repository that is only accessible by the organization that purchased the NVIDIA GRID license.  It is not legal to freely distribute the driver image. 

$ podman push quay.io/bschmaus/ocp-nvidia-vgpu-nstaller:latest
Getting image source signatures
Copying blob b0b1274fc88c done  
Copying blob 525ed45dbdb1 done  
Copying blob 8aa226ded434 done  
Copying blob d9ad9932e964 done  
Copying blob 5bc03dec6239 done  
Copying blob ab10e1e28fa3 done  
Copying blob 4eff86c961b3 done  
Copying blob e1790381e6f7 done  
Copying blob a8701ba769cc done  
Copying blob 38a3912b1d62 done  
Copying blob 257db9f06185 done  
Copying blob 09fd8acd3579 done  
Copying config bcbb311e35 done  
Writing manifest to image destination
Copying config bcbb311e35 [--------------------------------------] 0.0b / 5.6KiB
Writing manifest to image destination
Storing signatures


Now that we have a driver image lets create a custom resource that will use and apply that driver image to the cluster nodes that have the label hasGPU via a deamonset.  The file will look similar below but will need the container image path to be updated to fit ones environment.

$ cat << EOF > ~/1000-drivercontainer.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: simple-kmod-driver-container
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: simple-kmod-driver-container
rules:
- apiGroups:
  - security.openshift.io
  resources:
  - securitycontextconstraints
  verbs:
  - use
  resourceNames:
  - privileged
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: simple-kmod-driver-container
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: simple-kmod-driver-container
subjects:
- kind: ServiceAccount
  name: simple-kmod-driver-container
userNames:
- system:serviceaccount:simple-kmod-demo:simple-kmod-driver-container
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: simple-kmod-driver-container
spec:
  selector:
    matchLabels:
      app: simple-kmod-driver-container
  template:
    metadata:
      labels:
        app: simple-kmod-driver-container
    spec:
      serviceAccount: simple-kmod-driver-container
      serviceAccountName: simple-kmod-driver-container
      hostPID: true
      hostIPC: true
      containers:
      - image: quay.io/bschmaus/ocp-nvidia-vgpu-nstaller:latest
        name: simple-kmod-driver-container
        imagePullPolicy: Always
        command: ["/root/nvidia/entrypoint.sh"]
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "systemctl stop kmods-via-containers@simple-kmod"]
        securityContext:
          privileged: true

          allowedCapabilities:
          - '*'
          capabilities:
            add: ["SYS_ADMIN"]
        volumeMounts:
        - mountPath: /dev/vfio/
          name: vfio
        - mountPath: /sys/fs/cgroup
          name: cgroup
      volumes:
      - hostPath:
          path: /sys/fs/cgroup
          type: Directory
        name: cgroup
      - hostPath:
          path: /dev/vfio/
          type: Directory
        name: vfio
      nodeSelector:
        hasGpu: "true"
EOF

Now that we have our custom resource driver yaml lets apply it to the cluster:

$ oc create -f 1000-drivercontainer.yaml
serviceaccount/simple-kmod-driver-container created
role.rbac.authorization.k8s.io/simple-kmod-driver-container created
rolebinding.rbac.authorization.k8s.io/simple-kmod-driver-container created
daemonset.apps/simple-kmod-driver-container created

We can validate the daemonset is running by looking at the daemonsets under openshift-nfd:

$ oc get daemonset simple-kmod-driver-container 
NAME                           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
simple-kmod-driver-container   1         1         1       1            1           hasGpu=true     9m23s

Now lets further validate by logging into the worker node as the core user, sudo up to root and then list out the loaded kernel modules.  We should see the NVIDIA drivers loaded:

# sudo bash
# lsmod| grep nvi
nvidia_vgpu_vfio       65536  0
nvidia              35274752  10 nvidia_vgpu_vfio
mdev                   20480  2 vfio_mdev,nvidia_vgpu_vfio
vfio                   36864  3 vfio_mdev,nvidia_vgpu_vfio,vfio_iommu_type1
drm                   569344  4 drm_kms_helper,nvidia,mgag200

Once we have confirmed the NVIDIA drivers are loaded lets enumerate through the possible mdev_type devices for our GPU card.   Using the commands below we can show what the different options are for carving up the GPU card from a vGPU perspective.  In our example below we have a variety of ways we could use this card.  However it should be noted that only one nvidia-(n) device can be used.  That is if we choose nvidia-22 and carve up each GPU into a single vGPU then we end up with one vGPU per physical GPU on the card.   As another example if we chose nvidia-15 we would then end up with 8 vGPUs per physical GPU on the card.

# for device in /sys/class/mdev_bus/*; do for mdev_type in "$device"/mdev_supported_types/*; do     MDEV_TYPE=$(basename $mdev_type);     DESCRIPTION=$(cat $mdev_type/description);     NAME=$(cat $mdev_type/name); echo "mdev_type: $MDEV_TYPE --- description: $DESCRIPTION --- name: $NAME";   done; done | sort | uniq
mdev_type: nvidia-11 --- description: num_heads=2, frl_config=45, framebuffer=512M, max_resolution=2560x1600, max_instance=16 --- name: GRID M60-0B
mdev_type: nvidia-12 --- description: num_heads=2, frl_config=60, framebuffer=512M, max_resolution=2560x1600, max_instance=16 --- name: GRID M60-0Q
mdev_type: nvidia-13 --- description: num_heads=1, frl_config=60, framebuffer=1024M, max_resolution=1280x1024, max_instance=8 --- name: GRID M60-1A
mdev_type: nvidia-14 --- description: num_heads=4, frl_config=45, framebuffer=1024M, max_resolution=5120x2880, max_instance=8 --- name: GRID M60-1B
mdev_type: nvidia-15 --- description: num_heads=4, frl_config=60, framebuffer=1024M, max_resolution=5120x2880, max_instance=8 --- name: GRID M60-1Q
mdev_type: nvidia-16 --- description: num_heads=1, frl_config=60, framebuffer=2048M, max_resolution=1280x1024, max_instance=4 --- name: GRID M60-2A
mdev_type: nvidia-17 --- description: num_heads=4, frl_config=45, framebuffer=2048M, max_resolution=5120x2880, max_instance=4 --- name: GRID M60-2B
mdev_type: nvidia-18 --- description: num_heads=4, frl_config=60, framebuffer=2048M, max_resolution=5120x2880, max_instance=4 --- name: GRID M60-2Q
mdev_type: nvidia-19 --- description: num_heads=1, frl_config=60, framebuffer=4096M, max_resolution=1280x1024, max_instance=2 --- name: GRID M60-4A
mdev_type: nvidia-20 --- description: num_heads=4, frl_config=60, framebuffer=4096M, max_resolution=5120x2880, max_instance=2 --- name: GRID M60-4Q
mdev_type: nvidia-210 --- description: num_heads=4, frl_config=45, framebuffer=2048M, max_resolution=5120x2880, max_instance=4 --- name: GRID M60-2B4
mdev_type: nvidia-21 --- description: num_heads=1, frl_config=60, framebuffer=8192M, max_resolution=1280x1024, max_instance=1 --- name: GRID M60-8A
mdev_type: nvidia-22 --- description: num_heads=4, frl_config=60, framebuffer=8192M, max_resolution=5120x2880, max_instance=1 --- name: GRID M60-8Q
mdev_type: nvidia-238 --- description: num_heads=4, frl_config=45, framebuffer=1024M, max_resolution=5120x2880, max_instance=8 --- name: GRID M60-1B4

In my example I am going to go ahead and use nvidia-22 and only pass one vGPU per physical GPU.  To do this we need to echo a unique uuid number into the following device path create file.   I will do this twice once for each physical GPU device.  Note that this can only be done once.  If attempted more then once an IO error will result.

# echo `uuidgen` > /sys/class/mdev_bus/0000:3e:00.0/mdev_supported_types/nvidia-22/create
# echo `uuidgen` > /sys/class/mdev_bus/0000:3d:00.0/mdev_supported_types/nvidia-22/create

Now that we have created our vGPU devices we next need to expose those devices to Containerized virtualization so they can then be consumed by a virtual machine.  To do this we need to patch the kubevirt-hyperconverged configuration.  So first lets create the patch file:

$ cat << EOF > ~/kubevirt-hyperconverged-patch.yaml
spec:
    permittedHostDevices:
      mediatedDevices:
      - mdevNameSelector: "GRID M60-8Q"
        resourceName: "nvidia.com/GRID_M60_8Q"
EOF

With the patch file created we next need to merge it with the existing kubevirt-hyperconverged configuration using the oc patch command:

$ oc patch hyperconverged kubevirt-hyperconverged -n openshift-cnv --patch "$(cat ~/kubevirt-hyperconverged-patch.yaml)" --type=merge
hyperconverged.hco.kubevirt.io/kubevirt-hyperconverged patched

Once applied wait a few minutes for the configuration to be reloaded.  Then to validate it run the oc describe node command against the node and look for the GPU devices under Capacity and Allocatable.   In our example we see two devices because we had 2 physical GPUs and we created a vGPU using nvidia-22 which allows for one vGPU per physical GPU.

$ oc describe node| sed '/Capacity/,/System/!d;/System/d'
Capacity:
  cpu:                            24
  devices.kubevirt.io/kvm:        1k
  devices.kubevirt.io/tun:        1k
  devices.kubevirt.io/vhost-net:  1k
  ephemeral-storage:              936104940Ki
  hugepages-1Gi:                  0
  hugepages-2Mi:                  0
  memory:                         131561680Ki
  nvidia.com/GRID_M60_8Q:         2
  pods:                           250
Allocatable:
  cpu:                            23500m
  devices.kubevirt.io/kvm:        1k
  devices.kubevirt.io/tun:        1k
  devices.kubevirt.io/vhost-net:  1k
  ephemeral-storage:              862714311276
  hugepages-1Gi:                  0
  hugepages-2Mi:                  0
  memory:                         130410704Ki
  nvidia.com/GRID_M60_8Q:         2
  pods:                           250

At this point VMs can now be deployed and consume the available vGPUs on the node.  To do this we need to create a VM resource configuration file like the example below.  Notice that we define in this file host devices and we pass in the NVIDIA host device of the vGPU name:

$ cat << EOF > ~/fedora-vm.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  annotations:
    kubemacpool.io/transaction-timestamp: '2022-02-09T17:23:53.76596817Z'
    kubevirt.io/latest-observed-api-version: v1
    kubevirt.io/storage-observed-api-version: v1alpha3
    name.os.template.kubevirt.io/fedora34: Fedora 33 or higher
    vm.kubevirt.io/validations: |
      [
        {
          "name": "minimal-required-memory",
          "path": "jsonpath::.spec.domain.resources.requests.memory",
          "rule": "integer",
          "message": "This VM requires more memory.",
          "min": 1073741824
        }
      ]
  resourceVersion: '19096098'
  name: fedora
  uid: 48bf787d-9240-444c-92fd-f0e5ce0ced23
  creationTimestamp: '2022-02-09T16:48:54Z'
  generation: 3
  managedFields:
    - apiVersion: kubevirt.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        'f:metadata':
          'f:annotations':
            .: {}
            'f:name.os.template.kubevirt.io/fedora34': {}
            'f:vm.kubevirt.io/validations': {}
          'f:labels':
            .: {}
            'f:app': {}
            'f:os.template.kubevirt.io/fedora34': {}
            'f:vm.kubevirt.io/template': {}
            'f:vm.kubevirt.io/template.namespace': {}
            'f:vm.kubevirt.io/template.revision': {}
            'f:vm.kubevirt.io/template.version': {}
            'f:workload.template.kubevirt.io/server': {}
        'f:spec':
          .: {}
          'f:dataVolumeTemplates': {}
          'f:template':
            .: {}
            'f:metadata':
              .: {}
              'f:annotations': {}
              'f:labels': {}
            'f:spec':
              .: {}
              'f:domain':
                .: {}
                'f:cpu':
                  .: {}
                  'f:cores': {}
                  'f:sockets': {}
                  'f:threads': {}
                'f:devices':
                  .: {}
                  'f:disks': {}
                  'f:interfaces': {}
                  'f:networkInterfaceMultiqueue': {}
                  'f:rng': {}
                'f:machine':
                  .: {}
                  'f:type': {}
                'f:resources':
                  .: {}
                  'f:requests':
                    .: {}
                    'f:memory': {}
              'f:evictionStrategy': {}
              'f:hostname': {}
              'f:networks': {}
              'f:terminationGracePeriodSeconds': {}
              'f:volumes': {}
      manager: Mozilla
      operation: Update
      time: '2022-02-09T16:48:54Z'
    - apiVersion: kubevirt.io/v1alpha3
      fieldsType: FieldsV1
      fieldsV1:
        'f:status':
          'f:conditions': {}
          'f:printableStatus': {}
      manager: Go-http-client
      operation: Update
      subresource: status
      time: '2022-02-09T17:23:53Z'
  namespace: openshift-nfd
  labels:
    app: fedora
    os.template.kubevirt.io/fedora34: 'true'
    vm.kubevirt.io/template: fedora-server-large
    vm.kubevirt.io/template.namespace: openshift
    vm.kubevirt.io/template.revision: '1'
    vm.kubevirt.io/template.version: v0.16.4
    workload.template.kubevirt.io/server: 'true'
spec:
  dataVolumeTemplates:
    - metadata:
        creationTimestamp: null
        name: fedora-rootdisk-uqf5j
      spec:
        pvc:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 40Gi
          storageClassName: hostpath-provisioner
          volumeMode: Filesystem
        source:
          http:
            url: >-
              https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-34-1.2.x86_64.raw.xz
  running: true
  template:
    metadata:
      annotations:
        vm.kubevirt.io/flavor: large
        vm.kubevirt.io/os: fedora
        vm.kubevirt.io/workload: server
      creationTimestamp: null
      labels:
        kubevirt.io/domain: fedora
        kubevirt.io/size: large
        os.template.kubevirt.io/fedora34: 'true'
        vm.kubevirt.io/name: fedora
        workload.template.kubevirt.io/server: 'true'
    spec:
      domain:
        cpu:
          cores: 12
          sockets: 1
          threads: 1
        devices:
          disks:
            - disk:
                bus: virtio
              name: cloudinitdisk
            - bootOrder: 1
              disk:
                bus: virtio
              name: rootdisk
          hostDevices:
            - deviceName: nvidia.com/GRID_M60_8Q
              name: GRID_M60_8Q
          interfaces:
            - macAddress: '02:01:53:00:00:00'
              masquerade: {}
              model: virtio
              name: default
          networkInterfaceMultiqueue: true
          rng: {}
        machine:
          type: pc-q35-rhel8.4.0
        resources:
          requests:
            memory: 32Gi
      evictionStrategy: LiveMigrate
      hostname: fedora
      networks:
        - name: default
          pod: {}
      terminationGracePeriodSeconds: 180
      volumes:
        - cloudInitNoCloud:
            userData: |
              #cloud-config
              user: fedora
              password: password
              chpasswd:
                expire: false
              ssh_authorized_keys:
                - >-
                  ssh-rsa
                  SSH-KEY-HERE
          name: cloudinitdisk
        - dataVolume:
            name: fedora-rootdisk-uqf5j
          name: rootdisk
status:
  conditions:
    - lastProbeTime: '2022-02-09T17:24:09Z'
      lastTransitionTime: '2022-02-09T17:24:09Z'
      message: VMI does not exist
      reason: VMINotExists
      status: 'False'
      type: Ready
  printableStatus: Stopped
  volumeSnapshotStatuses:
    - enabled: false
      name: cloudinitdisk
      reason: 'Snapshot is not supported for this volumeSource type [cloudinitdisk]'
    - enabled: false
      name: rootdisk
      reason: >-
        No VolumeSnapshotClass: Volume snapshots are not configured for this
        StorageClass [hostpath-provisioner] [rootdisk]
EOF

Lets go ahead and create the virtual machine: 

$ oc create -f ~/fedora-vm.yaml
virtualmachine.kubevirt.io/fedora created

Wait a few moments for the virtual machine to get to a running state.   We can confirm its running by doing oc get vms:

$ oc get vms
NAME              AGE     STATUS    READY
fedora            8m17s   Running   True

Now lets expose the running virtual machines ssh port so we can ssh into it by using the virtctl command:

$ virtctl expose vmi fedora --port=22 --name=fedora-ssh --type=NodePort
Service fedora-ssh successfully exposed for vmi fedora

We can confirm the ssh port is exposed and get the port number it uses by running the oc get svc command:

$ oc get svc
NAME                                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)           AGE
fedora-ssh                               NodePort    172.30.220.248   none          22:30106/TCP      7s

Now lets ssh into the fedora virtual machine and become root:

$ ssh fedora@10.11.176.230 -p 30106
The authenticity of host '[10.11.176.230]:30106 ([10.11.176.230]:30106)' can't be established.
ECDSA key fingerprint is SHA256:Zmpcpm8vgQc3Oa72RFL0iKU/OPjHshAbHyGO7Smk8oE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[10.11.176.230]:30106' (ECDSA) to the list of known hosts.
Last login: Wed Feb  9 18:41:14 2022
[fedora@fedora ~]$ sudo bash
[root@fedora fedora]#

Once at a root prompt we can execute lspci and see the NVIDIA vGPU we passed to the virtual machine is listed as a device:

[root@fedora fedora]# lspci|grep NVIDIA
06:00.0 VGA compatible controller: NVIDIA Corporation GM204GL [Tesla M60] (rev a1)

At this point all that one would need to do is install the NVIDIA drivers on the virtual machine and then fire up their favorite application that would take advantage of the vGPU in the virtual machine!

Tuesday, January 11, 2022

Adding VmWare Worker Node to OpenShift Cluster the BareMetal IPI Way

 


In a previous blog I discussed how one could provide Intelligent Platform Management Interface (IPMI) capabilities to a VmWare virtual machine.  I also eluded to being able to deploy OpenShift Baremetal IPI on VmWare virtual machines given the IPMI requirement was met for the purpose of a non production lab scenario.   However since I do not have enough lab equipment to run a full blown VmWare ESXi with enough virtual machines to mimic an OpenShift Baremetal IPI deployment, I will do the next best thing and demonstrate how to add a VmWare virtual machine acting as an OpenShift worker using the scale up capability.

Before we get started though lets review the lab setup for this exercise.   The diagram below shows that we have a 3 master cluster on a RHEL KVM hypervisor node.  These nodes while virtual are using VBMC to enable IPMI and hence the cluster was deployed as a OpenShift Baremetal IPI cluster.   We have an additional worker we would like to add that resides on an ESXi hypervisor host.   Using the virtualbmcforvsphere container (discussed in a previous blog) we can mimic IPMI for that worker node and thus treat it like a baremetal node.

Now that we have an understanding of the lab layout lets get to adding the additional VmWare worker node to our cluster.   The first step is to create the vmware-bmh.yaml which will contain the secret information for the IPMI credentials base64 encoded and the baremetal host information:

$ cat << EOF > ~/vmware-bmh.yaml
---
apiVersion: v1
kind: Secret
metadata:
  name: worker-4-bmc-secret
type: Opaque
data:
  username: YWRtaW4=
  password: cGFzc3dvcmQ=
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
  name: worker-4
spec:
  online: true
  bootMACAddress: 00:50:56:83:da:a1
  bmc:
    address: ipmi://192.168.0.10:6801
    credentialsName: worker-4-bmc-secret
EOF

Once we have created the vmware-bmh.yaml file we can go ahead and create the resources with the oc command below:

$ oc create -f vmware-bmh.yaml -n openshift-machine-api
secret/worker-4-bmc-secret created
baremetalhost.metal3.io/worker-4 created	

Once the command is executed this will kick off the process of registering the node in ironic, turning the node on via IPMI and then inspecting the node to determine its resource properties.  The video below will show what is happening on the console of the worker node during this process:


Besides watching from the console, we can also run some oc commands to see the status of the worker node during this process as well:

$ oc get baremetalhosts -n openshift-machine-api
NAME       STATE                    CONSUMER               ONLINE   ERROR
master-0   externally provisioned   kni20-cmq65-master-0   true     
master-1   externally provisioned   kni20-cmq65-master-1   true     
master-2   externally provisioned   kni20-cmq65-master-2   true     
worker-4   registering                                     true 
$ oc get baremetalhosts -n openshift-machine-api
NAME       STATE                    CONSUMER               ONLINE   ERROR
master-0   externally provisioned   kni20-cmq65-master-0   true     
master-1   externally provisioned   kni20-cmq65-master-1   true     
master-2   externally provisioned   kni20-cmq65-master-2   true     
worker-4   inspecting                                      true     

$ oc get baremetalhosts -n openshift-machine-api
NAME       STATE                    CONSUMER               ONLINE   ERROR
master-0   externally provisioned   kni20-cmq65-master-0   true     
master-1   externally provisioned   kni20-cmq65-master-1   true     
master-2   externally provisioned   kni20-cmq65-master-2   true     
worker-4   match profile                                   true     

$ oc get baremetalhosts -n openshift-machine-api
NAME       STATE                    CONSUMER               ONLINE   ERROR
master-0   externally provisioned   kni20-cmq65-master-0   true     
master-1   externally provisioned   kni20-cmq65-master-1   true     
master-2   externally provisioned   kni20-cmq65-master-2   true     
worker-4   ready                                           true  

Once the process is complete the new worker node will be marked ready and left powered on.  Now we can move onto scaling up the cluster.   To do this we first need to find the name of the machineset which in this case is kni20-cmq65-worker-0.  With that information we can then scale up the node count from 0 to 1 and this will trigger the provisioning process:

$ oc -n openshift-machine-api get machineset
NAME                   DESIRED   CURRENT   READY   AVAILABLE   AGE
kni20-cmq65-worker-0   0         0                             17h

$ oc -n openshift-machine-api scale machineset kni20-cmq65-worker-0 --replicas=1
machineset.machine.openshift.io/kni20-cmq65-worker-0 scaled

The video below will show what happens during the scaling process from the worker nodes console point of view.  In summary what will happen is the node will turn on, an RHCOS image will get written, the node will reboot, the ostree will get updated, the node will reboot again and finally the services to enable the node to join the cluster will start:


Besides watching from the console of the worker node we can also following along at the cli with the oc command to show the state of the worker node:

$ oc get baremetalhosts -n openshift-machine-api
NAME       STATE                    CONSUMER                     ONLINE   ERROR
master-0   externally provisioned   kni20-cmq65-master-0         true     
master-1   externally provisioned   kni20-cmq65-master-1         true     
master-2   externally provisioned   kni20-cmq65-master-2         true     
worker-4   provisioning             kni20-cmq65-worker-0-lhd92   true 

And again using the oc command we can see the worker node has been provisioned:

$ oc get baremetalhosts -n openshift-machine-api
NAME       STATE                    CONSUMER                     ONLINE   ERROR
master-0   externally provisioned   kni20-cmq65-master-0         true     
master-1   externally provisioned   kni20-cmq65-master-1         true     
master-2   externally provisioned   kni20-cmq65-master-2         true     
worker-4   provisioned              kni20-cmq65-worker-0-lhd92   true 

Once the worker node has shown provisioned and the node has rebooted the second time, we can then follow the status of the worker node with the oc get nodes command: 

$ oc get nodes
NAME                             STATUS     ROLES           AGE   VERSION
master-0.kni20.schmaustech.com   Ready      master,worker   17h   v1.22.0-rc.0+a44d0f0
master-1.kni20.schmaustech.com   Ready      master,worker   17h   v1.22.0-rc.0+a44d0f0
master-2.kni20.schmaustech.com   Ready      master,worker   17h   v1.22.0-rc.0+a44d0f0
worker-4.kni20.schmaustech.com   NotReady   worker          39s   v1.22.0-rc.0+a44d0f0

Finally after the scaling process is completed and the worker node should display that it is ready and joined to the cluster:

$ oc get nodes
NAME                             STATUS   ROLES           AGE   VERSION
master-0.kni20.schmaustech.com   Ready    master,worker   17h   v1.22.0-rc.0+a44d0f0
master-1.kni20.schmaustech.com   Ready    master,worker   17h   v1.22.0-rc.0+a44d0f0
master-2.kni20.schmaustech.com   Ready    master,worker   17h   v1.22.0-rc.0+a44d0f0
worker-4.kni20.schmaustech.com   Ready    worker          58s   v1.22.0-rc.0+a44d0f0

Hopefully this provides a good example of how to use VmWare virtual machines to simulate baremetal nodes for OpenShift IPI deployments.

Thursday, January 06, 2022

BareMetal IPI OpenShift Lab on VmWare?

 

I see a lot of customers asking about being able to deploy an OpenShift Baremetal IPI lab or proof of concepts in VmWare.  Many want to do it to try out the deployment method without having to invest in the physical hardware.   The problem faced with VmWare is the lack of an Intelligent Platform Management Interface (IPMI) for the virtual machines.   I am not knocking VmWare either in this case because they do offer a robust API via Vcenter that lets one do quite a bit via scripting for automation.  However the OpenShift Baremetal IPI install process requires IPMI or RedFish which are standards on server hardware.  There does exist though a project that can possibly fill this gap though but it should only be used for labs and proof of concepts not production.

The project that solves this issue is called virtualbmc-for-vsphere.  If the name virtualbmc sounds familiar its because that project was originally designed to provide IPMI to KVM virtual machines.  However this forked version of virtualbmc-for-vsphere uses the same concepts to provide an IPMI interface for VmWare virtual machines.  Only the code knows how to talk to Vcenter to power on/of and set bootdevices  of the virtual machines.  Here are some example of what IPMI commands are supported:

# Power the virtual machine on, off, graceful off, reset, and NMI. Note that NMI is currently experimental
ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 power on|off|soft|reset|diag

# Check the power status
ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 power status

# Set the boot device to network, disk or cdrom
ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 chassis bootdev pxe|disk|cdrom

# Get the current boot device
ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 chassis bootparam get 5

# Get the channel info. Note that its output is always a dummy, not actual information.
ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 channel info

# Get the network info. Note that its output is always a dummy, not actual information.
ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 lan print 1

From the commands above it looks like we get all the bits that are required from an IPMI standpoint when doing a OpenShift BareMetal IPI deployment.  

Before I proceed to show how to setup virtualbmc-for-vsphere lets quick look at our test virtual machine within Vcenter (vcenter.schmaustech.com).   From the picture below we can see that there is a virtual machine called rheltest which is currently powered on and has an ipaddress of 192.168.0.226.  Once we get virtualbmc-for-vsphere configured we will use IPMI commands to power down the host and then power it back up.


Now that we have familiarized ourself with the VmWare environment lets take a moment to setup virtualbmc-for-vsphere.   There are one of two methods for installation: using pip (more information can be found here) and running via a container.   In this discussion I will be using the container method since that is more portable for me and easier to stand up and remove from my lab environment.  The first thing we need to do is pull the image:

# podman pull ghcr.io/kurokobo/vbmc4vsphere:0.0.4
Trying to pull ghcr.io/kurokobo/vbmc4vsphere:0.0.4...
Getting image source signatures
Copying blob 7a5d07f2fd13 done  
Copying blob 25a245937421 done  
Copying blob 2606867e5cc9 done  
Copying blob 385bb58d08e6 done  
Copying blob ab14b629693d done  
Copying blob bf5952930446 done  
Copying config 789cdc97ba done  
Writing manifest to image destination
Storing signatures
789cdc97ba7461f673cc7ffc8395339f38869abb679ebd0703c2837f493062db

With the image pulled we need to start the container with the following syntax below.  I should note that the -p option can be specified more then once using different port numbers.  Each of the port numbers will then in turn be used for a virtual machine running in VmWare.

# podman run -d --name vbmc4vsphere -p "6801:6801/udp" -v vbmc-volume:/vbmc/.vbmc ghcr.io/kurokobo/vbmc4vsphere:0.0.4
ddf82bfdb7899e9232462ae3e8ea821d327b0db1bc8501c3827644aad9830736
# podman ps
CONTAINER ID  IMAGE                                 COMMAND               CREATED        STATUS            PORTS                   NAMES
ddf82bfdb789  ghcr.io/kurokobo/vbmc4vsphere:0.0.4   --foreground          3 seconds ago  Up 3 seconds ago  0.0.0.0:6801->6801/udp  vbmc4vsphere

Now that the vbmc4vsphere container is running lets go ahead and get a bash shell within the container:

# podman exec -it vbmc4vsphere /bin/bash
root@ddf82bfdb789:/# 

Inside the container we will go ahead and use the vbmc command to add our rheltest virtual machine.  For this command to work we need to specify the port that will be listening (should be one of the ports specified with the -p option at container run time), a IPMI username and password, the vcenter username and password and the vcenter hostname or ipaddress:

root@ddf82bfdb789:/# vbmc add rheltest --port 6801 --username admin --password password --viserver 192.168.0.30 --viserver-password vcenterpassword --viserver-username administrator@vsphere.local
root@ddf82bfdb789:/# vbmc list
+----------+--------+---------+------+
| VM name  | Status | Address | Port |
+----------+--------+---------+------+
| rheltest | down   | ::      | 6801 |
+----------+--------+---------+------+
root@ddf82bfdb789:/# 

Once the entry is created we need to start it so its listening for incoming IPMI requests:

root@ddf82bfdb789:/# vbmc start rheltest
root@ddf82bfdb789:/# vbmc list
+----------+---------+---------+------+
| VM name  | Status  | Address | Port |
+----------+---------+---------+------+
| rheltest | running | ::      | 6801 |
+----------+---------+---------+------+
root@ddf82bfdb789:/# exit
exit
#

Now lets grab the ipaddress off the host where the virtualbmc-for-vsphere container is running.   We need this value when we specify the host in our IPMI command:

# ip addr show dev ens3
2: ens3: <ltBROADCAST,MULTICAST,UP,LOWER_UP>gt mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:b9:97:58 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.10/24 brd 192.168.0.255 scope global noprefixroute ens3
       valid_lft forever preferred_lft forever
    inet6 fe80::6baa:4a96:db6b:88ee/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

Now let test if we can see the power status of our rheltest host with ipmitool.  We know in our previous screenshot that it was on.  I will also run a ping to show the host is up and reachable.

# ipmitool -I lanplus -U admin -P password -H 192.168.0.10 -p 6801 power status
Chassis Power is on

# ping 192.168.0.226 -c 4
PING 192.168.0.226 (192.168.0.226) 56(84) bytes of data.
64 bytes from 192.168.0.226: icmp_seq=1 ttl=64 time=0.753 ms
64 bytes from 192.168.0.226: icmp_seq=2 ttl=64 time=0.736 ms
64 bytes from 192.168.0.226: icmp_seq=3 ttl=64 time=0.651 ms
64 bytes from 192.168.0.226: icmp_seq=4 ttl=64 time=0.849 ms

--- 192.168.0.226 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3109ms
rtt min/avg/max/mdev = 0.651/0.747/0.849/0.072 ms


So we confirmed the host is up so lets go ahead and power it off:

# ipmitool -I lanplus -U admin -P password -H 192.168.0.10 -p 6801 power off
Chassis Power Control: Down/Off

Now lets check with ipmitool and see if the status is also marked as off and if it responds to a ping:

# ipmitool -I lanplus -U admin -P password -H 192.168.0.10 -p 6801 power status
Chassis Power is off

# ping 192.168.0.226 -c 4 -t 10
PING 192.168.0.226 (192.168.0.226) 56(84) bytes of data.
From 192.168.0.10 icmp_seq=1 Destination Host Unreachable
From 192.168.0.10 icmp_seq=2 Destination Host Unreachable
From 192.168.0.10 icmp_seq=3 Destination Host Unreachable
From 192.168.0.10 icmp_seq=4 Destination Host Unreachable

--- 192.168.0.226 ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3099ms
pipe 4

Looks like the host is off and no longer responding which is what we expected.  From the Vcenter console we can see rheltest has also been powered off.   I should note that since we are using the VmWare API's under the covers in virtualbmc-for-vsphere the shutdown task also got recorded in Vcenter under recent tasks.


Lets go ahead and power rheltest back on with the ipmitool command:

# ipmitool -I lanplus -U admin -P password -H 192.168.0.10 -p 6801 power on
Chassis Power Control: Up/On

We can again use ipmitool to validate the power status and ping to validate the connectivity:

# ipmitool -I lanplus -U admin -P password -H 192.168.0.10 -p 6801 power status
Chassis Power is on

# ping 192.168.0.226 -c 4
PING 192.168.0.226 (192.168.0.226) 56(84) bytes of data.
64 bytes from 192.168.0.226: icmp_seq=1 ttl=64 time=0.860 ms
64 bytes from 192.168.0.226: icmp_seq=2 ttl=64 time=1.53 ms
64 bytes from 192.168.0.226: icmp_seq=3 ttl=64 time=0.743 ms
64 bytes from 192.168.0.226: icmp_seq=4 ttl=64 time=0.776 ms

--- 192.168.0.226 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3066ms
rtt min/avg/max/mdev = 0.743/0.976/1.528/0.323 ms

Looks like rheltest is back up again and reachable. The Vcenter console also shows that rheltest has been powered on again:


Now that we understand how virtualbmc-for-vsphere works it would be rather easy to configure an OpenShift BareMetal IPI lab inside of VmWare.   While I will not go into the details here there are additional blogs I have written around the requirements for doing a baremetal IPI deployment and those should be no different in this scenario now that we have the IPMI requirement met in VmWare.