panda-server-source 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panda_server_source-0.8.0.dist-info/METADATA +55 -0
- panda_server_source-0.8.0.dist-info/RECORD +375 -0
- panda_server_source-0.8.0.dist-info/WHEEL +4 -0
- panda_server_source-0.8.0.dist-info/licenses/LICENSE.txt +202 -0
- pandajedi/__init__.py +0 -0
- pandajedi/jedibrokerage/AtlasAnalJobBroker.py +2343 -0
- pandajedi/jedibrokerage/AtlasBrokerUtils.py +1292 -0
- pandajedi/jedibrokerage/AtlasProdJobBroker.py +2064 -0
- pandajedi/jedibrokerage/AtlasProdTaskBroker.py +629 -0
- pandajedi/jedibrokerage/GenJobBroker.py +381 -0
- pandajedi/jedibrokerage/JobBrokerBase.py +133 -0
- pandajedi/jedibrokerage/TaskBrokerBase.py +15 -0
- pandajedi/jedibrokerage/__init__.py +0 -0
- pandajedi/jediconfig/__init__.py +0 -0
- pandajedi/jediconfig/jedi_config.py +40 -0
- pandajedi/jedicore/FactoryBase.py +182 -0
- pandajedi/jedicore/Interaction.py +433 -0
- pandajedi/jedicore/JediDBProxy.py +56 -0
- pandajedi/jedicore/JediDBProxyPool.py +37 -0
- pandajedi/jedicore/JediDBSchemaInfo.py +17 -0
- pandajedi/jedicore/JediException.py +13 -0
- pandajedi/jedicore/JediTaskBuffer.py +984 -0
- pandajedi/jedicore/JediTaskBufferInterface.py +50 -0
- pandajedi/jedicore/MsgWrapper.py +124 -0
- pandajedi/jedicore/ProcessUtils.py +34 -0
- pandajedi/jedicore/SiteCandidate.py +90 -0
- pandajedi/jedicore/ThreadUtils.py +205 -0
- pandajedi/jedicore/__init__.py +0 -0
- pandajedi/jediddm/AtlasDDMClient.py +1708 -0
- pandajedi/jediddm/DDMClientBase.py +16 -0
- pandajedi/jediddm/DDMInterface.py +83 -0
- pandajedi/jediddm/GenDDMClient.py +17 -0
- pandajedi/jediddm/__init__.py +0 -0
- pandajedi/jedidog/AtlasAnalWatchDog.py +648 -0
- pandajedi/jedidog/AtlasDataCarouselWatchDog.py +189 -0
- pandajedi/jedidog/AtlasDataLocalityUpdaterWatchDog.py +215 -0
- pandajedi/jedidog/AtlasProdWatchDog.py +443 -0
- pandajedi/jedidog/AtlasQueueFillerWatchDog.py +800 -0
- pandajedi/jedidog/AtlasTaskWithholderWatchDog.py +245 -0
- pandajedi/jedidog/AtlasWorkflowManagerWatchDog.py +63 -0
- pandajedi/jedidog/GenWatchDog.py +21 -0
- pandajedi/jedidog/JumboWatchDog.py +147 -0
- pandajedi/jedidog/TypicalWatchDogBase.py +122 -0
- pandajedi/jedidog/WatchDogBase.py +53 -0
- pandajedi/jedidog/__init__.py +0 -0
- pandajedi/jediexec/reniceJEDI.py +25 -0
- pandajedi/jedigen/AtlasTaskGenerator.py +110 -0
- pandajedi/jedigen/TaskGeneratorBase.py +15 -0
- pandajedi/jedigen/__init__.py +0 -0
- pandajedi/jedimsgprocessor/__init__.py +0 -0
- pandajedi/jedimsgprocessor/ai_parameter_adjuster_msg_processor +71 -0
- pandajedi/jedimsgprocessor/atlas_idds_msg_processor.py +66 -0
- pandajedi/jedimsgprocessor/base_msg_processor.py +33 -0
- pandajedi/jedimsgprocessor/forwarding_msg_processor.py +29 -0
- pandajedi/jedimsgprocessor/hpo_msg_processor.py +93 -0
- pandajedi/jedimsgprocessor/jedi_contents_feeder_msg_processor.py +63 -0
- pandajedi/jedimsgprocessor/jedi_job_generator_msg_processor.py +185 -0
- pandajedi/jedimsgprocessor/jedi_post_processor_msg_processor.py +82 -0
- pandajedi/jedimsgprocessor/kafka_msg_processor.py +44 -0
- pandajedi/jedimsgprocessor/panda_callback_msg_processor.py +182 -0
- pandajedi/jedimsgprocessor/panda_to_jedi_msg_processor.py +74 -0
- pandajedi/jedimsgprocessor/processing_msg_processor.py +134 -0
- pandajedi/jedimsgprocessor/status_report_msg_processor.py +129 -0
- pandajedi/jedimsgprocessor/tape_carousel_msg_processor.py +118 -0
- pandajedi/jedimsgprocessor/workflow_manager_msg_processor.py +89 -0
- pandajedi/jediorder/ContentsFeeder.py +785 -0
- pandajedi/jediorder/JediDaemon.py +27 -0
- pandajedi/jediorder/JediKnight.py +55 -0
- pandajedi/jediorder/JediMaster.py +238 -0
- pandajedi/jediorder/JediMsgProcessor.py +32 -0
- pandajedi/jediorder/JobBroker.py +33 -0
- pandajedi/jediorder/JobGenerator.py +2763 -0
- pandajedi/jediorder/JobSplitter.py +308 -0
- pandajedi/jediorder/JobThrottler.py +28 -0
- pandajedi/jediorder/PostProcessor.py +166 -0
- pandajedi/jediorder/TaskBroker.py +268 -0
- pandajedi/jediorder/TaskCommando.py +334 -0
- pandajedi/jediorder/TaskGenerator.py +17 -0
- pandajedi/jediorder/TaskRefiner.py +611 -0
- pandajedi/jediorder/TaskSetupper.py +17 -0
- pandajedi/jediorder/WatchDog.py +79 -0
- pandajedi/jediorder/__init__.py +0 -0
- pandajedi/jedipprocess/AtlasAnalPostProcessor.py +404 -0
- pandajedi/jedipprocess/AtlasPostProcessorUtils.py +50 -0
- pandajedi/jedipprocess/AtlasProdPostProcessor.py +178 -0
- pandajedi/jedipprocess/GenPostProcessor.py +35 -0
- pandajedi/jedipprocess/MailTemplates.py +185 -0
- pandajedi/jedipprocess/PostProcessorBase.py +269 -0
- pandajedi/jedipprocess/__init__.py +0 -0
- pandajedi/jedirefine/AtlasAnalTaskRefiner.py +259 -0
- pandajedi/jedirefine/AtlasProdTaskRefiner.py +162 -0
- pandajedi/jedirefine/GenTaskRefiner.py +61 -0
- pandajedi/jedirefine/RefinerUtils.py +154 -0
- pandajedi/jedirefine/TaskRefinerBase.py +996 -0
- pandajedi/jedirefine/__init__.py +0 -0
- pandajedi/jedisetup/AtlasTaskSetupper.py +309 -0
- pandajedi/jedisetup/GenTaskSetupper.py +16 -0
- pandajedi/jedisetup/SimpleTaskSetupper.py +152 -0
- pandajedi/jedisetup/TaskSetupperBase.py +15 -0
- pandajedi/jedisetup/__init__.py +0 -0
- pandajedi/jeditest/SchemaChecker.py +41 -0
- pandajedi/jeditest/addContWoSoTask.py +54 -0
- pandajedi/jeditest/addDataCarouselRequest.py +47 -0
- pandajedi/jeditest/addEventServiceTask.py +101 -0
- pandajedi/jeditest/addHpoTask.py +81 -0
- pandajedi/jeditest/addIDDSStageInRequest.py +56 -0
- pandajedi/jeditest/addNonAtlasTask.py +71 -0
- pandajedi/jeditest/addNonAtlasTaskEnc.py +64 -0
- pandajedi/jeditest/addTestTask.py +124 -0
- pandajedi/jeditest/addTestTaskParamToDEFT.py +70 -0
- pandajedi/jeditest/addTestTaskWithMerger.py +97 -0
- pandajedi/jeditest/brokerTest.py +78 -0
- pandajedi/jeditest/cancelDataCarouselRequest.py +35 -0
- pandajedi/jeditest/contentsFeederTest.py +28 -0
- pandajedi/jeditest/contentsFeederTestWithTask.py +18 -0
- pandajedi/jeditest/ddmTest.py +4 -0
- pandajedi/jeditest/jobGenForOneTask.py +38 -0
- pandajedi/jeditest/jobGeneratorTest.py +43 -0
- pandajedi/jeditest/jobSplitterTest.py +104 -0
- pandajedi/jeditest/jobThrottlerTest.py +22 -0
- pandajedi/jeditest/msgProcessorTest.py +9 -0
- pandajedi/jeditest/postProcessorTest.py +18 -0
- pandajedi/jeditest/resubmitDataCarouselRequest.py +35 -0
- pandajedi/jeditest/resubmitTaskParam.py +20 -0
- pandajedi/jeditest/taskBrokerTest.py +22 -0
- pandajedi/jeditest/taskBrokerTestWithTask.py +38 -0
- pandajedi/jeditest/taskBufferTest.py +5 -0
- pandajedi/jeditest/taskCommandoTest.py +28 -0
- pandajedi/jeditest/taskRefinerTest.py +30 -0
- pandajedi/jeditest/testOneTaskToRefine.py +49 -0
- pandajedi/jeditest/testOneTaskToTaskBroker.py +42 -0
- pandajedi/jeditest/testPandaDDMMap.py +62 -0
- pandajedi/jeditest/updateDataCarouselRequestParams.py +50 -0
- pandajedi/jeditest/watchDogTest.py +25 -0
- pandajedi/jeditest/workQueueTest.py +22 -0
- pandajedi/jedithrottle/AtlasAnalJobThrottler.py +19 -0
- pandajedi/jedithrottle/AtlasProdJobThrottler.py +18 -0
- pandajedi/jedithrottle/GenJobThrottler.py +26 -0
- pandajedi/jedithrottle/JobThrottlerBase.py +496 -0
- pandajedi/jedithrottle/__init__.py +0 -0
- pandaserver/__init__.py +0 -0
- pandaserver/api/__init__.py +0 -0
- pandaserver/api/v1/__init__.py +0 -0
- pandaserver/api/v1/common.py +339 -0
- pandaserver/api/v1/credential_management_api.py +372 -0
- pandaserver/api/v1/data_carousel_api.py +264 -0
- pandaserver/api/v1/event_api.py +269 -0
- pandaserver/api/v1/file_server_api.py +942 -0
- pandaserver/api/v1/harvester_api.py +445 -0
- pandaserver/api/v1/http_client.py +166 -0
- pandaserver/api/v1/idds_api.py +180 -0
- pandaserver/api/v1/job_api.py +463 -0
- pandaserver/api/v1/metaconfig_api.py +122 -0
- pandaserver/api/v1/pilot_api.py +895 -0
- pandaserver/api/v1/statistics_api.py +207 -0
- pandaserver/api/v1/system_api.py +186 -0
- pandaserver/api/v1/task_api.py +1520 -0
- pandaserver/api/v1/tests/__init__.py +0 -0
- pandaserver/api/v1/tests/credential_management_api_tests.py +116 -0
- pandaserver/api/v1/tests/data_carousel_api_tests.py +82 -0
- pandaserver/api/v1/tests/harvester_api_tests.py +188 -0
- pandaserver/api/v1/tests/idds_api_tests.py +45 -0
- pandaserver/api/v1/tests/job_api_tests.py +112 -0
- pandaserver/api/v1/tests/pilot_api_tests.py +154 -0
- pandaserver/api/v1/tests/statistics_api_tests.py +104 -0
- pandaserver/api/v1/tests/system_api_tests.py +58 -0
- pandaserver/api/v1/tests/task_api_tests.py +590 -0
- pandaserver/api/v1/version.py +2 -0
- pandaserver/api/v1/workflow_api.py +136 -0
- pandaserver/brokerage/SiteMapper.py +406 -0
- pandaserver/brokerage/__init__.py +0 -0
- pandaserver/brokerage/broker.py +272 -0
- pandaserver/config/__init__.py +0 -0
- pandaserver/config/config_utils.py +15 -0
- pandaserver/config/daemon_config.py +29 -0
- pandaserver/config/mcp_config.py +38 -0
- pandaserver/config/panda_config.py +233 -0
- pandaserver/configurator/Carbon.py +65 -0
- pandaserver/configurator/Configurator.py +908 -0
- pandaserver/configurator/__init__.py +0 -0
- pandaserver/configurator/aux.py +73 -0
- pandaserver/daemons/__init__.py +0 -0
- pandaserver/daemons/master.py +110 -0
- pandaserver/daemons/master_systemd.py +79 -0
- pandaserver/daemons/scripts/__init__.py +0 -0
- pandaserver/daemons/scripts/add_main.py +264 -0
- pandaserver/daemons/scripts/add_sub.py +221 -0
- pandaserver/daemons/scripts/cache_pilots.py +47 -0
- pandaserver/daemons/scripts/cache_schedconfig.py +286 -0
- pandaserver/daemons/scripts/carbon.py +50 -0
- pandaserver/daemons/scripts/configurator.py +94 -0
- pandaserver/daemons/scripts/copyArchive.py +1387 -0
- pandaserver/daemons/scripts/datasetManager.py +1154 -0
- pandaserver/daemons/scripts/dummy_test.py +25 -0
- pandaserver/daemons/scripts/evpPD2P.py +144 -0
- pandaserver/daemons/scripts/hs_scrapers.py +264 -0
- pandaserver/daemons/scripts/metric_collector.py +927 -0
- pandaserver/daemons/scripts/panda_activeusers_query.py +80 -0
- pandaserver/daemons/scripts/pilotStreaming.py +109 -0
- pandaserver/daemons/scripts/process_workflow_files_daemon.py +166 -0
- pandaserver/daemons/scripts/recover_lost_files_daemon.py +177 -0
- pandaserver/daemons/scripts/task_evaluator.py +405 -0
- pandaserver/daemons/scripts/tmpwatch.py +70 -0
- pandaserver/daemons/scripts/worker_synchronization.py +97 -0
- pandaserver/daemons/utils.py +753 -0
- pandaserver/dataservice/DataServiceUtils.py +323 -0
- pandaserver/dataservice/ErrorCode.py +25 -0
- pandaserver/dataservice/RecoverLostFilesCore.py +322 -0
- pandaserver/dataservice/__init__.py +0 -0
- pandaserver/dataservice/activator.py +79 -0
- pandaserver/dataservice/adder_atlas_plugin.py +1106 -0
- pandaserver/dataservice/adder_dummy_plugin.py +31 -0
- pandaserver/dataservice/adder_gen.py +954 -0
- pandaserver/dataservice/adder_plugin_base.py +27 -0
- pandaserver/dataservice/adder_result.py +75 -0
- pandaserver/dataservice/adder_simple_plugin.py +160 -0
- pandaserver/dataservice/closer.py +247 -0
- pandaserver/dataservice/closer_atlas_plugin.py +90 -0
- pandaserver/dataservice/ddm.py +1381 -0
- pandaserver/dataservice/ddm_handler.py +95 -0
- pandaserver/dataservice/dyn_data_distributer.py +824 -0
- pandaserver/dataservice/event_lookup_client_ei.py +77 -0
- pandaserver/dataservice/event_picker.py +268 -0
- pandaserver/dataservice/finisher.py +194 -0
- pandaserver/dataservice/setupper.py +225 -0
- pandaserver/dataservice/setupper_atlas_plugin.py +1851 -0
- pandaserver/dataservice/setupper_dummy_plugin.py +47 -0
- pandaserver/dataservice/setupper_plugin_base.py +67 -0
- pandaserver/jobdispatcher/DispatcherUtils.py +35 -0
- pandaserver/jobdispatcher/ErrorCode.py +19 -0
- pandaserver/jobdispatcher/JobDispatcher.py +1625 -0
- pandaserver/jobdispatcher/Protocol.py +443 -0
- pandaserver/jobdispatcher/Watcher.py +186 -0
- pandaserver/jobdispatcher/__init__.py +0 -0
- pandaserver/pandamcp/__init__.py +0 -0
- pandaserver/pandamcp/mcp_main.py +56 -0
- pandaserver/pandamcp/mcp_test_client.py +95 -0
- pandaserver/pandamcp/mcp_utils.py +74 -0
- pandaserver/proxycache/__init__.py +0 -0
- pandaserver/proxycache/panda_proxy_cache.py +240 -0
- pandaserver/proxycache/token_cache.py +141 -0
- pandaserver/server/.gacl +6 -0
- pandaserver/server/panda.py +539 -0
- pandaserver/srvcore/CoreUtils.py +572 -0
- pandaserver/srvcore/MailUtils.py +106 -0
- pandaserver/srvcore/__init__.py +0 -0
- pandaserver/srvcore/allowed_methods.py +100 -0
- pandaserver/srvcore/oidc_utils.py +152 -0
- pandaserver/srvcore/panda_request.py +196 -0
- pandaserver/srvcore/srv_msg_utils.py +42 -0
- pandaserver/taskbuffer/ConBridge.py +513 -0
- pandaserver/taskbuffer/DBProxyPool.py +95 -0
- pandaserver/taskbuffer/DataCarousel.py +3053 -0
- pandaserver/taskbuffer/DatasetSpec.py +137 -0
- pandaserver/taskbuffer/DdmSpec.py +201 -0
- pandaserver/taskbuffer/ErrorCode.py +87 -0
- pandaserver/taskbuffer/EventServiceUtils.py +396 -0
- pandaserver/taskbuffer/FileSpec.py +290 -0
- pandaserver/taskbuffer/GlobalShares.py +244 -0
- pandaserver/taskbuffer/HarvesterMetricsSpec.py +92 -0
- pandaserver/taskbuffer/Initializer.py +82 -0
- pandaserver/taskbuffer/InputChunk.py +1080 -0
- pandaserver/taskbuffer/JediCacheSpec.py +57 -0
- pandaserver/taskbuffer/JediDatasetSpec.py +588 -0
- pandaserver/taskbuffer/JediFileSpec.py +242 -0
- pandaserver/taskbuffer/JediTaskSpec.py +1687 -0
- pandaserver/taskbuffer/JobSpec.py +1006 -0
- pandaserver/taskbuffer/JobUtils.py +232 -0
- pandaserver/taskbuffer/NucleusSpec.py +116 -0
- pandaserver/taskbuffer/OraDBProxy.py +174 -0
- pandaserver/taskbuffer/PanDAMsgProcessor.py +8 -0
- pandaserver/taskbuffer/PandaDBSchemaInfo.py +18 -0
- pandaserver/taskbuffer/ParseJobXML.py +350 -0
- pandaserver/taskbuffer/PickleFileSpec.py +18 -0
- pandaserver/taskbuffer/PickleJobSpec.py +25 -0
- pandaserver/taskbuffer/PrioUtil.py +40 -0
- pandaserver/taskbuffer/ProcessGroups.py +111 -0
- pandaserver/taskbuffer/ResourceSpec.py +175 -0
- pandaserver/taskbuffer/SQLDumper.py +37 -0
- pandaserver/taskbuffer/SQLManager.py +60 -0
- pandaserver/taskbuffer/SiteSpec.py +304 -0
- pandaserver/taskbuffer/SupErrors.py +10 -0
- pandaserver/taskbuffer/TaskBuffer.py +2882 -0
- pandaserver/taskbuffer/TaskBufferInterface.py +163 -0
- pandaserver/taskbuffer/Utils.py +848 -0
- pandaserver/taskbuffer/WorkQueue.py +227 -0
- pandaserver/taskbuffer/WorkQueueMapper.py +203 -0
- pandaserver/taskbuffer/WorkerSpec.py +143 -0
- pandaserver/taskbuffer/WrappedCursor.py +487 -0
- pandaserver/taskbuffer/WrappedPickle.py +88 -0
- pandaserver/taskbuffer/WrappedPostgresConn.py +24 -0
- pandaserver/taskbuffer/__init__.py +0 -0
- pandaserver/taskbuffer/db_proxy_mods/OraDBProxy_structure.png +0 -0
- pandaserver/taskbuffer/db_proxy_mods/__init__.py +7 -0
- pandaserver/taskbuffer/db_proxy_mods/base_module.py +841 -0
- pandaserver/taskbuffer/db_proxy_mods/data_carousel_module.py +772 -0
- pandaserver/taskbuffer/db_proxy_mods/entity_module.py +3804 -0
- pandaserver/taskbuffer/db_proxy_mods/job_complex_module.py +5661 -0
- pandaserver/taskbuffer/db_proxy_mods/job_standalone_module.py +2640 -0
- pandaserver/taskbuffer/db_proxy_mods/metrics_module.py +874 -0
- pandaserver/taskbuffer/db_proxy_mods/misc_standalone_module.py +4272 -0
- pandaserver/taskbuffer/db_proxy_mods/task_complex_module.py +6172 -0
- pandaserver/taskbuffer/db_proxy_mods/task_event_module.py +4953 -0
- pandaserver/taskbuffer/db_proxy_mods/task_standalone_module.py +4999 -0
- pandaserver/taskbuffer/db_proxy_mods/task_utils_module.py +2063 -0
- pandaserver/taskbuffer/db_proxy_mods/worker_module.py +1944 -0
- pandaserver/taskbuffer/db_proxy_mods/workflow_module.py +832 -0
- pandaserver/taskbuffer/retryModule.py +552 -0
- pandaserver/taskbuffer/task_split_rules.py +308 -0
- pandaserver/taskbuffer/workflow_processor.py +249 -0
- pandaserver/taskbuffer/wrapped_oracle_conn.py +18 -0
- pandaserver/test/SchemaChecker.py +40 -0
- pandaserver/test/__init__.py +0 -0
- pandaserver/test/banUser.py +43 -0
- pandaserver/test/boostPrio.py +19 -0
- pandaserver/test/boostUser.py +35 -0
- pandaserver/test/callbackDDM.py +11 -0
- pandaserver/test/daod_on_demand.py +95 -0
- pandaserver/test/finishJob.py +184 -0
- pandaserver/test/finishTaskJEDI.py +11 -0
- pandaserver/test/getJobs.py +53 -0
- pandaserver/test/killJob.py +79 -0
- pandaserver/test/killJobLowPrio.py +99 -0
- pandaserver/test/killJobsInTask.py +56 -0
- pandaserver/test/killTask.py +80 -0
- pandaserver/test/killTaskJEDI.py +11 -0
- pandaserver/test/killUser.py +83 -0
- pandaserver/test/lsst/README.txt +7 -0
- pandaserver/test/lsst/lsstSubmit.py +139 -0
- pandaserver/test/lsst/lsstSubmitMERGEtest.py +141 -0
- pandaserver/test/lsst/lsstSubmitPhosim332.py +53 -0
- pandaserver/test/lsst/prepare-client-tarball-from-bigpanda-server.sh +41 -0
- pandaserver/test/reassignJobs.py +17 -0
- pandaserver/test/reassignSite.py +82 -0
- pandaserver/test/reassignTask.py +117 -0
- pandaserver/test/reassignWaiting.py +39 -0
- pandaserver/test/recoverLostFiles.py +7 -0
- pandaserver/test/reloadInputDS.py +51 -0
- pandaserver/test/sendCommandToJob.py +26 -0
- pandaserver/test/setDebugMode.py +38 -0
- pandaserver/test/setPriority.py +28 -0
- pandaserver/test/testEvgen.py +60 -0
- pandaserver/test/testEvgen17.py +58 -0
- pandaserver/test/testG4sim.py +81 -0
- pandaserver/test/testG4sim17.py +90 -0
- pandaserver/test/testGlobalShares.py +131 -0
- pandaserver/test/testReco.py +103 -0
- pandaserver/test/testSimulReco14.py +98 -0
- pandaserver/test/testSiteMap.py +26 -0
- pandaserver/test/testUpdateWorkerPilotStatus.py +9 -0
- pandaserver/userinterface/Client.py +1686 -0
- pandaserver/userinterface/UserIF.py +1799 -0
- pandaserver/userinterface/__init__.py +0 -0
- pandaserver/workflow/__init__.py +0 -0
- pandaserver/workflow/data_handler_plugins/base_data_handler.py +48 -0
- pandaserver/workflow/data_handler_plugins/ddm_collection_data_handler.py +100 -0
- pandaserver/workflow/data_handler_plugins/panda_task_data_handler.py +140 -0
- pandaserver/workflow/pcwl_test.py +122 -0
- pandaserver/workflow/pcwl_utils.py +384 -0
- pandaserver/workflow/psnakemake_container.json +68 -0
- pandaserver/workflow/psnakemake_task.json +62 -0
- pandaserver/workflow/psnakemake_test.py +60 -0
- pandaserver/workflow/snakeparser/__init__.py +3 -0
- pandaserver/workflow/snakeparser/extensions.py +74 -0
- pandaserver/workflow/snakeparser/log.py +53 -0
- pandaserver/workflow/snakeparser/names.py +3 -0
- pandaserver/workflow/snakeparser/parser.py +332 -0
- pandaserver/workflow/snakeparser/utils.py +29 -0
- pandaserver/workflow/step_handler_plugins/base_step_handler.py +85 -0
- pandaserver/workflow/step_handler_plugins/panda_task_step_handler.py +280 -0
- pandaserver/workflow/workflow_base.py +531 -0
- pandaserver/workflow/workflow_core.py +1974 -0
- pandaserver/workflow/workflow_core_smoketest.py +388 -0
- pandaserver/workflow/workflow_parser.py +222 -0
- pandaserver/workflow/workflow_utils.py +1047 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from fastmcp import Client
|
|
5
|
+
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
6
|
+
|
|
7
|
+
# argparse setup
|
|
8
|
+
parser = argparse.ArgumentParser(description="MCP test client")
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
"--transport",
|
|
11
|
+
type=str,
|
|
12
|
+
choices=("streamable-http", "sse"),
|
|
13
|
+
default="streamable-http",
|
|
14
|
+
help="Transport to use: streamable-http or sse (default: streamable-http)",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("--host", type=str, default="localhost", help="PanDA Server host (default: localhost)")
|
|
17
|
+
parser.add_argument("--port", type=int, default=25443, help="PanDA Server port (default: 25443)")
|
|
18
|
+
parser.add_argument("--use_http", action=argparse.BooleanOptionalAction, default=False, help="Use HTTP instead of HTTPS (default: False)")
|
|
19
|
+
parser.add_argument("--token", type=str, default=None, help="OIDC ID token for write-operations (default: None)")
|
|
20
|
+
parser.add_argument("--vo", type=str, default=None, help="Virtual organization with ID token is given (default: None)")
|
|
21
|
+
parser.add_argument("--tool", type=str, default="is_alive", help="A tool name to test (default: is_alive)")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--kv",
|
|
24
|
+
dest="kv",
|
|
25
|
+
metavar="KEY=VAL",
|
|
26
|
+
action="append",
|
|
27
|
+
help="Arguments to invoke the test function. Key=Value pairs repeat for multiple (e.g. --kv a=1 --kv b=2 --kv c=True)",
|
|
28
|
+
)
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
# parse --kv KEY=VAL (repeatable) into a dict stored back on args.kv
|
|
32
|
+
raw_kv = args.kv or []
|
|
33
|
+
kv = {}
|
|
34
|
+
for item in raw_kv:
|
|
35
|
+
if "=" not in item:
|
|
36
|
+
parser.error(f"invalid --kv value {item!r}, expected KEY=VALUE")
|
|
37
|
+
k, v = item.split("=", 1)
|
|
38
|
+
v = v.strip()
|
|
39
|
+
if v == "True":
|
|
40
|
+
v = True
|
|
41
|
+
elif v == "False":
|
|
42
|
+
v = False
|
|
43
|
+
kv[k] = v
|
|
44
|
+
args.kv = kv
|
|
45
|
+
|
|
46
|
+
# construct base URL
|
|
47
|
+
if args.use_http:
|
|
48
|
+
base_url = f"http://{args.host}:{args.port}/mcp/"
|
|
49
|
+
else:
|
|
50
|
+
base_url = f"https://{args.host}:{args.port}/mcp/"
|
|
51
|
+
|
|
52
|
+
headers = {"Origin": args.vo} if args.token else None
|
|
53
|
+
|
|
54
|
+
# select transport
|
|
55
|
+
if args.transport == "streamable-http":
|
|
56
|
+
transport = StreamableHttpTransport(url=base_url, auth=args.token, headers=headers)
|
|
57
|
+
else:
|
|
58
|
+
transport = SSETransport(url=base_url, auth=args.token, headers=headers)
|
|
59
|
+
|
|
60
|
+
# create client
|
|
61
|
+
client = Client(transport)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# test function
|
|
65
|
+
async def cl():
|
|
66
|
+
# Connection is established here
|
|
67
|
+
async with client:
|
|
68
|
+
if client.is_connected():
|
|
69
|
+
print("Client connected")
|
|
70
|
+
else:
|
|
71
|
+
print("Client failed to connect")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Make MCP calls within the context
|
|
75
|
+
tools = await client.list_tools()
|
|
76
|
+
print(f"\nAvailable tools:")
|
|
77
|
+
for tool in tools:
|
|
78
|
+
print(f"- {tool.name} -\n")
|
|
79
|
+
print(f" Description: {tool.description}")
|
|
80
|
+
|
|
81
|
+
print("\n" * 2)
|
|
82
|
+
print(f"Testing {args.tool}:")
|
|
83
|
+
result = await client.call_tool(args.tool, args.kv)
|
|
84
|
+
print(f"Result: {result}\n")
|
|
85
|
+
|
|
86
|
+
# Connection is closed automatically here
|
|
87
|
+
if not client.is_connected():
|
|
88
|
+
print("Client disconnected")
|
|
89
|
+
else:
|
|
90
|
+
print("Client still connected")
|
|
91
|
+
print("Done")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
asyncio.run(cl())
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os.path
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
6
|
+
from fastmcp.tools.tool import Tool
|
|
7
|
+
|
|
8
|
+
from pandaserver.api.v1.http_client import HttpClient, api_url, api_url_ssl
|
|
9
|
+
from pandaserver.srvcore.panda_request import PandaRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_tool(func: Callable) -> Tool:
|
|
13
|
+
"""
|
|
14
|
+
Create an MCP tool that wraps the API call.
|
|
15
|
+
|
|
16
|
+
:param func: The function to wrap. It should be a callable that takes PandaRequest as the first argument.
|
|
17
|
+
:return: An MCP tool.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# construct the URL based on the module and function name
|
|
21
|
+
mod_path = inspect.getfile(inspect.getmodule(func))
|
|
22
|
+
mod_name = os.path.basename(mod_path).split("_")[0]
|
|
23
|
+
|
|
24
|
+
# determine http method based on the docstring
|
|
25
|
+
http_method = None
|
|
26
|
+
for line in func.__doc__.splitlines():
|
|
27
|
+
line = line.strip()
|
|
28
|
+
if line.startswith("HTTP Method:"):
|
|
29
|
+
http_method = line.split(":")[1].strip().lower()
|
|
30
|
+
if http_method not in ["get", "post"]:
|
|
31
|
+
raise ValueError(f"Unsupported HTTP method: {http_method}")
|
|
32
|
+
break
|
|
33
|
+
if http_method is None:
|
|
34
|
+
raise ValueError("HTTP Method not specified in the function docstring")
|
|
35
|
+
|
|
36
|
+
if http_method == "get":
|
|
37
|
+
url = f"{api_url}/{mod_name}/{func.__name__}"
|
|
38
|
+
else:
|
|
39
|
+
url = f"{api_url_ssl}/{mod_name}/{func.__name__}"
|
|
40
|
+
|
|
41
|
+
# remove PandaRequest from the signature and annotations
|
|
42
|
+
sig = inspect.signature(func)
|
|
43
|
+
params = []
|
|
44
|
+
for p in sig.parameters.values():
|
|
45
|
+
if p.annotation != PandaRequest:
|
|
46
|
+
params.append(p)
|
|
47
|
+
|
|
48
|
+
annotations = {}
|
|
49
|
+
for k, v in func.__annotations__.items():
|
|
50
|
+
if v != PandaRequest:
|
|
51
|
+
annotations[k] = v
|
|
52
|
+
|
|
53
|
+
# create a new function that wraps the API call
|
|
54
|
+
def wrapped_func(**kwarg):
|
|
55
|
+
nonlocal url, http_method
|
|
56
|
+
# extract the id_token and auth_vo from the headers
|
|
57
|
+
original_headers = get_http_headers()
|
|
58
|
+
id_token = original_headers.get("authorization")
|
|
59
|
+
if id_token and id_token.startswith("Bearer "):
|
|
60
|
+
id_token = original_headers["authorization"].split(" ")[1]
|
|
61
|
+
auth_vo = original_headers.get("origin")
|
|
62
|
+
oidc = id_token is not None
|
|
63
|
+
|
|
64
|
+
# set the id_token and auth_vo in the HttpClient
|
|
65
|
+
http_client = HttpClient()
|
|
66
|
+
http_client.override_oidc(oidc, id_token, auth_vo)
|
|
67
|
+
status, output = getattr(http_client, http_method)(url, kwarg)
|
|
68
|
+
return output
|
|
69
|
+
|
|
70
|
+
# set the signature and annotations to the wrapped function to align with the original API call
|
|
71
|
+
wrapped_func.__signature__ = sig.replace(parameters=params)
|
|
72
|
+
wrapped_func.__annotations__ = annotations
|
|
73
|
+
|
|
74
|
+
return Tool.from_function(wrapped_func, name=func.__name__, description=func.__doc__)
|
|
File without changes
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from pandacommon.pandalogger.LogWrapper import LogWrapper
|
|
8
|
+
from pandacommon.pandalogger.PandaLogger import PandaLogger
|
|
9
|
+
|
|
10
|
+
_logger = PandaLogger().getLogger("ProxyCache")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute(program, log_stream):
|
|
14
|
+
"""Run a program on the command line. Return stderr, stdout and status."""
|
|
15
|
+
log_stream.info(f"executable: {program}")
|
|
16
|
+
pipe = subprocess.Popen(
|
|
17
|
+
program,
|
|
18
|
+
bufsize=-1,
|
|
19
|
+
shell=True,
|
|
20
|
+
close_fds=False,
|
|
21
|
+
cwd="/tmp",
|
|
22
|
+
stdout=subprocess.PIPE,
|
|
23
|
+
stderr=subprocess.PIPE,
|
|
24
|
+
)
|
|
25
|
+
stdout, stderr = pipe.communicate()
|
|
26
|
+
return stdout, stderr, pipe.wait()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cat(filename):
|
|
30
|
+
"""Given filename, print its text contents."""
|
|
31
|
+
with open(filename, "r") as f:
|
|
32
|
+
out = f.read()
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MyProxyInterface(object):
|
|
37
|
+
"""Class to store and retrieve proxies from my proxies."""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.__target_path = "/tmp/proxies"
|
|
41
|
+
self.__cred_name = "panda"
|
|
42
|
+
if not os.path.exists(self.__target_path):
|
|
43
|
+
os.makedirs(self.__target_path)
|
|
44
|
+
|
|
45
|
+
def store(
|
|
46
|
+
self,
|
|
47
|
+
user_dn,
|
|
48
|
+
cred_name,
|
|
49
|
+
production=False,
|
|
50
|
+
server_name="myproxy.cern.ch",
|
|
51
|
+
role=None,
|
|
52
|
+
log_stream=None,
|
|
53
|
+
):
|
|
54
|
+
log_stream.info("store proxy")
|
|
55
|
+
|
|
56
|
+
# Retrieve proxy from myproxy
|
|
57
|
+
proxy_path = os.path.join(
|
|
58
|
+
self.__target_path,
|
|
59
|
+
hashlib.sha1(f"{user_dn}.plain".encode("utf-8")).hexdigest(),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# check if empty dummy file
|
|
63
|
+
if os.path.exists(proxy_path) and os.stat(proxy_path).st_size == 0:
|
|
64
|
+
if datetime.datetime.now(datetime.timezone.utc) - datetime.datetime.fromtimestamp(
|
|
65
|
+
os.path.getctime(proxy_path), datetime.timezone.utc
|
|
66
|
+
) < datetime.timedelta(hours=1):
|
|
67
|
+
log_stream.info(f"skip too early to try again according to timestamp of {proxy_path}")
|
|
68
|
+
return 2
|
|
69
|
+
cmd = f"myproxy-logon -s {server_name} --no_passphrase --out {proxy_path} -l '{user_dn}' -k {cred_name} -t 0"
|
|
70
|
+
|
|
71
|
+
stdout, stderr, status = execute(cmd, log_stream)
|
|
72
|
+
if stdout:
|
|
73
|
+
log_stream.info(f"stdout is {stdout} ")
|
|
74
|
+
if stderr:
|
|
75
|
+
log_stream.info(f"stderr is {stderr} ")
|
|
76
|
+
# make a dummy to avoid too early reattempt
|
|
77
|
+
open(proxy_path, "w").close()
|
|
78
|
+
log_stream.info(f"test the status of plain... {status}")
|
|
79
|
+
if status == 1:
|
|
80
|
+
return status
|
|
81
|
+
|
|
82
|
+
if role is not None:
|
|
83
|
+
log_stream.info(f"proxy needs {role} - need to add voms attributes and store it in the cache")
|
|
84
|
+
tmpExtension = self.getExtension(role)
|
|
85
|
+
prodproxy_path = os.path.join(
|
|
86
|
+
self.__target_path,
|
|
87
|
+
str(hashlib.sha1((user_dn + tmpExtension).encode("utf-8")).hexdigest()),
|
|
88
|
+
)
|
|
89
|
+
prodcmd = f"voms-proxy-init -vomses /etc/vomses -valid 96:00 -rfc -cert {proxy_path} -key {proxy_path} -out {prodproxy_path} -n -voms {role}"
|
|
90
|
+
stdout, stderr, status = execute(prodcmd, log_stream)
|
|
91
|
+
if stdout:
|
|
92
|
+
log_stream.info(f"stdout is {stdout} ")
|
|
93
|
+
if stderr:
|
|
94
|
+
log_stream.info(f"stderr is {stderr} ")
|
|
95
|
+
log_stream.debug(f"test the status of production... {status}")
|
|
96
|
+
elif production:
|
|
97
|
+
log_stream.info("production proxy needed - need to add voms attributes and store it in the cache")
|
|
98
|
+
prodproxy_path = os.path.join(
|
|
99
|
+
self.__target_path,
|
|
100
|
+
str(hashlib.sha1(f"{user_dn}.prod".encode("utf-8")).hexdigest()),
|
|
101
|
+
)
|
|
102
|
+
log_stream.info(prodproxy_path)
|
|
103
|
+
prodcmd = f"voms-proxy-init -vomses /etc/vomses -valid 96:00 -rfc -cert {proxy_path} -key {proxy_path} -out {prodproxy_path} -n -voms atlas:/atlas/Role=production"
|
|
104
|
+
stdout, stderr, status = execute(prodcmd, log_stream)
|
|
105
|
+
if stdout:
|
|
106
|
+
log_stream.info(f"stdout is {stdout} ")
|
|
107
|
+
if stderr:
|
|
108
|
+
log_stream.info(f"stderr is {stderr} ")
|
|
109
|
+
log_stream.info(f"test the status of production... {status}")
|
|
110
|
+
else:
|
|
111
|
+
# Now we need to add atlas roles and store it
|
|
112
|
+
atlasproxy_path = os.path.join(self.__target_path, hashlib.sha1(user_dn.encode("utf-8")).hexdigest())
|
|
113
|
+
atlasrolescmd = f"voms-proxy-init -vomses /etc/vomses -valid 96:00 -rfc -cert {proxy_path} -key {proxy_path} -out {atlasproxy_path} -n -voms atlas"
|
|
114
|
+
stdout, stderr, status = execute(atlasrolescmd, log_stream)
|
|
115
|
+
if stdout:
|
|
116
|
+
log_stream.info(f"stdout is {stdout} ")
|
|
117
|
+
if stderr:
|
|
118
|
+
log_stream.info(f"stderr is {stderr} ")
|
|
119
|
+
log_stream.info(f"test the status of atlas... {status}")
|
|
120
|
+
# make dummy to avoid too early attempts
|
|
121
|
+
if status != 0 and not os.path.exists(proxy_path):
|
|
122
|
+
open(proxy_path, "w").close()
|
|
123
|
+
return status
|
|
124
|
+
|
|
125
|
+
def retrieve(self, user_dn, production=False, role=None):
|
|
126
|
+
"""Retrieve proxy from proxy cache."""
|
|
127
|
+
if role is not None:
|
|
128
|
+
tmpExtension = self.getExtension(role)
|
|
129
|
+
proxy_path = os.path.join(
|
|
130
|
+
self.__target_path,
|
|
131
|
+
str(hashlib.sha1((user_dn + tmpExtension).encode("utf-8")).hexdigest()),
|
|
132
|
+
)
|
|
133
|
+
elif production:
|
|
134
|
+
proxy_path = os.path.join(
|
|
135
|
+
self.__target_path,
|
|
136
|
+
str(hashlib.sha1(f"{user_dn}.prod".encode("utf-8")).hexdigest()),
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
proxy_path = os.path.join(self.__target_path, hashlib.sha1(user_dn.encode("utf-8")).hexdigest())
|
|
140
|
+
if os.path.isfile(proxy_path):
|
|
141
|
+
return cat(proxy_path)
|
|
142
|
+
else:
|
|
143
|
+
_logger.warning(f"proxy file does not exist : DN:{user_dn} role:{role} file:{proxy_path}")
|
|
144
|
+
|
|
145
|
+
# get proxy path
|
|
146
|
+
def get_proxy_path(self, user_dn, production, role):
|
|
147
|
+
if role is not None:
|
|
148
|
+
tmpExtension = self.getExtension(role)
|
|
149
|
+
return os.path.join(
|
|
150
|
+
self.__target_path,
|
|
151
|
+
str(hashlib.sha1((user_dn + tmpExtension).encode("utf-8")).hexdigest()),
|
|
152
|
+
)
|
|
153
|
+
elif production:
|
|
154
|
+
return os.path.join(
|
|
155
|
+
self.__target_path,
|
|
156
|
+
str(hashlib.sha1(f"{user_dn}.prod".encode("utf-8")).hexdigest()),
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
return os.path.join(
|
|
160
|
+
self.__target_path,
|
|
161
|
+
hashlib.sha1(user_dn.encode("utf-8")).hexdigest(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def checkProxy(self, user_dn, production=False, role=None, name=None):
|
|
165
|
+
log_stream = LogWrapper(_logger, f'< name="{name}" role={role} >')
|
|
166
|
+
log_stream.info(f"check proxy for {user_dn}")
|
|
167
|
+
|
|
168
|
+
# Check the validity of a proxy
|
|
169
|
+
proxy_path = self.get_proxy_path(user_dn, production, role)
|
|
170
|
+
is_ok = False
|
|
171
|
+
if os.path.isfile(proxy_path):
|
|
172
|
+
log_stream.info("proxy is there. Need to check validity")
|
|
173
|
+
cmd = f"voms-proxy-info -exists -hours 94 -file {proxy_path}"
|
|
174
|
+
stdout, stderr, status = execute(cmd, log_stream)
|
|
175
|
+
if stdout:
|
|
176
|
+
log_stream.info(f"stdout is {stdout} ")
|
|
177
|
+
if stderr:
|
|
178
|
+
log_stream.info(f"stderr is {stderr} ")
|
|
179
|
+
if status == 1:
|
|
180
|
+
log_stream.info("proxy expires in 94h or less. We need to renew proxy!")
|
|
181
|
+
ret = self.store(
|
|
182
|
+
user_dn,
|
|
183
|
+
self.__cred_name,
|
|
184
|
+
production,
|
|
185
|
+
role=role,
|
|
186
|
+
log_stream=log_stream,
|
|
187
|
+
)
|
|
188
|
+
if ret == 0:
|
|
189
|
+
log_stream.info("proxy retrieval successful")
|
|
190
|
+
# copy with compact name
|
|
191
|
+
alt_proxy_path = self.get_proxy_path(name, production, role)
|
|
192
|
+
shutil.copyfile(proxy_path, alt_proxy_path)
|
|
193
|
+
is_ok = True
|
|
194
|
+
elif ret == 2:
|
|
195
|
+
log_stream.info("proxy retrieval on hold")
|
|
196
|
+
else:
|
|
197
|
+
log_stream.error("proxy retrieval failed")
|
|
198
|
+
else:
|
|
199
|
+
log_stream.info("proxy is valid for more than 3 days")
|
|
200
|
+
is_ok = True
|
|
201
|
+
else:
|
|
202
|
+
log_stream.info("proxy is not in the cache repo. will try to get it from myproxy")
|
|
203
|
+
ret = self.store(user_dn, self.__cred_name, production, role=role, log_stream=log_stream)
|
|
204
|
+
if ret == 0:
|
|
205
|
+
log_stream.info("proxy stored successfully")
|
|
206
|
+
alt_proxy_path = self.get_proxy_path(name, production, role)
|
|
207
|
+
shutil.copyfile(proxy_path, alt_proxy_path)
|
|
208
|
+
is_ok = True
|
|
209
|
+
elif ret == 2:
|
|
210
|
+
log_stream.info("proxy retrieval on hold")
|
|
211
|
+
else:
|
|
212
|
+
log_stream.error("proxy retrieval failed")
|
|
213
|
+
if is_ok:
|
|
214
|
+
plain_path = os.path.join(
|
|
215
|
+
self.__target_path,
|
|
216
|
+
hashlib.sha1(f"{user_dn}.plain".encode("utf-8")).hexdigest(),
|
|
217
|
+
)
|
|
218
|
+
if os.path.isfile(plain_path):
|
|
219
|
+
return self.checkValidity(plain_path, log_stream)
|
|
220
|
+
else:
|
|
221
|
+
log_stream.error("plain proxy not there at the moment!")
|
|
222
|
+
|
|
223
|
+
def checkValidity(self, proxy_path, log_stream):
|
|
224
|
+
log_stream.info("Need to check validity and expiry!")
|
|
225
|
+
time_left_thresholds = [24, 94, 168]
|
|
226
|
+
status = 0
|
|
227
|
+
for threshold in time_left_thresholds:
|
|
228
|
+
cmd = f"voms-proxy-info -exists -hours {threshold} -file {proxy_path}"
|
|
229
|
+
stdout, stderr, status = execute(cmd, log_stream)
|
|
230
|
+
if status == 1:
|
|
231
|
+
log_stream.warning(f"proxy expires in {threshold} hours")
|
|
232
|
+
return threshold
|
|
233
|
+
|
|
234
|
+
return status
|
|
235
|
+
|
|
236
|
+
# get extension
|
|
237
|
+
def getExtension(self, role):
|
|
238
|
+
if role is not None:
|
|
239
|
+
return "." + role.split("=")[-1]
|
|
240
|
+
return None
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
download access tokens for OIDC token exchange flow
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
import os.path
|
|
8
|
+
import pathlib
|
|
9
|
+
|
|
10
|
+
from pandacommon.pandalogger.LogWrapper import LogWrapper
|
|
11
|
+
from pandacommon.pandalogger.PandaLogger import PandaLogger
|
|
12
|
+
from pandacommon.pandautils.PandaUtils import naive_utcnow
|
|
13
|
+
|
|
14
|
+
from pandaserver.config import panda_config
|
|
15
|
+
from pandaserver.srvcore.oidc_utils import get_access_token
|
|
16
|
+
|
|
17
|
+
# logger
|
|
18
|
+
_logger = PandaLogger().getLogger("token_cache")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TokenCache:
|
|
22
|
+
"""
|
|
23
|
+
A class used to download and give access tokens for OIDC token exchange flow
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# constructor
|
|
28
|
+
def __init__(self, target_path: str = None, file_prefix: str = None, refresh_interval: int = 60, task_buffer=None):
|
|
29
|
+
"""
|
|
30
|
+
Constructs all the necessary attributes for the TokenCache object.
|
|
31
|
+
|
|
32
|
+
:param target_path: The base path to store the access tokens
|
|
33
|
+
:param file_prefix: The prefix of the access token files
|
|
34
|
+
:param refresh_interval: The interval to refresh the access tokens (default is 60 minutes)
|
|
35
|
+
:param task_buffer: TaskBuffer object
|
|
36
|
+
"""
|
|
37
|
+
if target_path:
|
|
38
|
+
self.target_path = target_path
|
|
39
|
+
else:
|
|
40
|
+
self.target_path = "/tmp/proxies"
|
|
41
|
+
if not os.path.exists(self.target_path):
|
|
42
|
+
os.makedirs(self.target_path)
|
|
43
|
+
if file_prefix:
|
|
44
|
+
self.file_prefix = file_prefix
|
|
45
|
+
else:
|
|
46
|
+
self.file_prefix = "access_token_"
|
|
47
|
+
self.refresh_interval = refresh_interval
|
|
48
|
+
self.task_buffer = task_buffer
|
|
49
|
+
# cache for access tokens
|
|
50
|
+
self.cached_access_tokens = {}
|
|
51
|
+
|
|
52
|
+
# construct target path
|
|
53
|
+
def construct_target_path(self, client_name: str) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Constructs the target path to store an access token
|
|
56
|
+
|
|
57
|
+
:param client_name: client name
|
|
58
|
+
:return: the target path
|
|
59
|
+
"""
|
|
60
|
+
return os.path.join(self.target_path, f"{self.file_prefix}{client_name}")
|
|
61
|
+
|
|
62
|
+
# main
|
|
63
|
+
def run(self):
|
|
64
|
+
""" "
|
|
65
|
+
Main function to download access tokens
|
|
66
|
+
"""
|
|
67
|
+
tmp_log = LogWrapper(_logger)
|
|
68
|
+
tmp_log.debug("================= start ==================")
|
|
69
|
+
try:
|
|
70
|
+
# check config
|
|
71
|
+
if not hasattr(panda_config, "token_cache_config") or not panda_config.token_cache_config:
|
|
72
|
+
tmp_log.debug("token_cache_config is not set in panda_config")
|
|
73
|
+
# check config path
|
|
74
|
+
elif not os.path.exists(panda_config.token_cache_config):
|
|
75
|
+
tmp_log.debug(f"config file {panda_config.token_cache_config} not found")
|
|
76
|
+
# read config
|
|
77
|
+
else:
|
|
78
|
+
with open(panda_config.token_cache_config) as f:
|
|
79
|
+
token_cache_config = json.load(f)
|
|
80
|
+
for client_name, client_config in token_cache_config.items():
|
|
81
|
+
tmp_log.debug(f"client_name={client_name}")
|
|
82
|
+
# token file path
|
|
83
|
+
token_file_path = client_config.get("token_file_path")
|
|
84
|
+
if not token_file_path:
|
|
85
|
+
token_file_path = self.construct_target_path(client_name)
|
|
86
|
+
# check if fresh
|
|
87
|
+
is_fresh = False
|
|
88
|
+
if os.path.exists(token_file_path):
|
|
89
|
+
mod_time = datetime.datetime.fromtimestamp(os.stat(token_file_path).st_mtime, datetime.timezone.utc)
|
|
90
|
+
if datetime.datetime.now(datetime.timezone.utc) - mod_time < datetime.timedelta(minutes=self.refresh_interval):
|
|
91
|
+
tmp_log.debug(f"skip since {token_file_path} is fresh")
|
|
92
|
+
is_fresh = True
|
|
93
|
+
# get access token
|
|
94
|
+
if not is_fresh:
|
|
95
|
+
status_code, output = get_access_token(
|
|
96
|
+
client_config["endpoint"], client_config["client_id"], client_config["secret"], client_config.get("scope")
|
|
97
|
+
)
|
|
98
|
+
if status_code:
|
|
99
|
+
with open(token_file_path, "w") as f:
|
|
100
|
+
f.write(output)
|
|
101
|
+
tmp_log.debug(f"dump access token to {token_file_path}")
|
|
102
|
+
else:
|
|
103
|
+
tmp_log.error(output)
|
|
104
|
+
# touch file to avoid immediate reattempt
|
|
105
|
+
pathlib.Path(token_file_path).touch()
|
|
106
|
+
tmp_log.debug(f"touch {token_file_path} to avoid immediate reattempt")
|
|
107
|
+
# register token keys
|
|
108
|
+
if client_config.get("use_token_key") is True and self.task_buffer is not None:
|
|
109
|
+
token_key_lifetime = client_config.get("token_key_lifetime", 96)
|
|
110
|
+
tmp_log.debug(f"register token key for {client_name}")
|
|
111
|
+
tmp_stat = self.task_buffer.register_token_key(client_name, token_key_lifetime)
|
|
112
|
+
if not tmp_stat:
|
|
113
|
+
tmp_log.error("failed")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
tmp_log.error(f"failed with {str(e)}")
|
|
116
|
+
tmp_log.debug("================= end ==================")
|
|
117
|
+
tmp_log.debug("done")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# get access token for a client
|
|
121
|
+
def get_access_token(self, client_name: str) -> str | None:
|
|
122
|
+
"""
|
|
123
|
+
Get an access token string for a client. None is returned if the access token is not found
|
|
124
|
+
|
|
125
|
+
:param client_name : client name
|
|
126
|
+
:return: the access token
|
|
127
|
+
"""
|
|
128
|
+
time_now = naive_utcnow()
|
|
129
|
+
if client_name in self.cached_access_tokens and self.cached_access_tokens[client_name]["last_update"] + datetime.timedelta(minutes=10) < time_now:
|
|
130
|
+
# use cached token since it is still fresh
|
|
131
|
+
pass
|
|
132
|
+
else:
|
|
133
|
+
target_path = self.construct_target_path(client_name)
|
|
134
|
+
token = None
|
|
135
|
+
if os.path.exists(target_path):
|
|
136
|
+
with open(target_path) as f:
|
|
137
|
+
token = f.read()
|
|
138
|
+
if not token:
|
|
139
|
+
token = None
|
|
140
|
+
self.cached_access_tokens[client_name] = {"token": token, "last_update": time_now}
|
|
141
|
+
return self.cached_access_tokens[client_name]["token"]
|