Friday, August 06, 2021

Deploying Single Node OpenShift via Assisted Installer API

 


The Assisted Installer is a project to help simplify OpenShift Container Platform (OCP) installation for a number of different platforms, but focuses on bare metal deployments. The service provides validation and discovery of targeted hardware and greatly improves success rates of installations.  It can be accessed via Red Hat’s provided SaaS portal to deploy an OCP cluster either on baremetal or virtual machines.   

In this article however I want to demonstrate a Single Node OpenShift deployment without using the UI web interface and instead rely on the underlying REST API that drives the Assisted Installer.  This can be useful for automating the deployment of clusters without user intervention.

The first step to achieve this will be to obtain an OpenShift Cluster Manager API Token.  This token provides the ability to authenticate against your Red Hat OpenShift Cluster Manager account without the need of a username or password.

Place this token into a file called ocm-token:

$ echo "Token String From OCM API Token Link Above" > ~/ocm-token

Next lets set some variables that we will refer to throughout this deployment process:

export OFFLINE_ACCESS_TOKEN=$(cat ~/ocm-token)                                # Loading my token into a variable
export ASSISTED_SERVICE_API="api.openshift.com"                               # Setting the Assisted Installer API endpoint
export CLUSTER_VERSION="4.8"                                                  # OpenShift version
export CLUSTER_IMAGE="quay.io/openshift-release-dev/ocp-release:4.8.2-x86_64" # OpenShift Quay image version
export CLUSTER_NAME="kni1"                                                    # OpenShift cluster name
export CLUSTER_DOMAIN="schmaustech.com"                                       # Domain name where my cluster will be deployed
export CLUSTER_NET_TYPE="OVNKubernetes"                                       # Network type to deploy with OpenShift
export MACHINE_CIDR_NET="192.168.0.0/24"                                      # Machine CIDR network 
export SNO_STATICIP_NODE_NAME="master-0"                                      # Node name of my SNO node
export PULL_SECRET=$(cat ~/pull-secret.json | jq -R .)                        # Loading my pull-secret into variable
export CLUSTER_SSHKEY=$(cat ~/.ssh/id_rsa.pub)                                # Loading the public key into variable

With the primary variables set lets go ahead and create a deployment.json file.  This file will reference some of the variables we set previously and also have a few that are statically set.   The key one to notice in this deployment is the high_availability_mode.  Having that variable set to None ensures we are doing a Single Node OpenShift (SNO) deployment:

cat << EOF > ~/deployment.json
{
  "kind": "Cluster",
  "name": "$CLUSTER_NAME",
  "openshift_version": "$CLUSTER_VERSION",
  "ocp_release_image": "$CLUSTER_IMAGE",
  "base_dns_domain": "$CLUSTER_DOMAIN",
  "hyperthreading": "all",
  "user_managed_networking": true,
  "vip_dhcp_allocation": false,
  "high_availability_mode": "None",
  "hosts": [],
  "ssh_public_key": "$CLUSTER_SSHKEY",
  "pull_secret": $PULL_SECRET,
  "network_type": "OVNKubernetes"
}
EOF


Now that we have the deployment.json file created lets refresh our bearer token:

$ export TOKEN=$(curl \
--silent \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "client_id=cloud-services" \
--data-urlencode "refresh_token=${OFFLINE_ACCESS_TOKEN}" \
https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token | \
jq -r .access_token)

With the token refereshed lets go ahead and create our deployment via the assisted installer REST API using curl and a post command.   When the command completes the output will only be a cluster id with some quotes on it.  I used sed to clean off the quotes so we end up with just the UUID number.  Note that the cluster configuration has only been created at this point but not installed.

$ export CLUSTER_ID=$( curl -s -X POST "https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters" \
  -d @./deployment.json \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.id' )

$ export CLUSTER_ID=$( sed -e 's/^"//' -e 's/"$//' <<<"$CLUSTER_ID")
$ echo $CLUSTER_ID
e85fc7d5-f274-4359-acc5-48044fc67132

At this point we need to generate a discovery iso for the SNO node to be booted from.  However before we do that I wanted to make sure that my SNO node was using a static IP address instead of the default of DHCP.  To do this we need to create a data file that contains the information on how the static IP should be set.   NMState will take this information when applied to the OCP node during the installation.   Below we have defined some arguments that provide a mac interface map and a NMState yaml file.  All of this information gets pushed into the DATA variable which is just pointing to a temp file.

$ DATA=$(mktemp)
$ jq -n --arg SSH_KEY "$CLUSTER_SSHKEY" --arg NMSTATE_YAML1 "$(cat ~/sno-server.yaml)"  \
'{
  "ssh_public_key": $SSH_KEY,
  "image_type": "full-iso",
  "static_network_config": [
    {
      "network_yaml": $NMSTATE_YAML1,
      "mac_interface_map": [{"mac_address": "52:54:00:82:23:e2", "logical_nic_name": "ens9"}]
    }
  ]
}' >> $DATA

The sno-server.yaml used in the NMState argument looks like the following below.  It contains the IP address, mask, interface and route information.

$ cat ~/sno-server.yaml 
dns-resolver:
  config:
    server:
    - 192.168.0.10
interfaces:
- ipv4:
    address:
    - ip: 192.168.0.204
      prefix-length: 24
    dhcp: false
    enabled: true
  name: ens9
  state: up
  type: ethernet
routes:
  config:
  - destination: 0.0.0.0/0
    next-hop-address: 192.168.0.1
    next-hop-interface: ens9
    table-id: 254

We can confirm that the DATA was set appropriately by looking at the DATA variable and then cat out the tmp file it points to:

$ echo $DATA
/tmp/tmp.3Jqw7lU6Qf

$ cat /tmp/tmp.3Jqw7lU6Qf
{
  "ssh_public_key": "SSHKEY REDACTED",
  "image_type": "full-iso",
  "static_network_config": [
    {
      "network_yaml": "dns-resolver:\n  config:\n    server:\n    - 192.168.0.10\ninterfaces:\n- ipv4:\n    address:\n    - ip: 192.168.0.204\n      prefix-length: 24\n    dhcp: false\n    enabled: true\n  name: ens9\n  state: up\n  type: ethernet\nroutes:\n  config:\n  - destination: 0.0.0.0/0\n    next-hop-address: 192.168.0.1\n    next-hop-interface: ens9\n    table-id: 254",
      "mac_interface_map": [
        {
          "mac_address": "52:54:00:82:23:e2",
          "logical_nic_name": "ens9"
        }
      ]
    }
  ]
}

With the static IP configuration set we can go ahead and generate our discovery ISO with another curl post command.   The command will generate quite a bit of output but our main concern is visually seeing the section where the static network configuration gets defined:

$ curl -X POST \
"https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID/downloads/image" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d @$DATA

