Skip to content

Commit

Permalink
Add initial support for a Beaker provider.
Browse files Browse the repository at this point in the history
This change adds a very basic Beaker provider and its bind.
For now, this relies on the beaker-client cli and config through that.
Currently the only supported checkout/execute method is via job xml.
  • Loading branch information
JacobCallahan committed May 15, 2023
1 parent e34979b commit e7ac7be
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Copy the example settings file to `broker_settings.yaml` and edit it.

(optional) If you are using the Container provider, install the extra dependency based on your container runtime of choice with either `pip install broker[podman]` or `pip install broker[docker]`.

(optional) If you are using the Beaker provider, install the extra dependency with `dnf install krb5-devel` and then `pip install broker[beaker]`.

To run Broker outside of its base directory, specify the directory with the `BROKER_DIRECTORY` environment variable.

Configure the `broker_settings.yaml` file to set configuration values for broker's interaction with its providers.
Expand Down
117 changes: 117 additions & 0 deletions broker/binds/beaker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import json
import subprocess
from logzero import logger
from pathlib import Path


class BeakerBind:

def __init__(self, hub_url, auth="krbv", **kwargs):
self.hub_url = hub_url
self._base_args = ["--insecure", f"--hub={self.hub_url}"]
if auth == "basic":
# If we're not using system kerberos auth, add in explicit basic auth
self.username = kwargs.pop("username", None)
self.password = kwargs.pop("password", None)
self._base_args.extend([
f"--username {self.username}",
f"--password {self.password}",
])
self.__dict__.update(kwargs)

def _exec_command(self, *cmd_args, **cmd_kwargs):
exec_cmd = ["bkr"]
exec_cmd.extend(cmd_args)
exec_cmd.extend(self._base_args)
exec_cmd.extend([
f"--{k.replace('_', '-')}={v}"
for k, v in cmd_kwargs.items()
])
logger.debug(f"Executing beaker command: {exec_cmd}")
return subprocess.check_output(exec_cmd).decode()

def job_submit(self, job_xml, wait=True):
if not Path(job_xml).exists():
raise FileNotFoundError(f"Job XML file {job_xml} not found")
result = self._exec_command("job-submit", job_xml, wait=wait)
if not wait:
# get the job id from the output
# format is "Submitted: ['J:7849837'] where the number is the job id
for line in result.splitlines():
if line.startswith("Submitted:"):
return line.split("'")[1].replace("J:", "")

def job_watch(self, job_id):
job_id = f"J:{job_id}"
return self._exec_command("job-watch", job_id)

def job_results(self, job_id, format="beaker-results-xml", pretty=False):
job_id = f"J:{job_id}"
return self._exec_command("job-results", job_id, format=format, prettyxml=pretty)

def job_clone(self, job_id, wait=True):
job_id = f"J:{job_id}"
return self._exec_command("job-clone", job_id, wait=wait)

def job_cancel(self, job_id):
job_id = f"J:{job_id}"
return self._exec_command("job-cancel", job_id)

def job_delete(self, job_id):
job_id = f"J:{job_id}"
return self._exec_command("job-delete", job_id)

def system_release(self, system_id):
return self._exec_command("system-release", system_id)

def system_list(self, **kwargs):
"""Due to the number of arguments, we will not validate before submitting
Accepted arguments are:
available available to be used by this user
free available to this user and not currently being used
removed which have been removed
mine owned by this user
type=TYPE of TYPE
status=STATUS with STATUS
pool=POOL in POOL
arch=ARCH with ARCH
dev-vendor-id=VENDOR-ID with a device that has VENDOR-ID
dev-device-id=DEVICE-ID with a device that has DEVICE-ID
dev-sub-vendor-id=SUBVENDOR-ID with a device that has SUBVENDOR-ID
dev-sub-device-id=SUBDEVICE-ID with a device that has SUBDEVICE-ID
dev-driver=DRIVER with a device that has DRIVER
dev-description=DESCRIPTION with a device that has DESCRIPTION
xml-filter=XML matching the given XML filter
host-filter=NAME matching pre-defined host filter
"""
# convert the flags passed in kwargs to arguments
args = []
for key in {"available", "free", "removed", "mine"}:
if kwargs.pop(key, False):
args.append(f"--{key}")
return self._exec_command("system-list", *args, **kwargs)

