summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/tests/test_libvirt_config.py363
-rw-r--r--nova/virt/libvirt/config.py358
2 files changed, 721 insertions, 0 deletions
diff --git a/nova/tests/test_libvirt_config.py b/nova/tests/test_libvirt_config.py
new file mode 100644
index 000000000..186fb5a18
--- /dev/null
+++ b/nova/tests/test_libvirt_config.py
@@ -0,0 +1,363 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from lxml import objectify
+
+from nova import test
+
+from nova.virt.libvirt import config
+
+
+class LibvirtConfigBaseTest(test.TestCase):
+ def assertXmlEqual(self, expectedXmlstr, actualXmlstr):
+ expected = etree.tostring(objectify.fromstring(expectedXmlstr))
+ actual = etree.tostring(objectify.fromstring(actualXmlstr))
+ self.assertEqual(expected, actual)
+
+
+class LibvirtConfigTest(LibvirtConfigBaseTest):
+
+ def test_config_plain(self):
+ obj = config.LibvirtConfigObject(root_name="demo")
+ xml = obj.to_xml()
+
+ self.assertXmlEqual(xml, "<demo/>")
+
+ def test_config_ns(self):
+ obj = config.LibvirtConfigObject(root_name="demo", ns_prefix="foo",
+ ns_uri="http://example.com/foo")
+ xml = obj.to_xml()
+
+ self.assertXmlEqual(xml, """
+ <foo:demo xmlns:foo="http://example.com/foo"/>""")
+
+ def test_config_text(self):
+ obj = config.LibvirtConfigObject(root_name="demo")
+ root = obj.format_dom()
+ root.append(obj._text_node("foo", "bar"))
+
+ xml = etree.tostring(root)
+ self.assertXmlEqual(xml, "<demo><foo>bar</foo></demo>")
+
+
+class LibvirtConfigGuestDiskTest(LibvirtConfigBaseTest):
+
+ def test_config_file(self):
+ obj = config.LibvirtConfigGuestDisk()
+ obj.source_type = "file"
+ obj.source_path = "/tmp/hello"
+ obj.target_dev = "/dev/hda"
+ obj.target_bus = "ide"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <disk type="file" device="disk">
+ <source file="/tmp/hello"/>
+ <target bus="ide" dev="/dev/hda"/>
+ </disk>""")
+
+ def test_config_block(self):
+ obj = config.LibvirtConfigGuestDisk()
+ obj.source_type = "block"
+ obj.source_path = "/tmp/hello"
+ obj.source_device = "cdrom"
+ obj.driver_name = "qemu"
+ obj.target_dev = "/dev/hdc"
+ obj.target_bus = "ide"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <disk type="block" device="cdrom">
+ <driver name="qemu"/>
+ <source dev="/tmp/hello"/>
+ <target bus="ide" dev="/dev/hdc"/>
+ </disk>""")
+
+ def test_config_network(self):
+ obj = config.LibvirtConfigGuestDisk()
+ obj.source_type = "network"
+ obj.source_protocol = "iscsi"
+ obj.source_host = "foo.bar.com"
+ obj.driver_name = "qemu"
+ obj.driver_format = "qcow2"
+ obj.target_dev = "/dev/hda"
+ obj.target_bus = "ide"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <disk type="network" device="disk">
+ <driver name="qemu" type="qcow2"/>
+ <source protocol="iscsi" name="foo.bar.com"/>
+ <target bus="ide" dev="/dev/hda"/>
+ </disk>""")
+
+
+class LibvirtConfigGuestFilesysTest(LibvirtConfigBaseTest):
+
+ def test_config_mount(self):
+ obj = config.LibvirtConfigGuestFilesys()
+ obj.source_type = "mount"
+ obj.source_dir = "/tmp/hello"
+ obj.target_dir = "/mnt"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <filesystem type="mount">
+ <source dir="/tmp/hello"/>
+ <target dir="/mnt"/>
+ </filesystem>""")
+
+
+class LibvirtConfigGuestInputTest(LibvirtConfigBaseTest):
+
+ def test_config_tablet(self):
+ obj = config.LibvirtConfigGuestInput()
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <input type="tablet" bus="usb"/>""")
+
+
+class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest):
+
+ def test_config_graphics(self):
+ obj = config.LibvirtConfigGuestGraphics()
+ obj.type = "vnc"
+ obj.autoport = True
+ obj.keymap = "en_US"
+ obj.listen = "127.0.0.1"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <graphics type="vnc" autoport="yes" keymap="en_US" listen="127.0.0.1"/>
+ """)
+
+
+class LibvirtConfigGuestSerialTest(LibvirtConfigBaseTest):
+
+ def test_config_file(self):
+ obj = config.LibvirtConfigGuestSerial()
+ obj.type = "file"
+ obj.source_path = "/tmp/vm.log"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <serial type="file">
+ <source file="/tmp/vm.log"/>
+ </serial>""")
+
+
+class LibvirtConfigGuestSerialTest(LibvirtConfigBaseTest):
+ def test_config_pty(self):
+ obj = config.LibvirtConfigGuestConsole()
+ obj.type = "pty"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <console type="pty"/>""")
+
+
+class LibvirtConfigGuestInterfaceTest(LibvirtConfigBaseTest):
+ def test_config_ethernet(self):
+ obj = config.LibvirtConfigGuestInterface()
+ obj.net_type = "ethernet"
+ obj.mac_addr = "DE:AD:BE:EF:CA:FE"
+ obj.model = "virtio"
+ obj.target_dev = "vnet0"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <interface type="ethernet">
+ <mac address="DE:AD:BE:EF:CA:FE"/>
+ <model type="virtio"/>
+ <target dev="vnet0"/>
+ </interface>""")
+
+ def test_config_bridge(self):
+ obj = config.LibvirtConfigGuestInterface()
+ obj.net_type = "bridge"
+ obj.source_dev = "br0"
+ obj.mac_addr = "DE:AD:BE:EF:CA:FE"
+ obj.model = "virtio"
+ obj.filtername = "clean-traffic"
+ obj.filterparams.append({"key": "IP", "value": "192.168.122.1"})
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <interface type="bridge">
+ <mac address="DE:AD:BE:EF:CA:FE"/>
+ <model type="virtio"/>
+ <source bridge="br0"/>
+ <filterref filter="clean-traffic">
+ <parameter name="IP" value="192.168.122.1"/>
+ </filterref>
+ </interface>""")
+
+ def test_config_bridge_ovs(self):
+ obj = config.LibvirtConfigGuestInterface()
+ obj.net_type = "bridge"
+ obj.source_dev = "br0"
+ obj.mac_addr = "DE:AD:BE:EF:CA:FE"
+ obj.model = "virtio"
+ obj.vporttype = "openvswitch"
+ obj.vportparams.append({"key": "instanceid", "value": "foobar"})
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <interface type="bridge">
+ <mac address="DE:AD:BE:EF:CA:FE"/>
+ <model type="virtio"/>
+ <source bridge="br0"/>
+ <virtualport type="openvswitch">
+ <parameters instanceid="foobar"/>
+ </virtualport>
+ </interface>""")
+
+ def test_config_8021Qbh(self):
+ obj = config.LibvirtConfigGuestInterface()
+ obj.net_type = "direct"
+ obj.mac_addr = "DE:AD:BE:EF:CA:FE"
+ obj.model = "virtio"
+ obj.source_dev = "eth0"
+ obj.vporttype = "802.1Qbh"
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <interface type="direct">
+ <mac address="DE:AD:BE:EF:CA:FE"/>
+ <model type="virtio"/>
+ <source mode="private" dev="eth0"/>
+ <virtualport type="802.1Qbh"/>
+ </interface>""")
+
+
+class LibvirtConfigGuestTest(LibvirtConfigBaseTest):
+
+ def test_config_lxc(self):
+ obj = config.LibvirtConfigGuest()
+ obj.virt_type = "lxc"
+ obj.memory = 1024 * 1024 * 100
+ obj.vcpus = 2
+ obj.name = "demo"
+ obj.uuid = "b38a3f43-4be2-4046-897f-b67c2f5e0147"
+ obj.os_type = "exe"
+ obj.os_init_path = "/sbin/init"
+
+ fs = config.LibvirtConfigGuestFilesys()
+ fs.source_dir = "/root/lxc"
+ fs.target_dir = "/"
+
+ obj.add_device(fs)
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <domain type="lxc">
+ <uuid>b38a3f43-4be2-4046-897f-b67c2f5e0147</uuid>
+ <name>demo</name>
+ <memory>104857600</memory>
+ <vcpu>2</vcpu>
+ <os>
+ <type>exe</type>
+ <init>/sbin/init</init>
+ </os>
+ <devices>
+ <filesystem type="mount">
+ <source dir="/root/lxc"/>
+ <target dir="/"/>
+ </filesystem>
+ </devices>
+ </domain>""")
+
+ def test_config_xen(self):
+ obj = config.LibvirtConfigGuest()
+ obj.virt_type = "xen"
+ obj.memory = 1024 * 1024 * 100
+ obj.vcpus = 2
+ obj.name = "demo"
+ obj.uuid = "b38a3f43-4be2-4046-897f-b67c2f5e0147"
+ obj.os_type = "linux"
+ obj.os_kernel = "/tmp/vmlinuz"
+ obj.os_initrd = "/tmp/ramdisk"
+ obj.os_root = "root=xvda"
+ obj.os_cmdline = "console=xvc0"
+
+ disk = config.LibvirtConfigGuestDisk()
+ disk.source_type = "file"
+ disk.source_path = "/tmp/img"
+ disk.target_dev = "/dev/xvda"
+ disk.target_bus = "xen"
+
+ obj.add_device(disk)
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <domain type="xen">
+ <uuid>b38a3f43-4be2-4046-897f-b67c2f5e0147</uuid>
+ <name>demo</name>
+ <memory>104857600</memory>
+ <vcpu>2</vcpu>
+ <os>
+ <type>linux</type>
+ <kernel>/tmp/vmlinuz</kernel>
+ <initrd>/tmp/ramdisk</initrd>
+ <cmdline>console=xvc0</cmdline>
+ <root>root=xvda</root>
+ </os>
+ <devices>
+ <disk type="file" device="disk">
+ <source file="/tmp/img"/>
+ <target bus="xen" dev="/dev/xvda"/>
+ </disk>
+ </devices>
+ </domain>""")
+
+ def test_config_kvm(self):
+ obj = config.LibvirtConfigGuest()
+ obj.virt_type = "kvm"
+ obj.memory = 1024 * 1024 * 100
+ obj.vcpus = 2
+ obj.name = "demo"
+ obj.uuid = "b38a3f43-4be2-4046-897f-b67c2f5e0147"
+ obj.os_type = "linux"
+ obj.os_boot_dev = "hd"
+
+ disk = config.LibvirtConfigGuestDisk()
+ disk.source_type = "file"
+ disk.source_path = "/tmp/img"
+ disk.target_dev = "/dev/vda"
+ disk.target_bus = "virtio"
+
+ obj.add_device(disk)
+
+ xml = obj.to_xml()
+ self.assertXmlEqual(xml, """
+ <domain type="kvm">
+ <uuid>b38a3f43-4be2-4046-897f-b67c2f5e0147</uuid>
+ <name>demo</name>
+ <memory>104857600</memory>
+ <vcpu>2</vcpu>
+ <os>
+ <type>linux</type>
+ <boot dev="hd"/>
+ </os>
+ <devices>
+ <disk type="file" device="disk">
+ <source file="/tmp/img"/>
+ <target bus="virtio" dev="/dev/vda"/>
+ </disk>
+ </devices>
+ </domain>""")
diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py
new file mode 100644
index 000000000..44790a068
--- /dev/null
+++ b/nova/virt/libvirt/config.py
@@ -0,0 +1,358 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Configuration for libvirt objects.
+
+Classes to represent the configuration of various libvirt objects
+and support conversion to/from XML
+"""
+
+from nova import log as logging
+
+from lxml import etree
+
+
+LOG = logging.getLogger(__name__)
+
+
+class LibvirtConfigObject(object):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigObject, self).__init__()
+
+ self.root_name = kwargs.get("root_name")
+ self.ns_prefix = kwargs.get('ns_prefix')
+ self.ns_uri = kwargs.get('ns_uri')
+
+ if "xml_str" in kwargs:
+ self.parse_dom(kwargs.get("xml_str"))
+
+ def _text_node(self, name, value):
+ child = etree.Element(name)
+ child.text = str(value)
+ return child
+
+ def format_dom(self):
+ if self.ns_uri is None:
+ return etree.Element(self.root_name)
+ else:
+ return etree.Element("{" + self.ns_uri + "}" + self.root_name,
+ nsmap={self.ns_prefix: self.ns_uri})
+
+ def parse_dom(xmldoc):
+ raise NotImplementedError()
+
+ def to_xml(self, pretty_print=True):
+ root = self.format_dom()
+ xml_str = etree.tostring(root, pretty_print=pretty_print)
+ LOG.debug("Generated XML %s " % (xml_str,))
+ return xml_str
+
+
+class LibvirtConfigGuestDevice(LibvirtConfigObject):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestDevice, self).__init__(**kwargs)
+
+
+class LibvirtConfigGuestDisk(LibvirtConfigGuestDevice):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestDisk, self).__init__(root_name="disk",
+ **kwargs)
+
+ self.source_type = "file"
+ self.source_device = "disk"
+ self.driver_name = None
+ self.driver_format = None
+ self.driver_cache = None
+ self.source_path = None
+ self.source_protocol = None
+ self.source_host = None
+ self.target_dev = None
+ self.target_path = None
+ self.target_bus = None
+
+ def format_dom(self):
+ dev = super(LibvirtConfigGuestDisk, self).format_dom()
+
+ dev.set("type", self.source_type)
+ dev.set("device", self.source_device)
+ if self.driver_name is not None or \
+ self.driver_format is not None or \
+ self.driver_cache is not None:
+ drv = etree.Element("driver")
+ if self.driver_name is not None:
+ drv.set("name", self.driver_name)
+ if self.driver_format is not None:
+ drv.set("type", self.driver_format)
+ if self.driver_cache is not None:
+ drv.set("cache", self.driver_cache)
+ dev.append(drv)
+
+ if self.source_type == "file":
+ dev.append(etree.Element("source", file=self.source_path))
+ elif self.source_type == "block":
+ dev.append(etree.Element("source", dev=self.source_path))
+ elif self.source_type == "mount":
+ dev.append(etree.Element("source", dir=self.source_path))
+ elif self.source_type == "network":
+ dev.append(etree.Element("source", protocol=self.source_protocol,
+ name=self.source_host))
+
+ if self.source_type == "mount":
+ dev.append(etree.Element("target", dir=self.target_path))
+ else:
+ dev.append(etree.Element("target", dev=self.target_dev,
+ bus=self.target_bus))
+
+ return dev
+
+
+class LibvirtConfigGuestFilesys(LibvirtConfigGuestDevice):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestFilesys, self).__init__(root_name="filesystem",
+ **kwargs)
+
+ self.source_type = "mount"
+ self.source_dir = None
+ self.target_dir = "/"
+
+ def format_dom(self):
+ dev = super(LibvirtConfigGuestFilesys, self).format_dom()
+
+ dev.set("type", self.source_type)
+
+ dev.append(etree.Element("source", dir=self.source_dir))
+ dev.append(etree.Element("target", dir=self.target_dir))
+
+ return dev
+
+
+class LibvirtConfigGuestInterface(LibvirtConfigGuestDevice):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestInterface, self).__init__(
+ root_name="interface",
+ **kwargs)
+
+ self.net_type = None
+ self.target_dev = None
+ self.model = None
+ self.mac_addr = None
+ self.script = None
+ self.source_dev = None
+ self.vporttype = None
+ self.vportparams = []
+ self.filtername = None
+ self.filterparams = []
+
+ def format_dom(self):
+ dev = super(LibvirtConfigGuestInterface, self).format_dom()
+
+ dev.set("type", self.net_type)
+ dev.append(etree.Element("mac", address=self.mac_addr))
+ if self.model:
+ dev.append(etree.Element("model", type=self.model))
+ if self.net_type == "ethernet":
+ if self.script is not None:
+ dev.append(etree.Element("script", path=self.script))
+ dev.append(etree.Element("target", dev=self.target_dev))
+ elif self.net_type == "direct":
+ dev.append(etree.Element("source", dev=self.source_dev,
+ mode="private"))
+ else:
+ dev.append(etree.Element("source", bridge=self.source_dev))
+
+ if self.vporttype is not None:
+ vport = etree.Element("virtualport", type=self.vporttype)
+ for p in self.vportparams:
+ param = etree.Element("parameters")
+ param.set(p['key'], p['value'])
+ vport.append(param)
+ dev.append(vport)
+
+ if self.filtername is not None:
+ filter = etree.Element("filterref", filter=self.filtername)
+ for p in self.filterparams:
+ filter.append(etree.Element("parameter",
+ name=p['key'],
+ value=p['value']))
+ dev.append(filter)
+
+ return dev
+
+ def add_filter_param(self, key, value):
+ self.filterparams.append({'key': key, 'value': value})
+
+ def add_vport_param(self, key, value):
+ self.vportparams.append({'key': key, 'value': value})
+
+
+class LibvirtConfigGuestInput(LibvirtConfigGuestDevice):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestInput, self).__init__(root_name="input",
+ **kwargs)
+
+ self.type = "tablet"
+ self.bus = "usb"
+
+ def format_dom(self):
+ dev = super(LibvirtConfigGuestInput, self).format_dom()
+
+ dev.set("type", self.type)
+ dev.set("bus", self.bus)
+
+ return dev
+
+
+class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestGraphics, self).__init__(root_name="graphics",
+ **kwargs)
+
+ self.type = "vnc"
+ self.autoport = True
+ self.keymap = None
+ self.listen = None
+
+ def format_dom(self):
+ dev = super(LibvirtConfigGuestGraphics, self).format_dom()
+
+ dev.set("type", self.type)
+ if self.autoport:
+ dev.set("autoport", "yes")
+ else:
+ dev.set("autoport", "no")
+ if self.keymap:
+ dev.set("keymap", self.keymap)
+ if self.listen:
+ dev.set("listen", self.listen)
+
+ return dev
+
+
+class LibvirtConfigGuestChar(LibvirtConfigGuestDevice):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestChar, self).__init__(**kwargs)
+
+ self.type = "pty"
+ self.source_path = None
+ self.target_port = None
+
+ def format_dom(self):
+ dev = super(LibvirtConfigGuestChar, self).format_dom()
+
+ dev.set("type", self.type)
+ if self.type == "file":
+ dev.append(etree.Element("source", path=self.source_path))
+ if self.target_port is not None:
+ dev.append(etree.Element("target", port=str(self.target_port)))
+
+ return dev
+
+
+class LibvirtConfigGuestSerial(LibvirtConfigGuestChar):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestSerial, self).__init__(root_name="serial",
+ **kwargs)
+
+
+class LibvirtConfigGuestConsole(LibvirtConfigGuestChar):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuestConsole, self).__init__(root_name="console",
+ **kwargs)
+
+
+class LibvirtConfigGuest(LibvirtConfigObject):
+
+ def __init__(self, **kwargs):
+ super(LibvirtConfigGuest, self).__init__(root_name="domain",
+ **kwargs)
+
+ self.virt_type = None
+ self.uuid = None
+ self.name = None
+ self.memory = 1024 * 1024 * 500
+ self.vcpus = 1
+ self.acpi = False
+ self.os_type = None
+ self.os_kernel = None
+ self.os_initrd = None
+ self.os_cmdline = None
+ self.os_root = None
+ self.os_init_path = None
+ self.os_boot_dev = None
+ self.devices = []
+
+ def _format_basic_props(self, root):
+ root.append(self._text_node("uuid", self.uuid))
+ root.append(self._text_node("name", self.name))
+ root.append(self._text_node("memory", self.memory))
+ root.append(self._text_node("vcpu", self.vcpus))
+
+ def _format_os(self, root):
+ os = etree.Element("os")
+ os.append(self._text_node("type", self.os_type))
+ if self.os_kernel is not None:
+ os.append(self._text_node("kernel", self.os_kernel))
+ if self.os_initrd is not None:
+ os.append(self._text_node("initrd", self.os_initrd))
+ if self.os_cmdline is not None:
+ os.append(self._text_node("cmdline", self.os_cmdline))
+ if self.os_root is not None:
+ os.append(self._text_node("root", self.os_root))
+ if self.os_init_path is not None:
+ os.append(self._text_node("init", self.os_init_path))
+ if self.os_boot_dev is not None:
+ os.append(etree.Element("boot", dev=self.os_boot_dev))
+ root.append(os)
+
+ def _format_features(self, root):
+ if self.acpi:
+ features = etree.Element("features")
+ features.append(etree.Element("acpi"))
+ root.append(features)
+
+ def _format_devices(self, root):
+ if len(self.devices) == 0:
+ return
+ devices = etree.Element("devices")
+ for dev in self.devices:
+ devices.append(dev.format_dom())
+ root.append(devices)
+
+ def format_dom(self):
+ root = super(LibvirtConfigGuest, self).format_dom()
+
+ root.set("type", self.virt_type)
+
+ self._format_basic_props(root)
+ self._format_os(root)
+ self._format_features(root)
+ self._format_devices(root)
+
+ return root
+
+ def add_device(self, dev):
+ self.devices.append(dev)