aboutsummaryrefslogtreecommitdiff
path: root/lava/device
diff options
context:
space:
mode:
authorAntonio Terceiro <antonio.terceiro@linaro.org>2013-11-18 18:47:24 -0300
committerAntonio Terceiro <antonio.terceiro@linaro.org>2013-11-18 18:47:24 -0300
commit99ea8ab8541c3144c5e62cec8cdd327dc13c4388 (patch)
tree67c033d678e6ea7b0223d9608e5bf7dcd4752f20 /lava/device
parent49c36d653c19c018e473d7a1a27bdf5cb607c4be (diff)
Imported Upstream version 0.8upstream/0.8
Diffstat (limited to 'lava/device')
-rw-r--r--lava/device/__init__.py97
-rw-r--r--lava/device/commands.py122
-rw-r--r--lava/device/templates.py82
-rw-r--r--lava/device/tests/__init__.py0
-rw-r--r--lava/device/tests/test_commands.py182
-rw-r--r--lava/device/tests/test_device.py119
6 files changed, 602 insertions, 0 deletions
diff --git a/lava/device/__init__.py b/lava/device/__init__.py
new file mode 100644
index 0000000..35fe181
--- /dev/null
+++ b/lava/device/__init__.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Device class."""
+
+import re
+
+from copy import deepcopy
+
+from lava.device.templates import (
+ DEFAULT_TEMPLATE,
+ HOSTNAME_PARAMETER,
+ KNOWN_TEMPLATES,
+)
+from lava.helper.template import expand_template
+
+
+def __re_compile(name):
+ """Creates a generic regex for the specified device name.
+
+ :param name: The name of the device.
+ :return A Pattern object.
+ """
+ return re.compile('^.*{0}.*'.format(name), re.I)
+
+
+# Dictionary of know devices.
+# Keys are the general device name taken from lava.device.templates, values
+# are tuples of: a regex matcher to match the device, and the device associated
+# template.
+KNOWN_DEVICES = dict([(device, (__re_compile(device), template))
+ for device, template in KNOWN_TEMPLATES.iteritems()])
+
+
+class Device(object):
+
+ """A generic device."""
+
+ def __init__(self, data, hostname=None):
+ self.data = deepcopy(data)
+ self.hostname = hostname
+
+ def write(self, conf_file):
+ """Writes the object to file.
+
+ :param conf_file: The full path of the file where to write."""
+ with open(conf_file, 'w') as write_file:
+ write_file.write(str(self))
+
+ def update(self, config):
+ """Updates the Device object values based on the provided config.
+
+ :param config: A Config instance.
+ """
+ # We should always have a hostname, since it defaults to the name
+ # given on the command line for the config file.
+ if self.hostname is not None:
+ # We do not ask the user again this parameter.
+ self.data[HOSTNAME_PARAMETER.id].value = self.hostname
+ self.data[HOSTNAME_PARAMETER.id].asked = True
+
+ expand_template(self.data, config)
+
+ def __str__(self):
+ string_list = []
+ for key, value in self.data.iteritems():
+ string_list.append("{0} = {1}\n".format(str(key), str(value)))
+ return "".join(string_list)
+
+
+def get_known_device(name):
+ """Tries to match a device name with a known device type.
+
+ :param name: The name of the device we want matched to a real device.
+ :return A Device instance.
+ """
+ instance = Device(DEFAULT_TEMPLATE, hostname=name)
+ for _, (matcher, dev_template) in KNOWN_DEVICES.iteritems():
+ if matcher.match(name):
+ instance = Device(dev_template, hostname=name)
+ break
+ return instance
diff --git a/lava/device/commands.py b/lava/device/commands.py
new file mode 100644
index 0000000..a8ce66d
--- /dev/null
+++ b/lava/device/commands.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Device specific commands class.
+"""
+
+import os
+import sys
+
+from lava.device import get_known_device
+from lava.helper.command import (
+ BaseCommand,
+)
+from lava.helper.dispatcher import (
+ get_device_file,
+ get_devices_path,
+)
+from lava.tool.command import CommandGroup
+from lava.tool.errors import CommandError
+from lava_tool.utils import (
+ can_edit_file,
+ edit_file,
+)
+
+DEVICE_FILE_SUFFIX = "conf"
+
+
+class device(CommandGroup):
+ """LAVA devices handling."""
+
+ namespace = "lava.device.commands"
+
+
+class add(BaseCommand):
+ """Adds a new device."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(add, cls).register_arguments(parser)
+ parser.add_argument("DEVICE", help="The name of the device to add.")
+
+ def invoke(self):
+ real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
+
+ if get_device_file(real_file_name) is not None:
+ print >> sys.stdout, ("A device configuration file named '{0}' "
+ "already exists.".format(real_file_name))
+ print >> sys.stdout, ("Use 'lava device config {0}' to edit "
+ "it.".format(self.args.DEVICE))
+ sys.exit(-1)
+
+ devices_path = get_devices_path()
+ device_conf_file = os.path.abspath(os.path.join(devices_path,
+ real_file_name))
+
+ device = get_known_device(self.args.DEVICE)
+ device.update(self.config)
+ device.write(device_conf_file)
+
+ print >> sys.stdout, ("Created device file '{0}' in: {1}".format(
+ real_file_name, devices_path))
+ edit_file(device_conf_file)
+
+
+class remove(BaseCommand):
+ """Removes the specified device."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(remove, cls).register_arguments(parser)
+ parser.add_argument("DEVICE",
+ help="The name of the device to remove.")
+
+ def invoke(self):
+ real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
+ device_conf = get_device_file(real_file_name)
+
+ if device_conf:
+ try:
+ os.remove(device_conf)
+ print >> sys.stdout, ("Device configuration file '{0}' "
+ "removed.".format(real_file_name))
+ except OSError:
+ raise CommandError("Cannot remove file '{0}' at: {1}.".format(
+ real_file_name, os.path.dirname(device_conf)))
+ else:
+ print >> sys.stdout, ("No device configuration file '{0}' "
+ "found.".format(real_file_name))
+
+
+class config(BaseCommand):
+ """Opens the specified device config file."""
+ @classmethod
+ def register_arguments(cls, parser):
+ super(config, cls).register_arguments(parser)
+ parser.add_argument("DEVICE",
+ help="The name of the device to edit.")
+
+ def invoke(self):
+ real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
+ device_conf = get_device_file(real_file_name)
+
+ if device_conf and can_edit_file(device_conf):
+ edit_file(device_conf)
+ else:
+ raise CommandError("Cannot edit file '{0}'".format(real_file_name))
diff --git a/lava/device/templates.py b/lava/device/templates.py
new file mode 100644
index 0000000..e260117
--- /dev/null
+++ b/lava/device/templates.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This is just a place where to store a template like dictionary that
+will be used to serialize a Device object.
+"""
+
+from copy import copy
+
+from lava.parameter import Parameter
+
+# The hostname parameter is always in the DEFAULT config section.
+HOSTNAME_PARAMETER = Parameter("hostname")
+DEVICE_TYPE_PARAMETER = Parameter("device_type", depends=HOSTNAME_PARAMETER)
+CONNECTION_COMMAND_PARMAETER = Parameter("connection_command",
+ depends=DEVICE_TYPE_PARAMETER)
+
+DEFAULT_TEMPLATE = {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': DEVICE_TYPE_PARAMETER,
+ 'connection_command': CONNECTION_COMMAND_PARMAETER,
+}
+
+# Specialized copies of the parameters.
+# We need this or we might end up asking the user twice the same parameter due
+# to different object references when one Parameter depends on a "specialized"
+# one, different from the defaults.
+PANDA_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
+PANDA_DEVICE_TYPE.value = "panda"
+PANDA_DEVICE_TYPE.asked = True
+
+PANDA_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
+PANDA_CONNECTION_COMMAND.depends = PANDA_DEVICE_TYPE
+
+VEXPRESS_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
+VEXPRESS_DEVICE_TYPE.value = "vexpress"
+VEXPRESS_DEVICE_TYPE.asked = True
+
+VEXPRESS_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
+VEXPRESS_CONNECTION_COMMAND.depends = VEXPRESS_DEVICE_TYPE
+
+QEMU_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
+QEMU_DEVICE_TYPE.value = "qemu"
+QEMU_DEVICE_TYPE.asked = True
+
+QEMU_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
+QEMU_CONNECTION_COMMAND.depends = QEMU_DEVICE_TYPE
+
+# Dictionary with templates of known devices.
+KNOWN_TEMPLATES = {
+ 'panda': {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': PANDA_DEVICE_TYPE,
+ 'connection_command': PANDA_CONNECTION_COMMAND,
+ },
+ 'vexpress': {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': VEXPRESS_DEVICE_TYPE,
+ 'connection_command': VEXPRESS_CONNECTION_COMMAND,
+ },
+ 'qemu': {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': QEMU_DEVICE_TYPE,
+ 'connection_command': QEMU_CONNECTION_COMMAND,
+ }
+}
diff --git a/lava/device/tests/__init__.py b/lava/device/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/device/tests/__init__.py
diff --git a/lava/device/tests/test_commands.py b/lava/device/tests/test_commands.py
new file mode 100644
index 0000000..91b204f
--- /dev/null
+++ b/lava/device/tests/test_commands.py
@@ -0,0 +1,182 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+lava.device.commands unit tests.
+"""
+
+import os
+
+from mock import (
+ MagicMock,
+ call,
+ patch,
+)
+
+from lava.device.commands import (
+ add,
+ config,
+ remove,
+)
+from lava.helper.tests.helper_test import HelperTest
+from lava.tool.errors import CommandError
+
+
+class AddCommandTest(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ add_command = add(self.parser, self.args)
+ add_command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("DEVICE", args)
+
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.Device.__str__")
+ @patch("lava.device.Device.update")
+ @patch("lava.device.commands.get_device_file")
+ @patch("lava.device.commands.get_devices_path")
+ def test_add_invoke_0(self, mocked_get_devices_path,
+ mocked_get_device_file, mocked_update, mocked_str,
+ mocked_edit_file):
+ # Tests invocation of the add command. Verifies that the conf file is
+ # written to disk.
+ mocked_get_devices_path.return_value = self.temp_dir
+ mocked_get_device_file.return_value = None
+ mocked_str.return_value = ""
+
+ add_command = add(self.parser, self.args)
+ add_command.invoke()
+
+ expected_path = os.path.join(self.temp_dir,
+ ".".join([self.device, "conf"]))
+ self.assertTrue(os.path.isfile(expected_path))
+
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.commands.get_known_device")
+ @patch("lava.device.commands.get_devices_path")
+ @patch("lava.device.commands.sys.exit")
+ @patch("lava.device.commands.get_device_file")
+ def test_add_invoke_1(self, mocked_get_device_file, mocked_sys_exit,
+ mocked_get_devices_path, mocked_get_known_device,
+ mocked_edit_file):
+ mocked_get_devices_path.return_value = self.temp_dir
+ mocked_get_device_file.return_value = self.temp_file.name
+
+ add_command = add(self.parser, self.args)
+ add_command.invoke()
+
+ self.assertTrue(mocked_sys_exit.called)
+
+
+class RemoveCommandTests(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ command = remove(self.parser, self.args)
+ command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("DEVICE", args)
+
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.Device.__str__", return_value="")
+ @patch("lava.device.Device.update")
+ @patch("lava.device.commands.get_device_file")
+ @patch("lava.device.commands.get_devices_path")
+ def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock,
+ mocked_update, mocked_str, mocked_edit_file):
+ # Tests invocation of the remove command. Verifies that the conf file
+ # has been correctly removed.
+ # First we add a new conf file, then we remove it.
+ get_device_file_mock.return_value = None
+ get_devices_path_mock.return_value = self.temp_dir
+
+ add_command = add(self.parser, self.args)
+ add_command.invoke()
+
+ expected_path = os.path.join(self.temp_dir,
+ ".".join([self.device, "conf"]))
+
+ # Set new values for the mocked function.
+ get_device_file_mock.return_value = expected_path
+
+ remove_command = remove(self.parser, self.args)
+ remove_command.invoke()
+
+ self.assertFalse(os.path.isfile(expected_path))
+
+ @patch("lava.device.commands.get_device_file",
+ new=MagicMock(return_value="/root"))
+ def test_remove_invoke_raises(self):
+ # Tests invocation of the remove command, with a non existent device
+ # configuration file.
+ remove_command = remove(self.parser, self.args)
+ self.assertRaises(CommandError, remove_command.invoke)
+
+
+class ConfigCommanTests(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ command = config(self.parser, self.args)
+ command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("DEVICE", args)
+
+ @patch("lava.device.commands.can_edit_file", create=True)
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.commands.get_device_file")
+ def test_config_invoke_0(self, mocked_get_device_file, mocked_edit_file,
+ mocked_can_edit_file):
+ command = config(self.parser, self.args)
+
+ mocked_can_edit_file.return_value = True
+ mocked_get_device_file.return_value = self.temp_file.name
+ command.invoke()
+
+ self.assertTrue(mocked_edit_file.called)
+ self.assertEqual([call(self.temp_file.name)],
+ mocked_edit_file.call_args_list)
+
+ @patch("lava.device.commands.get_device_file",
+ new=MagicMock(return_value=None))
+ def test_config_invoke_raises_0(self):
+ # Tests invocation of the config command, with a non existent device
+ # configuration file.
+ config_command = config(self.parser, self.args)
+ self.assertRaises(CommandError, config_command.invoke)
+
+ @patch("lava.device.commands.get_device_file",
+ new=MagicMock(return_value="/etc/password"))
+ def test_config_invoke_raises_1(self):
+ # Tests invocation of the config command, with a non writable file.
+ # Hopefully tests are not run as root.
+ config_command = config(self.parser, self.args)
+ self.assertRaises(CommandError, config_command.invoke)
diff --git a/lava/device/tests/test_device.py b/lava/device/tests/test_device.py
new file mode 100644
index 0000000..c8185f4
--- /dev/null
+++ b/lava/device/tests/test_device.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Device class unit tests.
+"""
+
+from mock import patch
+
+from lava.config import Config
+from lava.device import (
+ Device,
+ get_known_device,
+)
+from lava.device.templates import (
+ HOSTNAME_PARAMETER,
+ PANDA_DEVICE_TYPE,
+ PANDA_CONNECTION_COMMAND,
+)
+from lava.helper.tests.helper_test import HelperTest
+from lava.parameter import Parameter
+
+
+class DeviceTest(HelperTest):
+
+ def test_get_known_device_panda_0(self):
+ # User creates a new device with a guessable name for a device.
+ instance = get_known_device('panda_new_01')
+ self.assertIsInstance(instance, Device)
+ self.assertEqual(instance.data['device_type'].value, 'panda')
+
+ def test_get_known_device_panda_1(self):
+ # User creates a new device with a guessable name for a device.
+ # Name passed has capital letters.
+ instance = get_known_device('new_PanDa_02')
+ self.assertIsInstance(instance, Device)
+ self.assertEqual(instance.data['device_type'].value, 'panda')
+
+ def test_get_known_device_vexpress_0(self):
+ # User creates a new device with a guessable name for a device.
+ # Name passed has capital letters.
+ instance = get_known_device('a_VexPress_Device')
+ self.assertIsInstance(instance, Device)
+ self.assertEqual(instance.data['device_type'].value, 'vexpress')
+
+ def test_get_known_device_vexpress_1(self):
+ # User creates a new device with a guessable name for a device.
+ instance = get_known_device('another-vexpress')
+ self.assertIsInstance(instance, Device)
+ self.assertIsInstance(instance.data['device_type'], Parameter)
+ self.assertEqual(instance.data['device_type'].value, 'vexpress')
+
+ @patch("lava.config.Config.save")
+ def test_device_update_1(self, patched_save):
+ # Tests that when calling update() on a Device, the template gets
+ # updated with the correct values from a Config instance.
+ hostname = "panda_device"
+
+ config = Config()
+ config._config_file = self.temp_file.name
+ config.put_parameter(HOSTNAME_PARAMETER, hostname)
+ config.put_parameter(PANDA_DEVICE_TYPE, "panda")
+ config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
+
+ expected = {
+ "hostname": hostname,
+ "device_type": "panda",
+ "connection_command": "test"
+ }
+
+ instance = get_known_device(hostname)
+ instance.update(config)
+
+ self.assertEqual(expected, instance.data)
+
+ @patch("lava.config.Config.save")
+ def test_device_write(self, mocked_save):
+ # User tries to create a new panda device. The conf file is written
+ # and contains the expected results.
+ hostname = "panda_device"
+
+ config = Config()
+ config._config_file = self.temp_file.name
+ config.put_parameter(HOSTNAME_PARAMETER, hostname)
+ config.put_parameter(PANDA_DEVICE_TYPE, "panda")
+ config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
+
+ expected = {
+ "hostname": hostname,
+ "device_type": "panda",
+ "connection_command": "test"
+ }
+
+ instance = get_known_device(hostname)
+ instance.update(config)
+ instance.write(self.temp_file.name)
+
+ expected = ("hostname = panda_device\nconnection_command = test\n"
+ "device_type = panda\n")
+
+ obtained = ""
+ with open(self.temp_file.name) as f:
+ obtained = f.read()
+ self.assertEqual(expected, obtained)