Friday, May 20, 2022

Install OpenShift with Agent Installer


There are so many ways to install OpenShift: Assisted Installer, UPI, IPI, Red Hat Advanced Cluster Management and ZTP.  However I have always longed for a single ISO image I could just boot my physical hardware and it would form a OpenShift cluster.  Well that dream is on course to become a reality with the Agent Installer a tool that can generate an ephemeral OpenShift installation image.  In the following blog I will demonstrate how to use this early incarnation of the tool.

As I stated the Agent Installer generates a single ISO image that one would use to boot all of the nodes they would want to be part of a newly deployed cluster.  However this current example may change some as the code gets developed and merged into the mainstream Openshift installer.  However if one is interested in exploring this new method the following can be a preview of what is to come.

The first step in trying out the Agent Installer is to grab the OpenShift installer source code from Github and checkout the agent-installer branch:

$ git clone https://github.com/openshift/installer
Cloning into 'installer'...
remote: Enumerating objects: 204497, done.
remote: Counting objects: 100% (210/210), done.
remote: Compressing objects: 100% (130/130), done.
remote: Total 204497 (delta 99), reused 153 (delta 70), pack-reused 204287
Receiving objects: 100% (204497/204497), 873.44 MiB | 10.53 MiB/s, done.
Resolving deltas: 100% (132947/132947), done.
Updating files: 100% (86883/86883), done.

$ git checkout 88db7ef
Updating files: 100% (23993/23993), done.
Note: switching to '88db7ef'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 88db7eff2 Fix unnecessary delays in start-cluster-installation

$ git branch
* (HEAD detached at 88db7eff2)
  master

Once we have the source code checked out we need to go ahead and build the the OpenShift install binary:

$ hack/build.sh
+ minimum_go_version=1.17
++ go version
++ cut -d ' ' -f 3
+ current_go_version=go1.17.7
++ version 1.17.7
++ IFS=.
++ printf '%03d%03d%03d\n' 1 17 7
++ unset IFS
++ version 1.17
++ IFS=.
++ printf '%03d%03d%03d\n' 1 17
++ unset IFS
+ '[' 001017007 -lt 001017000 ']'
+ make -C terraform all
make: Entering directory '/home/bschmaus/installer/terraform'
cd providers/alicloud; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-alicloud "$path"; \
zip -1j ../../bin/terraform-provider-alicloud.zip ../../bin/terraform-provider-alicloud;
  adding: terraform-provider-alicloud (deflated 81%)
cd providers/aws; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-aws "$path"; \
zip -1j ../../bin/terraform-provider-aws.zip ../../bin/terraform-provider-aws;
  adding: terraform-provider-aws (deflated 75%)
cd providers/azureprivatedns; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-azureprivatedns "$path"; \
zip -1j ../../bin/terraform-provider-azureprivatedns.zip ../../bin/terraform-provider-azureprivatedns;
  adding: terraform-provider-azureprivatedns (deflated 62%)
cd providers/azurerm; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-azurerm "$path"; \
zip -1j ../../bin/terraform-provider-azurerm.zip ../../bin/terraform-provider-azurerm;
  adding: terraform-provider-azurerm (deflated 77%)
cd providers/azurestack; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-azurestack "$path"; \
zip -1j ../../bin/terraform-provider-azurestack.zip ../../bin/terraform-provider-azurestack;
  adding: terraform-provider-azurestack (deflated 64%)
cd providers/google; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-google "$path"; \
zip -1j ../../bin/terraform-provider-google.zip ../../bin/terraform-provider-google;
  adding: terraform-provider-google (deflated 68%)
cd providers/ibm; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-ibm "$path"; \
zip -1j ../../bin/terraform-provider-ibm.zip ../../bin/terraform-provider-ibm;
  adding: terraform-provider-ibm (deflated 67%)
cd providers/ignition; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-ignition "$path"; \
zip -1j ../../bin/terraform-provider-ignition.zip ../../bin/terraform-provider-ignition;
  adding: terraform-provider-ignition (deflated 61%)
