Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check_format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
run: |
git fetch --unshallow
git remote add upstream https://git.launchpad.net/cloud-init
- name: "Install Python 3.10"
- name: "Install Python 3.11.9"
uses: actions/setup-python@v5
with:
python-version: '3.11.9'
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: CLA Check

on: [pull_request]
on:
pull_request:
branches-ignore:
- 'ubuntu/**'

jobs:
cla-check:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:
unittests:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
toxenv: [py3]
slug: [""]
experimental: [false]
check-latest: [false]
continue-on-error: [false]
include:
- python-version: "3.8"
- python-version: "3.9"
toxenv: lowest-supported
slug: (lowest-supported)
continue-on-error: false
Expand Down
5 changes: 4 additions & 1 deletion cloudinit/config/cc_lxd.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None:
bridge_cfg = lxd_cfg.get("bridge", {})
supplemental_schema_validation(init_cfg, bridge_cfg, preseed_str)

if not subp.which("lxd"):
try:
subp.subp(["snap", "list", "lxd"])
except subp.ProcessExecutionError:
# Non-zero exit means no LXD snap yet.
try:
subp.subp(["snap", "install", "lxd"])
except subp.ProcessExecutionError as e:
Expand Down
37 changes: 31 additions & 6 deletions cloudinit/reporting/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ def as_dict(self):

