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

Add name filters (SOFTWARE-5442) #2911

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
94 changes: 5 additions & 89 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
import urllib.parse

from webapp import default_config
from webapp.common import readfile, to_xml_bytes, to_json_bytes, Filters, support_cors, simplify_attr_list, is_null, escape
from webapp.common import readfile, to_xml_bytes, to_json_bytes, Filters, support_cors, simplify_attr_list, is_null, \
escape
from webapp.exceptions import DataError, ResourceNotRegistered, ResourceMissingService
from webapp.forms import GenerateDowntimeForm, GenerateResourceGroupDowntimeForm
from webapp.models import GlobalData
from webapp.topology import GRIDTYPE_1, GRIDTYPE_2
from webapp.oasis_managers import get_oasis_manager_endpoint_info


Expand Down Expand Up @@ -283,6 +283,7 @@ def rgsummary_xml():
return _get_xml_or_fail(global_data.get_topology().get_resource_summary, request.args)



@app.route('/rgdowntime/xml')
def rgdowntime_xml():
return _get_xml_or_fail(global_data.get_topology().get_downtimes, request.args)
Expand All @@ -291,7 +292,7 @@ def rgdowntime_xml():
@app.route('/rgdowntime/ical')
def rgdowntime_ical():
try:
filters = get_filters_from_args(request.args)
filters = Filters.from_args(request.args, global_data)
except InvalidArgumentsError as e:
return Response("Invalid arguments: " + str(e), status=400)
response = make_response(global_data.get_topology().get_downtimes_ical(False, filters).to_ical())
Expand Down Expand Up @@ -704,94 +705,9 @@ def _make_choices(iterable, select_one=False):
return c


def get_filters_from_args(args) -> Filters:
filters = Filters()
def filter_value(filter_key):
filter_value_key = filter_key + "_value"
if filter_key in args:
filter_value_str = args.get(filter_value_key, "")
if filter_value_str == "0":
return False
elif filter_value_str == "1":
return True
else:
raise InvalidArgumentsError("{0} must be 0 or 1".format(filter_value_key))
filters.active = filter_value("active")
filters.disable = filter_value("disable")
filters.oasis = filter_value("oasis")

if "gridtype" in args:
gridtype_1, gridtype_2 = args.get("gridtype_1", ""), args.get("gridtype_2", "")
if gridtype_1 == "on" and gridtype_2 == "on":
pass
elif gridtype_1 == "on":
filters.grid_type = GRIDTYPE_1
elif gridtype_2 == "on":
filters.grid_type = GRIDTYPE_2
else:
raise InvalidArgumentsError("gridtype_1 or gridtype_2 or both must be \"on\"")
if "service_hidden_value" in args: # note no "service_hidden" args
if args["service_hidden_value"] == "0":
filters.service_hidden = False
elif args["service_hidden_value"] == "1":
filters.service_hidden = True
else:
raise InvalidArgumentsError("service_hidden_value must be 0 or 1")
if "downtime_attrs_showpast" in args:
# doesn't make sense for rgsummary but will be ignored anyway
try:
v = args["downtime_attrs_showpast"]
if v == "all":
filters.past_days = -1
elif not v:
filters.past_days = 0
else:
filters.past_days = int(args["downtime_attrs_showpast"])
except ValueError:
raise InvalidArgumentsError("downtime_attrs_showpast must be an integer, \"\", or \"all\"")
if "has_wlcg" in args:
filters.has_wlcg = True

# 2 ways to filter by a key like "facility", "service", "sc", "site", etc.:
# - either pass KEY_1=on, KEY_2=on, etc.
# - pass KEY_sel[]=1, KEY_sel[]=2, etc. (multiple KEY_sel[] args).
for filter_key, filter_list, description in [
("facility", filters.facility_id, "facility ID"),
("rg", filters.rg_id, "resource group ID"),
("service", filters.service_id, "service ID"),
("sc", filters.support_center_id, "support center ID"),
("site", filters.site_id, "site ID"),
("vo", filters.vo_id, "VO ID"),
("voown", filters.voown_id, "VO owner ID"),
]:
if filter_key in args:
pat = re.compile(r"{0}_(\d+)".format(filter_key))
arg_sel = "{0}_sel[]".format(filter_key)
for k, v in args.items():
if k == arg_sel:
try:
filter_list.append(int(v))
except ValueError:
raise InvalidArgumentsError("{0}={1}: must be int".format(k,v))
elif pat.match(k):
m = pat.match(k)
filter_list.append(int(m.group(1)))
if not filter_list:
raise InvalidArgumentsError("at least one {0} must be specified"
" via the syntax <code>{1}_<b>ID</b>=on</code>"
" or <code>{1}_sel[]=<b>ID</b></code>."
" (These may be specified multiple times for multiple IDs.)"\
.format(description, filter_key))

if filters.voown_id:
filters.populate_voown_name(global_data.get_vos_data().get_vo_id_to_name())

return filters


