Skip to content

Commit

Permalink
add: computer title generation for zero-touch deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
st3v3nmw committed Jan 31, 2024
1 parent 8df12c7 commit 1079aa1
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 1 deletion.
57 changes: 57 additions & 0 deletions landscape/client/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@

from landscape import VERSION
from landscape.client import DEFAULT_CONFIG
from landscape.client import snap_http
from landscape.client.snap_utils import get_assertions
from landscape.client.snap_utils import is_snap
from landscape.client.upgraders import UPGRADE_MANAGERS
from landscape.lib import logging
from landscape.lib.config import BaseConfiguration as _BaseConfiguration
from landscape.lib.format import expandvars
from landscape.lib.network import get_fqdn
from landscape.lib.persist import Persist


Expand Down Expand Up @@ -77,6 +82,12 @@ class Configuration(BaseConfiguration):

DEFAULT_URL = "https://landscape.canonical.com/message-system"

def __init__(self):
super().__init__()

if is_snap():
self.auto_configure_snap()

def make_parser(self):
"""Parser factory for supported options.
Expand Down Expand Up @@ -189,6 +200,24 @@ def juju_filename(self):
backwards-compatibility."""
return os.path.join(self.data_path, "juju-info.json")

def auto_configure_snap(self):
"""Automatically configure the client snap."""
client_conf = snap_http.get_conf("landscape-client").result
auto_enroll_conf = client_conf.get("auto-register", {})

enabled = auto_enroll_conf.get("enabled", False)
configured = auto_enroll_conf.get("configured", False)
if not enabled or configured:
return

title = generate_computer_title(auto_enroll_conf)
if title:
self.computer_title = title
auto_enroll_conf["configured"] = True
client_conf.update("auto-register", auto_enroll_conf)
snap_http.set_conf("landscape-client", client_conf)
self.write()


