Repository: metal3-io/baremetal-operator
Pull request #715 (2020-11-10 15:41:28)

Look up existing nodes by MAC address
If an existing Ironic node, and a BMH share a MAC address, link them together.

Also includes some new functions in the test server for testing port logic

metal3-io-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/metal3-io/baremetal-operator/pull/715#" title="Author self-approved">honza</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **hardys**
You can assign the PR to them by writing `/assign @hardys` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=metal3-io%2Fbaremetal-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["hardys"]} -->

Patch:
diff --git a/docs/deploying.md b/docs/deploying.md
index 5f505e59..641ab9f3 100644
--- a/docs/deploying.md
+++ b/docs/deploying.md
@@ -158,10 +158,6 @@ for you.
   IRONIC_CACERT_FILE
 - IRONIC_INSPECTOR_CAKEY_FILE : CA certificate key path, unneeded if inspector
   certificates exist
-- MARIADB_KEY_FILE: Path to the key of MariaDB
-- MARIADB_CERT_FILE:  Path to the cert of MariaDB
-- MARIADB_CAKEY_FILE: Path to the CA key of MariaDB
-- MARIADB_CACERT_FILE: Path to the CA certificate of MariaDB
 
 Then run :
 
diff --git a/ironic-deployment/ironic/ironic.yaml b/ironic-deployment/ironic/ironic.yaml
index 1043186b..aee5326b 100644
--- a/ironic-deployment/ironic/ironic.yaml
+++ b/ironic-deployment/ironic/ironic.yaml
@@ -90,28 +90,12 @@ spec:
                 secretKeyRef:
                   name: mariadb-password
                   key: password
-        - name: ironic-log-watch
-          image: quay.io/metal3-io/ironic
-          imagePullPolicy: Always
-          command:
-            - /bin/runlogwatch.sh
-          volumeMounts:
-            - mountPath: /shared
-              name: ironic-data-volume
         - name: ironic-inspector
           image: quay.io/metal3-io/ironic-inspector
           imagePullPolicy: Always
           envFrom:
             - configMapRef:
                 name: ironic-bmo-configmap
-        - name: ironic-inspector-log-watch
-          image: quay.io/metal3-io/ironic-inspector
-          imagePullPolicy: Always
-          command:
-            - /bin/runlogwatch.sh
-          volumeMounts:
-            - mountPath: /shared
-              name: ironic-data-volume
       initContainers:
         - name: ironic-ipa-downloader
           image: quay.io/metal3-io/ironic-ipa-downloader
diff --git a/ironic-deployment/tls/default/kustomization.yaml b/ironic-deployment/tls/default/kustomization.yaml
index d0651118..197297c8 100644
--- a/ironic-deployment/tls/default/kustomization.yaml
+++ b/ironic-deployment/tls/default/kustomization.yaml
@@ -21,14 +21,6 @@ secretGenerator:
   - tls.crt=ironic-inspector.crt
   - tls.key=ironic-inspector.key
   type: "kubernetes.io/tls"
-- name: mariadb-ca-cert
-  files:
-  - tls.crt=mariadb-ca.crt
-- name: mariadb-cert
-  files:
-  - tls.crt=mariadb.crt
-  - tls.key=mariadb.key
-  type: "kubernetes.io/tls"
 
 patchesStrategicMerge:
 - tls.yaml
diff --git a/ironic-deployment/tls/default/tls.yaml b/ironic-deployment/tls/default/tls.yaml
index 6907b3a4..e62296f4 100644
--- a/ironic-deployment/tls/default/tls.yaml
+++ b/ironic-deployment/tls/default/tls.yaml
@@ -17,9 +17,6 @@ spec:
         - name: cert-ironic-inspector-ca
           mountPath: "/certs/ca/ironic-inspector"
           readOnly: true
-        - name: cert-mariadb-ca
-          mountPath: "/certs/ca/mariadb"
-          readOnly: true
       - name: ironic-conductor
         volumeMounts:
         - name: cert-ironic
@@ -31,9 +28,6 @@ spec:
         - name: cert-ironic-inspector-ca
           mountPath: "/certs/ca/ironic-inspector"
           readOnly: true
-        - name: cert-mariadb-ca
-          mountPath: "/certs/ca/mariadb"
-          readOnly: true
       - name: ironic-inspector
         volumeMounts:
         - name: cert-ironic-ca
@@ -42,14 +36,6 @@ spec:
         - name: cert-ironic-inspector
           mountPath: "/certs/ironic-inspector"
           readOnly: true
-      - name: mariadb
-        volumeMounts:
-        - name: cert-mariadb
-          mountPath: "/certs/mariadb"
-          readOnly: true
-        - name: cert-mariadb-ca
-          mountPath: "/certs/ca/mariadb"
-          readOnly: true
       volumes:
       - name: cert-ironic-ca
         secret:
@@ -62,10 +48,4 @@ spec:
           secretName: ironic-cert
       - name: cert-ironic-inspector
         secret:
