Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foreman broker #291

Merged
merged 6 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
env:
BROKER_DIRECTORY: "${{ github.workspace }}/broker_dir"
run: |
cp broker_settings.yaml.example ${BROKER_DIRECTORY}/broker_settings.yaml
pip install uv
uv pip install --system "broker[dev,docker] @ ."
ls -l "$BROKER_DIRECTORY"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ ENV/
*settings.yaml
inventory.yaml
*.bak
/bin/*
162 changes: 162 additions & 0 deletions broker/binds/foreman.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Foreman provider implementation."""
import time

from logzero import logger
import requests

from broker import exceptions
from broker.settings import settings


class ForemanBind:
"""Default runtime to query Foreman."""

headers = {
"Content-Type": "application/json",
}

def __init__(self, **kwargs):
self.foreman_username = settings.foreman.foreman_username
self.foreman_password = settings.foreman.foreman_password
self.url = settings.foreman.foreman_url
self.prefix = settings.foreman.name_prefix
self.verify = settings.foreman.verify
self.session = requests.session()

def _interpret_response(self, response):
"""Handle responses from Foreman, in particular catch errors."""
if "error" in response:
if "Unable to authenticate user" in response["error"]["message"]:
raise exceptions.AuthenticationError(response["error"]["message"])
raise exceptions.ForemanBindError(
provider=self.__class__.__name__,
message=" ".join(response["error"]["full_messages"]),
)
if "errors" in response:
raise exceptions.ForemanBindError(
provider=self.__class__.__name__, message=" ".join(response["errors"]["base"])
)
return response

def _get(self, endpoint):
"""Send GET request to Foreman API."""
response = self.session.get(
self.url + endpoint,
auth=(self.foreman_username, self.foreman_password),
headers=self.headers,
verify=self.verify,
).json()
return self._interpret_response(response)

def _post(self, endpoint, **kwargs):
"""Send POST request to Foreman API."""
response = self.session.post(
self.url + endpoint,
auth=(self.foreman_username, self.foreman_password),
headers=self.headers,
verify=self.verify,
**kwargs,
).json()
return self._interpret_response(response)

def _delete(self, endpoint, **kwargs):
"""Send DELETE request to Foreman API."""
response = self.session.delete(
self.url + endpoint,
auth=(self.foreman_username, self.foreman_password),
headers=self.headers,
verify=self.verify,
**kwargs,
)
return self._interpret_response(response)

def obtain_id_from_name(self, resource_type, resource_name):
"""Obtain id for resource with given name.

:param resource_type: Resource type, like hostgroups, hosts, ...

:param resource_name: String-like identifier of the resource

:return: ID of the found object
"""
response = self._get(
f"/api/{resource_type}?per_page=200",
)
try:
result = response["results"]
resource = next(
x
for x in result
if x.get("title") == resource_name or x.get("name") == resource_name
)
id_ = resource["id"]
except KeyError:
logger.error(f"Could not find {resource_type} {resource_name}")
raise
except StopIteration:
raise exceptions.ForemanBindError(
provider=self.__class__.__name__,
message=f"Could not find {resource_name} in {resource_type}",
)
return id_

def create_job_invocation(self, data):
"""Run a job from the provided data."""
return self._post(
"/api/job_invocations",
json=data,
)["id"]

def job_output(self, job_id):
"""Return output of job."""
return self._get(f"/api/job_invocations/{job_id}/outputs")["outputs"][0]["output"]

def wait_for_job_to_finish(self, job_id):
"""Poll API for job status until it is finished.

:param job_id: id of the job to poll
"""
still_running = True
while still_running:
response = self._get(f"/api/job_invocations/{job_id}")
still_running = response["status_label"] == "running"
time.sleep(1)

def hostgroups(self):
"""Return list of available hostgroups."""
return self._get("/api/hostgroups")

def hostgroup(self, name):
"""Return list of available hostgroups."""
hostgroup_id = self.obtain_id_from_name("hostgroups", name)
return self._get(f"/api/hostgroups/{hostgroup_id}")

def hosts(self):
"""Return list of hosts deployed using this prefix."""
return self._get(f"/api/hosts?search={self.prefix}")["results"]

def image_uuid(self, compute_resource_id, image_name):
"""Return the uuid of a VM image on a specific compute resource."""
try:
return self._get(
"/api/compute_resources/"
f"{compute_resource_id}"
f"/images/?search=name={image_name}"
)["results"][0]["uuid"]
except IndexError:
raise exceptions.ForemanBindError(f"Could not find {image_name} in VM images")

def create_host(self, data):
"""Create a host from the provided data."""
return self._post("/api/hosts", json=data)

def wait_for_host_to_install(self, hostname):
"""Poll API for host build status until it is built.

:param hostname: name of the host which is currently being built
"""
building = True
while building:
host_status = self._get(f"/api/hosts/{hostname}")
building = host_status["build_status"] != 0
time.sleep(1)
6 changes: 6 additions & 0 deletions broker/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,9 @@ class UserError(BrokerError):
"""Raised when a user causes an otherwise unclassified error."""

error_code = 13


class ForemanBindError(BrokerError):
"""Raised when a problem occurs at the Foreman bind level."""

error_code = 14
Loading