(...)
"static_network_config":"dns-resolver:\n  config:\n    server:\n    - 192.168.0.10\ninterfaces:\n- ipv4:\n    address:\n    - ip: 192.168.0.204\n      prefix-length: 24\n    dhcp: false\n    enabled: true\n  name: ens9\n  state: up\n  type: ethernet\nroutes:\n  config:\n  - destination: 0.0.0.0/0\n    next-hop-address: 192.168.0.1\n    next-hop-interface: ens9\n    table-id: 254HHHHH52:54:00:82:23:e2=ens9","type":"full-iso"}
(...)

Now that the discovery image has been created lets go ahead and download that image:

$ curl -L \
  "http://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID/downloads/image" \
  -o ~/discovery-image-$CLUSTER_NAME.iso \
  -H "Authorization: Bearer $TOKEN"
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  984M  100  984M    0     0  10.4M      0  0:01:34  0:01:34 --:--:-- 10.5M


Now that the image is downloaded we can move it to where we need to boot the SNO node machine.  This node could be baremetal or in my case a virtual machine.   If it was baremetal for example, like a Dell, we might use racadm to do a virtual media mount and then ipmitool to power on the server.   In my case since I am using a virtual machine, I need to do a couple things.   First I copy the image over to my KVM hypervisor host.  Next I ensure the power is off on my virtual machine.  I can use ipmitool here because I am leveraging virtual BMC.  Next I use the virsh command to change the media to my ISO that I moved over.   I format the disk image on my virtual machine that way I do not have to mess around with boot order as the primary disk will be skipped because its empty and the CDROM will boot.  And finally I power on the host to initiate the discover phase.   At this point we have to wait for the node to boot up and report in what was discovered from an introspection perspective.  I usually wait 5 minutes before proceeding hence why I have the sleep command. 

$ scp ~/discovery-image-kni1.iso root@192.168.0.5:/slowdata/images/

$ /usr/bin/ipmitool -I lanplus -H192.168.0.10 -p6252 -Uadmin -Ppassword chassis power off

$ ssh root@192.168.0.5 "virsh change-media rhacm-master-0 hda /slowdata/images/discovery-image-kni1.iso"

$ ssh root@192.168.0.5 "virt-format --format=raw --partition=none -a /fastdata2/images/master-0.img"

$ /usr/bin/ipmitool -I lanplus -H192.168.0.10 -p6252 -Uadmin -Ppassword chassis power on

$ sleep 300


After 5 minutes the node should have reporting in to the Assisted Installer portal.   And inventory of the hardware of the machine and capabilities is provided in the portal.   We can now proceed with the deployment.

First though we need to ensure the hostname is set correctly.  With DHCP it was automatically being set but since we used a static IP I found I needed to set it manually.  To do this we will patch the installation and set the requested_hostname:

$ curl -X PATCH \
  "https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{ \"requested_hostname\": \"$SNO_STATICIP_NODE_NAME.$CLUSTER_NAME.$CLUSTER_DOMAIN\"}" | jq

(...)
"requested_hostname": "master-0.kni1.schmaustech.com",
(...)

We also need to patch the machine network to the appropriate network:

$ curl -X PATCH \
  "https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{ \"machine_network_cidr\": \"$MACHINE_CIDR_NET\"}" | jq

(...)
"machine_network_cidr": "192.168.0.0/24",
(...)

Finally after all of the preparation we can finally run the curl post command that actually starts the installation process:

$ curl -X POST \
  "https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID/actions/install" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" | jq

(...)
  "status": "preparing-for-installation",
  "status_info": "Preparing cluster for installation",
  "status_updated_at": "2021-08-06T20:56:17.565Z",
(...)


The installation process does take about 60 minutes or so minutes to complete so go grab lunch or a cup of coffee.

After 60 minutes or so we can check and see if the cluster is installed or still in progress.  The first thing we should do though is refresh our token again:

$ export TOKEN=$(curl \
--silent \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "client_id=cloud-services" \
--data-urlencode "refresh_token=${OFFLINE_ACCESS_TOKEN}" \
https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token | \
jq -r .access_token)

After we have refreshed our token lets go ahead and confirm if indeed the cluster has finished installing.   We can achieve this by doing a curl get against the cluster ID.   There will be a lot of output but we are specifically looking for the status and status_info lines:

$ curl -s -X GET \
   -H "Content-Type: application/json" \
   -H "Authorization: Bearer $TOKEN" \
   "https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID" | jq .

(...)
  "status": "installed",
  "status_info": "Cluster is installed",
  "status_updated_at": "2021-08-06T21:45:04.375Z",
(...)

From the output above my cluster has completed so now I can pull my kubeconfig down and redirect it to a file:

$ curl -s -X GET \
  "https://$ASSISTED_SERVICE_API/api/assisted-install/v1/clusters/$CLUSTER_ID/downloads/kubeconfig" > kubeconfig-kni1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN"

Now lets export the kubeconfig variable and look at the cluster with some oc commands:

$ export KUBECONFIG=~/kubeconfig-kni1

$ oc get nodes -o wide
NAME                            STATUS   ROLES           AGE    VERSION           INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                                                       KERNEL-VERSION                 CONTAINER-RUNTIME
master-0.kni1.schmaustech.com   Ready    master,worker   156m   v1.21.1+051ac4f   192.168.0.204   none        Red Hat Enterprise Linux CoreOS 48.84.202107202156-0 (Ootpa)   4.18.0-305.10.2.el8_4.x86_64   cri-o://1.21.2-5.rhaos4.8.gitb27d974.el8

$ oc get co
NAME                                       VERSION   AVAILABLE   PROGRESSING   DEGRADED   SINCE
authentication                             4.8.2     True        False         False      134m
baremetal                                  4.8.2     True        False         False      144m
cloud-credential                           4.8.2     True        False         False      147m
cluster-autoscaler                         4.8.2     True        False         False      146m
config-operator                            4.8.2     True        False         False      151m
console                                    4.8.2     True        False         False      135m
csi-snapshot-controller                    4.8.2     True        False         False      147m
dns                                        4.8.2     True        False         False      144m
etcd                                       4.8.2     True        False         False      146m
image-registry                             4.8.2     True        False         False      141m
ingress                                    4.8.2     True        False         False      141m
insights                                   4.8.2     True        False         False      135m
kube-apiserver                             4.8.2     True        False         False      143m
kube-controller-manager                    4.8.2     True        False         False      143m
kube-scheduler                             4.8.2     True        False         False      143m
kube-storage-version-migrator              4.8.2     True        False         False      151m
machine-api                                4.8.2     True        False         False      146m
machine-approver                           4.8.2     True        False         False      147m
machine-config                             4.8.2     True        False         False      143m
marketplace                                4.8.2     True        False         False      144m
monitoring                                 4.8.2     True        False         False      138m
network                                    4.8.2     True        False         False      152m
node-tuning                                4.8.2     True        False         False      146m
openshift-apiserver                        4.8.2     True        False         False      143m
openshift-controller-manager               4.8.2     True        False         False      146m
openshift-samples                          4.8.2     True        False         False      142m
operator-lifecycle-manager                 4.8.2     True        False         False      144m
operator-lifecycle-manager-catalog         4.8.2     True        False         False      147m
operator-lifecycle-manager-packageserver   4.8.2     True        False         False      144m
service-ca                                 4.8.2     True        False         False      151m
storage                                    4.8.2     True        False         False      146m

