Skip to content

Commit

Permalink
feat(wsl): Special handling Landscape client config tags (canonical#5460
Browse files Browse the repository at this point in the history
)

UP4W business logic is so that its data overrides user at a key (module) 
level.
That means the entire Landscape config is overriden if both agent data
and user data contains config for that module.
Yet, for better usability, computer tags must be assignable per instance.
That's not possible with agent.yaml, because it's meant to be global.
Its config data affects all Ubuntu WSL instances.

Thus this aims to make a special case for landscape.client.tags,
if present in user provided data (either Landscape or local user -
  whatever is picked up before merging with agent.yaml)
its value overwrites any tags set by agent.yaml.

Only landscape.client.tags are treated specially.
The pre-existing merge rules still apply for any other value present in
both agent.yaml and user provided data.

Fixes UDENG-2464
  • Loading branch information
CarlosNihelton authored and holmanb committed Aug 2, 2024
1 parent 1c6e81d commit 94d077f
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 11 deletions.
11 changes: 11 additions & 0 deletions cloudinit/sources/DataSourceWSL.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,13 @@ def _get_data(self) -> bool:
# provides them instead.
# That's the reason for not using util.mergemanydict().
merged: dict = {}
user_tags: str = ""
overridden_keys: typing.List[str] = []
if user_data:
merged = user_data
user_tags = (
merged.get("landscape", {}).get("client", {}).get("tags", "")
)
if agent_data:
if user_data:
LOG.debug("Merging both user_data and agent.yaml configs.")
Expand All @@ -345,6 +349,13 @@ def _get_data(self) -> bool:
", ".join(overridden_keys)
)
)
if user_tags and merged.get("landscape", {}).get("client"):
LOG.debug(
"Landscape client conf updated with user-data"
" landscape.client.tags: %s",
user_tags,
)
merged["landscape"]["client"]["tags"] = user_tags

self.userdata_raw = "#cloud-config\n%s" % yaml.dump(merged)
return True
Expand Down
5 changes: 4 additions & 1 deletion doc/rtd/reference/datasources/wsl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ following paths:
the Ubuntu Pro for WSL agent. If this file is present, its modules will be
merged with (1), overriding any conflicting modules. If (1) is not provided,
then this file will be merged with any valid user-provided configuration
instead.
instead. Exception is made for Landscape client config computer tags. If
user provided data contains a value for ``landscape.client.tags`` it will be
used instead of the one provided by the ``agent.yaml``, which is treated as
a default.

Then, if a file from (1) is not found, a user-provided configuration will be
looked for instead in the following order:
Expand Down
273 changes: 263 additions & 10 deletions tests/unittests/sources/test_wsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ def test_get_data_sh(self, m_lsb_release, tmpdir, paths):

@mock.patch("cloudinit.util.get_linux_distro")
def test_data_precedence(self, m_get_linux_dist, tmpdir, paths):
"""Validates the precedence of user-data files."""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO

# Set up basic user data:
Expand Down Expand Up @@ -400,19 +402,32 @@ def test_data_precedence(self, m_get_linux_dist, tmpdir, paths):

assert "" == shell_script

# Additionally set up some UP4W agent data:
@mock.patch("cloudinit.util.get_linux_distro")
def test_interaction_with_pro(self, m_get_linux_dist, tmpdir, paths):
"""Validates the interaction of user-data and Pro For WSL agent data"""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO

user_file = tmpdir.join(".cloud-init", "ubuntu-24.04.user-data")
user_file.dirpath().mkdir()
user_file.write("#cloud-config\nwrite_files:\n- path: /etc/wsl.conf")

# Now the winner should be the merge of the agent and Landscape data.
# The winner should be the merge of the agent and user provided data.
ubuntu_pro_tmp = tmpdir.join(".ubuntupro", ".cloud-init")
os.makedirs(ubuntu_pro_tmp, exist_ok=True)

agent_file = ubuntu_pro_tmp.join("agent.yaml")
agent_file.write(
"""#cloud-config
landscape:
host:
url: landscape.canonical.com:6554
client:
account_name: agenttest
ubuntu_advantage:
account_name: agenttest
url: https://landscape.canonical.com/message-system
ping_url: https://landscape.canonical.com/ping
tags: wsl
ubuntu_pro:
token: testtoken"""
)

