Skip to content

Commit

Permalink
Add support for kickstart to mrack plugin (#3064)
Browse files Browse the repository at this point in the history
Co-authored-by: Miloš Prchlík <mprchlik@redhat.com>
Co-authored-by: Petr Šplíchal <psplicha@redhat.com>
  • Loading branch information
3 people authored Oct 1, 2024
1 parent ec24069 commit c001920
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 33 deletions.
4 changes: 4 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
6 changes: 6 additions & 0 deletions spec/plans/provision/kickstart.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions tmt/schemas/provision/beaker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
33 changes: 3 additions & 30 deletions tmt/steps/provision/artemis.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
UpdatableMessage,
dict_to_yaml,
field,
normalize_string_dict,
retry_session,
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 32 additions & 3 deletions tmt/steps/provision/mrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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}',
Expand Down
47 changes: 47 additions & 0 deletions tmt/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit c001920

Please sign in to comment.