summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2021-11-18 15:08:33 -0700
committerGitHub <noreply@github.com>2021-11-18 15:08:33 -0700
commit7ebf04e3a1920b451a0bb4b6302706a2b6632d6b (patch)
tree064594d23bd05dcebcf74e550b2d739a829e9304
parent3c6b594b90b0647f398f41962def45fd0953ce5b (diff)
downloadvyos-cloud-init-7ebf04e3a1920b451a0bb4b6302706a2b6632d6b.tar.gz
vyos-cloud-init-7ebf04e3a1920b451a0bb4b6302706a2b6632d6b.zip
lxd: add preference for LXD cloud-init.* config keys over user keys (#1108)
LXD now adds cloud-init scoped configuration keys network-config, user-data and vendor-data. The existing user.user-data, user.vendor-data, user.network-config and meta-data will be deprecated in newer LXD. cloud-init will prefer LXD config keys cloud-init.* keys above user.* keys even if both are present. Warnings will be emitted for ignored user.* keys if cloud-init.* overrides are present. Expectation is that the configuration user.network-config, user.meta-data, user.user-data and user.vendor-data* keys should not be present at the same time as the comparable cloud-init.* keys.
-rw-r--r--cloudinit/sources/DataSourceLXD.py75
-rw-r--r--cloudinit/sources/tests/test_lxd.py252
2 files changed, 277 insertions, 50 deletions
diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py
index 732b32ff..55ae52a2 100644
--- a/cloudinit/sources/DataSourceLXD.py
+++ b/cloudinit/sources/DataSourceLXD.py
@@ -10,6 +10,7 @@ Notes:
* TODO( Hotplug support using websockets API 1.0/events )
"""
+from json.decoder import JSONDecodeError
import os
import requests
@@ -41,14 +42,16 @@ LXD_SOCKET_API_VERSION = "1.0"
# Config key mappings to alias as top-level instance data keys
CONFIG_KEY_ALIASES = {
+ "cloud-init.user-data": "user-data",
+ "cloud-init.network-config": "network-config",
+ "cloud-init.vendor-data": "vendor-data",
"user.user-data": "user-data",
"user.network-config": "network-config",
- "user.network_mode": "network_mode",
"user.vendor-data": "vendor-data"
}
-def generate_fallback_network_config(network_mode: str = "") -> dict:
+def generate_fallback_network_config() -> dict:
"""Return network config V1 dict representing instance network config."""
network_v1 = {
"version": 1,
@@ -76,12 +79,6 @@ def generate_fallback_network_config(network_mode: str = "") -> dict:
network_v1["config"][0]["name"] = "enc9"
else:
network_v1["config"][0]["name"] = "enp5s0"
- if network_mode == "link-local":
- network_v1["config"][0]["subnets"][0]["control"] = "manual"
- elif network_mode not in ("", "dhcp"):
- LOG.warning(
- "Ignoring unexpected value user.network_mode: %s", network_mode
- )
return network_v1
@@ -244,10 +241,7 @@ class DataSourceLXD(sources.DataSource):
"network-config"
)
else:
- network_mode = self._crawled_metadata.get("network_mode", "")
- self._network_config = generate_fallback_network_config(
- network_mode
- )
+ self._network_config = generate_fallback_network_config()
return self._network_config
@@ -294,7 +288,7 @@ def read_metadata(
with requests.Session() as session:
session.mount(version_url, LXDSocketAdapter())
# Raw meta-data as text
- md_route = "{route}/meta-data".format(route=version_url)
+ md_route = "{route}meta-data".format(route=version_url)
response = session.get(md_route)
LOG.debug("[GET] [HTTP:%d] %s", response.status_code, md_route)
if not response.ok:
@@ -302,7 +296,7 @@ def read_metadata(
"Invalid HTTP response [{code}] from {route}: {resp}".format(
code=response.status_code,
route=md_route,
- resp=response.txt
+ resp=response.text
)
)
@@ -310,19 +304,42 @@ def read_metadata(
if metadata_only:
return md # Skip network-data, vendor-data, user-data
- config_url = version_url + "config"
- # Represent all advertized/available config routes under
- # the dict path {LXD_SOCKET_API_VERSION: {config: {...}}.
- LOG.debug("[GET] %s", config_url)
- config_routes = session.get(config_url).json()
md[LXD_SOCKET_API_VERSION] = {
"config": {},
"meta-data": md["meta-data"]
}
- for config_route in config_routes:
+
+ config_url = version_url + "config"
+ # Represent all advertized/available config routes under
+ # the dict path {LXD_SOCKET_API_VERSION: {config: {...}}.
+ response = session.get(config_url)
+ LOG.debug("[GET] [HTTP:%d] %s", response.status_code, config_url)
+ if not response.ok:
+ raise sources.InvalidMetaDataException(
+ "Invalid HTTP response [{code}] from {route}: {resp}".format(
+ code=response.status_code,
+ route=config_url,
+ resp=response.text
+ )
+ )
+ try:
+ config_routes = response.json()
+ except JSONDecodeError as exc:
+ raise sources.InvalidMetaDataException(
+ "Unable to determine cloud-init config from {route}."
+ " Expected JSON but found: {resp}".format(
+ route=config_url,
+ resp=response.text
+ )
+ ) from exc
+
+ # Sorting keys to ensure we always process in alphabetical order.
+ # cloud-init.* keys will sort before user.* keys which is preferred
+ # precedence.
+ for config_route in sorted(config_routes):
url = "http://lxd{route}".format(route=config_route)
- LOG.debug("[GET] %s", url)
response = session.get(url)
+ LOG.debug("[GET] [HTTP:%d] %s", response.status_code, url)
if response.ok:
cfg_key = config_route.rpartition("/")[-1]
# Leave raw data values/format unchanged to represent it in
@@ -331,9 +348,21 @@ def read_metadata(
md[LXD_SOCKET_API_VERSION]["config"][cfg_key] = response.text
# Promote common CONFIG_KEY_ALIASES to top-level keys.
if cfg_key in CONFIG_KEY_ALIASES:
- md[CONFIG_KEY_ALIASES[cfg_key]] = response.text
+ # Due to sort of config_routes, promote cloud-init.*
+ # aliases before user.*. This allows user.* keys to act as
+ # fallback config on old LXD, with new cloud-init images.
+ if CONFIG_KEY_ALIASES[cfg_key] not in md:
+ md[CONFIG_KEY_ALIASES[cfg_key]] = response.text
+ else:
+ LOG.warning(
+ "Ignoring LXD config %s in favor of %s value.",
+ cfg_key, cfg_key.replace("user", "cloud-init", 1)
+ )
else:
- LOG.debug("Skipping %s on invalid response", url)
+ LOG.debug(
+ "Skipping %s on [HTTP:%d]:%s",
+ url, response.status_code, response.text
+ )
return md
diff --git a/cloudinit/sources/tests/test_lxd.py b/cloudinit/sources/tests/test_lxd.py
index c2027616..fc2a41df 100644
--- a/cloudinit/sources/tests/test_lxd.py
+++ b/cloudinit/sources/tests/test_lxd.py
@@ -2,13 +2,17 @@
from collections import namedtuple
from copy import deepcopy
+import json
+import re
import stat
from unittest import mock
import yaml
import pytest
-from cloudinit.sources import DataSourceLXD as lxd, UNSET
+from cloudinit.sources import (
+ DataSourceLXD as lxd, InvalidMetaDataException, UNSET
+)
DS_PATH = "cloudinit.sources.DataSourceLXD."
@@ -24,8 +28,6 @@ NETWORK_V1 = {
}
]
}
-NETWORK_V1_MANUAL = deepcopy(NETWORK_V1)
-NETWORK_V1_MANUAL["config"][0]["subnets"][0]["control"] = "manual"
def _add_network_v1_device(devname) -> dict:
@@ -79,18 +81,15 @@ def lxd_ds(request, paths, lxd_metadata):
class TestGenerateFallbackNetworkConfig:
@pytest.mark.parametrize(
- "uname_machine,systemd_detect_virt,network_mode,expected", (
+ "uname_machine,systemd_detect_virt,expected", (
# None for systemd_detect_virt returns None from which
- ({}, None, "", NETWORK_V1),
- ({}, None, "dhcp", NETWORK_V1),
- # invalid network_mode logs warning
- ({}, None, "bogus", NETWORK_V1),
- ({}, None, "link-local", NETWORK_V1_MANUAL),
- ("anything", "lxc\n", "", NETWORK_V1),
+ ({}, None, NETWORK_V1),
+ ({}, None, NETWORK_V1),
+ ("anything", "lxc\n", NETWORK_V1),
# `uname -m` on kvm determines devname
- ("x86_64", "kvm\n", "", _add_network_v1_device("enp5s0")),
- ("ppc64le", "kvm\n", "", _add_network_v1_device("enp0s5")),
- ("s390x", "kvm\n", "", _add_network_v1_device("enc9"))
+ ("x86_64", "kvm\n", _add_network_v1_device("enp5s0")),
+ ("ppc64le", "kvm\n", _add_network_v1_device("enp0s5")),
+ ("s390x", "kvm\n", _add_network_v1_device("enc9"))
)
)
@mock.patch(DS_PATH + "util.system_info")
@@ -103,22 +102,14 @@ class TestGenerateFallbackNetworkConfig:
m_system_info,
uname_machine,
systemd_detect_virt,
- network_mode,
expected,
- caplog
):
- """Return network config v2 based on uname -m, systemd-detect-virt.
-
- LXC config network_mode of "link-local" will determine whether to set
- "activation-mode: manual", leaving the interface down.
- """
+ """Return network config v2 based on uname -m, systemd-detect-virt."""
if systemd_detect_virt is None:
m_which.return_value = None
m_system_info.return_value = {"uname": ["", "", "", "", uname_machine]}
m_subp.return_value = (systemd_detect_virt, "")
- assert expected == lxd.generate_fallback_network_config(
- network_mode=network_mode
- )
+ assert expected == lxd.generate_fallback_network_config()
if systemd_detect_virt is None:
assert 0 == m_subp.call_count
assert 0 == m_system_info.call_count
@@ -130,10 +121,6 @@ class TestGenerateFallbackNetworkConfig:
assert 0 == m_system_info.call_count
else:
assert 1 == m_system_info.call_count
- if network_mode not in ("dhcp", "", "link-local"):
- assert "Ignoring unexpected value user.network_mode: {}".format(
- network_mode
- ) in caplog.text
class TestDataSourceLXD:
@@ -182,4 +169,215 @@ class TestIsPlatformViable:
else:
assert 0 == m_lstat.call_count
+
+class TestReadMetadata:
+ @pytest.mark.parametrize(
+ "url_responses,expected,logs", (
+ ( # Assert non-JSON format from config route
+ {
+ "http://lxd/1.0/meta-data": "local-hostname: md\n",
+ "http://lxd/1.0/config": "[NOT_JSON",
+ },
+ InvalidMetaDataException(
+ "Unable to determine cloud-init config from"
+ " http://lxd/1.0/config. Expected JSON but found:"
+ " [NOT_JSON"),
+ ["[GET] [HTTP:200] http://lxd/1.0/meta-data",
+ "[GET] [HTTP:200] http://lxd/1.0/config"],
+ ),
+ ( # Assert success on just meta-data
+ {
+ "http://lxd/1.0/meta-data": "local-hostname: md\n",
+ "http://lxd/1.0/config": "[]",
+ },
+ {"1.0": {"config": {}, "meta-data": "local-hostname: md\n"},
+ "meta-data": "local-hostname: md\n"},
+ ["[GET] [HTTP:200] http://lxd/1.0/meta-data",
+ "[GET] [HTTP:200] http://lxd/1.0/config"],
+ ),
+ ( # Assert 404s for config routes log skipping
+ {
+ "http://lxd/1.0/meta-data": "local-hostname: md\n",
+ "http://lxd/1.0/config":
+ '["/1.0/config/user.custom1",'
+ ' "/1.0/config/user.meta-data",'
+ ' "/1.0/config/user.network-config",'
+ ' "/1.0/config/user.user-data",'
+ ' "/1.0/config/user.vendor-data"]',
+ "http://lxd/1.0/config/user.custom1": "custom1",
+ "http://lxd/1.0/config/user.meta-data": "", # 404
+ "http://lxd/1.0/config/user.network-config": "net-config",
+ "http://lxd/1.0/config/user.user-data": "", # 404
+ "http://lxd/1.0/config/user.vendor-data": "", # 404
+ },
+ {
+ "1.0": {
+ "config": {
+ "user.custom1": "custom1", # Not promoted
+ "user.network-config": "net-config",
+ },
+ "meta-data": "local-hostname: md\n",
+ },
+ "meta-data": "local-hostname: md\n",
+ "network-config": "net-config",
+ },
+ [
+ "Skipping http://lxd/1.0/config/user.vendor-data on"
+ " [HTTP:404]",
+ "Skipping http://lxd/1.0/config/user.meta-data on"
+ " [HTTP:404]",
+ "Skipping http://lxd/1.0/config/user.user-data on"
+ " [HTTP:404]",
+ "[GET] [HTTP:200] http://lxd/1.0/config",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.custom1",
+ "[GET] [HTTP:200]"
+ " http://lxd/1.0/config/user.network-config",
+ ],
+ ),
+ ( # Assert all CONFIG_KEY_ALIASES promoted to top-level keys
+ {
+ "http://lxd/1.0/meta-data": "local-hostname: md\n",
+ "http://lxd/1.0/config":
+ '["/1.0/config/user.custom1",'
+ ' "/1.0/config/user.meta-data",'
+ ' "/1.0/config/user.network-config",'
+ ' "/1.0/config/user.user-data",'
+ ' "/1.0/config/user.vendor-data"]',
+ "http://lxd/1.0/config/user.custom1": "custom1",
+ "http://lxd/1.0/config/user.meta-data": "meta-data",
+ "http://lxd/1.0/config/user.network-config": "net-config",
+ "http://lxd/1.0/config/user.user-data": "user-data",
+ "http://lxd/1.0/config/user.vendor-data": "vendor-data",
+ },
+ {
+ "1.0": {
+ "config": {
+ "user.custom1": "custom1", # Not promoted
+ "user.meta-data": "meta-data",
+ "user.network-config": "net-config",
+ "user.user-data": "user-data",
+ "user.vendor-data": "vendor-data",
+ },
+ "meta-data": "local-hostname: md\n",
+ },
+ "meta-data": "local-hostname: md\n",
+ "network-config": "net-config",
+ "user-data": "user-data",
+ "vendor-data": "vendor-data",
+ },
+ [
+ "[GET] [HTTP:200] http://lxd/1.0/meta-data",
+ "[GET] [HTTP:200] http://lxd/1.0/config",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.custom1",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.meta-data",
+ "[GET] [HTTP:200]"
+ " http://lxd/1.0/config/user.network-config",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.user-data",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.vendor-data",
+ ],
+ ),
+ ( # Assert cloud-init.* config key values prefered over user.*
+ {
+ "http://lxd/1.0/meta-data": "local-hostname: md\n",
+ "http://lxd/1.0/config":
+ '["/1.0/config/user.meta-data",'
+ ' "/1.0/config/user.network-config",'
+ ' "/1.0/config/user.user-data",'
+ ' "/1.0/config/user.vendor-data",'
+ ' "/1.0/config/cloud-init.network-config",'
+ ' "/1.0/config/cloud-init.user-data",'
+ ' "/1.0/config/cloud-init.vendor-data"]',
+ "http://lxd/1.0/config/user.meta-data": "user.meta-data",
+ "http://lxd/1.0/config/user.network-config":
+ "user.network-config",
+ "http://lxd/1.0/config/user.user-data": "user.user-data",
+ "http://lxd/1.0/config/user.vendor-data":
+ "user.vendor-data",
+ "http://lxd/1.0/config/cloud-init.meta-data":
+ "cloud-init.meta-data",
+ "http://lxd/1.0/config/cloud-init.network-config":
+ "cloud-init.network-config",
+ "http://lxd/1.0/config/cloud-init.user-data":
+ "cloud-init.user-data",
+ "http://lxd/1.0/config/cloud-init.vendor-data":
+ "cloud-init.vendor-data",
+ },
+ {
+ "1.0": {
+ "config": {
+ "user.meta-data": "user.meta-data",
+ "user.network-config": "user.network-config",
+ "user.user-data": "user.user-data",
+ "user.vendor-data": "user.vendor-data",
+ "cloud-init.network-config":
+ "cloud-init.network-config",
+ "cloud-init.user-data": "cloud-init.user-data",
+ "cloud-init.vendor-data":
+ "cloud-init.vendor-data",
+ },
+ "meta-data": "local-hostname: md\n",
+ },
+ "meta-data": "local-hostname: md\n",
+ "network-config": "cloud-init.network-config",
+ "user-data": "cloud-init.user-data",
+ "vendor-data": "cloud-init.vendor-data",
+ },
+ [
+ "[GET] [HTTP:200] http://lxd/1.0/meta-data",
+ "[GET] [HTTP:200] http://lxd/1.0/config",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.meta-data",
+ "[GET] [HTTP:200]"
+ " http://lxd/1.0/config/user.network-config",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.user-data",
+ "[GET] [HTTP:200] http://lxd/1.0/config/user.vendor-data",
+ "[GET] [HTTP:200]"
+ " http://lxd/1.0/config/cloud-init.network-config",
+ "[GET] [HTTP:200]"
+ " http://lxd/1.0/config/cloud-init.user-data",
+ "[GET] [HTTP:200]"
+ " http://lxd/1.0/config/cloud-init.vendor-data",
+ "Ignoring LXD config user.user-data in favor of"
+ " cloud-init.user-data value.",
+ "Ignoring LXD config user.network-config in favor of"
+ " cloud-init.network-config value.",
+ "Ignoring LXD config user.vendor-data in favor of"
+ " cloud-init.vendor-data value.",
+ ],
+ ),
+ )
+ )
+ @mock.patch.object(lxd.requests.Session, 'get')
+ def test_read_metadata_handles_unexpected_content_or_http_status(
+ self, session_get, url_responses, expected, logs, caplog
+ ):
+ """read_metadata handles valid and invalid content and status codes."""
+
+ def fake_get(url):
+ """Mock Response json, ok, status_code, text from url_responses."""
+ m_resp = mock.MagicMock()
+ content = url_responses.get(url, '')
+ m_resp.json.side_effect = lambda: json.loads(content)
+ if content:
+ mock_ok = mock.PropertyMock(return_value=True)
+ mock_status_code = mock.PropertyMock(return_value=200)
+ else:
+ mock_ok = mock.PropertyMock(return_value=False)
+ mock_status_code = mock.PropertyMock(return_value=404)
+ type(m_resp).ok = mock_ok
+ type(m_resp).status_code = mock_status_code
+ mock_text = mock.PropertyMock(return_value=content)
+ type(m_resp).text = mock_text
+ return m_resp
+
+ session_get.side_effect = fake_get
+
+ if isinstance(expected, Exception):
+ with pytest.raises(type(expected), match=re.escape(str(expected))):
+ lxd.read_metadata()
+ else:
+ assert expected == lxd.read_metadata()
+ caplogs = caplog.text
+ for log in logs:
+ assert log in caplogs
+
# vi: ts=4 expandtab