Everything looks good with this example Single Node OpenShift installation!   If one is interested in pursuing more complex examples it might be worth looking at what is available with the Assisted Installer REST API.   To do that take a look at this swagger.yaml file and use it with the online Swagger Editor.

Thursday, July 08, 2021

Simplify Red Hat Quay/Clair Upgrade From 3.3.4 to 3.5.4

 

Security in Kubernetes and OpenShift environments for many customers is very important.  Everything from firewall rules, role bindings and limiting access to services is on the table.   For customers who run their own private registry using Red Hat Quay that security is extended with Clair which can scan their images as they are placed into the registry for known vulnerabilities.    The binary combo of Quay and Clair makes it a great combination.  However along with the sense of security comes the need for a smooth transition when upgrading Quay and Clair.   In the following blog I will demonstrate how I upgraded my Quay & Clair  3.3.4 environment to 3.5.4.   

The upgrade of Quay & Clair from 3.3.4 to 3.5.4 one would think is very trivial but there are a few things that change along the way which means the order of operation is important here.  Some of those changes are as follows and ones I experienced:

  • From 3.3.4 to 3.4.5 there is a database schema update that is performed so one will want to ensure they have a good backup of their MySQL or PostgreSQL.
  • From 3.3.4 to 3.4.5 the Clair scanning version changes from version 2 to version 4.  This means we first need to configure a brand new PostgreSQL database and then configure Clair and Quay for v4 scanning.
  • From 3.3.4 to 3.4.5 SSL certs now require to have Subject Alternative Names (SAN)

 Before we start the upgrade lets look at the current running environment.  In all my examples the docker command is used.  This could easily be replaced with podman and even in the official documentation podman is the preferred command from Quay 3.4+.

First lets look at the running containers by running the docker ps command:

# docker ps
CONTAINER ID     IMAGE                                             COMMAND                  CREATED        STATUS      PORTS                                                            NAMES
713cb9e63813     quay.io/redhat/clair-jwt:v3.3.4                   "/clair/clair-entr..."   3 hours ago    Up 3 hours  0.0.0.0:6060-6061->6060-6061/tcp                                 youthful_ptolemy
32c922276bf4     quay.io/redhat/quay:v3.3.4                        "/quay-registry/qu..."   3 hours ago    Up 3 hours  7443/tcp, 9091/tcp, 0.0.0.0:80->8080/tcp, 0.0.0.0:443->8443/tcp  blissful_thompson
1b05a8997caa     registry.access.redhat.com/rhscl/mysql-57-rhel7   "container-entrypo..."   3 hours ago    Up 3 hours  0.0.0.0:3306->3306/tcp                                           mysql
9de27908c351     postgres                                          "docker-entrypoint..."   3 hours ago    Up 3 hours  0.0.0.0:5432->5432/tcp                                           postgres
53b420f0c0be     registry.access.redhat.com/rhscl/redis-32-rhel7   "container-entrypo..."   3 hours ago    Up 3 hours  0.0.0.0:6379->6379/tcp                                           jovial_sinoussi

From the output above we can see that in this environment all processes run on the same server with the exception of the S3 storage that actually stores our registry data.   So lets break down what is running:

Container ID

Process

Purpose

External Port

Internal Port

713cb9e63813

Clair 3.3.4

Image Scanning

6060 & 6061

6060 & 6061

9de27908c351

PostgreSQL 

DB for Clair

5432

5432

32c922276bf4

Quay 3.3.4

Registry

80 & 443

8080 & 8443

1b05a8997caa

MySQL

DB for Quay

3306

3306

53b420f0c0be

Redis

DB Cache

6379

6379


Next I want to show the firewalld rules we have allowed.  Note that the list below also shows the additional ports I want opened for the upgrade process.

# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources: 
  services: dhcpv6-client ssh
  ports: 8443/tcp 80/tcp 443/tcp 3306/tcp 6379/tcp 5432/tcp 6060/tcp 6161/tcp 6061/tcp 6062/tcp 6063/tcp 5433/tcp 8080/tcp 8081/tcp 8089/tcp
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

Now lets take a look at the current Quay config.yaml file:

# cat /mnt/quay/config/config.yaml
AUTHENTICATION_TYPE: Database
BITTORRENT_FILENAME_PEPPER: af936296-6a1f-444a-8642-a2cd26184cb4
BUILDLOGS_REDIS:
  host: quay.schmaustech.com
  port: 6379
DATABASE_SECRET_KEY: '19413931088941115129071567106986409590936979178495908481080155841238934557252'
DB_URI: mysql+pymysql://quayuser:JzxCTamgFBmHRhcGFtoPHFkrx1BH2vwQ@192.168.0.11/enterpriseregistrydb
DEFAULT_TAG_EXPIRATION: 2w
DISTRIBUTED_STORAGE_CONFIG:
  default:
  - RadosGWStorage
  - access_key: BKIKJAA5BMMU2RHO6IBB
    bucket_name: schmaustech
    hostname: 192.168.0.21
    is_secure: false
    port: '9100'
    secret_key: V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
    storage_path: /datastorage/registry
DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS: []
DISTRIBUTED_STORAGE_PREFERENCE:
- default
ENTERPRISE_LOGO_URL: /static/img/RH_Logo_Quay_Black_UX-horizontal.svg
FEATURE_ACI_CONVERSION: false
FEATURE_ANONYMOUS_ACCESS: true
FEATURE_APP_REGISTRY: false
FEATURE_APP_SPECIFIC_TOKENS: true
FEATURE_BUILD_SUPPORT: false
FEATURE_CHANGE_TAG_EXPIRATION: true
FEATURE_DIRECT_LOGIN: true
FEATURE_MAILING: false
FEATURE_PARTIAL_USER_AUTOCOMPLETE: true
FEATURE_REPO_MIRROR: false
FEATURE_REQUIRE_TEAM_INVITE: true
FEATURE_RESTRICTED_V1_PUSH: true
FEATURE_SECURITY_NOTIFICATIONS: true
FEATURE_SECURITY_SCANNER: true
FEATURE_USERNAME_CONFIRMATION: true
FEATURE_USER_CREATION: true
FEATURE_USER_LOG_ACCESS: true
GITHUB_LOGIN_CONFIG: {}
GITHUB_TRIGGER_CONFIG: {}
GITLAB_TRIGGER_KIND: {}
GPG2_PRIVATE_KEY_FILENAME: signing-private.gpg
GPG2_PUBLIC_KEY_FILENAME: signing-public.gpg
LOGS_MODEL: database
LOGS_MODEL_CONFIG: {}
LOG_ARCHIVE_LOCATION: default
MAIL_DEFAULT_SENDER: support@quay.io
MAIL_PORT: 587
MAIL_USE_TLS: true
PREFERRED_URL_SCHEME: https
REGISTRY_TITLE: Red Hat Quay
REGISTRY_TITLE_SHORT: Red Hat Quay
REPO_MIRROR_SERVER_HOSTNAME: null
REPO_MIRROR_TLS_VERIFY: true
SECRET_KEY: '61665332226336867411689064816174816874267671771605676993213345229382029684425'
SECURITY_SCANNER_ENDPOINT: http://quay.schmaustech.com:6060
SECURITY_SCANNER_ISSUER_NAME: security_scanner
SERVER_HOSTNAME: quay.schmaustech.com
SETUP_COMPLETE: true
SIGNING_ENGINE: gpg2
SUPER_USERS:
- admin
TAG_EXPIRATION_OPTIONS:
- 0s
- 1d
- 1w
- 2w
- 4w
TEAM_RESYNC_STALE_TIME: 60m
TESTING: false
USERFILES_LOCATION: default
USERFILES_PATH: userfiles/
USER_EVENTS_REDIS:
  host: quay.schmaustech.com
  port: 6379
USE_CDN: false

And then lets take a look at the current Clair config.yaml keeping in mind this is Clair v2 and during the upgrade process to Quay 3.4.3 we will be upgrading to Clair v4 so this file will be replaced:

# cat /mnt/clair/config/config.yaml 
clair:
  database:
    type: pgsql
    options:
      # A PostgreSQL Connection string pointing to the Clair Postgres database.
      # Documentation on the format can be found at: http://www.postgresql.org/docs/9.4/static/libpq-connect.html
      source: postgresql://postgres:password@quay.schmaustech.com:5432/clairtest?sslmode=disable 
      cachesize: 16384
  api:
    #The port at which Clair will report its health status. For example, if Clair is running at
    #https://clair.mycompany.com, the health will be reported at
    #http://clair.mycompany.com:6061/health.
    healthport: 6061

    port: 6062
    timeout: 900s

    # paginationkey can be any random set of characters. *Must be the same across all Clair instances*.
    paginationkey:

  updater:
    # interval defines how often Clair will check for updates from its upstream vulnerability databases.
    interval: 6h
  notifier:
    attempts: 3
    renotifyinterval: 1h
    http:
      # QUAY_ENDPOINT defines the endpoint at which Quay is running.
      # For example: https://myregistry.mycompany.com
      endpoint: https://quay.schmaustech.com/secscan/notify
      proxy: http://localhost:6063

jwtproxy:
  signer_proxy:
    enabled: true
    listen_addr: :6063
    ca_key_file: /certificates/mitm.key # Generated internally, do not change.
    ca_crt_file: /certificates/mitm.crt # Generated internally, do not change.
    signer:
      issuer: security_scanner
      expiration_time: 5m
      max_skew: 1m
      nonce_length: 32
      private_key:
        type: autogenerated
        options:
          rotate_every: 12h
          key_folder: /clair/config/
          key_server:
            type: keyregistry
            options:
              # QUAY_ENDPOINT defines the endpoint at which Quay is running.
              # For example: https://myregistry.mycompany.com
              registry: https://quay.schmaustech.com/keys/


  verifier_proxies:
  - enabled: true
    # The port at which Clair will listen.
    listen_addr: :6060

    # If Clair is to be served via TLS, uncomment these lines. See the "Running Clair under TLS"
    # section below for more information.
    #key_file: /clair/config/domain.key
    #crt_file: /clair/config/domain.crt

    verifier:
      # CLAIR_ENDPOINT is the endpoint at which this Clair will be accessible. Note that the port
      # specified here must match the listen_addr port a few lines above this.
      # Example: https://myclair.mycompany.com:6060
      audience: http://quay.schmaustech.com:6060

      upstream: http://localhost:6062
      key_server:
        type: keyregistry
        options:
          # QUAY_ENDPOINT defines the endpoint at which Quay is running.
          # Example: https://myregistry.mycompany.com
          registry: https://quay.schmaustech.com/keys/

At this point we have looked at the current running environment but before we begin upgrade I would recommend taking a Quay database backup whether its running MySQL or PostgreSQL.  We should do this because during the upgrade process of Quay the DB schema will get updated and so if we had to revert back we would need to restore from backup.

Now lets start the upgrade process from 3.3.4 to 3.4.5.   During this upgrade process we will be upgrading three components:  Redis, Clair & Quay.  Lets start with the Redis upgrade first as we can do this with the current Quay environment.   Below we will stop the current Redis container and then start the new Redis container for the 3.4.3 environment:

# docker stop 53b420f0c0be
53b420f0c0be

# docker run -d --rm --name redis -p 6379:6379 -e REDIS_PASSWORD=strongpassword registry.redhat.io/rhel8/redis-5:1
84f42084adae1e2e9d5b11aa8e06b90dde49dce015b8f30e72541631c8e98e05

# docker ps
CONTAINER ID        IMAGE                                       COMMAND                  CREATED             STATUS              PORTS                       NAMES
84f42084adae        registry.redhat.io/rhel8/redis-5:1          "container-entrypo..."   5 seconds ago       Up 4 seconds        0.0.0.0:6379->6379/tcp      redis

Since we added a password switch to Redis on the new version we need to also update the Quay config.yaml to include that password:

BUILDLOGS_REDIS:
    host: quay.schmaustech.com
    password: strongpassword
    port: 6379
    
USER_EVENTS_REDIS:
    host: quay.schmaustech.com
    password: strongpassword
    port: 6379

Now lets stop and start the Quay 3.3.4 container and confirm our Redis change takes effect:

# docker stop 32c922276bf4
32c922276bf4

# docker run --restart=always -p 443:8443 -p 80:8080    --sysctl net.core.somaxconn=4096    --privileged=true    -v /mnt/quay/config:/conf/stack:Z    -v /mnt/quay/storage:/datastorage:Z    -d quay.io/redhat/quay:v3.3.4
f0ab641a5d253532e263d38820ca1849f17428e0d7d12b6f79b676222d310b06

# docker ps
CONTAINER ID    IMAGE                        COMMAND                  CREATED              STATUS              PORTS                                                             NAMES
f0ab641a5d25    quay.io/redhat/quay:v3.3.4   "/quay-registry/qu..."   About a minute ago   Up About a minute   7443/tcp, 9091/tcp, 0.0.0.0:80->8080/tcp, 0.0.0.0:443->8443/tcp   gifted_einstein