-          secretName: ironic-inspector-cert
-      - name: cert-mariadb
-        secret:
-          secretName: mariadb-cert
-      - name: cert-mariadb-ca
-        secret:
-          secretName: mariadb-ca-cert
+          secretName: ironic-inspector-cert
\ No newline at end of file
diff --git a/ironic-deployment/tls/keepalived/kustomization.yaml b/ironic-deployment/tls/keepalived/kustomization.yaml
index e30ebeff..d6a3cb32 100644
--- a/ironic-deployment/tls/keepalived/kustomization.yaml
+++ b/ironic-deployment/tls/keepalived/kustomization.yaml
@@ -21,14 +21,6 @@ secretGenerator:
   - tls.crt=ironic-inspector.crt
   - tls.key=ironic-inspector.key
   type: "kubernetes.io/tls"
-- name: mariadb-ca-cert
-  files:
-  - tls.crt=mariadb-ca.crt
-- name: mariadb-cert
-  files:
-  - tls.crt=mariadb.crt
-  - tls.key=mariadb.key
-  type: "kubernetes.io/tls"
 
 patchesStrategicMerge:
 - tls.yaml
diff --git a/ironic-deployment/tls/keepalived/tls.yaml b/ironic-deployment/tls/keepalived/tls.yaml
index 6907b3a4..e62296f4 100644
--- a/ironic-deployment/tls/keepalived/tls.yaml
+++ b/ironic-deployment/tls/keepalived/tls.yaml
@@ -17,9 +17,6 @@ spec:
         - name: cert-ironic-inspector-ca
           mountPath: "/certs/ca/ironic-inspector"
           readOnly: true
-        - name: cert-mariadb-ca
-          mountPath: "/certs/ca/mariadb"
-          readOnly: true
       - name: ironic-conductor
         volumeMounts:
         - name: cert-ironic
@@ -31,9 +28,6 @@ spec:
         - name: cert-ironic-inspector-ca
           mountPath: "/certs/ca/ironic-inspector"
           readOnly: true
-        - name: cert-mariadb-ca
-          mountPath: "/certs/ca/mariadb"
-          readOnly: true
       - name: ironic-inspector
         volumeMounts:
         - name: cert-ironic-ca
@@ -42,14 +36,6 @@ spec:
         - name: cert-ironic-inspector
           mountPath: "/certs/ironic-inspector"
           readOnly: true
-      - name: mariadb
-        volumeMounts:
-        - name: cert-mariadb
-          mountPath: "/certs/mariadb"
-          readOnly: true
-        - name: cert-mariadb-ca
-          mountPath: "/certs/ca/mariadb"
-          readOnly: true
       volumes:
       - name: cert-ironic-ca
         secret:
@@ -62,10 +48,4 @@ spec:
           secretName: ironic-cert
       - name: cert-ironic-inspector
         secret:
-          secretName: ironic-inspector-cert
-      - name: cert-mariadb
-        secret:
-          secretName: mariadb-cert
-      - name: cert-mariadb-ca
-        secret:
-          secretName: mariadb-ca-cert
+          secretName: ironic-inspector-cert
\ No newline at end of file
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 2446675a..fad72631 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -230,6 +230,31 @@ func (p *ironicProvisioner) validateNode(ironicNode *nodes.Node) (errorMessage s
 	return "", nil
 }
 
+func (p *ironicProvisioner) listAllPorts(address string) ([]ports.Port, error) {
+	var allPorts []ports.Port
+
+	opts := ports.ListOpts{}
+
+	if address != "" {
+		opts.Address = address
+	}
+
+	pager := ports.List(p.client, opts)
+
+	if pager.Err != nil {
+		return allPorts, pager.Err
+	}
+
+	allPages, err := pager.AllPages()
+
+	if err != nil {
+		return allPorts, err
+	}
+
+	return ports.ExtractPorts(allPages)
+
+}
+
 // Look for an existing registration for the host in Ironic.
 func (p *ironicProvisioner) findExistingHost() (ironicNode *nodes.Node, err error) {
 	// Try to load the node by UUID
@@ -258,11 +283,48 @@ func (p *ironicProvisioner) findExistingHost() (ironicNode *nodes.Node, err erro
 		p.log.Info("found existing node by name")
 		return ironicNode, nil
 	case gophercloud.ErrDefault404:
-		return nil, nil
+		p.log.Info(
+			fmt.Sprintf("node with name %s doesn't exist", p.host.Name))
 	default:
 		return nil, errors.Wrap(err,
 			fmt.Sprintf("failed to find node by name %s", p.host.Name))
 	}
+
+	// Try to load the node by port address
+	p.log.Info("looking for existing node by MAC", "MAC", p.host.Spec.BootMACAddress)
+	allPorts, err := p.listAllPorts(p.host.Spec.BootMACAddress)
+
+	if err != nil {
+		p.log.Info("failed to find an existing port with address", "MAC", p.host.Spec.BootMACAddress)
+		return nil, nil
+	}
+
+	if len(allPorts) > 0 {
+		nodeUUID := allPorts[0].NodeUUID
+		ironicNode, err = nodes.Get(p.client, nodeUUID).Extract()
+		switch err.(type) {
+		case nil:
+			p.log.Info("found existing node by ID")
+
+			// If the node has a name, this means we didn't find it above.
+			if ironicNode.Name != "" {
+				return nil, errors.New(fmt.Sprint("node found by MAC but has a name: ", ironicNode.Name))
+			}
+
+			return ironicNode, nil
+		case gophercloud.ErrDefault404:
+			return nil, errors.Wrap(err,
+				fmt.Sprintf("port exists but linked node doesn't %s", nodeUUID))
+		default:
+			return nil, errors.Wrap(err,
+				fmt.Sprintf("port exists but failed to find linked node by ID %s", nodeUUID))
+		}
+	} else {
+		p.log.Info("port with address doesn't exist", "MAC", p.host.Spec.BootMACAddress)
+	}
+
+	return nil, nil
+
 }
 
 // ValidateManagementAccess registers the host with the provisioning
