From a37b5e387082864b534f5a2e3a1db38c3e9e7540 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Wed, 15 Feb 2023 12:11:08 -0300 Subject: [PATCH 1/3] Check for the needed programs in the live system Currently only the realm program is executed by the ad controller. Thus we only check if its accessible. Clients should call this endpoint prior to showing the AD screen. --- subiquity/common/apidef.py | 6 ++++++ subiquity/models/ad.py | 10 ++++++++++ subiquity/server/controllers/ad.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index 9cda11da..409e8ff1 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -418,6 +418,12 @@ class API: # POST expects input validated by the check methods below: def POST(data: Payload[ADConnectionInfo]) -> None: ... + class has_support: + """ Whether the live system supports Active Directory or not. + Network status is not considered. + Clients should call this before showing the AD page. """ + def GET() -> bool: ... + class check_domain_name: def GET(domain_name: Payload[str]) \ -> List[AdDomainNameValidation]: ... diff --git a/subiquity/models/ad.py b/subiquity/models/ad.py index 559ba4c3..9cf8c24b 100644 --- a/subiquity/models/ad.py +++ b/subiquity/models/ad.py @@ -27,6 +27,16 @@ class ADModel: self.do_join = False self.conn_info: Optional[ADConnectionInfo] = None + def set_domain(self, domain: str): + if not domain: + return + + if self.conn_info: + self.conn_info.domain_name = domain + + else: + self.conn_info = ADConnectionInfo(domain_name=domain) + async def target_packages(self): # NOTE Those packages must be present in the target system to allow # joining to a domain. diff --git a/subiquity/server/controllers/ad.py b/subiquity/server/controllers/ad.py index 94e300d8..44a4be3d 100644 --- a/subiquity/server/controllers/ad.py +++ b/subiquity/server/controllers/ad.py @@ -14,6 +14,7 @@ # along with this program. If not, see . import logging +import os import re from typing import List, Optional, Set from subiquitycore.utils import arun_command @@ -43,6 +44,9 @@ class DcPingStrategy: cmd = "/usr/sbin/realm" arg = "discover" + def has_support(self) -> bool: + return os.access(self.cmd, os.X_OK) + async def ping(self, address: str) -> AdDomainNameValidation: cp = await arun_command([self.cmd, self.arg, address], env={}) if cp.returncode: @@ -63,6 +67,9 @@ class StubDcPingStrategy(DcPingStrategy): return AdDomainNameValidation.OK + def has_support(self) -> bool: + return True + class ADController(SubiquityController): """ Implements the server part of the Active Directory feature. """ @@ -104,6 +111,13 @@ class ADController(SubiquityController): async def check_password_GET(self, password: str) -> AdPasswordValidation: return AdValidators.password(password) + async def has_support_GET(self) -> bool: + """ Returns True if the executables required + to configure AD are present in the live system.""" + return self.ping_strgy.has_support() + + # async def discover_domain_controller(self) -> str: + # Helper out-of-class functions grouped. class AdValidators: From 9b50c8db074a4accde6494a7a77fb0b2e67a0a02 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 16 Feb 2023 10:21:10 -0300 Subject: [PATCH 2/3] Auto discover the domain at start `realm discover` without a domain name attempts to find one from DHCP. This attempts to run it from the controller start method. Only creates the discover task if there is support Avoids exception on controller start. --- subiquity/server/controllers/ad.py | 27 +++++++++++++++++++++++++-- subiquity/tests/api/test_api.py | 4 ++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/subiquity/server/controllers/ad.py b/subiquity/server/controllers/ad.py index 44a4be3d..8c103d49 100644 --- a/subiquity/server/controllers/ad.py +++ b/subiquity/server/controllers/ad.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import asyncio import logging import os import re @@ -54,6 +55,18 @@ class DcPingStrategy: return AdDomainNameValidation.OK + async def discover(self) -> str: + """ Attempts to discover a domain through the network. + Returns the domain or an empty string on error. """ + cp = await arun_command([self.cmd, self.arg], env={}) + discovered = "" + if cp.returncode == 0: + # A typical output looks like: + # 'creative.com\n type: kerberos\n realm-name: CREATIVE.COM\n...' + discovered = cp.stdout.split('\n')[0].strip() + + return discovered + class StubDcPingStrategy(DcPingStrategy): """ For testing purpose. This class doesn't talk to the network. @@ -67,6 +80,9 @@ class StubDcPingStrategy(DcPingStrategy): return AdDomainNameValidation.OK + async def discover(self) -> str: + return "ubuntu.com" + def has_support(self) -> bool: return True @@ -84,6 +100,15 @@ class ADController(SubiquityController): else: self.ping_strgy = DcPingStrategy() + def start(self): + if self.ping_strgy.has_support(): + asyncio.create_task(self._try_discover_domain()) + + async def _try_discover_domain(self): + discovered_domain = await self.ping_strgy.discover() + if discovered_domain: + self.model.set_domain(discovered_domain) + async def GET(self) -> Optional[ADConnectionInfo]: """Returns the currently configured AD settings""" return self.model.conn_info @@ -116,8 +141,6 @@ class ADController(SubiquityController): to configure AD are present in the live system.""" return self.ping_strgy.has_support() - # async def discover_domain_controller(self) -> str: - # Helper out-of-class functions grouped. class AdValidators: diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py index 761164ed..31c2c10b 100644 --- a/subiquity/tests/api/test_api.py +++ b/subiquity/tests/api/test_api.py @@ -1659,8 +1659,8 @@ class TestActiveDirectory(TestAPI): async with start_server('examples/simple.json') as instance: endpoint = '/active_directory' ad_dict = await instance.get(endpoint) - # Starts empty - self.assertIsNone(ad_dict) + # Starts with the detected domain. + self.assertEqual('ubuntu.com', ad_dict['domain_name']) # Post works by "returning None" ad_dict = { From b60305edb276169e6b86d9ae41073136fed196bc Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Fri, 17 Feb 2023 23:09:38 -0300 Subject: [PATCH 3/3] Replaces raw asyncio.create_task `run_bg_task` is the default recommendation instead. --- subiquity/server/controllers/ad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subiquity/server/controllers/ad.py b/subiquity/server/controllers/ad.py index 8c103d49..a34cbdc0 100644 --- a/subiquity/server/controllers/ad.py +++ b/subiquity/server/controllers/ad.py @@ -13,12 +13,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import asyncio import logging import os import re from typing import List, Optional, Set from subiquitycore.utils import arun_command +from subiquitycore.async_helpers import run_bg_task from subiquity.common.apidef import API from subiquity.common.types import ( @@ -102,7 +102,7 @@ class ADController(SubiquityController): def start(self): if self.ping_strgy.has_support(): - asyncio.create_task(self._try_discover_domain()) + run_bg_task(self._try_discover_domain()) async def _try_discover_domain(self): discovered_domain = await self.ping_strgy.discover()