Now lets move onto the upgrade of the Clair component.  For this we will need a new database for Clair to consume.    In my example I am going to run a new PostgreSQL database container completely separate from the current PostgreSQL container we originally had for our 3.3.4 environment.  To do this I will run the following commands:

# mkdir /mnt/postgres-clair4
# setfacl -m u:26:-wx /mnt/postgres-clair4
# docker run -d --rm --name postgresql-clair4 -e POSTGRESQL_USER=clairuser -e POSTGRESQL_PASSWORD=clairpass -e POSTGRESQL_DATABASE=clair -e POSTGRESQL_ADMIN_PASSWORD=password -p 5433:5432 -v /mnt/postgres-clair4:/var/lib/pgsql/data:Z -d registry.redhat.io/rhel8/postgresql-10:1
2b4b1afa3345d24bb6058c713e2f4d363a09da5bff75242d18bad69ddb720d34

# docker ps
CONTAINER ID        IMAGE                                           COMMAND                  CREATED             STATUS              PORTS                     NAMES
2b4b1afa3345        registry.redhat.io/rhel8/postgresql-10:1        "container-entrypo..."   3 seconds ago       Up 2 seconds        0.0.0.0:5433->5432/tcp    postgresql-clair4


Because Clair requires the uuid-ossp extensions in the PostgreSQL database we need to run the following SQL command against the database:

# docker exec -it postgresql-clair4 /bin/bash -c 'echo "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" | psql -d clair -U postgres'
CREATE EXTENSION

With the Clair v4 PostgreSQL database running lets turn our attention to the Clair v4 config.yaml.  Below I created my Clair config path and then added the contents to the config.yaml:

# mkdir -p /mnt/clair4/config
# cat /mnt/clair4/config/config.yaml 
http_listen_addr: :8081
introspection_addr: :8089
log_level: debug
indexer:
  connstring: host=quay.schmaustech.com port=5433 dbname=clair user=clairuser password=clairpass sslmode=disable
  scanlock_retry: 10
  layer_scan_concurrency: 5
  migrations: true
matcher:
  connstring: host=quay.schmaustech.com port=5433 dbname=clair user=clairuser password=clairpass sslmode=disable
  max_conn_pool: 100
  run: ""
  migrations: true
  indexer_addr: clair-indexer
notifier:
  connstring: host=quay.schmaustech.com port=5433 dbname=clair user=clairuser password=clairpass sslmode=disable
  delivery_interval: 1m
  poll_interval: 5m
  migrations: true
auth:
  psk:
    key: "MTU5YzA4Y2ZkNzJoMQ=="
    iss: ["quay"]
# tracing and metrics
trace:
  name: "jaeger"
  probability: 1
  jaeger:
    agent_endpoint: "localhost:6831"
    service_name: "clair"
metrics:
  name: "prometheus"

Before we start up the Clair v4 container though lets go ahead and make the changes we need to the Quay config.yaml as well.  Add or verify the following lines in the configuration:

FEATURE_SECURITY_NOTIFICATIONS: false
FEATURE_SECURITY_SCANNER: true
SECURITY_SCANNER_INDEXING_INTERVAL: 30
SECURITY_SCANNER_V4_ENDPOINT: http://quay.schmaustech.com:8081
SECURITY_SCANNER_V4_PSK: MTU5YzA4Y2ZkNzJoMQ==
SERVER_HOSTNAME: quay.schmaustech.com

At this point we are ready to stop the current Quay and Clair containers:

# docker stop 713cb9e63813 f0ab641a5d25 9de27908c351
713cb9e63813
f0ab641a5d25
9de27908c351

# docker rm 713cb9e63813 f0ab641a5d25 9de27908c351
713cb9e63813
f0ab641a5d25
9de27908c351

Next lets start up the Clair v4 container:

# docker run -d --rm --name clairv4 -p 8081:8081 -p 8089:8089 -e CLAIR_CONF=/clair/config.yaml -e CLAIR_MODE=combo -v /mnt/clair4/config:/clair:Z registry.redhat.io/quay/clair-rhel8:v3.4.5
b627ef1ee4669341d8a5fe72c095e14bb8931200a08d016f95faaec19d466423
[root@quay config]# docker ps
CONTAINER ID        IMAGE                                             COMMAND                  CREATED             STATUS              PORTS                                                      NAMES
b627ef1ee466        registry.redhat.io/quay/clair-rhel8:v3.4.5        "/usr/bin/dumb-ini..."   3 seconds ago       Up 1 second         0.0.0.0:8081->8081/tcp, 6060/tcp, 0.0.0.0:8089->8089/tcp   clairv4And finally lets start up the new Quay 3.4.3 container:

Once Clair v4 is up and running we can then start Quay up on version 3.4.5.  Note however that the image scanning may need some additional time to come up as Clair is repopulating the database of vulnerabilities.

# docker run -d --rm -p 443:8443 -p 8080:8080 --name quay -v /mnt/quay/config:/conf/stack:Z -v /mnt/quay/storage:/datastorage:Z registry.redhat.io/quay/quay-rhel8:v3.4.5
1f127bd3791e600a9bcf6f6e20f1fb1e368de27868da802f9ec8d6e5bf87310a
# docker ps
CONTAINER ID        IMAGE                                             COMMAND                  CREATED             STATUS              PORTS                                                      NAMES
1f127bd3791e        registry.redhat.io/quay/quay-rhel8:v3.4.5         "dumb-init -- /qua..."   4 seconds ago       Up 4 seconds        7443/tcp, 0.0.0.0:8080->8080/tcp, 0.0.0.0:443->8443/tcp    quay


If everything went well we should now have a Quay 3.4.3 and Clair 3.4.5 environment.   Before I proceed I want to validate that I can still push/pull content from my registry and that Clair v4 is scanning images appropriately.


Also this is a good time to quickly look at what the running environment should look like:

# docker ps
CONTAINER ID        IMAGE                                           COMMAND                  CREATED           STATUS            PORTS                                                      NAMES
1f127bd3791e        registry.redhat.io/quay/quay-rhel8:v3.4.5       "dumb-init -- /qua..."   35 minutes ago    Up 35 minutes     7443/tcp, 0.0.0.0:8080->8080/tcp, 0.0.0.0:443->8443/tcp  quay 
627ef1ee466        registry.redhat.io/quay/clair-rhel8:v3.4.5      "/usr/bin/dumb-ini..."   38 minutes ago     Up 38 minutes     0.0.0.0:8081->8081/tcp, 6060/tcp, 0.0.0.0:8089->8089/tcp  clairv4
2b4b1afa3345        registry.redhat.io/rhel8/postgresql-10:1        "container-entrypo..."   2 hours ago       Up 2 hours        0.0.0.0:5433->5432/tcp                                      postgresql-clair4
84f42084adae        registry.redhat.io/rhel8/redis-5:1              "container-entrypo..."   2 hours ago       Up 2 hours        0.0.0.0:6379->6379/tcp                                      redis
1b05a8997caa        registry.access.redhat.com/rhscl/mysql-57-rhel7 "container-entrypo..."   20 hours ago      Up 20 hours       0.0.0.0:3306->3306/tcp                                      mysql

