diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 872905e39d1..d564cbbc289 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -11,6 +11,7 @@ import sys from collections import namedtuple from contextlib import suppress +from pathlib import Path from typing import Dict, Iterable, List, Optional, Set, Tuple, Union from cloudinit import ( @@ -459,8 +460,13 @@ def _reflect_cur_instance(self): # Remove the old symlink and attach a new one so # that further reads/writes connect into the right location idir = self._get_ipath() - util.del_file(self.paths.instance_link) - util.sym_link(idir, self.paths.instance_link) + destination = Path(self.paths.instance_link).resolve().absolute() + already_instancified = destination == Path(idir).absolute() + if already_instancified: + LOG.info("Instance link already exists, not recreating it.") + else: + util.del_file(self.paths.instance_link) + util.sym_link(idir, self.paths.instance_link) # Ensures these dirs exist dir_list = [] @@ -499,10 +505,16 @@ def _reflect_cur_instance(self): ) self._write_to_cache() - # Ensure needed components are regenerated - # after change of instance which may cause - # change of configuration - self._reset() + if already_instancified and previous_ds == ds: + LOG.info( + "Not re-loading configuration, instance " + "id and datasource have not changed." + ) + # Ensure needed components are regenerated + # after change of instance which may cause + # change of configuration + else: + self._reset() return iid def previous_iid(self): diff --git a/tests/integration_tests/test_instance_id.py b/tests/integration_tests/test_instance_id.py new file mode 100644 index 00000000000..ed94b6b61c7 --- /dev/null +++ b/tests/integration_tests/test_instance_id.py @@ -0,0 +1,97 @@ +from typing import cast + +import pytest +from pycloudlib.lxd.instance import LXDInstance + +from cloudinit import subp +from tests.integration_tests.instances import IntegrationInstance +from tests.integration_tests.integration_settings import PLATFORM + +_INSTANCE_ID = 0 + + +def setup_meta_data(instance: LXDInstance): + """Increment the instance id and apply it to the instance.""" + global _INSTANCE_ID + _INSTANCE_ID += 1 + command = [ + "lxc", + "config", + "set", + instance.name, + f"user.meta-data=instance-id: test_{_INSTANCE_ID}", + ] + subp.subp(command) + + +# class TestInstanceID: +@pytest.mark.skipif( + PLATFORM not in ["lxd_container", "lxd_vm"], + reason="Uses lxd-specific behavior.", +) +@pytest.mark.lxd_setup.with_args(setup_meta_data) +@pytest.mark.lxd_use_exec +def test_instance_id_changes(client: IntegrationInstance): + """Verify instance id change behavior + + If the id from the datasource changes, cloud-init should update the + instance id link. + """ + client.execute("cloud-init status --wait") + # check that instance id is the one we set + assert ( + "test_1" + == client.execute("cloud-init query instance-id").stdout.rstrip() + ) + assert ( + "/var/lib/cloud/instances/test_1" + == client.execute( + "readlink -f /var/lib/cloud/instance" + ).stdout.rstrip() + ) + + instance = cast(LXDInstance, client.instance) + setup_meta_data(instance) + client.restart() + client.execute("cloud-init status --wait") + # check that instance id is the one we reset + assert ( + "test_2" + == client.execute("cloud-init query instance-id").stdout.rstrip() + ) + assert ( + "/var/lib/cloud/instances/test_2" + == client.execute( + "readlink -f /var/lib/cloud/instance" + ).stdout.rstrip() + ) + + +@pytest.mark.lxd_use_exec +def test_instance_id_no_changes(client: IntegrationInstance): + """Verify instance id no change behavior + + If the id from the datasource does not change, cloud-init should not + update the instance id link. + """ + instance_id = client.execute( + "cloud-init query instance-id" + ).stdout.rstrip() + assert ( + f"/var/lib/cloud/instances/{instance_id}" + == client.execute( + "readlink -f /var/lib/cloud/instance" + ).stdout.rstrip() + ) + client.restart() + client.execute("cloud-init status --wait") + assert ( + instance_id + == client.execute("cloud-init query instance-id").stdout.rstrip() + ) + assert ( + f"/var/lib/cloud/instances/{instance_id}" + == client.execute( + "readlink -f /var/lib/cloud/instance" + ).stdout.rstrip() + )