@@ -430,6 +492,29 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
 			p.log.Info("setting provisioning id", "ID", p.status.ID)
 		}
 
+		if ironicNode.Name == "" {
+			updates := nodes.UpdateOpts{
+				nodes.UpdateOperation{
+					Op:    nodes.ReplaceOp,
+					Path:  "/name",
+					Value: p.host.Name,
+				},
+			}
+			ironicNode, err = nodes.Update(p.client, ironicNode.UUID, updates).Extract()
+			switch err.(type) {
+			case nil:
+			case gophercloud.ErrDefault409:
+				p.log.Info("could not update ironic node name, busy")
+				result.Dirty = true
+				result.RequeueAfter = provisionRequeueDelay
+				return result, nil
+			default:
+				return result, errors.Wrap(err, "failed to update ironc node name")
+			}
+			p.log.Info("updated ironic node name")
+
+		}
+
 		// Look for the case where we previously enrolled this node
 		// and now the credentials have changed.
 		if credentialsChanged {
diff --git a/tools/deploy.sh b/tools/deploy.sh
index c1774469..6f0f6a26 100755
--- a/tools/deploy.sh
+++ b/tools/deploy.sh
@@ -21,8 +21,6 @@ DEPLOY_KEEPALIVED="${5,,}"
 
 IRONIC_HOST="${IRONIC_HOST}"
 IRONIC_HOST_IP="${IRONIC_HOST_IP}"
-MARIADB_HOST="${MARIADB_HOST:-"mariaDB"}"
-MARIADB_HOST_IP="${MARIADB_HOST_IP:-"127.0.0.1"}"
 KUBECTL_ARGS="${KUBECTL_ARGS:-""}"
 
 SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
@@ -126,30 +124,18 @@ if [ "${DEPLOY_TLS}" == "true" ]; then
     IRONIC_INSPECTOR_CERT_FILE="${IRONIC_INSPECTOR_CERT_FILE:-"${IRONIC_DATA_DIR}certs/ironic-inspector.crt"}"
     IRONIC_INSPECTOR_KEY_FILE="${IRONIC_INSPECTOR_KEY_FILE:-"${IRONIC_DATA_DIR}certs/ironic-inspector.key"}"
 
-    MARIADB_CACERT_FILE="${MARIADB_CACERT_FILE:-"${IRONIC_CACERT_FILE}"}"
-    MARIADB_CAKEY_FILE="${MARIADB_CAKEY_FILE:-"${IRONIC_CAKEY_FILE}"}"
-    MARIADB_CERT_FILE="${MARIADB_CERT_FILE:-"${IRONIC_DATA_DIR}certs/mariadb.crt"}"
-    MARIADB_KEY_FILE="${MARIADB_KEY_FILE:-"${IRONIC_DATA_DIR}certs/mariadb.key"}"
-
     if [ ! -f "${IRONIC_CAKEY_FILE}" ]; then
         openssl genrsa -out "${IRONIC_CAKEY_FILE}" 2048
     fi
     if [ ! -f "${IRONIC_INSPECTOR_CAKEY_FILE}" ]; then
         openssl genrsa -out "${IRONIC_INSPECTOR_CAKEY_FILE}" 2048
     fi
-    if [ ! -f "${MARIADB_CAKEY_FILE}" ]; then
-        openssl genrsa -out "${MARIADB_CAKEY_FILE}" 2048
-    fi
-
     if [ ! -f "${IRONIC_CACERT_FILE}" ]; then
         openssl req -x509 -new -nodes -key "${IRONIC_CAKEY_FILE}" -sha256 -days 1825 -out "${IRONIC_CACERT_FILE}" -subj /CN="ironic CA"/
     fi
     if [ ! -f "${IRONIC_INSPECTOR_CACERT_FILE}" ]; then
         openssl req -x509 -new -nodes -key "${IRONIC_INSPECTOR_CAKEY_FILE}" -sha256 -days 1825 -out "${IRONIC_INSPECTOR_CACERT_FILE}" -subj /CN="ironic inspector CA"/
     fi
-    if [ ! -f "${MARIADB_CACERT_FILE}" ]; then
-        openssl req -x509 -new -nodes -key "${MARIADB_CAKEY_FILE}" -sha256 -days 1825 -out "${MARIADB_CACERT_FILE}" -subj /CN="mariadb CA"/
-    fi
 
     if [ ! -f "${IRONIC_KEY_FILE}" ]; then
         openssl genrsa -out "${IRONIC_KEY_FILE}" 2048
@@ -157,9 +143,6 @@ if [ "${DEPLOY_TLS}" == "true" ]; then
     if [ ! -f "${IRONIC_INSPECTOR_KEY_FILE}" ]; then
         openssl genrsa -out "${IRONIC_INSPECTOR_KEY_FILE}" 2048
     fi
-    if [ ! -f "${MARIADB_KEY_FILE}" ]; then
-        openssl genrsa -out "${MARIADB_KEY_FILE}" 2048
-    fi
 
     if [ ! -f "${IRONIC_CERT_FILE}" ]; then
         openssl req -new -key "${IRONIC_KEY_FILE}" -out /tmp/ironic.csr -subj /CN="${IRONIC_HOST}"/
@@ -169,11 +152,6 @@ if [ "${DEPLOY_TLS}" == "true" ]; then
         openssl req -new -key "${IRONIC_INSPECTOR_KEY_FILE}" -out /tmp/ironic.csr -subj /CN="${IRONIC_HOST}"/
         openssl x509 -req -in /tmp/ironic.csr -CA "${IRONIC_INSPECTOR_CACERT_FILE}" -CAkey "${IRONIC_INSPECTOR_CAKEY_FILE}" -CAcreateserial -out "${IRONIC_INSPECTOR_CERT_FILE}" -days 825 -sha256 -extfile <(printf "subjectAltName=IP:%s" "${IRONIC_HOST_IP}")
     fi
-    if [ ! -f "${MARIADB_CERT_FILE}" ]; then
-        openssl req -new -key "${MARIADB_KEY_FILE}" -out /tmp/mariadb.csr -subj /CN="${MARIADB_HOST}"/
-        openssl x509 -req -in /tmp/mariadb.csr -CA "${MARIADB_CACERT_FILE}" -CAkey "${MARIADB_CAKEY_FILE}" -CAcreateserial -out "${MARIADB_CERT_FILE}" -days 825 -sha256 -extfile <(printf "subjectAltName=IP:%s" "${MARIADB_HOST_IP}")
-    fi
-
 
     if [ "${DEPLOY_BMO}" == "true" ]; then
         cp "${IRONIC_CACERT_FILE}" "${SCRIPTDIR}/config/tls/ca.crt"
@@ -187,17 +165,12 @@ if [ "${DEPLOY_TLS}" == "true" ]; then
         else
             IRONIC_TLS_SCENARIO="${SCRIPTDIR}/ironic-deployment/tls/default"
         fi
-        # Ensure that the MariaDB key file allow a non-owned user to read.
-        chmod 604 "${MARIADB_KEY_FILE}"
         cp "${IRONIC_CACERT_FILE}" "${IRONIC_TLS_SCENARIO}/ironic-ca.crt"
         cp "${IRONIC_INSPECTOR_CACERT_FILE}" "${IRONIC_TLS_SCENARIO}/ironic-inspector-ca.crt"
-        cp "${MARIADB_CACERT_FILE}" "${IRONIC_TLS_SCENARIO}/mariadb-ca.crt"
         cp "${IRONIC_CERT_FILE}" "${IRONIC_TLS_SCENARIO}/ironic.crt"
-        cp "${IRONIC_INSPECTOR_CERT_FILE}" "${IRONIC_TLS_SCENARIO}/ironic-inspector.crt"
-        cp "${MARIADB_CERT_FILE}" "${IRONIC_TLS_SCENARIO}/mariadb.crt"
         cp "${IRONIC_KEY_FILE}" "${IRONIC_TLS_SCENARIO}/ironic.key"
+        cp "${IRONIC_INSPECTOR_CERT_FILE}" "${IRONIC_TLS_SCENARIO}/ironic-inspector.crt"
         cp "${IRONIC_INSPECTOR_KEY_FILE}" "${IRONIC_TLS_SCENARIO}/ironic-inspector.key"
-        cp "${MARIADB_KEY_FILE}" "${IRONIC_TLS_SCENARIO}/mariadb.key"
     fi
 fi
 
@@ -245,8 +218,5 @@ if [ "${DEPLOY_TLS}" == "true" ]; then
         rm "${IRONIC_TLS_SCENARIO}/ironic.key"
         rm "${IRONIC_TLS_SCENARIO}/ironic-inspector.crt"
         rm "${IRONIC_TLS_SCENARIO}/ironic-inspector.key"
-        rm "${IRONIC_TLS_SCENARIO}/mariadb-ca.crt"
-        rm "${IRONIC_TLS_SCENARIO}/mariadb.crt"
-        rm "${IRONIC_TLS_SCENARIO}/mariadb.key"
     fi
 fi
diff --git a/tools/run_local_ironic.sh b/tools/run_local_ironic.sh
index 5ce3b5fb..5170bb81 100755
--- a/tools/run_local_ironic.sh
+++ b/tools/run_local_ironic.sh
@@ -13,7 +13,7 @@ CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}"
 HTTP_PORT="6180"
 PROVISIONING_IP="${PROVISIONING_IP:-"172.22.0.1"}"
 CLUSTER_PROVISIONING_IP="${CLUSTER_PROVISIONING_IP:-"172.22.0.2"}"
