Warning, /swf-monitor/scripts/pcs-task-cmd is written in an unsupported language. File is not indexed.
0001 #!/usr/bin/env python3
0002 """
0003 pcs-task-cmd — fetch a PCS ProdTask's submission artifact, or submit it.
0004
0005 Thin HTTP client over swf-monitor's REST API. No Django, no DB access.
0006 The server regenerates from current PCS state on every call, so what
0007 you print here reflects the task + dataset + config as they exist now.
0008
0009 Inspection mode (default; stdlib-only):
0010 pcs-task-cmd <name> --format {condor|panda|jedi|dump}
0011
0012 # condor: bash one-liner (env-prefixed submit_csv.sh)
0013 # panda: prun command
0014 # jedi: JSON taskParamMap for Client.insertTaskParams()
0015 # dump: full JSON view of the task + dataset + tags + config
0016
0017 Submission mode (one-command JEDI submit, requires pandaclient):
0018 pcs-task-cmd <name> --submit
0019
0020 Fetches the taskParamMap, calls pandaclient.Client.insertTaskParams()
0021 using the operator's existing PanDA auth (proxy or OIDC token), then
0022 posts the resulting jediTaskID back to swf-monitor so PCS records the
0023 submission. Prints jediTaskID on success.
0024
0025 Env:
0026 SWFMON_URL base URL, default https://epic-devcloud.org/prod
0027 SWFMON_TOKEN optional DRF token (required if record-submission
0028 endpoint is non-public on this deployment)
0029 """
0030 import argparse
0031 import json
0032 import os
0033 import re
0034 import sys
0035 import urllib.parse
0036 import urllib.request
0037
0038
0039 DEFAULT_URL = os.environ.get('SWFMON_URL', 'https://epic-devcloud.org/prod')
0040
0041
0042 def _swfmon_request(method, url, body=None):
0043 """Make a swf-monitor request with optional token and JSON body."""
0044 data = None
0045 headers = {}
0046 if body is not None:
0047 data = json.dumps(body).encode('utf-8')
0048 headers['Content-Type'] = 'application/json'
0049 token = os.environ.get('SWFMON_TOKEN')
0050 if token:
0051 headers['Authorization'] = f'Token {token}'
0052 req = urllib.request.Request(url, data=data, headers=headers, method=method)
0053 return urllib.request.urlopen(req)
0054
0055
0056 def _fetch_format(base_url, name, fmt):
0057 qs = urllib.parse.urlencode({'name': name, 'fmt': fmt})
0058 url = f'{base_url.rstrip("/")}/pcs/api/prod-tasks/command/?{qs}'
0059 with _swfmon_request('GET', url) as resp:
0060 return resp.read().decode()
0061
0062
0063 def _record_submission(base_url, name, jedi_task_id, status='submitted'):
0064 qs = urllib.parse.urlencode({'name': name})
0065 url = f'{base_url.rstrip("/")}/pcs/api/prod-tasks/record-submission/?{qs}'
0066 with _swfmon_request('POST', url,
0067 {'jedi_task_id': jedi_task_id, 'status': status}) as resp:
0068 return json.loads(resp.read().decode())
0069
0070
0071 def _do_inspect(base_url, name, fmt):
0072 try:
0073 body = _fetch_format(base_url, name, fmt)
0074 except urllib.error.HTTPError as e:
0075 sys.stderr.write(f'HTTP {e.code}: {e.read().decode(errors="replace")}\n')
0076 return 1
0077 except urllib.error.URLError as e:
0078 sys.stderr.write(f'Request failed: {e.reason}\n')
0079 return 1
0080 sys.stdout.write(body)
0081 if sys.stdout.isatty():
0082 sys.stdout.write('\n')
0083 return 0
0084
0085
0086 def _do_submit(base_url, name):
0087 # 1. Fetch taskParamMap from PCS.
0088 try:
0089 params_json = _fetch_format(base_url, name, 'jedi')
0090 except urllib.error.HTTPError as e:
0091 sys.stderr.write(f'PCS HTTP {e.code} fetching taskParamMap: '
0092 f'{e.read().decode(errors="replace")}\n')
0093 return 1
0094 except urllib.error.URLError as e:
0095 sys.stderr.write(f'PCS request failed: {e.reason}\n')
0096 return 1
0097 try:
0098 task_params = json.loads(params_json)
0099 except json.JSONDecodeError as e:
0100 sys.stderr.write(f'Could not parse taskParamMap JSON: {e}\n')
0101 return 1
0102
0103 # A task without a site is placed by JEDI's brokerage after the job
0104 # throttler has chosen what to generate, so the throttler cannot hold
0105 # it off a saturated queue (swf-epicprod EPIC_JOB_THROTTLER.md). PCS
0106 # places every task; one with no site is refused here, as the EVGEN
0107 # submitter refuses it.
0108 if not str(task_params.get('site') or '').strip():
0109 sys.stderr.write(
0110 f'{name}: the task names no site; place it in PCS first '
0111 f'(swf-epicprod CONTINUOUS_PRODUCTION.md, Placement)\n')
0112 return 1
0113
0114 # 2. Lazy-import pandaclient — only required for --submit path.
0115 try:
0116 from pandaclient import Client # noqa: WPS433
0117 except ImportError as e:
0118 sys.stderr.write(
0119 f'pandaclient not importable ({e}). Install panda-client and '
0120 f'ensure your PanDA auth context (proxy or OIDC token) is live.\n'
0121 )
0122 return 1
0123
0124 # 3. Submit to JEDI. Returns (comms_status, (inner_code, message)) on
0125 # comms success; (255, output) on comms failure.
0126 try:
0127 comms_status, result = Client.insertTaskParams(task_params)
0128 except Exception as e:
0129 sys.stderr.write(f'Client.insertTaskParams raised: {e}\n')
0130 return 1
0131 if comms_status != 0:
0132 sys.stderr.write(f'PanDA comms failure (status={comms_status}): {result}\n')
0133 return 1
0134 try:
0135 inner_code, message = result
0136 except (TypeError, ValueError):
0137 sys.stderr.write(f'Unexpected response shape: {result}\n')
0138 return 1
0139 if inner_code != 0:
0140 sys.stderr.write(
0141 f'JEDI rejected submission: code={inner_code} msg={message}\n'
0142 )
0143 return 1
0144
0145 # 4. Parse jediTaskID from message. Server-side message formats
0146 # (panda-server/.../task_event_module.py):
0147 # "succeeded. new jediTaskID=12345"
0148 # "reactivation accepted. jediTaskID=12345 ..."
0149 # "<status>. new tasks params have been set to jediTaskID=12345. "
0150 m = re.search(r'jediTaskID=(\d+)', message)
0151 if not m:
0152 sys.stderr.write(f'Could not parse jediTaskID from message: {message}\n')
0153 return 1
0154 jedi_task_id = int(m.group(1))
0155
0156 # 5. Record back to PCS. If write-back fails we still surface the
0157 # jediTaskID — submission succeeded, only PCS bookkeeping failed.
0158 try:
0159 _record_submission(base_url, name, jedi_task_id)
0160 except urllib.error.HTTPError as e:
0161 sys.stderr.write(
0162 f'Submitted to JEDI as jediTaskID={jedi_task_id}, but PCS '
0163 f'record-submission HTTP {e.code}: '
0164 f'{e.read().decode(errors="replace")}\n'
0165 )
0166 return 2
0167 except urllib.error.URLError as e:
0168 sys.stderr.write(
0169 f'Submitted to JEDI as jediTaskID={jedi_task_id}, but PCS '
0170 f'record-submission request failed: {e.reason}\n'
0171 )
0172 return 2
0173
0174 sys.stdout.write(
0175 f'Submitted: jediTaskID={jedi_task_id} (PCS status=submitted)\n'
0176 f' PanDA message: {message}\n'
0177 )
0178 return 0
0179
0180
0181 def main() -> int:
0182 ap = argparse.ArgumentParser(description=__doc__.strip().splitlines()[0])
0183 ap.add_argument('name', help='ProdTask name')
0184 mode = ap.add_mutually_exclusive_group(required=True)
0185 mode.add_argument('--format', choices=['condor', 'panda', 'jedi', 'dump'],
0186 help='inspection mode: print submission artifact')
0187 mode.add_argument('--submit', action='store_true',
0188 help='submission mode: send taskParamMap to JEDI '
0189 'and record the resulting jediTaskID in PCS')
0190 ap.add_argument('--url', default=DEFAULT_URL,
0191 help=f'swf-monitor base URL (default: {DEFAULT_URL})')
0192 args = ap.parse_args()
0193
0194 if args.submit:
0195 return _do_submit(args.url, args.name)
0196 return _do_inspect(args.url, args.name, args.format)
0197
0198
0199 if __name__ == '__main__':
0200 sys.exit(main())