def get_versioned_persist(service):
"""Get a L{Persist} database with upgrade rules applied.
Expand All @@ -204,3 +233,31 @@ def get_versioned_persist(service):
upgrade_manager.initialize(persist)
persist.save(service.persist_filename)
return persist


def generate_computer_title(auto_enroll_conf):
assertions = get_assertions("serial")
wait_for_serial = auto_enroll_conf.get("wait-for-serial-as", True)
if not assertions and wait_for_serial:
return
serial = assertions[0]["serial"]
model = assertions[0]["model"]
brand = assertions[0]["brand-id"]

hostname = get_fqdn()
wait_for_hostname = auto_enroll_conf.get("wait-for-hostname", False)
if "localhost" in hostname and wait_for_hostname:
return

computer_title_pattern = auto_enroll_conf.get(
"computer-title-pattern",
"${hostname}",
)

return expandvars(
computer_title_pattern,
serial=serial,
hostname=hostname,
model=model,
brand=brand,
)
45 changes: 45 additions & 0 deletions landscape/client/snap_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os

import yaml

from landscape.client import snap_http
from landscape.client.snap_http import SnapdHttpException


def is_snap() -> bool:
"""Is the landscape-client a snap?"""
return os.environ.get("SNAP_REVISION") is not None


def get_assertions(assertion_type: str):
"""Get and parse assertions."""
try:
response = snap_http.get_assertions(assertion_type)
except SnapdHttpException:
return

# the snapd API returns multiple assertions as a stream of
# bytes separated by double newlines, something like this:
# <assertion1-headers>: <value>
# <assertion1-headers>: <value>
#
# signature
#
# <assertion2-headers>: <value>
# <assertion2-headers>: <value>
#
# signature

# extract the assertion headers + their signatures as separate assertions
assertions = []
result = response.result.decode()
if result:
sections = result.split("\n\n")
rest = sections
while rest:
headers, signature, *rest = rest
assertion = yaml.safe_load(headers)
assertion["signature"] = signature
assertions.append(assertion)

return assertions
1 change: 1 addition & 0 deletions landscape/client/watchdog.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from landscape.lib.logging import rotate_logs
from landscape.lib.twisted_util import gather_results


GRACEFUL_WAIT_PERIOD = 10
MAXIMUM_CONSECUTIVE_RESTARTS = 5
RESTART_BURST_DELAY = 30 # seconds
Expand Down
33 changes: 33 additions & 0 deletions landscape/lib/format.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspect
import re


def format_object(object):
Expand Down Expand Up @@ -29,3 +30,35 @@ def format_percent(percent):
if not percent:
percent = 0.0
return f"{float(percent):.02f}%"


def expandvars(pattern: str, **kwargs) -> str:
"""Expand the pattern by replacing the params with values in `kwargs`.
This implements a small subset of shell parameter expansion and the
patterns can only be in the following forms:
- ${parameter}
- ${parameter:offset} - start at `offset` to the end
- ${parameter:offset:length} - start at `offset` to `offset + length`
For simplicity, `offset` and `length` MUST be positive values.
"""
regex = re.compile(
r"\$\{([a-zA-Z][a-zA-Z0-9]*)(?::([0-9]+))?(?::([0-9]+))?\}",
re.MULTILINE,
)

def _replace(match):
param = match.group(1)
result = kwargs[param]

offset, length = match.group(2), match.group(3)
if offset:
start = int(offset)
end = None
if length:
end = start + int(length)
return result[start:end]

return result

return re.sub(regex, _replace, pattern)
89 changes: 89 additions & 0 deletions landscape/lib/tests/test_format.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest

from landscape.lib.format import expandvars
from landscape.lib.format import format_delta
from landscape.lib.format import format_object
from landscape.lib.format import format_percent
Expand Down Expand Up @@ -63,3 +64,91 @@ def test_format_int(self):

def test_format_none(self):
self.assertEqual(format_percent(None), "0.00%")


class ExpandVarsTest(unittest.TestCase):
def test_expand_without_offset_and_length(self):
self.assertEqual(
expandvars("${serial}", serial="f315cab5"),
"f315cab5",
)
self.assertEqual(
expandvars("before:${serial}", serial="f315cab5"),
"before:f315cab5",
)
self.assertEqual(
expandvars("${serial}:after", serial="f315cab5"),
"f315cab5:after",
)
self.assertEqual(
expandvars("be$fore:${serial}:after", serial="f315cab5"),
"be$fore:f315cab5:after",
)

def test_expand_with_offset(self):
self.assertEqual(
expandvars("${serial:7}", serial="01234567890abcdefgh"),
"7890abcdefgh",
)
self.assertEqual(
expandvars("before:${serial:7}", serial="01234567890abcdefgh"),
"before:7890abcdefgh",
)
self.assertEqual(
expandvars("${serial:7}:after", serial="01234567890abcdefgh"),
"7890abcdefgh:after",
)
self.assertEqual(
expandvars(
"be$fore:${serial:7}:after",
serial="01234567890abcdefgh",
),
"be$fore:7890abcdefgh:after",
)

def test_expand_with_offset_and_length(self):
self.assertEqual(
expandvars("${serial:7:0}", serial="01234567890abcdefgh"),
"",
)
self.assertEqual(
expandvars("before:${serial:7:2}", serial="01234567890abcdefgh"),
"before:78",
)
self.assertEqual(
expandvars("${serial:7:2}:after", serial="01234567890abcdefgh"),
"78:after",
)
self.assertEqual(
expandvars(
"be$fore:${serial:7:2}:after",
serial="01234567890abcdefgh",
),
"be$fore:78:after",
)

def test_expand_multiple(self):
self.assertEqual(
expandvars(
"${model:8:7}-${serial:0:8}",
model="generic-classic",
serial="f315cab5-ba74-4d3c-be85-713406455773",
),
"classic-f315cab5",
)

def test_expand_offset_longer_than_substitute(self):
self.assertEqual(
expandvars("${serial:50}", serial="01234567890abcdefgh"),
"",
)

def test_expand_length_longer_than_substitute(self):
self.assertEqual(
expandvars("${serial:1:100}", serial="01234567890abcdefgh"),
"1234567890abcdefgh",
)

def test_expand_with_unknown_param(self):
with self.assertRaises(KeyError):
expandvars("${serial}-${unknown}", serial="01234567890abcdefgh")
1 change: 1 addition & 0 deletions setup_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"landscape.client.patch",
"landscape.client.reactor",
"landscape.client.service",
"landscape.client.snap_utils",
"landscape.client.sysvconfig",
"landscape.client.watchdog",
]
Expand Down
2 changes: 1 addition & 1 deletion snap-http

0 comments on commit 1079aa1

Please sign in to comment.