-PROVISIONING_CIDR="${PROVISIONING_CIDR:-"24"}" 
+PROVISIONING_CIDR="${PROVISIONING_CIDR:-"24"}"
 PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-"ironicendpoint"}"
 CLUSTER_DHCP_RANGE="${CLUSTER_DHCP_RANGE:-"172.22.0.10,172.22.0.100"}"
 
@@ -25,16 +25,6 @@ IRONIC_INSPECTOR_CACERT_FILE="${IRONIC_INSPECTOR_CACERT_FILE:-}"
 IRONIC_INSPECTOR_CERT_FILE="${IRONIC_INSPECTOR_CERT_FILE:-}"
 IRONIC_INSPECTOR_KEY_FILE="${IRONIC_INSPECTOR_KEY_FILE:-}"
 
-MARIADB_CACERT_FILE="${MARIADB_CACERT_FILE:-}"
-MARIADB_CERT_FILE="${MARIADB_CERT_FILE:-}"
-MARIADB_KEY_FILE="${MARIADB_KEY_FILE:-}"
-
-# Ensure that the MariaDB key file allow a non-owned user to read.
-if [ -n "${MARIADB_KEY_FILE}" ]
-then
-  chmod 604 "${MARIADB_KEY_FILE}"
-fi
-
 if [ -n "$IRONIC_CERT_FILE" ]; then
     export IRONIC_BASE_URL="https://${CLUSTER_PROVISIONING_IP}"
     if [ -z "$IRONIC_CACERT_FILE" ]; then