cd providers/ironic; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-ironic "$path"; \
zip -1j ../../bin/terraform-provider-ironic.zip ../../bin/terraform-provider-ironic;
  adding: terraform-provider-ironic (deflated 60%)
cd providers/libvirt; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-libvirt "$path"; \
zip -1j ../../bin/terraform-provider-libvirt.zip ../../bin/terraform-provider-libvirt;
  adding: terraform-provider-libvirt (deflated 61%)
cd providers/local; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-local "$path"; \
zip -1j ../../bin/terraform-provider-local.zip ../../bin/terraform-provider-local;
  adding: terraform-provider-local (deflated 59%)
cd providers/nutanix; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-nutanix "$path"; \
zip -1j ../../bin/terraform-provider-nutanix.zip ../../bin/terraform-provider-nutanix;
  adding: terraform-provider-nutanix (deflated 60%)
cd providers/openstack; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-openstack "$path"; \
zip -1j ../../bin/terraform-provider-openstack.zip ../../bin/terraform-provider-openstack;
  adding: terraform-provider-openstack (deflated 62%)
cd providers/ovirt; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-ovirt "$path"; \
zip -1j ../../bin/terraform-provider-ovirt.zip ../../bin/terraform-provider-ovirt;
  adding: terraform-provider-ovirt (deflated 66%)
cd providers/random; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-random "$path"; \
zip -1j ../../bin/terraform-provider-random.zip ../../bin/terraform-provider-random;
  adding: terraform-provider-random (deflated 59%)
cd providers/vsphere; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-vsphere "$path"; \
zip -1j ../../bin/terraform-provider-vsphere.zip ../../bin/terraform-provider-vsphere;
  adding: terraform-provider-vsphere (deflated 68%)
cd providers/vsphereprivate; \
if [ -f main.go ]; then path="."; else path=./vendor/`grep _ tools.go|awk '{ print $2 }'|sed 's|"||g'`; fi; \
go build -ldflags "-s -w" -o ../../bin/terraform-provider-vsphereprivate "$path"; \
zip -1j ../../bin/terraform-provider-vsphereprivate.zip ../../bin/terraform-provider-vsphereprivate;
  adding: terraform-provider-vsphereprivate (deflated 69%)
cd terraform; \
go build -ldflags "-s -w" -o ../bin/terraform ./vendor/github.com/hashicorp/terraform
make: Leaving directory '/home/bschmaus/installer/terraform'
+ copy_terraform_to_mirror
++ go env GOOS
++ go env GOARCH
+ TARGET_OS_ARCH=linux_amd64
+ rm -rf '/home/bschmaus/installer/pkg/terraform/providers/mirror/*/'
+ find /home/bschmaus/installer/terraform/bin/ -maxdepth 1 -name 'terraform-provider-*.zip' -exec bash -c '
      providerName="$(basename "$1" | cut -d - -f 3 | cut -d . -f 1)"
      targetOSArch="$2"
      dstDir="${PWD}/pkg/terraform/providers/mirror/openshift/local/$providerName"
      mkdir -p "$dstDir"
      echo "Copying $providerName provider to mirror"
      cp "$1" "$dstDir/terraform-provider-${providerName}_1.0.0_${targetOSArch}.zip"
    ' shell '{}' linux_amd64 ';'
