# Installing OpenShift in a disconnected network, step-by-step
Making an offline installation bundle for OpenShift requires [mirroring/downloading the container images](https://docs.openshift.com/container-platform/4.12/installing/disconnected_install/installing-mirroring-disconnected.html) and then [hosting those container images in a container registry](https://docs.openshift.com/container-platform/4.12/installing/disconnected_install/installing-mirroring-creating-registry.html) that is accessible by the cluster nodes. The download process can put the container images into the local filesystem or upload them directly into the container registry. (a USB stick, a directory that will be burnt to a DVD, or a folder that will be uploaded into S3 or similar storage)
:::info
A minimal download of OpenShift 4.12 requires ~15GB of space
A minimal download of OpenShift Platform Plus requires ~50GB of space
If you're using mounted storage, consider setting `--quayRoot` to a subdirectory of the mountpoint. Uninstalling the mirror registry will fail otherwise.
:::
Lots of good information in this blog - [Mirroring OpenShift Registries: The Easy Way by Ben Schmaus and Daniel Messer (August 23, 2022)](https://cloud.redhat.com/blog/mirroring-openshift-registries-the-easy-way).
## Local DNS with `dnsmasq` (optional)
`/etc/dnsmasq.d/disconnected.conf`:
```=
interface=eth1
bind-interfaces
# server=10.0.0.1 # Use an upstream DNS server after ours
dhcp-range=192.168.0.180,192.168.0.199
dhcp-option=option:router,192.168.0.10
dhcp-option=option:ntp-server,192.168.0.10
auth-zone=airgap.local
host-record=api.cluster.airgap.local,192.168.0.100
host-record=api-int.cluster.airgap.local,192.168.0.100
host-record=ingress.cluster.airgap.local,192.168.0.101
cname=*.apps.cluster.airgap.local,ingress.cluster.airgap.local
# dhcp-host=00:de:ad:be:ef:01,192.168.0.179,bastion # if using static leases
```
## Install `mirror-registry` (aka mini Quay)
```
df -h /data
sudo setfacl -m u:$USER:rwx /data
wget https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/mirror-registry/latest/mirror-registry.tar.gz
### wget "https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/mirror-registry/latest/mirror-registry.tar.gz"
tar xvzf mirror-registry.tar.gz
./mirror-registry install --help
### password must be at least 8 characters and contain no whitespace
./mirror-registry install --quayRoot /data/mirror-registry --initUser admin --initPassword redhat123
sudo cp -v /data/mirror-registry/quay-rootCA/rootCA.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
podman login -u admin -p redhat123 $(hostname -f):8443
firewall-cmd --add-port 8443/tcp --permanent
firewall-cmd --reload
```
### Starting and stopping the `mirror-registry`
Running the `./mirror-registry install ...` results in several `systemd` services being created. You can see which services were created like this:
```
systemctl -a | grep quay
quay-app.service loaded active running Quay Container
quay-pod.service loaded active exited Infra Container for Quay
quay-postgres.service loaded active running PostgreSQL Podman Container for Quay
quay-redis.service loaded active running Redis Podman Container for Quay
```
These services will automatically start when the system is rebooted. The `quay-redis`, `quay-db`, and `quay-app` services depend on the `quay-pod` service. You can restart everything with one command:
```
systemctl restart quay-pod
```
### Alternative to `mirror-registry`: Use docker registry
First thing we need to do is create some directories for our container registry we will be setting up and changing the owner to our current user
```
sudo mkdir -p /opt/registry/{auth,certs,data}
sudo chown -R $USER /opt/registry
```
Next we will create a certificate for our registry to use
```
cd /opt/registry/certs
openssl req -newkey rsa:4096 -nodes -sha256 -x509 -days 365 \
-keyout domain.key -out domain.crt \
-addext "subjectAltName = DNS:registry.airgap.local"
Country Name (2 letter code) [XX]:US
State or Province Name (full name) []: North Carolina
Locality Name (eg, city) [Default City]:Raleigh
Organization Name (eg, company) [Default Company Ltd]:Red Hat
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:registry.airgap.local
Email Address []:<your-email-address>test@example.com
```
The common name is the one that matters the rest of these can be pretty much any value but the common name must be the correct name for your machine in order for the certificate to properly resolve
Next we will add simple password authentication on our registry we will just use the username openshift and the password redhat for demonstration purposes
```
htpasswd -bBc /opt/registry/auth/htpasswd openshift redhat
```
Now we can setup our resgistry to run, use the password and certificate we created and automaticatlly start in case the vm ever restarts
```
podman run -d --name mirror-registry \
-p 5000:5000 --restart=always \
-v /opt/registry/data:/var/lib/registry:z \
-v /opt/registry/auth:/auth:z \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
-v /opt/registry/certs:/certs:z \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
docker.io/library/registry:2
```
Update system trust store
```
sudo cp /opt/registry/certs/domain.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
curl -u openshift:redhat https://registry.airgap.local:5000/v2/_catalog
```
## Install the `oc-mirror` plugin
```
wget https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/oc-mirror.tar.gz
mkdir -p $HOME/.local/bin
tar xvzf ./oc-mirror.tar.gz -C $HOME/.local/bin
chmod a+x $HOME/.local/bin/oc-mirror
oc plugin list
oc mirror --help
```
## Add mirror-registry credentials to pull-secret
Download your "pull secret" from the Red Hat OpenShift Cluster Manager at https://console.redhat.com/openshift/install/pull-secret.
```
jq . pull-secret.txt
mkdir -p $HOME/.docker
mv -v pull-secret.txt $HOME/.docker/config.json
podman login -u openshift -p redhat --authfile $HOME/.docker/config.json $(hostname -f):8443
# OPTIONAL - remove the Insights & Telemetry connection
# https://docs.openshift.com/container-platform/4.12/support/remote_health_monitoring/opting-out-of-remote-health-reporting.html
podman logout --authfile $HOME/.docker/config.json cloud.openshift.com
# Confirm contents and create a backup
jq . $HOME/.docker/config.json
cp -v $HOME/.docker/config.json ~/pull-secret.json
```
```
$ oc-mirror list releases
$ oc-mirror list releases --version 4.12 --channels
$ oc-mirror list operators
$ oc-mirror list operators --version 4.12 --catalogs
$ oc-mirror list operators --version 4.12 \
--catalog registry.redhat.io/redhat/redhat-operator-index:v4.12 \
--package odf-operator \
--channel stable-4.12
Catalog -> "redhat", "certified", "marketplace", or "community"
Package -> "3scale-operator" | "advanced-cluster-management" ... "web-terminal"
Channel -> "stable-4.10 | stable-4.11"
Version -> "4.11.0 | 4.11.1 | 4.11.2"
$ oc-mirror init | tee imageset-config.yaml
$ vi imageset-config.yaml
```
EXAMPLE IMAGESETCONFIG
https://gist.github.com/johnsimcall/27a7bb96a76ee8021c13bc4e2c4ad9fa - NVIDIA
https://gist.github.com/johnsimcall/e27549db5da5cd26206fd0e4fd6b1a61 - OCP-Virt and ODF
## Add `imageContentSources:` to install-config.yaml
oc-mirror, from what I can tell, assumes that you already have a cluster installed because it outputs `ImageContentSourcePolicy` YAML, but unlike `oc adm release mirror` it does not output anything that can be added directly to the `install-config.yaml`.
You can copy-paste from the YAML files. You may have multiple YAML sections, one for `release-0`, another for `operator-0`, and one for `generic-0`. Installation *requires* the `release-0` section, which when added to my `install-config.yaml` looks like this...
```=
apiVersion: v1
baseDomain: example.redhat.com
...
additionalTrustBundlePolicy: Always
additionalTrustBundle: |
-----BEGIN CERTIFICATE-----
MII...lots of text...
-----END CERTIFICATE-----
imageContentSources:
- mirrors:
- jcall-testing.dota-lab.iad.redhat.com:8443/openshift/release
source: quay.io/openshift-release-dev/ocp-v4.0-art-dev
- mirrors:
- jcall-testing.dota-lab.iad.redhat.com:8443/openshift/release-images
source: quay.io/openshift-release-dev/ocp-release
```
## Create the installation files (agent-based)
I'll describe using the new agent-based installer here
### Sample agent-config.yaml
```yaml=
apiVersion: v1alpha1
kind: AgentConfig
metadata:
name: cluster
rendezvousIP: 172.31.255.252
hosts:
- hostname: master-0
interfaces:
- name: eno1
macAddress: 00:50:56:82:8c:dc
rootDeviceHints:
deviceName: /dev/sda
- hostname: master-1
interfaces:
- name: eno1
macAddress: 00:50:56:82:8c:dd
rootDeviceHints:
deviceName: /dev/sda
- hostname: master-2
interfaces:
- name: eno1
macAddress: 00:50:56:82:8c:de
- name: eno2
macAddress: 00:50:56:82:8c:df
networkConfig:
interfaces:
- name: eno2
type: ethernet
state: down
mac-address: 00:50:56:82:8c:df
ipv4:
enabled: false
```
### Make the agent-based ISO image and boot
```
mkdir ocp-airgap/
cp install-config.yaml.backup ocp-airgap/install-config.yaml
openshift-install agent create agent-config-template
# cp agent-config.yaml.backup ocp-airgap/agent-config.yaml
openshift-install --dir=ocp-airgap agent create image
cd ocp-airgap/
ln -s agent.x86_64.iso ocp-airgap-agent.iso
cd ..
openshift-install --dir=ocp-airgap agent wait-for bootstrap-complete --log-level=debug
openshift-install --dir=ocp-airgap agent wait-for install-complete --log-level=debug
```
## Day 2 Operations
### Disable the default OperatorHub Catalog Sources
```
oc patch OperatorHub cluster --type merge \
--patch '{"spec":{"disableAllDefaultSources":true}}'
```
### Create a new disconnected Operator Catalog
```
oc get catalogsource --all-namespaces
No resources found
oc create -f oc-mirror-workspace/results-1676565189/catalogSource-redhat-operator-index.yaml
catalogsource.operators.coreos.com/redhat-operator-index created
# oc create -f - <<< $(sed 's/name: redhat-operator-index/name: disconnected-redhat-operators/' oc-mirror-workspace/results-1676565189/catalogSource-redhat-operator-index.yaml)
# catalogsource.operators.coreos.com/disconnected-redhat-operators created
oc get catalogsource --all-namespaces
NAMESPACE NAME DISPLAY TYPE PUBLISHER AGE
openshift-marketplace redhat-operator-index grpc 4s
oc get pods -n openshift-marketplace
NAME READY STATUS RESTARTS AGE
marketplace-operator-645774fdc7-hnjgk 1/1 Running 0 20m
redhat-operator-index-sj6q7 1/1 Running 0 44s
oc logs -n openshift-marketplace redhat-operator-index-sj6q7
time="2023-02-16T17:10:14Z" level=info msg="serving registry" configs=/configs port=50051
```
### Disable Automatic Boot Sources for OpenShift Virtualization (if installed)
```bash
$ oc patch hco kubevirt-hyperconverged -n openshift-cnv --type json \
-p '[{"op": "replace",
"path": "/spec/featureGates/enableCommonBootImageImport",
"value": false}]'
```
### Update Content: Run the mirror process (to a filesystem)
I had trouble mirroring the kubevirt-operator because of a missing `virtio-win` container image. I also had to specify both the "stable" and "stable-1.1" channels of `redhat-oadp-operator` because the dependecy resolution wouldn't proceed with only "stable".
```=
---
kind: ImageSetConfiguration
apiVersion: mirror.openshift.io/v1alpha2
storageConfig:
local:
path: /data/oc-mirror-imageset-openshift-platform-plus-smaller
mirror:
platform:
channels:
- name: stable-4.12
type: ocp
additionalImages:
- name: registry.redhat.io/ubi8/ubi:latest
helm: {}
operators:
- catalog: registry.redhat.io/redhat/redhat-operator-index:v4.12
packages:
- name: advanced-cluster-management
channels:
- name: release-2.7
- name: cincinnati-operator
- name: cluster-logging
channels:
- name: stable
- name: compliance-operator
channels:
- name: release-0.1
- name: devworkspace-operator
channels:
- name: fast
- name: mta-operator
- name: mtc-operator
- name: mtr-operator
- name: odf-operator
channels:
- name: stable-4.12
- name: quay-bridge-operator
channels:
- name: stable-3.8
- name: quay-operator
channels:
- name: stable-3.8
- name: redhat-oadp-operator
channels:
- name: stable
- name: stable-1.1
- name: rhacs-operator
channels:
- name: latest
- name: web-terminal
```
```
$ grep path imageset-config-openshift-platform-plus-smaller.yaml
path: /data/oc-mirror-imageset-openshift-platform-plus-smaller
$ time oc mirror file:///data/oc-mirror-imageset-openshift-platform-plus-smaller \
--config=imageset-config-openshift-platform-plus-smaller.yaml 2>&1 \
| tee -a imageset-config-openshift-platform-plus-smaller.logs
$ time oc mirror file:///data/oc-mirror-imageset-openshift-platform-plus/ \
--config=imageset-config.yaml.all-operators 2>&1 \
| tee -a imageset-config.yaml.all-operators.filepath.logs
```
## Run the mirror process (into a Container Registry)
```
$ time oc mirror docker://$(hostname -f):8443 \
--config=imageset-config.yaml.all-operators 2>&1 \
| tee -a imageset-config.yaml.all-operators.filepath.logs
```
## Podman tips and tricks
### Manually explore an Operator Catalog (e.g. Red Hat, Community, Certified)
```
# podman run --rm -it --entrypoint=/bin/sh registry.redhat.io/redhat/redhat-operator-index:v4.12
## it's better to "image mount" because "run" the image doesn't provide `jq` or `yq`
podman pull registry.redhat.io/redhat/redhat-operator-index:v4.12
podman unshare
cd $(podman image mount registry.redhat.io/redhat/redhat-operator-index:v4.12)
ls configs
```
```
for i in $(jq --raw-output '. | select( .schema | contains("olm.package")) | .name' configs/*/catalog.json); do echo " - name: $i" | tee -a ~/imageset-config.yaml.all-operators; done
```
```
jq .name configs/*/catalog.json
# list available operators
# oc mirror list operators --version 4.12 --catalog registry.redhat.io/redhat/redhat-operator-index:v4.12
# the `oc mirror` command takes 60 seconds to run
ls configs/
# browse the metadata of an Operator
jq . configs/advanced-cluster-management/catalog.json | less -i
# report the available channels
# oc mirror list operators --version 4.12 --catalog registry.redhat.io/redhat/redhat-operator-index:v4.12 --package odf-operator --channel stable-4.12
jq '.schema, .name' configs/advanced-cluster-management/catalog.json
"olm.package"
"advanced-cluster-management"
"olm.channel"
"release-2.6"
"olm.channel"
"release-2.7"
```
## Poking the Quay API
I wanted to find a way to delete all of the Quay images and start over, without changing my CA certificate, or doing `./mirror-registry uninstall ...` I found that I could create a Quay "super user" token and use that on the command line. [Apparently tokens are deprecated](https://docs.quay.io/glossary/access-token.html), but I couldn't figure out how to make a Robot Account work for me. First create a new Organization. I called mine "adminorg", then [follow the instructions to create a token](https://access.redhat.com/documentation/en-us/red_hat_quay/3/html/red_hat_quay_api_guide/using_the_red_hat_quay_api#create_oauth_access_token).
```
# my TOKEN is 40 characters - a robot account's password/"token" is 64 characters
export TOKEN="abcdefghijklmnopqrstuvwxyz0123456789ABCD"
# sad that this query doesn't list repos as "org/repo"
curl -s -X GET -H "Authorization: Bearer $TOKEN" 'https://jcall-testing.dota-lab.iad.redhat.com:8443/api/v1/repository?public=true' | jq --raw-output '.repositories[].name'
# must delete "org/repo" instead of "repo"
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" 'https://jcall-testing.dota-lab.iad.redhat.com:8443/api/v1/repository/advanced-cluster-security/rhacs-operator-bundle'
# list all of the organizations, except the "adminorg" which holds my $TOKEN
curl -s -X GET -H "Authorization: Bearer $TOKEN" 'https://jcall-testing.dota-lab.iad.redhat.com:8443/api/v1/superuser/organizations/' | jq --raw-output '.organizations[].name' | grep -v adminorg
# deleting the org removes all repos
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "https://jcall-testing.dota-lab.iad.redhat.com:8443/api/v1/superuser/organizations/advanced-cluster-security"
# delete all of the orgs -- DANGER!!!
for ORG in $(curl -s -X GET -H "Authorization: Bearer $TOKEN" 'https://jcall-testing.dota-lab.iad.redhat.com:8443/api/v1/superuser/organizations/' | jq --raw-output '.organizations[].name' | grep -v adminorg ); do
echo "Deleting \"$ORG\" organization..."
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "https://jcall-testing.dota-lab.iad.redhat.com:8443/api/v1/superuser/organizations/$ORG"
done
```
### Extracting openshift-install from the mirrored content
```bash
oc adm release extract -a /run/user/1000/containers/auth.json --command=openshift-install airgap-bastion.dota-lab.iad.redhat.com:8443/openshift/release-images:4.12.51-x86_64
```