Chad Smith has proposed merging ~chad.smith/cloud-init:bug/1840080-ubuntu-drivers-emit-latelink into cloud-init:master.
Commit message: ubuntu-drivers: emit latelink=true to /etc/default/ to accept nvidia eula To accept NVIDIA EULA, cloud-init needs to emit latelink=true to the INI file /etc/default/linux-modules-nvidia prior to installing nvidia drivers with the ubuntu-drivers command. This will allow NVIDIA modules prior to installing drivers enabled for linking to the running kernel. LP: #1840080 Requested reviews: cloud-init commiters (cloud-init-dev) Related bugs: Bug #1840080 in cloud-init (Ubuntu): "cloud-init cc_ubuntu_drivers does not set up /etc/default/linux-modules-nvidia" https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1840080 For more details, see: https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/371369 -- Your team cloud-init commiters is requested to review the proposed merge of ~chad.smith/cloud-init:bug/1840080-ubuntu-drivers-emit-latelink into cloud-init:master.
diff --git a/cloudinit/config/cc_ubuntu_drivers.py b/cloudinit/config/cc_ubuntu_drivers.py index 91feb60..593b1b0 100644 --- a/cloudinit/config/cc_ubuntu_drivers.py +++ b/cloudinit/config/cc_ubuntu_drivers.py @@ -2,6 +2,7 @@ """Ubuntu Drivers: Interact with third party drivers in Ubuntu.""" +import os from textwrap import dedent from cloudinit.config.schema import ( @@ -61,9 +62,48 @@ schema = { OLD_UBUNTU_DRIVERS_STDERR_NEEDLE = ( "ubuntu-drivers: error: argument <command>: invalid choice: 'install'") +ETC_DEFAULT_FILE_NVIDIA='/etc/default/linux-modules-nvidia' + __doc__ = get_schema_doc(schema) # Supplement python help() +def ammend_driver_defaults(config, config_file): + """Update INI-type config_file with config values provided. + + Create config_file if it doesn't exist. + + Other tools in linux-restricted-modules cloud have been executed to write + the config_file. So, preserve any pre-existing content and updating + config keys to new values if already present. + + Append config key=value lines if key is not yet in + config_file. + + @param config: Dict of key value pairs to write or update in config_file + @param config_file: path to defaults file. + """ + if not config_file: + config_file = ETC_DEFAULT_FILE_NVIDIA + if os.path.exists(config_file): + lines = util.load_file(config_file).splitlines() + replaced_keys = set() + for idx, line in enumerate(lines): + for key in config.keys(): + if line.startswith('{k}='.format(k=key)): + lines[idx] = '{k}={v}'.format(k=key, v=config[key]) + replaced_keys.update([key]) + break + new_keys = set(config.keys()).difference(replaced_keys) + lines.extend( + ['{k}={v}'.format(k=k, v=config[k]) for k in new_keys] + ['']) + else: + lines = [ + '{k}={v}'.format(k=k, v=v) for (k, v) in sorted(config.items())] + lines.insert(0, '# Written by cloud-init #cloud-config') + lines.append('') + util.write_file(config_file, '\n'.join(lines)) + + def install_drivers(cfg, pkg_install_func): if not isinstance(cfg, dict): raise TypeError( @@ -92,6 +132,8 @@ def install_drivers(cfg, pkg_install_func): LOG.debug("Installing NVIDIA drivers (%s=%s, version=%s)", cfgpath, nv_acc, version_cfg if version_cfg else 'latest') + ammend_driver_defaults({'latelink':'true'}, ETC_DEFAULT_FILE_NVIDIA) + try: util.subp(['ubuntu-drivers', 'install', '--gpgpu', driver_arg]) except util.ProcessExecutionError as exc: diff --git a/cloudinit/config/tests/test_ubuntu_drivers.py b/cloudinit/config/tests/test_ubuntu_drivers.py index efba4ce..73f6fda 100644 --- a/cloudinit/config/tests/test_ubuntu_drivers.py +++ b/cloudinit/config/tests/test_ubuntu_drivers.py @@ -6,7 +6,7 @@ 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 -from cloudinit.util import ProcessExecutionError +from cloudinit.util import ProcessExecutionError, load_file, write_file MPATH = "cloudinit.config.cc_ubuntu_drivers." OLD_UBUNTU_DRIVERS_ERROR_STDERR = ( @@ -14,6 +14,26 @@ OLD_UBUNTU_DRIVERS_ERROR_STDERR = ( "(choose from 'list', 'autoinstall', 'devices', 'debug')\n") +class TestAmmendDriverDefaults(CiTestCase): + + def test_write_new_file_sorted_if_absent(self): + """Create config_file with cloud-init header comment if absent.""" + outfile = self.tmp_path('driver-config') + drivers.ammend_driver_defaults({'k2': 'v2', 'k1': 'v1'}, outfile) + expected = '# Written by cloud-init #cloud-config\nk1=v1\nk2=v2\n' + self.assertEqual(expected, load_file(outfile)) + + def test_ammend_keys_if_present(self): + """If config keys are already present, ammend the key value.""" + existing = '# preexisting file content\nk2=oldv2\nk1=oldv1\n' + outfile = self.tmp_path('driver-config') + write_file(outfile, existing) + drivers.ammend_driver_defaults({'k2': 'newv2', 'k3': 'v3'}, outfile) + expected = existing.replace('k2=oldv2', 'k2=newv2') + expected += 'k3=v3\n' + self.assertEqual(expected, load_file(outfile)) + + class TestUbuntuDrivers(CiTestCase): cfg_accepted = {'drivers': {'nvidia': {'license-accepted': True}}} install_gpgpu = ['ubuntu-drivers', 'install', '--gpgpu', 'nvidia'] @@ -28,9 +48,10 @@ class TestUbuntuDrivers(CiTestCase): {'drivers': {'nvidia': {'license-accepted': "TRUE"}}}, schema=drivers.schema, strict=True) + @mock.patch(MPATH + "ammend_driver_defaults") @mock.patch(MPATH + "util.subp", return_value=('', '')) @mock.patch(MPATH + "util.which", return_value=False) - def _assert_happy_path_taken(self, config, m_which, m_subp): + def _assert_happy_path_taken(self, config, m_which, m_subp, m_ammend_cfg): """Positive path test through handle. Package should be installed.""" myCloud = mock.MagicMock() drivers.handle('ubuntu_drivers', config, myCloud, None, None) @@ -38,6 +59,9 @@ class TestUbuntuDrivers(CiTestCase): myCloud.distro.install_packages.call_args_list) self.assertEqual([mock.call(self.install_gpgpu)], m_subp.call_args_list) + self.assertEqual( + [mock.call({'latelink': 'true'}, drivers.ETC_DEFAULT_FILE_NVIDIA)], + m_ammend_cfg.call_args_list) def test_handle_does_package_install(self): self._assert_happy_path_taken(self.cfg_accepted) @@ -48,10 +72,11 @@ class TestUbuntuDrivers(CiTestCase): new_config['drivers']['nvidia']['license-accepted'] = true_value self._assert_happy_path_taken(new_config) + @mock.patch(MPATH + "ammend_driver_defaults") @mock.patch(MPATH + "util.subp", side_effect=ProcessExecutionError( stdout='No drivers found for installation.\n', exit_code=1)) @mock.patch(MPATH + "util.which", return_value=False) - def test_handle_raises_error_if_no_drivers_found(self, m_which, m_subp): + def test_handle_raises_error_if_no_drivers_found(self, m_which, m_subp, _): """If ubuntu-drivers doesn't install any drivers, raise an error.""" myCloud = mock.MagicMock() with self.assertRaises(Exception): @@ -108,9 +133,10 @@ class TestUbuntuDrivers(CiTestCase): myLog.debug.call_args_list[0][0][0]) self.assertEqual(0, m_install_drivers.call_count) + @mock.patch(MPATH + "ammend_driver_defaults") @mock.patch(MPATH + "util.subp", return_value=('', '')) @mock.patch(MPATH + "util.which", return_value=True) - def test_install_drivers_no_install_if_present(self, m_which, m_subp): + 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'], @@ -128,11 +154,12 @@ class TestUbuntuDrivers(CiTestCase): drivers.install_drivers("mystring", pkg_install_func=pkg_install) self.assertEqual(0, pkg_install.call_count) + @mock.patch(MPATH + "ammend_driver_defaults") @mock.patch(MPATH + "util.subp", side_effect=ProcessExecutionError( stderr=OLD_UBUNTU_DRIVERS_ERROR_STDERR, exit_code=2)) @mock.patch(MPATH + "util.which", return_value=False) def test_install_drivers_handles_old_ubuntu_drivers_gracefully( - self, m_which, m_subp): + self, m_which, m_subp, _): """Older ubuntu-drivers versions should emit message and raise error""" myCloud = mock.MagicMock() with self.assertRaises(Exception): @@ -153,9 +180,10 @@ class TestUbuntuDriversWithVersion(TestUbuntuDrivers): 'drivers': {'nvidia': {'license-accepted': True, 'version': '123'}}} install_gpgpu = ['ubuntu-drivers', 'install', '--gpgpu', 'nvidia:123'] + @mock.patch(MPATH + "ammend_driver_defaults") @mock.patch(MPATH + "util.subp", return_value=('', '')) @mock.patch(MPATH + "util.which", return_value=False) - def test_version_none_uses_latest(self, m_which, m_subp): + def test_version_none_uses_latest(self, m_which, m_subp, _): myCloud = mock.MagicMock() version_none_cfg = { 'drivers': {'nvidia': {'license-accepted': True, 'version': None}}}
_______________________________________________ Mailing list: https://launchpad.net/~cloud-init-dev Post to : cloud-init-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~cloud-init-dev More help : https://help.launchpad.net/ListHelp