Copying alicloud provider to mirror
Copying aws provider to mirror
Copying azureprivatedns provider to mirror
Copying azurerm provider to mirror
Copying azurestack provider to mirror
Copying google provider to mirror
Copying ibm provider to mirror
Copying ignition provider to mirror
Copying ironic provider to mirror
Copying libvirt provider to mirror
Copying local provider to mirror
Copying nutanix provider to mirror
Copying openstack provider to mirror
Copying ovirt provider to mirror
Copying random provider to mirror
Copying vsphere provider to mirror
Copying vsphereprivate provider to mirror
+ mkdir -p /home/bschmaus/installer/pkg/terraform/providers/mirror/terraform/
+ cp /home/bschmaus/installer/terraform/bin/terraform /home/bschmaus/installer/pkg/terraform/providers/mirror/terraform/
+ MODE=release
++ git rev-parse --verify 'HEAD^{commit}'
+ GIT_COMMIT=d74e210f30edf110764d87c8223a18b8a9952253
++ git describe --always --abbrev=40 --dirty
+ GIT_TAG=unreleased-master-6040-gd74e210f30edf110764d87c8223a18b8a9952253
+ DEFAULT_ARCH=amd64
+ GOFLAGS=-mod=vendor
+ LDFLAGS=' -X github.com/openshift/installer/pkg/version.Raw=unreleased-master-6040-gd74e210f30edf110764d87c8223a18b8a9952253 -X github.com/openshift/installer/pkg/version.Commit=d74e210f30edf110764d87c8223a18b8a9952253 -X github.com/openshift/installer/pkg/version.defaultArch=amd64'
+ TAGS=
+ OUTPUT=bin/openshift-install
+ export CGO_ENABLED=0
+ CGO_ENABLED=0
+ case "${MODE}" in
+ LDFLAGS=' -X github.com/openshift/installer/pkg/version.Raw=unreleased-master-6040-gd74e210f30edf110764d87c8223a18b8a9952253 -X github.com/openshift/installer/pkg/version.Commit=d74e210f30edf110764d87c8223a18b8a9952253 -X github.com/openshift/installer/pkg/version.defaultArch=amd64 -s -w'
+ TAGS=' release'
+ test '' '!=' y
+ go generate ./data
writing assets_vfsdata.go
+ echo ' release'
+ grep -q libvirt
+ go build -mod=vendor -ldflags ' -X github.com/openshift/installer/pkg/version.Raw=unreleased-master-6040-gd74e210f30edf110764d87c8223a18b8a9952253 -X github.com/openshift/installer/pkg/version.Commit=d74e210f30edf110764d87c8223a18b8a9952253 -X github.com/openshift/installer/pkg/version.defaultArch=amd64 -s -w' -tags ' release' -o bin/openshift-install ./cmd/openshift-install 

Once the OpenShift install binary is built we next need to create a manifests directory under the installer directory.  In this manifest directory we will be creating six files that basically give the Agent Installer the blueprints of what our cluster should look like.  First lets create the directory:

$ pwd 
/home/bschmaus/installer

$ mkdir manifests

With the directory created we can move onto creating the agent cluster install resource file.  This file specifies the clusters configuration such as number of control plane and/or worker nodes, the api and ingress vip and the cluster networking.   In my example I will be deploying a 3 node compact cluster which referenced a cluster deployment named kni22:

$ cat << EOF > ./manifests/agent-cluster-install.yaml
apiVersion: extensions.hive.openshift.io/v1beta1
kind: AgentClusterInstall
metadata:
  name: kni22
  namespace: kni22
spec:
  apiVIP: 192.168.0.125
  ingressVIP: 192.168.0.126
  clusterDeploymentRef:
    name: kni22
  imageSetRef:
    name: openshift-v4.10.0
  networking:
    clusterNetwork:
    - cidr: 10.128.0.0/14
      hostPrefix: 23
    serviceNetwork:
    - 172.30.0.0/16
  provisionRequirements:
    controlPlaneAgents: 3
    workerAgents: 0 
  sshPublicKey: 'INSERT PUBLIC SSH KEY HERE'
EOF

Next we will create the cluster deployment resource file which defines the cluster name, domain, and other details:

$ cat << EOF > ./manifests/cluster-deployment.yaml
apiVersion: hive.openshift.io/v1
kind: ClusterDeployment
metadata:
  name: kni22
  namespace: kni22
spec:
  baseDomain: schmaustech.com
  clusterInstallRef:
    group: extensions.hive.openshift.io
    kind: AgentClusterInstall
    name: kni22-agent-cluster-install
    version: v1beta1
  clusterName: kni22
  controlPlaneConfig:
    servingCertificates: {}
  platform:
    agentBareMetal:
      agentSelector:
        matchLabels:
          bla: aaa
  pullSecretRef:
    name: pull-secret
