From: Bastian Blank <waldi@debian.org>
Date: Tue, 16 Aug 2022 15:45:11 +0200
Subject: [PATCH] config: Support APT automated mirror selection

From 8dffe8af24604e1949cc991c1db181a69695d945 Mon Sep 17 00:00:00 2001
Forwarded: https://github.com/canonical/cloud-init/pull/1670
---
 cloudinit/config/cc_apt_configure.py               | 22 ++++++-
 .../config/schemas/schema-cloud-config-v1.json     |  5 ++
 .../config/test_apt_configure_mirrorlists_v3.py    | 69 ++++++++++++++++++++++
 3 files changed, 95 insertions(+), 1 deletion(-)
 create mode 100644 tests/unittests/config/test_apt_configure_mirrorlists_v3.py

Index: cloud-init/cloudinit/config/cc_apt_configure.py
===================================================================
--- cloud-init.orig/cloudinit/config/cc_apt_configure.py
+++ cloud-init/cloudinit/config/cc_apt_configure.py
@@ -146,7 +146,10 @@ def apply_apt(cfg, cloud, gpg):
         matcher = re.compile(matchcfg).search
     _ensure_dependencies(cfg, matcher, cloud)
 
-    if util.is_false(cfg.get("preserve_sources_list", False)):
+    if util.is_true(cfg.get("generate_mirrorlists", False)):
+        generate_mirrorlists(cfg, mirrors, cloud)
+
+    elif util.is_false(cfg.get("preserve_sources_list", False)):
         keys = add_mirror_keys(cfg, cloud, gpg)
         generate_sources_list(cfg, release, mirrors, cloud, keys)
         rename_apt_lists(mirrors, arch)
@@ -590,6 +593,23 @@ def generate_sources_list(cfg, release,
             util.del_file(apt_sources_list)
 
 
+def generate_mirrorlists(cfg, mirrors, cloud):
+    """generate_mirrorlists
+    create one file for every mirror for apt-transport-mirror(1)"""
+    aptmir = pathlib.Path("/etc/apt/mirrors")
+    util.ensure_dir(str(aptmir))
+    util.write_file(
+        str(aptmir / f"{cloud.distro.name}.list"),
+        f"{mirrors['PRIMARY']}\n",
+        mode=0o644,
+    )
+    util.write_file(
+        str(aptmir / f"{cloud.distro.name}-security.list"),
+        f"{mirrors['SECURITY']}\n",
+        mode=0o644,
+    )
+
+
 def add_apt_key_raw(key, file_name, gpg, hardened=False):
     """
     actual adding of a key as defined in key argument
Index: cloud-init/cloudinit/config/schemas/schema-cloud-config-v1.json
===================================================================
--- cloud-init.orig/cloudinit/config/schemas/schema-cloud-config-v1.json
+++ cloud-init/cloudinit/config/schemas/schema-cloud-config-v1.json
@@ -1095,6 +1095,11 @@
               "default": false,
               "description": "By default, cloud-init will generate a new sources list in ``/etc/apt/sources.list.d`` based on any changes specified in cloud config. To disable this behavior and preserve the sources list from the pristine image, set **preserve_sources_list** to ``true``.\n\nThe **preserve_sources_list** option overrides all other config keys that would alter ``sources.list`` or ``sources.list.d``, **except** for additional sources to be added to ``sources.list.d``."
             },
+            "generate_mirrorlists": {
+              "type": "boolean",
+              "default": false,
+              "description": "Write lists for APT automated mirror selection (``apt-transport-mirror(1)``). It will write separate lists for both the PRIMARY and SECURITY mirror into ``/etc/apt/mirrors/${DIST}.list`` and ``/etc/apt/mirrors/${DIST}-security.list``.  Those can then be used in the APT source.list as ``mirror+file:///etc/apt/mirrors/${DIST}.list``. No ``/etc/apt/sources.list`` will be writte in this case."
+            },
             "disable_suites": {
               "type": "array",
               "items": {
Index: cloud-init/tests/unittests/config/test_apt_configure_mirrorlists_v3.py
===================================================================
--- /dev/null
+++ cloud-init/tests/unittests/config/test_apt_configure_mirrorlists_v3.py
@@ -0,0 +1,80 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+""" test_apt_custom_mirrorlists
+Test generation of apt mirror lists
+"""
+import logging
+import shutil
+import tempfile
+from contextlib import ExitStack
+from unittest import mock
+
+import pytest
+
+from cloudinit import subp, util
+from cloudinit.config import cc_apt_configure
+from tests.unittests import helpers as t_help
+from tests.unittests.util import get_cloud
+
+LOG = logging.getLogger(__name__)
+
+@pytest.fixture
+def mock_lsb_release():
+    """Fixture to mock lsb_release"""
+    with mock.patch("cloudinit.util.lsb_release") as mock_lsr:
+        mock_lsr.return_value = {"codename": "fakerel"}
+        yield mock_lsr
+
+@pytest.fixture
+def mock_dpkg_arch():
+    """Fixture to mock get_dpkg_architecture"""
+    with mock.patch("cloudinit.util.get_dpkg_architecture") as mock_arch:
+        mock_arch.return_value = "amd64"
+        yield mock_arch
+
+@pytest.fixture
+def temp_root():
+    """Fixture to create and clean up a temporary directory"""
+    root_dir = tempfile.mkdtemp()
+    try:
+        yield root_dir
+    finally:
+        shutil.rmtree(root_dir)
+
+def test_apt_v3_mirrors_list(mock_lsb_release, mock_dpkg_arch, temp_root):
+    """Test generation of mirrorlists"""
+    # Configuration for apt mirror list generation
+    cfg = {"apt": {"generate_mirrorlists": True}}
+
+    # Get cloud instance
+    mycloud = get_cloud("ubuntu")
+
+    # Use ExitStack to manage multiple context managers
+    with ExitStack() as stack:
+        # Mock write_file and ensure_dir
+        mock_writefile = stack.enter_context(
+            mock.patch.object(util, "write_file")
+        )
+        stack.enter_context(mock.patch.object(util, "ensure_dir"))
+
+        # Call the apt configuration handler
+        cc_apt_configure.handle("test", cfg, mycloud, None)
+
+    # Assert that write_file was called with expected arguments
+    assert mock_writefile.call_count == 2
+
+    # Check the specific calls to write_file
+    mock_writefile.assert_has_calls([
+        mock.call(
+            "/etc/apt/mirrors/ubuntu.list",
+            "http://archive.ubuntu.com/ubuntu/\n",
+            mode=0o644,
+        ),
+        mock.call(
+            "/etc/apt/mirrors/ubuntu-security.list",
+            "http://security.ubuntu.com/ubuntu/\n",
+            mode=0o644,
+        )
+    ], any_order=True)
+
+# vi: ts=4 expandtab
