diff --git a/docs/releases.rst b/docs/releases.rst index b994400ab1..7861e8c8fb 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -62,6 +62,10 @@ guest with the `Input–output memory management unit` has been added into the :ref:`/spec/hardware` specification and implemented in the :ref:`/plugins/provision/beaker` provision plugin. +The :ref:`/plugins/provision/beaker` provision plugin now newly +supports providing a custom :ref:`/spec/plans/provision/kickstart` +configuration. + tmt-1.36.1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/spec/plans/provision/kickstart.fmf b/spec/plans/provision/kickstart.fmf index 819677a331..cde646e841 100644 --- a/spec/plans/provision/kickstart.fmf +++ b/spec/plans/provision/kickstart.fmf @@ -74,3 +74,9 @@ example: "no-autopart harness=restraint" kernel-options: "ksdevice=eth1" kernel-options-post: "quiet" + +link: + - implemented-by: /tmt/steps/provision/mrack.py + note: since 1.37 + - implemented-by: /tmt/steps/provision/artemis.py + note: since 1.22 diff --git a/tmt/schemas/provision/beaker.yaml b/tmt/schemas/provision/beaker.yaml index e9db0e6079..50acc91467 100644 --- a/tmt/schemas/provision/beaker.yaml +++ b/tmt/schemas/provision/beaker.yaml @@ -37,6 +37,9 @@ properties: hardware: $ref: "/schemas/provision/hardware#/definitions/hardware" + kickstart: + $ref: "/schemas/provision/kickstart#/definitions/kickstart" + role: $ref: "/schemas/common#/definitions/role" diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 049ae27d84..0d33ad6048 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -719,6 +719,9 @@ def show( if isinstance(value, (list, tuple)): printable_value = fmf.utils.listed(value) + elif isinstance(value, dict): + printable_value = tmt.utils.format_value(value) + elif isinstance(value, tmt.hardware.Hardware): printable_value = tmt.utils.dict_to_yaml(value.to_spec()) diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index 57f8998b1a..9f7e9078e9 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -17,6 +17,7 @@ UpdatableMessage, dict_to_yaml, field, + normalize_string_dict, retry_session, ) @@ -86,34 +87,6 @@ DEFAULT_RETRY_BACKOFF_FACTOR = 1 -def _normalize_user_data( - key_address: str, - raw_value: Any, - logger: tmt.log.Logger) -> dict[str, str]: - if isinstance(raw_value, dict): - return { - str(key).strip(): str(value).strip() for key, value in raw_value.items() - } - - if isinstance(raw_value, (list, tuple)): - user_data = {} - - for datum in raw_value: - try: - key, value = datum.split('=', 1) - - except ValueError as exc: - raise tmt.utils.NormalizationError( - key_address, datum, 'a KEY=VALUE string') from exc - - user_data[key.strip()] = value.strip() - - return user_data - - raise tmt.utils.NormalizationError( - key_address, value, 'a dictionary or a list of KEY=VALUE strings') - - def _normalize_log_type( key_address: str, raw_value: Any, @@ -181,14 +154,14 @@ class ArtemisGuestData(tmt.steps.provision.GuestSshData): metavar='KEY=VALUE', help='Optional data to attach to guest.', multiple=True, - normalize=_normalize_user_data) + normalize=normalize_string_dict) kickstart: dict[str, str] = field( default_factory=dict, option='--kickstart', metavar='KEY=VALUE', help='Optional Beaker kickstart to use when provisioning the guest.', multiple=True, - normalize=_normalize_user_data) + normalize=normalize_string_dict) log_type: list[str] = field( default_factory=list, diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index be94845153..d2be3b1d10 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -15,7 +15,14 @@ import tmt.steps import tmt.steps.provision import tmt.utils -from tmt.utils import Command, Path, ProvisionError, ShellScript, UpdatableMessage, field +from tmt.utils import ( + Command, + Path, + ProvisionError, + ShellScript, + UpdatableMessage, + field, + ) mrack: Any providers: Any @@ -691,8 +698,7 @@ def _translate_tmt_hw(self, hw: tmt.hardware.Hardware) -> dict[str, Any]: def create_host_requirement(self, host: CreateJobParameters) -> dict[str, Any]: """ Create single input for Beaker provisioner """ - - req: dict[str, Any] = super().create_host_requirement(dataclasses.asdict(host)) + req: dict[str, Any] = super().create_host_requirement(host.to_mrack()) if host.hardware and host.hardware.constraint: req.update(self._translate_tmt_hw(host.hardware)) @@ -703,6 +709,8 @@ def create_host_requirement(self, host: CreateJobParameters) -> dict[str, Any]: # Whiteboard must be added *after* request preparation, to overwrite the default one. req['whiteboard'] = host.whiteboard + logger.debug('mrack request', req, level=4) + logger.info('whiteboard', host.whiteboard, 'green') return req @@ -779,6 +787,13 @@ class BeakerGuestData(tmt.steps.provision.GuestSshData): {DEFAULT_API_SESSION_REFRESH} seconds by default. """, normalize=tmt.utils.normalize_int) + kickstart: dict[str, str] = field( + default_factory=dict, + option='--kickstart', + metavar='KEY=VALUE', + help='Optional Beaker kickstart to use when provisioning the guest.', + multiple=True, + normalize=tmt.utils.normalize_string_dict) beaker_job_owner: Optional[str] = field( default=None, @@ -821,10 +836,22 @@ class CreateJobParameters: os: str arch: str hardware: Optional[tmt.hardware.Hardware] + kickstart: dict[str, str] whiteboard: Optional[str] beaker_job_owner: Optional[str] group: str = 'linux' + def to_mrack(self) -> dict[str, Any]: + data = dataclasses.asdict(self) + + data['beaker'] = {} + + if self.kickstart: + data['beaker']['ks_meta'] = self.kickstart.get('metadata') + data['beaker']['ks_append'] = self.kickstart + + return data + class BeakerAPI: # req is a requirement passed to Beaker mrack provisioner @@ -923,6 +950,7 @@ class GuestBeaker(tmt.steps.provision.GuestSsh): arch: str image: str = "fedora-latest" hardware: Optional[tmt.hardware.Hardware] = None + kickstart: dict[str, str] beaker_job_owner: Optional[str] = None @@ -995,6 +1023,7 @@ def _create(self, tmt_name: str) -> None: data = CreateJobParameters( tmt_name=tmt_name, hardware=self.hardware, + kickstart=self.kickstart, arch=self.arch, os=self.image, name=f'{self.image}-{self.arch}', diff --git a/tmt/utils/__init__.py b/tmt/utils/__init__.py index cf35c3f27d..587564dcb0 100644 --- a/tmt/utils/__init__.py +++ b/tmt/utils/__init__.py @@ -5389,6 +5389,53 @@ def normalize_shell_script( raise NormalizationError(key_address, value, 'a string') +def normalize_string_dict( + key_address: str, + raw_value: Any, + logger: tmt.log.Logger) -> dict[str, str]: + """ + Normalize a key/value dictionary. + + The input value could be specified in two ways: + + * a dictionary, or + * a list of ``KEY=VALUE`` strings. + + For example, the following are acceptable inputs: + + .. code-block:: python + + {'foo': 'bar', 'qux': 'quux'} + + ['foo=bar', 'qux=quux'] + + :param value: input value from key source. + """ + + if isinstance(raw_value, dict): + return { + str(key).strip(): str(value).strip() for key, value in raw_value.items() + } + + if isinstance(raw_value, (list, tuple)): + normalized = {} + + for datum in cast(list[str], raw_value): + try: + key, value = datum.split('=', 1) + + except ValueError as exc: + raise NormalizationError( + key_address, datum, 'a KEY=VALUE string') from exc + + normalized[key.strip()] = value.strip() + + return normalized + + raise tmt.utils.NormalizationError( + key_address, value, 'a dictionary or a list of KEY=VALUE strings') + + class NormalizeKeysMixin(_CommonBase): """ Mixin adding support for loading fmf keys into object attributes.