Skip to content

Commit

Permalink
Add authentication to CLI when submitting jobs with priority
Browse files Browse the repository at this point in the history
  • Loading branch information
val500 committed Oct 8, 2024
1 parent db8a492 commit d837e99
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 10 deletions.
63 changes: 59 additions & 4 deletions cli/testflinger_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ def __init__(self):
or os.environ.get("TESTFLINGER_SERVER")
or "https://testflinger.canonical.com"
)
self.client_id = (
self.args.client_id
or self.config.get("client_id")
or os.environ.get("TESTFLINGER_CLIENT_ID")
)
self.secret_key = (
self.args.secret_key
or self.config.get("secret_key")
or os.environ.get("TESTFLINGER_SECRET_KEY")
)

# Allow config subcommand without worrying about server or client
if (
hasattr(self.args, "func")
Expand Down Expand Up @@ -185,6 +196,16 @@ def get_args(self):
parser.add_argument(
"--server", default=None, help="Testflinger server to use"
)
parser.add_argument(
"--client_id",
default=None,
help="Client ID to authenticate with Testflinger server",
)
parser.add_argument(
"--secret_key",
default=None,
help="Secret key to be used with client id for authentication",
)
sub = parser.add_subparsers()
arg_artifacts = sub.add_parser(
"artifacts",
Expand Down Expand Up @@ -373,19 +394,24 @@ def submit(self):
except FileNotFoundError:
sys.exit(f"File not found: {self.args.filename}")
job_dict = yaml.safe_load(data)
if "job_priority" in job_dict:
jwt = self.authenticate_with_server()
auth_headers = {"Authorization": jwt}
else:
auth_headers = None

attachments_data = self.extract_attachment_data(job_dict)
if attachments_data is None:
# submit job, no attachments
job_id = self.submit_job_data(job_dict)
job_id = self.submit_job_data(job_dict, headers=auth_headers)
else:
with tempfile.NamedTemporaryFile(suffix="tar.gz") as archive:
archive_path = Path(archive.name)
# create attachments archive prior to job submission
logger.info("Packing attachments into %s", archive_path)
self.pack_attachments(archive_path, attachments_data)
# submit job, followed by the submission of the archive
job_id = self.submit_job_data(job_dict)
job_id = self.submit_job_data(job_dict, headers=auth_headers)
try:
logger.info("Submitting attachments for %s", job_id)
self.submit_job_attachments(job_id, path=archive_path)
Expand All @@ -406,10 +432,10 @@ def submit(self):
if self.args.poll:
self.do_poll(job_id)

def submit_job_data(self, data: dict):
def submit_job_data(self, data: dict, headers: dict = None):
"""Submit data that was generated or read from a file as a test job"""
try:
job_id = self.client.submit_job(data)
job_id = self.client.submit_job(data, headers=headers)
except client.HTTPError as exc:
if exc.status == 400:
sys.exit(
Expand Down Expand Up @@ -482,6 +508,35 @@ def submit_job_attachments(self, job_id: str, path: Path):
f"failed after {tries} tries"
)

def authenticate_with_server(self):
"""
Authenticate client id and secret key with server
and return JWT with permissions
"""
if self.client_id is None or self.secret_key is None:
sys.exit("Must provide client id and secret key for priority jobs")

try:
jwt = self.client.authenticate(self.client_id, self.secret_key)
except client.HTTPError as exc:
if exc.status == 401:
sys.exit(
"Authentication with Testflinger server failed. "
"Check your client id and secret key"
)
if exc.status == 404:
sys.exit(
"Received 404 error from server. Are you "
"sure this is a testflinger server?"
)
# This shouldn't happen, so let's get more information
logger.error(
"Unexpected error status from testflinger server: %s",
exc.status,
)
sys.exit(1)
return jwt

def show(self):
"""Show the requested job JSON for a specified JOB_ID"""
try:
Expand Down
33 changes: 27 additions & 6 deletions cli/testflinger_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pathlib import Path
import sys
import urllib.parse
import base64

import requests

Expand All @@ -45,7 +46,7 @@ class Client:
def __init__(self, server):
self.server = server

def get(self, uri_frag, timeout=15):
def get(self, uri_frag, timeout=15, headers=None):
"""Submit a GET request to the server
:param uri_frag:
endpoint for the GET request
Expand All @@ -54,7 +55,7 @@ def get(self, uri_frag, timeout=15):
"""
uri = urllib.parse.urljoin(self.server, uri_frag)
try:
req = requests.get(uri, timeout=timeout)
req = requests.get(uri, timeout=timeout, headers=headers)
except requests.exceptions.ConnectionError:
logger.error("Unable to communicate with specified server.")
raise
Expand All @@ -68,7 +69,7 @@ def get(self, uri_frag, timeout=15):
raise HTTPError(req.status_code)
return req.text

def put(self, uri_frag, data, timeout=15):
def put(self, uri_frag, data, timeout=15, headers=None):
"""Submit a POST request to the server
:param uri_frag:
endpoint for the POST request
Expand All @@ -77,7 +78,9 @@ def put(self, uri_frag, data, timeout=15):
"""
uri = urllib.parse.urljoin(self.server, uri_frag)
try:
req = requests.post(uri, json=data, timeout=timeout)
req = requests.post(
uri, json=data, timeout=timeout, headers=headers
)
except requests.exceptions.ConnectTimeout:
logger.error(
"Timeout while trying to communicate with the server."
Expand Down Expand Up @@ -147,7 +150,7 @@ def post_job_state(self, job_id, state):
data = {"job_state": state}
self.put(endpoint, data)

def submit_job(self, data: dict) -> str:
def submit_job(self, data: dict, headers: dict = None) -> str:
"""Submit a test job to the testflinger server
:param job_data:
Expand All @@ -156,9 +159,27 @@ def submit_job(self, data: dict) -> str:
ID for the test job
"""
endpoint = "/v1/job"
response = self.put(endpoint, data)
response = self.put(endpoint, data, headers=headers)
return json.loads(response).get("job_id")

def authenticate(self, client_id: str, secret_key: str) -> dict:
"""Authenticates client id and secret key with the server
and returns JWT with allowed permissions
:param job_data:
Dictionary containing data for the job to submit
:return:
ID for the test job
"""
endpoint = "/v1/oauth2/token"
id_key_pair = f"{client_id}:{secret_key}"
encoded_id_key_pair = base64.b64encode(
id_key_pair.encode("utf-8")
).decode("utf-8")
headers = {"Authorization": f"Basic {encoded_id_key_pair}"}
response = self.put(endpoint, {}, headers=headers)
return response

def post_attachment(self, job_id: str, path: Path, timeout: int):
"""Send a test job attachment to the testflinger server
Expand Down

0 comments on commit d837e99

Please sign in to comment.