Merge ~ogayot/curtin:curthooks-fix-apt-config-translation into curtin:master

Proposed by Olivier Gayot
Status: Merged
Approved by: Olivier Gayot
Approved revision: cc23249707a2773ecb1f10b17de35fff4620de9a
Merged at revision: cc23249707a2773ecb1f10b17de35fff4620de9a
Proposed branch: ~ogayot/curtin:curthooks-fix-apt-config-translation
Merge into: curtin:master
Diff against target: 173 lines (+86/-9)
4 files modified
curtin/commands/apt_config.py (+8/-8)
curtin/commands/curthooks.py (+1/-1)
tests/unittests/test_apt_source.py (+44/-0)
tests/unittests/test_curthooks.py (+33/-0)
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Dan Bungert Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+437494@code.launchpad.net

Commit message

apt-config: fix curthooks unconditionally triggering apt-config

The following commit:

  1257a38f translate old curtin apt features to new format

introduced the ability to translate old top level keys related to apt
features (e.g., debconf_selections, apt-proxy, apt-mirrors) to the new
format where they are stored as children of 'apt'.

```
                              apt:
debconf_selections: => debconf_selections:
  foobar foobar
```

Sadly, by doing so, it introduced a regression, making curthooks call
apt_config.handle_apt unconditionally.

The curthooks.do_apt_config function only calls apt_config.handle_apt if
the configuration does not contain the 'apt' key or if the 'apt' key has
the value None (i.e., null in YAML). When implementing the translation
from old to new configuration format, we accidentally forced the 'apt'
key to exist and coerced it to dictionary.

This effectively defeats the condition in curthooks.do_apt_config.

Fixed by making sure we only create the apt key if needed when doing the
translation.

Description of the change

The conditional execution of apt_config.handle_apt from curthooks builtins has been broken for a while. A regression was introduced in 2016 when adding a mechanism to translate things like:

```
                                  apt:
    debconf_selections: => debconf_selections:
      foobar foobar
```

The function responsible for calling handle_apt is as follows:

def do_apt_config(cfg, target):
    cfg = apt_config.translate_old_apt_features(cfg)
    apt_cfg = cfg.get("apt")
    if apt_cfg is not None:
        LOG.info("curthooks handling apt to target %s with config %s",
                 target, apt_cfg)
        apt_config.handle_apt(apt_cfg, target)
    else:
        LOG.info("No apt config provided, skipping")

Sadly, the implementation of translate_old_apt_features made is so that the `if apt_cfg is not None:` condition is always true.

This first patch fixes the issue by making sure the translate_old_apt_features function only creates an 'apt' key and assign it a dictionary, if necessary. This also covers the functions with unit tests.

The second patch also fixes an obvious mistake in a debug log.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Good job on figuring out what's going on here. As usual I think we can clean the situation up a bit more though! Let me know what you think.

Revision history for this message
Olivier Gayot (ogayot) wrote :

Thanks. I'm leaning towards making translate_old_apt_features mutate the config. Shallow copies are not enough in this context.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Dan Bungert (dbungert) wrote :

LGTM, quick test suggestion then please merge if Michael is content.

review: Approve
Revision history for this message
Dan Bungert (dbungert) :
Revision history for this message
Olivier Gayot (ogayot) :
Revision history for this message
Dan Bungert (dbungert) :
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

