diff --git a/coldfront/config/email.py b/coldfront/config/email.py
index aee4d5f0d..6d9db2978 100644
--- a/coldfront/config/email.py
+++ b/coldfront/config/email.py
@@ -22,3 +22,4 @@
EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS = ENV.list('EMAIL_ALLOCATION_EXPIRING_NOTIFICATION_DAYS', cast=int, default=[7, 14, 30])
EMAIL_SIGNATURE = ENV.str('EMAIL_SIGNATURE', default='', multiline=True)
EMAIL_ADMINS_ON_ALLOCATION_EXPIRE = ENV.bool('EMAIL_ADMINS_ON_ALLOCATION_EXPIRE', default=False)
+ADMIN_REMINDER_EMAIL = ENV.str('ADMIN_REMINDER_EMAIL', default='')
diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py
index dd0d1875f..90b1ef48b 100644
--- a/coldfront/core/allocation/forms.py
+++ b/coldfront/core/allocation/forms.py
@@ -150,18 +150,7 @@ def clean(self):
[d[:3], d[3:8], d[8:12], d[12:18], d[18:24], d[24:28], d[28:33]]
)
cleaned_expensecode = insert_dashes(replace_productcode(digits_only(expense_code)))
- if 'ifxbilling' in settings.INSTALLED_APPS:
- try:
- matched_fiineaccts = FiineAPI.listAccounts(code=cleaned_expensecode)
- if not matched_fiineaccts:
- self.add_error(
- "expense_code",
- "expense code not found in system - please check the code or get in touch with a system administrator."
- )
- except Exception:
- #Not authorized to use accounts_list
- pass
- cleaned_data['expense_code'] = cleaned_expensecode
+ cleaned_data['expense_code'] = cleaned_expensecode
elif existing_expense_codes and existing_expense_codes != '------':
cleaned_data['expense_code'] = existing_expense_codes
return cleaned_data
diff --git a/coldfront/core/allocation/models.py b/coldfront/core/allocation/models.py
index 805394bf0..53f5b21de 100644
--- a/coldfront/core/allocation/models.py
+++ b/coldfront/core/allocation/models.py
@@ -598,20 +598,29 @@ def clean(self):
expected_value_type = self.allocation_attribute_type.attribute_type.name.strip()
error = None
- if expected_value_type == 'Float' and not isinstance(literal_eval(self.value), (float,int)):
- error = 'Value must be a float.'
- elif expected_value_type == 'Int' and not isinstance(literal_eval(self.value), int):
- error = 'Value must be an integer.'
- elif expected_value_type == 'Yes/No' and self.value not in ['Yes', 'No']:
+ if expected_value_type in ['Float', 'Int']:
+ try:
+ literal_val = literal_eval(self.value)
+ except SyntaxError as exc:
+ error = 'Value must be entirely numeric. Please remove any non-numeric characters.'
+ raise ValidationError(
+ f'Invalid Value "{self.value}" for "{self.allocation_attribute_type.name}". {error}'
+ ) from exc
+ if expected_value_type == 'Float' and not isinstance(literal_val, (float,int)):
+ error = 'Value must be a float.'
+ elif expected_value_type == 'Int' and not isinstance(literal_val, int):
+ error = 'Value must be an integer.'
+ elif expected_value_type == "Yes/No" and self.value not in ["Yes", "No"]:
error = 'Allowed inputs are "Yes" or "No".'
- elif expected_value_type == 'Date':
+ elif expected_value_type == "Date":
try:
datetime.datetime.strptime(self.value.strip(), '%Y-%m-%d')
except ValueError:
error = 'Date must be in format YYYY-MM-DD'
if error:
raise ValidationError(
- 'Invalid Value "%s" for "%s". %s' % (self.value, self.allocation_attribute_type.name, error))
+ f'Invalid Value "{self.value}" for "{self.allocation_attribute_type.name}". {error}'
+ )
def __str__(self):
return str(self.allocation_attribute_type.name)
diff --git a/coldfront/core/allocation/tasks.py b/coldfront/core/allocation/tasks.py
index ac9face92..b83f49c2b 100644
--- a/coldfront/core/allocation/tasks.py
+++ b/coldfront/core/allocation/tasks.py
@@ -31,6 +31,7 @@
EMAIL_ADMINS_ON_ALLOCATION_EXPIRE = import_from_settings('EMAIL_ADMINS_ON_ALLOCATION_EXPIRE')
EMAIL_ADMIN_LIST = import_from_settings('EMAIL_ADMIN_LIST')
+ADMIN_REMINDER_EMAIL = import_from_settings('ADMIN_REMINDER_EMAIL')
def update_statuses():
@@ -69,12 +70,14 @@ def send_request_reminder_emails():
'signature': EMAIL_SIGNATURE,
'url_base': f'{CENTER_BASE_URL.strip("/")}/allocation/change-request/'
}
- send_admin_email_template(
- 'Pending Allocation Changes',
- 'email/pending_allocation_changes.txt',
- allocation_change_template_context,
- )
+ send_email_template(
+ subject='Pending Allocation Changes',
+ template_name='email/pending_allocation_changes.txt',
+ template_context=allocation_change_template_context,
+ sender=EMAIL_SENDER,
+ receiver_list=[ADMIN_REMINDER_EMAIL,],
+ )
# Allocation Requests are allocations marked as "new"
pending_allocations = Allocation.objects.filter(
status__name = 'New', created__lte=req_alert_date
@@ -87,11 +90,15 @@ def send_request_reminder_emails():
'signature': EMAIL_SIGNATURE,
'url_base': f'{CENTER_BASE_URL.strip("/")}/allocation/'
}
- send_admin_email_template(
- 'Pending Allocations',
- 'email/pending_allocations.txt',
- new_allocation_template_context,
+
+ send_email_template(
+ subject='Pending Allocations',
+ template_name='email/pending_allocations.txt',
+ template_context=new_allocation_template_context,
+ sender=EMAIL_SENDER,
+ receiver_list=[ADMIN_REMINDER_EMAIL,],
)
+
# return statement for testing
return (pending_changerequests, pending_allocations)
diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html
index fbf634206..e95d9634b 100644
--- a/coldfront/core/allocation/templates/allocation/allocation_detail.html
+++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html
@@ -106,11 +106,11 @@
Allocation Information
Service Period: |
1 Month |
- {% if offer_letter_code %}
+ {% if expense_code %}
Requested Expense Code: |
- {% for code in offer_letter_code %}
+ {% for code in expense_code %}
{{ code.value }}
{% endfor %}
|
diff --git a/coldfront/core/allocation/tests/test_models.py b/coldfront/core/allocation/tests/test_models.py
index 940e2cb2b..ff7cf2ded 100644
--- a/coldfront/core/allocation/tests/test_models.py
+++ b/coldfront/core/allocation/tests/test_models.py
@@ -1,6 +1,7 @@
"""Unit tests for the allocation models"""
from django.test import TestCase
+from django.core.exceptions import ValidationError
from coldfront.core.test_helpers.factories import setup_models, AllocationFactory
@@ -8,13 +9,14 @@
"coldfront/core/test_helpers/test_data/test_fixtures/ifx.json",
]
+
class AllocationModelTests(TestCase):
"""tests for Allocation model"""
fixtures = UTIL_FIXTURES
@classmethod
def setUpTestData(cls):
- """Set up project to test model properties and methods"""
+ """Set up allocation to test model properties and methods"""
setup_models(cls)
def test_allocation_str(self):
@@ -23,9 +25,9 @@ def test_allocation_str(self):
self.proj_allocation.get_parent_resource.name,
self.proj_allocation.project.pi
)
-
self.assertEqual(str(self.proj_allocation), allocation_str)
+
def test_allocation_usage_property(self):
"""Test that allocation usage property displays correctly"""
self.assertEqual(self.proj_allocation.usage, 10)
@@ -34,3 +36,27 @@ def test_allocation_usage_property_na(self):
"""Create allocation with no usage. Usage property should return None"""
allocation = AllocationFactory()
self.assertIsNone(allocation.usage)
+
+class AllocationAttributeModelTests(TestCase):
+ """Tests for allocationattribute models"""
+ fixtures = UTIL_FIXTURES
+
+ @classmethod
+ def setUpTestData(cls):
+ """Set up allocationattribute to test model properties and methods"""
+ setup_models(cls)
+ cls.allocationattribute = cls.proj_allocation.allocationattribute_set.get(
+ allocation_attribute_type__name='Storage Quota (TB)'
+ )
+
+ def test_allocationattribute_clean_no_error(self):
+ """cleaning a numeric value for an int or float AllocationAttributeType produces no error"""
+ self.allocationattribute.value = "1000"
+ self.allocationattribute.clean()
+
+ def test_allocationattribute_clean_nonnumeric_error(self):
+ """cleaning a non-numeric value for int or float AllocationAttributeTypes returns an informative error message"""
+
+ self.allocationattribute.value = "1000TB"
+ with self.assertRaisesMessage(ValidationError, 'Value must be entirely numeric. Please remove any non-numeric characters.'):
+ self.allocationattribute.clean()
diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py
index e4b7379f7..b76baaeee 100644
--- a/coldfront/core/allocation/views.py
+++ b/coldfront/core/allocation/views.py
@@ -68,6 +68,7 @@
if 'ifxbilling' in settings.INSTALLED_APPS:
+ from fiine.client import API as FiineAPI
from ifxbilling.models import Account, UserProductAccount
if 'django_q' in settings.INSTALLED_APPS:
from django_q.tasks import Task
@@ -560,6 +561,10 @@ def form_valid(self, form):
quantity = form_data.get('quantity', 1)
allocation_account = form_data.get('allocation_account', None)
+ if resource_obj.name == "Tier 3" and quantity % 20 != 0:
+ form.add_error("quantity", format_html("Tier 3 quantity must be a multiple of 20."))
+ return self.form_invalid(form)
+
# A resource is selected that requires an account name selection but user has no account names
if (
ALLOCATION_ACCOUNT_ENABLED
@@ -659,7 +664,19 @@ def form_valid(self, form):
'quantity':quantity,
'nese': nese,
'used_percentage': used_percentage,
+ 'expense_code': expense_code,
+ 'unmatched_code': False,
}
+
+ if 'ifxbilling' in settings.INSTALLED_APPS:
+ try:
+ matched_fiineaccts = FiineAPI.listAccounts(code=expense_code)
+ if not matched_fiineaccts:
+ other_vars['unmatched_code'] = True
+ except Exception:
+ #Not authorized to use accounts_list
+ pass
+
send_allocation_admin_email(
allocation_obj,
'New Allocation Request',
@@ -1800,7 +1817,6 @@ def post(self, request, *args, **kwargs):
if new_value != attribute_change.new_value:
attribute_change.new_value = new_value
attribute_change.save()
-
if action == 'update':
message = 'Allocation change request updated!'
if action == 'approve':
@@ -1986,11 +2002,21 @@ def post(self, request, *args, **kwargs):
if form_data.get('end_date_extension') != 0:
change_requested = True
+
+ # if requested resource is on NESE, add to vars
+ nese = bool(allocation_obj.resources.filter(name__contains="nesetape"))
+
if attrs_to_change:
for entry in formset:
formset_data = entry.cleaned_data
new_value = formset_data.get('new_value')
+ # require nese shares to be divisible by 20
+ tbs = int(new_value) if formset_data['name'] == 'Storage Quota (TB)' else False
+ if nese and tbs and tbs % 20 != 0:
+ messages.error(request, "Tier 3 quantity must be a multiple of 20.")
+ return HttpResponseRedirect(reverse('allocation-change', kwargs={'pk': pk}))
+
if new_value != '':
change_requested = True
allocation_attribute = AllocationAttribute.objects.get(
@@ -2027,17 +2053,19 @@ def post(self, request, *args, **kwargs):
for a in attribute_changes_to_make
if a[0].allocation_attribute_type.name == 'Storage Quota (TB)'
]
- # if requested resource is on NESE, add to vars
- nese = bool(allocation_obj.resources.filter(name__contains="nesetape"))
email_vars = {'justification': justification}
if quantity:
quantity_num = int(float(quantity[0][1]))
difference = quantity_num - int(float(allocation_obj.size))
used_percentage = allocation_obj.get_parent_resource.used_percentage
+ current_size = allocation_obj.size
+ if nese:
+ current_size = round(current_size, -1)
+ difference = round(difference, -1)
email_vars['quantity'] = quantity_num
email_vars['nese'] = nese
- email_vars['current_size'] = allocation_obj.size
+ email_vars['current_size'] = current_size
email_vars['difference'] = difference
email_vars['used_percentage'] = used_percentage
diff --git a/coldfront/core/portal/views.py b/coldfront/core/portal/views.py
index 0db6dac9f..855e7ed80 100644
--- a/coldfront/core/portal/views.py
+++ b/coldfront/core/portal/views.py
@@ -34,7 +34,7 @@ def home(request):
& Q(projectuser__status__name='Active')
)
)
- ).distinct().order_by('-created')[:5]
+ ).distinct().order_by('-created')
allocation_list = Allocation.objects.filter(
Q(status__name__in=['Active', 'New', 'Renewal Requested', ]) &
@@ -44,7 +44,7 @@ def home(request):
(Q(project__projectuser__role__name='Manager') |
Q(allocationuser__user=request.user) &
Q(allocationuser__status__name='Active'))
- ).distinct().order_by('-created')[:5]
+ ).distinct().order_by('-created')
managed_allocations = Allocation.objects.filter(
Q(status__name__in=['Active', 'New', 'Renewal Requested', ])
diff --git a/coldfront/core/project/templates/project/project_detail.html b/coldfront/core/project/templates/project/project_detail.html
index e5ff96484..59849672a 100644
--- a/coldfront/core/project/templates/project/project_detail.html
+++ b/coldfront/core/project/templates/project/project_detail.html
@@ -601,6 +601,8 @@
+
+
@@ -677,34 +679,14 @@ lfrtip',
order: [[ 3, "desc" ]],
- buttons: [
- {
- name: 'primary',
- extend: 'collection',
- background: false,
- autoClose: true,
- text: 'Export',
- buttons: [ 'csv', 'excel', 'pdf' ]
- }
- // {
- // name: 'toggleusers',
- // text: function() {
- // return $('#projectuser_table').attr('filter') == "on" ? 'Show All Users' : "Show Active Users"
- // },
- // action: function(e, dt, node, config) {
- // var table = $('#projectuser_table');
- // var filter = table.attr('filter') === "on" ? 'off' : "on";
- // document.querySelector('#projectuser_table').setAttribute('filter', filter);
- // if (filter == 'on') {
- // $.fn.dataTable.ext.search.push(
- // function(settings, data, dataIndex) {
- // return $(dt.row(dataIndex).node()).attr('status') == "Active";
- // });
- // } else {$.fn.dataTable.ext.search.pop();}
- // table.DataTable().draw();
- // this.text(filter == 'on' ? 'Show All Users' : "Show Active Users")
- // }
- ]
+ buttons: [{
+ name: 'primary',
+ extend: 'collection',
+ background: false,
+ autoClose: true,
+ text: 'Export',
+ buttons: [ 'csv', 'excel', 'pdf' ]
+ }]
});
$('#allocation_history').DataTable({
diff --git a/coldfront/core/resource/management/commands/add_resource_defaults.py b/coldfront/core/resource/management/commands/add_resource_defaults.py
index 8da072dbe..119bbbad9 100644
--- a/coldfront/core/resource/management/commands/add_resource_defaults.py
+++ b/coldfront/core/resource/management/commands/add_resource_defaults.py
@@ -76,6 +76,7 @@ def handle(self, *args, **options):
for name, desc, is_public, rtype, parent_name, default_value in (
('Tier 0', 'Bulk - Lustre', True, storage_tier, None, 1),
('Tier 1', 'Enterprise - Isilon', True, storage_tier, None, 1),
+ ('Tier 2', 'CEPH storage', True, storage_tier, None, 1),
('Tier 3', 'Attic Storage - Tape', True, storage_tier, None, 20),
('holylfs04/tier0', 'Holyoke data center lustre storage', True, storage, 'Tier 0', 1),
('holylfs05/tier0', 'Holyoke data center lustre storage', True, storage, 'Tier 0', 1),
@@ -83,6 +84,18 @@ def handle(self, *args, **options):
('holy-isilon/tier1', 'Tier1 storage with snapshots and disaster recovery copy', True, storage, 'Tier 1', 1),
('bos-isilon/tier1', 'Tier1 storage for on-campus storage mounting', True, storage, 'Tier 1', 1),
('holystore01/tier0', 'Luster storage under Tier0', True, storage, 'Tier 0', 1),
+ ('b-nfs02-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs03-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs04-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs05-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs06-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs07-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs08-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('b-nfs09-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('h-nfs16-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('h-nfs17-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('h-nfs18-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
+ ('h-nfs19-p/tier2', 'Tier2 CEPH storage', True, storage, 'Tier 2', 1),
):
resource_defaults = {
diff --git a/coldfront/core/utils/fasrc.py b/coldfront/core/utils/fasrc.py
index 4ac3aec62..faf4c1b40 100644
--- a/coldfront/core/utils/fasrc.py
+++ b/coldfront/core/utils/fasrc.py
@@ -59,8 +59,10 @@ def select_one_project_allocation(project_obj, resource_obj, dirpath=None):
project_obj
resource_obj
"""
- allocation_query = project_obj.allocation_set.filter(
- resources__id=resource_obj.id)
+ filter_vals = {'resources__id': resource_obj.id}
+ # if dirpath:
+ # filter_vals['allocationattribute__value'] = dirpath
+ allocation_query = project_obj.allocation_set.filter(**filter_vals)
if allocation_query.count() == 1:
allocation_obj = allocation_query.first()
elif allocation_query.count() < 1:
diff --git a/coldfront/core/utils/mail.py b/coldfront/core/utils/mail.py
index 5a653f7d9..977ac188b 100644
--- a/coldfront/core/utils/mail.py
+++ b/coldfront/core/utils/mail.py
@@ -106,11 +106,12 @@ def send_allocation_admin_email(
url_path = reverse('allocation-request-list')
url = build_link(url_path, domain_url=domain_url)
- pi_name = f'{allocation_obj.project.pi.first_name} {allocation_obj.project.pi.last_name} ({allocation_obj.project.pi.username})'
+ pi_name = f'{allocation_obj.project.pi.first_name} {allocation_obj.project.pi.last_name}'
resource_name = allocation_obj.get_parent_resource
ctx = email_template_context()
- ctx['pi'] = pi_name
+ ctx['pi_name'] = pi_name
+ ctx['pi_username'] = f'{allocation_obj.project.pi.username}'
ctx['resource'] = resource_name
ctx['url'] = url
if other_vars:
diff --git a/coldfront/plugins/fasrc/management/commands/id_import_new_allocations.py b/coldfront/plugins/fasrc/management/commands/id_import_new_allocations.py
index c1703a90a..737898bd9 100644
--- a/coldfront/plugins/fasrc/management/commands/id_import_new_allocations.py
+++ b/coldfront/plugins/fasrc/management/commands/id_import_new_allocations.py
@@ -11,18 +11,25 @@
from django.core.management.base import BaseCommand
from coldfront.core.project.models import ProjectStatusChoice
-from coldfront.core.allocation.models import (AllocationUser,
- AllocationAttribute,
- AllocationAttributeType,
- AllocationStatusChoice,
- AllocationUserStatusChoice)
+from coldfront.core.allocation.models import (
+ AllocationUser,
+ AllocationAttribute,
+ AllocationStatusChoice,
+ AllocationAttributeType,
+ AllocationUserStatusChoice,
+)
from coldfront.core.utils.fasrc import update_csv, select_one_project_allocation, save_json
from coldfront.core.resource.models import Resource
-from coldfront.plugins.sftocf.utils import (StarFishRedash,
- STARFISH_SERVER,
- pull_sf_push_cf_redash)
-from coldfront.plugins.fasrc.utils import (AllTheThingsConn,
- match_entries_with_projects, push_quota_data)
+from coldfront.plugins.sftocf.utils import (
+ StarFishRedash,
+ STARFISH_SERVER,
+ pull_sf_push_cf_redash
+)
+from coldfront.plugins.fasrc.utils import (
+ AllTheThingsConn,
+ match_entries_with_projects,
+ push_quota_data
+)
logger = logging.getLogger(__name__)
@@ -52,24 +59,28 @@ def handle(self, *args, **options):
redash_api = StarFishRedash(STARFISH_SERVER)
allocation_usages = redash_api.return_query_results(query='subdirectory')
- subdir_type = AllocationAttributeType.objects.get(name="Subdirectory")
+ subdir_type = AllocationAttributeType.objects.get(name='Subdirectory')
- for lab, allocations in result_json_cleaned.items():
- project = proj_models.get(title=lab)
+ for project in proj_models:
if project.status.name == 'New':
project.status = ProjectStatusChoice.objects.get(name='Active')
project.save()
+
+ for lab, allocations in result_json_cleaned.items():
+ project = proj_models.get(title=lab)
for entry in allocations:
lab_name = entry['lab']
lab_server = entry['server']
lab_path = entry['fs_path'].replace(f'/n/{entry["server"]}/', '')
- resource = Resource.objects.get(name__contains=entry["server"])
- alloc_obj = select_one_project_allocation(project, resource, dirpath=entry['fs_path'])
+ resource = Resource.objects.get(name__contains=entry['server'])
+ alloc_obj = select_one_project_allocation(project, resource, dirpath=lab_path)
if alloc_obj is not None:
continue
- lab_usage_entries = [i for i in allocation_usages if i['vol_name'] == lab_server
- and lab_path in i['path'] and i['group_name'] == lab_name]
+ lab_usage_entries = [
+ i for i in allocation_usages if i['vol_name'] == lab_server
+ and lab_path in i['path'] and i['group_name'] == lab_name
+ ]
if not lab_usage_entries:
continue
@@ -81,27 +92,30 @@ def handle(self, *args, **options):
'start_date': datetime.now(),
'is_changeable': True,
'justification': f'Allocation Information for {lab_name}',
- }
- )
+ }
+ )
# do not modify status of inactive allocations
+ allocation_str = f'{lab_name} {lab_server} {lab_path}'
if created:
allocation.resources.add(resource)
AllocationAttribute.objects.create(
allocation=allocation,
allocation_attribute_type_id=subdir_type.pk,
value=lab_path
- )
- print(f'allocation created: {lab_name}')
+ )
+ print(f'allocation created: {allocation_str}')
allocation.save()
- command_report['allocations_added'].append(f'{lab_name} {lab_server} {lab_path}')
- row = {'project_title': lab_name,
- 'server': lab_server,
- 'path': lab_path,
- 'date': datetime.now()}
+ command_report['allocations_added'].append(allocation_str)
+ row = {
+ 'project_title': lab_name,
+ 'server': lab_server,
+ 'path': lab_path,
+ 'date': datetime.now()
+ }
added_allocations_df = added_allocations_df.append(row, ignore_index=True)
else:
- command_report['allocations_existing'].append(f'{lab_name} {lab_server} {lab_path}')
+ command_report['allocations_existing'].append(allocation_str)
continue
pi_obj = project.pi
try:
@@ -112,7 +126,8 @@ def handle(self, *args, **options):
'status': AllocationUserStatusChoice.objects.get(name='Active')}
)
except ValidationError:
- logger.warning('adding PI %s to allocation %s failed', pi_obj.pi.username, allocation.pk)
+ logger.warning('adding PI %s to allocation %s failed',
+ pi_obj.pi.username, allocation.pk)
created = None
if created:
print('PI added: ' + project.pi.username)
diff --git a/coldfront/plugins/fasrc/utils.py b/coldfront/plugins/fasrc/utils.py
index 28ca4ca7b..1f8666657 100644
--- a/coldfront/plugins/fasrc/utils.py
+++ b/coldfront/plugins/fasrc/utils.py
@@ -27,50 +27,69 @@ def produce_query_statement(self, vol_type, volumes=None):
query_dict = {
'quota': {
- 'match': '[r:HasQuota]-(e:Quota) MATCH (d:ConfigValue {Name: \'Quota.Invocation\'})',
- 'validation_query': 'NOT ((e.SizeGB IS null) OR (e.usedBytes = 0 AND e.SizeGB = 1024)) AND NOT (e.Path IS null)',
+ 'relation': 'HasQuota',
+ 'match': "(e:Quota) MATCH (d:ConfigValue {Name: 'Quota.Invocation'})",
+ 'server': 'filesystem',
+ 'validation_query':
+ "NOT ((e.SizeGB IS null) OR (e.usedBytes = 0 AND e.SizeGB = 1024)) \
+ AND (datetime() - duration('P31D') <= datetime(r.DotsLFSUpdateDate)) \
+ AND NOT (e.Path IS null)",
'r_updated': 'DotsLFSUpdateDate',
- 'storage_type': '\'Quota\'',
+ 'storage_type': 'Quota',
'usedgb': 'usedGB',
'sizebytes': 'limitBytes',
'usedbytes': 'usedBytes',
'fs_path': 'Path',
- 'server': 'filesystem',
'server_replace': '/n/',
'path_replace': '/n//',
'unique':'datetime(e.DotsLFSUpdateDate) as begin_date'
},
'isilon': {
- 'match': '[r:Owns]-(e:IsilonPath) MATCH (d:ConfigValue {Name: \'IsilonPath.Invocation\'})',
- 'validation_query':"r.DotsUpdateDate = d.DotsUpdateDate \
- AND NOT (e.Path =~ '.*/rc_admin/.*')\
- AND (e.Path =~ '.*labs.*')\
- AND NOT (e.SizeGB = 0)",
- 'r_updated': 'DotsUpdateDate',
- 'storage_type':'\'Isilon\'',
+ 'relation': 'Owns',
+ 'match': "(e:IsilonPath) MATCH (d:ConfigValue {Name: 'IsilonPath.Invocation'})",
+ 'server': 'Isilon',
+ 'validation_query': "r.DotsUpdateDate = d.DotsUpdateDate \
+ AND NOT (e.Path =~ '.*/rc_admin/.*')\
+ AND (e.Path =~ '.*labs.*')\
+ AND (datetime() - duration('P31D') <= datetime(r.DotsUpdateDate)) \
+ AND NOT (e.SizeGB = 0)",
'fs_path':'Path',
- 'server':'Isilon',
+ 'r_updated': 'DotsUpdateDate',
+ 'storage_type': 'Isilon',
'usedgb': 'UsedGB',
'sizebytes': 'SizeBytes',
'usedbytes': 'UsedBytes',
'server_replace': '01.rc.fas.harvard.edu',
'path_replace': '/ifs/',
- 'unique':'datetime(e.DotsUpdateDate) as begin_date'
- }
+ 'unique': 'datetime(e.DotsUpdateDate) as begin_date'
+ },
+ 'volume': {
+ 'relation': 'Owns',
+ 'match': '(e:Volume)',
+ 'server': 'Hostname',
+ 'validation_query': 'NOT (e.SizeGB = 0)',
+ 'r_updated': 'DotsLVSUpdateDate',
+ 'storage_type': 'Volume',
+ 'fs_path': 'LogicalVolume',
+ 'path_replace': '/dev/data/',
+ 'usedgb': 'UsedGB',
+ 'sizebytes': 'SizeGB * 1073741824',
+ 'usedbytes': 'UsedGB * 1073741824',
+ 'server_replace': '.rc.fas.harvard.edu',
+ 'unique': 'datetime(e.DotsLVSUpdateDate) as update_date, \
+ datetime(e.DotsLVDisplayUpdateDate) as display_date'
+ },
}
d = query_dict[vol_type]
- if volumes:
- volumes = '|'.join(volumes)
- else:
- volumes = '|'.join([r.name.split('/')[0] for r in Resource.objects.all()])
- where = f"(e.{d['server']} =~ \'.*({volumes}).*\')"
-
+ if not volumes:
+ volumes = [r.name.split('/')[0] for r in Resource.objects.filter(resource_type__name='Storage')]
+ volumes = '|'.join(volumes)
+ where = f"(e.{d['server']} =~ '.*({volumes}).*')"
statement = {
- 'statement': f"MATCH p=(g:Group)-{d['match']} \
+ 'statement': f"MATCH p=(g:Group)-[r:{d['relation']}]-{d['match']} \
WHERE {where} AND {d['validation_query']}\
AND NOT (g.ADSamAccountName =~ '.*(disabled|rc_admin).*')\
- AND (datetime() - duration('P31D') <= datetime(r.{d['r_updated']})) \
RETURN \
{d['unique']}, \
g.ADSamAccountName as lab,\
@@ -79,7 +98,7 @@ def produce_query_statement(self, vol_type, volumes=None):
e.{d['usedbytes']} as byte_usage,\
(e.{d['usedgb']} / 1024.0) as tb_usage,\
replace(e.{d['fs_path']}, '{d['path_replace']}', '') as fs_path, \
- {d['storage_type']} as storage_type, \
+ '{d['storage_type']}' as storage_type, \
datetime(r.{d['r_updated']}) as rel_updated, \
replace(e.{d['server']}, '{d['server_replace']}', '') as server"
}
@@ -89,28 +108,32 @@ def produce_query_statement(self, vol_type, volumes=None):
class QuotaDataPuller:
"""pull and standardize quota data"""
- def pull(self, format):
- standardizer = self.get_standardizer(format)
+ def __init__(self, volumes=None):
+ self.volumes = volumes
+
+ def pull(self, standard):
+ standardizer = self.get_standardizer(standard)
return standardizer()
- def get_standardizer(self, format):
- if format == 'ATTQuery':
+ def get_standardizer(self, standard):
+ if standard == 'ATTQuery':
return self._standardize_attquery
- if format == 'NESEfile':
+ if standard == 'NESEfile':
return self._standardize_nesefile
- raise ValueError(format)
+ raise ValueError(standard)
def _standardize_attquery(self):
- attconn = AllTheThingsConn()
+ attconn = AllTheThingsConn(volumes=self.volumes)
resp_json = attconn.pull_quota_data()
return attconn.format_query_results(resp_json)
def _standardize_nesefile(self):
datafile = 'nese_data/pools'
header_file = 'nese_data/pools.header'
- translator = dict((
- kv.split('=') for kv in (l.strip('\n') for l in open('nese_data/groupkey'))
- ))
+ with open('nese_data/groupkey') as groupkey_file:
+ translator = dict((
+ kv.split('=') for kv in (l.strip('\n') for l in groupkey_file)
+ ))
headers_df = pd.read_csv(header_file, header=0, delim_whitespace=True)
headers = headers_df.columns.values.tolist()
data = pd.read_csv(datafile, names=headers, delim_whitespace=True)
@@ -133,50 +156,44 @@ def _standardize_nesefile(self):
return nesedict
-
class AllTheThingsConn:
- def __init__(self):
+ def __init__(self, volumes=None):
self.url = 'https://allthethings.rc.fas.harvard.edu:7473/db/data/transaction/commit'
self.token = import_from_settings('NEO4JP', '')
self.headers = generate_headers(self.token)
+ self.volumes = volumes
def post_query(self, query):
- resp = requests.post(self.url, headers=self.headers, data=json.dumps(query), verify=False)
+ resp = requests.post(self.url, headers=self.headers, data=json.dumps(query))
return json.loads(resp.text)
def format_query_results(self, resp_json):
result_dicts = list(resp_json['results'])
- return [dict(zip(rdict['columns'],entrydict['row'])) \
- for rdict in result_dicts for entrydict in rdict['data'] ]
+ return [dict(zip(d['columns'], ed['row'])) for d in result_dicts for ed in d['data']]
def stage_user_member_query(self, groupsearch, pi=False):
- match_statement = f'MATCH (u:User)-[r:MemberOf|ManagedBy]-(g:Group) \
- WHERE (g.ADSamAccountName =~ \'{groupsearch}\')'
- return_statement = 'type(r) AS relationship,\
- g.ADManaged_By AS group_manager'
+ match_vars = '(u:User)-[r:MemberOf|ManagedBy]-(g:Group) WHERE'
+ return_vars = 'type(r) AS relationship, g.ADManaged_By AS group_manager'
if pi:
- match_statement = f"MATCH (g:Group) WITH g\
- MATCH (u:User)\
- WHERE (g.ADSamAccountName =~ \'({groupsearch})\') \
- AND u.ADSamAccountName = g.ADManaged_By"
- return_statement = 'u.ADParentCanonicalName AS path, \
- u.ADDepartment AS department, '
+ match_vars = '(g:Group) WITH g MATCH (u:User)\
+ WHERE u.ADSamAccountName = g.ADManaged_By AND'
+ return_vars = 'u.ADParentCanonicalName AS path, u.ADDepartment AS department'
query = {'statements': [{
- 'statement': f'{match_statement} \
- RETURN \
- u.ADgivenName AS first_name, \
- u.ADsurname AS last_name, \
- u.ADSamAccountName AS user_name, \
- u.ADenabled AS user_enabled, \
- g.ADSamAccountName AS group_name,\
- {return_statement} \
- g.ADManaged_By AS group_manager, \
- u.ADgidNumber AS user_gid_number, \
- u.ADTitle AS title, \
- u.ADCompany AS company, \
- g.ADgidNumber AS group_gid_number'
- }]}
+ 'statement': f"MATCH {match_vars} (g.ADSamAccountName =~ '({groupsearch})')\
+ RETURN \
+ u.ADgivenName AS first_name, \
+ u.ADsurname AS last_name, \
+ u.ADSamAccountName AS user_name, \
+ u.ADenabled AS user_enabled, \
+ g.ADSamAccountName AS group_name,\
+ {return_vars},\
+ g.ADManaged_By AS group_manager, \
+ u.ADgidNumber AS user_gid_number, \
+ u.ADTitle AS title, \
+ u.ADCompany AS company, \
+ g.ADgidNumber AS group_gid_number"
+ }]}
resp_json = self.post_query(query)
resp_json_formatted = self.format_query_results(resp_json)
return resp_json_formatted
@@ -188,14 +205,13 @@ def collect_group_membership(self, groupsearch):
resp_json_formatted = self.stage_user_member_query(groupsearch)
return resp_json_formatted
-
def collect_pi_data(self, grouplist):
"""collect information on pis for a given list of groups
"""
resp_json_formatted = self.stage_user_member_query(grouplist, pi=True)
return resp_json_formatted
- def pull_quota_data(self, volumes=None):
+ def pull_quota_data(self):
"""Produce JSON file of quota data for LFS and Isilon from AlltheThings.
Parameters
----------
@@ -203,12 +219,9 @@ def pull_quota_data(self, volumes=None):
"""
logger = logging.getLogger('import_quotas')
query = ATTAllocationQuery()
- if volumes:
- volumes = '|'.join(volumes)
- else:
- volumes = '|'.join([r.name.split('/')[0] for r in Resource.objects.all()])
- query.produce_query_statement('isilon')
- query.produce_query_statement('quota')
+ query.produce_query_statement('isilon', volumes=self.volumes)
+ query.produce_query_statement('quota', volumes=self.volumes)
+ query.produce_query_statement('volume', volumes=self.volumes)
resp_json = self.post_query(query.queries)
logger.debug(resp_json)
return resp_json
@@ -223,6 +236,7 @@ def matched_dict_processing(allocation, data_dicts, paired_allocs, log_message):
logger.warning('too many matches for allocation %s: %s', allocation, data_dicts)
return paired_allocs
+
def pair_allocations_data(project, quota_dicts):
"""pair allocations with usage dicts"""
logger = logging.getLogger('import_quotas')
@@ -253,7 +267,9 @@ def pair_allocations_data(project, quota_dicts):
]
unpaired_dicts = [d for d in unpaired_dicts if d not in paired_allocs.values()]
if unpaired_dicts or unpaired_allocs:
- logger.warning("WARNING: unpaired allocation data: %s %s", unpaired_allocs, unpaired_dicts)
+ logger.warning(
+ "WARNING: unpaired allocation data: %s %s", unpaired_allocs, unpaired_dicts
+ )
return paired_allocs
@@ -311,9 +327,9 @@ def push_quota_data(result_file):
defaults={'value':True}
)
counts['complete'] += 1
- except Exception as e:
+ except Exception as exc:
allocation_name = f"{data_dict['lab']}/{data_dict['server']}"
- errored_allocations[allocation_name] = e
+ errored_allocations[allocation_name] = exc
log_missing('allocation', missing_allocations)
logger.warning('error counts: %s', counts)
logger.warning('errored_allocations:\n%s', errored_allocations)
@@ -330,10 +346,11 @@ def match_entries_with_projects(result_json):
[result_json.pop(t) for t in missing_proj_titles]
return result_json, proj_models
+
def pull_push_quota_data(volumes=None):
logger = logging.getLogger('import_quotas')
- att_data = QuotaDataPuller().pull('ATTQuery')
- nese_data = QuotaDataPuller().pull('NESEfile')
+ att_data = QuotaDataPuller(volumes=volumes).pull('ATTQuery')
+ nese_data = QuotaDataPuller(volumes=volumes).pull('NESEfile')
combined_data = att_data + nese_data
resp_json_by_lab = {entry['lab']:[] for entry in combined_data}
[resp_json_by_lab[e['lab']].append(e) for e in combined_data]
@@ -347,8 +364,5 @@ def pull_push_quota_data(volumes=None):
def generate_headers(token):
"""Generate 'headers' attribute by using the 'token' attribute.
"""
- headers = {
- 'accept': 'application/json',
- 'Authorization': f'Bearer {token}',
- }
+ headers = {'accept': 'application/json', 'Authorization': f'Bearer {token}'}
return headers
diff --git a/coldfront/plugins/ifx/management/commands/processIfxappsMessages.py b/coldfront/plugins/ifx/management/commands/processIfxappsMessages.py
index 2033c6976..3e5737c12 100644
--- a/coldfront/plugins/ifx/management/commands/processIfxappsMessages.py
+++ b/coldfront/plugins/ifx/management/commands/processIfxappsMessages.py
@@ -5,6 +5,7 @@
'''
import logging
from time import sleep
+from django.db import connection
from django.conf import settings
from django.core.management.base import BaseCommand
from ifxbilling.fiine import handle_fiine_ifxapps_messages
@@ -80,3 +81,4 @@ def handle(self, *args, **kwargs):
logger.error(e)
sleep(sleep_seconds)
+ connection.close()
diff --git a/coldfront/plugins/ifx/models.py b/coldfront/plugins/ifx/models.py
index 6da48207a..1c0fd6d82 100644
--- a/coldfront/plugins/ifx/models.py
+++ b/coldfront/plugins/ifx/models.py
@@ -136,12 +136,13 @@ def resource_post_save(sender, instance, **kwargs):
except ProductResource.DoesNotExist:
# Need to create a Product and ProductResource for this Resource
products = FiineAPI.listProducts(product_name=instance.name)
+ facility = Facility.objects.get(name='Research Computing Storage')
if not products:
- facility = Facility.objects.get(name='Research Computing Storage')
product = create_new_product(product_name=instance.name, product_description=instance.name, facility=facility)
else:
fiine_product = products[0].to_dict()
- fiine_product.pop('facility')
+ fiine_product.pop('object_code_category')
+ fiine_product['facility'] = facility
fiine_product['billing_calculator'] = 'coldfront.plugins.ifx.calculator.NewColdfrontBillingCalculator'
(product, created) = Product.objects.get_or_create(**fiine_product)
product_resource = ProductResource.objects.create(product=product, resource=instance)
diff --git a/coldfront/templates/email/allocation_renewed.txt b/coldfront/templates/email/allocation_renewed.txt
index c08a1cf0c..4671c63fa 100644
--- a/coldfront/templates/email/allocation_renewed.txt
+++ b/coldfront/templates/email/allocation_renewed.txt
@@ -1,2 +1,3 @@
-A allocation has been renewed for {{pi}} - {{resource}}. Please activate the allocation:
+A allocation has been renewed for {{pi_name}} ({{pi_username}}) - {{resource}}.
+Please activate the allocation:
{{url}}
diff --git a/coldfront/templates/email/new_allocation_change_request.txt b/coldfront/templates/email/new_allocation_change_request.txt
index 2a2fc1003..2feed9f94 100644
--- a/coldfront/templates/email/new_allocation_change_request.txt
+++ b/coldfront/templates/email/new_allocation_change_request.txt
@@ -1,5 +1,5 @@
{% load mathfilters %}
-An allocation change request for has been made for {{pi}} - {{resource}}.
+An allocation change request for has been made for {{pi_name}} ({{pi_username}}) - {{resource}}.
{% if quantity %}
Requested total size: {{quantity}} TB.
@@ -11,14 +11,14 @@ This will require adding {{difference|floatformat}} TB to the current size of {{
{% endif %}
{% if nese %}
-Here is a draft ticket to send to NESE:
+Here is a draft ticket to send to NESE. Please carefully check the values to ensure their accuracy.
To: help@nese.mghpcc.org
Subject: TB Globus tape setup for HU
-Service (access type) = S3 vs Globus:
-If Globus, short description for Globus Share:
-Tennant name (8 char max - Ex. acohen): {{pi.username}}
+Service (access type) = S3 vs Globus: Globus
+If Globus, short description for Globus Share: {{pi_name}}
+Tennant name (11 char max - Ex. acohen): {{pi_username}}
Size in 20TB allotments: {{quantity|div:19.48|floatformat:"0"}} (This is a difference of {{difference|div:19.48|floatformat:"0"}} 20TB units from the current size of {{current_size|div:19.48|floatformat:"0"}} 20TB units)
Any additional details:
Any additional administrators (if applicable):
diff --git a/coldfront/templates/email/new_allocation_request.txt b/coldfront/templates/email/new_allocation_request.txt
index 1b280e49f..4697a00fe 100644
--- a/coldfront/templates/email/new_allocation_request.txt
+++ b/coldfront/templates/email/new_allocation_request.txt
@@ -1,5 +1,5 @@
{% load mathfilters %}
-A new allocation has been requested for {{pi}} - {{resource}}.
+A new allocation has been requested for {{pi_name}} ({{pi_username}}) - {{resource}}.
Requested size: {{quantity}} TB.
Justification:
{{justification}}
@@ -8,15 +8,21 @@ Justification:
{{resource}} was last recorded as {{used_percentage}}% allocated.
{% endif %}
+{% if unmatched_code %}
+The expense code entered does not match any in the FIINE database.
+Please check it for accuracy and ensure that it is added to FIINE:
+{{expense_code}}
+{% endif %}
+
{% if nese %}
Here is a draft ticket to send to NESE:
To: help@nese.mghpcc.org
Subject: TB Globus tape setup for HU
-Service (access type) = S3 vs Globus:
-If Globus, short description for Globus Share:
-Tennant name (8 char max - Ex. acohen): {{pi.username}}
+Service (access type) = S3 vs Globus: Globus
+If Globus, short description for Globus Share: {{pi_name}}
+Tennant name (8 char max - Ex. acohen): {{pi_username}}
Size in 20TB allotments: {{quantity|div:19.48|floatformat:"0"}}
Any additional details:
Any additional administrators (if applicable):
diff --git a/container_startup.sh b/container_startup.sh
index 6e2f289de..78668d236 100644
--- a/container_startup.sh
+++ b/container_startup.sh
@@ -9,7 +9,7 @@
service redis-server start
python ./manage.py qcluster &
python ./manage.py add_scheduled_tasks
-python ./manage.py collectstatic
+python ./manage.py collectstatic --noinput
# initial_setup does not appear to work as requested.
python ./manage.py initial_setup &