class FinishReportingEvent(ReportingEvent):
def __init__(
self, name, description, result=status.SUCCESS, post_files=None
self,
name,
description,
duration,
result=status.SUCCESS,
post_files=None,
):
super(FinishReportingEvent, self).__init__(
FINISH_EVENT_TYPE, name, description
Expand All @@ -85,18 +90,24 @@ def __init__(
if post_files is None:
post_files = []
self.post_files = post_files
self.duration = duration
if result not in status:
raise ValueError("Invalid result: %s" % result)

def as_string(self):
return "{0}: {1}: {2}: {3}".format(
self.event_type, self.name, self.result, self.description
return "{0}: {1}: {2}: {3} (duration: {4:.3f}s)".format(
self.event_type,
self.name,
self.result,
self.description,
self.duration,
)

def as_dict(self):
"""The event represented as json friendly."""
data = super(FinishReportingEvent, self).as_dict()
data["result"] = self.result
data["duration"] = self.duration
if self.post_files:
data["files"] = _collect_file_info(self.post_files)
return data
Expand Down Expand Up @@ -134,14 +145,18 @@ def report_event(event, excluded_handler_types=None):


def report_finish_event(
event_name, event_description, result=status.SUCCESS, post_files=None
event_name,
event_description,
duration,
result=status.SUCCESS,
post_files=None,
):
"""Report a "finish" event.

See :py:func:`.report_event` for parameter details.
"""
event = FinishReportingEvent(
event_name, event_description, result, post_files=post_files
event_name, event_description, duration, result, post_files=post_files
)
return report_event(event)

Expand Down Expand Up @@ -215,6 +230,7 @@ def __init__(
self.message = message
self.result_on_exception = result_on_exception
self.result = status.SUCCESS
self.start_timestamp = None
if post_files is None:
post_files = []
self.post_files = post_files
Expand Down Expand Up @@ -247,6 +263,7 @@ def __repr__(self):

def __enter__(self):
self.result = status.SUCCESS
self.start_timestamp = time.monotonic()
if self.reporting_enabled:
report_start_event(self.fullname, self.description)
if self.parent:
Expand Down Expand Up @@ -291,8 +308,16 @@ def __exit__(self, exc_type, exc_value, traceback):
if self.parent:
self.parent.children[self.name] = (result, msg)
if self.reporting_enabled:
if self.start_timestamp is not None:
duration = time.monotonic() - self.start_timestamp
else:
duration = 0.0
report_finish_event(
self.fullname, msg, result, post_files=self.post_files
self.fullname,
msg,
duration=duration,
result=result,
post_files=self.post_files,
)


Expand Down
2 changes: 2 additions & 0 deletions cloudinit/reporting/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ def _encode_event(self, event):
}
if hasattr(event, self.RESULT_KEY):
meta_data[self.RESULT_KEY] = event.result
if hasattr(event, "duration"):
meta_data["duration"] = event.duration
meta_data[self.MSG_KEY] = event.description
value = json.dumps(meta_data, separators=self.JSON_SEPARATORS)
# if it reaches the maximum length of kvp value,
Expand Down
30 changes: 10 additions & 20 deletions cloudinit/sources/DataSourceCloudStack.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from socket import gaierror, getaddrinfo, inet_ntoa
from struct import pack

from cloudinit import dmi, net, performance, sources, subp
from cloudinit import dmi, net, performance, sources
from cloudinit import url_helper as uhelp
from cloudinit import util
from cloudinit.net import dhcp
Expand Down Expand Up @@ -48,25 +48,15 @@ def __init__(self, virtual_router_address):
self.virtual_router_address = virtual_router_address

def _do_request(self, domu_request):
# The password server was in the past, a broken HTTP server, but is now
# fixed. wget handles this seamlessly, so it's easier to shell out to
# that rather than write our own handling code.
output, _ = subp.subp(
[
"wget",
"--quiet",
"--tries",
"3",
"--timeout",
"20",
"--output-document",
"-",
"--header",
"DomU_Request: {0}".format(domu_request),
"{0}:8080".format(self.virtual_router_address),
]
)
return output.strip()
url = f"http://{self.virtual_router_address}:8080"
headers = {"DomU_Request": domu_request}

resp = uhelp.readurl(url, headers=headers, timeout=20, retries=3)

if not resp.ok():
raise RuntimeError("Failed to fetch VM password from CloudStack")

return resp.contents.decode("utf-8").strip()

@performance.timed("Getting password", log_mode="always")
def get_password(self):
Expand Down
90 changes: 30 additions & 60 deletions cloudinit/sources/DataSourceScaleway.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,68 +322,38 @@ def network_config(self):
if self._network_config != sources.UNSET:
return self._network_config

if self.metadata["private_ip"] is None:
# New method of network configuration

netcfg = {}
ip_cfg = {}
for ip in self.metadata["public_ips"]:
# Use DHCP for primary address
if ip["address"] == self.ephemeral_fixed_address:
ip_cfg["dhcp4"] = True
# Force addition of a route to the metadata API
route = {
"on-link": True,
"to": "169.254.42.42/32",
"via": "62.210.0.1",
}
netcfg = {}
ip_cfg = {}
for ip in self.metadata["public_ips"]:
# Use DHCP for primary address
if ip["address"] == self.ephemeral_fixed_address:
ip_cfg["dhcp4"] = True
# Force addition of a route to the metadata API
route = {
"on-link": True,
"to": "169.254.42.42/32",
"via": "62.210.0.1",
}
if "routes" in ip_cfg.keys():
ip_cfg["routes"] += [route]
else:
ip_cfg["routes"] = [route]
else:
if "addresses" in ip_cfg.keys():
ip_cfg["addresses"] += (
f'{ip["address"]}/{ip["netmask"]}',
)
else:
ip_cfg["addresses"] = (f'{ip["address"]}/{ip["netmask"]}',)
if ip["family"] == "inet6":
route = {"via": ip["gateway"], "to": "::/0"}
if "routes" in ip_cfg.keys():
ip_cfg["routes"] += [route]
else:
ip_cfg["routes"] = [route]
else:
if "addresses" in ip_cfg.keys():
ip_cfg["addresses"] += (
f'{ip["address"]}/{ip["netmask"]}',
)
else:
ip_cfg["addresses"] = (
f'{ip["address"]}/{ip["netmask"]}',
)
if ip["family"] == "inet6":
route = {"via": ip["gateway"], "to": "::/0"}
if "routes" in ip_cfg.keys():
ip_cfg["routes"] += [route]
else:
ip_cfg["routes"] = [route]
netcfg[self.distro.fallback_interface] = ip_cfg
self._network_config = {"version": 2, "ethernets": netcfg}
else:
# Kept for backward compatibility
netcfg = {
"type": "physical",
"name": "%s" % self.distro.fallback_interface,
}
subnets = [{"type": "dhcp4"}]
if self.metadata["ipv6"]:
subnets += [
{
"type": "static",
"address": "%s" % self.metadata["ipv6"]["address"],
"netmask": "%s" % self.metadata["ipv6"]["netmask"],
"routes": [
{
"network": "::",
"prefix": "0",
"gateway": "%s"
% self.metadata["ipv6"]["gateway"],
}
],
}
]
netcfg["subnets"] = subnets
self._network_config = {"version": 1, "config": [netcfg]}
LOG.debug("network_config : %s", self._network_config)
netcfg[self.distro.fallback_interface] = ip_cfg
self._network_config = {"version": 2, "ethernets": netcfg}

return self._network_config

@property
Expand All @@ -410,11 +380,11 @@ def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False):

@property
def availability_zone(self):
return None
return self.metadata["zone"]

@property
def region(self):
return None
return self.metadata["zone"].rpartition("-")[0]


datasources = [
Expand Down
22 changes: 5 additions & 17 deletions cloudinit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,23 +584,11 @@ def get_linux_distro():
distro_name = platform.system().lower()
distro_version = platform.release()
else:
dist = ("", "", "")
try:
# Was removed in 3.8
dist = platform.dist() # type: ignore # pylint: disable=W1505,E1101
except Exception:
pass
finally:
found = None
for entry in dist:
if entry:
found = 1
if not found:
LOG.warning(
"Unable to determine distribution, template "
"expansion may have unexpected results"
)
return dist
LOG.warning(
"Unable to determine distribution, template "
"expansion may have unexpected results"
)
return "", "", ""

return (distro_name, distro_version, flavor)

Expand Down
7 changes: 4 additions & 3 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
cloud-init (25.4~2g78a74880-0ubuntu1) UNRELEASED; urgency=medium
cloud-init (25.4~2gb117d244-0ubuntu1) resolute; urgency=medium

* d/tests: exercise cloud-init status
* d/cloud-init-base.config - Remove old unnecessary logic.
* d/cloud-init-base.postinst - Remove old unnecessary logic.
* d/cloud-init-base.preinst - Delete file, logic was obsolete.
* d/cloud-init-base.prerm - Delete file, logic was obsolete.
* Upstream snapshot based on upstream/main at 78a74880.
* Upstream snapshot based on upstream/main at b117d244.
- Bugs fixed in this snapshot: (LP: #2136198)

-- Chad Smith <chad.smith@canonical.com> Tue, 09 Dec 2025 07:38:56 -0700
-- Chad Smith <chad.smith@canonical.com> Thu, 18 Dec 2025 17:07:20 -0700

cloud-init (25.4~1gcb12e00e-0ubuntu2) resolute; urgency=medium

Expand Down
7 changes: 4 additions & 3 deletions doc/rtd/development/contribute_code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ Cloud-init adheres to `PEP 8`_, and this is enforced by CI tests.
Python support
--------------

Cloud-init upstream currently supports Python 3.8 and above.
Cloud-init upstream currently supports Python 3.9 and above.

Cloud-init upstream will stay compatible with a particular Python version for 6
years after release. After 6 years, we will stop testing upstream changes
years after release. After that, upstream will stop testing upstream changes
against the unsupported version of Python and may introduce breaking changes.
This policy may change as needed.

The following table lists the cloud-init versions in which the minimum Python
version changed:
Expand All @@ -35,6 +34,8 @@ version changed:

* - Cloud-init version
- Python version
* - 25.4
- 3.9+
* - 24.3
- 3.8+
* - 22.1
Expand Down
7 changes: 1 addition & 6 deletions integration-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
#
pycloudlib>=1!10.0.2,<1!11

# Avoid breaking change in `testpaths` treatment forced
# test/unittests/conftest.py to be loaded by our integration-tests tox env
# resulting in an unmet dependency issue:
# https://github.com/pytest-dev/pytest/issues/11104
pytest!=7.3.2
pytest-timeout

# Even when xdist is not actively used, we have fixtures that require it
pytest-xdist

packaging
passlib
coverage==7.2.7 # Last version supported in Python 3.7
coverage
Loading