LGTM, certainly an improvement.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/commands/apt_config.py b/curtin/commands/apt_config.py
2index f9cce34..e02327a 100644
3--- a/curtin/commands/apt_config.py
4+++ b/curtin/commands/apt_config.py
5@@ -686,11 +686,9 @@ def apt_command(args):
6
7
8 def translate_old_apt_features(cfg):
9- """translate the few old apt related features into the new config format"""
10- predef_apt_cfg = cfg.get("apt")
11- if predef_apt_cfg is None:
12- cfg['apt'] = {}
13- predef_apt_cfg = cfg.get("apt")
14+ """translate the few old apt related features into the new config format ;
15+ by mutating the cfg object. """
16+ predef_apt_cfg = cfg.get("apt", {})
17
18 if cfg.get('apt_proxy') is not None:
19 if predef_apt_cfg.get('proxy') is not None:
20@@ -700,9 +698,10 @@ def translate_old_apt_features(cfg):
21 LOG.error(msg)
22 raise ValueError(msg)
23
24+ cfg.setdefault('apt', {})
25 cfg['apt']['proxy'] = cfg.get('apt_proxy')
26 LOG.debug("Transferred %s into new format: %s", cfg.get('apt_proxy'),
27- cfg.get('apte'))
28+ cfg.get('apt'))
29 del cfg['apt_proxy']
30
31 if cfg.get('apt_mirrors') is not None:
32@@ -714,6 +713,7 @@ def translate_old_apt_features(cfg):
33 raise ValueError(msg)
34
35 old = cfg.get('apt_mirrors')
36+ cfg.setdefault('apt', {})
37 cfg['apt']['primary'] = [{"arches": ["default"],
38 "uri": old.get('ubuntu_archive')}]
39 cfg['apt']['security'] = [{"arches": ["default"],
40@@ -730,6 +730,7 @@ def translate_old_apt_features(cfg):
41 "are mutually exclusive")
42 LOG.error(msg)
43 raise ValueError(msg)
44+ cfg.setdefault('apt', {})
45 cfg['apt']['preserve_sources_list'] = False
46
47 if cfg.get('debconf_selections') is not None:
48@@ -741,14 +742,13 @@ def translate_old_apt_features(cfg):
49 raise ValueError(msg)
50
51 selsets = cfg.get('debconf_selections')
52+ cfg.setdefault('apt', {})
53 cfg['apt']['debconf_selections'] = selsets
54 LOG.info("Transferred %s into new format: %s",
55 cfg.get('debconf_selections'),
56 cfg.get('apt'))
57 del cfg['debconf_selections']
58
59- return cfg
60-
61
62 CMD_ARGUMENTS = (
63 ((('-c', '--config'),
64diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
65index 69833a2..74a67e8 100644
66--- a/curtin/commands/curthooks.py
67+++ b/curtin/commands/curthooks.py
68@@ -89,7 +89,7 @@ UEFI_BOOT_ENTRY_IS_NETWORK = r'.*(Network|PXE|NIC|Ethernet|LAN|IP4|IP6)+.*'
69
70
71 def do_apt_config(cfg, target):
72- cfg = apt_config.translate_old_apt_features(cfg)
73+ apt_config.translate_old_apt_features(cfg)
74 apt_cfg = cfg.get("apt")
75 if apt_cfg is not None:
76 LOG.info("curthooks handling apt to target %s with config %s",
77diff --git a/tests/unittests/test_apt_source.py b/tests/unittests/test_apt_source.py
78index 9fb3fc5..9a840c2 100644
79--- a/tests/unittests/test_apt_source.py
80+++ b/tests/unittests/test_apt_source.py
81@@ -678,6 +678,50 @@ Pin-Priority: -1
82
83 mockobj.assert_called_with("preferencesfn", expected_content)
84
85+ def test_translate_old_apt_features(self):
86+ cfg = {}
87+ apt_config.translate_old_apt_features(cfg)
88+ self.assertEqual(cfg, {})
89+
90+ cfg = {"debconf_selections": "foo"}
91+ apt_config.translate_old_apt_features(cfg)
92+ self.assertEqual(cfg, {"apt": {"debconf_selections": "foo"}})
93+
94+ cfg = {"apt_proxy": {"http_proxy": "http://proxy:3128"}}
95+ apt_config.translate_old_apt_features(cfg)
96+ self.assertEqual(cfg, {
97+ "apt": {
98+ "proxy": {"http_proxy": "http://proxy:3128"},
99+ }}
100+ )
101+
102+ cfg = {
103+ "apt": {"debconf_selections": "foo"},
104+ "apt_proxy": {"http_proxy": "http://proxy:3128"},
105+ }
106+ apt_config.translate_old_apt_features(cfg)
107+ self.assertEqual(cfg, {
108+ "apt": {
109+ "proxy": {"http_proxy": "http://proxy:3128"},
110+ "debconf_selections": "foo",
111+ }}
112+ )
113+
114+ def test_translate_old_apt_features_conflicts(self):
115+ with self.assertRaisesRegex(ValueError, 'mutually exclusive'):
116+ apt_config.translate_old_apt_features({
117+ "debconf_selections": "foo",
118+ "apt": {
119+ "debconf_selections": "bar",
120+ }})
121+
122+ with self.assertRaisesRegex(ValueError, 'mutually exclusive'):
123+ apt_config.translate_old_apt_features({
124+ "apt_proxy": {"http_proxy": "http://proxy:3128"},
125+ "apt": {
126+ "proxy": {"http_proxy": "http://proxy:3128"},
127+ }})
128+
129 def test_mirror(self):
130 """test_mirror - Test defining a mirror"""
131 pmir = "http://us.archive.ubuntu.com/ubuntu/"
132diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
133index c13434c..9e4fa87 100644
134--- a/tests/unittests/test_curthooks.py
135+++ b/tests/unittests/test_curthooks.py
136@@ -2268,4 +2268,37 @@ class TestUefiFindGrubDeviceIds(CiTestCase):
137 self._sconfig(cfg)))
138
139
140+class TestDoAptConfig(CiTestCase):
141+ def setUp(self):
142+ super(TestDoAptConfig, self).setUp()
143+ self.handle_apt_sym = 'curtin.commands.curthooks.apt_config.handle_apt'
144+
145+ def test_no_apt_config(self):
146+ with patch(self.handle_apt_sym) as m_handle_apt:
147+ curthooks.do_apt_config({}, target="/")
148+ m_handle_apt.assert_not_called()
149+
150+ def test_apt_config_none(self):
151+ with patch(self.handle_apt_sym) as m_handle_apt:
152+ curthooks.do_apt_config({"apt": None}, target="/")
153+ m_handle_apt.assert_not_called()
154+
155+ def test_apt_config_dict(self):
156+ with patch(self.handle_apt_sym) as m_handle_apt:
157+ curthooks.do_apt_config({"apt": {}}, target="/")
158+ m_handle_apt.assert_called()
159+
160+ def test_with_apt_config(self):
161+ with patch(self.handle_apt_sym) as m_handle_apt:
162+ curthooks.do_apt_config(
163+ {"apt": {"proxy": {"http_proxy": "http://proxy:3128"}}},
164+ target="/")
165+ m_handle_apt.assert_called_once()
166+
167+ def test_with_debconf_selections(self):
168+ # debconf_selections are translated to apt config
169+ with patch(self.handle_apt_sym) as m_handle_apt:
170+ curthooks.do_apt_config({"debconf_selections": "foo"}, target="/")
171+ m_handle_apt.assert_called_once()
172+
173 # vi: ts=4 expandtab syntax=python

Subscribers

People subscribed via source and target branches