Here is the chart view again of the environment breakdown:

Container ID

Process

Purpose

External Port

Internal Port

f8f4f441a89d

Clair 3.4.5

Image Scanning

8080 & 8081

8080 & 8081

2b4b1afa3345

PostgreSQL 

DB for Clair

5433

5432

1f127bd3791e

Quay 3.4.5

Registry

8080 & 443

8080 & 8443

1b05a8997caa

MySQL

DB for Quay

3306

3306

84f42084adae

Redis

DB Cache

6379

6379


At this point I am feeling confident we can move forward with the upgrade process to Quay 3.5.4.   Thankfully this step is not as complex as the move from 3.3.4 to 3.4.5.  In fact its just a matter of switching out the images being used for the containers.  The first step then is to stop the current Quay and Clair containers:

# docker stop f8f4f441a89d 1f127bd3791e
f8f4f441a89d
1f127bd3791e


In Quay 3.5.4 Helm & OCI artifacts have been enabled by default however since this is an upgrade we need to add those feature switches to the Quay config.yaml if we want them enabled:

FEATURE_GENERAL_OCI_SUPPORT: true
FEATURE_HELM_OCI_SUPPORT: true

Once we have stopped the current running containers for Quay and Clair and we have made any changes we needed to the config.yaml we can begin to start the Clair 3.5.4 container:

# docker run -d --rm --name clairv4 -p 8081:8081 -p 8089:8089 -e CLAIR_CONF=/clair/config.yaml -e CLAIR_MODE=combo -v /mnt/clair4/config:/clair:Z registry.redhat.io/quay/clair-rhel8:v3.5.4
0a046b625f4ef348e43a181db81341772cd690162df5eb34478dc525fabd591b
# docker ps
CONTAINER ID    IMAGE                                         COMMAND                CREATED         STATUS          PORTS                                                      NAMES
0a046b625f4e    registry.redhat.io/quay/clair-rhel8:v3.5.4    "/usr/bin/dumb-ini..." 3 seconds ago   Up 2 seconds    0.0.0.0:8081->8081/tcp, 6060/tcp, 0.0.0.0:8089->8089/tcp   clairv4

Once Clair is running we can now start the Quay 3.5.4 container:

# docker run -d --rm -p 443:8443 -p 8080:8080 --name quay -v /mnt/quay/config:/conf/stack:Z -v /mnt/quay/storage:/datastorage:Z registry.redhat.io/quay/quay-rhel8:v3.5.4
264ff68abb49fd53b0a2debeffd9684fb6504fa1558b874baadd271ac7a08225
# docker ps
CONTAINER ID   IMAGE                                       COMMAND                CREATED        STATUS         PORTS                                                      NAMES
264ff68abb49   registry.redhat.io/quay/quay-rhel8:v3.5.4   "dumb-init -- /qua..." 4 seconds ago  Up 3 seconds   7443/tcp, 0.0.0.0:8080->8080/tcp, 0.0.0.0:443->8443/tcp    quay

At this point we should again confirm that we can push/pull images from the Quay environment and also confirm that any new images are being scanned properly by Clair.



If everything checks out and works then we can declare success!


Friday, June 04, 2021

Migrating From OpenShiftSDN to OVNKubernetes

 


In previous version of OpenShift if one wanted to switch from OpenShiftSDN to OVNKubernetes there was no migration path and hence the cluster needed to be reinstalled.   That burden is about to become a thing of the past with OpenShift 4.8 because in this version and beyond the ability to migrate the cluster without re-installation is now possible.   In the following details below I will outline and show an example of how this process looks on a working OpenShift cluster.

First lets cover some basics about the environment setup.   I have a five node OpenShift 4.8.0-fc.7 pre-release cluster.   Three of those nodes are masters and the other two are worker nodes in a baremetal IPI deployment.   My current network type is configured as OpenShiftSDN.

First lets validate that the current cluster is running without issues.  We can start by confirming all the nodes are in a ready state:

$ oc get nodes
NAME                                 STATUS   ROLES    AGE     VERSION
master-0.n6s2d.dynamic.opentlc.com   Ready    master   31m     v1.21.0-rc.0+4b2b6ff
master-1.n6s2d.dynamic.opentlc.com   Ready    master   31m     v1.21.0-rc.0+4b2b6ff
master-2.n6s2d.dynamic.opentlc.com   Ready    master   31m     v1.21.0-rc.0+4b2b6ff
worker-0.n6s2d.dynamic.opentlc.com   Ready    worker   13m     v1.21.0-rc.0+4b2b6ff
worker-1.n6s2d.dynamic.opentlc.com   Ready    worker   8m53s   v1.21.0-rc.0+4b2b6ff

Next lets confirm the cluster operators are all functioning properly and are available:

$ oc get co
NAME                                       VERSION      AVAILABLE   PROGRESSING   DEGRADED   SINCE
authentication                             4.8.0-fc.7   True        False         False      51s
baremetal                                  4.8.0-fc.7   True        True          False      29m
cloud-credential                           4.8.0-fc.7   True        False         False      43m
cluster-autoscaler                         4.8.0-fc.7   True        False         False      29m
config-operator                            4.8.0-fc.7   True        False         False      30m
console                                    4.8.0-fc.7   True        False         False      7m30s
csi-snapshot-controller                    4.8.0-fc.7   True        False         False      15m
dns                                        4.8.0-fc.7   True        False         False      29m
etcd                                       4.8.0-fc.7   True        False         False      28m
image-registry                             4.8.0-fc.7   True        False         False      25m
ingress                                    4.8.0-fc.7   True        False         False      12m
insights                                   4.8.0-fc.7   True        False         False      23m
kube-apiserver                             4.8.0-fc.7   True        False         False      27m
kube-controller-manager                    4.8.0-fc.7   True        False         False      27m
kube-scheduler                             4.8.0-fc.7   True        False         False      28m
kube-storage-version-migrator              4.8.0-fc.7   True        False         False      17m
machine-api                                4.8.0-fc.7   True        False         False      24m
machine-approver                           4.8.0-fc.7   True        False         False      30m
machine-config                             4.8.0-fc.7   True        False         False      29m
marketplace                                4.8.0-fc.7   True        False         False      29m
monitoring                                 4.8.0-fc.7   True        False         False      8m9s
network                                    4.8.0-fc.7   True        False         False      30m
node-tuning                                4.8.0-fc.7   True        False         False      29m
openshift-apiserver                        4.8.0-fc.7   True        False         False      15m
openshift-controller-manager               4.8.0-fc.7   True        False         False      28m
openshift-samples                          4.8.0-fc.7   True        False         False      25m
operator-lifecycle-manager                 4.8.0-fc.7   True        False         False      29m
operator-lifecycle-manager-catalog         4.8.0-fc.7   True        False         False      29m
operator-lifecycle-manager-packageserver   4.8.0-fc.7   True        False         False      26m
service-ca                                 4.8.0-fc.7   True        False         False      30m
storage                                    4.8.0-fc.7   True        False         False      30m