@@ -73,11 +63,9 @@ sudo "${CONTAINER_RUNTIME}" pull "$IRONIC_INSPECTOR_IMAGE"
 sudo "${CONTAINER_RUNTIME}" pull "$IRONIC_KEEPALIVED_IMAGE"
 
 CERTS_MOUNTS=""
-
 if [ -n "$IRONIC_CACERT_FILE" ]; then
      CERTS_MOUNTS="-v ${IRONIC_CACERT_FILE}:/certs/ca/ironic/tls.crt "
 fi
-
 if [ -n "$IRONIC_CERT_FILE" ]; then
      CERTS_MOUNTS="${CERTS_MOUNTS} -v ${IRONIC_CERT_FILE}:/certs/ironic/tls.crt "
 fi
@@ -94,16 +82,6 @@ if [ -n "$IRONIC_INSPECTOR_KEY_FILE" ]; then
      CERTS_MOUNTS="${CERTS_MOUNTS} -v ${IRONIC_INSPECTOR_KEY_FILE}:/certs/ironic-inspector/tls.key "
 fi
 
-if [ -n "$MARIADB_CACERT_FILE" ]; then
-     CERTS_MOUNTS="${CERTS_MOUNTS} -v ${MARIADB_CACERT_FILE}:/certs/ca/mariadb/tls.crt "
-fi
-if [ -n "$MARIADB_CERT_FILE" ]; then
-     CERTS_MOUNTS="${CERTS_MOUNTS} -v ${MARIADB_CERT_FILE}:/certs/mariadb/tls.crt "
-fi
-if [ -n "$MARIADB_KEY_FILE" ]; then
-     CERTS_MOUNTS="${CERTS_MOUNTS} -v ${MARIADB_KEY_FILE}:/certs/mariadb/tls.key "
-fi
-
 BASIC_AUTH_MOUNTS=""
 IRONIC_HTPASSWD=""
 if [ -n "$IRONIC_USERNAME" ]; then
@@ -132,8 +110,7 @@ sudo mkdir -p "$IRONIC_DATA_DIR/html/images"
 # By default, image directory points to dir having needed images when metal3-dev-env environment in use.
 # In other cases user has to store images beforehand.
 
-for name in ironic ironic-api ironic-conductor ironic-inspector dnsmasq httpd mariadb ipa-downloader \
-    ironic-endpoint-keepalived ironic-log-watch ironic-inspector-log-watch; do
+for name in ironic ironic-api ironic-conductor ironic-inspector dnsmasq httpd mariadb ipa-downloader ironic-endpoint-keepalived; do
     sudo "${CONTAINER_RUNTIME}" ps | grep -w "$name$" && sudo "${CONTAINER_RUNTIME}" kill "$name"
     sudo "${CONTAINER_RUNTIME}" ps --all | grep -w "$name$" && sudo "${CONTAINER_RUNTIME}" rm "$name" -f
 done
@@ -180,7 +157,7 @@ sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name httpd \
 # https://github.com/metal3-io/ironic/blob/master/runmariadb.sh
 # shellcheck disable=SC2086
 sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name mariadb \
-     ${POD} ${CERTS_MOUNTS} --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
+     ${POD} --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
      -v "$IRONIC_DATA_DIR:/shared" --entrypoint /bin/runmariadb \
      --env "MARIADB_PASSWORD=$mariadb_password" "${IRONIC_IMAGE}"
 
@@ -210,21 +187,9 @@ sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic-endpoin
     ${POD} --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
     -v "$IRONIC_DATA_DIR:/shared" "${IRONIC_KEEPALIVED_IMAGE}"
 
-# Start ironic-log-watch
-# shellcheck disable=SC2086
-sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic-log-watch \
-    ${POD} --entrypoint /bin/runlogwatch.sh \
-     -v "$IRONIC_DATA_DIR:/shared" "${IRONIC_IMAGE}"
-
 # Start Ironic Inspector
 # shellcheck disable=SC2086
 sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic-inspector \
      ${POD} ${CERTS_MOUNTS} ${BASIC_AUTH_MOUNTS} ${IRONIC_INSPECTOR_HTPASSWD} \
      --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
      -v "$IRONIC_DATA_DIR:/shared" "${IRONIC_INSPECTOR_IMAGE}"
