Skip to content

Commit

Permalink
fix: find-free-cloud and cc_users on mod-cloud
Browse files Browse the repository at this point in the history
Related-to: #500
Change-Id: Ibdcd5a1103d061878341a286f802159cccd2ddce
  • Loading branch information
grafuls committed Sep 30, 2024
1 parent 8492924 commit b63e2f1
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 231 deletions.
430 changes: 315 additions & 115 deletions src/quads/cli/cli.py

Large diffs are not rendered by default.

38 changes: 26 additions & 12 deletions src/quads/quads_api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import os
import requests

from json import JSONDecodeError
from typing import Optional, List
from requests import Response
from requests.auth import HTTPBasicAuth
from requests.adapters import HTTPAdapter, Retry
from typing import List, Optional
from urllib import parse as url_parse
from urllib.parse import urlencode

import requests
from requests import Response
from requests.adapters import HTTPAdapter, Retry
from requests.auth import HTTPBasicAuth

from quads.config import Config
from quads.server.models import Host, Cloud, Schedule, Interface, Vlan, Assignment
from quads.server.models import Assignment, Cloud, Host, Interface, Schedule, Vlan


class APIServerException(Exception):
Expand Down Expand Up @@ -40,11 +40,15 @@ def __init__(self, config: Config):
self.session = requests.Session()
retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504])
self.session.mount("http://", HTTPAdapter(max_retries=retries))
self.auth = HTTPBasicAuth(self.config.get("quads_api_username"), self.config.get("quads_api_password"))
self.auth = HTTPBasicAuth(
self.config.get("quads_api_username"), self.config.get("quads_api_password")
)

# Base functions
def get(self, endpoint: str) -> Response:
_response = self.session.get(os.path.join(self.base_url, endpoint), verify=False, auth=self.auth)
_response = self.session.get(
os.path.join(self.base_url, endpoint), verify=False, auth=self.auth
)
if _response.status_code == 500:
raise APIServerException("Check the flask server logs")
if _response.status_code == 400:
Expand Down Expand Up @@ -84,7 +88,9 @@ def patch(self, endpoint, data) -> Response:
return _response

def delete(self, endpoint) -> Response:
_response = self.session.delete(os.path.join(self.base_url, endpoint), verify=False, auth=self.auth)
_response = self.session.delete(
os.path.join(self.base_url, endpoint), verify=False, auth=self.auth
)
if _response.status_code == 500:
raise APIServerException("Check the flask server logs")
if _response.status_code == 400:
Expand Down Expand Up @@ -167,12 +173,20 @@ def get_clouds(self) -> List[Cloud]:
clouds.append(Cloud(**cloud))
return [cloud for cloud in sorted(clouds, key=lambda x: x.name)]

def get_free_clouds(self) -> List[Cloud]:
response = self.get("clouds/free/")
clouds = []
for cloud in response.json():
clouds.append(Cloud(**cloud))
return clouds

def get_cloud(self, cloud_name) -> Optional[Cloud]:
cloud_obj = None
response = self.get(os.path.join("clouds", cloud_name))
response = self.get(f"clouds?name={cloud_name}")
obj_json = response.json()
if obj_json:
cloud_obj = Cloud(**obj_json)
cloud_response = obj_json[0]
cloud_obj = Cloud(**cloud_response)
return cloud_obj

def insert_cloud(self, data) -> Response:
Expand Down
8 changes: 4 additions & 4 deletions src/quads/server/blueprints/assignments.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import re

from flask import Blueprint, jsonify, request, Response, make_response
from flask import Blueprint, Response, jsonify, make_response, request
from sqlalchemy import inspect

from quads.server.blueprints import check_access
from quads.server.dao.assignment import AssignmentDao
from quads.server.dao.baseDao import EntryNotFound, InvalidArgument, BaseDao
from quads.server.dao.baseDao import BaseDao, EntryNotFound, InvalidArgument
from quads.server.dao.cloud import CloudDao
from quads.server.dao.vlan import VlanDao
from quads.server.models import Assignment
from sqlalchemy import inspect

assignment_bp = Blueprint("assignments", __name__)