And for fun lets show that the OpenShiftSDN pods are running as that is our current network type on this cluster:

$ oc get pods -n openshift-sdn 
NAME                   READY   STATUS    RESTARTS   AGE
sdn-controller-chcvr   1/1     Running   3          31m
sdn-controller-wzhdz   1/1     Running   0          31m
sdn-controller-zp4qr   1/1     Running   0          31m
sdn-czhf6              2/2     Running   0          9m14s
sdn-fvnt6              2/2     Running   0          31m
sdn-njcfn              2/2     Running   0          14m
sdn-nmpt2              2/2     Running   0          31m
sdn-zj2fz              2/2     Running   0          31m

At this point I feel comfortable moving onto the migration process.  If anything above looked suspicious or not functioning properly then I would pause and address that first.   For now though we can move on and the first this we will do is backup the current configuration for the cluster network:

$ oc get Network.config.openshift.io cluster -o yaml > cluster-openshift-sdn.yaml
$

Once we have our backup we can now set the migration field on the cluster network operator.  Note that this will not trigger a migration but rather tell the machine config operator (MCO) to apply new machine configs to all the nodes in the cluster in preparation for OVN-Kubernetes:

$ oc patch Network.operator.openshift.io cluster --type='merge' --patch '{ "spec": { "migration": {"networkType": "OVNKubernetes" } } }'
network.operator.openshift.io/cluster patched

At this point if we look at the mcp and describe our nodes and grep for macine config we can see things are starting to update from an MCO perspective:

$ oc get mcp
NAME     CONFIG                                             UPDATED   UPDATING   DEGRADED   MACHINECOUNT   READYMACHINECOUNT   UPDATEDMACHINECOUNT   DEGRADEDMACHINECOUNT   AGE
master   rendered-master-1309c87d08601c58ddf8abd9bb08a7ea   False     True       False      3              0                   0                     0                      34m
worker   rendered-worker-67d75dc2ab02f295344703f157da14d7   False     True       False      2              0                   0                     0                      34m

$ oc describe node | egrep "hostname|machineconfig"
                    kubernetes.io/hostname=master-0.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-master-1309c87d08601c58ddf8abd9bb08a7ea
                    machineconfiguration.openshift.io/desiredConfig: rendered-master-1309c87d08601c58ddf8abd9bb08a7ea
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done
                    kubernetes.io/hostname=master-1.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-master-1309c87d08601c58ddf8abd9bb08a7ea
                    machineconfiguration.openshift.io/desiredConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Working
                    kubernetes.io/hostname=master-2.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-master-1309c87d08601c58ddf8abd9bb08a7ea
                    machineconfiguration.openshift.io/desiredConfig: rendered-master-1309c87d08601c58ddf8abd9bb08a7ea
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done
                    kubernetes.io/hostname=worker-0.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-worker-67d75dc2ab02f295344703f157da14d7
                    machineconfiguration.openshift.io/desiredConfig: rendered-worker-e17471318b8b3a61fc931cedeca303e1
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Working
                    kubernetes.io/hostname=worker-1.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-worker-67d75dc2ab02f295344703f157da14d7
                    machineconfiguration.openshift.io/desiredConfig: rendered-worker-67d75dc2ab02f295344703f157da14d7
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done

We will know the updates are done when we see that the updated field for the mcp is set to true for both the worker and master nodes.  We should also see that the status of the machine configuration state is set to done for every node in the cluster.  Do not preceed until the output looks similar to the below:

$ oc get mcp
NAME     CONFIG                                             UPDATED   UPDATING   DEGRADED   MACHINECOUNT   READYMACHINECOUNT   UPDATEDMACHINECOUNT   DEGRADEDMACHINECOUNT   AGE
master   rendered-master-9b49007a4d027c9d7f40cfd3c485e31f   True      False      False      3              3                   3                     0                      46m
worker   rendered-worker-e17471318b8b3a61fc931cedeca303e1   True      False      False      2              2                   2                     0                      46m

$ oc describe node | egrep "hostname|machineconfig"
                    kubernetes.io/hostname=master-0.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/desiredConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done
                    kubernetes.io/hostname=master-1.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/desiredConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done
                    kubernetes.io/hostname=master-2.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/desiredConfig: rendered-master-9b49007a4d027c9d7f40cfd3c485e31f
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done
                    kubernetes.io/hostname=worker-0.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-worker-e17471318b8b3a61fc931cedeca303e1
                    machineconfiguration.openshift.io/desiredConfig: rendered-worker-e17471318b8b3a61fc931cedeca303e1
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done
                    kubernetes.io/hostname=worker-1.n6s2d.dynamic.opentlc.com
                    machineconfiguration.openshift.io/controlPlaneTopology: HighlyAvailable
                    machineconfiguration.openshift.io/currentConfig: rendered-worker-e17471318b8b3a61fc931cedeca303e1
                    machineconfiguration.openshift.io/desiredConfig: rendered-worker-e17471318b8b3a61fc931cedeca303e1
                    machineconfiguration.openshift.io/reason: 
                    machineconfiguration.openshift.io/state: Done

To confirm that OVN-Kubernetes has been appropriately staged for the migration we can look at the machine config for both workers and masters and see that the configure-ovs.sh script has been set to start OVNKubernetes:

$ oc get machineconfig rendered-master-9b49007a4d027c9d7f40cfd3c485e31f -o yaml | grep ExecStart | grep OVNKubernetes
ExecStart=/usr/local/bin/configure-ovs.sh OVNKubernetes $ oc get machineconfig rendered-worker-e17471318b8b3a61fc931cedeca303e1 -o yaml | grep ExecStart | grep OVNKubernetes ExecStart=/usr/local/bin/configure-ovs.sh OVNKubernetes

At this point we are ready to start the migration process and we do that by patching the network type and setting it to OVNKubernetes:

$ oc patch Network.config.openshift.io cluster --type='merge' --patch '{ "spec": { "networkType": "OVNKubernetes" } }'
network.config.openshift.io/cluster patched

Once we have executed the patch command the multus daemon set will begin to be rolled out.  We can observe that by watching the rollout status:

$ oc -n openshift-multus rollout status daemonset/multus
Waiting for daemon set "multus" rollout to finish: 1 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 1 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 1 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 2 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 2 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 2 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 3 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 3 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 3 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 4 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 4 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 4 out of 5 new pods have been updated...
Waiting for daemon set "multus" rollout to finish: 4 of 5 updated pods are available...
daemon set "multus" successfully rolled out