EOF

Moving on we now create the cluster image set resource file which contains OpenShift image information such as the repository and image name.  This will be the version of the cluster that gets deployed in our 3 node compact cluster.  In this example we are using 4.10.10:

$ cat << EOF > ./manifests/cluster-image-set.yaml
apiVersion: hive.openshift.io/v1
kind: ClusterImageSet
metadata:
  name: ocp-release-4.10.10-x86-64-for-4.10.0-0-to-4.11.0-0
spec:
  releaseImage: quay.io/openshift-release-dev/ocp-release:4.10.10-x86_64
EOF

Next we define the infrastructure environment file which  contains information for pulling OpenShift onto the target host nodes we are deploying to:

$ cat << EOF > ./manifests/infraenv.yaml 
apiVersion: agent-install.openshift.io/v1beta1
kind: InfraEnv
metadata:
  name: kni22
  namespace: kni22
spec:
  clusterRef:
    name: kni22  
    namespace: kni22
  pullSecretRef:
    name: pull-secret
  sshAuthorizedKey: 'INSERT PUBLIC SSH KEY HERE'
  nmStateConfigLabelSelector:
    matchLabels:
      kni22-nmstate-label-name: kni22-nmstate-label-value
EOF

The next file is the nmstate configuration file and this file provides all the details for all of the host that will be booted using the ISO image we are going to create.   Since we have a 3 node compact cluster to deploy we notice that in the file below we have specified three nmstate configurations.  Each configuration is for a node and generates a static IP address on the nodes enp2s0 interface that matches the MAC address defined.   This enables the ISO to boot up and not necessarily require DHCP in the environment which is what a lot of customers are looking for.   Again my example has 3 configurations but if we had worker nodes we would add those in too.   Lets go ahead and create the file:

$ cat << EOF > ./manifests/nmstateconfig.yaml
---
apiVersion: agent-install.openshift.io/v1beta1
kind: NMStateConfig
metadata:
  name: mynmstateconfig01
  namespace: openshift-machine-api
  labels:
    kni22-nmstate-label-name: kni22-nmstate-label-value
spec:
  config:
    interfaces:
      - name: enp2s0
        type: ethernet
        state: up
        mac-address: 52:54:00:e7:05:72
        ipv4:
          enabled: true
          address:
            - ip: 192.168.0.116
              prefix-length: 24
          dhcp: false
    dns-resolver:
      config:
        server:
          - 192.168.0.10
    routes:
      config:
        - destination: 0.0.0.0/0
          next-hop-address: 192.168.0.1
          next-hop-interface: enp2s0
          table-id: 254
  interfaces:
    - name: "enp2s0"
      macAddress: 52:54:00:e7:05:72
---
apiVersion: agent-install.openshift.io/v1beta1
kind: NMStateConfig
metadata:
  name: mynmstateconfig02
  namespace: openshift-machine-api
  labels:
    kni22-nmstate-label-name: kni22-nmstate-label-value
spec:
  config:
    interfaces:
      - name: enp2s0
        type: ethernet
        state: up
        mac-address: 52:54:00:95:fd:f3
        ipv4:
          enabled: true
          address:
            - ip: 192.168.0.117
              prefix-length: 24
          dhcp: false
    dns-resolver:
      config:
        server:
          - 192.168.0.10
    routes:
      config:
        - destination: 0.0.0.0/0
          next-hop-address: 192.168.0.1
          next-hop-interface: enp2s0
          table-id: 254
  interfaces:
    - name: "enp2s0"
      macAddress: 52:54:00:95:fd:f3
---
apiVersion: agent-install.openshift.io/v1beta1
kind: NMStateConfig
metadata:
  name: mynmstateconfig03
  namespace: openshift-machine-api
  labels:
    kni22-nmstate-label-name: kni22-nmstate-label-value
