File indexing completed on 2026-11-09 10:47:30
0001 """Removing datasets from a disk RSE through the monitor
0002 (monitor_app/panda/rse_deletions.py): the preview and confirmation
0003 requests, the guards on confirmation, the operations-role gate, and the
0004 reading of the agent's request files. No table and no database: the
0005 ActiveMQ send and the authority lookup are mocked, and the request files
0006 live in a temporary SWF_TMP_DIR."""
0007 import json
0008 import shutil
0009 import tempfile
0010 import uuid
0011 from datetime import timedelta
0012 from pathlib import Path
0013 from unittest import mock
0014
0015 from django.contrib.auth.models import User
0016 from django.test import SimpleTestCase, override_settings
0017 from django.utils import timezone
0018 from rest_framework.test import APIRequestFactory, force_authenticate
0019
0020 from monitor_app.panda import api as panda_api
0021 from monitor_app.panda import rse_deletions as rd
0022
0023 SEND = 'monitor_app.panda.rse_deletions.ActiveMQConnectionManager'
0024
0025 PLAN = {'rse': 'BNL-XRD', 'pattern': '/RECO/26.03.0/*', 'datasets': [
0026 {'name': '/RECO/26.03.0/a', 'files': 10, 'bytes': 1000, 'eligible': True,
0027 'reason': '', 'tape': 'JLAB-TAPE-SE rule OK, 10/10 files'},
0028 {'name': '/RECO/26.03.0/b', 'files': 10, 'bytes': 1000, 'eligible': False,
0029 'reason': 'tape copy incomplete', 'tape': ''},
0030 ], 'totals': {'datasets': 2, 'eligible': 1, 'refused': 1,
0031 'eligible_files': 10, 'eligible_bytes': 1000}}
0032
0033
0034 class RseDeletionTests(SimpleTestCase):
0035 def setUp(self):
0036 self.tmp = tempfile.mkdtemp()
0037 self.addCleanup(shutil.rmtree, self.tmp)
0038 settings_patch = override_settings(SWF_TMP_DIR=self.tmp)
0039 settings_patch.enable()
0040 self.addCleanup(settings_patch.disable)
0041 patcher = mock.patch(SEND)
0042 self.send = patcher.start().return_value.send_message
0043 self.send.return_value = True
0044 self.addCleanup(patcher.stop)
0045
0046 def _write(self, **fields):
0047 now = timezone.now()
0048 record = dict(id=str(uuid.uuid4()), rse='BNL-XRD', pattern='/RECO/26.03.0/*',
0049 requested_by='op', status='previewed', plan=PLAN,
0050 previewed_at=now.isoformat(), updated_at=now.isoformat())
0051 record.update(fields)
0052 directory = Path(self.tmp) / 'rse-deletions'
0053 directory.mkdir(exist_ok=True)
0054 (directory / f"{record['id']}.json").write_text(json.dumps(record))
0055 return record
0056
0057 def _sent(self):
0058 return json.loads(self.send.call_args[0][1])
0059
0060 def test_preview_queues_the_agent_and_refuses_bad_requests(self):
0061 rec = rd.request_preview(rse='BNL-XRD', pattern='/RECO/26.03.0/*', requested_by='op')
0062 self.assertEqual(rec['status'], 'previewing')
0063 msg = self._sent()
0064 self.assertEqual((msg['phase'], msg['deletion_id'], msg['rse']),
0065 ('preview', rec['id'], 'BNL-XRD'))
0066 for rse, pattern in (('JLAB-TAPE-SE', '/RECO/26.03.0/*'), ('BNL-XRD', '/RECO/*')):
0067 with self.assertRaises(rd.RseDeletionRequestError):
0068 rd.request_preview(rse=rse, pattern=pattern, requested_by='op')
0069
0070 def test_execute_sends_only_eligible_names_after_typed_confirmation(self):
0071 rec = self._write()
0072 with self.assertRaises(rd.RseDeletionRequestError):
0073 rd.request_execute(rec['id'], confirm_rse='EIC-XRD', confirmed_by='op')
0074 self.send.reset_mock()
0075 out = rd.request_execute(rec['id'], confirm_rse='BNL-XRD', confirmed_by='op')
0076 self.assertEqual(out['status'], 'queued')
0077 msg = self._sent()
0078 self.assertEqual((msg['phase'], msg['names'], msg['created_by']),
0079 ('execute', ['/RECO/26.03.0/a'], 'op'))
0080
0081 def test_execute_refuses_a_stale_or_unpreviewed_request(self):
0082 old = (timezone.now() - timedelta(hours=2)).isoformat()
0083 for rec in (self._write(previewed_at=old),
0084 self._write(status='done'),
0085 self._write(plan=dict(PLAN, datasets=PLAN['datasets'][1:]))):
0086 with self.assertRaises(rd.RseDeletionRequestError):
0087 rd.request_execute(rec['id'], confirm_rse='BNL-XRD', confirmed_by='op')
0088 with self.assertRaises(rd.RseDeletionRequestError) as e:
0089 rd.request_execute(str(uuid.uuid4()), confirm_rse='BNL-XRD', confirmed_by='op')
0090 self.assertEqual(e.exception.status, 404)
0091 self.send.assert_not_called()
0092
0093 def test_a_pending_request_the_agent_abandoned_reads_failed(self):
0094 old = (timezone.now() - timedelta(hours=3)).isoformat()
0095 rec = rd.serialize(self._write(status='running', updated_at=old,
0096 result={'datasets': [{'name': 'x', 'outcome': 'removed'}]}))
0097 self.assertEqual(rec['status'], 'failed')
0098 self.assertFalse(rec['pending'])
0099 self.assertEqual(rec['result']['datasets'][0]['outcome'], 'removed')
0100 self.assertEqual(rd.serialize(self._write(status='running'))['status'], 'running')
0101
0102 def test_unreadable_ids_are_not_found(self):
0103 self.assertIsNone(rd.read_request('../../etc/passwd'))
0104 self.assertIsNone(rd.read_request(str(uuid.uuid4())))
0105
0106 def test_only_the_operations_role_acts_on_the_internal_monitor(self):
0107 factory = APIRequestFactory()
0108 body = {'rse': 'BNL-XRD', 'pattern': '/RECO/26.03.0/*'}
0109 for ops, tunnel, expected in ((False, False, 403), (True, True, 403), (True, False, 202)):
0110 request = factory.post('/api/panda/rse-deletions/', body, format='json')
0111 force_authenticate(request, user=User(username='op'))
0112 with mock.patch('monitor_app.authority.get_authority', return_value={}), \
0113 mock.patch('monitor_app.authority.is_ops', return_value=ops), \
0114 mock.patch.object(panda_api, 'is_tunnel_request', return_value=tunnel):
0115 response = panda_api.rse_deletion_request(request)
0116 self.assertEqual(response.status_code, expected, (ops, tunnel))