def user_systems(self): # to be used for inventory sync
return self.system_list(mine=True).splitlines()

def system_details(self, system_id, format="json"):
return self._exec_command("system-details", system_id, format=format)

def system_details_curated(self, system_id):
full_details = json.loads(self.system_details(system_id))
return {
"fqdn": full_details["fqdn"],
"mac_address": full_details["mac_address"],
"reserved_on": full_details["current_reservation"]["start_time"],
"expires_on": full_details["current_reservation"]["finish_time"],
"reserved_for": "{display_name} <{email_address}>".format(
display_name=full_details["current_reservation"]["user"]["display_name"],
email_address=full_details["current_reservation"]["user"]["email_address"],
),
"owner": "{display_name} <{email_address}>".format(
display_name=full_details["owner"]["display_name"],
email_address=full_details["owner"]["email_address"],
),
"id": full_details["id"]
}
79 changes: 79 additions & 0 deletions broker/providers/beaker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import click
import inspect
from dynaconf import Validator
from logzero import logger
from broker.providers import Provider
from broker.binds.beaker import BeakerBind

class Beaker(Provider):

_validators = [
Validator("beaker.hub_url", must_exist=True),
]
_checkout_options = [
click.option(
"--job-xml",
type=str,
help="Path to the job XML file to submit",
),
]
_execute_options = [
click.option(
"--job-xml",
type=str,
help="Path to the job XML file to submit",
),
]
_extend_options = [] # TODO: find out the appropriate option


def __init__(self, **kwargs):
super().__init__(**kwargs)
self.runtime = BeakerBind(self.hub_url, **kwargs)

def _host_release(self):
caller_host = inspect.stack()[1][0].f_locals["host"]
broker_args = getattr(caller_host, "_broker_args", {}).get("_broker_args", {})
return self.runtime.system_release(broker_args["id"])

def _set_attributes(self, host_inst, broker_args=None):
host_inst.__dict__.update(
{
"_prov_inst": self,
"_broker_provider": "Beaker",
"_broker_provider_instance": self.instance,
"_broker_args": broker_args,
"release": self._host_release,
}
)

def construct_host(self, provider_params, host_classes, **kwargs):
"""Constructs broker host from a beaker system information
:param provider_params: a beaker system information dictionary
:param host_classes: host object
:return: constructed broker host object
"""
logger.debug(
f"constructing with {provider_params=}\n{host_classes=}\n{kwargs=}"
)
if not provider_params:
host_inst = host_classes[kwargs.get("type", "host")](**kwargs)
# cont_inst = self._cont_inst_by_name(host_inst.name)
self._set_attributes(host_inst, broker_args=kwargs)
return host_inst

@Provider.register_action("job_xml")
def submit_job(self, job_xml):
job_id = self.runtime.job_submit(job_xml)
logger.info(f"Submitted job {job_id}")
# wait for the job to finish
result = self.runtime.job_watch(job_id)

def get_inventory(self, **kwargs):
hosts = self.runtime.user_systems()
with click.progressbar(hosts, label='Compiling host information') as hosts_bar:
compiled_host_info = [self._compile_host_info(host) for host in hosts_bar]
return compiled_host_info
2 changes: 2 additions & 0 deletions broker_settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Container:
# name_prefix: test
results_limit: 50
auto_map_ports: False
Beaker:
hub_url:
TestProvider:
instances:
- test1:
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ docker =
docker
paramiko
podman = podman-py
beaker = beaker-client

[options.entry_points]
console_scripts =
Expand Down

0 comments on commit e7ac7be

Please sign in to comment.