I did some analysis and the issue seems to be the "$MIRROR/$RELEASE" part of the custom template.
The relevant source code part (the generate_sources_list function in curtin/commands/apt_config.py):
```
333 def generate_sources_list(cfg, release, mirrors, target=None, arch=None):
334 """ generate_sources_list
335 create a source.list file based on a custom or default template
336 by replacing mirrors and release in the template
337 """
338 aptsrc = "/etc/apt/sources.list"
339
340 tmpl = cfg.get('sources_list', None)
341 from_file = False
342 if tmpl is None:
343 LOG.info("No custom template provided, fall back to modify"
344 "mirrors in %s on the target system", aptsrc)
345 tmpl = util.load_file(paths.target_path(target, aptsrc))
346 from_file = True
347
348 entries = [SourceEntry(line) for line in tmpl.splitlines(True)]
349 if from_file:
350 # when loading from an existing file, we also replace default
351 # URIs with configured mirrors
352 entries = update_default_mirrors(entries, mirrors, target, arch)
353
354 entries = update_mirrors(entries, mirrors)
355 entries = update_dist(entries, release)
```
The problem here is that $RELEASE appears in the "uri" attribute, e.g.: "'uri': '$MIRROR/$RELEASE'".
This causes the update_mirrors function (curtin/commands/apt_config.py) to fail:
```
251 def update_mirrors(entries, mirrors):
252 """perform template replacement of mirror placeholders with configured
253 values"""
254 for entry in entries:
255 entry.uri = util.render_string(entry.uri, mirrors)
256 return entries
```
The problem persists with Ubuntu 22.04 LTS (tested with one of the final RCs: https:/ /iso.qa. ubuntu. com/qatracker/ milestones/ 432/builds/ 247146/ downloads).
I did some analysis and the issue seems to be the "$MIRROR/$RELEASE" part of the custom template.
The relevant source code part (the generate_ sources_ list function in curtin/ commands/ apt_config. py): sources_ list(cfg, release, mirrors, target=None, arch=None): sources_ list sources. list" 'sources_ list', None) file(paths. target_ path(target, aptsrc)) (True)] default_ mirrors( entries, mirrors, target, arch) mirrors( entries, mirrors) dist(entries, release)
```
333 def generate_
334 """ generate_
335 create a source.list file based on a custom or default template
336 by replacing mirrors and release in the template
337 """
338 aptsrc = "/etc/apt/
339
340 tmpl = cfg.get(
341 from_file = False
342 if tmpl is None:
343 LOG.info("No custom template provided, fall back to modify"
344 "mirrors in %s on the target system", aptsrc)
345 tmpl = util.load_
346 from_file = True
347
348 entries = [SourceEntry(line) for line in tmpl.splitlines
349 if from_file:
350 # when loading from an existing file, we also replace default
351 # URIs with configured mirrors
352 entries = update_
353
354 entries = update_
355 entries = update_
```
After SourceEntry from python-apt ("from aptsources. sourceslist import SourceEntry") parses the custom template (s. apt.sources_list above), the data ("entries") looks as follows: sources. list', 'template': None, 'children': []} $RELEASE- updates' , 'dist': '$RELEASE-updates', 'comps': ['main', 'restricted'], 'comment': '', 'line': 'deb $MIRROR/ $RELEASE- updates $RELEASE-updates main restricted\n', 'file': '/etc/apt/ sources. list', 'template': None, 'children': []} $RELEASE- security' , 'dist': '$RELEASE- security' , 'comps': ['main', 'restricted'], 'comment': '', 'line': 'deb $MIRROR/ $RELEASE- security $RELEASE-security main restricted\n', 'file': '/etc/apt/ sources. list', 'template': None, 'children': []} sources. list', 'template': None, 'children': []} sources. list', 'template': None, 'children': []} $RELEASE- updates' , 'dist': '$RELEASE-updates', 'comps': ['universe'], 'comment': '', 'line': 'deb $MIRROR/ $RELEASE- updates $RELEASE-updates universe\n', 'file': '/etc/apt/ sources. list', 'template': None, 'children': []} $RELEASE- security' , 'dist': '$RELEASE- security' , 'comps': ['universe'], 'comment': '', 'line': 'deb $MIRROR/ $RELEASE- security $RELEASE-security universe\n', 'file': '/etc/apt/ sources. list', 'template': None, 'children': []}
```
{'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE', 'dist': '$RELEASE', 'comps': ['main', 'restricted'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE $RELEASE main restricted\n', 'file': '/etc/apt/
{'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/
{'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/
{'invalid': True, 'disabled': False, 'type': '', 'architectures': [], 'trusted': None, 'uri': '', 'dist': '', 'comps': [], 'comment': '', 'line': '\n', 'file': '/etc/apt/
{'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE', 'dist': '$RELEASE', 'comps': ['universe'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE $RELEASE universe\n', 'file': '/etc/apt/
{'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/
{'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/
```
The problem here is that $RELEASE appears in the "uri" attribute, e.g.: "'uri': '$MIRROR/ $RELEASE' ".
This causes the update_mirrors function (curtin/ commands/ apt_config. py) to fail: mirrors( entries, mirrors): string( entry.uri, mirrors)
```
251 def update_
252 """perform template replacement of mirror placeholders with configured
253 values"""
254 for entry in entries:
255 entry.uri = util.render_
256 return entries
```
The "mirrors" parameter has the following value: "{'PRIMARY': 'http:// ubuntu. company. tld', 'SECURITY': 'http:// ubuntu. company. tld', 'MIRROR': 'http:// ubuntu. company. tld'}"
Therefore, it fails with "KeyError: 'RELEASE'".
The $RELEASE key is normally handled by the update_dist function but it only operates on entry.dist and not entry.uri.
The easiest (but not necessarily most elegant) solution would probably be to put the "RELEASE" key in the "mirrors" dictionary as well.