Expand All @@ -436,17 +451,93 @@ def test_data_precedence(self, m_get_linux_dist, tmpdir, paths):
)
assert "wsl.conf" in userdata
assert "packages" not in userdata
assert "ubuntu_advantage" in userdata
assert "ubuntu_pro" in userdata
assert "landscape" in userdata
assert "agenttest" in userdata

# Additionally set up some Landscape provided user data
@mock.patch("cloudinit.util.get_linux_distro")
def test_landscape_vs_local_user(self, m_get_linux_dist, tmpdir, paths):
"""Validates the precendence of Landscape-provided over local data"""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO

user_file = tmpdir.join(".cloud-init", "ubuntu-24.04.user-data")
user_file.dirpath().mkdir()
user_file.write(
"""#cloud-config
ubuntu_pro:
token: usertoken
package_update: true"""
)

ubuntu_pro_tmp = tmpdir.join(".ubuntupro", ".cloud-init")
os.makedirs(ubuntu_pro_tmp, exist_ok=True)
landscape_file = ubuntu_pro_tmp.join("%s.user-data" % INSTANCE_NAME)
landscape_file.write(
"""#cloud-config
landscape:
client:
account_name: landscapetest
tags: tag_aiml,tag_dev
locale: en_GB.UTF-8"""
)

# Run the datasource
ds = wsl.DataSourceWSL(
sys_cfg=SAMPLE_CFG,
distro=_get_distro("ubuntu"),
paths=paths,
)

assert ds.get_data() is True
ud = ds.get_userdata()
assert ud is not None
userdata = cast(
str,
join_payloads_from_content_type(
cast(MIMEMultipart, ud), "text/cloud-config"
),
)

assert (
"locale" in userdata
and "landscapetest" in userdata
and "ubuntu_pro" not in userdata
and "package_update" not in userdata
), "Landscape data should have overriden user provided data"

@mock.patch("cloudinit.util.get_linux_distro")
def test_landscape_provided_data(self, m_get_linux_dist, tmpdir, paths):
"""Validates the interaction of Pro For WSL agent and Landscape data"""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO

ubuntu_pro_tmp = tmpdir.join(".ubuntupro", ".cloud-init")
os.makedirs(ubuntu_pro_tmp, exist_ok=True)

agent_file = ubuntu_pro_tmp.join("agent.yaml")
agent_file.write(
"""#cloud-config
landscape:
host:
url: hosted.com:6554
client:
account_name: agenttest
url: https://hosted.com/message-system
ping_url: https://hosted.com/ping
ssl_public_key: C:\\Users\\User\\server.pem
tags: wsl
ubuntu_pro:
token: testtoken"""
)

landscape_file = ubuntu_pro_tmp.join("%s.user-data" % INSTANCE_NAME)
landscape_file.write(
"""#cloud-config
landscape:
client:
account_name: landscapetest
tags: tag_aiml,tag_dev
package_update: true"""
)

Expand All @@ -469,14 +560,176 @@ def test_data_precedence(self, m_get_linux_dist, tmpdir, paths):
),
)

assert "wsl.conf" not in userdata
assert "packages" not in userdata
assert "ubuntu_advantage" in userdata
assert "ubuntu_pro" in userdata, "Agent data should be present"
assert "package_update" in userdata, (
"package_update entry should not be overriden by agent data"
" nor ignored"
)
assert "landscape" in userdata
assert (
"landscapetest" not in userdata and "agenttest" in userdata
), "Landscape account name should have been overriden by agent data"
# Make sure we have tags from Landscape data, not agent's
assert (
"tag_aiml" in userdata and "tag_dev" in userdata
), "User-data should override agent data's Landscape computer tags"
assert "wsl" not in userdata

@mock.patch("cloudinit.util.get_linux_distro")
def test_with_landscape_no_tags(self, m_get_linux_dist, tmpdir, paths):
"""Validates the Pro For WSL default Landscape tags are applied"""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO

ubuntu_pro_tmp = tmpdir.join(".ubuntupro", ".cloud-init")
os.makedirs(ubuntu_pro_tmp, exist_ok=True)

