diff --git a/coldfront/plugins/xdmod/management/commands/xdmod_usage.py b/coldfront/plugins/xdmod/management/commands/xdmod_usage.py
index 37314829e..554f72cd7 100644
--- a/coldfront/plugins/xdmod/management/commands/xdmod_usage.py
+++ b/coldfront/plugins/xdmod/management/commands/xdmod_usage.py
@@ -1,14 +1,13 @@
import logging
import os
import sys
-import tempfile
-from django.conf import settings
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
from django.db.models import Q
+from django.contrib.auth import get_user_model
-from coldfront.core.allocation.models import Allocation
-from coldfront.core.utils.common import import_from_settings
+from coldfront.core.allocation.models import Allocation, AllocationUserStatusChoice
+from coldfront.core.resource.models import Resource
from coldfront.plugins.xdmod.utils import (XDMOD_ACCOUNT_ATTRIBUTE_NAME,
XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME,
XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME,
@@ -17,15 +16,21 @@
XDMOD_RESOURCE_ATTRIBUTE_NAME,
XDMOD_STORAGE_ATTRIBUTE_NAME,
XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME,
- XdmodError, XdmodNotFoundError,
- xdmod_fetch_cloud_core_time,
- xdmod_fetch_total_cpu_hours, xdmod_fetch_total_storage)
+ XdmodNotFoundError,
+ XDModFetcher)
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'Sync usage data from XDMoD to ColdFront'
+ filter_user = ''
+ filter_project = ''
+ filter_resource = ''
+ filter_account = ''
+ sync = False
+ print_header = False
+ fetch_expired = False
def add_arguments(self, parser):
parser.add_argument("-r", "--resource",
@@ -36,14 +41,15 @@ def add_arguments(self, parser):
help="Report usage for specific username")
parser.add_argument("-p", "--project",
help="Report usage for specific cloud project")
- parser.add_argument(
- "-s", "--sync", help="Update allocation attributes with latest data from XDMoD", action="store_true")
- parser.add_argument(
- "-x", "--header", help="Include header in output", action="store_true")
- parser.add_argument(
- "-m", "--statistic", help="XDMoD statistic (default total_cpu_hours)", required=True)
- parser.add_argument(
- "--expired", help="XDMoD statistic for archived projects", action="store_true")
+ parser.add_argument("-s", "--sync",
+ help="Update allocation attributes with latest data from XDMoD", action="store_true")
+ parser.add_argument("-x", "--header",
+ help="Include header in output", action="store_true")
+ parser.add_argument("-m", "--statistic",
+ help="XDMoD statistic (default total_cpu_hours)",
+ default='total_cpu_hours')
+ parser.add_argument("--expired",
+ help="XDMoD statistic for archived projects", action="store_true")
def write(self, data):
try:
@@ -53,6 +59,68 @@ def write(self, data):
os.dup2(devnull, sys.stdout.fileno())
sys.exit(1)
+ def run_resource_checks(self, resource):
+ rname = resource.get_attribute(XDMOD_RESOURCE_ATTRIBUTE_NAME)
+ if not rname and resource.parent_resource:
+ rname = resource.parent_resource.get_attribute(
+ XDMOD_RESOURCE_ATTRIBUTE_NAME)
+ if self.filter_resource and self.filter_resource != rname:
+ return None
+ return rname
+
+ def id_allocation_resources(self, s):
+ resources = []
+ for r in s.resources.all():
+ rname = self.run_resource_checks(r)
+ resources.append(rname)
+ return resources
+
+ def attribute_check(self, s, attr_name, num=False):
+ attr = s.get_attribute(attr_name)
+ check_pass = attr
+ if num:
+ check_pass = attr is not None
+ if not check_pass:
+ logger.warning("%s attribute not found for allocation: %s",
+ attr_name, s)
+ return None
+ return attr
+
+ def filter_allocations(self, allocations, account_attr_name=XDMOD_ACCOUNT_ATTRIBUTE_NAME):
+ cleared_resources = Resource.objects.filter(
+ Q(resourceattribute__resource_attribute_type__name=XDMOD_RESOURCE_ATTRIBUTE_NAME) |
+ Q(parent_resource__resourceattribute__resource_attribute_type__name=XDMOD_RESOURCE_ATTRIBUTE_NAME)
+ )
+
+ allocations = (
+ allocations.select_related('project')
+ .prefetch_related(
+ 'resources', 'allocationattribute_set', 'allocationuser_set'
+ )
+ .filter(resources__in=cleared_resources)
+ )
+ if self.fetch_expired:
+ allocations = allocations.filter(~Q(status__name='Active'))
+ else:
+ allocations = allocations.filter(status__name='Active')
+
+ if self.filter_user:
+ allocations = allocations.filter(project__pi__username=self.filter_user)
+
+ if self.filter_project:
+ allocations = allocations.filter(
+ Q(allocationattribute__allocation_attribute_type__name=XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME)
+ & Q(allocationattribute__value=self.filter_project)
+ )
+
+ if self.filter_account:
+ allocations = allocations.filter(
+ Q(allocationattribute__allocation_attribute_type__name=account_attr_name)
+ & Q(allocationattribute__value=self.filter_account)
+ )
+
+ return allocations
+
def process_total_storage(self):
header = [
'allocation_id',
@@ -66,79 +134,39 @@ def process_total_storage(self):
if self.print_header:
self.write('\t'.join(header))
- allocations = Allocation.objects.prefetch_related(
- 'project',
- 'resources',
- 'allocationattribute_set',
- 'allocationuser_set'
- ).filter(
+ allocations = Allocation.objects.filter(
allocationattribute__allocation_attribute_type__name__in=[
XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME,
XDMOD_STORAGE_ATTRIBUTE_NAME,
]
)
- if self.fetch_expired:
- allocations = allocations.filter(
- ~Q(status__name='Active'),
- )
- else:
- allocations = allocations.filter(
- status__name='Active',
- )
-
- if self.filter_user:
- allocations = allocations.filter(project__pi__username=self.filter_user)
-
- if self.filter_account:
- allocations = allocations.filter(
- Q(allocationattribute__allocation_attribute_type__name=XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME) &
- Q(allocationattribute__value=self.filter_account)
- )
+ allocations = self.filter_allocations(
+ allocations, account_attr_name=XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME)
for s in allocations.distinct():
- account_name = s.get_attribute(XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME)
- if not account_name:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME, s)
+ account_name = self.attribute_check(s, XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME)
+ cpu_hours = self.attribute_check(s, XDMOD_STORAGE_ATTRIBUTE_NAME, num=True)
+ if None in [account_name, cpu_hours]:
continue
- cpu_hours = s.get_attribute(XDMOD_STORAGE_ATTRIBUTE_NAME)
- if not cpu_hours:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_STORAGE_ATTRIBUTE_NAME, s)
- continue
-
- resources = []
- for r in s.resources.all():
- rname = r.get_attribute(XDMOD_RESOURCE_ATTRIBUTE_NAME)
- if not rname and r.parent_resource:
- rname = r.parent_resource.get_attribute(
- XDMOD_RESOURCE_ATTRIBUTE_NAME)
-
- if not rname:
- continue
-
- if self.filter_resource and self.filter_resource != rname:
- continue
-
- resources.append(rname)
-
- if len(resources) == 0:
- logger.warn("%s attribute not found on any resouces for allocation: %s",
- XDMOD_RESOURCE_ATTRIBUTE_NAME, s)
- continue
+ resources = self.id_allocation_resources(s)
+ fetcher = XDModFetcher(resources=resources)
try:
- usage = xdmod_fetch_total_storage(
- s.start_date, s.end_date, account_name, resources=resources, statistics='avg_physical_usage')
+ usage = fetcher.xdmod_fetch_storage(
+ account_name, start_date=s.start_date,
+ end_date=s.end_date, statistic='avg_physical_usage'
+ )
except XdmodNotFoundError:
- logger.warn(
- "No data in XDMoD found for allocation %s account %s resources %s", s, account_name, resources)
+ logger.warning(
+ "No data in XDMoD found for allocation %s account %s resources %s",
+ s, account_name, resources)
continue
- logger.warn("Total GB = %s for allocation %s account %s GB %s resources %s",
- usage, s, account_name, cpu_hours, resources)
+ logger.warning(
+ "Total GB = %s for allocation %s account %s GB %s resources %s",
+ usage, s, account_name, cpu_hours, resources)
if self.sync:
s.set_usage(XDMOD_STORAGE_ATTRIBUTE_NAME, usage)
@@ -152,7 +180,6 @@ def process_total_storage(self):
]))
-
def process_total_gpu_hours(self):
header = [
'allocation_id',
@@ -166,78 +193,36 @@ def process_total_gpu_hours(self):
if self.print_header:
self.write('\t'.join(header))
- allocations = Allocation.objects.prefetch_related(
- 'project',
- 'resources',
- 'allocationattribute_set',
- 'allocationuser_set'
- ).filter(
+ allocations = Allocation.objects.filter(
allocationattribute__allocation_attribute_type__name__in=[
XDMOD_ACCOUNT_ATTRIBUTE_NAME,
XDMOD_ACC_HOURS_ATTRIBUTE_NAME,
]
)
-
- if self.fetch_expired:
- allocations = allocations.filter(
- ~Q(status__name='Active'),
- )
- else:
- allocations = allocations.filter(
- status__name='Active',
- )
-
- if self.filter_user:
- allocations = allocations.filter(project__pi__username=self.filter_user)
-
- if self.filter_account:
- allocations = allocations.filter(
- Q(allocationattribute__allocation_attribute_type__name=XDMOD_ACCOUNT_ATTRIBUTE_NAME) &
- Q(allocationattribute__value=self.filter_account)
- )
+ allocations = self.filter_allocations(allocations)
for s in allocations.distinct():
- account_name = s.get_attribute(XDMOD_ACCOUNT_ATTRIBUTE_NAME)
- if not account_name:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_ACCOUNT_ATTRIBUTE_NAME, s)
+ account_name = self.attribute_check(s, XDMOD_ACCOUNT_ATTRIBUTE_NAME)
+ cpu_hours = self.attribute_check(s, XDMOD_ACC_HOURS_ATTRIBUTE_NAME, num=True)
+ if None in [account_name, cpu_hours]:
continue
- cpu_hours = s.get_attribute(XDMOD_ACC_HOURS_ATTRIBUTE_NAME)
- if not cpu_hours:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_ACC_HOURS_ATTRIBUTE_NAME, s)
- continue
-
- resources = []
- for r in s.resources.all():
- rname = r.get_attribute(XDMOD_RESOURCE_ATTRIBUTE_NAME)
- if not rname and r.parent_resource:
- rname = r.parent_resource.get_attribute(
- XDMOD_RESOURCE_ATTRIBUTE_NAME)
-
- if not rname:
- continue
-
- if self.filter_resource and self.filter_resource != rname:
- continue
-
- resources.append(rname)
-
- if len(resources) == 0:
- logger.warn("%s attribute not found on any resouces for allocation: %s",
- XDMOD_RESOURCE_ATTRIBUTE_NAME, s)
- continue
+ resources = self.id_allocation_resources(s)
+ fetcher = XDModFetcher(resources=resources)
try:
- usage = xdmod_fetch_total_cpu_hours(
- s.start_date, s.end_date, account_name, resources=resources, statistics='total_gpu_hours')
+ usage = fetcher.xdmod_fetch_cpu_hours(
+ account_name, start_date=s.start_date, end_date=s.end_date,
+ statistic='total_gpu_hours'
+ )
except XdmodNotFoundError:
- logger.warn(
- "No data in XDMoD found for allocation %s account %s resources %s", s, account_name, resources)
+ logger.warning(
+ "No data in XDMoD found for allocation %s account %s resources %s",
+ s, account_name, resources)
continue
- logger.warn("Total Accelerator hours = %s for allocation %s account %s gpu_hours %s resources %s",
+ logger.warning(
+ "Total Accelerator hours = %s for allocation %s account %s gpu_hours %s resources %s",
usage, s, account_name, cpu_hours, resources)
if self.sync:
s.set_usage(XDMOD_ACC_HOURS_ATTRIBUTE_NAME, usage)
@@ -264,73 +249,67 @@ def process_total_cpu_hours(self):
if self.print_header:
self.write('\t'.join(header))
- allocations = Allocation.objects.prefetch_related(
- 'project',
- 'resources',
- 'allocationattribute_set',
- 'allocationuser_set'
- ).filter(
- status__name='Active',
- ).filter(
+ allocations = Allocation.objects.filter(
allocationattribute__allocation_attribute_type__name__in=[
XDMOD_ACCOUNT_ATTRIBUTE_NAME,
XDMOD_CPU_HOURS_ATTRIBUTE_NAME,
]
)
-
- if self.filter_user:
- allocations = allocations.filter(project__pi__username=self.filter_user)
-
- if self.filter_account:
- allocations = allocations.filter(
- Q(allocationattribute__allocation_attribute_type__name=XDMOD_ACCOUNT_ATTRIBUTE_NAME) &
- Q(allocationattribute__value=self.filter_account)
- )
+ allocations = self.filter_allocations(allocations)
for s in allocations.distinct():
- account_name = s.get_attribute(XDMOD_ACCOUNT_ATTRIBUTE_NAME)
- if not account_name:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_ACCOUNT_ATTRIBUTE_NAME, s)
+ account_name = self.attribute_check(s, XDMOD_ACCOUNT_ATTRIBUTE_NAME)
+ cpu_hours = self.attribute_check(s, XDMOD_CPU_HOURS_ATTRIBUTE_NAME, num=True)
+ if None in [account_name, cpu_hours]:
continue
- cpu_hours = s.get_attribute(XDMOD_CPU_HOURS_ATTRIBUTE_NAME)
- if not cpu_hours:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_CPU_HOURS_ATTRIBUTE_NAME, s)
- continue
-
- resources = []
- for r in s.resources.all():
- rname = r.get_attribute(XDMOD_RESOURCE_ATTRIBUTE_NAME)
- if not rname and r.parent_resource:
- rname = r.parent_resource.get_attribute(
- XDMOD_RESOURCE_ATTRIBUTE_NAME)
-
- if not rname:
- continue
-
- if self.filter_resource and self.filter_resource != rname:
- continue
-
- resources.append(rname)
-
- if len(resources) == 0:
- logger.warn("%s attribute not found on any resouces for allocation: %s",
- XDMOD_RESOURCE_ATTRIBUTE_NAME, s)
- continue
+ resources = self.id_allocation_resources(s)
+ fetcher = XDModFetcher(resources=resources)
try:
- usage = xdmod_fetch_total_cpu_hours(
- s.start_date, s.end_date, account_name, resources=resources)
- except XdmodNotFoundError:
- logger.warn(
- "No data in XDMoD found for allocation %s account %s resources %s", s, account_name, resources)
+ usage = fetcher.xdmod_fetch_cpu_hours(account_name)
+ except XdmodNotFoundError as e:
+ logger.warning(
+ "No data in XDMoD found for allocation %s account %s resources %s: %s",
+ s, account_name, resources, e)
continue
- logger.warn("Total CPU hours = %s for allocation %s account %s cpu_hours %s resources %s",
+ logger.warning(
+ "Total CPU hours = %s for allocation %s account %s cpu_hours %s resources %s",
usage, s, account_name, cpu_hours, resources)
+ # collect user-level usage and update allocationuser entries with them
+ auser_status_active = AllocationUserStatusChoice.objects.get(name='Active')
+
+ usage_data = fetcher.xdmod_fetch_cpu_hours(account_name, group_by='per-user')
+ no_use_allocation_users = s.allocationuser_set.filter(
+ ~Q(user__username__in=usage_data.keys())
+ )
+
+ for user in no_use_allocation_users:
+ user.usage = 0
+ user.save()
+ for username, user_usage in usage_data.items():
+ try:
+ user_obj = get_user_model().objects.get(username=username)
+ except:
+ # if user not present, add to ifx
+ logger.warning("user missing from ifx: %s", username)
+ continue
+ user, created = s.allocationuser_set.get_or_create(
+ user=user_obj,
+ defaults={
+ 'usage': user_usage, 'unit': 'CPU Hours',
+ 'status': auser_status_active
+ }
+ )
+ if not created:
+ user.usage = user_usage
+ user.save()
if self.sync:
+ cpu_hours_attr = s.allocationattribute_set.get(
+ allocation_attribute_type__name=XDMOD_CPU_HOURS_ATTRIBUTE_NAME)
+ cpu_hours_attr.value = usage
+ cpu_hours_attr.save()
s.set_usage(XDMOD_CPU_HOURS_ATTRIBUTE_NAME, usage)
self.write('\t'.join([
@@ -355,72 +334,36 @@ def process_cloud_core_time(self):
if self.print_header:
self.write('\t'.join(header))
- allocations = Allocation.objects.prefetch_related(
- 'project',
- 'resources',
- 'allocationattribute_set',
- 'allocationuser_set'
- ).filter(
- status__name='Active',
- ).filter(
+ allocations = Allocation.objects.filter(
allocationattribute__allocation_attribute_type__name__in=[
XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME,
XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME,
]
)
-
- if self.filter_user:
- allocations = allocations.filter(project__pi__username=self.filter_user)
-
- if self.filter_project:
- allocations = allocations.filter(
- Q(allocationattribute__allocation_attribute_type__name=XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME) &
- Q(allocationattribute__value=self.filter_project)
- )
+ allocations = self.filter_allocations(allocations)
for s in allocations.distinct():
- project_name = s.get_attribute(XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME)
- if not project_name:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME, s)
- continue
-
- core_time = s.get_attribute(XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME)
- if not core_time:
- logger.warn("%s attribute not found for allocation: %s",
- XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME, s)
- continue
-
- resources = []
- for r in s.resources.all():
- rname = r.get_attribute(XDMOD_RESOURCE_ATTRIBUTE_NAME)
- if not rname and r.parent_resource:
- rname = r.parent_resource.get_attribute(
- XDMOD_RESOURCE_ATTRIBUTE_NAME)
-
- if not rname:
- continue
-
- if self.filter_resource and self.filter_resource != rname:
- continue
-
- resources.append(rname)
-
- if len(resources) == 0:
- logger.warn("%s attribute not found on any resouces for allocation: %s",
- XDMOD_RESOURCE_ATTRIBUTE_NAME, s)
+ project_name = self.attribute_check(
+ s, XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME)
+ core_time = self.attribute_check(
+ s, XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME, num=True)
+ if None in [project_name, core_time]:
continue
+ resources = self.id_allocation_resources(s)
+ fetcher = XDModFetcher(resources=resources)
try:
- usage = xdmod_fetch_cloud_core_time(
- s.start_date, s.end_date, project_name, resources=resources)
+ usage = fetcher.xdmod_fetch_cloud_core_time(
+ project_name, start_date=s.start_date, end_date=s.end_date)
except XdmodNotFoundError:
- logger.warn(
- "No data in XDMoD found for allocation %s project %s resources %s", s, project_name, resources)
+ logger.warning(
+ "No data in XDMoD found for allocation %s project %s resources %s",
+ s, project_name, resources)
continue
- logger.warn("Cloud core time = %s for allocation %s project %s core_time %s resources %s",
- usage, s, project_name, core_time, resources)
+ logger.warning(
+ "Cloud core time = %s for allocation %s project %s core_time %s resources %s",
+ usage, s, project_name, core_time, resources)
if self.sync:
s.set_usage(XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME, usage)
@@ -434,50 +377,31 @@ def process_cloud_core_time(self):
]))
def handle(self, *args, **options):
- verbosity = int(options['verbosity'])
- root_logger = logging.getLogger('')
- if verbosity == 0:
- root_logger.setLevel(logging.ERROR)
- elif verbosity == 2:
- root_logger.setLevel(logging.INFO)
- elif verbosity == 3:
- root_logger.setLevel(logging.DEBUG)
- else:
- root_logger.setLevel(logging.WARN)
- self.sync = False
if options['sync']:
self.sync = True
- logger.warn("Syncing ColdFront with XDMoD")
+ logger.warning("Syncing ColdFront with XDMoD")
+
+ filters = {
+ 'username': self.filter_user,
+ 'account': self.filter_account,
+ 'project': self.filter_project,
+ 'resource': self.filter_resource,
+ }
+ for filter_name, filter_attr in filters.items():
+ if options[filter_name]:
+ logger.info("Filtering output by %s: %s", filter_name, options[filter_name])
+ filter_attr = options[filter_name]
+
+ bool_opts = {
+ 'header':self.print_header,
+ 'expired':self.fetch_expired,
+ }
+ for opt, attribute in bool_opts.items():
+ if options[opt]:
+ attribute = True
statistic = 'total_cpu_hours'
- self.filter_user = ''
- self.filter_project = ''
- self.filter_resource = ''
- self.filter_account = ''
- self.print_header = False
- self.fetch_expired = False
-
- if options['username']:
- logger.info("Filtering output by username: %s",
- options['username'])
- self.filter_user = options['username']
- if options['account']:
- logger.info("Filtering output by account: %s", options['account'])
- self.filter_account = options['account']
- if options['project']:
- logger.info("Filtering output by project: %s", options['project'])
- self.filter_project = options['project']
- if options['resource']:
- logger.info("Filtering output by resource: %s",
- options['resource'])
- self.filter_resource = options['resource']
- if options['header']:
- self.print_header = True
-
- if options['expired']:
- self.fetch_expired = True
-
if options['statistic']:
statistic = options['statistic']
@@ -488,7 +412,7 @@ def handle(self, *args, **options):
elif statistic == 'total_acc_hours':
self.process_total_gpu_hours()
elif statistic == 'total_storage':
- self.process_total_storage()
+ self.process_total_storage()
else:
logger.error("Unsupported XDMoD statistic")
sys.exit(1)
diff --git a/coldfront/plugins/xdmod/tests.py b/coldfront/plugins/xdmod/tests.py
new file mode 100644
index 000000000..d81b7faaa
--- /dev/null
+++ b/coldfront/plugins/xdmod/tests.py
@@ -0,0 +1,121 @@
+"""Testing suite for the xdmod plugin."""
+import json
+import xml.etree.ElementTree as ET
+from unittest import mock
+from unittest.mock import patch, Mock
+
+from django.test import TestCase
+
+from coldfront.plugins.xdmod.utils import XDModFetcher, XdmodNotFoundError, XdmodError
+
+XDMOD_XML_SIMPLE = """
+
+CPU Hours: Total: by PI
+
+
+PI
+ smith_lab
+
+
+2023-09-01
+2023-12-01
+
+PI
+CPU Hours: Total
+
+
+
+
+
+smith_lab
+ |
+
+720.1442
+ |
+
+
+"""
+
+def read_in_xml(xml):
+ """Read in the xml."""
+ root = ET.fromstring(xml)
+ return root.find('rows')
+
+
+class XDModFetcherTestCase(TestCase):
+ """Tests for the xdmod plugin."""
+
+ def setUp(self):
+ """Set up the test case."""
+ self.xdmodfetcher = XDModFetcher()
+ # if the return value is valid xml, return the xml
+ mock_response = Mock(text=XDMOD_XML_SIMPLE)
+ mock_response.json.side_effect = json.decoder.JSONDecodeError('JSON decode error', '', 0)
+ self.correct_return_value = mock_response
+
+ @mock.patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_fetch_data(self, mock_get):
+ """Test that the fetcher works."""
+ # if the return value is not xml, raise an exception
+ mock_get.return_value = Mock(text="Hello")
+ with self.assertRaises(XdmodError):
+ self.xdmodfetcher.fetch_data(None)
+ # if the return value is empty xml, raise an exception
+ mock_get.return_value = Mock(text="")
+ with self.assertRaises(XdmodNotFoundError):
+ self.xdmodfetcher.fetch_data(None)
+ mock_get.return_value = self.correct_return_value
+ actual_result = ET.tostring(self.xdmodfetcher.fetch_data(None)).decode()
+ self.assertEqual(actual_result, ET.tostring(read_in_xml(XDMOD_XML_SIMPLE)).decode())
+
+ @mock.patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_fetch_value(self, mock_get):
+ """Test that fetch_value works."""
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.fetch_value(None), '720.1442')
+
+ @mock.patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_fetch_value_error(self, mock_get):
+ """Test that fetch_value raises an exception if the xml is invalid."""
+ mock_get.return_value = Mock(status_code=200, text="no_second_value |
")
+ with self.assertRaises(XdmodError):
+ self.xdmodfetcher.fetch_value(None)
+
+ @mock.patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_fetch_table(self, mock_get):
+ """Test that fetch_table works."""
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.fetch_table(None), {"smith_lab": '720.1442'})
+
+ @mock.patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_fetch_table_error(self, mock_get):
+ """Test that fetch_table raises an exception if the xml is invalid."""
+ mock_get.return_value = Mock(status_code=200, text="no_second_value |
")
+ with self.assertRaises(XdmodError):
+ self.xdmodfetcher.fetch_table(None)
+
+ @mock.patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_xdmod_fetch(self, mock_get):
+ """Test that xdmod_fetch works."""
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.xdmod_fetch("account", "statistic", "realm"), '720.1442')
+
+ @patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_xdmod_fetch_all_project_usages(self, mock_get):
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.xdmod_fetch_all_project_usages("statistic"), {"smith_lab": '720.1442'})
+
+ @patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_xdmod_fetch_cpu_hours(self, mock_get):
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.xdmod_fetch_cpu_hours("account", group_by='total', statistics='total_cpu_hours'), '720.1442')
+
+ @patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_xdmod_fetch_storage(self, mock_get):
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.xdmod_fetch_storage("account", group_by='total', statistic='physical_usage'), 720.1442 / 1E9)
+
+ @patch("coldfront.plugins.xdmod.utils.requests.get")
+ def test_xdmod_fetch_cloud_core_time(self, mock_get):
+ mock_get.return_value = self.correct_return_value
+ self.assertEqual(self.xdmodfetcher.xdmod_fetch_cloud_core_time("project"), '720.1442')
diff --git a/coldfront/plugins/xdmod/utils.py b/coldfront/plugins/xdmod/utils.py
index 85f0c338e..926434a31 100644
--- a/coldfront/plugins/xdmod/utils.py
+++ b/coldfront/plugins/xdmod/utils.py
@@ -1,22 +1,32 @@
import logging
-import requests
import json
import xml.etree.ElementTree as ET
+from datetime import datetime
+
+import requests
from coldfront.core.utils.common import import_from_settings
-XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME = import_from_settings('XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME', 'Cloud Account Name')
-XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME = import_from_settings('XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME', 'Core Usage (Hours)')
+XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_CLOUD_PROJECT_ATTRIBUTE_NAME', 'Cloud Account Name')
+XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_CLOUD_CORE_TIME_ATTRIBUTE_NAME', 'Core Usage (Hours)')
-XDMOD_ACCOUNT_ATTRIBUTE_NAME = import_from_settings('XDMOD_ACCOUNT_ATTRIBUTE_NAME', 'slurm_account_name')
+XDMOD_ACCOUNT_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_ACCOUNT_ATTRIBUTE_NAME', 'slurm_account_name')
-XDMOD_RESOURCE_ATTRIBUTE_NAME = import_from_settings('XDMOD_RESOURCE_ATTRIBUTE_NAME', 'xdmod_resource')
+XDMOD_RESOURCE_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_RESOURCE_ATTRIBUTE_NAME', 'xdmod_resource')
-XDMOD_CPU_HOURS_ATTRIBUTE_NAME = import_from_settings('XDMOD_CPU_HOURS_ATTRIBUTE_NAME', 'Core Usage (Hours)')
-XDMOD_ACC_HOURS_ATTRIBUTE_NAME = import_from_settings('XDMOD_ACC_HOURS_ATTRIBUTE_NAME', 'Accelerator Usage (Hours)')
+XDMOD_CPU_HOURS_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_CPU_HOURS_ATTRIBUTE_NAME', 'Core Usage (Hours)')
+XDMOD_ACC_HOURS_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_ACC_HOURS_ATTRIBUTE_NAME', 'Accelerator Usage (Hours)')
-XDMOD_STORAGE_ATTRIBUTE_NAME = import_from_settings('XDMOD_STORAGE_ATTRIBUTE_NAME', 'Storage Quota (GB)')
-XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME = import_from_settings('XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME', 'Storage_Group_Name')
+XDMOD_STORAGE_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_STORAGE_ATTRIBUTE_NAME', 'Storage Quota (GB)')
+XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME = import_from_settings(
+ 'XDMOD_STORAGE_GROUP_ATTRIBUTE_NAME', 'Storage_Group_Name')
XDMOD_API_URL = import_from_settings('XDMOD_API_URL')
@@ -31,6 +41,15 @@
'query_group': 'tg_usage',
}
+def get_quarter_start_end():
+ y = datetime.today().year
+ quarter_starts = [f'{y}-01-01', f'{y}-04-01', f'{y}-07-01', f'{y}-10-01']
+ quarter_ends = [f'{y}-03-31', f'{y}-06-30', f'{y}-09-30', f'{y}-12-31']
+ quarter = (datetime.today().month-1)//3
+ return (quarter_starts[quarter], quarter_ends[quarter])
+
+QUARTER_START, QUARTER_END = get_quarter_start_end()
+
logger = logging.getLogger(__name__)
class XdmodError(Exception):
@@ -39,147 +58,113 @@ class XdmodError(Exception):
class XdmodNotFoundError(XdmodError):
pass
-def xdmod_fetch_total_cpu_hours(start, end, account, resources=None, statistics='total_cpu_hours'):
- if resources is None:
- resources = []
-
- url = '{}{}'.format(XDMOD_API_URL, _ENDPOINT_CORE_HOURS)
- payload = _DEFAULT_PARAMS
- payload['pi_filter'] = '"{}"'.format(account)
- payload['resource_filter'] = '"{}"'.format(','.join(resources))
- payload['start_date'] = start
- payload['end_date'] = end
- payload['group_by'] = 'pi'
- payload['realm'] = 'Jobs'
- payload['operation'] = 'get_data'
- payload['statistic'] = statistics
- r = requests.get(url, params=payload)
-
- logger.info(r.url)
- logger.info(r.text)
-
- try:
- error = r.json()
- # XXX fix me. Here we assume any json response is bad as we're
- # expecting xml but XDMoD should just return json always.
- raise XdmodNotFoundError('Got json response but expected XML: {}'.format(error))
- except json.decoder.JSONDecodeError as e:
- pass
- except requests.exceptions.JSONDecodeError:
- pass
-
- try:
- root = ET.fromstring(r.text)
- except ET.ParserError as e:
- raise XdmodError('Invalid XML data returned from XDMoD API: {}'.format(e))
-
- rows = root.find('rows')
- if len(rows) != 1:
- raise XdmodNotFoundError('Rows not found for {} - {}'.format(account, resources))
-
- cells = rows.find('row').findall('cell')
- if len(cells) != 2:
- raise XdmodError('Invalid XML data returned from XDMoD API: Cells not found')
-
- core_hours = cells[1].find('value').text
-
- return core_hours
-
-
-
-def xdmod_fetch_total_storage(start, end, account, resources=None, statistics='physical_usage'):
- if resources is None:
- resources = []
-
- payload_end = end
- if payload_end is None:
- payload_end = '2099-01-01'
- url = '{}{}'.format(XDMOD_API_URL, _ENDPOINT_CORE_HOURS)
- payload = _DEFAULT_PARAMS
- payload['pi_filter'] = '"{}"'.format(account)
- payload['resource_filter'] = '{}'.format(','.join(resources))
- payload['start_date'] = start
- payload['end_date'] = payload_end
- payload['group_by'] = 'pi'
- payload['realm'] = 'Storage'
- payload['operation'] = 'get_data'
- payload['statistic'] = statistics
- r = requests.get(url, params=payload)
-
- logger.info(r.url)
- logger.info(r.text)
- if is_json(r.content):
- error = r.json()
- # XXX fix me. Here we assume any json response is bad as we're
- # expecting xml but XDMoD should just return json always.
-
- # print('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
- # print(f'XDMOD synchronization error: ({start}, {end}, {account}, {resources}, response: {r})')
- # print(r.content)
- # print(r.url)
- # print(payload)
- # print('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\n')
-
- raise XdmodNotFoundError('Got json response but expected XML: {}'.format(error))
-
-
- try:
- root = ET.fromstring(r.text)
- except ET.ParserError as e:
- raise XdmodError('Invalid XML data returned from XDMoD API: {}'.format(e))
-
- rows = root.find('rows')
- if len(rows) != 1:
- raise XdmodNotFoundError('Rows not found for {} - {}'.format(account, resources))
-
- cells = rows.find('row').findall('cell')
- if len(cells) != 2:
- raise XdmodError('Invalid XML data returned from XDMoD API: Cells not found')
-
- physical_usage = float(cells[1].find('value').text) / 1E9
-
- return physical_usage
-
-def xdmod_fetch_cloud_core_time(start, end, project, resources=None):
- if resources is None:
- resources = []
-
- url = '{}{}'.format(XDMOD_API_URL, _ENDPOINT_CORE_HOURS)
- payload = _DEFAULT_PARAMS
- payload['project_filter'] = project
- payload['resource_filter'] = '"{}"'.format(','.join(resources))
- payload['start_date'] = start
- payload['end_date'] = end
- payload['group_by'] = 'project'
- payload['realm'] = 'Cloud'
- payload['operation'] = 'get_data'
- payload['statistic'] = 'cloud_core_time'
- r = requests.get(url, params=payload)
-
- logger.info(r.url)
- logger.info(r.text)
-
- try:
- error = r.json()
- # XXX fix me. Here we assume any json response is bad as we're
- # expecting xml but XDMoD should just return json always.
- raise XdmodNotFoundError('Got json response but expected XML: {}'.format(error))
- except json.decoder.JSONDecodeError as e:
- pass
-
- try:
- root = ET.fromstring(r.text)
- except ET.ParserError as e:
- raise XdmodError('Invalid XML data returned from XDMoD API: {}'.format(e))
-
- rows = root.find('rows')
- if len(rows) != 1:
- raise XdmodNotFoundError('Rows not found for {} - {}'.format(project, resources))
-
- cells = rows.find('row').findall('cell')
- if len(cells) != 2:
- raise XdmodError('Invalid XML data returned from XDMoD API: Cells not found')
-
- core_hours = cells[1].find('value').text
-
- return core_hours
+class XDModFetcher:
+ def __init__(self, start=QUARTER_START, end=QUARTER_END, resources=None):
+ self.url = f'{XDMOD_API_URL}{_ENDPOINT_CORE_HOURS}'
+ self.resources = resources
+ payload = _DEFAULT_PARAMS
+ payload['start_date'] = start
+ payload['end_date'] = end
+ if resources:
+ payload['resource_filter'] = f'"{",".join(resources)}"'
+ self.payload = payload
+ self.group_by = {'total':'pi', 'per-user':'person'}
+
+ def fetch_data(self, payload, search_item=None):
+ r = requests.get(self.url, params=payload)
+ logger.info(r.url)
+ logger.info(r.text)
+
+ try:
+ error = r.json()
+ raise XdmodNotFoundError(f'Got json response but expected XML: {error}')
+ except json.decoder.JSONDecodeError as e:
+ pass
+
+ try:
+ root = ET.fromstring(r.text)
+ except ET.ParserError as e:
+ raise XdmodError(f'Invalid XML data returned from XDMoD API: {e}') from e
+
+ rows = root.find('rows')
+ if len(rows) < 1:
+ raise XdmodNotFoundError(
+ f'Rows not found for {search_item} - {self.payload["resource_filter"]}'
+ )
+ return rows
+
+ def fetch_value(self, payload, search_item=None):
+ rows = self.fetch_data(payload, search_item=search_item)
+ cells = rows.find('row').findall('cell')
+ if len(cells) != 2:
+ raise XdmodError('Invalid XML data returned from XDMoD API: Cells not found')
+ stats = cells[1].find('value').text
+ return stats
+
+ def fetch_table(self, payload, search_item=None):
+ """make a dictionary of usernames and their associated core hours from
+ XML data.
+ """
+ # return rows extracted from XML data
+ rows = self.fetch_data(payload, search_item=search_item)
+ # Produce a dict of usernames and their associated core hours from those rows
+ stats = {}
+ for row in rows:
+ cells = row.findall('cell')
+ username = cells[0].find('value').text
+ stats[username] = cells[1].find('value').text
+ return stats
+
+ def xdmod_fetch(self, account, statistic, realm, start_date=None, end_date=None, group_by='total'):
+ """fetch either total or per-user usage stats for specified project"""
+ payload = dict(self.payload)
+ payload['pi_filter'] = f'"{account}"'
+ payload['group_by'] = self.group_by[group_by]
+ payload['statistic'] = statistic
+ payload['realm'] = realm
+ if start_date:
+ payload['start_date'] = start_date
+ if end_date:
+ payload['end_date'] = end_date
+ if group_by == 'total':
+ core_hours = self.fetch_value(payload, search_item=account)
+ elif group_by == 'per-user':
+ core_hours = self.fetch_table(payload, search_item=account)
+ else:
+ raise Exception('unrecognized group_by value')
+ return core_hours
+
+ def xdmod_fetch_all_project_usages(self, statistic):
+ """return usage statistics for all projects"""
+ payload = dict(self.payload)
+ payload['group_by'] = 'pi'
+ payload['realm'] = 'Jobs'
+ payload['statistic'] = statistic
+ stats = self.fetch_table(payload)
+ return stats
+
+ def xdmod_fetch_cpu_hours(self, account, start_date=None, end_date=None, group_by='total', statistic='total_cpu_hours'):
+ """fetch either total or per-user cpu hours"""
+ core_hours = self.xdmod_fetch(account, statistic, 'Jobs', start_date=start_date, end_date=end_date, group_by=group_by)
+ return core_hours
+
+ def xdmod_fetch_storage(self, account, start_date=None, end_date=None, group_by='total', statistic='physical_usage'):
+ """fetch total or per-user storage stats."""
+ stats = self.xdmod_fetch(account, statistic, 'Storage', start_date=start_date, end_date=end_date, group_by=group_by)
+ physical_usage = float(stats) / 1E9
+ return physical_usage
+
+ def xdmod_fetch_cloud_core_time(self, project, start_date=None, end_date=None):
+ """fetch cloud core time."""
+ payload = dict(self.payload)
+ payload['project_filter'] = project
+ payload['group_by'] = 'project'
+ payload['realm'] = 'Cloud'
+ payload['statistic'] = 'cloud_core_time'
+ if start_date:
+ payload['start_date'] = start_date
+ if end_date:
+ payload['end_date'] = end_date
+
+ core_hours = self.fetch_value(payload, search_item=project)
+ return core_hours