spec:
  config:
    interfaces:
      - name: enp2s0
        type: ethernet
        state: up
        mac-address: 52:54:00:e8:b9:18
        ipv4:
          enabled: true
          address:
            - ip: 192.168.0.118
              prefix-length: 24
          dhcp: false
    dns-resolver:
      config:
        server:
          - 192.168.0.10
    routes:
      config:
        - destination: 0.0.0.0/0
          next-hop-address: 192.168.0.1
          next-hop-interface: enp2s0
          table-id: 254
  interfaces:
    - name: "enp2s0"
      macAddress: 52:54:00:e8:b9:18
EOF

The final file we need to create is the pull-secret resource file which contains the pull-secret values so that our cluster can pull in the required OpenShift images to instantiate the cluster:

$ cat << EOF > ./manifests/pull-secret.yaml 
apiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
  name: pull-secret
  namespace: kni22
stringData:
  .dockerconfigjson: 'INSERT JSON FORMATTED PULL-SECRET'
EOF

At this point we should now have our six required files defined to build our Agent Installer ISO:

$ ls -1 ./manifests/
agent-cluster-install.yaml
cluster-deployment.yaml
cluster-image-set.yaml
infraenv.yaml
nmstateconfig.yaml
pull-secret.yaml 

We are now ready to use the Openshift install binary we compiled earlier with the Agent Installer code to generate our ephemeral OpenShift ISO.   We do this by issuing the following command which introduces the agent option.  This in turn will read in the manifest details we generated and download the corresponding RHCOS image and then inject our details into the image writing out a file called agent.iso:

$ bin/openshift-install agent create image 
INFO adding MAC interface map to host static network config - Name:  enp2s0  MacAddress: 52:54:00:e7:05:72 
INFO adding MAC interface map to host static network config - Name:  enp2s0  MacAddress: 52:54:00:95:fd:f3 
INFO adding MAC interface map to host static network config - Name:  enp2s0  MacAddress: 52:54:00:e8:b9:18 
INFO[0000] Adding NMConnection file <enp2s0 .nmconnection="">  pkg=manifests
INFO[0000] Adding NMConnection file <enp2s0 .nmconnection="">  pkg=manifests
INFO[0001] Adding NMConnection file <enp2s0 .nmconnection="">  pkg=manifests
INFO[0001] Start configuring static network for 3 hosts  pkg=manifests
INFO[0001] Adding NMConnection file <enp2s0 .nmconnection="">  pkg=manifests
INFO[0001] Adding NMConnection file <enp2s0 .nmconnection="">  pkg=manifests
INFO[0001] Adding NMConnection file <enp2s0 .nmconnection="">  pkg=manifests
INFO Obtaining RHCOS image file from 'https://rhcos-redirector.apps.art.xq1c.p1.openshiftapps.com/art/storage/releases/rhcos-4.11/411.85.202203181601-0/x86_64/rhcos-411.85.202203181601-0-live.x86_64.iso' 
INFO   

Once the agent create image command completes we are left with a agent.iso image which is in fact our OpenShift install ISO:

$ ls -l ./output/
total 1073152
-rw-rw-r--. 1 bschmaus bschmaus 1098907648 May 20 08:55 agent.iso

Since the nodes I will be using to demonstrate this 3 node compact cluster are virtual machines all on the same KVM hypervisor I will go ahead and copy the agent.iso image over to that host:

$ scp ./output/agent.iso root@192.168.0.22:/var/lib/libvirt/images/
root@192.168.0.22's password: 
agent.iso   

With the image moved over to the hypervisor host I went ahead and ensured each virtual machine we are using (asus3-vm[1-3]) has the image set.  Further the hosts are designed boot off the ISO if the disk is empty.  We can confirm everything is ready with the following output:

# virsh list --all
 Id   Name        State
----------------------------
 -    asus3-vm1   shut off
 -    asus3-vm2   shut off
 -    asus3-vm3   shut off
 -    asus3-vm4   shut off
 -    asus3-vm5   shut off
 -    asus3-vm6   shut off