Expand Down Expand Up @@ -225,7 +225,7 @@ def update_assignment(assignment_id: str) -> Response:
value = data.get(attr.key)
if value is not None:
if attr.key == "ccuser":
value = value.split(",")
value = re.split(r"[, ]+", value)
value = [user.strip() for user in value]
if attr.key == "cloud":
_cloud = CloudDao.get_cloud(value)
Expand Down
45 changes: 20 additions & 25 deletions src/quads/server/blueprints/clouds.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import json
from datetime import datetime

from flask import Blueprint, jsonify, request, Response, make_response
from quads.server.dao.assignment import AssignmentDao
from flask import Blueprint, Response, jsonify, make_response, request

from quads.config import Config
from quads.server.blueprints import check_access
from quads.server.dao.assignment import AssignmentDao
from quads.server.dao.baseDao import EntryNotFound, InvalidArgument
from quads.server.dao.cloud import CloudDao
from quads.server.dao.host import HostDao
Expand All @@ -14,28 +14,6 @@
cloud_bp = Blueprint("clouds", __name__)


@cloud_bp.route("/<cloud>/")
def get_cloud(cloud: str) -> Response:
"""
GET request that returns the cloud with the given name.
---
tags:
- API
:param cloud: str: Specify the cloud name
:return: A response object that contains the json representation of the cloud
"""
_cloud = CloudDao.get_cloud(cloud)
if not _cloud:
response = {
"status_code": 400,
"error": "Bad Request",
"message": f"Cloud not found: {cloud}",
}
return make_response(jsonify(response), 400)
return jsonify(_cloud.as_dict())