agent_file = ubuntu_pro_tmp.join("agent.yaml")
agent_file.write(
"""#cloud-config
landscape:
host:
url: landscape.canonical.com:6554
client:
account_name: agenttest
url: https://landscape.canonical.com/message-system
ping_url: https://landscape.canonical.com/ping
tags: wsl
ubuntu_pro:
token: testtoken"""
)
# Set up some Landscape provided user data without tags
landscape_file = ubuntu_pro_tmp.join("%s.user-data" % INSTANCE_NAME)
landscape_file.write(
"""#cloud-config
landscape:
client:
account_name: landscapetest
package_update: true"""
)

# Run the datasource
ds = wsl.DataSourceWSL(
sys_cfg=SAMPLE_CFG,
distro=_get_distro("ubuntu"),
paths=paths,
)

assert ds.get_data() is True
ud = ds.get_userdata()

assert ud is not None
userdata = cast(
str,
join_payloads_from_content_type(
cast(MIMEMultipart, ud), "text/cloud-config"
),
)

assert (
"tags: wsl" in userdata
), "Landscape computer tags should match UP4W agent's data defaults"

@mock.patch("cloudinit.util.get_linux_distro")
def test_with_no_tags_at_all(self, m_get_linux_dist, tmpdir, paths):
"""Asserts the DS still works if there are no Landscape tags at all"""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO

user_file = tmpdir.join(".cloud-init", "ubuntu-24.04.user-data")
user_file.dirpath().mkdir()
user_file.write("#cloud-config\nwrite_files:\n- path: /etc/wsl.conf")

ubuntu_pro_tmp = tmpdir.join(".ubuntupro", ".cloud-init")
os.makedirs(ubuntu_pro_tmp, exist_ok=True)

agent_file = ubuntu_pro_tmp.join("agent.yaml")
# Make sure we don't crash if there are no tags anywhere.
agent_file.write(
"""#cloud-config
ubuntu_pro:
token: up4w_token"""
)
# Set up some Landscape provided user data without tags
landscape_file = ubuntu_pro_tmp.join("%s.user-data" % INSTANCE_NAME)
landscape_file.write(
"""#cloud-config
landscape:
client:
account_name: landscapetest
package_update: true"""
)

# Run the datasource
ds = wsl.DataSourceWSL(
sys_cfg=SAMPLE_CFG,
distro=_get_distro("ubuntu"),
paths=paths,
)

assert ds.get_data() is True
ud = ds.get_userdata()

assert ud is not None
userdata = cast(
str,
join_payloads_from_content_type(
cast(MIMEMultipart, ud), "text/cloud-config"
),
)
assert "landscapetest" in userdata
assert "up4w_token" in userdata
assert "tags" not in userdata

@mock.patch("cloudinit.util.get_linux_distro")
def test_with_no_client_subkey(self, m_get_linux_dist, tmpdir, paths):
"""Validates the DS works without the landscape.client subkey"""

m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO
ubuntu_pro_tmp = tmpdir.join(".ubuntupro", ".cloud-init")
os.makedirs(ubuntu_pro_tmp, exist_ok=True)

agent_file = ubuntu_pro_tmp.join("agent.yaml")
# Make sure we don't crash if there is no client subkey.
# (That would be a bug in the agent as there is no other config
# value for landscape outside of landscape.client, so I'm making up
# some non-sense keys just to make sure we won't crash)
agent_file.write(
"""#cloud-config
landscape:
server:
port: 6554
ubuntu_pro:
token: up4w_token"""
)

landscape_file = ubuntu_pro_tmp.join("%s.user-data" % INSTANCE_NAME)
landscape_file.write(
"""#cloud-config
landscape:
client:
account_name: landscapetest
package_update: true"""
)
# Run the datasource
ds = wsl.DataSourceWSL(
sys_cfg=SAMPLE_CFG,
distro=_get_distro("ubuntu"),
paths=paths,
)

assert ds.get_data() is True
ud = ds.get_userdata()

assert ud is not None
userdata = cast(
str,
join_payloads_from_content_type(
cast(MIMEMultipart, ud), "text/cloud-config"
),
)
assert "landscapetest" not in userdata
assert (
"port: 6554" in userdata
), "agent data should override the entire landscape config."

assert "up4w_token" in userdata

0 comments on commit 94d077f

Please sign in to comment.