One the rollout status is complete we next need to reboot all the nodes in the cluster.  The following script below makes it easy to do this process assuming ssh keys are configured properly:

$ cat << EOF > ~/reboot-nodes.sh
#!/bin/bash
 
for ip in $(oc get nodes  -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}')
do
  echo "reboot node $ip"
  ssh -o StrictHostKeyChecking=no core@\$ip sudo shutdown -r -t 3
done
EOF

Once we have created the script we can go ahead and execute it to start the reboot process:

$ ~/reboot-nodes.sh
reboot node 10.20.0.100
Warning: Permanently added '10.20.0.100' (ECDSA) to the list of known hosts.
Shutdown scheduled for Fri 2021-06-04 13:44:07 UTC, use 'shutdown -c' to cancel.
reboot node 10.20.0.101
Warning: Permanently added '10.20.0.101' (ECDSA) to the list of known hosts.
Shutdown scheduled for Fri 2021-06-04 13:44:08 UTC, use 'shutdown -c' to cancel.
reboot node 10.20.0.102
Warning: Permanently added '10.20.0.102' (ECDSA) to the list of known hosts.
Shutdown scheduled for Fri 2021-06-04 13:44:08 UTC, use 'shutdown -c' to cancel.
reboot node 10.20.0.200
Warning: Permanently added '10.20.0.200' (ECDSA) to the list of known hosts.
Shutdown scheduled for Fri 2021-06-04 13:44:09 UTC, use 'shutdown -c' to cancel.
reboot node 10.20.0.201
Warning: Permanently added '10.20.0.201' (ECDSA) to the list of known hosts.
Shutdown scheduled for Fri 2021-06-04 13:44:10 UTC, use 'shutdown -c' to cancel.


Depending on the number of nodes and how quickly they reboot this process could take a little bit.    After some time has passed though we should be cable to confirm that the nodes are back up and in a ready state:

$ oc get nodes
NAME                                 STATUS   ROLES    AGE   VERSION
master-0.n6s2d.dynamic.opentlc.com   Ready    master   64m   v1.21.0-rc.0+4b2b6ff
master-1.n6s2d.dynamic.opentlc.com   Ready    master   64m   v1.21.0-rc.0+4b2b6ff
master-2.n6s2d.dynamic.opentlc.com   Ready    master   64m   v1.21.0-rc.0+4b2b6ff
worker-0.n6s2d.dynamic.opentlc.com   Ready    worker   46m   v1.21.0-rc.0+4b2b6ff
worker-1.n6s2d.dynamic.opentlc.com   Ready    worker   41m   v1.21.0-rc.0+4b2b6ff

We can also confirm that the network type is now configured as OVNKubernetes:

$ oc get network.config/cluster -o jsonpath='{.status.networkType}{"\n"}'
OVNKubernetes

We should also confirm that there are no pods that are in a pending or crashloop state:

$ oc get pods --all-namespaces -o wide --sort-by='{.spec.nodeName}' | egrep -v "Running|Completed"
NAMESPACE          NAME       READY   STATUS      RESTARTS   AGE    IP            NODE               NOMINATED NODE   READINESS GATES

And we can also validate that all the cluster operators are running appropriately:

$ oc get co
NAME                                       VERSION      AVAILABLE   PROGRESSING   DEGRADED   SINCE
authentication                             4.8.0-fc.7   True        False         False      4m36s
baremetal                                  4.8.0-fc.7   True        False         False      63m
cloud-credential                           4.8.0-fc.7   True        False         False      76m
cluster-autoscaler                         4.8.0-fc.7   True        False         False      63m
config-operator                            4.8.0-fc.7   True        False         False      64m
console                                    4.8.0-fc.7   True        False         False      4m21s
csi-snapshot-controller                    4.8.0-fc.7   True        False         False      21m
dns                                        4.8.0-fc.7   True        False         False      63m
etcd                                       4.8.0-fc.7   True        False         False      62m
image-registry                             4.8.0-fc.7   True        False         False      59m
ingress                                    4.8.0-fc.7   True        False         False      4m5s
insights                                   4.8.0-fc.7   True        False         False      57m
kube-apiserver                             4.8.0-fc.7   True        False         False      61m
kube-controller-manager                    4.8.0-fc.7   True        False         False      60m
kube-scheduler                             4.8.0-fc.7   True        False         False      61m
kube-storage-version-migrator              4.8.0-fc.7   True        False         False      27m
machine-api                                4.8.0-fc.7   True        False         False      58m
machine-approver                           4.8.0-fc.7   True        False         False      63m
machine-config                             4.8.0-fc.7   True        False         False      63m
marketplace                                4.8.0-fc.7   True        False         False      63m
monitoring                                 4.8.0-fc.7   True        False         False      4m6s
network                                    4.8.0-fc.7   True        False         False      64m
node-tuning                                4.8.0-fc.7   True        False         False      63m
openshift-apiserver                        4.8.0-fc.7   True        False         False      4m40s
openshift-controller-manager               4.8.0-fc.7   True        False         False      62m
openshift-samples                          4.8.0-fc.7   True        False         False      59m
operator-lifecycle-manager                 4.8.0-fc.7   True        False         False      63m
operator-lifecycle-manager-catalog         4.8.0-fc.7   True        False         False      63m
operator-lifecycle-manager-packageserver   4.8.0-fc.7   True        False         False      4m30s
service-ca                                 4.8.0-fc.7   True        False         False      64m
storage                                    4.8.0-fc.7   True        False         False      64m

If everything looked good from the steps above we can go ahead and remove the cluster network object configuration object by setting it to null:

$ oc patch Network.operator.openshift.io cluster --type='merge' --patch '{ "spec": { "migration": null } }'
network.operator.openshift.io/cluster patched

We can also remove the customer configuration for OpenShiftSDN:

$ oc patch Network.operator.openshift.io cluster --type='merge' --patch '{ "spec": { "defaultNetwork": { "openshiftSDNConfig": null } } }'
network.operator.openshift.io/cluster patched (no change)

And finally we can remove the old openshift-sdn:

$ oc delete namespace openshift-sdn
namespace "openshift-sdn" deleted

And just for one final confirmation of success lets confirm the OVNKubernetes pods are up and running:

$ oc get pods -n openshift-ovn-kubernetes
NAME                   READY   STATUS    RESTARTS   AGE
ovnkube-master-5v8ch   6/6     Running   14         50m
ovnkube-master-kmwkp   6/6     Running   6          50m
ovnkube-master-zlgnv   6/6     Running   14         50m
ovnkube-node-7vrmq     4/4     Running   4          50m
ovnkube-node-kz4l9     4/4     Running   4          50m
ovnkube-node-nhbdz     4/4     Running   5          50m
ovnkube-node-nwnnk     4/4     Running   5          50m
ovnkube-node-t27gb     4/4     Running   5          50m