Skip to content

Commit

Permalink
Foreman broker (#291)
Browse files Browse the repository at this point in the history
* Add foreman broker

* add example settings for Foreman broker

* Reorganize classes

- Rename ForemanAPI to ForemanBind
- Put Foreman Bind in a separate file
- Create exception for FormanBind
- Make internal method private
- Add method to query hostgroup information

* Apply organizational changes in tests too

* Get rid of constructor call in provider_help

* Hotfix for github action

---------

Co-authored-by: Jan Bundesmann <[email protected]>
  • Loading branch information
2 people authored and JacobCallahan committed May 29, 2024
1 parent d6366b5 commit f575187
Show file tree
Hide file tree
Showing 11 changed files with 993 additions and 0 deletions.
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

0 comments on commit f575187

Please sign in to comment.