@cloud_bp.route("/")
def get_clouds() -> Response:
"""
Expand All @@ -62,6 +40,21 @@ def get_clouds() -> Response:
return jsonify([_cloud.as_dict() for _cloud in _clouds] if _clouds else {})


@cloud_bp.route("/free/")
def get_free_clouds() -> Response:
"""
Returns a list of all free clouds in the database.
---
tags:
- API
:return: The list of free clouds
"""
_clouds = CloudDao.get_free_clouds()

return jsonify([_cloud.as_dict() for _cloud in _clouds])


@cloud_bp.route("/", methods=["POST"])
@check_access(["admin"])
def create_cloud() -> Response:
Expand Down Expand Up @@ -192,7 +185,9 @@ def get_summary() -> Response:
description = Config["spare_pool_description"]
owner = Config["spare_pool_owner"]
else:
date = datetime.strptime(_date, "%Y-%m-%dT%H:%M") if _date else datetime.now()
date = (
datetime.strptime(_date, "%Y-%m-%dT%H:%M") if _date else datetime.now()
)
schedules = ScheduleDao.get_current_schedule(cloud=_cloud, date=date)
count = len(schedules)
total_count += count
Expand Down
30 changes: 27 additions & 3 deletions src/quads/server/dao/cloud.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from datetime import datetime
from typing import List, Optional, Type

from sqlalchemy import Boolean
from sqlalchemy import Boolean, or_

from quads.config import Config
from quads.server.dao.baseDao import (
OPERATORS,
BaseDao,
EntryExisting,
EntryNotFound,
OPERATORS,
InvalidArgument,
)
from quads.server.models import db, Cloud
from quads.server.models import Assignment, Cloud, Schedule, db


class CloudDao(BaseDao):
Expand Down Expand Up @@ -68,6 +70,26 @@ def get_clouds() -> List[Cloud]:
clouds = db.session.query(Cloud).order_by(Cloud.name.asc()).all()
return clouds

@staticmethod
def get_free_clouds() -> List[Cloud]:
free_clouds = (
db.session.query(Cloud)
.outerjoin(Assignment, Cloud.id == Assignment.cloud_id)
.outerjoin(Schedule, Assignment.id == Schedule.assignment_id)
.filter(
Cloud.name != Config["spare_pool_name"],
or_(
Schedule.end <= datetime.now(),
Assignment.id == None,
Schedule.id == None,
),
)
.order_by(Cloud.name.asc())
.distinct()
.all()
)
return free_clouds

@staticmethod
def filter_clouds_dict(data: dict) -> List[Type[Cloud]]:
filter_tuples = []
Expand Down Expand Up @@ -103,6 +125,8 @@ def filter_clouds_dict(data: dict) -> List[Type[Cloud]]:
)
if filter_tuples:
_clouds = CloudDao.create_query_select(Cloud, filters=filter_tuples)
if not _clouds:
raise EntryNotFound("No clouds found with the given filters")
else:
_clouds = CloudDao.get_clouds()
return _clouds
37 changes: 17 additions & 20 deletions src/quads/server/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ paths:
- BearerAuth: [ ]

/clouds/{cloudName}/:
get:
summary: Returns a cloud by name
delete:
summary: Delete cloud by cloud name
tags:
- Clouds
parameters:
Expand All @@ -193,7 +193,18 @@ paths:
type: string
responses:
'200':
description: Cloud name
description: Deleted Cloud
security:
- BearerAuth: [ ]

/clouds/free/:
get:
summary: Returns all free clouds that are available for new assignments
tags:
- Clouds
responses:
'200':
description: Free clouds
headers:
x-next:
description: A link to the next page of responses
Expand All @@ -202,29 +213,15 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Cloud'
type: array
items:
$ref: '#/components/schemas/Cloud'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
summary: Delete cloud by cloud name
tags:
- Clouds
parameters:
- name: cloudName
in: path
description: Cloud name
required: true
schema:
type: string
responses:
'200':
description: Deleted Cloud
security:
- BearerAuth: [ ]

/clouds/summary/:
get:
Expand Down
34 changes: 28 additions & 6 deletions tests/api/test_clouds.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,15 @@ def test_valid_single(self, test_client, auth):
cloud_name = f"cloud{str(cloud_id).zfill(2)}"
response = unwrap_json(
test_client.get(
f"/api/v3/clouds/{cloud_name}",
f"/api/v3/clouds?name={cloud_name}",
headers=auth_header,
)
)
cloud = response.json[0]
assert response.status_code == 200
assert response.json["id"] == cloud_id
assert response.json["name"] == cloud_name
assert response.json["last_redefined"] is not None
assert cloud["id"] == cloud_id
assert cloud["name"] == cloud_name
assert cloud["last_redefined"] is not None

def test_valid_multiple(self, test_client, auth):
"""
Expand All @@ -108,6 +109,27 @@ def test_valid_multiple(self, test_client, auth):
assert response.json[cloud_id - 1]["name"] == cloud_name
assert response.json[cloud_id - 1]["last_redefined"] is not None

def test_free_cloud(self, test_client, auth):
"""
| GIVEN: Clouds from test_valid in database and user logged in
| WHEN: User tries to read all free clouds
| THEN: User should be able to read all free clouds
"""
auth_header = auth.get_auth_header()
response = unwrap_json(
test_client.get(
"/api/v3/clouds/free/",
headers=auth_header,
)
)
assert response.status_code == 200
assert len(response.json) == 9
for cloud_id in range(2, 11):
cloud_name = f"cloud{str(cloud_id).zfill(2)}"
assert response.json[cloud_id - 2]["id"] == cloud_id
assert response.json[cloud_id - 2]["name"] == cloud_name
assert response.json[cloud_id - 2]["last_redefined"] is not None

def test_invalid_not_found_single(self, test_client, auth):
"""
| GIVEN: Clouds from test_valid in database and user logged in
Expand All @@ -117,11 +139,11 @@ def test_invalid_not_found_single(self, test_client, auth):
auth_header = auth.get_auth_header()
cloud_name = "cloud11"
response = unwrap_json(
test_client.get(f"/api/v3/clouds/{cloud_name}", headers=auth_header)
test_client.get(f"/api/v3/clouds?name={cloud_name}", headers=auth_header)
)
assert response.status_code == 400
assert response.json["error"] == "Bad Request"
assert response.json["message"] == "Cloud not found: cloud11"
assert response.json["message"] == "No clouds found with the given filters"

def test_invalid_filter(self, test_client, auth):
"""
Expand Down
Loading

0 comments on commit b63e2f1

Please sign in to comment.