cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #04341
[Merge] ~smoser/cloud-init:feature/driver-enablement into cloud-init:master
Scott Moser has proposed merging ~smoser/cloud-init:feature/driver-enablement into cloud-init:master.
Commit message:
Add ubuntu_drivers config module.
The ubuntu_drivers config module allows usage of 'ubuntu-drivers'
command. At this point it only serves as a way of installing a
general purpose graphics processing unit (gpgpu) functionality.
Requested reviews:
cloud-init commiters (cloud-init-dev)
For more details, see:
https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/341417
see commit message
--
Your team cloud-init commiters is requested to review the proposed merge of ~smoser/cloud-init:feature/driver-enablement into cloud-init:master.
diff --git a/cloudinit/config/cc_ubuntu_drivers.py b/cloudinit/config/cc_ubuntu_drivers.py
new file mode 100644
index 0000000..bfb8871
--- /dev/null
+++ b/cloudinit/config/cc_ubuntu_drivers.py
@@ -0,0 +1,80 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Ubuntu Drivers: Interact with third party drivers in Ubuntu."""
+
+from textwrap import dedent
+
+from cloudinit.config.schema import (
+ get_schema_doc, validate_cloudconfig_schema)
+from cloudinit import log as logging
+from cloudinit.settings import PER_INSTANCE
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+frequency = PER_INSTANCE
+distros = ['ubuntu']
+schema = {
+ 'id': 'cc_ubuntu_drivers',
+ 'name': 'Ubuntu Drivers',
+ 'title': 'Interact with third party drivers in Ubuntu.',
+ 'description': dedent("""\
+ This module interacts with 'ubuntu-drivers' to install third party
+ driver packages."""),
+ 'distros': distros,
+ 'examples': [dedent("""\
+ drivers:
+ nvidia:
+ license-accepted: true
+ """)],
+ 'frequency': frequency,
+ 'type': 'object',
+ 'additionalProperties': True,
+ 'properties': {
+ 'drivers': {
+ 'type': 'object',
+ 'additionalProperties': False,
+ 'properties': {
+ 'nvidia': {
+ 'type': 'object',
+ 'additionalProperties': False,
+ 'required': ['license-accepted'],
+ 'properties': {
+ 'license-accepted': {
+ 'type': 'boolean',
+ 'description': "Is the license agreed to?",
+ },
+ },
+ },
+ },
+ },
+ },
+}
+
+__doc__ = get_schema_doc(schema) # Supplement python help()
+
+
+def install_drivers(cfg, pkg_install_func):
+ if not isinstance(cfg, dict):
+ raise TypeError("'drivers' config is not a dictionary.")
+
+ if len(cfg) and not util.which('ubuntu-drivers'):
+ LOG.debug("'ubuntu-drivers' command not available. "
+ "Installing ubuntu-drivers-common")
+ pkg_install_func(['ubuntu-drivers-common'])
+
+ cfgpath = 'nvidia/license-accepted'
+ nv_acc = util.get_cfg_by_path(cfg, cfgpath)
+ LOG.debug("config drivers/%s=%s", cfgpath, nv_acc)
+ if nv_acc is True:
+ LOG.debug("Installing nvidia drivers.")
+ util.subp(['ubuntu-drivers', 'autoinstall', '--gpgpu'])
+
+
+def handle(name, cfg, cloud, log, _args):
+ if "drivers" not in cfg:
+ log.debug("Skipping module named %s, no 'drivers' key in config", name)
+ return
+
+ validate_cloudconfig_schema(cfg, schema)
+ install_drivers(cfg['drivers'], cloud.distro.install_packages)
diff --git a/cloudinit/config/tests/test_ubuntu_drivers.py b/cloudinit/config/tests/test_ubuntu_drivers.py
new file mode 100644
index 0000000..224a371
--- /dev/null
+++ b/cloudinit/config/tests/test_ubuntu_drivers.py
@@ -0,0 +1,56 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit.tests.helpers import CiTestCase, skipUnlessJsonSchema, mock
+from cloudinit.config.schema import (
+ SchemaValidationError, validate_cloudconfig_schema)
+from cloudinit.config import cc_ubuntu_drivers as drivers
+
+MPATH = "cloudinit.config.cc_ubuntu_drivers."
+
+
+class TestUbuntuDrivers(CiTestCase):
+ cfg_accepted = {'drivers': {'nvidia': {'license-accepted': True}}}
+ auto_install_gpgpu = ['ubuntu-drivers', 'autoinstall', '--gpgpu']
+
+ @skipUnlessJsonSchema()
+ def test_schema_requires_boolean_for_license_accepted(self):
+ with self.assertRaisesRegex(
+ SchemaValidationError, ".*license-accepted.*TRUE.*boolean"):
+ validate_cloudconfig_schema(
+ {'drivers': {'nvidia': {'license-accepted': "TRUE"}}},
+ schema=drivers.schema, strict=True)
+
+ @mock.patch(MPATH + "util.subp")
+ @mock.patch(MPATH + "util.which", return_value=False)
+ def test_handle_does_package_install(self, m_which, m_subp):
+ """Positive path test through handle. Package should be installed."""
+ myCloud = mock.MagicMock()
+ drivers.handle(
+ 'ubuntu_drivers', self.cfg_accepted, myCloud, None, None)
+ myCloud.distro.install_packages.assert_has_calls(
+ [mock.call(['ubuntu-drivers-common'])])
+ m_subp.assert_has_calls([mock.call(self.auto_install_gpgpu)])
+
+ @mock.patch(MPATH + "install_drivers")
+ def test_handle_no_drivers_does_nothing(self, m_install_drivers):
+ """If no 'drivers' key in the config, nothing should be done."""
+ myCloud = mock.MagicMock()
+ myLog = mock.MagicMock()
+ drivers.handle('ubuntu_drivers', {'foo': 'bzr'}, myCloud, myLog, None)
+ self.assertIn('Skipping module named',
+ myLog.debug.call_args_list[0][0][0])
+ m_install_drivers.assert_not_called()
+
+ @mock.patch(MPATH + "util.subp")
+ @mock.patch(MPATH + "util.which", return_value=True)
+ def test_install_drivers_no_install_if_present(self, m_which, m_subp):
+ """If 'ubuntu-drivers' is present, no package install should occur."""
+ pkg_install = mock.MagicMock()
+ drivers.install_drivers(self.cfg_accepted['drivers'],
+ pkg_install_func=pkg_install)
+ pkg_install.assert_not_called()
+ m_which.assert_called_with("ubuntu-drivers")
+ m_subp.assert_has_calls([mock.call(self.auto_install_gpgpu)])
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 083a8ef..219cb57 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -635,6 +635,21 @@ def get_cfg_option_list(yobj, key, default=None):
# get a cfg entry by its path array
# for f['a']['b']: get_cfg_by_path(mycfg,('a','b'))
def get_cfg_by_path(yobj, keyp, default=None):
+ """Return the value of the item at path C{keyp} in C{yobj}.
+
+ example:
+ get_cfg_by_path({'a': {'b': {'num': 4}}}, 'a/b/num') == 4
+ get_cfg_by_path({'a': {'b': {'num': 4}}}, 'c/d') == None
+
+ @param yobj: A dictionary.
+ @param keyp: A path inside yobj. it can be a '/' delimited string,
+ or an iterable.
+ @param default: The default to return if the path does not exist.
+ @return: The value of the item at keyp."
+ is not found."""
+
+ if isinstance(keyp, six.string_types):
+ keyp = keyp.split("/")
cur = yobj
for tok in keyp:
if tok not in cur:
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index cf2e240..36061b5 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -110,6 +110,9 @@ cloud_final_modules:
- landscape
- lxd
{% endif %}
+{% if variant in ["ubuntu", "unknown"] %}
+ - ubuntu_drivers
+{% endif %}
{% if variant not in ["freebsd"] %}
- puppet
- chef
diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
index 1ecb6c6..68b9bb8 100644
--- a/tests/unittests/test_handler/test_schema.py
+++ b/tests/unittests/test_handler/test_schema.py
@@ -26,6 +26,7 @@ class GetSchemaTest(CiTestCase):
'cc_ntp',
'cc_resizefs',
'cc_runcmd',
+ 'cc_ubuntu_drivers',
'cc_zypper_add_repo'
],
[subschema['id'] for subschema in schema['allOf']])
Follow ups