From bfa6a7d995bf35472a220028597f10ad4ebcf9dd Mon Sep 17 00:00:00 2001
From: Gregory Thiemonge <gthiemon@redhat.com>
Date: Fri, 4 Oct 2024 17:21:07 +0200
Subject: [PATCH] Add more coverage in octavia controller function tests

---
 tests/functional/api_fixture.go             |  25 +-
 tests/functional/base_test.go               |  91 +++-
 tests/functional/neutron_api_fixture.go     | 532 +++++++++++++++++++-
 tests/functional/nova_api_fixture.go        |  90 ++++
 tests/functional/octavia_controller_test.go | 443 +++++++++++++---
 5 files changed, 1094 insertions(+), 87 deletions(-)

diff --git a/tests/functional/api_fixture.go b/tests/functional/api_fixture.go
index 66019a6a..f33bc7c5 100644
--- a/tests/functional/api_fixture.go
+++ b/tests/functional/api_fixture.go
@@ -27,6 +27,7 @@ import (
 	. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
 
 	"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/users"
 	keystone_helpers "github.com/openstack-k8s-operators/keystone-operator/api/test/helpers"
 
 	api "github.com/openstack-k8s-operators/lib-common/modules/test/apis"
@@ -172,11 +173,13 @@ func keystoneGetProject(
 	f.APIFixture.Log.Info(fmt.Sprintf("GetProject returns %s", string(bytes)))
 }
 
-func SetupAPIFixtures(logger logr.Logger) (
-	*keystone_helpers.KeystoneAPIFixture,
-	*NovaAPIFixture,
-	*NeutronAPIFixture,
-) {
+type APIFixtures struct {
+	Keystone *keystone_helpers.KeystoneAPIFixture
+	Nova     *NovaAPIFixture
+	Neutron  *NeutronAPIFixture
+}
+
+func SetupAPIFixtures(logger logr.Logger) APIFixtures {
 	nova := NewNovaAPIFixtureWithServer(logger)
 	nova.Setup()
 	DeferCleanup(nova.Cleanup)
@@ -188,6 +191,12 @@ func SetupAPIFixtures(logger logr.Logger) (
 	neutronURL := neutron.Endpoint()
 
 	keystone := keystone_helpers.NewKeystoneAPIFixtureWithServer(logger)
+	keystone.Users = map[string]users.User{
+		"octavia": {
+			Name: "octavia",
+			ID:   uuid.New().String(),
+		},
+	}
 	keystone.Setup(
 		api.Handler{Pattern: "/", Func: keystone.HandleVersion},
 		api.Handler{Pattern: "/v3/users", Func: keystone.HandleUsers},
@@ -205,5 +214,9 @@ func SetupAPIFixtures(logger logr.Logger) (
 			}
 		}})
 	DeferCleanup(keystone.Cleanup)
-	return keystone, nova, neutron
+	return APIFixtures{
+		Keystone: keystone,
+		Nova:     nova,
+		Neutron:  neutron,
+	}
 }
diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go
index 31c4022f..5776804f 100644
--- a/tests/functional/base_test.go
+++ b/tests/functional/base_test.go
@@ -17,12 +17,14 @@ limitations under the License.
 package functional_test
 
 import (
+	"encoding/json"
 	"fmt"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
 	. "github.com/onsi/gomega"    //revive:disable:dot-imports
 
+	networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
@@ -32,6 +34,7 @@ import (
 	condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
 	"github.com/openstack-k8s-operators/lib-common/modules/common/endpoint"
 	octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1"
+	"github.com/openstack-k8s-operators/octavia-operator/pkg/octavia"
 )
 
 const (
@@ -106,8 +109,14 @@ func SimulateKeystoneReady(
 
 func GetDefaultOctaviaSpec() map[string]interface{} {
 	return map[string]interface{}{
-		"databaseInstance": "test-octavia-db-instance",
-		"secret":           SecretName,
+		"databaseInstance":           "test-octavia-db-instance",
+		"secret":                     SecretName,
+		"octaviaNetworkAttachment":   "octavia-attachement",
+		"databaseAccount":            "octavia-db-account",
+		"persistenceDatabaseAccount": "octavia-persistence-db-account",
+		"lbMgmtNetwork": map[string]interface{}{
+			"availabilityZones": []string{"az0"},
+		},
 	}
 }
 
@@ -215,3 +224,81 @@ func OctaviaAPIConditionGetter(name types.NamespacedName) condition.Conditions {
 	instance := GetOctaviaAPI(name)
 	return instance.Status.Conditions
 }
+
+func SimulateOctaviaAPIReady(name types.NamespacedName) {
+	Eventually(func(g Gomega) {
+		octaviaAPI := GetOctaviaAPI(name)
+		octaviaAPI.Status.ObservedGeneration = octaviaAPI.Generation
+		octaviaAPI.Status.ReadyCount = 1
+		g.Expect(k8sClient.Status().Update(ctx, octaviaAPI)).To(Succeed())
+	}, timeout, interval).Should(Succeed())
+}
+
+func CreateNAD(name types.NamespacedName) client.Object {
+	raw := map[string]interface{}{
+		"apiVersion": "k8s.cni.cncf.io/v1",
+		"kind":       "NetworkAttachmentDefinition",
+		"metadata": map[string]interface{}{
+			"name":      name.Name,
+			"namespace": name.Namespace,
+		},
+		"spec": map[string]interface{}{
+			"config": `{
+				"cniVersion": "0.3.1",
+				"name": "octavia",
+				"type": "bridge",
+				"bridge": "octbr",
+				"ipam": {
+				    "type": "whereabouts",
+				    "range": "172.23.0.0/24",
+				    "range_start": "172.23.0.30",
+				    "range_end": "172.23.0.70",
+				    "routes": [{
+				        "dst": "172.24.0.0/16",
+				        "gw" : "172.23.0.150"
+		            }]
+		        }
+			}`,
+		},
+	}
+	return th.CreateUnstructured(raw)
+}
+
+func GetNADConfig(name types.NamespacedName) *octavia.NADConfig {
+	nad := &networkv1.NetworkAttachmentDefinition{}
+	Eventually(func(g Gomega) {
+		g.Expect(k8sClient.Get(ctx, name, nad)).Should(Succeed())
+	}, timeout, interval).Should(Succeed())
+
+	nadConfig := &octavia.NADConfig{}
+	jsonDoc := []byte(nad.Spec.Config)
+	err := json.Unmarshal(jsonDoc, nadConfig)
+	if err != nil {
+		return nil
+	}
+
+	return nadConfig
+}
+
+func CreateNode(name types.NamespacedName) client.Object {
+	raw := map[string]interface{}{
+		"apiVersion": "v1",
+		"kind":       "Node",
+		"metadata": map[string]interface{}{
+			"name":      name.Name,
+			"namespace": name.Namespace,
+		},
+		"spec": map[string]interface{}{},
+	}
+	return th.CreateUnstructured(raw)
+
+}
+
+func CreateSSHPubKey() client.Object {
+	return th.CreateConfigMap(types.NamespacedName{
+		Name:      "octavia-ssh-pubkey",
+		Namespace: namespace,
+	}, map[string]interface{}{
+		"key": []byte("public key"),
+	})
+}
diff --git a/tests/functional/neutron_api_fixture.go b/tests/functional/neutron_api_fixture.go
index e7bf455e..5ca4300b 100644
--- a/tests/functional/neutron_api_fixture.go
+++ b/tests/functional/neutron_api_fixture.go
@@ -24,15 +24,27 @@ import (
 	"strings"
 
 	"github.com/go-logr/logr"
+	"github.com/google/uuid"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/quotas"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
 
 	api "github.com/openstack-k8s-operators/lib-common/modules/test/apis"
 )
 
 type NeutronAPIFixture struct {
 	api.APIFixture
-	Quotas       map[string]quotas.Quota
-	DefaultQuota quotas.Quota
+	Quotas         map[string]quotas.Quota
+	DefaultQuota   quotas.Quota
+	Networks       map[string]networks.Network
+	Subnets        map[string]subnets.Subnet
+	SecGroups      map[string]groups.SecGroup
+	Ports          map[string]ports.Port
+	Routers        map[string]routers.Router
+	InterfaceInfos map[string]routers.InterfaceInfo
 }
 
 func (f *NeutronAPIFixture) registerHandler(handler api.Handler) {
@@ -40,9 +52,517 @@ func (f *NeutronAPIFixture) registerHandler(handler api.Handler) {
 }
 
 func (f *NeutronAPIFixture) Setup() {
+	f.registerHandler(api.Handler{Pattern: "/v2.0/networks/", Func: f.networkHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/networks", Func: f.networkHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/subnets/", Func: f.subnetHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/subnets", Func: f.subnetHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/security-groups/", Func: f.securityGroupHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/security-groups", Func: f.securityGroupHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/ports/", Func: f.portHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/ports", Func: f.portHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/routers/", Func: f.routerHandler})
+	f.registerHandler(api.Handler{Pattern: "/v2.0/routers", Func: f.routerHandler})
 	f.registerHandler(api.Handler{Pattern: "/v2.0/quotas/", Func: f.quotasHandler})
 }
 
+// Network
+func (f *NeutronAPIFixture) networkHandler(w http.ResponseWriter, r *http.Request) {
+	f.LogRequest(r)
+	switch r.Method {
+	case "GET":
+		f.getNetwork(w, r)
+	case "POST":
+		f.postNetwork(w, r)
+	default:
+		f.UnexpectedRequest(w, r)
+		return
+	}
+}
+
+func (f *NeutronAPIFixture) getNetwork(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	if len(items) == 4 {
+		var n struct {
+			Networks []networks.Network `json:"networks"`
+		}
+		n.Networks = []networks.Network{}
+		query := r.URL.Query()
+		name := query["name"]
+		tenantID := query["tenant_id"]
+		for _, network := range f.Networks {
+			if len(name) > 0 && name[0] != network.Name {
+				continue
+			}
+			if len(tenantID) > 0 && tenantID[0] != network.TenantID {
+				continue
+			}
+			n.Networks = append(n.Networks, network)
+		}
+		bytes, err := json.Marshal(&n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+func (f *NeutronAPIFixture) postNetwork(w http.ResponseWriter, r *http.Request) {
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var n struct {
+		Network networks.Network `json:"network"`
+	}
+
+	err = json.Unmarshal(bytes, &n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	networkID := uuid.New().String()
+	n.Network.ID = networkID
+	f.Networks[networkID] = n.Network
+
+	bytes, err = json.Marshal(&n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+// Subnet
+func (f *NeutronAPIFixture) subnetHandler(w http.ResponseWriter, r *http.Request) {
+	f.LogRequest(r)
+	switch r.Method {
+	case "GET":
+		f.getSubnet(w, r)
+	case "POST":
+		f.postSubnet(w, r)
+	case "PUT":
+		f.putSubnet(w, r)
+	default:
+		f.UnexpectedRequest(w, r)
+		return
+	}
+}
+
+func (f *NeutronAPIFixture) getSubnet(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	if len(items) == 4 {
+		var n struct {
+			Subnets []subnets.Subnet `json:"subnets"`
+		}
+		n.Subnets = []subnets.Subnet{}
+		query := r.URL.Query()
+		name := query["name"]
+		tenantID := query["tenant_id"]
+		networkID := query["network_id"]
+		ipVersion := query["ip_version"]
+		for _, subnet := range f.Subnets {
+			if len(name) > 0 && name[0] != subnet.Name {
+				continue
+			}
+			if len(tenantID) > 0 && tenantID[0] != subnet.TenantID {
+				continue
+			}
+			if len(networkID) > 0 && networkID[0] != subnet.NetworkID {
+				continue
+			}
+			if len(ipVersion) > 0 && ipVersion[0] != fmt.Sprintf("%d", subnet.IPVersion) {
+				continue
+			}
+			n.Subnets = append(n.Subnets, subnet)
+		}
+		bytes, err := json.Marshal(&n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+func (f *NeutronAPIFixture) postSubnet(w http.ResponseWriter, r *http.Request) {
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var n struct {
+		Subnet subnets.Subnet `json:"subnet"`
+	}
+
+	err = json.Unmarshal(bytes, &n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	networkID := n.Subnet.NetworkID
+	subnetID := uuid.New().String()
+	n.Subnet.ID = subnetID
+	f.Subnets[subnetID] = n.Subnet
+
+	network := f.Networks[networkID]
+	network.Subnets = append(network.Subnets, subnetID)
+	f.Networks[networkID] = network
+
+	bytes, err = json.Marshal(&n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+func (f *NeutronAPIFixture) putSubnet(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	subnetID := items[4]
+
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var n struct {
+		Subnet subnets.Subnet `json:"subnet"`
+	}
+
+	err = json.Unmarshal(bytes, &n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	subnet := f.Subnets[subnetID]
+	if len(n.Subnet.HostRoutes) > 0 {
+		subnet.HostRoutes = n.Subnet.HostRoutes
+	}
+	f.Subnets[subnetID] = subnet
+
+	bytes, err = json.Marshal(&n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+// SecGroup
+func (f *NeutronAPIFixture) securityGroupHandler(w http.ResponseWriter, r *http.Request) {
+	f.LogRequest(r)
+	switch r.Method {
+	case "GET":
+		f.getSecurityGroup(w, r)
+	case "POST":
+		f.postSecurityGroup(w, r)
+	default:
+		f.UnexpectedRequest(w, r)
+		return
+	}
+}
+
+func (f *NeutronAPIFixture) getSecurityGroup(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	if len(items) == 4 {
+		var n struct {
+			SecGroups []groups.SecGroup `json:"security_groups"`
+		}
+		n.SecGroups = []groups.SecGroup{}
+		query := r.URL.Query()
+		name := query["name"]
+		tenantID := query["tenant_id"]
+		for _, securityGroup := range f.SecGroups {
+			if len(name) > 0 && name[0] != securityGroup.Name {
+				continue
+			}
+			if len(tenantID) > 0 && tenantID[0] != securityGroup.TenantID {
+				continue
+			}
+			n.SecGroups = append(n.SecGroups, securityGroup)
+		}
+		bytes, err := json.Marshal(&n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+func (f *NeutronAPIFixture) postSecurityGroup(w http.ResponseWriter, r *http.Request) {
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var n struct {
+		SecGroup groups.SecGroup `json:"security_group"`
+	}
+
+	err = json.Unmarshal(bytes, &n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	securityGroupID := uuid.New().String()
+	n.SecGroup.ID = securityGroupID
+	f.SecGroups[securityGroupID] = n.SecGroup
+
+	bytes, err = json.Marshal(&n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+// Port
+func (f *NeutronAPIFixture) portHandler(w http.ResponseWriter, r *http.Request) {
+	f.LogRequest(r)
+	switch r.Method {
+	case "GET":
+		f.getPort(w, r)
+	case "POST":
+		f.postPort(w, r)
+	default:
+		f.UnexpectedRequest(w, r)
+		return
+	}
+}
+
+func (f *NeutronAPIFixture) getPort(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	if len(items) == 4 {
+		var n struct {
+			Ports []ports.Port `json:"ports"`
+		}
+		n.Ports = []ports.Port{}
+		query := r.URL.Query()
+		name := query["name"]
+		tenantID := query["tenant_id"]
+		networkID := query["network_id"]
+		for _, port := range f.Ports {
+			if len(name) > 0 && name[0] != port.Name {
+				continue
+			}
+			if len(tenantID) > 0 && tenantID[0] != port.TenantID {
+				continue
+			}
+			if len(networkID) > 0 && networkID[0] != port.NetworkID {
+				continue
+			}
+			n.Ports = append(n.Ports, port)
+		}
+		bytes, err := json.Marshal(&n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+func (f *NeutronAPIFixture) postPort(w http.ResponseWriter, r *http.Request) {
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var n struct {
+		Port ports.Port `json:"port"`
+	}
+
+	err = json.Unmarshal(bytes, &n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	network := f.Networks[n.Port.NetworkID]
+
+	portID := uuid.New().String()
+	n.Port.ID = portID
+	n.Port.FixedIPs = []ports.IP{
+		{
+			IPAddress: fmt.Sprintf("%s.ipaddress", portID),
+			SubnetID:  network.Subnets[0],
+		},
+	}
+	f.Ports[portID] = n.Port
+
+	bytes, err = json.Marshal(&n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+// Router
+func (f *NeutronAPIFixture) routerHandler(w http.ResponseWriter, r *http.Request) {
+	f.LogRequest(r)
+	switch r.Method {
+	case "GET":
+		f.getRouter(w, r)
+	case "POST":
+		f.postRouter(w, r)
+	case "PUT":
+		f.putRouter(w, r)
+	default:
+		f.UnexpectedRequest(w, r)
+		return
+	}
+}
+
+func (f *NeutronAPIFixture) getRouter(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	if len(items) == 4 {
+		var n struct {
+			Routers []routers.Router `json:"routers"`
+		}
+		n.Routers = []routers.Router{}
+		query := r.URL.Query()
+		name := query["name"]
+		tenantID := query["tenant_id"]
+		for _, router := range f.Routers {
+			if len(name) > 0 && name[0] != router.Name {
+				continue
+			}
+			if len(tenantID) > 0 && tenantID[0] != router.TenantID {
+				continue
+			}
+			n.Routers = append(n.Routers, router)
+		}
+		bytes, err := json.Marshal(&n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+func (f *NeutronAPIFixture) postRouter(w http.ResponseWriter, r *http.Request) {
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var n struct {
+		Router routers.Router `json:"router"`
+	}
+
+	err = json.Unmarshal(bytes, &n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	routerID := uuid.New().String()
+	n.Router.ID = routerID
+	f.Routers[routerID] = n.Router
+
+	bytes, err = json.Marshal(&n)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+func (f *NeutronAPIFixture) putRouter(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	routerID := items[4]
+	action := items[5]
+
+	if action == "add_router_interface" {
+		bytes, err := io.ReadAll(r.Body)
+		if err != nil {
+			f.InternalError(err, "Error reading request body", w, r)
+			return
+		}
+
+		var n struct {
+			SubnetID string `json:"subnet_id"`
+			PortID   string `json:"port_id"`
+		}
+
+		err = json.Unmarshal(bytes, &n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		var subnetID string
+		if n.SubnetID == "" {
+			subnetID = f.Ports[n.PortID].FixedIPs[0].SubnetID
+		} else {
+			subnetID = n.SubnetID
+		}
+		f.InterfaceInfos[fmt.Sprintf("%s:%s", routerID, subnetID)] = routers.InterfaceInfo{
+			SubnetID: subnetID,
+			PortID:   n.PortID,
+		}
+
+		bytes, err = json.Marshal(&n)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+// Quota
 func (f *NeutronAPIFixture) quotasHandler(w http.ResponseWriter, r *http.Request) {
 	f.LogRequest(r)
 	switch r.Method {
@@ -125,7 +645,13 @@ func AddNeutronAPIFixture(log logr.Logger, server *api.FakeAPIServer) *NeutronAP
 			SecurityGroup:     10,
 			SecurityGroupRule: 10,
 		},
-		Quotas: map[string]quotas.Quota{},
+		Quotas:         map[string]quotas.Quota{},
+		Networks:       map[string]networks.Network{},
+		Subnets:        map[string]subnets.Subnet{},
+		SecGroups:      map[string]groups.SecGroup{},
+		Ports:          map[string]ports.Port{},
+		Routers:        map[string]routers.Router{},
+		InterfaceInfos: map[string]routers.InterfaceInfo{},
 	}
 	return fixture
 }
diff --git a/tests/functional/nova_api_fixture.go b/tests/functional/nova_api_fixture.go
index e31a88f1..7b0d6b97 100644
--- a/tests/functional/nova_api_fixture.go
+++ b/tests/functional/nova_api_fixture.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/go-logr/logr"
 
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
 	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
 
 	api "github.com/openstack-k8s-operators/lib-common/modules/test/apis"
@@ -34,6 +35,7 @@ type NovaAPIFixture struct {
 	api.APIFixture
 	QuotaSets       map[string]quotasets.QuotaSet
 	DefaultQuotaSet quotasets.QuotaSet
+	KeyPairs        map[string]keypairs.KeyPair
 }
 
 func (f *NovaAPIFixture) registerHandler(handler api.Handler) {
@@ -41,9 +43,96 @@ func (f *NovaAPIFixture) registerHandler(handler api.Handler) {
 }
 
 func (f *NovaAPIFixture) Setup() {
+	f.registerHandler(api.Handler{Pattern: "/os-keypairs", Func: f.keyPairHandler})
+	f.registerHandler(api.Handler{Pattern: "/os-keypairs/", Func: f.keyPairHandler})
 	f.registerHandler(api.Handler{Pattern: "/os-quota-sets/", Func: f.quotaSetsHandler})
 }
 
+func (f *NovaAPIFixture) keyPairHandler(w http.ResponseWriter, r *http.Request) {
+	f.LogRequest(r)
+	switch r.Method {
+	case "GET":
+		f.getKeyPair(w, r)
+	case "POST":
+		f.postKeyPair(w, r)
+	case "DELETE":
+		f.deleteKeyPair(w, r)
+	default:
+		f.UnexpectedRequest(w, r)
+		return
+	}
+}
+
+func (f *NovaAPIFixture) getKeyPair(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	if len(items) == 3 {
+		type pair struct {
+			KeyPair keypairs.KeyPair `json:"keypair"`
+		}
+		var k struct {
+			KeyPairs []pair `json:"keypairs"`
+		}
+		k.KeyPairs = []pair{}
+		query := r.URL.Query()
+		userID := query["user_id"]
+		for _, keypair := range f.KeyPairs {
+			if len(userID) > 0 && userID[0] != keypair.UserID {
+				continue
+			}
+			k.KeyPairs = append(k.KeyPairs, pair{KeyPair: keypair})
+		}
+		bytes, err := json.Marshal(&k)
+		if err != nil {
+			f.InternalError(err, "Error during marshalling response", w, r)
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(200)
+		fmt.Fprint(w, string(bytes))
+	}
+}
+
+func (f *NovaAPIFixture) postKeyPair(w http.ResponseWriter, r *http.Request) {
+	bytes, err := io.ReadAll(r.Body)
+	if err != nil {
+		f.InternalError(err, "Error reading request body", w, r)
+		return
+	}
+
+	var k struct {
+		KeyPair keypairs.KeyPair `json:"keypair"`
+	}
+
+	err = json.Unmarshal(bytes, &k)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	f.KeyPairs[k.KeyPair.Name] = k.KeyPair
+
+	bytes, err = json.Marshal(&k)
+	if err != nil {
+		f.InternalError(err, "Error during marshalling response", w, r)
+		return
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(201)
+	fmt.Fprint(w, string(bytes))
+}
+
+func (f *NovaAPIFixture) deleteKeyPair(w http.ResponseWriter, r *http.Request) {
+	items := strings.Split(r.URL.Path, "/")
+	keypair := items[len(items)-1]
+
+	delete(f.KeyPairs, keypair)
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(202)
+}
+
 func (f *NovaAPIFixture) quotaSetsHandler(w http.ResponseWriter, r *http.Request) {
 	f.LogRequest(r)
 	switch r.Method {
@@ -128,6 +217,7 @@ func AddNovaAPIFixture(log logr.Logger, server *api.FakeAPIServer) *NovaAPIFixtu
 			ServerGroupMembers: 10,
 		},
 		QuotaSets: map[string]quotasets.QuotaSet{},
+		KeyPairs:  map[string]keypairs.KeyPair{},
 	}
 	return fixture
 }
diff --git a/tests/functional/octavia_controller_test.go b/tests/functional/octavia_controller_test.go
index af7630ec..e586d920 100644
--- a/tests/functional/octavia_controller_test.go
+++ b/tests/functional/octavia_controller_test.go
@@ -20,13 +20,16 @@ import (
 	"fmt"
 
 	"github.com/google/uuid"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
 	. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
 	. "github.com/onsi/gomega"    //revive:disable:dot-imports
 	"k8s.io/apimachinery/pkg/types"
 
 	corev1 "k8s.io/api/core/v1"
 
-	keystone_helpers "github.com/openstack-k8s-operators/keystone-operator/api/test/helpers"
 	"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
 
 	//revive:disable-next-line:dot-imports
@@ -36,6 +39,70 @@ import (
 	"github.com/openstack-k8s-operators/octavia-operator/pkg/octavia"
 )
 
+func createAndSimulateKeystone(
+	octaviaName types.NamespacedName,
+) APIFixtures {
+	apiFixtures := SetupAPIFixtures(logger)
+	keystoneName := keystone.CreateKeystoneAPIWithFixture(namespace, apiFixtures.Keystone)
+	DeferCleanup(keystone.DeleteKeystoneAPI, keystoneName)
+	keystonePublicEndpoint := fmt.Sprintf("http://keystone-for-%s-public", octaviaName.Name)
+	SimulateKeystoneReady(keystoneName, keystonePublicEndpoint, apiFixtures.Keystone.Endpoint())
+	return apiFixtures
+}
+
+func createAndSimulateOctaviaSecrets(
+	octaviaName types.NamespacedName,
+) {
+	DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaSecret(octaviaName.Namespace))
+	DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaCaPassphraseSecret(octaviaName.Namespace, octaviaName.Name))
+	SimulateOctaviaCertsSecret(octaviaName.Namespace, octaviaName.Name)
+}
+
+func createAndSimulateTransportURL(
+	transportURLName types.NamespacedName,
+	transportURLSecretName types.NamespacedName,
+) {
+	DeferCleanup(k8sClient.Delete, ctx, CreateTransportURL(transportURLName))
+	DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName))
+	infra.SimulateTransportURLReady(transportURLName)
+}
+
+func createAndSimulateDB(spec map[string]interface{}) {
+	DeferCleanup(
+		mariadb.DeleteDBService,
+		mariadb.CreateDBService(
+			namespace,
+			spec["databaseInstance"].(string),
+			corev1.ServiceSpec{
+				Ports: []corev1.ServicePort{{Port: 3306}},
+			},
+		),
+	)
+	mariadb.CreateMariaDBAccount(namespace, spec["databaseAccount"].(string), mariadbv1.MariaDBAccountSpec{
+		Secret:   "osp-secret",
+		UserName: "octavia",
+	})
+	mariadb.CreateMariaDBAccount(namespace, spec["persistenceDatabaseAccount"].(string), mariadbv1.MariaDBAccountSpec{
+		Secret:   "osp-secret",
+		UserName: "octavia",
+	})
+	mariadb.CreateMariaDBDatabase(namespace, octavia.DatabaseCRName, mariadbv1.MariaDBDatabaseSpec{})
+	mariadb.CreateMariaDBDatabase(namespace, octavia.PersistenceDatabaseCRName, mariadbv1.MariaDBDatabaseSpec{})
+	mariadb.SimulateMariaDBAccountCompleted(types.NamespacedName{Namespace: namespace, Name: spec["databaseAccount"].(string)})
+	mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: octavia.DatabaseCRName})
+	mariadb.SimulateMariaDBAccountCompleted(types.NamespacedName{Namespace: namespace, Name: spec["persistenceDatabaseAccount"].(string)})
+	mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: octavia.PersistenceDatabaseCRName})
+}
+
+func createAndSimulateOctaviaAPI(octaviaName types.NamespacedName) {
+	octaviaAPIName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      fmt.Sprintf("%s-api", octaviaName.Name),
+	}
+	DeferCleanup(th.DeleteInstance, CreateOctaviaAPI(octaviaAPIName, GetDefaultOctaviaAPISpec()))
+	SimulateOctaviaAPIReady(octaviaAPIName)
+}
+
 var _ = Describe("Octavia controller", func() {
 	var name string
 	var spec map[string]interface{}
@@ -134,9 +201,9 @@ var _ = Describe("Octavia controller", func() {
 	When("a proper secret is provider, TransportURL is created", func() {
 		BeforeEach(func() {
 			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaSecret(namespace))
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName))
-			infra.SimulateTransportURLReady(transportURLName)
+
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
 		})
 
 		It("should be in state of having the input ready", func() {
@@ -168,21 +235,11 @@ var _ = Describe("Octavia controller", func() {
 
 	// Certs
 	When("Certificates are created", func() {
-		var keystoneAPIFixture *keystone_helpers.KeystoneAPIFixture
-
 		BeforeEach(func() {
-			keystoneAPIFixture, _, _ = SetupAPIFixtures(logger)
-			keystoneName := keystone.CreateKeystoneAPIWithFixture(namespace, keystoneAPIFixture)
-			DeferCleanup(keystone.DeleteKeystoneAPI, keystoneName)
-			keystonePublicEndpoint := fmt.Sprintf("http://keystone-for-%s-public", octaviaName.Name)
-			SimulateKeystoneReady(keystoneName, keystonePublicEndpoint, keystoneAPIFixture.Endpoint())
-
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaSecret(namespace))
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaCaPassphraseSecret(namespace, octaviaName.Name))
+			createAndSimulateKeystone(octaviaName)
 
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURL(transportURLName))
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName))
-			infra.SimulateTransportURLReady(transportURLName)
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
 
 			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
 		})
@@ -215,24 +272,13 @@ var _ = Describe("Octavia controller", func() {
 
 	// Quotas
 	When("Quotas are created", func() {
-		var keystoneAPIFixture *keystone_helpers.KeystoneAPIFixture
-		var novaAPIFixture *NovaAPIFixture
-		var neutronAPIFixture *NeutronAPIFixture
+		var apiFixtures APIFixtures
 
 		BeforeEach(func() {
-			keystoneAPIFixture, novaAPIFixture, neutronAPIFixture = SetupAPIFixtures(logger)
-			keystoneName := keystone.CreateKeystoneAPIWithFixture(namespace, keystoneAPIFixture)
-			DeferCleanup(keystone.DeleteKeystoneAPI, keystoneName)
-			keystonePublicEndpoint := fmt.Sprintf("http://keystone-for-%s-public", octaviaName.Name)
-			SimulateKeystoneReady(keystoneName, keystonePublicEndpoint, keystoneAPIFixture.Endpoint())
+			apiFixtures = createAndSimulateKeystone(octaviaName)
 
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaSecret(namespace))
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaCaPassphraseSecret(namespace, octaviaName.Name))
-			SimulateOctaviaCertsSecret(namespace, octaviaName.Name)
-
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURL(transportURLName))
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName))
-			infra.SimulateTransportURLReady(transportURLName)
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
 
 			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
 		})
@@ -248,14 +294,14 @@ var _ = Describe("Octavia controller", func() {
 			instance := GetOctavia(octaviaName)
 			project := GetProject(instance.Spec.TenantName)
 
-			quotaset := novaAPIFixture.QuotaSets[project.ID]
+			quotaset := apiFixtures.Nova.QuotaSets[project.ID]
 			Expect(quotaset.RAM).To(Equal(-1))
 			Expect(quotaset.Cores).To(Equal(-1))
 			Expect(quotaset.Instances).To(Equal(-1))
 			Expect(quotaset.ServerGroups).To(Equal(-1))
 			Expect(quotaset.ServerGroupMembers).To(Equal(-1))
 
-			quota := neutronAPIFixture.Quotas[project.ID]
+			quota := apiFixtures.Neutron.Quotas[project.ID]
 			Expect(quota.Port).To(Equal(-1))
 			Expect(quota.SecurityGroup).To(Equal(-1))
 			Expect(quota.SecurityGroupRule).To(Equal(-1))
@@ -266,22 +312,11 @@ var _ = Describe("Octavia controller", func() {
 
 	// DB
 	When("DB is created", func() {
-		var keystoneAPIFixture *keystone_helpers.KeystoneAPIFixture
-
 		BeforeEach(func() {
-			keystoneAPIFixture, _, _ = SetupAPIFixtures(logger)
-			keystoneName := keystone.CreateKeystoneAPIWithFixture(namespace, keystoneAPIFixture)
-			DeferCleanup(keystone.DeleteKeystoneAPI, keystoneName)
-			keystonePublicEndpoint := fmt.Sprintf("http://keystone-for-%s-public", octaviaName.Name)
-			SimulateKeystoneReady(keystoneName, keystonePublicEndpoint, keystoneAPIFixture.Endpoint())
+			createAndSimulateKeystone(octaviaName)
 
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaSecret(namespace))
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaCaPassphraseSecret(namespace, octaviaName.Name))
-			SimulateOctaviaCertsSecret(namespace, octaviaName.Name)
-
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURL(transportURLName))
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName))
-			infra.SimulateTransportURLReady(transportURLName)
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
 
 			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
 
@@ -323,40 +358,17 @@ var _ = Describe("Octavia controller", func() {
 
 	// Config
 	When("The Config Secrets are created", func() {
-		var keystoneAPIFixture *keystone_helpers.KeystoneAPIFixture
 
 		BeforeEach(func() {
-			keystoneAPIFixture, _, _ = SetupAPIFixtures(logger)
-			keystoneName := keystone.CreateKeystoneAPIWithFixture(namespace, keystoneAPIFixture)
-			DeferCleanup(keystone.DeleteKeystoneAPI, keystoneName)
-			keystonePublicEndpoint := fmt.Sprintf("http://keystone-for-%s-public", octaviaName.Name)
-			SimulateKeystoneReady(keystoneName, keystonePublicEndpoint, keystoneAPIFixture.Endpoint())
+			createAndSimulateKeystone(octaviaName)
 
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaSecret(namespace))
-			DeferCleanup(k8sClient.Delete, ctx, CreateOctaviaCaPassphraseSecret(namespace, octaviaName.Name))
-			SimulateOctaviaCertsSecret(namespace, octaviaName.Name)
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
 
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURL(transportURLName))
-			DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName))
-			infra.SimulateTransportURLReady(transportURLName)
+			createAndSimulateDB(spec)
 
 			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
 
-			DeferCleanup(
-				mariadb.DeleteDBService,
-				mariadb.CreateDBService(
-					namespace,
-					GetOctavia(octaviaName).Spec.DatabaseInstance,
-					corev1.ServiceSpec{
-						Ports: []corev1.ServicePort{{Port: 3306}},
-					},
-				),
-			)
-
-			mariadb.SimulateMariaDBAccountCompleted(types.NamespacedName{Namespace: namespace, Name: GetOctavia(octaviaName).Spec.DatabaseAccount})
-			mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: octavia.DatabaseCRName})
-			mariadb.SimulateMariaDBAccountCompleted(types.NamespacedName{Namespace: namespace, Name: GetOctavia(octaviaName).Spec.PersistenceDatabaseAccount})
-			mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: octavia.PersistenceDatabaseCRName})
 			th.SimulateJobSuccess(types.NamespacedName{Namespace: namespace, Name: octaviaName.Name + "-db-sync"})
 		})
 
@@ -431,11 +443,195 @@ var _ = Describe("Octavia controller", func() {
 		})
 	})
 
-	// Create Networks Annotation
+	// Networks Annotation
+	When("Network Annotation is created", func() {
+		BeforeEach(func() {
+			createAndSimulateKeystone(octaviaName)
+
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
+
+			createAndSimulateDB(spec)
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{
+				Name:      spec["octaviaNetworkAttachment"].(string),
+				Namespace: namespace,
+			}))
+
+			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
+		})
+
+		It("should set the NetworkAttachementReady condition", func() {
+			th.ExpectCondition(
+				octaviaName,
+				ConditionGetterFunc(OctaviaConditionGetter),
+				condition.NetworkAttachmentsReadyCondition,
+				corev1.ConditionTrue,
+			)
+		})
+	})
 
 	// API Deployment
 
 	// Network Management
+	When("The management network is created", func() {
+		var apiFixtures APIFixtures
+
+		BeforeEach(func() {
+			apiFixtures = createAndSimulateKeystone(octaviaName)
+
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
+
+			createAndSimulateDB(spec)
+
+			createAndSimulateOctaviaAPI(octaviaName)
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{
+				Name:      spec["octaviaNetworkAttachment"].(string),
+				Namespace: namespace,
+			}))
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNode(types.NamespacedName{
+				Namespace: namespace,
+				Name:      "node1",
+			}))
+
+			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
+
+			th.SimulateJobSuccess(types.NamespacedName{Namespace: namespace, Name: octaviaName.Name + "-db-sync"})
+		})
+
+		It("should create appropriate resources in Neutron", func() {
+			// Replace with condition for LbMgmtNetwork when it's merged
+			th.ExpectCondition(
+				octaviaName,
+				ConditionGetterFunc(OctaviaConditionGetter),
+				condition.ExposeServiceReadyCondition,
+				corev1.ConditionTrue,
+			)
+
+			instance := GetOctavia(octaviaName)
+			tenant := GetProject(instance.Spec.TenantName)
+			adminTenant := GetProject(octavia.AdminTenant)
+
+			nadConfig := GetNADConfig(types.NamespacedName{
+				Name:      instance.Spec.OctaviaNetworkAttachment,
+				Namespace: namespace})
+
+			// Networks
+			expectedNetworks := map[string]networks.Network{
+				octavia.LbMgmtNetName: {
+					Description:           octavia.LbMgmtNetDescription,
+					TenantID:              tenant.ID,
+					AvailabilityZoneHints: instance.Spec.LbMgmtNetworks.AvailabilityZones,
+				},
+				octavia.LbProvNetName: {
+					Description:           octavia.LbProvNetDescription,
+					TenantID:              adminTenant.ID,
+					AvailabilityZoneHints: instance.Spec.LbMgmtNetworks.AvailabilityZones,
+				},
+			}
+
+			resultNetworks := map[string]networks.Network{}
+			for _, network := range apiFixtures.Neutron.Networks {
+				resultNetworks[network.Name] = network
+			}
+			Expect(resultNetworks).To(HaveLen(2))
+			for name, expectedNetwork := range expectedNetworks {
+				network := resultNetworks[name]
+				Expect(network).ToNot(Equal(networks.Network{}), "Network %s doesn't appear to exist", name)
+				Expect(network.Description).To(Equal(expectedNetwork.Description))
+				Expect(network.TenantID).To(Equal(expectedNetwork.TenantID))
+				Expect(network.AvailabilityZoneHints).To(Equal(expectedNetwork.AvailabilityZoneHints))
+			}
+
+			lbMgmtPortAddress := ""
+			lbMgmtPortID := ""
+			for _, port := range apiFixtures.Neutron.Ports {
+				if port.Name == octavia.LbMgmtRouterPortName {
+					lbMgmtPortAddress = port.FixedIPs[0].IPAddress
+					lbMgmtPortID = port.ID
+					break
+				}
+			}
+			// Subnets
+			expectedSubnets := map[string]subnets.Subnet{
+				octavia.LbMgmtSubnetName: {
+					Description: octavia.LbMgmtSubnetDescription,
+					TenantID:    tenant.ID,
+					NetworkID:   resultNetworks[octavia.LbMgmtNetName].ID,
+					CIDR:        nadConfig.IPAM.Routes[0].Destination.String(),
+					HostRoutes: []subnets.HostRoute{{
+						DestinationCIDR: nadConfig.IPAM.CIDR.String(),
+						NextHop:         lbMgmtPortAddress,
+					}},
+				},
+				octavia.LbProvSubnetName: {
+					Description: octavia.LbProvSubnetDescription,
+					TenantID:    adminTenant.ID,
+					NetworkID:   resultNetworks[octavia.LbProvNetName].ID,
+					CIDR:        nadConfig.IPAM.CIDR.String(),
+				},
+			}
+
+			resultSubnets := map[string]subnets.Subnet{}
+			for _, subnet := range apiFixtures.Neutron.Subnets {
+				resultSubnets[subnet.Name] = subnet
+			}
+			Expect(resultSubnets).To(HaveLen(2))
+			for name, expectedSubnet := range expectedSubnets {
+				subnet := resultSubnets[name]
+				Expect(subnet).ToNot(Equal(subnets.Subnet{}), "Subnet %s doesn't appear to exist", name)
+				Expect(subnet.Description).To(Equal(expectedSubnet.Description))
+				Expect(subnet.TenantID).To(Equal(expectedSubnet.TenantID))
+				Expect(subnet.NetworkID).To(Equal(expectedSubnet.NetworkID))
+				Expect(subnet.CIDR).To(Equal(expectedSubnet.CIDR))
+				Expect(subnet.HostRoutes).To(Equal(expectedSubnet.HostRoutes))
+			}
+
+			// Routers
+			expectedRouters := map[string]routers.Router{
+				octavia.LbRouterName: {
+					GatewayInfo: routers.GatewayInfo{
+						NetworkID: resultNetworks[octavia.LbProvNetName].ID,
+						ExternalFixedIPs: []routers.ExternalFixedIP{
+							{
+								SubnetID: resultSubnets[octavia.LbProvSubnetName].ID,
+							},
+						},
+					},
+					AvailabilityZoneHints: instance.Spec.LbMgmtNetworks.AvailabilityZones,
+				},
+			}
+
+			resultRouters := map[string]routers.Router{}
+			for _, router := range apiFixtures.Neutron.Routers {
+				resultRouters[router.Name] = router
+			}
+			Expect(resultRouters).To(HaveLen(1))
+			for name, expectedRouter := range expectedRouters {
+				router := resultRouters[name]
+				Expect(router).ToNot(Equal(routers.Router{}), "Router %s doesn't appear to exist", name)
+				Expect(router.GatewayInfo.NetworkID).To(Equal(expectedRouter.GatewayInfo.NetworkID))
+				Expect(router.GatewayInfo.ExternalFixedIPs[0].SubnetID).To(Equal(expectedRouter.GatewayInfo.ExternalFixedIPs[0].SubnetID))
+				Expect(router.AvailabilityZoneHints).To(Equal(expectedRouter.AvailabilityZoneHints))
+			}
+
+			expectedInterfaces := map[string]routers.InterfaceInfo{
+				fmt.Sprintf("%s:%s", resultRouters[octavia.LbRouterName].ID, resultSubnets[octavia.LbMgmtSubnetName].ID): {
+					SubnetID: resultSubnets[octavia.LbMgmtSubnetName].ID,
+					PortID:   lbMgmtPortID,
+				},
+			}
+			for id, expectedInterfaces := range expectedInterfaces {
+				iface := apiFixtures.Neutron.InterfaceInfos[id]
+				Expect(iface).ToNot(Equal(routers.InterfaceInfo{}), "Interface %s doesn't appear to exist", id)
+				Expect(iface.SubnetID).To(Equal(expectedInterfaces.SubnetID))
+				Expect(iface.PortID).To(Equal(expectedInterfaces.PortID))
+			}
+		})
+	})
 
 	// Predictable IPs
 
@@ -444,6 +640,101 @@ var _ = Describe("Octavia controller", func() {
 	// Rsyslog Daemonset
 
 	// Amp SSH Config
+	When("The Amphora SSH config map is created", func() {
+		var apiFixtures APIFixtures
+
+		BeforeEach(func() {
+			apiFixtures = createAndSimulateKeystone(octaviaName)
+
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
+
+			createAndSimulateDB(spec)
+
+			createAndSimulateOctaviaAPI(octaviaName)
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{
+				Name:      spec["octaviaNetworkAttachment"].(string),
+				Namespace: namespace,
+			}))
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNode(types.NamespacedName{
+				Namespace: namespace,
+				Name:      "node1",
+			}))
+
+			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
+
+			th.SimulateJobSuccess(types.NamespacedName{Namespace: namespace, Name: octaviaName.Name + "-db-sync"})
+		})
+
+		It("should set OctaviaAmphoraSSHReady condition", func() {
+			th.ExpectCondition(
+				octaviaName,
+				ConditionGetterFunc(OctaviaConditionGetter),
+				octaviav1.OctaviaAmphoraSSHReadyCondition,
+				corev1.ConditionTrue,
+			)
+		})
+
+		It("should upload a new keypair", func() {
+			keyPairs := apiFixtures.Nova.KeyPairs
+			Expect(keyPairs[octavia.NovaKeyPairName]).ShouldNot(Equal(keypairs.KeyPair{}))
+		})
+
+		It("should set a key in the config map", func() {
+			instance := GetOctavia(octaviaName)
+			configMap := th.GetConfigMap(types.NamespacedName{
+				Name:      instance.Spec.LoadBalancerSSHPubKey,
+				Namespace: namespace})
+			Expect(configMap).ShouldNot(BeNil())
+			Expect(configMap.Data["key"]).Should(
+				ContainSubstring("ecdsa-"))
+		})
+	})
+
+	When("The Amphora SSH config map and the keypair already exist", func() {
+		var apiFixtures APIFixtures
+
+		BeforeEach(func() {
+			apiFixtures = createAndSimulateKeystone(octaviaName)
+			apiFixtures.Nova.KeyPairs = map[string]keypairs.KeyPair{
+				"octavia-ssh-keypair": {
+					Name:      "octavia-ssh-keypair",
+					PublicKey: "foobar",
+					UserID:    apiFixtures.Keystone.Users["octavia"].ID,
+				}}
+
+			createAndSimulateOctaviaSecrets(octaviaName)
+			createAndSimulateTransportURL(transportURLName, transportURLSecretName)
+
+			createAndSimulateDB(spec)
+
+			createAndSimulateOctaviaAPI(octaviaName)
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{
+				Name:      spec["octaviaNetworkAttachment"].(string),
+				Namespace: namespace,
+			}))
+
+			DeferCleanup(k8sClient.Delete, ctx, CreateNode(types.NamespacedName{
+				Namespace: namespace,
+				Name:      "node1",
+			}))
+
+			DeferCleanup(th.DeleteInstance, CreateSSHPubKey())
+
+			DeferCleanup(th.DeleteInstance, CreateOctavia(octaviaName, spec))
+
+			th.SimulateJobSuccess(types.NamespacedName{Namespace: namespace, Name: octaviaName.Name + "-db-sync"})
+		})
+
+		// PENDING https://issues.redhat.com/browse/OSPRH-10543
+		PIt("should not update the keypair", func() {
+			keyPairs := apiFixtures.Nova.KeyPairs
+			Expect(keyPairs["octavia-ssh-keypair"].PublicKey).Should(Equal("foobar"))
+		})
+	})
 
 	// Amphora Image
 })