-
-# Start ironic-inspector-log-watch
-# shellcheck disable=SC2086
-sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic-inspector-log-watch \
-    ${POD} --entrypoint /bin/runlogwatch.sh \
-     -v "$IRONIC_DATA_DIR:/shared" "${IRONIC_INSPECTOR_IMAGE}"


Test patch:
diff --git a/pkg/provisioner/ironic/delete_test.go b/pkg/provisioner/ironic/delete_test.go
deleted file mode 100644
index 16b8e438..00000000
--- a/pkg/provisioner/ironic/delete_test.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package ironic
-
-import (
-	"net/http"
-	"testing"
-	"time"
-
-	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
-	"github.com/stretchr/testify/assert"
-
-	"github.com/metal3-io/baremetal-operator/pkg/bmc"
-	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
-	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver"
-)
-
-func TestDelete(t *testing.T) {
-
-	nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
-
-	cases := []struct {
-		name      string
-		ironic    *testserver.IronicMock
-		inspector *testserver.InspectorMock
-		hostName  string
-
-		expectedDirty        bool
-		expectedRequestAfter time.Duration
-		expectedUpdate       *nodes.UpdateOperation
-
-		expectedError string
-	}{
-		{
-			name: "delete-host-fail",
-			ironic: testserver.NewIronic(t).Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    true,
-				},
-			).DeleteError(nodeUUID, http.StatusInternalServerError),
-			expectedError: "failed to remove host",
-		},
-		{
-			name: "delete-host-busy",
-			ironic: testserver.NewIronic(t).Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    true,
-				},
-			).DeleteError(nodeUUID, http.StatusConflict),
-			expectedDirty:        true,
-			expectedRequestAfter: 0,
-		},
-		{
-			name: "delete-host-not-found",
-			ironic: testserver.NewIronic(t).Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    true,
-				},
-			).DeleteError(nodeUUID, http.StatusNotFound),
-			expectedDirty:        true,
-			expectedRequestAfter: 0,
-		},
-		{
-			name: "delete-ok",
-			ironic: testserver.NewIronic(t).Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    true,
-				},
-			).Delete(nodeUUID),
-			expectedDirty:        true,
-			expectedRequestAfter: 0,
-		},
-		{
-			name: "host-not-found",
-
-			hostName: "worker-0",
-			ironic:   testserver.NewIronic(t).Ready().NodeError(nodeUUID, http.StatusGatewayTimeout),
-
-			expectedError: "failed to find existing host: failed to find node by ID 33ce8659-7400-4c68-9535-d10766f07a58: Expected HTTP response code \\[200\\].*",
-		},
-		{
-			name:   "not-ironic-node",
-			ironic: testserver.NewIronic(t).Ready().NoNode(nodeUUID).NoNode("myhost"),
-		},
-		{
-			name: "available-node",
-			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "available",
-				},
-			),
-			expectedDirty:        true,
-			expectedRequestAfter: provisionRequeueDelay,
-		},
-		{
-			name: "not-in-maintenance-update-fail",
-			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    false,
-				},
-			).NodeUpdateError(nodeUUID, http.StatusInternalServerError),
-
-			expectedError: "failed to set host maintenance flag",
-		},
-		{
-			name: "not-in-maintenance-update-busy",
-			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    false,
-				},
-			).NodeUpdateError(nodeUUID, http.StatusConflict),
-
-			expectedDirty:        true,
-			expectedRequestAfter: 0,
-		},
-		{
-			name: "not-in-maintenance-update",
-			ironic: testserver.NewIronic(t).Node(
-				nodes.Node{
-					UUID:           nodeUUID,
-					ProvisionState: "active",
-					Maintenance:    false,
-				},
-			).NodeUpdate(nodes.Node{
-				UUID: nodeUUID,
-			}),
-			expectedDirty:        true,
-			expectedRequestAfter: 0,
-			expectedUpdate: &nodes.UpdateOperation{
-				Op:    "replace",
-				Path:  "/maintenance",
-				Value: true,
-			},
-		},
-	}
-
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			if tc.ironic != nil {
-				tc.ironic.Start()
-				defer tc.ironic.Stop()
-			}
-
-			if tc.inspector != nil {
-				tc.inspector.Start()
-				defer tc.inspector.Stop()
-			}
-
-			host := makeHost()
-			host.Status.Provisioning.ID = nodeUUID
-
-			if tc.hostName != "" {
-				host.Name = tc.hostName
-			}
-
-			auth := clients.AuthConfig{Type: clients.NoAuth}
-			prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, nullEventPublisher,
-				tc.ironic.Endpoint(), auth, tc.inspector.Endpoint(), auth,
-			)
-			if err != nil {
-				t.Fatalf("could not create provisioner: %s", err)
-			}
-
-			result, err := prov.Delete()
-
-			assert.Equal(t, tc.expectedDirty, result.Dirty)
-			assert.Equal(t, tc.expectedRequestAfter, result.RequeueAfter)
-
-			if tc.expectedUpdate != nil {
-				assert.Equal(t, *tc.expectedUpdate, tc.ironic.GetLastNodeUpdateRequestFor(nodeUUID)[0])
-			}
-
-			if tc.expectedError == "" {
-				assert.NoError(t, err)
-			} else {
-				assert.Error(t, err)
-				assert.Regexp(t, tc.expectedError, err.Error())
-			}
-		})
-	}
-}
diff --git a/pkg/provisioner/ironic/testserver/ironic.go b/pkg/provisioner/ironic/testserver/ironic.go
index 494767c8..758f56e0 100644
--- a/pkg/provisioner/ironic/testserver/ironic.go
+++ b/pkg/provisioner/ironic/testserver/ironic.go
@@ -5,9 +5,11 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"testing"
 
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
+	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
 )
 
 // IronicMock is a test server that implements Ironic's semantics