# virsh domblklist asus3-vm1
 Target   Source
---------------------------------------------------
 sda      /var/lib/libvirt/images/asus3-vm1.qcow2
 sdb      /var/lib/libvirt/images/agent.iso

# virsh domblklist asus3-vm2
 Target   Source
---------------------------------------------------
 sda      /var/lib/libvirt/images/asus3-vm2.qcow2
 sdb      /var/lib/libvirt/images/agent.iso

# virsh domblklist asus3-vm3
 Target   Source
---------------------------------------------------
 sda      /var/lib/libvirt/images/asus3-vm3.qcow2
 sdb      /var/lib/libvirt/images/agent.iso
 

# virsh start asus3-vm1
Domain asus3-vm1 started

Once the first virtual machine is started we can switch over to the console and watch it boot up:


During the boot process the system will come up to a standard login prompt on the console.  Then in the background on the host it will start pulling in the required containers to run the familiar Assisted Installer UI.  I gave this process about 5 minutes before I attempted to access the web UI.   To access the web UI we can point our browser to the ipaddress of node we just booted and port 8080:


We should see a visible kni22 cluster with a status of draft because no nodes have been associated to it yet.  Next we will click on kni22 to bring us into the configuration:


We can see the familiar Assisted Installer discovery screen and we can also see our first host is listed.   At this point lets turn on the other two nodes that will make up our 3 node compact cluster and let them also boot from the agent ISO we created.

After the other two  nodes have booted we should see them appear in the web UI.  We also can see that the node names are all localhost.  This was due to the fact I set static IP addresses in the nmstate.yaml above.   If we had gone with DHCP the names would have been set by DHCP.  Nevertheless though we can go ahead and edit each hostname and set it to the proper name and click next to continue:


There will be an additional configuration page where other configuration items will be set and could be changed if needed but we will click next through that screen to bring us to the summary page:



If everything looks correct we can go ahead and click on the install cluster button to start the deployment:




At this point the cluster installation begins.  I should point out however we will not be able to watch the installation complete from the web UI.  The reason being is that the other two nodes will get their RHCOS images written to disk, reboot and then instantiate part of the cluster.  At that point the first node, the one running the web UI, will also get its RHCOS image written to disk and reboot.  After that the web UI is not longer available to watch.   With that in mind I recommend grabbing the kubeconfig for the cluster by clicking on the download kubeconfig button.

Once the web UI is no longer accessible we can monitor the installation from command line using the kubeconfig we downloaded.   First lets see where the nodes are at:

$ export KUBECONFIG=/home/bschmaus/kubeconfig-kni22

$ oc get nodes
NAME        STATUS   ROLES           AGE   VERSION
asus3-vm1   Ready    master,worker   2m    v1.23.5+9ce5071
asus3-vm2   Ready    master,worker   29m   v1.23.5+9ce5071
asus3-vm3   Ready    master,worker   29m   v1.23.5+9ce5071

All the nodes are in a ready state and marked as both a control node and worker.   Now lets see where the cluster operators are at:

$ oc get co 
NAME                                       VERSION   AVAILABLE   PROGRESSING   DEGRADED   SINCE   MESSAGE
authentication                             4.10.10   False       True          True       18m     WellKnownAvailable: The well-known endpoint is not yet available: need at least 3 kube-apiservers, got 2
baremetal                                  4.10.10   True        False         False      17m     
cloud-controller-manager                   4.10.10   True        False         False      29m     
cloud-credential                           4.10.10   True        False         False      34m     
cluster-autoscaler                         4.10.10   True        False         False      16m     
config-operator                            4.10.10   True        False         False      18m     
console                                    4.10.10   True        False         False      4m33s   
csi-snapshot-controller                    4.10.10   True        False         False      18m     
dns                                        4.10.10   True        False         False      17m     
etcd                                       4.10.10   True        True          False      16m     NodeInstallerProgressing: 1 nodes are at revision 0; 2 nodes are at revision 4; 0 nodes have achieved new revision 5
image-registry                             4.10.10   True        False         False      9m44s   
ingress                                    4.10.10   True        False         False      11m     
insights                                   4.10.10   True        False         False      12m     
kube-apiserver                             4.10.10   True        True          False      4m29s   NodeInstallerProgressing: 1 nodes are at revision 0; 2 nodes are at revision 6
kube-controller-manager                    4.10.10   True        True          False      14m     NodeInstallerProgressing: 1 nodes are at revision 0; 2 nodes are at revision 7
kube-scheduler                             4.10.10   True        True          False      14m     NodeInstallerProgressing: 1 nodes are at revision 0; 2 nodes are at revision 6
kube-storage-version-migrator              4.10.10   True        False         False      18m     
machine-api                                4.10.10   True        False         False      7m53s   
machine-approver                           4.10.10   True        False         False      17m     
machine-config                             4.10.10   True        False         False      17m     
marketplace                                4.10.10   True        False         False      16m     
monitoring                                 4.10.10   True        False         False      5m52s   
network                                    4.10.10   True        True          False      19m     DaemonSet "openshift-multus/network-metrics-daemon" is not available (awaiting 1 nodes)...
node-tuning                                4.10.10   True        False         False      15m     
openshift-apiserver                        4.10.10   True        False         False      4m45s   
openshift-controller-manager               4.10.10   True        False         False      15m     
openshift-samples                          4.10.10   True        False         False      7m37s   
operator-lifecycle-manager                 4.10.10   True        False         False      17m     
operator-lifecycle-manager-catalog         4.10.10   True        False         False      17m     
operator-lifecycle-manager-packageserver   4.10.10   True        False         False      11m     
service-ca                                 4.10.10   True        False         False      19m     
storage                                    4.10.10   True        False         False      19m  

The cluster operators are still rolling out so lets give it a few more minutes and we will check again:

$ oc get co
NAME                                       VERSION   AVAILABLE   PROGRESSING   DEGRADED   SINCE   MESSAGE
authentication                             4.10.10   True        False         False      13m     
baremetal                                  4.10.10   True        False         False      31m     
cloud-controller-manager                   4.10.10   True        False         False      43m     
cloud-credential                           4.10.10   True        False         False      49m     
cluster-autoscaler                         4.10.10   True        False         False      31m     
config-operator                            4.10.10   True        False         False      33m     
console                                    4.10.10   True        False         False      19m     
csi-snapshot-controller                    4.10.10   True        False         False      33m     
dns                                        4.10.10   True        False         False      32m     
etcd                                       4.10.10   True        False         False      31m     
image-registry                             4.10.10   True        False         False      24m     
ingress                                    4.10.10   True        False         False      26m     
insights                                   4.10.10   True        False         False      27m     
kube-apiserver                             4.10.10   True        False         False      19m     
kube-controller-manager                    4.10.10   True        False         False      29m     
kube-scheduler                             4.10.10   True        False         False      28m     
kube-storage-version-migrator              4.10.10   True        False         False      33m     
machine-api                                4.10.10   True        False         False      22m     
machine-approver                           4.10.10   True        False         False      32m     
machine-config                             4.10.10   True        False         False      32m     
marketplace                                4.10.10   True        False         False      31m     
monitoring                                 4.10.10   True        False         False      20m     
network                                    4.10.10   True        False         False      34m     
node-tuning                                4.10.10   True        False         False      30m     
openshift-apiserver                        4.10.10   True        False         False      19m     
openshift-controller-manager               4.10.10   True        False         False      29m     
openshift-samples                          4.10.10   True        False         False      22m     
operator-lifecycle-manager                 4.10.10   True        False         False      32m     
operator-lifecycle-manager-catalog         4.10.10   True        False         False      32m     
operator-lifecycle-manager-packageserver   4.10.10   True        False         False      26m     
service-ca                                 4.10.10   True        False         False      34m     
storage                                    4.10.10   True        False         False      34m   