def _get_xml_or_fail(getter_function, args):
try:
filters = get_filters_from_args(args)
filters = Filters.from_args(args, global_data)
except InvalidArgumentsError as e:
return Response("Invalid arguments: " + str(e), status=400)
return Response(
Expand Down
114 changes: 101 additions & 13 deletions src/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re
import flask
from flask import testing
import pytest
import xmltodict
from pytest_mock import MockerFixture

# Rewrites the path so the app can be imported like it normally is
Expand Down Expand Up @@ -64,16 +66,16 @@ def client():

class TestAPI:

def test_sanity(self, client: flask.Flask):
def test_sanity(self, client: testing.FlaskClient):
response = client.get('/')
assert response.status_code == 200

@pytest.mark.parametrize('endpoint', TEST_ENDPOINTS)
def test_endpoint_existence(self, endpoint, client: flask.Flask):
def test_endpoint_existence(self, endpoint, client: testing.FlaskClient):
response = client.get(endpoint)
assert response.status_code != 404

def test_cache_authfile(self, client: flask.Flask, mocker: MockerFixture):
def test_cache_authfile(self, client: testing.FlaskClient, mocker: MockerFixture):
mocker.patch("webapp.ldap_data.get_ligo_ldap_dn_list", mocker.MagicMock(return_value=["deadbeef.0"]))
resources = client.get('/miscresource/json').json
for resource in resources.values():
Expand All @@ -84,7 +86,7 @@ def test_cache_authfile(self, client: flask.Flask, mocker: MockerFixture):
assert previous_endpoint.status_code == current_endpoint.status_code
assert previous_endpoint.data == current_endpoint.data

def test_cache_authfile_public(self, client: flask.Flask):
def test_cache_authfile_public(self, client: testing.FlaskClient):
resources = client.get('/miscresource/json').json
for resource in resources.values():
resource_fqdn = resource["FQDN"]
Expand All @@ -94,7 +96,7 @@ def test_cache_authfile_public(self, client: flask.Flask):
assert previous_endpoint.status_code == current_endpoint.status_code
assert previous_endpoint.data == current_endpoint.data

def test_origin_authfile(self, client: flask.Flask):
def test_origin_authfile(self, client: testing.FlaskClient):
resources = client.get('/miscresource/json').json
for resource in resources.values():
resource_fqdn = resource["FQDN"]
Expand All @@ -104,7 +106,7 @@ def test_origin_authfile(self, client: flask.Flask):
assert previous_endpoint.status_code == current_endpoint.status_code
assert previous_endpoint.data == current_endpoint.data

def test_origin_authfile_public(self, client: flask.Flask):
def test_origin_authfile_public(self, client: testing.FlaskClient):
resources = client.get('/miscresource/json').json
for resource in resources.values():
resource_fqdn = resource["FQDN"]
Expand All @@ -114,7 +116,7 @@ def test_origin_authfile_public(self, client: flask.Flask):
assert previous_endpoint.status_code == current_endpoint.status_code
assert previous_endpoint.data == current_endpoint.data

def test_cache_scitokens(self, client: flask.Flask):
def test_cache_scitokens(self, client: testing.FlaskClient):
resources = client.get('/miscresource/json').json
for resource in resources.values():
resource_fqdn = resource["FQDN"]
Expand All @@ -124,7 +126,7 @@ def test_cache_scitokens(self, client: flask.Flask):
assert previous_endpoint.status_code == current_endpoint.status_code
assert previous_endpoint.data == current_endpoint.data

def test_origin_scitokens(self, client: flask.Flask):
def test_origin_scitokens(self, client: testing.FlaskClient):
resources = client.get('/miscresource/json').json
for resource in resources.values():
resource_fqdn = resource["FQDN"]
Expand All @@ -134,7 +136,7 @@ def test_origin_scitokens(self, client: flask.Flask):
assert previous_endpoint.status_code == current_endpoint.status_code
assert previous_endpoint.data == current_endpoint.data

def test_resource_stashcache_files(self, client: flask.Flask, mocker: MockerFixture):
def test_resource_stashcache_files(self, client: testing.FlaskClient, mocker: MockerFixture):
"""Tests that the resource table contains the same files as the singular api outputs"""

# Disable legacy auth until it's turned back on in Resource.get_stashcache_files()
Expand Down Expand Up @@ -180,7 +182,7 @@ def test_stashcache_file(key, endpoint, fqdn, resource_stashcache_files):
else:
app.config["STASHCACHE_LEGACY_AUTH"] = old_legacy_auth

def test_stashcache_namespaces(self, client: flask.Flask):
def test_stashcache_namespaces(self, client: testing.FlaskClient):
def validate_cache_schema(cc):
assert HOST_PORT_RE.match(cc["auth_endpoint"])
assert HOST_PORT_RE.match(cc["endpoint"])
Expand Down Expand Up @@ -704,7 +706,7 @@ class TestEndpointContent:
mock_facility.add_site(mock_site)
mock_site.add_resource_group(mock_resource_group)

def test_resource_defaults(self, client: flask.Flask):
def test_resource_defaults(self, client: testing.FlaskClient):
resources = client.get('/miscresource/json').json

# Check that it is not empty
Expand All @@ -715,7 +717,7 @@ def test_resource_defaults(self, client: flask.Flask):
"Description", "FQDN", "FQDNAliases", "VOOwnership",
"WLCGInformation", "ContactLists", "IsCCStar"])