@@ -87,19 +89,6 @@ func (m *IronicMock) buildURL(url string, method string) string {
 	return fmt.Sprintf("%s:%s", url, method)
 }
 
-// Delete configures the server with a valid response for [DELETE] /v1/nodes/ on the
-// specific node id
-func (m *IronicMock) Delete(id string) *IronicMock {
-	m.ResponseWithCode(m.buildURL("/v1/nodes/"+id, http.MethodDelete), "", http.StatusAccepted)
-	return m
-}
-
-// DeleteError configures the server with an error response for [DELETE] /v1/nodes/
-func (m *IronicMock) DeleteError(id string, errorCode int) *IronicMock {
-	m.ResponseWithCode(m.buildURL("/v1/nodes/"+id, http.MethodDelete), "", errorCode)
-	return m
-}
-
 // Node configures the server with a valid response for /v1/nodes/{name,uuid}
 func (m *IronicMock) Node(node nodes.Node) *IronicMock {
 	if node.UUID != "" {
@@ -111,12 +100,6 @@ func (m *IronicMock) Node(node nodes.Node) *IronicMock {
 	return m
 }
 
-// NodeUpdateError configures configures the server with an error response for [PATCH] /v1/nodes/{id}
-func (m *IronicMock) NodeUpdateError(id string, errorCode int) *IronicMock {
-	m.ResponseWithCode(m.buildURL("/v1/nodes/"+id, http.MethodPatch), "", errorCode)
-	return m
-}
-
 // NodeUpdate configures the server with a valid response for PATCH
 // for /v1/nodes/{name,uuid}
 func (m *IronicMock) NodeUpdate(node nodes.Node) *IronicMock {
@@ -230,3 +213,25 @@ func (m *IronicMock) WithNodeValidate(nodeUUID string) *IronicMock {
 	m.ResponseWithCode("/v1/nodes/"+nodeUUID+"/validate", "{}", http.StatusOK)
 	return m
 }
+
+// Port configures the server with a valid response for
+//    [GET] /v1/nodes/<node uuid>/ports
+//    [GET] /v1/ports
+//    [GET] /v1/ports?=address=<node uuid>
+func (m *IronicMock) Port(port ports.Port) *IronicMock {
+	if port.NodeUUID == "" {
+		m.MockServer.t.Error("When using withPort(), the port must include a NodeUUID.")
+	}
+
+	resp := map[string][]ports.Port{
+		"ports": {port},
+	}
+
+	address := url.QueryEscape(port.Address)
+
+	m.ResponseJSON(m.buildURL("/v1/nodes/"+port.NodeUUID+"/ports", http.MethodGet), resp)
+	m.ResponseJSON(m.buildURL("/v1/ports", http.MethodGet), resp)
+	m.ResponseJSON(m.buildURL("/v1/ports?address="+address, http.MethodGet), resp)
+
+	return m
+}
diff --git a/pkg/provisioner/ironic/validatemanagementaccess_test.go b/pkg/provisioner/ironic/validatemanagementaccess_test.go
index 81df8668..24d69fe9 100644
--- a/pkg/provisioner/ironic/validatemanagementaccess_test.go
+++ b/pkg/provisioner/ironic/validatemanagementaccess_test.go
@@ -1,9 +1,11 @@
 package ironic
 
 import (
+	"net/http"
 	"testing"
 
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
+	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/metal3-io/baremetal-operator/pkg/bmc"
@@ -179,3 +181,169 @@ func TestValidateManagementAccessNewCredentials(t *testing.T) {
 	newValues := updates[0].Value.(map[string]interface{})
 	assert.Equal(t, "test.bmc", newValues["test_address"])
 }
+
+func TestValidateManagementAccessLinkExistingIronicNodeByMAC(t *testing.T) {
+	// Create an Ironic node, and then create a host with a matching MAC
+	// Test to see if the node was found, and if the link is made
+
+	existingNode := nodes.Node{
+		UUID: "33ce8659-7400-4c68-9535-d10766f07a58",
+	}
+
+	existingNodePort := ports.Port{
+		NodeUUID: existingNode.UUID,
+		Address:  "11:11:11:11:11:11",
+	}
+
+	createCallback := func(node nodes.Node) {
+		t.Fatal("create callback should not be invoked for existing node")
+	}
+
+	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic.AddDefaultResponse("/v1/nodes/myhost", "GET", http.StatusNotFound, "")
+	ironic.AddDefaultResponse("/v1/nodes/"+existingNode.UUID, "PATCH", http.StatusOK, "{}")
+	ironic.Start()
+	defer ironic.Stop()
+
+	host := makeHost()
+	host.Spec.BootMACAddress = "11:11:11:11:11:11"
+	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
+
+	auth := clients.AuthConfig{Type: clients.NoAuth}
+	prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, nullEventPublisher,
+		ironic.Endpoint(), auth, testserver.NewInspector(t).Endpoint(), auth,
+	)
+	if err != nil {
+		t.Fatalf("could not create provisioner: %s", err)
+	}
+
+	result, err := prov.ValidateManagementAccess(false)
+	if err != nil {
+		t.Fatalf("error from ValidateManagementAccess: %s", err)
+	}
+	assert.Equal(t, "", result.ErrorMessage)
+	assert.NotEqual(t, "", host.Status.Provisioning.ID)
+}
+
+func TestValidateManagementAccessExistingPortWithWrongUUID(t *testing.T) {
+	// Create a node, and a port.  The port has a node uuid that doesn't match the node.
+	// ValidateManagementAccess should return an error.
+
+	existingNode := nodes.Node{
+		UUID: "33ce8659-7400-4c68-9535-d10766f07a58",
+	}
+
+	existingNodePort := ports.Port{
+		NodeUUID: "random-wrong-id",
+		Address:  "11:11:11:11:11:11",
+	}
+
+	createCallback := func(node nodes.Node) {
+		t.Fatal("create callback should not be invoked for existing node")
+	}
+
+	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic.AddDefaultResponse("/v1/nodes/myhost", "GET", http.StatusNotFound, "")
+	ironic.AddDefaultResponse("/v1/nodes/random-wrong-id", "GET", http.StatusNotFound, "")
+	ironic.Start()
+	defer ironic.Stop()
+
+	host := makeHost()
+	host.Spec.BootMACAddress = "11:11:11:11:11:11"
+	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
+
+	auth := clients.AuthConfig{Type: clients.NoAuth}
+	prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, nullEventPublisher,
+		ironic.Endpoint(), auth, testserver.NewInspector(t).Endpoint(), auth,
+	)
+	if err != nil {
+		t.Fatalf("could not create provisioner: %s", err)
+	}
+
+	_, err = prov.ValidateManagementAccess(false)
+	assert.EqualError(t, err, "failed to find existing host: port exists but linked node doesn't random-wrong-id: Resource not found")
+}
+
+func TestValidateManagementAccessExistingPortButHasName(t *testing.T) {
+	// Create a node, and a port.
+	// The port is linked to the node.
+	// The port address matches the BMH BootMACAddress.
+	// The node has a name, and the name doesn't match the BMH.
+	// ValidateManagementAccess should return an error.
+
+	existingNode := nodes.Node{
+		UUID: "33ce8659-7400-4c68-9535-d10766f07a58",
+		Name: "wrong-name",
+	}
+
+	existingNodePort := ports.Port{
+		NodeUUID: existingNode.UUID,
+		Address:  "11:11:11:11:11:11",
+	}
+
+	createCallback := func(node nodes.Node) {
+		t.Fatal("create callback should not be invoked for existing node")
+	}
+
+	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic.AddDefaultResponse("/v1/nodes/myhost", "GET", http.StatusNotFound, "")
+	ironic.Start()
+	defer ironic.Stop()
+
+	host := makeHost()
+	host.Spec.BootMACAddress = "11:11:11:11:11:11"
+	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
+
+	auth := clients.AuthConfig{Type: clients.NoAuth}
+	prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, nullEventPublisher,
+		ironic.Endpoint(), auth, testserver.NewInspector(t).Endpoint(), auth,
+	)
+	if err != nil {
+		t.Fatalf("could not create provisioner: %s", err)
+	}
+
+	_, err = prov.ValidateManagementAccess(false)
+	assert.EqualError(t, err, "failed to find existing host: node found by MAC but has a name: wrong-name")
+}
+
+func TestValidateManagementAccessAddTwoHostsWithSameMAC(t *testing.T) {
+
+	existingNode := nodes.Node{
+		UUID: "33ce8659-7400-4c68-9535-d10766f07a58",
+		Name: "myhost",
+	}
+
+	existingNodePort := ports.Port{
+		NodeUUID: existingNode.UUID,
+		Address:  "11:11:11:11:11:11",
+	}
+
+	createCallback := func(node nodes.Node) {
+		t.Fatal("create callback should not be invoked for existing node")
+	}
+
+	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic.Start()
+	defer ironic.Stop()
+
+	host := makeHost()
+
+	// This value is differen than the port that actually exists
+	host.Spec.BootMACAddress = "22:22:22:22:22:22"
+	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
+
+	auth := clients.AuthConfig{Type: clients.NoAuth}
+	prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, nullEventPublisher,
+		ironic.Endpoint(), auth, testserver.NewInspector(t).Endpoint(), auth,
+	)
+	if err != nil {
+		t.Fatalf("could not create provisioner: %s", err)
+	}
+
+	result, err := prov.ValidateManagementAccess(false)
+	if err != nil {
+		t.Fatalf("error from ValidateManagementAccess: %s", err)
+	}
+	assert.Equal(t, "", result.ErrorMessage)
+	assert.NotEqual(t, "", host.Status.Provisioning.ID)
+}