At this point our cluster installation is completed.  However I forgot to mention that while the web UI was up we should have ssh'd to the bootstrap node and shelled into the assisted installer container running to retrieve our kubeadmin password under the /data directory.   However I purposely skipped that part so I could show how we can just reset the kubeadmin password instead.

First I want to thank Andrew Block and his write up on how to do this here.  So lets go ahead and create the kubeadmin-rotate.go file here in the kuberotate directory we create:

 $ mkdir ~/kuberotate
$cd ~/kuberotate

$ cat << EOF > ./kubeadmin-rotate.go 
package main import ( "fmt" "crypto/rand" "golang.org/x/crypto/bcrypt" b64 "encoding/base64" "math/big" ) // generateRandomPasswordHash generates a hash of a random ASCII password // 5char-5char-5char-5char func generateRandomPasswordHash(length int) (string, string, error) { const ( lowerLetters = "abcdefghijkmnopqrstuvwxyz" upperLetters = "ABCDEFGHIJKLMNPQRSTUVWXYZ" digits = "23456789" all = lowerLetters + upperLetters + digits ) var password string for i := 0; i < length; i++ { n, err := rand.Int(rand.Reader, big.NewInt(int64(len(all)))) if err != nil { return "", "", err } newchar := string(all[n.Int64()]) if password == "" { password = newchar } if i < length-1 { n, err = rand.Int(rand.Reader, big.NewInt(int64(len(password)+1))) if err != nil { return "", "",err } j := n.Int64() password = password[0:j] + newchar + password[j:] } } pw := []rune(password) for _, replace := range []int{5, 11, 17} { pw[replace] = '-' } bytes, err := bcrypt.GenerateFromPassword([]byte(string(pw)), bcrypt.DefaultCost) if err != nil { return "", "",err } return string(pw), string(bytes), nil } func main() { password, hash, err := generateRandomPasswordHash(23) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("Actual Password: %s\n", password) fmt.Printf("Hashed Password: %s\n", hash) fmt.Printf("Data to Change in Secret: %s\n", b64.StdEncoding.EncodeToString([]byte(hash))) } EOF

Next lets go ahead and initialize our go project:

$ go mod init kuberotate
go: creating new go.mod: module kuberotate

With the project initialized lets go ahead and pull in the module dependencies by executing a go mod tidy which will pull in the bcrypt module:

$ go mod tidy
go: finding module for package golang.org/x/crypto/bcrypt
go: found golang.org/x/crypto/bcrypt in golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898

And finally since I just want to run the program instead of compile it I will just run a go run kubeadmin-rotate.go which will print out the password, a hashed password and a base64 encoded version of the hashed password:

$ go run kubeadmin-rotate.go 
Actual Password: gWdYr-62GLh-QIynG-Boj7n
Hashed Password: $2a$10$DN48Jp4YkuEEVMWZNyOR2.LkLn1ZZOJOtzR8c9detf1lVAQ2iVQGK
Data to Change in Secret: JDJhJDEwJERONDhKcDRZa3VFRVZNV1pOeU9SMi5Ma0xuMVpaT0pPdHpSOGM5ZGV0ZjFsVkFRMmlWUUdL

The last step is to patch the kubeadmin secret with the hashed password that was base64 encoded:

$ oc patch secret -n kube-system kubeadmin --type json -p '[{"op": "replace", "path": "/data/kubeadmin", "value": "JDJhJDEwJERONDhKcDRZa3VFRVZNV1pOeU9SMi5Ma0xuMVpaT0pPdHpSOGM5ZGV0ZjFsVkFRMmlWUUdL"}]'
secret/kubeadmin patched

Now we can go over to the OpenShift console and see if we can login.   And sure enough with the password we had above we can and confirm our 3 node OpenShift cluster installed by the agent installer is ready to be used for workloads:


Hopefully this blog was useful to provide a preview of what the agent installer will look like.  Keep in mind the code is under rapid development and so things could change but change is always good!

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.