def test_site_defaults(self, client: flask.Flask):
def test_site_defaults(self, client: testing.FlaskClient):
sites = client.get('/miscsite/json').json

# Check that it is not empty
Expand All @@ -724,7 +726,7 @@ def test_site_defaults(self, client: flask.Flask):
# Check that the site contains the appropriate keys
assert set(sites.popitem()[1]).issuperset(["ID", "Name", "IsCCStar"])

def test_facility_defaults(self, client: flask.Flask):
def test_facility_defaults(self, client: testing.FlaskClient):
facilities = client.get('/miscfacility/json').json

# Check that it is not empty
Expand All @@ -733,6 +735,92 @@ def test_facility_defaults(self, client: flask.Flask):
# Check that the site contains the appropriate keys
assert set(facilities.popitem()[1]).issuperset(["ID", "Name", "IsCCStar"])

def test_filter_by_service_name(self, client: testing.FlaskClient):
"""Checks inclusion of service name filtering on Resource Class"""

xml = client.get('/rgsummary/xml?service_name[]=CE').data
r = xmltodict.parse(xml)

resources = []
for rg in r["ResourceSummary"]["ResourceGroup"]:
if type(rg["Resources"]["Resource"]) == list:
resources.extend(rg["Resources"]["Resource"])
else:
resources.append(rg["Resources"]["Resource"])

services = []
for r in resources:
if "Services" not in r:
continue
if type(r["Services"]["Service"]) == list:
services.extend(r["Services"]["Service"])
else:
services.append(r["Services"]["Service"])

assert all(list(s["Name"] == "CE" for s in services))

def test_rgsummary_filter_by_facility_name(self, client: testing.FlaskClient):

xml = client.get('/rgsummary/xml?facility_name[]=California%20Institute%20of%20Technology').data
r = xmltodict.parse(xml)

for rg in r["ResourceSummary"]["ResourceGroup"]:
assert rg["Facility"]["Name"] == "California Institute of Technology"

def test_rgsummary_filter_by_site_name(self, client: testing.FlaskClient):

xml = client.get('/rgsummary/xml?site_name[]=Caltech%20CMS%20Tier2').data
r = xmltodict.parse(xml)

assert r["ResourceSummary"]["ResourceGroup"]["Site"]["Name"] == "Caltech CMS Tier2"

def test_rgsummary_filter_by_support_center_name(self, client: testing.FlaskClient):

xml = client.get('/rgsummary/xml?sc_name[]=Self%20Supported').data
r = xmltodict.parse(xml)

for rg in r["ResourceSummary"]["ResourceGroup"]:
assert rg["SupportCenter"]["Name"] == "Self Supported"

def test_rgsummary_filter_by_rg_name(self, client: testing.FlaskClient):

xml = client.get('/rgsummary/xml?rg_name[]=FIUPG').data
r = xmltodict.parse(xml)

assert r["ResourceSummary"]["ResourceGroup"]["GroupName"] == "FIUPG"

def test_rgdowntime_filter_by_facility_name(self, client: testing.FlaskClient):

xml = client.get('/rgdowntime/xml?facility_name[]=Florida%20Institute%20of%20Technology&downtime_attrs_showpast=all').data
r = xmltodict.parse(xml)

assert r["Downtimes"]["PastDowntimes"]["Downtime"][0]["ResourceGroup"]["GroupName"] == "FLTECH"

def test_rgdowntime_filter_by_site_name(self, client: testing.FlaskClient):

xml = client.get('/rgdowntime/xml?site_name[]=Florida%20Tech&downtime_attrs_showpast=all').data
r = xmltodict.parse(xml)

for downtime in r["Downtimes"]["PastDowntimes"]["Downtime"]:
assert downtime["ResourceGroup"]["GroupName"] == "FLTECH"

def test_rgdowntime_filter_by_support_center_name(self, client: testing.FlaskClient):

xml = client.get('/rgdowntime/xml?sc_name[]=Community%20Support%20Center&downtime_attrs_showpast=all').data
r = xmltodict.parse(xml)

included_resource_groups = [rg['ResourceGroup']['GroupName'] for rg in r['Downtimes']['PastDowntimes']['Downtime']]

assert "Utah-SLATE-Notchpeak" in included_resource_groups
assert "OSG_IN_IUCAA_SARATHI" not in included_resource_groups # This has a UChicago Support Center

def test_rgdowntime_filter_by_rg_name(self, client: testing.FlaskClient):

xml = client.get('/rgdowntime/xml?rg_name[]=FLTECH&downtime_attrs_showpast=all').data
r = xmltodict.parse(xml)

for downtime in r["Downtimes"]["PastDowntimes"]["Downtime"]:
assert downtime["ResourceGroup"]["GroupName"] == "FLTECH"

if __name__ == '__main__':
pytest.main()
Loading