diff --git a/runner/libvirt/customize.go b/runner/libvirt/customize.go
index b683611803c891f6e9c4800767206f186a096722..8cf48541e0c9967d41302a08b40b3cdd869802aa 100644
--- a/runner/libvirt/customize.go
+++ b/runner/libvirt/customize.go
@@ -33,7 +33,7 @@ func customizeDomain(ctx context.Context, uri string, d *device, extraCommands i
 	cmd := exec.CommandContext(ctx, "virt-customize", "-q",
 		"-d", d.name,
 		"-c", uri,
-		"--hostname", d.topoDev.Name,
+		"--hostname", d.Name,
 		"--timezone", "Etc/UTC",
 		// This rename script basically does s/eth/swp/ and breaks
 		// proper interface naming using udev rules. Delete it.
@@ -88,8 +88,8 @@ func commandsForFunction(d *device) []byte {
 		// libguestfs (1.44) thinks it doesn't know how to set
 		// hostnames for CL. Work around by directly writing to
 		// /etc/hostname.
-		fmt.Fprintf(&buf, "write /etc/hostname:%s\\\n\n", d.topoDev.Name)
-		if d.topoDev.Function() == topology.OOBSwitch {
+		fmt.Fprintf(&buf, "write /etc/hostname:%s\\\n\n", d.Name)
+		if d.Function() == topology.OOBSwitch {
 			writeExtraMgmtSwitchCommands(&buf, d)
 		}
 		return buf.Bytes()
@@ -112,7 +112,7 @@ func commandsForFunction(d *device) []byte {
 	// what we have in the topology file.
 	buf.WriteString("write /etc/lldpd.d/ifname.conf:configure lldp portidsubtype ifname\\\n\n")
 
-	if d.topoDev.Function() == topology.OOBServer {
+	if d.Function() == topology.OOBServer {
 		writeExtraMgmtServerCommands(&buf, d)
 	}
 	// Only required for SELinux-enabled systems (mostly Fedora/EL)
@@ -170,7 +170,7 @@ PREFIX=%d
 func writeExtraMgmtServerCommands(w io.Writer, d *device) {
 	io.WriteString(w, "install nftables,dnsmasq\n")
 	// We assume that the prefix has already been validated.
-	p := netaddr.MustParseIPPrefix(d.topoDev.Attr("mgmt_ip"))
+	p := netaddr.MustParseIPPrefix(d.Attr("mgmt_ip"))
 	io.WriteString(w, "write /etc/sysconfig/network-scripts/ifcfg-eth0:"+
 		"TYPE=Ethernet\\\nDEVICE=eth0\\\nPEERDNS=yes\\\nBOOTPROTO=dhcp\\\nONBOOT=yes\n")
 	io.WriteString(w, "write /etc/sysconfig/network-scripts/ifcfg-eth1:"+
@@ -214,7 +214,7 @@ func gatherHosts(ctx context.Context, r *Runner, t *topology.T) []etherHost {
 			// most likely, device does not have a mgmt interface
 			continue
 		}
-		mgmtIP := d.topoDev.MgmtIP()
+		mgmtIP := d.MgmtIP()
 		if mgmtIP == nil {
 			continue
 		}
diff --git a/runner/libvirt/helper.go b/runner/libvirt/helper.go
index cd23c1abb5e64615d5348c04901c82393e7cab2b..fce67d57d334e3ce22273aa1c2d7d4d6a2243004 100644
--- a/runner/libvirt/helper.go
+++ b/runner/libvirt/helper.go
@@ -215,7 +215,7 @@ func isASCIIDigit(c rune) bool {
 }
 
 func hasFunction(d *device, fs ...topology.DeviceFunction) bool {
-	return topology.HasFunction(&d.topoDev, fs...)
+	return topology.HasFunction(&d.Device, fs...)
 }
 
 // Returns whether d defaults to Cumulus Linux.
diff --git a/runner/libvirt/runner.go b/runner/libvirt/runner.go
index 13c5c43cb64740c3f1f659229ee028869d60927d..05710b9f827f8b10e451bbf98fc34ddd0abf731b 100644
--- a/runner/libvirt/runner.go
+++ b/runner/libvirt/runner.go
@@ -358,7 +358,7 @@ func (r *Runner) buildInventory(t *topology.T) (err error) {
 			tunnelIP: tunnelIP,
 			pool:     r.storagePool,
 			config:   config,
-			topoDev:  topoDev,
+			Device:   topoDev,
 		}
 	}
 	nextPort := uint(r.portBase)
@@ -449,7 +449,7 @@ func (r *Runner) downloadBaseImages(ctx context.Context, t *topology.T) (err err
 	wantImages := make(map[string]struct{})
 	haveImages := make(map[string]*libvirt.StorageVol)
 	for _, d := range r.devices {
-		osImage := d.topoDev.OSImage()
+		osImage := d.OSImage()
 		if osImage == "" {
 			continue
 		}
@@ -535,9 +535,9 @@ func (r *Runner) createVolumes(ctx context.Context, t *topology.T) (err error) {
 
 	for _, d := range r.devices {
 		var backing *libvirtxml.StorageVolumeBackingStore
-		capacity := d.topoDev.DiskSize()
+		capacity := d.DiskSize()
 
-		if osImage := d.topoDev.OSImage(); osImage != "" {
+		if osImage := d.OSImage(); osImage != "" {
 			base := r.baseImages[osImage]
 			if base == nil {
 				// we should've failed earlier already
@@ -674,7 +674,7 @@ func (r *Runner) customizeDomains(ctx context.Context, t *topology.T) (err error
 	customizeCtx, cancel := context.WithCancel(ctx)
 	defer cancel()
 	for _, d := range r.devices {
-		if d.topoDev.OSImage() == "" {
+		if d.OSImage() == "" {
 			// Cannot customize blank disk image.
 			continue
 		}
@@ -691,7 +691,7 @@ func (r *Runner) customizeDomains(ctx context.Context, t *topology.T) (err error
 				fmt.Fprintf(&buf, "ssh-inject root:string:%s\n", k)
 			}
 		}
-		if d.topoDev.Function() == topology.OOBServer {
+		if d.Function() == topology.OOBServer {
 			hosts := gatherHosts(ctx, r, t)
 			for _, h := range hosts {
 				fmt.Fprintf(&buf, "append-line /etc/hosts:%s %s\n",
@@ -792,7 +792,7 @@ func (r *Runner) writeSSHConfig(ctx context.Context, t *topology.T) (err error)
 			continue
 		}
 		user := "root"
-		if hasCumulusFunction(&device{topoDev: d}) {
+		if hasCumulusFunction(&device{Device: d}) {
 			user = "cumulus"
 		}
 		fmt.Fprintf(w, `Host %s
@@ -825,22 +825,22 @@ func (r *Runner) writeBMCConfig(ctx context.Context, t *topology.T) (err error)
 
 // internal representation for a device
 type device struct {
+	topology.Device
 	name       string
 	tunnelIP   net.IP
 	interfaces []iface
 	pool       string
 	config     []byte
-	topoDev    topology.Device
 }
 
 func (d *device) templateArgs() *domainTemplateArgs {
 	args := &domainTemplateArgs{
 		Name:    d.name,
-		VCPUs:   d.topoDev.VCPUs(),
-		Memory:  d.topoDev.Memory() >> 10, // libvirt wants KiB
+		VCPUs:   d.VCPUs(),
+		Memory:  d.Memory() >> 10, // libvirt wants KiB
 		Pool:    d.pool,
 		PXEBoot: false, // set below if enabled for an interface
-		UEFI:    d.topoDev.Attr("efi") != "",
+		UEFI:    d.Attr("efi") != "",
 	}
 	for _, intf := range d.interfaces {
 		typ := "udp"