├── src ├── __init__.py ├── generated │ ├── configdefaults.rst │ ├── user.rst │ ├── snapshot.rst │ ├── logdumper.rst │ ├── allocation.rst │ ├── grant.rst │ ├── loopdelay.rst │ ├── misctypes.rst │ ├── packetmanager.rst │ ├── mediator.rst │ ├── actionablepacket.rst │ ├── account.rst │ ├── organization.rst │ ├── taskstatus.rst │ ├── serviceprovider.rst │ ├── config.rst │ ├── person.rst │ ├── spexception.rst │ ├── miscfuncs.rst │ ├── retryingproxy.rst │ ├── project.rst │ ├── amieparms.rst │ ├── filewait.rst │ ├── packethandler.rst │ └── parmdesc.rst ├── utilities.rst ├── handler │ ├── __init__.py │ ├── data_account_create.py │ ├── data_project_create.py │ ├── request_merge_person.py │ ├── request_person_merge.py │ ├── request_project_reactivate.py │ ├── request_user_modify.py │ ├── request_account_inactivate.py │ ├── request_project_inactivate.py │ ├── request_account_reactivate.py │ ├── request_account_create.py │ ├── request_project_create.py │ └── subtasks.py ├── configdefaults.py ├── internals.rst ├── scripts.rst ├── api.rst ├── spexception.py ├── sp_implementation.rst ├── conf.py ├── index.rst ├── allocation.py ├── contract.py ├── user.py ├── logdumper.py ├── misctypes.py ├── organization.py ├── miscfuncs.py ├── loopdelay.py ├── filewait.py ├── account.py ├── retryingproxy.py ├── config.py ├── person.py ├── snapshot.py ├── actionablepacket.py └── project.py ├── tests ├── __init__.py ├── filewait_test_releaser.py ├── t_packethandler.py ├── fixtures │ ├── inform_transaction_complete.py │ ├── request_account_create.py │ └── request_project_create.py ├── t_filewait.py ├── t_allocation.py ├── t_config.py ├── t_misctypes.py ├── t_parmdesc.py ├── t_miscfuncs.py ├── t_organization.py ├── t_account.py ├── t_project.py ├── t_person.py ├── t_amieparms.py ├── t_spsession.py └── t_retryingproxy.py ├── AMIE-API-Testing.pdf ├── pip-packages ├── .gitignore ├── .dockerignore ├── entrypoint.sh ├── setup.py ├── pyproject.toml ├── base └── pyproject.toml ├── make.bat ├── Makefile ├── config.ini ├── README.md ├── Dockerfile ├── venv-init ├── runtests ├── runc ├── bin └── test-scenario └── wrapper.sh /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AMIE-API-Testing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCAR/amiemediator/main/AMIE-API-Testing.pdf -------------------------------------------------------------------------------- /src/generated/configdefaults.rst: -------------------------------------------------------------------------------- 1 | configdefaults 2 | ============== 3 | 4 | .. automodule:: configdefaults 5 | 6 | -------------------------------------------------------------------------------- /pip-packages: -------------------------------------------------------------------------------- 1 | sphinx 2 | myst-parser 3 | urllib3 4 | pprintpp 5 | git+https://github.com/XSEDE/amieclient.git@v0.6.1 6 | -------------------------------------------------------------------------------- /src/generated/user.rst: -------------------------------------------------------------------------------- 1 | user 2 | ==== 3 | 4 | .. automodule:: user 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | ModifyUser 12 | -------------------------------------------------------------------------------- /src/generated/snapshot.rst: -------------------------------------------------------------------------------- 1 | snapshot 2 | ======== 3 | 4 | .. automodule:: snapshot 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | Snapshots 12 | -------------------------------------------------------------------------------- /src/generated/logdumper.rst: -------------------------------------------------------------------------------- 1 | logdumper 2 | ========= 3 | 4 | .. automodule:: logdumper 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | LogDumper 12 | -------------------------------------------------------------------------------- /src/generated/allocation.rst: -------------------------------------------------------------------------------- 1 | allocation 2 | ========== 3 | 4 | .. automodule:: allocation 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | UpdateAllocation 12 | -------------------------------------------------------------------------------- /src/generated/grant.rst: -------------------------------------------------------------------------------- 1 | grant 2 | ===== 3 | 4 | .. automodule:: grant 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | ChooseOrAddGrant 12 | LookupGrant 13 | -------------------------------------------------------------------------------- /src/generated/loopdelay.rst: -------------------------------------------------------------------------------- 1 | loopdelay 2 | ========= 3 | 4 | .. automodule:: loopdelay 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | LoopDelay 12 | WaitParms 13 | -------------------------------------------------------------------------------- /src/generated/misctypes.rst: -------------------------------------------------------------------------------- 1 | misctypes 2 | ========= 3 | 4 | .. automodule:: misctypes 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | DateTime 12 | TimeUtil 13 | -------------------------------------------------------------------------------- /src/generated/packetmanager.rst: -------------------------------------------------------------------------------- 1 | packetmanager 2 | ============= 3 | 4 | .. automodule:: packetmanager 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | PacketManager 12 | -------------------------------------------------------------------------------- /src/generated/mediator.rst: -------------------------------------------------------------------------------- 1 | mediator 2 | ======== 3 | 4 | .. automodule:: mediator 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | AMIEMediator 12 | AMIESession 13 | -------------------------------------------------------------------------------- /src/generated/actionablepacket.rst: -------------------------------------------------------------------------------- 1 | actionablepacket 2 | ================ 3 | 4 | .. automodule:: actionablepacket 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | ActionablePacket 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/.#* 3 | **/#*# 4 | **/*~ 5 | **/*.swp 6 | **/.bash_history 7 | **/.idea 8 | sp/** 9 | gendoc/** 10 | src/amie.rst 11 | src/viewpackets.rst 12 | src/test-scenario.rst 13 | venv/** 14 | **/__pycache__ 15 | -------------------------------------------------------------------------------- /src/generated/account.rst: -------------------------------------------------------------------------------- 1 | account 2 | ======= 3 | 4 | .. automodule:: account 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | CreateAccount 12 | InactivateAccount 13 | ReactivateAccount 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/.#.* 3 | **/*~ 4 | **/*.swp 5 | **/.bash_history 6 | .git 7 | .gitignore 8 | README.md 9 | sp 10 | gendoc/** 11 | src/amie.rst 12 | src/viewpackets.rst 13 | src/test-scenario.rst 14 | venv/** 15 | **/__pycache__ 16 | -------------------------------------------------------------------------------- /src/generated/organization.rst: -------------------------------------------------------------------------------- 1 | organization 2 | ============ 3 | 4 | .. automodule:: organization 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | AMIEOrg 12 | ChooseOrAddOrg 13 | LookupOrg 14 | -------------------------------------------------------------------------------- /src/generated/taskstatus.rst: -------------------------------------------------------------------------------- 1 | taskstatus 2 | ========== 3 | 4 | .. automodule:: taskstatus 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | Product 12 | State 13 | TaskStatus 14 | TaskStatusList 15 | -------------------------------------------------------------------------------- /src/generated/serviceprovider.rst: -------------------------------------------------------------------------------- 1 | serviceprovider 2 | =============== 3 | 4 | .. automodule:: serviceprovider 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | SPSession 12 | ServiceProvider 13 | ServiceProviderIF 14 | -------------------------------------------------------------------------------- /src/generated/config.rst: -------------------------------------------------------------------------------- 1 | config 2 | ====== 3 | 4 | .. automodule:: config 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | ConfigLoader 12 | 13 | .. rubric:: Exceptions 14 | 15 | .. autosummary:: 16 | 17 | ConfigError 18 | -------------------------------------------------------------------------------- /src/generated/person.rst: -------------------------------------------------------------------------------- 1 | person 2 | ====== 3 | 4 | .. automodule:: person 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | AMIEPerson 12 | ActivatePerson 13 | ChooseOrAddPerson 14 | LookupPerson 15 | MergePerson 16 | UpdatePersonDNs 17 | -------------------------------------------------------------------------------- /src/generated/spexception.rst: -------------------------------------------------------------------------------- 1 | spexception 2 | =========== 3 | 4 | .. automodule:: spexception 5 | 6 | 7 | .. rubric:: Exceptions 8 | 9 | .. autosummary:: 10 | 11 | ServiceProviderError 12 | ServiceProviderRequestFailed 13 | ServiceProviderTemporaryError 14 | ServiceProviderTimeout 15 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ $# = 0 ] ; then 3 | if [ -t 0 ] && [ $# = 0 ] ; then 4 | exec /bin/bash 5 | fi 6 | echo "command/arguments required when no terminal is attached" >&2 7 | exit 1 8 | fi 9 | case $1 in 10 | -*) 11 | exec amie "$@" ;; 12 | *) 13 | exec "$@" ;; 14 | esac 15 | -------------------------------------------------------------------------------- /src/generated/miscfuncs.rst: -------------------------------------------------------------------------------- 1 | miscfuncs 2 | ========= 3 | 4 | .. automodule:: miscfuncs 5 | 6 | 7 | .. rubric:: Functions 8 | 9 | .. autosummary:: 10 | 11 | pformat 12 | to_expanded_string 13 | truthy 14 | 15 | .. rubric:: Classes 16 | 17 | .. autosummary:: 18 | 19 | Prettifiable 20 | -------------------------------------------------------------------------------- /src/generated/retryingproxy.rst: -------------------------------------------------------------------------------- 1 | retryingproxy 2 | ============= 3 | 4 | .. automodule:: retryingproxy 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | RetryingServiceProxy 12 | 13 | .. rubric:: Exceptions 14 | 15 | .. autosummary:: 16 | 17 | MaxRetryError 18 | RetryingServiceProxyError 19 | -------------------------------------------------------------------------------- /src/generated/project.rst: -------------------------------------------------------------------------------- 1 | project 2 | ======= 3 | 4 | .. automodule:: project 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | ChooseOrAddLocalFos 12 | ChooseOrAddProjectNameBase 13 | CreateProject 14 | InactivateProject 15 | LookupLocalFos 16 | LookupProjectNameBase 17 | ReactivateProject 18 | -------------------------------------------------------------------------------- /src/generated/amieparms.rst: -------------------------------------------------------------------------------- 1 | amieparms 2 | ========= 3 | 4 | .. automodule:: amieparms 5 | 6 | 7 | .. rubric:: Functions 8 | 9 | .. autosummary:: 10 | 11 | get_packet_keys 12 | parse_atrid 13 | process_parms 14 | strip_key_prefix 15 | 16 | .. rubric:: Classes 17 | 18 | .. autosummary:: 19 | 20 | AMIEParmDescAware 21 | -------------------------------------------------------------------------------- /src/generated/filewait.rst: -------------------------------------------------------------------------------- 1 | filewait 2 | ======== 3 | 4 | .. automodule:: filewait 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | FIFOFileWaiter 12 | FileWaiter 13 | FileWaiterImplABC 14 | PollingFileWaiter 15 | 16 | .. rubric:: Exceptions 17 | 18 | .. autosummary:: 19 | 20 | FileWaiterFileType 21 | -------------------------------------------------------------------------------- /src/generated/packethandler.rst: -------------------------------------------------------------------------------- 1 | packethandler 2 | ============= 3 | 4 | .. automodule:: packethandler 5 | 6 | 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | 11 | DefaultHandler 12 | PacketHandler 13 | ServiceProviderAdapter 14 | 15 | .. rubric:: Exceptions 16 | 17 | .. autosummary:: 18 | 19 | PacketHandlerError 20 | -------------------------------------------------------------------------------- /src/utilities.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | The py:module:`logdumper`, py:module:`miscfuncs`, and py:module:`misctypes` 5 | modules define utility functions and classes that are used internally but can 6 | also be used by the service provider implementation. 7 | 8 | .. autosummary:: 9 | :toctree: generated 10 | 11 | logdumper 12 | miscfuncs 13 | misctypes 14 | -------------------------------------------------------------------------------- /src/handler/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'data_account_create', 3 | 'data_project_create', 4 | 'request_account_create', 5 | 'request_account_inactivate', 6 | 'request_account_reactivate', 7 | 'request_person_merge', 8 | 'request_project_create', 9 | 'request_project_inactivate', 10 | 'request_project_reactivate', 11 | 'request_user_modify' 12 | ] 13 | -------------------------------------------------------------------------------- /src/configdefaults.py: -------------------------------------------------------------------------------- 1 | DFLT = { 2 | "pause_max": 3600, 3 | "min_retry_delay": 60, 4 | "max_retry_delay": 3600, 5 | "retry_time_max": 14400, 6 | "idle_loop_delay": 3600, 7 | "busy_loop_delay": 60, 8 | "reply_delay": 10, 9 | "snapshot_dir": "/tmp/amiemediator", 10 | "sp_min_retry_delay": 60, 11 | "sp_max_retry_delay": 3600, 12 | "sp_retry_time_max": 14400, 13 | } 14 | -------------------------------------------------------------------------------- /src/internals.rst: -------------------------------------------------------------------------------- 1 | Internals 2 | ========= 3 | 4 | The modules and classes documented here implement the mediator proper. 5 | 6 | .. autosummary:: 7 | :toctree: generated 8 | 9 | actionablepacket 10 | amieparms 11 | config 12 | configdefaults 13 | filewait 14 | loopdelay 15 | mediator 16 | packethandler 17 | packetmanager 18 | parmdesc 19 | retryingproxy 20 | snapshot 21 | -------------------------------------------------------------------------------- /src/generated/parmdesc.rst: -------------------------------------------------------------------------------- 1 | parmdesc 2 | ======== 3 | 4 | .. automodule:: parmdesc 5 | 6 | 7 | .. rubric:: Functions 8 | 9 | .. autosummary:: 10 | 11 | process_parms 12 | transform_args 13 | transform_value 14 | 15 | .. rubric:: Classes 16 | 17 | .. autosummary:: 18 | 19 | ParmDescAware 20 | 21 | .. rubric:: Exceptions 22 | 23 | .. autosummary:: 24 | 25 | ParmDescException 26 | -------------------------------------------------------------------------------- /src/scripts.rst: -------------------------------------------------------------------------------- 1 | AMIEMediator Scripts 2 | ==================== 3 | 4 | The **AMIEMediator** package includes three python scripts. 5 | 6 | The main script is ``amie``, which implements the mediator daemon program. 7 | 8 | The ``viewpackets`` script is a command-line tools for viewing the state of 9 | all requests. 10 | 11 | The ``test-scenario`` script sets up an AMIE test scenario. 12 | 13 | .. toctree:: 14 | :caption: Contents: 15 | 16 | amie 17 | viewpackets 18 | test-scenario 19 | 20 | -------------------------------------------------------------------------------- /src/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | The majority of development around the amiemediator package will center around 5 | the ServiceProvider API, specifically in developing a local service provider 6 | implementation. Documentation supporting this development is in the 7 | :doc:`sp_implementation` section. Documentation for the internals of the 8 | ``amie`` application is in the :doc:`internals` section. 9 | 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | :caption: Contents: 14 | 15 | sp_implementation 16 | utilities 17 | internals 18 | -------------------------------------------------------------------------------- /src/spexception.py: -------------------------------------------------------------------------------- 1 | 2 | class ServiceProviderRequestFailed(Exception): 3 | """Exception raised when a request could not be satisfied""" 4 | pass 5 | 6 | class ServiceProviderError(Exception): 7 | """Exception raised when the Service Provider hits an internal error""" 8 | pass 9 | 10 | class ServiceProviderTimeout(Exception): 11 | """Exception raised when get_packets() request times out""" 12 | pass 13 | 14 | class ServiceProviderTemporaryError(Exception): 15 | """Exception raised when an operation fails but should be retried""" 16 | pass 17 | -------------------------------------------------------------------------------- /src/handler/data_account_create.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | 3 | class DataAccountCreate(PacketHandler, packet_type="data_account_create"): 4 | 5 | def work(self, apacket): 6 | """Handle a "data_account_create" packet 7 | 8 | :param apacket: dict with extended packet data 9 | :type apacket: ActionablePacket 10 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 11 | to be sent back 12 | """ 13 | 14 | spa = self.sp_adapter 15 | 16 | spa.clear_transaction(apacket) 17 | 18 | itc = apacket.create_reply_packet() 19 | return itc 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='amiemediator', 8 | version='0.0.1', 9 | packages=find_packages(), 10 | install_requires=[ 11 | 'amieclient>=0.6.1', 12 | ], 13 | author='George B Williams', 14 | author_email='gwilliam@ucar.edu', 15 | python_requires='>=3.9', 16 | description='Tool that ties together amieclient and a local Service Provider.', 17 | long_description=long_description, 18 | long_description_content_type="text/markdown", 19 | project_urls={ 20 | 'Source': 'https://github.com/NCAR/amiemediator/', 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "amiemediator" 7 | version = "0.0.1" 8 | authors = [ 9 | { name="George Williams", email="gwilliam@ucar.edu" }, 10 | ] 11 | description = "Mediator service for connecting AMIE to a local Service Provider" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Operating System :: OS Independent", 17 | ] 18 | dependencies = [ 19 | 'sphinx', 20 | 'urllib3', 21 | 'amieclient @ git+https://github.com/XSEDE/amieclient.git', 22 | ] 23 | [project.urls] 24 | "Homepage" = "https://github.com/NCAR/amiemediator" 25 | "AMIEClient" = "https://github.com/XSEDE/amieclient" 26 | 27 | -------------------------------------------------------------------------------- /base/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "amiemediator" 7 | version = "0.0.1" 8 | authors = [ 9 | { name="George Williams", email="gwilliam@ucar.edu" }, 10 | ] 11 | description = "Mediator service for connecting AMIE to a local Service Provider" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Operating System :: OS Independent", 17 | ] 18 | dependencies = [ 19 | 'sphinx', 20 | 'urllib3', 21 | 'amieclient @ git+https://github.com/XSEDE/amieclient.git', 22 | ] 23 | [project.urls] 24 | "Homepage" = "https://github.com/NCAR/amiemediator" 25 | "AMIClient" = "https://github.com/XSEDE/amieclient" 26 | 27 | -------------------------------------------------------------------------------- /src/handler/data_project_create.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | import handler.subtasks as sub 3 | 4 | class DataProjectCreate(PacketHandler, packet_type="data_project_create"): 5 | 6 | def work(self, apacket): 7 | """Handle a "data_project_create" packet 8 | 9 | :param apacket: dict with extended packet data 10 | :type apacket: ActionablePacket 11 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 12 | to be sent back 13 | """ 14 | 15 | spa = self.sp_adapter 16 | 17 | ts = sub.update_person_DNs(spa, apacket, "Pi") 18 | if ts: 19 | return ts 20 | 21 | spa.clear_transaction(apacket) 22 | 23 | itc = apacket.create_reply_packet() 24 | return itc 25 | -------------------------------------------------------------------------------- /src/handler/request_merge_person.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import truthy 5 | import handler.subtasks as sub 6 | 7 | class RequestPersonMerge(PacketHandler, packet_type="request_user_modify"): 8 | 9 | def work(self, apacket): 10 | """Handle a "request_user_modify" packet 11 | 12 | :param apacket: dict with extended packet data 13 | :type apacket: ActionablePacket 14 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 15 | to be sent back 16 | """ 17 | 18 | spa = self.sp_adapter 19 | 20 | ts = sub.modify_user(spa, apacket) 21 | if ts: 22 | return ts 23 | 24 | itc = apacket.create_reply_packet() 25 | return itc 26 | -------------------------------------------------------------------------------- /src/handler/request_person_merge.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import truthy 5 | import handler.subtasks as sub 6 | 7 | class RequestPersonMerge(PacketHandler, packet_type="request_person_merge"): 8 | 9 | def work(self, apacket): 10 | """Handle a "request_person_merge" packet 11 | 12 | :param apacket: dict with extended packet data 13 | :type apacket: ActionablePacket 14 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 15 | to be sent back 16 | """ 17 | 18 | spa = self.sp_adapter 19 | 20 | ts = sub.merge_person(spa, apacket) 21 | if ts: 22 | return ts 23 | 24 | itc = apacket.create_reply_packet() 25 | return itc 26 | 27 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=src 11 | set BUILDDIR=gendoc 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /src/sp_implementation.rst: -------------------------------------------------------------------------------- 1 | Service Provider Implementation 2 | =============================== 3 | 4 | A site must provide its own service provider implementation. The 5 | ServiceProvider API is defined in the py:module:`serviceprovider` module. 6 | Exceptions are defined in the py:module:`spexception` module. 7 | 8 | The ServiceProvider API breaks down the actions required by an AMIE packet into 9 | separate tasks. The state of tasks is stored in object defined in the 10 | py:module:`taskstatus` module. 11 | 12 | The remaining modules documented here define classes that encapsulate 13 | parameters passed to the ServiceProvider API. These are all subclasses of 14 | py:class:`AMIEParmDescAware`, which simplifies parameter filtering, conversion, 15 | and documentation. 16 | 17 | 18 | .. autosummary:: 19 | :toctree: generated 20 | 21 | serviceprovider 22 | spexception 23 | taskstatus 24 | account 25 | allocation 26 | grant 27 | organization 28 | person 29 | project 30 | user 31 | -------------------------------------------------------------------------------- /src/handler/request_project_reactivate.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from taskstatus import TaskStatus 3 | import handler.subtasks as sub 4 | 5 | class RequestProjectReactivate(PacketHandler, 6 | packet_type="request_project_reactivate"): 7 | 8 | def work(self, apacket): 9 | """Handle a "request_project_reactivate" packet 10 | 11 | :param apacket: dict with extended packet data 12 | :type apacket: ActionablePacket 13 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 14 | to be sent back 15 | """ 16 | 17 | spa = self.sp_adapter 18 | 19 | reactivate_ts = spa.reactivate_project(apacket) 20 | if reactivate_ts['task_state'] != "successful": 21 | return reactivate_ts 22 | 23 | npr = apacket.create_reply_packet() 24 | npr.ProjectID = apacket['ProjectID'] 25 | npr.ResourceList = apacket['ResourceList'] 26 | 27 | return npr 28 | -------------------------------------------------------------------------------- /src/handler/request_user_modify.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import truthy 5 | import handler.subtasks as sub 6 | 7 | class RequestUserModify(PacketHandler, packet_type="request_user_modify"): 8 | 9 | def work(self, apacket): 10 | """Handle a "request_user_modify" packet 11 | 12 | :param apacket: dict with extended packet data 13 | :type apacket: ActionablePacket 14 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 15 | to be sent back 16 | """ 17 | 18 | spa = self.sp_adapter 19 | 20 | person_id = apacket.get('PersonID',None) 21 | ts = sub.modify_user(spa, apacket) 22 | if ts: 23 | return ts 24 | 25 | num = apacket.create_reply_packet() 26 | num.ActionType = apacket['ActionType'] 27 | num.PersonID = apacket['PersonID'] 28 | return num 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = src 9 | BUILDDIR = gendoc 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | src/amie.rst: 18 | amie -h > $@ 19 | 20 | src/test-scenario.rst: 21 | test-scenario -h > $@ 22 | 23 | src/viewpackets.rst: 24 | viewpackets -h > $@ 25 | 26 | # Catch-all target: route all unknown targets to Sphinx using the new 27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 28 | %: Makefile src/amie.rst src/test-scenario.rst src/viewpackets.rst 29 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 30 | 31 | clean: 32 | rm -f src/amie.rst src/viewpackets.rst src/test-scenario.rst 33 | rm -rf $(BUILDDIR)/* 34 | -------------------------------------------------------------------------------- /src/handler/request_account_inactivate.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import truthy 5 | 6 | class RequestAccountInactivate(PacketHandler, packet_type="request_account_inactivate"): 7 | 8 | def work(self, apacket): 9 | """Handle a "request_account_inactivate" packet 10 | 11 | :param apacket: dict with extended packet data 12 | :type apacket: ActionablePacket 13 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 14 | to be sent back 15 | """ 16 | 17 | spa = self.sp_adapter 18 | 19 | inactivate_ts = spa.inactivate_account(apacket) 20 | if inactivate_ts['task_state'] == "successful": 21 | nai = apacket.create_reply_packet() 22 | nai.PersonID = apacket['PersonID'] 23 | nai.ProjectID = apacket['ProjectID'] 24 | nai.ResourceList = apacket['ResourceList'] 25 | return nai 26 | return inactivate_ts 27 | -------------------------------------------------------------------------------- /src/handler/request_project_inactivate.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from taskstatus import TaskStatus 3 | 4 | class RequestProjectInactivate(PacketHandler, 5 | packet_type="request_project_inactivate"): 6 | 7 | def work(self, apacket): 8 | """Handle a "request_project_inactivate" packet 9 | 10 | Inactivate the project and all accounts on the project 11 | 12 | :param apacket: dict with extended packet data 13 | :type apacket: ActionablePacket 14 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 15 | to be sent back 16 | """ 17 | 18 | spa = self.sp_adapter 19 | inact_project_ts = spa.inactivate_project(apacket) 20 | if inact_project_ts['task_state'] == "successful": 21 | project_id = inact_project_ts.get_product_value('ProjectID') 22 | else: 23 | return inact_project_ts 24 | 25 | npi = apacket.create_reply_packet() 26 | npi.ProjectID = apacket['ProjectID'] 27 | npi.ResourceList = apacket['ResourceList'] 28 | return npi 29 | -------------------------------------------------------------------------------- /src/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'AMIE Mediator' 10 | copyright = '2024, George Williams' 11 | author = 'George Williams' 12 | release = '0.1' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | 'myst_parser', 19 | 'sphinx.ext.autodoc', 20 | 'sphinx.ext.autosummary', 21 | ] 22 | 23 | templates_path = ['_templates'] 24 | exclude_patterns = [] 25 | 26 | 27 | 28 | # -- Options for HTML output ------------------------------------------------- 29 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 30 | 31 | html_theme = 'alabaster' 32 | html_static_path = ['_static'] 33 | -------------------------------------------------------------------------------- /src/index.rst: -------------------------------------------------------------------------------- 1 | .. AMIEMediator documentation master file, created by 2 | sphinx-quickstart on Wed Sep 4 15:13:26 2024. 3 | 4 | AMIEMediator documentation 5 | ========================== 6 | 7 | The **AMIEMediator** package defines tools for mediating interactions between 8 | AMIE and a local service provider. 9 | 10 | The ``XSEDE/amieclient`` package is a python library for the AMIE REST API. It 11 | defines all data packets and low-level messaging methods, but leaves all 12 | higher-level and back-end processing tasks to the local service provider. 13 | 14 | The ``NCAR/amiemediator`` package attempts to simplify the implementation of 15 | these other tasks by providing a back-end ServiceProvider API and a configurable 16 | daemon that handles all interactions with the central AMIE server. The API 17 | tries to make as few assumptions as possible about the nature of the back-end 18 | service provider. The ServiceProvider implementation is expected to be provided 19 | by the local site as python modules that are resolved at run-time. See 20 | :doc:`sp_implementation` for details. 21 | 22 | .. toctree:: 23 | :maxdepth: 3 24 | :caption: Contents: 25 | 26 | scripts 27 | api 28 | 29 | -------------------------------------------------------------------------------- /src/allocation.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, process_parms) 2 | 3 | class UpdateAllocation(AMIEParmDescAware,dict): 4 | 5 | @process_parms( 6 | allowed=[ 7 | 'amie_transaction_id', 8 | 'amie_packet_id', 9 | 'job_id', 10 | 'amie_packet_type', 11 | 'task_name', 12 | 'timestamp', 13 | 14 | 'AllocationType', 15 | 'EndDate', 16 | 'ProjectID', 17 | 'Resource', 18 | 'ServiceUnitsAllocated', 19 | 'StartDate', 20 | ], 21 | required=[ 22 | 'amie_transaction_id', 23 | 'amie_packet_id', 24 | 'job_id', 25 | 'amie_packet_type', 26 | 'task_name', 27 | 'timestamp', 28 | 29 | 'AllocationType', 30 | 'EndDate', 31 | 'ProjectID', 32 | 'Resource', 33 | 'ServiceUnitsAllocated', 34 | 'StartDate', 35 | ]) 36 | def __init__(self, *args, **kwargs): 37 | """Validate, filter, and transform arguments to ``update_allocation()``""" 38 | dict.__init__(self, **kwargs) 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/contract.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, process_parms) 2 | 3 | class ChooseOrAddContractNumber(AMIEParmDescAware,dict): 4 | """ 5 | A class used when specifying a GrantNumber known to AMIE 6 | """ 7 | 8 | @process_parms( 9 | allowed=[ 10 | 'amie_transaction_id', 11 | 'amie_packet_id', 12 | 'job_id', 13 | 'amie_packet_type', 14 | 'task_name', 15 | 'timestamp', 16 | 17 | 'GrantNumber', 18 | 'GrantType', 19 | 'PfosNumber', 20 | 'local_fos', 21 | 'PiPersonID', 22 | 'PiFirstName', 23 | 'PiLastName', 24 | 'PiDepartment', 25 | 'ProjectTitle', 26 | 'StartDate', 27 | 'EndDate', 28 | ], 29 | required=[ 30 | 'amie_transaction_id', 31 | 'amie_packet_id', 32 | 'job_id', 33 | 'amie_packet_type', 34 | 'task_name', 35 | 'timestamp', 36 | 37 | 'GrantNumber', 38 | 'PfosNumber', 39 | 'PiPersonID', 40 | 'PiFirstName', 41 | 'PiLastName', 42 | 'StartDate', 43 | 'EndDate', 44 | ]) 45 | def __init__(self, **kwargs) -> dict: 46 | """Validate, filter, and transform arguments to ``choose_or_add_grant()``""" 47 | dict.__init__(self, **kwargs) 48 | 49 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [global] 2 | site_name = NCAR 3 | # Maximum seconds to wait (via "time.sleep()", API call, or otherwise) without 4 | # doing anything. This applies to individual pauses, not overall loop delays. 5 | # This allows for out-of-band events, logging, etc. to be handled regularly 6 | pause_max = 3600 7 | 8 | [amieclient] 9 | amie_url = https://a3mdev.xsede.org/amie-api-test 10 | api_key = %(amieclient_api_key)s 11 | 12 | [localsite] 13 | package = 14 | module = .serviceproviderspy 15 | 16 | [logging] 17 | level = DEBUG 18 | 19 | [mediator] 20 | # Directory for storing JSON snapshot files for status monitoring 21 | snapshot_dir = /tmp/snapshots 22 | 23 | # How long to wait (secs) between queries to AMIE when no specific packets are 24 | # expected 25 | idle_loop_delay = 14400 26 | 27 | # How long to wait (secs) between queries to AMIE when specific packets are 28 | # expected 29 | busy_loop_delay = 60 30 | 31 | # How long to wait (secs) after sending a non-ITC packet to AMIE before checking 32 | # for a response 33 | reply_delay = 10 34 | 35 | # The minimum time (secs) to wait before retrying when a call to the Service 36 | # Provider fails with a temporary error. The retry loop will double the delay 37 | # on subsequent retry attempts until sp_max_retry_delay is reached 38 | sp_min_retry_delay = 60 39 | 40 | # The maximum time (secs) to wait before retrying when a call to the Service 41 | # Provider fails with a temporary error. 42 | sp_max_retry_delay = 3600 43 | 44 | # The maximum time (secs) that Service Provider operations that fail with 45 | # temporary errors should be retried before failing 46 | sp_retry_time_max = 14400 47 | -------------------------------------------------------------------------------- /src/user.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, process_parms) 2 | 3 | class ModifyUser(AMIEParmDescAware,dict): 4 | """ 5 | A class used when modifying a user 6 | """ 7 | 8 | @process_parms( 9 | allowed=[ 10 | 'amie_transaction_id', 11 | 'amie_packet_id', 12 | 'job_id', 13 | 'amie_packet_type', 14 | 'task_name', 15 | 'timestamp', 16 | 17 | 'ActionType', 18 | 'AcademicDegree', 19 | 'BusinessPhoneComment', 20 | 'BusinessPhoneExtension', 21 | 'BusinessPhoneNumber', 22 | 'CitizenshipList', 23 | 'City', 24 | 'Country', 25 | 'Department', 26 | 'DnList', 27 | 'Email', 28 | 'FirstName', 29 | 'HomePhoneComment', 30 | 'HomePhoneExtension', 31 | 'HomePhoneNumber', 32 | 'LastName', 33 | 'MiddleName', 34 | 'NsfStatusCode', 35 | 'Organization', 36 | 'OrgCode', 37 | 'PersonID', 38 | 'State', 39 | 'StreetAddress', 40 | 'StreetAddress2', 41 | 'Title', 42 | 'Zip', 43 | ], 44 | required=[ 45 | 'amie_transaction_id', 46 | 'amie_packet_id', 47 | 'job_id', 48 | 'amie_packet_type', 49 | 'task_name', 50 | 'timestamp', 51 | 52 | 'ActionType', 53 | 'PersonID', 54 | ]) 55 | def __init__(self, **kwargs) -> dict: 56 | """Validate, filter, and transform arguments to ``modify_user()``""" 57 | dict.__init__(self, **kwargs) 58 | -------------------------------------------------------------------------------- /tests/filewait_test_releaser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os, sys 3 | import time 4 | from filewait import FileWaiter 5 | 6 | waitfile = sys.argv.pop(0) 7 | # fork 8 | pid = os.fork() 9 | if pid == 0: 10 | subpid = os.fork() 11 | if subpid == 0: 12 | fw = FileWaiter(waitfile) 13 | fw.release() 14 | sys.exit(0) 15 | else: 16 | return 17 | class TestFileWaiter(unittest.TestCase): 18 | 19 | def xtest_wait_timeout(self): 20 | starttime = time.time() 21 | waitfile = str(Path(tempdir.name,"tmwaitfile")) 22 | fw = FileWaiter(waitfile) 23 | released = fw.wait(2) 24 | endtime = time.time() 25 | elapsed = endtime - starttime 26 | self.assertFalse(released, 27 | msg="wait() did not return False") 28 | self.assertTrue((elapsed > 1.5) and (elapsed < 2.5), 29 | msg=str(elapsed) +" seconds elapsed on timeout") 30 | 31 | def test_wait_release(self): 32 | starttime = time.time() 33 | print("test_wait_release starttime="+str(starttime)) 34 | waitfile = str(Path(tempdir.name,"relwaitfile")) 35 | fw = FileWaiter(waitfile) 36 | spawn_release(waitfile) 37 | released = fw.wait(3) 38 | endtime = time.time() 39 | print("test_wait_release endtime="+str(endtime)) 40 | elapsed = endtime - starttime 41 | self.assertFalse(released, 42 | msg="wait() did not return True") 43 | self.assertTrue((elapsed > 0.0) and (elapsed < 2.5), 44 | msg=str(elapsed) +" seconds elapsed on release") 45 | 46 | 47 | if __name__ == '__main__': 48 | unittest.main() 49 | tempdir.cleanup() 50 | -------------------------------------------------------------------------------- /tests/t_packethandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | from amieclient.packet.base import Packet 5 | from amieclient.packet.project import RequestProjectCreate 6 | from fixtures.request_project_create import RPC_PKT_1 7 | from test.test_sp import ServiceProvider as TestServiceProvider 8 | from packethandler import (PacketHandlerError, 9 | PacketHandler, 10 | ServiceProviderAdapter) 11 | 12 | class MockHandler(PacketHandler, packet_type="request_project_create"): 13 | def __init__(self): 14 | super().__init__() 15 | self.counter = 3 16 | 17 | def work(self): 18 | self.counter -= 1 19 | return (self.counter > 0) 20 | 21 | class TestPacketHandler(unittest.TestCase): 22 | def setUp(self): 23 | self.sp = TestServiceProvider() 24 | self.packet = Packet.from_dict(RPC_PKT_1) 25 | 26 | def test_constructor(self): 27 | 28 | handler = MockHandler() 29 | 30 | self.assertTrue(isinstance(handler,MockHandler), 31 | msg="constructor failed") 32 | self.assertTrue(isinstance(handler.sp_adapter,ServiceProviderAdapter), 33 | msg="handler sp_adapter not set") 34 | 35 | did_work = handler.work() 36 | self.assertTrue(did_work, 37 | msg="run first iteration returned False") 38 | did_work = handler.work() 39 | self.assertTrue(did_work, 40 | msg="run second iteration returned False") 41 | did_work = handler.work() 42 | self.assertFalse(did_work, 43 | msg="run third iteration returned True") 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /tests/fixtures/inform_transaction_complete.py: -------------------------------------------------------------------------------- 1 | ITC_CANCEL_RPC_PKT_1 = { 2 | 'DATA_TYPE': 'Packet', 3 | 'type': 'inform_transaction_complete', 4 | 'header': { 5 | 'packet_rec_id': 174709748, 6 | 'packet_id': 2, 7 | 'transaction_id': 244207, 8 | 'trans_rec_id': 87139098, 9 | 'expected_reply_list': [ 10 | { 11 | 'type': 'data_project_create', 12 | 'timeout': 30240 13 | } 14 | ], 15 | 'local_site_name': 'PSC', 16 | 'remote_site_name': 'SDSC', 17 | 'originating_site_name': 'SDSC', 18 | 'outgoing_flag': False, 19 | 'transaction_state': 'in-progress', 20 | 'packet_state': 'in-progress', 21 | 'packet_timestamp': '2021-08-24T14:47:55.600Z', 22 | 'in_reply_to': 174709746 23 | }, 24 | 'body': { 25 | 'DetailCode': 99, 26 | 'Message': 'Cancelled', 27 | 'StatusCode': 99, 28 | }, 29 | } 30 | 31 | ITC_CANCEL_RPC_PKT_2 = { 32 | 'DATA_TYPE': 'Packet', 33 | 'type': 'inform_transaction_complete', 34 | 'header': { 35 | 'packet_rec_id': 174709749, 36 | 'packet_id': 2, 37 | 'transaction_id': 244208, 38 | 'trans_rec_id': 87139099, 39 | 'expected_reply_list': [ 40 | { 41 | 'type': 'data_project_create', 42 | 'timeout': 30240 43 | } 44 | ], 45 | 'local_site_name': 'PSC', 46 | 'remote_site_name': 'SDSC', 47 | 'originating_site_name': 'SDSC', 48 | 'outgoing_flag': False, 49 | 'transaction_state': 'in-progress', 50 | 'packet_state': 'in-progress', 51 | 'packet_timestamp': '2021-08-24T14:47:56.600Z', 52 | 'in_reply_to': 174709747 53 | }, 54 | 'body': { 55 | 'DetailCode': 99, 56 | 'Message': 'Cancelled', 57 | 'StatusCode': 99, 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /tests/t_filewait.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import tempfile 4 | from pathlib import Path 5 | import os, sys 6 | import time 7 | import subprocess 8 | from filewait import FileWaiter 9 | 10 | tempdir = tempfile.TemporaryDirectory() 11 | 12 | class TestFileWaiter(unittest.TestCase): 13 | 14 | def test_wait_timeout(self): 15 | starttime = time.time() 16 | waitfile = str(Path(tempdir.name,"tmwaitfile")) 17 | fw = FileWaiter(waitfile) 18 | released = fw.wait(2) 19 | endtime = time.time() 20 | elapsed = endtime - starttime 21 | self.assertFalse(released, 22 | msg="wait() did not return False") 23 | self.assertTrue((elapsed > 1.5) and (elapsed < 2.5), 24 | msg=str(elapsed) +" seconds elapsed on timeout") 25 | 26 | def test_wait_release(self): 27 | starttime = time.time() 28 | waitfile = str(Path(tempdir.name,"relwaitfile")) 29 | fw = FileWaiter(waitfile) 30 | subprocess.run([__file__,"release",waitfile,"1"]) 31 | released = fw.wait(3) 32 | endtime = time.time() 33 | elapsed = endtime - starttime 34 | self.assertFalse(released, 35 | msg="wait() did not return True") 36 | self.assertTrue((elapsed > 0.0) and (elapsed < 2.5), 37 | msg=str(elapsed) +" seconds elapsed on release") 38 | 39 | 40 | if __name__ == '__main__': 41 | if len(sys.argv) > 1: 42 | if sys.argv[1] == "release": 43 | waitfile = sys.argv[2] 44 | delay = int(sys.argv[3]) 45 | pid = os.fork() 46 | if pid == 0: 47 | subpid = os.fork() 48 | if subpid == 0: 49 | time.sleep(delay) 50 | fw = FileWaiter(waitfile) 51 | fw.release() 52 | sys.exit(0) 53 | 54 | unittest.main() 55 | tempdir.cleanup() 56 | -------------------------------------------------------------------------------- /tests/t_allocation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import json 4 | from misctypes import DateTime 5 | from allocation import AddOrUpdateAllocation 6 | 7 | def _make_test_value(parm2type,key): 8 | t = parm2type[key] 9 | if isinstance(t,list): 10 | val = [] 11 | elif t is list: 12 | val = [] 13 | elif t is dict: 14 | val = {} 15 | elif t is DateTime: 16 | if (key == "StartDate"): 17 | val = DateTime("2023-01-01T00:00:00-07:00") 18 | else: 19 | val = DateTime("2023-01-01T23:59:59-07:00") 20 | elif t is not str: 21 | val = t(1) 22 | else: 23 | val = key + "_val" 24 | return val 25 | 26 | class TestAddOrUpdateAllocation(unittest.TestCase): 27 | def test_constructor(self): 28 | parm2type = AddOrUpdateAllocation.parm2type.copy() 29 | func_info = AddOrUpdateAllocation.__init__.func_info 30 | inkeys = func_info['allowed'].copy() 31 | inparms = {} 32 | for inkey in inkeys: 33 | inparms[inkey] = _make_test_value(parm2type,inkey) 34 | 35 | inkeys.append('Nosuch') 36 | parm2type['Nosuch'] = str 37 | 38 | reqparms = func_info['required'].copy() 39 | for reqparm in reqparms: 40 | in2 = inparms.copy() 41 | del in2[reqparm] 42 | with self.assertRaises(KeyError, 43 | msg=reqparm + " not required"): 44 | parms = AddOrUpdateAllocation(**in2) 45 | 46 | parms = AddOrUpdateAllocation(**inparms) 47 | 48 | self.assertTrue(isinstance(parms,AddOrUpdateAllocation), 49 | msg='constructor() failed'); 50 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 51 | 52 | for key in func_info['allowed']: 53 | expected_val = _make_test_value(parm2type,key) 54 | val = parms[key] 55 | self.assertEqual(val,expected_val) 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /src/handler/request_account_reactivate.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import (truthy, get_first_nonEmpty) 5 | import handler.subtasks as sub 6 | 7 | class RequestAccountReactivate(PacketHandler, packet_type="request_account_reactivate"): 8 | 9 | def work(self, apacket): 10 | """Handle a "request_account_reactivate" packet 11 | 12 | :param apacket: dict with extended packet data 13 | :type apacket: ActionablePacket 14 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 15 | to be sent back 16 | """ 17 | 18 | spa = self.sp_adapter 19 | 20 | person_id = apacket.get('person_id',None) 21 | if person_id is None: 22 | person_id = apacket.get('PersonID',None) 23 | apacket['person_id'] = person_id 24 | 25 | person_active = apacket.get('person_active',False) 26 | if not person_active: 27 | ts = sub.activate_person(spa, apacket, 'User') 28 | if ts: 29 | return ts 30 | person_active = apacket['person_active'] 31 | 32 | person_id = get_first_nonEmpty(apacket,'person_id','PersonID') 33 | apacket['PersonID'] = person_id 34 | project_id = get_first_nonEmpty(apacket,'project_id','ProjectID') 35 | apacket['ProjectID'] = project_id 36 | user_notified = apacket.get('user_notified', None) 37 | if user_notified is None: 38 | ts = sub.notify_user(spa, apacket) 39 | if ts: 40 | return ts 41 | 42 | reactivate_ts = spa.reactivate_account(apacket) 43 | if reactivate_ts['task_state'] == "successful": 44 | nar = apacket.create_reply_packet() 45 | nar.PersonID = apacket['PersonID'] 46 | nar.ProjectID = apacket['ProjectID'] 47 | nar.ResourceList = apacket['ResourceList'] 48 | return nar 49 | return reactivate_ts 50 | -------------------------------------------------------------------------------- /tests/t_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import os 4 | import tempfile 5 | from config import ConfigLoader 6 | 7 | class TestConfig(unittest.TestCase): 8 | def test_config(self): 9 | with tempfile.TemporaryDirectory() as tempdir: 10 | os.environ['SECRETS_DIR'] = tempdir 11 | configfile = tempdir + "/config.ini" 12 | with open(configfile,"w") as cf: 13 | cf.write(""" 14 | [DEFAULT] 15 | configclass = ConfigLoader 16 | 17 | [amieclient] 18 | site_name = %(amieclient_site_name)s 19 | amie_url = https://a3mdev.xsede.org/amie-api-test 20 | api_key = %(amieclient_api_key)s 21 | 22 | [localsite] 23 | package = amiemod 24 | module = .test 25 | """) 26 | cf.close() 27 | secretfile = tempdir + "/amieclient_api_key" 28 | with open(secretfile,"w") as tf: 29 | tf.write("mysecretkey") 30 | tf.close 31 | 32 | os.environ['AMIECLIENT_SITE_NAME'] = "TEST" 33 | 34 | config = ConfigLoader.loadConfig(configfile) 35 | 36 | self.assertTrue(isinstance(config,dict), 37 | msg="dict construction failed") 38 | self.assertTrue('amieclient' in config, 39 | msg="amieclient section not in config") 40 | amieclient = config['amieclient'] 41 | self.assertEqual(amieclient['site_name'],"TEST", 42 | msg="value not injected from environment") 43 | self.assertEqual(amieclient['amie_url'],"https://a3mdev.xsede.org/amie-api-test", 44 | msg="literal value assigned") 45 | self.assertEqual(amieclient['api_key'],"mysecretkey", 46 | msg="value not injected from file") 47 | self.assertEqual(amieclient['configclass'],"ConfigLoader", 48 | msg="value not inherited from [DEFAULT]") 49 | self.assertEqual(len(amieclient),4, 50 | msg="extra variables not filtered out") 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /src/logdumper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from miscfuncs import to_expanded_string 3 | 4 | class LogDumper(object): 5 | 6 | def __init__(self, logger): 7 | """Logging helper that that produces nicely formatted log entries 8 | 9 | Attach a Logger object to a LogDumper object, then use the LogDumper in 10 | place of (or in addtition to) the Logger. 11 | 12 | :param logger: A Logger instance 13 | :type logger: Logger 14 | """ 15 | self.logger = logger 16 | 17 | def dump(self, level, *args): 18 | """Dump the given arguments at the given logging level 19 | 20 | :param level: The logging level 21 | :type level: Int 22 | :param args: Objects to be dumped 23 | :type args: List of objects 24 | """ 25 | if not self.logger.isEnabledFor(level): 26 | return 27 | dumplines = list() 28 | for arg in args: 29 | dumplines.append(to_expanded_string(arg)) 30 | dumplines = self._expand_lines(dumplines) 31 | for dumpline in dumplines: 32 | self.logger.log(level,dumpline) 33 | 34 | def _expand_lines(self,inlines): 35 | outlines = list() 36 | for line in inlines: 37 | if line is None: 38 | outlines.append("(None)") 39 | else: 40 | outlines.extend(line.split("\n")) 41 | return outlines 42 | 43 | def debug(self, *args): 44 | """Dump the given arguments at DEBUG level""" 45 | self.dump(logging.DEBUG, *args) 46 | 47 | def info(self, *args): 48 | """Dump the given arguments at INFO level""" 49 | self.dump(logging.INFO, *args) 50 | 51 | def warning(self, *args): 52 | """Dump the given arguments at WARNING level""" 53 | self.dump(logging.WARNING, *args) 54 | 55 | def error(self, *args): 56 | """Dump the given arguments at ERROR level""" 57 | self.dump(logging.ERROR, *args) 58 | 59 | def critical(self, *args): 60 | """Dump the given arguments at CRITICAL level""" 61 | self.dump(logging.CRITICAL, *args) 62 | 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amiemediator 2 | A tool for mediating interactions between AMIE and a local service provider. 3 | 4 | The XSEDE/amieclient package is a python library for the AMIE REST API. It 5 | defines all data packets and low-level messaging methods, but leaves all 6 | higher-level and back-end processing tasks to the local service provider. 7 | 8 | The NCAR/amiemediator package attempts to simplify the implementation of these 9 | other tasks by providing a back-end ServiceProvider API and a configurable 10 | daemon that handles all interactions with the central AMIE server. The API 11 | tries to make as few assumptions as possible about the nature of the back-end 12 | service provider. The ServiceProvider implementation is expected to be provided 13 | by the local site as python modules that are resolved as run-time. See 14 | src/serviceprovider.py for details. 15 | 16 | The daemon program is bin/amie; this program mediates between the AMIE server 17 | and the local service provider; specifically, it retrieves AMIE packets from 18 | the AMIE server, and for each packet it triggers a set of operations on the 19 | service provider using the service provider API. 20 | 21 | When an operation cannot be completed immediately, the service provider will 22 | create a "task" object. The "amie" program will automatically monitor the 23 | status of each task until all work for the packet is complete and a reply can 24 | be sent back to the AMIE server. 25 | 26 | Whenever the state of a task changes, "amie" writes/updates a "snapshot" file 27 | that captures the relevant state of the packet as readable text. All snapshot 28 | files are written to a directory identified in the "amie" configuration file 29 | ("snapshot_dir"). When work for a packet is complete, all related data is 30 | destroyed and the corresponding snapshot file is removed. The amiemediator 31 | package includes a command-line utility, bin/viewpackets, that monitors and 32 | displays the contents of the snapshot directory. 33 | 34 | An additional program is provided in the package: bin/test-scenario. This 35 | program initializes an AMIE test scenario as described in the *AMIE API Testing* 36 | document. 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/t_misctypes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import json 4 | from datetime import datetime 5 | from datetime import tzinfo 6 | from dateutil.parser import ParserError 7 | from misctypes import DateTime 8 | 9 | class TestDateTime(unittest.TestCase): 10 | def setUp(self): 11 | self.testtime_s = "2023-01-01T12:00:00-07:00" 12 | self.testtime = datetime.fromisoformat(self.testtime_s); 13 | self.testtimestamp = self.testtime.timestamp(); 14 | 15 | def test_constructor(self): 16 | with self.assertRaises(ParserError, 17 | msg="Invalid string accepted"): 18 | dt = DateTime("wtf") 19 | 20 | with self.assertRaises(TypeError, 21 | msg="Single int arg accepted"): 22 | dt = DateTime(2023) 23 | 24 | dt = DateTime(self.testtime_s) 25 | 26 | self.assertEqual(dt,self.testtime_s, 27 | msg='value from string wrong'); 28 | 29 | dt = DateTime(self.testtime) 30 | 31 | self.assertEqual(dt,self.testtime_s, 32 | msg='value from datetime wrong'); 33 | 34 | def test_attrs(self): 35 | dts = DateTime(self.testtime_s) 36 | dt = dts.datetime() 37 | self.assertEqual(dt.year,self.testtime.year, 38 | msg='year attr wrong'); 39 | self.assertEqual(dt.month,self.testtime.month, 40 | msg='month attr wrong'); 41 | self.assertEqual(dt.day,self.testtime.day, 42 | msg='day attr wrong'); 43 | self.assertEqual(dt.hour,self.testtime.hour, 44 | msg='hour attr wrong'); 45 | self.assertEqual(dt.minute,self.testtime.minute, 46 | msg='minute attr wrong'); 47 | self.assertEqual(dt.second,self.testtime.second, 48 | msg='second attr wrong'); 49 | self.assertEqual(dt.isoformat(),self.testtime_s, 50 | msg='iso string wrong'); 51 | self.assertEqual(dts.timestamp(),self.testtimestamp, 52 | msg='timestamp wrong') 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /src/misctypes.py: -------------------------------------------------------------------------------- 1 | from datetime import (datetime, timedelta) 2 | from dateutil.parser import parse as dtparse 3 | from time import sleep 4 | 5 | class DateTime(str): 6 | """ 7 | A subtype of str that only accepts parseable date+time string or a 8 | datetime value when created, and has a string value of an ISO datetime 9 | value. 10 | """ 11 | 12 | def __new__(cls, *args): 13 | if len(args) == 1 and isinstance(args[0], str): 14 | dt = dtparse(args[0]) 15 | elif len(args) == 1 and isinstance(args[0], datetime): 16 | dt = args[0] 17 | else: 18 | dt = datetime.__new__(cls,*args) 19 | 20 | s = str.__new__(cls,dt.isoformat()) 21 | s._timestamp = dt.timestamp() 22 | return s 23 | 24 | def timestamp(self): 25 | return self._timestamp 26 | 27 | def datetime(self): 28 | return datetime.fromisoformat(self) 29 | 30 | def __int__(self): 31 | return int(self._timestamp) 32 | 33 | @classmethod 34 | def now(cl): 35 | return DateTime(datetime.now()) 36 | 37 | 38 | class TimeUtil(object): 39 | """ 40 | Miscellaneous time-related functions 41 | """ 42 | 43 | def sleep(self, secs): 44 | """sleep() proxy - reimplement in subclass for testing""" 45 | 46 | isecs = int(secs) if secs else 0 47 | if isecs > 0: 48 | sleep(isecs) 49 | 50 | def now(self): 51 | """datetime.now() proxy - reimplement in subclass for testing""" 52 | return datetime.now() 53 | 54 | def timestamp(self, date_time=None): 55 | if date_time is None: 56 | date_time = datetime.now() 57 | return date_time.timestamp() 58 | 59 | def future_time(self, seconds, basetime=None): 60 | """Return datetime value given number of seconds in the future""" 61 | if basetime is None: 62 | basetime = self.now() 63 | elif isinstance(basetime,float): 64 | basetime = datetime.fromtimestamp(basetime/1000) 65 | 66 | return basetime if seconds == 0 \ 67 | else basetime + timedelta(seconds=int(seconds)) 68 | 69 | def timestamp_to_isoformat(self, timestamp): 70 | dt = datetime.fromtimestamp(timestamp) 71 | return dt.isoformat() 72 | -------------------------------------------------------------------------------- /src/organization.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, strip_key_prefix, process_parms) 2 | 3 | 4 | class AMIEOrg(AMIEParmDescAware,dict): 5 | """ 6 | A class used to describe operations on AMIE's concept of an 7 | 'organization'. 8 | """ 9 | 10 | @process_parms( 11 | allowed=[ 12 | 'OrgCode', 13 | 'Organization', 14 | 'StreetAddress', 15 | 'StreetAddress2', 16 | 'City', 17 | 'State', 18 | 'Country', 19 | 'Zip', 20 | ], 21 | required=[ 22 | 'OrgCode', 23 | 'Organization', 24 | ]) 25 | def __init__(self, *args, **kwargs): 26 | """lookup_org() result object 27 | 28 | The site client implementation should use this to create the result of 29 | lookup_org(). 30 | """ 31 | 32 | dict.__init__(self,**kwargs) 33 | 34 | class LookupOrg(AMIEParmDescAware,dict): 35 | 36 | @process_parms( 37 | allowed=[ 38 | 'OrgCode', 39 | ], 40 | required=[ 41 | 'OrgCode', 42 | ]) 43 | def __init__(self, *args, **kwargs) -> dict: 44 | """Validate, filter, and transform arguments to ``lookup_org()``""" 45 | return dict.__init__(self,**kwargs) 46 | 47 | class ChooseOrAddOrg(AMIEParmDescAware,dict): 48 | """ 49 | A class used when specifying an organization that will be known to AMIE 50 | """ 51 | 52 | @process_parms( 53 | allowed=[ 54 | 'amie_transaction_id', 55 | 'amie_packet_id', 56 | 'job_id', 57 | 'amie_packet_type', 58 | 'task_name', 59 | 'timestamp', 60 | 61 | 'City', 62 | 'Country', 63 | 'Organization', 64 | 'OrgCode', 65 | 'State', 66 | ], 67 | required=[ 68 | 'amie_transaction_id', 69 | 'amie_packet_id', 70 | 'job_id', 71 | 'amie_packet_type', 72 | 'task_name', 73 | 'timestamp', 74 | 75 | ['OrgCode','Organization'], 76 | ]) 77 | def __init__(self, **kwargs) -> dict: 78 | """Validate, filter, and transform arguments to ``choose_or_add_org()``""" 79 | dict.__init__(self, **kwargs) 80 | 81 | -------------------------------------------------------------------------------- /src/miscfuncs.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import pprint 3 | import re 4 | import xml.etree.ElementTree as ET 5 | 6 | _re_obj = re.compile('^<[^ ]+ object at 0x[0-9a-f]+>$') 7 | _pp = pprint.PrettyPrinter(indent=2) 8 | 9 | class Prettifiable(ABC): 10 | """Abstract Base Class that define pformat() and vformat methods""" 11 | 12 | @abstractmethod 13 | def pformat(self): 14 | """Abstract method to convert object to formatted string""" 15 | pass 16 | 17 | def vpformat(self): 18 | """Convert object to verbose formatted string""" 19 | self.pformat() 20 | 21 | def get_first_nonEmpty(kwargs, *args): 22 | for arg in args: 23 | value = kwargs.get(arg,None) 24 | if value: 25 | return value 26 | return None 27 | 28 | def truthy(val): 29 | """Return True if the given value looks to be true, False otherwise""" 30 | if val is None: 31 | return False 32 | elif type(val) is str: 33 | lval = val.lower() 34 | if lval == "" or lval == "0" or \ 35 | lval == "false" or lval == "f" or \ 36 | lval == "no" or lval == "n": 37 | return False 38 | return True 39 | else: 40 | b = True if val else False 41 | return b 42 | 43 | def pformat(arg, **kwargs): 44 | """Use pprint.PrettyPrinter(indent=2) to convert args to a formatted string 45 | """ 46 | return _pp.pformat(arg,kwargs) 47 | 48 | def to_expanded_string(val): 49 | """Easy-to-use conversion to formatted string, useful for debugging""" 50 | if val is None: 51 | return '(None)' 52 | elif isinstance(val,str): 53 | return _format_if_XML_or_return(val) 54 | elif isinstance(val,(float, int, bool)): 55 | return str(val) 56 | elif isinstance(val,Prettifiable): 57 | valdump = val.pformat() 58 | else: 59 | valdump = _pp.pformat(val) 60 | if _re_obj.match(valdump): 61 | valdump = valdump + ":\n" + _pp.pformat(vars(val)) 62 | 63 | return valdump 64 | 65 | def _format_if_XML_or_return(val): 66 | if val[0:1] == '<' and val[-1] == '>': 67 | try: 68 | t = ET.XML(val) 69 | ET.indent(t) 70 | xmlstr = ET.tostring(t, encoding='unicode') 71 | return xmlstr 72 | except ET.ParseError: 73 | pass 74 | return val 75 | 76 | 77 | -------------------------------------------------------------------------------- /tests/fixtures/request_account_create.py: -------------------------------------------------------------------------------- 1 | RAC_PKT_1 = { 2 | 'DATA_TYPE': 'Packet', 3 | 'type_id': 16, 4 | 'type': 'request_account_create', 5 | 'header': { 6 | 'expected_reply_list': [ 7 | {'type': 'notify_account_create', 'timeout': 30240} 8 | ], 9 | 'packet_id': 1, 10 | 'trans_rec_id': 87139097, 11 | 'transaction_id': 244206, 12 | 'packet_rec_id': 174709745, 13 | 'local_site_name': 'PSC', 14 | 'remote_site_name': 'SDSC', 15 | 'originating_site_name': 'SDSC', 16 | 'outgoing_flag': False, 17 | 'transaction_state': 'in-progress', 18 | 'packet_state': None, 19 | 'packet_timestamp': '2021-08-24T14:47:51.507Z', 20 | }, 21 | 'body': { 22 | 'AcademicDegree': [ 23 | {'Field': 'Computer and Computation Research', 'Degree': 'MS'} 24 | ], 25 | 'SitePersonId': [ 26 | {'PersonID': 'vraunak', 'Site': 'X-PORTAL'}, 27 | {'PersonID': 'vraunak', 'Site': 'XD-ALLOCATIONS'}, 28 | {'PersonID': 'RAUNAK12P', 'Site': 'PSC'}, 29 | {'PersonID': '112157', 'Site': 'SDSC'} 30 | ], 31 | 'RoleList': ['allocation_manager'], 32 | 'UserDnList': [ 33 | '/C=US/O=Pittsburgh Supercomputing Center/CN=Vikas Raunak', 34 | '/C=US/O=National Center for Supercomputing Applications/CN=Vikas Raunak' 35 | ], 36 | 'UserPersonID': '112157', 37 | 'NsfStatusCode': 'GS', 38 | 'UserOrgCode': '0032425', 39 | 'UserOrganization': 'Carnegie Mellon University', 40 | 'UserTitle': '', 41 | 'UserDepartment': 'SCS', 42 | 'UserLastName': 'Raunak', 43 | 'UserMiddleName': '', 44 | 'UserFirstName': 'Vikas', 45 | 'UserCountry': '9US', 46 | 'UserState': 'PA', 47 | 'UserZip': '15213', 48 | 'UserStreetAddress': 'Craig Street, Carnegie Mellon University, Pittsburgh 15213', 49 | 'UserCity': 'Pittsburgh', 50 | 'UserEmail': 'vraunak@andrew.cmu.edu', 51 | 'UserBusinessPhoneNumber': '4124781149', 52 | 'UserGlobalID': '71691', 53 | 'UserFavoriteColor': 'blue', 54 | 'AllocatedResource': 'comet-gpu.sdsc.xsede', 55 | 'UserRequestedLoginList': [''], 56 | 'ResourceList': ['comet-gpu.sdsc.xsede'], 57 | 'UserPasswordAccessEnable': '1', 58 | 'GrantNumber': 'IRI120015', 59 | 'ProjectID': 'CMU139' 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/loopdelay.py: -------------------------------------------------------------------------------- 1 | from misctypes import (DateTime, TimeUtil) 2 | from miscfuncs import to_expanded_string 3 | 4 | class WaitParms(object): 5 | def __init__(self, auto_update_delay, human_action_delay, idle_delay, 6 | timeutil=None): 7 | # Keep a timeutil object so that it can be easily mocked 8 | if timeutil is None: 9 | timeutil = TimeUtil() 10 | self.auto_update_delay = auto_update_delay 11 | self.human_action_delay = human_action_delay 12 | self.idle_delay = idle_delay 13 | self.timeutil = timeutil 14 | 15 | def get_timeutil(self): 16 | return self.timeutil 17 | 18 | class LoopDelay(object): 19 | def __init__(self, wait_parms, target_time=None): 20 | if target_time: 21 | target_time = self.now() 22 | # Keep a timeutil object so that it can be easily mocked 23 | self.timeutil = wait_parms.get_timeutil() 24 | self.wait_parms = wait_parms 25 | self.target_time = target_time 26 | 27 | def now(self): 28 | return self.timeutil.now() 29 | 30 | def calculate_target_time(self, 31 | base_time=None, 32 | immediate=False, 33 | expect_auto_response=False, 34 | expect_human_action=False): 35 | if base_time == None: 36 | base_time = self.now() 37 | self.base_time = base_time 38 | if immediate: 39 | self.target_time = base_time 40 | return 41 | elif expect_auto_response: 42 | delay = self.wait_parms.auto_update_delay 43 | elif expect_human_action: 44 | delay = self.wait_parms.human_action_delay 45 | else: 46 | delay = self.wait_parms.idle_delay 47 | self.target_time = self.timeutil.future_time(delay, base_time) 48 | 49 | def get_base_time(self): 50 | return self.base_time 51 | 52 | def set_target_time(self, target_time): 53 | self.target_time = target_time 54 | 55 | def get_target_time(self): 56 | return self.target_time 57 | 58 | def wait_secs(self): 59 | """Return positive seconds to wait until target_time, or None""" 60 | 61 | if self.target_time is None: 62 | return None 63 | currtime = self.timeutil.now() 64 | wait = int((self.target_time - currtime).total_seconds()) 65 | return wait if wait > 0 else None 66 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_QUALIFIER=:3.9-slim-bullseye 2 | FROM python${PYTHON_QUALIFIER} 3 | 4 | USER root 5 | 6 | RUN apt-get -y --allow-releaseinfo-change update && \ 7 | apt-get -y install \ 8 | curl \ 9 | git \ 10 | make 11 | 12 | ARG PACKAGE=amiemediator 13 | ARG IMAGE=ghcr.io/ncar/amiemediator 14 | ARG IMAGE_VERSION=snapshot 15 | ARG BRANCH=main 16 | ARG PACKAGE_DIR=/usr/local/amiemediator 17 | 18 | # 19 | # Define the timezone via the TZ build arg. 20 | # 21 | ARG TZ=America/Denver 22 | 23 | # 24 | # We define a default non-root user to run the container as. This can be 25 | # used for testing, etc. 26 | # 27 | ARG PYUSER=pyuser 28 | ARG PYUSERID=900 29 | ARG PYGROUP=pyuser 30 | ARG PYGROUPID=900 31 | 32 | ENV TZ=${TZ} \ 33 | PACKAGE=amiemediator \ 34 | PACKAGE_DIR=/usr/local/amiemediator \ 35 | PYUSER=${PYUSER} \ 36 | PYUSERID=${PYUSERID} \ 37 | PYGROUP=${PYGROUP} \ 38 | PYGROUPID=${PYGROUPID} \ 39 | PYTHONPYCACHEPREFIX=/tmp \ 40 | PYTHONPATH=${PACKAGE_DIR}/src \ 41 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/amiemediator/bin 42 | 43 | RUN mkdir -p ${PACKAGE_DIR} \ 44 | ${PACKAGE_DIR}/bin \ 45 | ${PACKAGE_DIR}/src \ 46 | ${PACKAGE_DIR}/test 47 | 48 | COPY config.ini pip-packages entrypoint.sh \ 49 | ${PACKAGE_DIR}/ 50 | COPY bin ${PACKAGE_DIR}/bin/ 51 | COPY src ${PACKAGE_DIR}/src 52 | COPY tests ${PACKAGE_DIR}/tests/ 53 | COPY runtests ${PACKAGE_DIR}/ 54 | 55 | RUN pip install --upgrade pip 56 | RUN while read pkg ; do \ 57 | pip install --upgrade --root-user-action=ignore ${pkg} ; \ 58 | done < ${PACKAGE_DIR}/pip-packages 59 | 60 | WORKDIR ${PACKAGE_DIR} 61 | 62 | # 63 | # Set up timezone. 64 | # Set up non-root user. 65 | # 66 | RUN set -e ; \ 67 | rm -f /etc/localtime ; \ 68 | ln -s /usr/share/zoneinfo/${TZ} /etc/localtime ; \ 69 | cp /etc/localtime /usr/local/etc/localtime ; \ 70 | echo "${TZ}" >/usr/local/etc/TZ ; \ 71 | POSIX_TZ=`tr '\000' '\n' /usr/local/etc/POSIX_TZ ; \ 74 | addgroup --gid $PYUSERID $PYUSER ; \ 75 | adduser --disabled-password \ 76 | --uid $PYUSERID \ 77 | --gid $PYGROUPID \ 78 | --gecos "PY package user" \ 79 | --home /home/$PYUSER \ 80 | --shell /bin/bash \ 81 | $PYUSER ; \ 82 | chown -R $PYUSERID:$PYGROUPID ${PACKAGE_DIR} 83 | 84 | USER $PYUSER 85 | 86 | ENTRYPOINT [ "/usr/local/amiemediator/entrypoint.sh" ] 87 | -------------------------------------------------------------------------------- /venv-init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROG=venv-init 3 | DESC="Initialize a python virtual environment for amiemediator" 4 | USAGE1="$PROG service_provider_dir" 5 | USAGE2="$PROG -h|--help" 6 | HELP_TEXT=" 7 | This script initializes a python virtual environment directory for 8 | amiemediator. It requires that python (v3.9 or higher) be installed 9 | on the host. 10 | 11 | Running amiemediator in a virtual environment is an alternative to 12 | running it in a container; it can be especially advantageous when using 13 | a python IDE. 14 | 15 | How to attach a serviceprovider? 16 | 17 | -v|--venv-dir=venv_dir (default=venv.d) 18 | -s|--sp-dir=sp_dir 19 | 20 | sp_dir is assumed to contain a package directory containing the service provider 21 | python modules, and optionally a 'pip-packages' file. 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | If the first argument is -h or --help, the script will display help 30 | text for the \"${PROG}\" script and quit. Otherwise, if the first 31 | argument starts with \"-\", the \"amie\" script will be run with 32 | all \"${PROG}\" arguments passed to \"amie\". Otherwise, the 33 | \"${PROG}\" arguments will be run as a command. For example, 34 | \"run run-all-tests\" will run the \"run-all-tests\" script in the 35 | amiemediator container. 36 | 37 | If the \"amie\" script is run, \"${PROG}\" will also check if 38 | the \"-c|--configfile\" argument is included in the argument list; 39 | if it is, \"${PROG}\" will ensure that the configuration file is mounted 40 | in the container. 41 | 42 | ENVIRONMENT 43 | IMAGE 44 | The name of the image to use instead of \"ghcr.io/ncar/${PACKAGE}\". 45 | PACKAGE_DIR 46 | The amiemediator top-level directory. Default is the directory 47 | containing the $PROG script. 48 | " 49 | 50 | SCRIPTDIR=`cd \`dirname $0\`; /bin/pwd` 51 | base_candidates=" 52 | ${PACKAGE_DIR} 53 | . 54 | .. 55 | ${SCRIPTDIR}/.. 56 | ${SCRIPTDIR}/.. 57 | ${SCRIPTDIR}/../.. 58 | " 59 | 60 | MEDIATOR_DIR= 61 | for base_candidate in ${base_candidates} ; do 62 | if [ -d "${base_candidate}" ] && [ -f "${base_candidate}/Dockerfile" ] && 63 | [ -d "${base_candidate}/mediator" ] 64 | then 65 | MEDIATOR_DIR=`cd "${base_candidate}" ; /bin/pwd` 66 | break 67 | fi 68 | done 69 | if [ ":${MEDIATOR_DIR}" = ":" ] ; then 70 | echo "${PROG}: cannot determine amiemediator package directory" >&2 71 | exit 1 72 | fi 73 | PACKAGE_DIR=${MEDIATOR_DIR} 74 | export PACKAGE_DIR 75 | 76 | VENV_DIR="${PACKAGE_DIR}/venv" 77 | 78 | -------------------------------------------------------------------------------- /runtests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROG=runtests 3 | DESC="Run tests" 4 | USAGE1="$PROG [tests...]" 5 | USAGE2="$PROG -h|--help" 6 | HELP_TEXT=" 7 | This script will run all tests named on the command line, or all tests 8 | found under directories named on the command line. If no arguments 9 | are given, the \"tests\" directory under the script's directory will be 10 | used. 11 | 12 | All tests must be executable scripts with names starting with \"t_\". 13 | The return value of all scripts is expected to be the number of failed 14 | tests. 15 | 16 | If the first argument is -h or --help, the script will display help 17 | text and quit. 18 | 19 | ENVIRONMENT 20 | PACKAGE_DIR 21 | The amiemediator top-level directory. Default is the directory 22 | containing the $PROG script. 23 | PYTHONPATH 24 | Where python will search for packages; if empty, the variable will 25 | be set to \"/src:/tests\", where is \${PACKAGE_DIR}; 26 | otherwise, \":/src:/tests\" will be appended to the 27 | original value. 28 | " 29 | SCRIPTDIR=`cd \`dirname $0\`; pwd` 30 | USAGE="Usage: 31 | $USAGE1 32 | $USAGE2 33 | " 34 | case $1 in 35 | '') 36 | set ${SCRIPTDIR}/tests ;; 37 | -h|--help) 38 | cat <&2 51 | exit 1 ;; 52 | esac 53 | 54 | PACKAGE_DIR="${PACKAGE_DIR:-${SCRIPTDIR}}" 55 | if [ ":${PYTHONPATH}" ] ; then 56 | PYTHONPATH="${PYTHONPATH}:${PACKAGE_DIR}/src:${PACKAGE_DIR}/tests" 57 | else 58 | PYTHONPATH="${PACKAGE_DIR}/src:${PACKAGE_DIR}/tests" 59 | fi 60 | export PYTHONPATH 61 | tstamp=`date +%s` 62 | STATFILE=/tmp/run-all-tests.$$.${tstamp} 63 | echo 0 0 >${STATFILE} 64 | trap "read nfiles trc <${STATFILE} ; rm -f ${STATFILE} ; exit ${trc}" 0 1 2 3 15 65 | 66 | for arg in "$@" ; do 67 | if [ -d "${arg}" ] ; then 68 | find "${arg}" -type f -name 't_*' ! -name '*~' -print 69 | else 70 | echo "${arg}" 71 | fi 72 | done | while read path ; do 73 | if [ ! -e ${path} ] ; then 74 | echo "${path}: no such file" >&2 75 | rc=1 76 | elif [ ! -f ${path} ] ; then 77 | echo "${path}: not a regular file" >&2 78 | rc=1 79 | elif [ ! -x ${path} ] ; then 80 | echo "${path}: file not executable" >&2 81 | rc=1 82 | else 83 | echo ${path}: 84 | ${path} 85 | rc=$? 86 | fi 87 | read nfiles trc <${STATFILE} 88 | nfiles=`expr ${nfiles} + 1` 89 | if [ ${rc} != 0 ] ; then 90 | trc=`expr ${trc} + ${rc}` 91 | fi 92 | echo "${nfiles} ${trc}" >${STATFILE} 93 | done 94 | read nfiles trc <${STATFILE} 95 | echo "${nfiles} files, ${trc} failures" 96 | trap '' 0 97 | rm -f ${STATFILE} 98 | exit ${rc} 99 | -------------------------------------------------------------------------------- /tests/t_parmdesc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import json 4 | from misctypes import DateTime 5 | from amieparms import (AMIEParmDescAware, process_parms) 6 | 7 | def upper(str): 8 | return str.upper() 9 | 10 | class Target(AMIEParmDescAware): 11 | 12 | parm2type = { 13 | 'name': str, 14 | 'id': int, 15 | 'timestamp': DateTime 16 | } 17 | parm2doc = { 18 | 'name': 'myname', 19 | 'id': 'myid', 20 | 'timestamp': 'mytimestamp' 21 | } 22 | 23 | @process_parms( 24 | allowed=[ 25 | 'name', 26 | 'id', 27 | 'timestamp', 28 | 'Comment', 29 | ], 30 | required=[ 31 | 'name', 32 | 'id' 33 | ]) 34 | def __init__(self,*args,**kwargs): 35 | """Test Target 36 | 37 | This is a dummy object for testing ParmDescAware 38 | :return: Nothing 39 | """ 40 | 41 | self.args = args 42 | self.kwargs = kwargs 43 | 44 | 45 | class TestParmDesc(unittest.TestCase): 46 | def test_init(self): 47 | inparms = { 48 | 'name': 'myname', 49 | 'id': 7, 50 | 'timestamp': '2023-01-01T12:00:00-07:00', 51 | 'Comment': 'defined in ParmDescAware', 52 | 'extra': 'not used' 53 | } 54 | t = Target(**inparms) 55 | 56 | self.assertTrue(isinstance(t,Target), 57 | msg='Constructor failed') 58 | args = t.args 59 | kwargs = t.kwargs 60 | 61 | self.assertEqual(len(args),0, 62 | msg='positional parms were passed in') 63 | self.assertTrue('name' in kwargs, 64 | msg='name parm not set') 65 | self.assertEqual(kwargs['name'],'myname', 66 | msg='name parm not set') 67 | self.assertEqual(kwargs['id'],7, 68 | msg='id parm not set') 69 | self.assertEqual(kwargs['timestamp'],'2023-01-01T12:00:00-07:00', 70 | msg='timestamp parm not set') 71 | self.assertTrue(isinstance(kwargs['timestamp'],DateTime), 72 | msg='timestamp parm not a DateTime instance') 73 | self.assertEqual(kwargs['Comment'],'defined in ParmDescAware', 74 | msg='Inherited Comment parm not set') 75 | self.assertFalse('extra' in kwargs, 76 | msg='extra parm passed in') 77 | 78 | init_doc = Target.__init__.__doc__ 79 | 80 | expected_doc = "Test Target\n" + \ 81 | "\n" + \ 82 | " This is a dummy object for testing ParmDescAware\n" + \ 83 | "\n" + \ 84 | " :param name: myname\n" + \ 85 | " :type name: str\n" + \ 86 | " :param id: myid\n" + \ 87 | " :type id: int\n" + \ 88 | " :param timestamp: mytimestamp\n" + \ 89 | " :type timestamp: DateTime, optional\n" + \ 90 | " :param Comment: (Unknown)\n" + \ 91 | " :type Comment: str, optional\n" + \ 92 | " :return: Nothing" 93 | 94 | self.assertEqual(init_doc,expected_doc, 95 | msg="__init__ docstring not supplemented") 96 | 97 | if __name__ == '__main__': 98 | unittest.main() 99 | -------------------------------------------------------------------------------- /src/handler/request_account_create.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import (truthy, get_first_nonEmpty) 5 | import handler.subtasks as sub 6 | 7 | class RequestAccountCreate(PacketHandler, packet_type="request_account_create"): 8 | 9 | def work(self, apacket): 10 | """Handle a "request_account_create" packet 11 | 12 | :param apacket: dict with extended packet data 13 | :type apacket: ActionablePacket 14 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 15 | to be sent back 16 | """ 17 | 18 | spa = self.sp_adapter 19 | 20 | # 21 | # The lower-case parameters we collect initially (e.g. "person_id", 22 | # "org_code", etc) represent the dynamic state of this request. The 23 | # first time work() is called, none of these "dynamic state" parameters 24 | # will be defined. On re-entry to work(), it is possible that some or 25 | # all or them will be defined. 26 | # 27 | org_code = apacket.get('org_code',None) 28 | if org_code is None: 29 | ts = sub.define_org_code(spa, apacket,'User') 30 | if ts: 31 | return ts 32 | org_code = apacket['org_code'] 33 | 34 | person_id = apacket.get('person_id',None) 35 | if person_id is None: 36 | ts = sub.define_person(spa, apacket,'User') 37 | if ts: 38 | return ts 39 | person_id = apacket['person_id'] 40 | site_org = apacket['site_org'] 41 | apacket['PersonID'] = person_id 42 | 43 | person_active = apacket.get('person_active',False) 44 | if not person_active: 45 | ts = sub.activate_person(spa, apacket,'User') 46 | if ts: 47 | return ts 48 | person_active = apacket['person_active'] 49 | 50 | remote_site_login = apacket.get('remote_site_login',False) 51 | if not remote_site_login: 52 | ts = sub.define_account(spa, apacket,'User') 53 | if ts: 54 | return ts 55 | remote_site_login = apacket['remote_site_login'] 56 | account_activity_time = apacket['account_activity_time'] 57 | project_id = apacket['project_id'] 58 | 59 | person_id = get_first_nonEmpty(apacket,'person_id','PersonID') 60 | apacket['PersonID'] = person_id 61 | project_id = get_first_nonEmpty(apacket,'project_id','ProjectID') 62 | apacket['ProjectID'] = project_id 63 | user_notified = apacket.get('user_notified', None) 64 | if user_notified is None: 65 | ts = sub.notify_user(spa, apacket) 66 | if ts: 67 | return ts 68 | 69 | nac = apacket.create_reply_packet() 70 | ad = apacket.get('AcademicDegree',None) 71 | if ad: 72 | nac.AcademicDegree = ad 73 | nac.ResourceList = apacket['ResourceList'] 74 | nac.UserFirstName = apacket['UserFirstName'] 75 | nac.UserLastName = apacket['UserLastName'] 76 | nac.UserOrganization = apacket['UserOrganization'] 77 | nac.UserOrgCode = apacket['UserOrgCode'] 78 | 79 | nac.AccountActivityTime = account_activity_time 80 | nac.ProjectID = project_id 81 | nac.UserPersonID = person_id 82 | nac.UserRemoteSiteLogin = remote_site_login 83 | 84 | return nac 85 | -------------------------------------------------------------------------------- /src/filewait.py: -------------------------------------------------------------------------------- 1 | from abc import (ABC, abstractmethod) 2 | import stat 3 | import signal, os, sys, errno 4 | import time 5 | 6 | class FileWaiterFileType(Exception): 7 | """Exception raised when wait file exists and is wrong type""" 8 | pass 9 | 10 | 11 | class FileWaiter(object): 12 | implem = None 13 | 14 | def __init__(self, path): 15 | """Use a file to wait for another process to release us 16 | 17 | :param path: the path of the file to use for synchronization 18 | :type path: str 19 | """ 20 | if not FileWaiter.implem: 21 | if os.name == "posix": 22 | FileWaiter.implem = FIFOFileWaiter(path) 23 | else: 24 | FileWaiter.implem = PollingFileWaiter(path) 25 | 26 | def wait(self, max_secs=600): 27 | """Wait for another process to release us 28 | 29 | :param max_secs: Maximum time to wait, in seconds 30 | :type max_secs: int 31 | :return: True if we were released, False if we timed out 32 | """ 33 | FileWaiter.implem.wait(max_secs) 34 | 35 | def release(self): 36 | """Release another process that might be waiting""" 37 | FileWaiter.implem.release() 38 | 39 | class FileWaiterImplABC(ABC): 40 | 41 | @abstractmethod 42 | def wait(self, max_secs): 43 | pass 44 | 45 | @abstractmethod 46 | def release(self): 47 | pass 48 | 49 | class FIFOFileWaiter(FileWaiterImplABC): 50 | 51 | def __init__(self, path): 52 | self.path = path 53 | if os.path.exists(self.path): 54 | if stat.S_ISFIFO(os.stat(self.path).st_mode): 55 | return 56 | self.implem = PollingFileWaiter() 57 | else: 58 | os.mkfifo(self.path) 59 | 60 | def wait(self, max_secs): 61 | pid = os.fork() 62 | if pid == 0: 63 | signal.alarm(max_secs) 64 | fd = os.open(self.path, os.O_RDONLY) 65 | os._exit(0) 66 | (wpid, status) = os.waitpid(pid, 0) 67 | signo = status & 255 68 | if signo: 69 | return False 70 | 71 | return True 72 | 73 | def release(self): 74 | try: 75 | fd = os.open(self.path, os.O_RDWR | os.O_NONBLOCK) 76 | os.close(fd) 77 | except OSError as e: 78 | if e.errno != errno.ENXIO: 79 | raise e 80 | 81 | class PollingFileWaiter(FileWaiterImplABC): 82 | 83 | def __init__(self, path): 84 | self.path = path 85 | if os.path.exists(self.path): 86 | if stat.S_ISREG(os.stat(self.path).st_mode): 87 | return 88 | raise FileWaiterFileType() 89 | with open(self.path, 'w') as fp: 90 | # truncate file; zero size implies no process is waiting 91 | pass 92 | 93 | def _get_file_size(self): 94 | statinfo = os.stat(self.path) 95 | return statinfo.ST_SIZE 96 | 97 | def wait(self, max_secs): 98 | with open(self.path, 'a') as fp: 99 | print('', file=fp) 100 | # add a char; non-zero size implies process is waiting 101 | initial_size = self._get_file_size() 102 | size = initial_size 103 | while True: 104 | if size < initial_size: 105 | return True 106 | if max_secs <= 0: 107 | return False 108 | time.sleep(1) 109 | size = self._get_file_size() 110 | max_secs = max_secs - 1 111 | 112 | def release(self): 113 | size = self._get_file_size() 114 | if size == 0: 115 | return 116 | with open(self.path, 'w') as fp: 117 | # truncate file 118 | pass 119 | 120 | 121 | -------------------------------------------------------------------------------- /tests/t_miscfuncs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pprintpp 3 | pprintpp.monkeypatch() 4 | import pprint 5 | import unittest 6 | from miscfuncs import (truthy, pformat, to_expanded_string) 7 | import miscfuncs 8 | 9 | class TestData(object): 10 | def __init__(self): 11 | self.foo = 'bar' 12 | 13 | class TestTruthy(unittest.TestCase): 14 | 15 | def test_truthy(self): 16 | self.assertTrue(truthy(True), 17 | msg='truthy(True) is false'); 18 | self.assertTrue(truthy(1), 19 | msg='truthy(1) is false'); 20 | self.assertTrue(truthy("1"), 21 | msg='truthy("1") is false'); 22 | self.assertTrue(truthy("true"), 23 | msg='truthy("true") is false'); 24 | self.assertTrue(truthy("TRUE"), 25 | msg='truthy("TRUE") is false'); 26 | self.assertTrue(truthy([0]), 27 | msg='truthy([0]) is false'); 28 | self.assertTrue(truthy({"":0}), 29 | msg='truthy({"":0}) is false'); 30 | self.assertFalse(truthy(""), 31 | msg='truthy("") is true'); 32 | self.assertFalse(truthy(0), 33 | msg='truthy(0) is true'); 34 | self.assertFalse(truthy([]), 35 | msg='truthy([]) is true'); 36 | self.assertFalse(truthy({}), 37 | msg='truthy({}) is true'); 38 | self.assertFalse(truthy("0"), 39 | msg='truthy("0") is true'); 40 | self.assertFalse(truthy("false"), 41 | msg='truthy("false") is true'); 42 | self.assertFalse(truthy("FALSE"), 43 | msg='truthy("FALSE") is true'); 44 | self.assertFalse(truthy("f"), 45 | msg='truthy("") is true'); 46 | self.assertFalse(truthy("F"), 47 | msg='truthy("F") is true'); 48 | self.assertFalse(truthy("no"), 49 | msg='truthy("no") is true'); 50 | self.assertFalse(truthy("NO"), 51 | msg='truthy("NO") is true'); 52 | self.assertFalse(truthy("n"), 53 | msg='truthy("n") is true'); 54 | self.assertFalse(truthy("N"), 55 | msg='truthy("N") is true'); 56 | 57 | def test_pformat(self): 58 | self.assertEqual(pformat(None),"None", 59 | msg='pformat(None) does not return expected string') 60 | 61 | self.assertEqual(pformat("foo"),"'foo'", 62 | msg="pformat(str) does not return 'str'") 63 | 64 | td = TestData() 65 | tds = pformat(td) 66 | self.assertEqual(tds[0:29],"<__main__.TestData object at ", 67 | msg='pformat(obj) does not return expected string') 68 | 69 | def test_to_expanded_string(self): 70 | self.assertEqual(to_expanded_string(None),"(None)", 71 | msg='to_expanded_string(None) does not return expected string') 72 | 73 | self.assertEqual(to_expanded_string("foo"),"foo", 74 | msg="to_expanded_string(str) does not return 'str'") 75 | 76 | td = TestData() 77 | tds = to_expanded_string(td) 78 | self.assertEqual(tds[0:29],"<__main__.TestData object at ", 79 | msg='pformat(obj) does not return expected string') 80 | self.assertEqual(tds[-14:],"{'foo': 'bar'}", 81 | msg='to_expanded_string(obj) does not return expected string') 82 | 83 | xml = "" 84 | xmls = to_expanded_string(xml) 85 | self.assertEqual(xmls,"\n \n", 86 | msg='pformat(xml) does not return expected string') 87 | 88 | if __name__ == '__main__': 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /src/account.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, process_parms) 2 | 3 | class CreateAccount(AMIEParmDescAware,dict): 4 | 5 | @process_parms( 6 | allowed=[ 7 | 'amie_transaction_id', 8 | 'amie_packet_id', 9 | 'job_id', 10 | 'amie_packet_type', 11 | 'task_name', 12 | 'timestamp', 13 | 14 | 'contract_number', 15 | 'GrantNumber', 16 | 'ProjectID', 17 | 'PersonID', 18 | 'Resource', 19 | ], 20 | required=[ 21 | 'amie_transaction_id', 22 | 'amie_packet_id', 23 | 'job_id', 24 | 'amie_packet_type', 25 | 'task_name', 26 | 'timestamp', 27 | 28 | ['ProjectID', 'contract_number'], 29 | 'PersonID', 30 | 'Resource', 31 | ]) 32 | def __init__(self, *args, **kwargs) -> dict: 33 | """Validate, filter, and transform arguments to ``create_account()``""" 34 | dict.__init__(self, **kwargs) 35 | 36 | class InactivateAccount(AMIEParmDescAware,dict): 37 | 38 | @process_parms( 39 | allowed=[ 40 | 'amie_transaction_id', 41 | 'amie_packet_id', 42 | 'job_id', 43 | 'amie_packet_type', 44 | 'task_name', 45 | 'timestamp', 46 | 47 | 'Comment', 48 | 'PersonID', 49 | 'ProjectID', 50 | 'Resource', 51 | ], 52 | required=[ 53 | 'amie_transaction_id', 54 | 'amie_packet_id', 55 | 'job_id', 56 | 'amie_packet_type', 57 | 'task_name', 58 | 'timestamp', 59 | 60 | 'PersonID', 61 | 'ProjectID', 62 | 'Resource', 63 | ]) 64 | def __init__(self, *args, **kwargs) -> dict: 65 | """Validate, filter, and transform arguments to ``inactivate_account()``""" 66 | dict.__init__(self, **kwargs) 67 | 68 | 69 | class ReactivateAccount(AMIEParmDescAware,dict): 70 | @process_parms( 71 | allowed=[ 72 | 'amie_transaction_id', 73 | 'amie_packet_id', 74 | 'job_id', 75 | 'amie_packet_type', 76 | 'task_name', 77 | 'timestamp', 78 | 79 | 'Comment', 80 | 'PersonID', 81 | 'ProjectID', 82 | 'Resource', 83 | ], 84 | required=[ 85 | 'amie_transaction_id', 86 | 'amie_packet_id', 87 | 'job_id', 88 | 'amie_packet_type', 89 | 'task_name', 90 | 'timestamp', 91 | 92 | 'PersonID', 93 | 'ProjectID', 94 | 'Resource', 95 | ]) 96 | def __init__(self, *args, **kwargs) -> dict: 97 | """Validate, filter, and transform arguments to ``reactivate_account()``""" 98 | dict.__init__(self, **kwargs) 99 | 100 | 101 | class NotifyUser(AMIEParmDescAware,dict): 102 | 103 | @process_parms( 104 | allowed=[ 105 | 'amie_transaction_id', 106 | 'amie_packet_id', 107 | 'job_id', 108 | 'amie_packet_type', 109 | 'task_name', 110 | 'timestamp', 111 | 112 | 'BusinessPhoneNumber', 113 | 'contingent_resources', 114 | 'Email', 115 | 'PersonID', 116 | 'person_id', 117 | 'project_id', 118 | 'ProjectID', 119 | 'RemoteSiteLogin', 120 | 'Resource', 121 | 'ResourceList', 122 | 'resource_name', 123 | 'Username', 124 | 'user_notified', 125 | ], 126 | required=[ 127 | 'amie_transaction_id', 128 | 'amie_packet_id', 129 | 'job_id', 130 | 'amie_packet_type', 131 | 'task_name', 132 | 'timestamp', 133 | 134 | [ 'project_id', 'ProjectID' ], 135 | [ 'person_id', 'RemoteSiteLogin' ], 136 | [ 'Resource', 'ResourceList', 'resource_name' ], 137 | ]) 138 | def __init__(self, *args, **kwargs) -> dict: 139 | """Validate, filter, and transform arguments to ``notify_user()``""" 140 | dict.__init__(self, **kwargs) 141 | -------------------------------------------------------------------------------- /tests/t_organization.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | from misctypes import DateTime 4 | from amieparms import (transform_value,AMIEParmDescAware) 5 | from organization import (AMIEOrg, LookupOrg, ChooseOrAddOrg) 6 | 7 | def _make_test_value(parm2type,key): 8 | t = parm2type[key] 9 | if isinstance(t,list): 10 | val = [] 11 | elif t is list: 12 | val = [] 13 | elif t is dict: 14 | val = {} 15 | elif t is DateTime: 16 | val = DateTime.now() 17 | elif t is not str: 18 | val = t(1) 19 | else: 20 | val = key + "_val" 21 | return val 22 | 23 | class TestAMIEOrg(unittest.TestCase): 24 | def test_constructor(self): 25 | parm2type = AMIEOrg.parm2type 26 | func_info = AMIEOrg.__init__.func_info 27 | inkeys = func_info['allowed'].copy() 28 | inparms = {} 29 | for inkey in inkeys: 30 | inparms[inkey] = _make_test_value(parm2type,inkey) 31 | 32 | inkeys.append('Nosuch') 33 | inparms['Nosuch'] = 'Nosuch_val' 34 | 35 | in2 = inparms.copy() 36 | del in2['OrgCode'] 37 | with self.assertRaises(KeyError, 38 | msg="OrgCode not required"): 39 | o = AMIEOrg(**in2) 40 | 41 | in2 = inparms.copy() 42 | del in2['Organization'] 43 | with self.assertRaises(KeyError, 44 | msg="Organization not required"): 45 | o = AMIEOrg(**in2) 46 | 47 | o = AMIEOrg(**inparms) 48 | 49 | self.assertTrue(isinstance(o,AMIEOrg), msg='constructor failed'); 50 | self.assertFalse('Nosuch' in o, msg='invalid key accepted') 51 | 52 | for key in func_info['allowed']: 53 | expected_val = _make_test_value(parm2type,key) 54 | val = o[key] 55 | self.assertEqual(val,expected_val) 56 | 57 | 58 | class TestLookupOrg(unittest.TestCase): 59 | def test_constructor(self): 60 | parm2type = LookupOrg.parm2type 61 | func_info = LookupOrg.__init__.func_info 62 | inkeys = func_info['allowed'].copy() 63 | inparms = {} 64 | for inkey in inkeys: 65 | inparms[inkey] = _make_test_value(parm2type,inkey) 66 | 67 | inkeys.append('Nosuch') 68 | inparms['Nosuch'] = 'Nosuch_val' 69 | 70 | in2 = inparms.copy() 71 | del in2['OrgCode'] 72 | with self.assertRaises(KeyError, 73 | msg="OrgCode not required"): 74 | o = LookupOrg(**in2) 75 | 76 | o = LookupOrg(**inparms) 77 | 78 | self.assertTrue(isinstance(o,LookupOrg), msg='constructor failed'); 79 | self.assertFalse('Nosuch' in o, msg='invalid key accepted') 80 | 81 | class TestChooseOrAddOrg(unittest.TestCase): 82 | def test_constructor(self): 83 | parm2type = ChooseOrAddOrg.parm2type 84 | func_info = ChooseOrAddOrg.__init__.func_info 85 | inkeys = func_info['allowed'].copy() 86 | inparms = {} 87 | for inkey in inkeys: 88 | inparms[inkey] = _make_test_value(parm2type,inkey) 89 | 90 | inkeys.append('Nosuch') 91 | inparms['Nosuch'] = 'Nosuch_val' 92 | 93 | in2 = inparms.copy() 94 | del in2['job_id'] 95 | with self.assertRaises(KeyError, 96 | msg="job_id not required"): 97 | o = ChooseOrAddOrg(**in2) 98 | 99 | in2 = inparms.copy() 100 | del in2['task_name'] 101 | with self.assertRaises(KeyError, 102 | msg="task_name not required"): 103 | o = ChooseOrAddOrg(**in2) 104 | 105 | in2 = inparms.copy() 106 | del in2['OrgCode'] 107 | del in2['Organization'] 108 | with self.assertRaises(KeyError, 109 | msg="OrgCode or Organization not required"): 110 | o = ChooseOrAddOrg(**in2) 111 | 112 | o = ChooseOrAddOrg(**inparms) 113 | 114 | self.assertTrue(isinstance(o,ChooseOrAddOrg), msg='constructor failed'); 115 | self.assertFalse('Nosuch' in o, msg='invalid key accepted') 116 | 117 | for key in func_info['allowed']: 118 | expected_val = _make_test_value(parm2type,key) 119 | val = o[key] 120 | self.assertEqual(val,expected_val) 121 | 122 | 123 | 124 | if __name__ == '__main__': 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /tests/t_account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import json 4 | from misctypes import DateTime 5 | from account import (CreateAccount, InactivateAccount, ReactivateAccount) 6 | 7 | def _make_test_value(parm2type,key): 8 | t = parm2type[key] 9 | if isinstance(t,list): 10 | val = [] 11 | elif t is list: 12 | val = [] 13 | elif t is dict: 14 | val = {} 15 | elif t is DateTime: 16 | if (key == "StartDate"): 17 | val = DateTime("2023-01-01T00:00:00-07:00") 18 | else: 19 | val = DateTime("2023-01-01T23:59:59-07:00") 20 | elif t is not str: 21 | val = t(1) 22 | else: 23 | val = key + "_val" 24 | return val 25 | 26 | class TestCreateAccount(unittest.TestCase): 27 | def test_constructor(self): 28 | parm2type = CreateAccount.parm2type.copy() 29 | func_info = CreateAccount.__init__.func_info 30 | inkeys = func_info['allowed'].copy() 31 | inparms = {} 32 | for inkey in inkeys: 33 | inparms[inkey] = _make_test_value(parm2type,inkey) 34 | 35 | inkeys.append('Nosuch') 36 | parm2type['Nosuch'] = str 37 | 38 | reqparms = func_info['required'].copy() 39 | for reqparm in reqparms: 40 | in2 = inparms.copy() 41 | del in2[reqparm] 42 | with self.assertRaises(KeyError, 43 | msg=reqparm + " not required"): 44 | parms = CreateAccount(**in2) 45 | 46 | parms = CreateAccount(**inparms) 47 | 48 | self.assertTrue(isinstance(parms,CreateAccount), 49 | msg='constructor() failed'); 50 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 51 | 52 | for key in func_info['allowed']: 53 | expected_val = _make_test_value(parm2type,key) 54 | val = parms[key] 55 | self.assertEqual(val,expected_val) 56 | 57 | class TestInactivateAccount(unittest.TestCase): 58 | def test_constructor(self): 59 | parm2type = InactivateAccount.parm2type.copy() 60 | func_info = InactivateAccount.__init__.func_info 61 | inkeys = func_info['allowed'].copy() 62 | inparms = {} 63 | for inkey in inkeys: 64 | inparms[inkey] = _make_test_value(parm2type,inkey) 65 | 66 | inkeys.append('Nosuch') 67 | parm2type['Nosuch'] = str 68 | 69 | reqparms = func_info['required'].copy() 70 | for reqparm in reqparms: 71 | in2 = inparms.copy() 72 | del in2[reqparm] 73 | with self.assertRaises(KeyError, 74 | msg=reqparm + " not required"): 75 | parms = InactivateAccount(**in2) 76 | 77 | parms = InactivateAccount(**inparms) 78 | 79 | self.assertTrue(isinstance(parms,InactivateAccount), 80 | msg='constructor() failed'); 81 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 82 | 83 | for key in func_info['allowed']: 84 | expected_val = _make_test_value(parm2type,key) 85 | val = parms[key] 86 | self.assertEqual(val,expected_val) 87 | 88 | class TestReactivateAccount(unittest.TestCase): 89 | def test_constructor(self): 90 | parm2type = ReactivateAccount.parm2type.copy() 91 | func_info = ReactivateAccount.__init__.func_info 92 | inkeys = func_info['allowed'].copy() 93 | inparms = {} 94 | for inkey in inkeys: 95 | inparms[inkey] = _make_test_value(parm2type,inkey) 96 | 97 | inkeys.append('Nosuch') 98 | parm2type['Nosuch'] = str 99 | 100 | reqparms = func_info['required'].copy() 101 | for reqparm in reqparms: 102 | in2 = inparms.copy() 103 | del in2[reqparm] 104 | with self.assertRaises(KeyError, 105 | msg=reqparm + " not required"): 106 | parms = ReactivateAccount(**in2) 107 | 108 | parms = ReactivateAccount(**inparms) 109 | 110 | self.assertTrue(isinstance(parms,ReactivateAccount), 111 | msg='constructor() failed'); 112 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 113 | 114 | for key in func_info['allowed']: 115 | expected_val = _make_test_value(parm2type,key) 116 | val = parms[key] 117 | self.assertEqual(val,expected_val) 118 | 119 | 120 | if __name__ == '__main__': 121 | unittest.main() 122 | -------------------------------------------------------------------------------- /tests/t_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import json 4 | from misctypes import DateTime 5 | from project import (Project, FindProjectsByGrant,CreateProject, 6 | InactivateProject,ReactivateProject) 7 | 8 | def _make_test_value(parm2type,key): 9 | t = parm2type[key] 10 | if isinstance(t,list): 11 | val = [] 12 | elif t is list: 13 | val = [] 14 | elif t is dict: 15 | val = {} 16 | elif t is DateTime: 17 | if (key == "StartDate"): 18 | val = DateTime("2023-01-01T00:00:00-07:00") 19 | else: 20 | val = DateTime("2023-01-01T23:59:59-07:00") 21 | elif t is not str: 22 | val = t(1) 23 | else: 24 | val = key + "_val" 25 | return val 26 | 27 | class TestCreateProject(unittest.TestCase): 28 | def test_constructor(self): 29 | parm2type = CreateProject.parm2type.copy() 30 | func_info = CreateProject.__init__.func_info 31 | inkeys = func_info['allowed'].copy() 32 | inparms = {} 33 | for inkey in inkeys: 34 | inparms[inkey] = _make_test_value(parm2type,inkey) 35 | 36 | inkeys.append('Nosuch') 37 | parm2type['Nosuch'] = str 38 | 39 | reqparms = func_info['required'].copy() 40 | for reqparm in reqparms: 41 | in2 = inparms.copy() 42 | del in2[reqparm] 43 | with self.assertRaises(KeyError, 44 | msg=reqparm + " not required"): 45 | parms = CreateProject(**in2) 46 | 47 | parms = CreateProject(**inparms) 48 | 49 | self.assertTrue(isinstance(parms,CreateProject), 50 | msg='constructor() failed'); 51 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 52 | 53 | for key in func_info['allowed']: 54 | expected_val = _make_test_value(parm2type,key) 55 | val = parms[key] 56 | self.assertEqual(val,expected_val) 57 | 58 | class TestInactivateProject(unittest.TestCase): 59 | def test_constructor(self): 60 | parm2type = InactivateProject.parm2type.copy() 61 | func_info = InactivateProject.__init__.func_info 62 | inkeys = func_info['allowed'].copy() 63 | inparms = {} 64 | for inkey in inkeys: 65 | inparms[inkey] = _make_test_value(parm2type,inkey) 66 | 67 | inkeys.append('Nosuch') 68 | parm2type['Nosuch'] = str 69 | 70 | reqparms = func_info['required'].copy() 71 | for reqparm in reqparms: 72 | in2 = inparms.copy() 73 | del in2[reqparm] 74 | with self.assertRaises(KeyError, 75 | msg=reqparm + " not required"): 76 | parms = InactivateProject(**in2) 77 | 78 | parms = InactivateProject(**inparms) 79 | 80 | self.assertTrue(isinstance(parms,InactivateProject), 81 | msg='constructor() failed'); 82 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 83 | 84 | for key in func_info['allowed']: 85 | expected_val = _make_test_value(parm2type,key) 86 | val = parms[key] 87 | self.assertEqual(val,expected_val) 88 | 89 | class TestReactivateProject(unittest.TestCase): 90 | def test_constructor(self): 91 | parm2type = ReactivateProject.parm2type.copy() 92 | func_info = ReactivateProject.__init__.func_info 93 | inkeys = func_info['allowed'].copy() 94 | inparms = {} 95 | for inkey in inkeys: 96 | inparms[inkey] = _make_test_value(parm2type,inkey) 97 | 98 | inkeys.append('Nosuch') 99 | parm2type['Nosuch'] = str 100 | 101 | reqparms = func_info['required'].copy() 102 | for reqparm in reqparms: 103 | in2 = inparms.copy() 104 | del in2[reqparm] 105 | with self.assertRaises(KeyError, 106 | msg=reqparm + " not required"): 107 | parms = ReactivateProject(**in2) 108 | 109 | parms = ReactivateProject(**inparms) 110 | 111 | self.assertTrue(isinstance(parms,ReactivateProject), 112 | msg='constructor() failed'); 113 | self.assertFalse('Nosuch' in parms, msg='invalid key accepted') 114 | 115 | for key in func_info['allowed']: 116 | expected_val = _make_test_value(parm2type,key) 117 | val = parms[key] 118 | self.assertEqual(val,expected_val) 119 | 120 | 121 | if __name__ == '__main__': 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /src/retryingproxy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from requests.exceptions import ConnectionError 3 | from misctypes import TimeUtil 4 | 5 | class RetryingServiceProxyError(Exception): 6 | """Exception raised when RetryingServiceProxy hits an internal error""" 7 | pass 8 | 9 | class MaxRetryError(Exception): 10 | """Exception raised when max retries have been attempted""" 11 | pass 12 | 13 | class RetryingServiceProxy: 14 | """Context Manager class for contacting an external service""" 15 | 16 | @classmethod 17 | def configure(cls, svc, 18 | min_retry_delay, max_retry_delay, retry_time_max, 19 | time_util=None, max_retry_exception=MaxRetryError, 20 | *temporary_exception_classes): 21 | """Configure the class 22 | 23 | :param svc: the service being proxied 24 | :type svc: object 25 | :param min_retry_delay: the minimum number of seconds to wait before 26 | retrying 27 | :type min_retry_delay: int 28 | :param max_retry_delay: the maximum number of seconds to wait before 29 | retrying; the actual retry starts at ``min_retry_delay`` and 30 | doubles with every temporary error until the ``max_retry_delay`` 31 | is reached 32 | :type min_retry_delay: int 33 | :param retry_time_max: the maximum number of seconds to retry 34 | before raising a timeout error 35 | :type retry_time_max: int 36 | :param time_util: the TimeUtil class to use for returning the current 37 | time and for sleeping. Default is :class:`misctypes.TimeUtil` 38 | :type time_util: class, optional 39 | :param max_retry_exception: the Exception class to raise when all the 40 | ``retry_time_max`` time has been reached. Default is 41 | :class:`~retryingproxy.MaxRetryError` 42 | :type max_retry_exception: class, optional 43 | :param temporary_exception_classes: any remaining arguments are 44 | Exception classes that will be recognized as "temporary" errors 45 | that should cause a retry 46 | :type temporary_excpetion_classes: list of class, optional 47 | """ 48 | cls.svc = svc 49 | cls.min_retry_delay = int(min_retry_delay) 50 | cls.max_retry_delay = int(max_retry_delay) 51 | cls.retry_time_max = int(retry_time_max) 52 | cls.time_util = TimeUtil() if time_util is None else time_util 53 | cls.max_retry_exception = max_retry_exception 54 | cls.retry_delay = None 55 | cls.retry_deadline = None 56 | tec = list(temporary_exception_classes) 57 | cls.temp_exception_classes = tec 58 | if ConnectionError not in tec: 59 | cls.temp_exception_classes.append(ConnectionError) 60 | cls.canonical_temp_exception_class = \ 61 | cls.temp_exception_classes[0] 62 | cls.logger = logging.getLogger(__name__) 63 | 64 | def __enter__(self): 65 | cls = self.__class__ 66 | if cls.svc is None: 67 | raise RetryingServiceProxyError("not configured") 68 | if cls.retry_delay is not None: 69 | cls.logger.debug("Sleeping " + str(self.retry_delay) + " sec") 70 | cls.time_util.sleep(self.retry_delay) 71 | return cls.svc 72 | 73 | def __exit__(self, exc_type, exc_value, exc_tb): 74 | cls = self.__class__ 75 | if exc_type is None: 76 | cls.retry_delay = None 77 | cls.retry_deadline = None 78 | return False 79 | 80 | for tecls in self.temp_exception_classes: 81 | if exc_type is tecls: 82 | self._update_retry(exc_value) 83 | if exc_type is not self.canonical_temp_exception_class: 84 | raise self.canonical_temp_exception_class() from exc_value 85 | break 86 | return False 87 | 88 | def _update_retry(self, exc): 89 | cls = self.__class__ 90 | if cls.retry_delay is None: 91 | cls.retry_delay = int(self.min_retry_delay) 92 | cls.retry_deadline = \ 93 | cls.time_util.future_time(int(cls.retry_time_max)) 94 | else: 95 | if cls.time_util.now() > cls.retry_deadline: 96 | cls.retry_delay = None 97 | cls.retry_deadline = None 98 | raise cls.max_retry_exception() from exc 99 | cls.retry_delay *= 2 100 | if cls.retry_delay > cls.max_retry_delay: 101 | cls.retry_delay = cls.max_retry_delay 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from configparser import ConfigParser 4 | 5 | class ConfigError(Exception): 6 | pass 7 | 8 | class ConfigLoader: 9 | """Front-end to ConfigParser 10 | 11 | This class uses ConfigParser to load "ini" files and inject secrets 12 | from files and or environment variables into the configuration. 13 | 14 | To make use of "secrets injection", a configuration file should use 15 | variable interpolation: for example, if a ``[localsite]`` section, needs 16 | to define variable ``password``, it can define it using the syntax 17 | ``password = %(localsite_password)s``, then rely on ConfigLoader to 18 | define ``localsite_password``. It does that as follows: 19 | 20 | ConfigLoader first determines the names of all sections in the 21 | configuration file, and all variables defined in each section. 22 | 23 | It then looks in the directory named in the ``SECRETS_DIR`` environment 24 | variable for files that begin with "
_", where
is a 25 | section name. For each matching file name, it defines a secret variable 26 | with the filename as the name and the file contents as the value. 27 | 28 | It then searches the environment for variables that have names starting 29 | with "_", where is a section name converted to 30 | upper case. For each matching environment variable, it defines a secret 31 | variable with the lower case environment variable name as a name and the 32 | environment variable value as the value. 33 | 34 | Config Loader then uses ConfigParser to load the configuration using the 35 | secret variables as additional defaults, with the expectation that 36 | the configuation uses variable interpolation to define the values of 37 | secrets. ConfigParser will also filter the resulting configuration so 38 | that secrets do not appear in sections that do not use them. 39 | """ 40 | 41 | def loadConfig(configfile) -> dict: 42 | parser = ConfigParser(default_section="NoSuCh",interpolation=None) 43 | parser.read(configfile) 44 | sections = parser.sections() 45 | # print("DEBUG config sections="+str(sections)) 46 | prefixes = [s+"_" for s in sections] 47 | # print("DEBUG config prefixes="+str(prefixes)) 48 | section2vars = {} 49 | for section in sections: 50 | vars = set() 51 | for var in parser[section]: 52 | vars.add(var) 53 | section2vars[section] = vars 54 | default_vars = [] if 'DEFAULT' not in section2vars else section2vars['DEFAULT'] 55 | 56 | defaults_dict = ConfigLoader._load_files_dict('SECRETS_DIR',prefixes) 57 | env_dict = ConfigLoader._load_env_dict(prefixes) 58 | defaults_dict.update(env_dict) 59 | 60 | config = {} 61 | 62 | parser = ConfigParser(defaults=defaults_dict) 63 | parser.read(configfile) 64 | for section in parser.sections(): 65 | config[section] = {} 66 | explicit_vars = section2vars[section] 67 | for var in parser[section]: 68 | if var in explicit_vars or var in default_vars: 69 | config[section][var] = parser[section][var] 70 | return config 71 | 72 | def _load_files_dict(key, prefixes): 73 | secrets_dir = os.environ.get(key,None) 74 | if secrets_dir is None: 75 | return {} 76 | elif not os.path.isdir(secrets_dir): 77 | return {} 78 | files = [f for f in os.listdir(secrets_dir) \ 79 | if os.path.isfile(os.path.join(secrets_dir, f))] 80 | secrets_dict = {}; 81 | failures = [] 82 | for filename in files: 83 | for p in prefixes: 84 | if filename.startswith(p): 85 | path = os.path.join(secrets_dir, filename) 86 | try: 87 | file = open(path,'r') 88 | value = file.read() 89 | secrets_dict[filename] = value.strip() 90 | except Exception as e: 91 | failures.add(filename + ": " + str(e)) 92 | finally: 93 | file.close() 94 | break 95 | if failures: 96 | msg = "Unable to load secrets from {secrets_dir}:\n" + \ 97 | "\n".join(failure) 98 | raise ConfigError(msg) 99 | 100 | return secrets_dict 101 | 102 | def _load_env_dict(prefixes): 103 | ucprefixes = [p.upper() for p in prefixes] 104 | env_dict = {} 105 | for envvar in os.environ: 106 | for p in ucprefixes: 107 | if envvar.startswith(p): 108 | env_dict[envvar.lower()] = os.environ[envvar] 109 | break 110 | return env_dict 111 | -------------------------------------------------------------------------------- /runc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROG=runc 3 | DESC="Run the amiemediator container in \"development mode\"" 4 | USAGE1="$PROG [-s secrets_dir] command..." 5 | USAGE2="$PROG -h|--help" 6 | PACKAGE=amiemediator 7 | PACKAGE_DIR="${PACKAGE_DIR:-/usr/local/${PACKAGE}}" 8 | IMAGE="${IMAGE:-ghcr.io/ncar/${PACKAGE}}" 9 | HELP_TEXT=" 10 | This script runs the indicated command in a container using the 11 | \"ghcr.io/ncar/${PACKAGE}\" image. The directory containing the 12 | \"${PROG}\" script will be mounted at /usr/local/${PACKAGE} in 13 | the container, which allows you to actively edit the python code in the 14 | ${PACKAGE} package outside the container while the container is 15 | running. 16 | 17 | If you execute the \"${PROG}\" script from the script's directory or 18 | under it, the working directory in the container will be at the same 19 | relative position under /usr/local/${PACKAGE}. 20 | 21 | If the first argument is -h or --help, the script will display help 22 | text for the \"${PROG}\" script and quit. Otherwise, if the first 23 | argument starts with \"-\", the \"amie\" script will be run with 24 | all \"${PROG}\" arguments passed to \"amie\". Otherwise, the 25 | \"${PROG}\" arguments will be run as a command. For example, 26 | \"run run-all-tests\" will run the \"run-all-tests\" script in the 27 | amiemediator container. 28 | 29 | If the \"amie\" script is run, \"${PROG}\" will also check if 30 | the \"-c|--configfile\" argument is included in the argument list; 31 | if it is, \"${PROG}\" will ensure that the configuration file is mounted 32 | in the container. 33 | 34 | ENVIRONMENT 35 | IMAGE 36 | The name of the image to use instead of \"ghcr.io/ncar/${PACKAGE}\". 37 | PACKAGE_DIR 38 | The amiemediator top-level directory. Default is the directory 39 | containing the $PROG script. 40 | " 41 | 42 | SCRIPTDIR=`cd \`dirname $0\`; /bin/pwd` 43 | AMIEMED_CMD= 44 | AMIEMED_CONFIG= 45 | CONFIG_MOUNT= 46 | WORKDIR= 47 | SECRETS_DIR=${SECRETS_DIR} 48 | SECRETS_ARGS= 49 | 50 | case $1 in 51 | -h|--help) 52 | cat <&2 82 | exit 1 83 | fi 84 | SECRETS_ARGS="-eSECRETS_DIR=/run/secrets -v${SECRETS_DIR}:/run/secrets:z" 85 | fi 86 | if [ ":${AMIEMED_CMD}" != ":" ] ; then 87 | set : "$@" eNdEnD 88 | shift 89 | while [ ":$1" != ":eNdEnD" ] ; do 90 | arg="$1" 91 | shift 92 | configarg= 93 | case ${arg} in 94 | -s|--site) 95 | SITE="$2" 96 | shift 97 | set : "$@" --site="${SITE}" 98 | shift ;; 99 | -c?*) 100 | configarg=`expr "${arg}" : '-c\(.*\)'` ;; 101 | --configfile=*) 102 | configarg=`expr "${arg}" : '--configfile=\(.*\)'` ;; 103 | -c|--configfile) 104 | configarg="$2" 105 | shift ;; 106 | *) 107 | set : "$@" ${arg} 108 | shift ;; 109 | esac 110 | if [ ":${configarg}" != ":" ] ; then 111 | if [ -f "${configarg}" ] ; then 112 | b=`basename ${configarg}` 113 | d=`dirname ${configarg}` 114 | ad=`cd "$d" ; /bin/pwd` 115 | case ${ad} in 116 | ${SCRIPTDIR}|${SCRIPTDIR}/*) 117 | : ;; 118 | *) 119 | configarg=${ad}/${b} 120 | CONFIG_MOUNT="-v${configarg}:${configarg}:z" ;; 121 | esac 122 | fi 123 | set : "$@" --configfile="${configarg}" 124 | shift 125 | fi 126 | done 127 | shift 128 | fi 129 | 130 | cwd=`/bin/pwd` 131 | case $cwd in 132 | ${SCRIPTDIR}|${SCRIPTDIR}/*) 133 | rcwd=`expr "${cwd}" : "${SCRIPTDIR}\\(.*\\)"` 134 | WORKDIR="-w ${PACKAGE_DIR}${rcwd}" ;; 135 | esac 136 | echo docker run -it --rm \ 137 | ${WORKDIR} ${CONFIG_MOUNT} \ 138 | -v${SCRIPTDIR}:${PACKAGE_DIR}:z ${SECRETS_ARGS} ${IMAGE} "$@" 139 | docker run -it --rm \ 140 | ${WORKDIR} ${CONFIG_MOUNT} \ 141 | -v${SCRIPTDIR}:${PACKAGE_DIR}:z ${SECRETS_ARGS} ${IMAGE} "$@" 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /tests/t_person.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | from misctypes import DateTime 4 | from amieparms import (transform_value,AMIEParmDescAware) 5 | from person import (AMIEPerson, LookupPerson, ChooseOrAddPerson) 6 | 7 | def _make_test_value(parm2type,key): 8 | t = parm2type[key] 9 | if isinstance(t,list): 10 | val = [] 11 | elif t is list: 12 | val = [] 13 | elif t is dict: 14 | val = {} 15 | elif t is DateTime: 16 | val = DateTime.now() 17 | elif t is not str: 18 | val = t(1) 19 | else: 20 | val = key + "_val" 21 | return val 22 | 23 | class TestAMIEPerson(unittest.TestCase): 24 | def test_constructor(self): 25 | parm2type = AMIEPerson.parm2type 26 | func_info = AMIEPerson.__init__.func_info 27 | inkeys = func_info['allowed'].copy() 28 | inparms = {} 29 | for inkey in inkeys: 30 | inparms[inkey] = _make_test_value(parm2type,inkey) 31 | 32 | inkeys.append('Nosuch') 33 | inparms['Nosuch'] = 'Nosuch_val' 34 | 35 | in2 = inparms.copy() 36 | del in2['PersonID'] 37 | with self.assertRaises(KeyError, 38 | msg="PersonID not required"): 39 | p = AMIEPerson(**in2) 40 | 41 | in2 = inparms.copy() 42 | del in2['FirstName'] 43 | del in2['LastName'] 44 | with self.assertRaises(KeyError, 45 | msg="FirstName or LastName not required"): 46 | p = AMIEPerson(**in2) 47 | 48 | p = AMIEPerson(**inparms) 49 | 50 | self.assertTrue(isinstance(p,AMIEPerson), msg='constructor failed'); 51 | self.assertFalse('Nosuch' in p, msg='invalid key accepted') 52 | 53 | for key in func_info['allowed']: 54 | expected_val = _make_test_value(parm2type,key) 55 | val = p[key] 56 | self.assertEqual(val,expected_val) 57 | 58 | 59 | class TestLookupPerson(unittest.TestCase): 60 | def test_constructor(self): 61 | parm2type = LookupPerson.parm2type 62 | func_info = LookupPerson.__init__.func_info 63 | inkeys = func_info['allowed'].copy() 64 | inparms = {} 65 | for inkey in inkeys: 66 | inparms[inkey] = _make_test_value(parm2type,inkey) 67 | 68 | inkeys.append('Nosuch') 69 | inparms['Nosuch'] = 'Nosuch_val' 70 | 71 | in2 = inparms.copy() 72 | del in2['PersonID'] 73 | p = LookupPerson(**in2) 74 | self.assertTrue(isinstance(p,LookupPerson), 75 | msg='constructor failed'); 76 | 77 | in2 = inparms.copy() 78 | del in2['GlobalID'] 79 | p = LookupPerson(**in2) 80 | self.assertTrue(isinstance(p,LookupPerson), 81 | msg='constructor failed'); 82 | 83 | in2 = inparms.copy() 84 | del in2['PersonID'] 85 | del in2['GlobalID'] 86 | with self.assertRaises(KeyError, 87 | msg="PersonID or GlobalID not required"): 88 | p = LookupPerson(**in2) 89 | 90 | p = LookupPerson(**inparms) 91 | 92 | self.assertTrue(isinstance(p,LookupPerson), msg='constructor failed'); 93 | self.assertFalse('Nosuch' in p, msg='invalid key accepted') 94 | 95 | class TestChooseOrAddPerson(unittest.TestCase): 96 | def test_constructor(self): 97 | parm2type = ChooseOrAddPerson.parm2type 98 | func_info = ChooseOrAddPerson.__init__.func_info 99 | inkeys = func_info['allowed'].copy() 100 | inparms = {} 101 | for inkey in inkeys: 102 | inparms[inkey] = _make_test_value(parm2type,inkey) 103 | 104 | inkeys.append('Nosuch') 105 | inparms['Nosuch'] = 'Nosuch_val' 106 | 107 | in2 = inparms.copy() 108 | del in2['job_id'] 109 | with self.assertRaises(KeyError, 110 | msg="job_id not required"): 111 | p = ChooseOrAddPerson(**in2) 112 | 113 | in2 = inparms.copy() 114 | del in2['task_name'] 115 | with self.assertRaises(KeyError, 116 | msg="task_name not required"): 117 | p = ChooseOrAddPerson(**in2) 118 | 119 | in2 = inparms.copy() 120 | del in2['FirstName'] 121 | del in2['LastName'] 122 | with self.assertRaises(KeyError, 123 | msg="FirstName or LastName not required"): 124 | p = ChooseOrAddPerson(**in2) 125 | 126 | in2 = inparms.copy() 127 | del in2['Email'] 128 | with self.assertRaises(KeyError, 129 | msg="Email not required"): 130 | p = ChooseOrAddPerson(**in2) 131 | 132 | in2 = inparms.copy() 133 | del in2['Organization'] 134 | with self.assertRaises(KeyError, 135 | msg="Organization not required"): 136 | p = ChooseOrAddPerson(**in2) 137 | 138 | p = ChooseOrAddPerson(**inparms) 139 | 140 | self.assertTrue(isinstance(p,ChooseOrAddPerson), msg='constructor failed'); 141 | self.assertFalse('Nosuch' in p, msg='invalid key accepted') 142 | 143 | for key in func_info['allowed']: 144 | expected_val = _make_test_value(parm2type,key) 145 | val = p[key] 146 | self.assertEqual(val,expected_val) 147 | 148 | 149 | 150 | if __name__ == '__main__': 151 | unittest.main() 152 | -------------------------------------------------------------------------------- /tests/t_amieparms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | from fixtures.request_account_create import RAC_PKT_1 4 | from fixtures.request_project_create import RPC_PKT_1 5 | from amieclient.packet.base import Packet 6 | from parmdesc import ParmDescAware 7 | from amieparms import (get_packet_keys, 8 | parse_atrid, 9 | strip_key_prefix, 10 | AMIEParmDescAware) 11 | 12 | class TestParmDesc(unittest.TestCase): 13 | 14 | def test_init(self): 15 | # just by loading AMIEParmDescAware, the parm2type map in 16 | # ParmDescAware should have been initialized with defaults 17 | 18 | parm2type = ParmDescAware.parm2type 19 | 20 | self.assertTrue(len(parm2type) > 0, 21 | msg='defaults not loaded') 22 | 23 | self.assertTrue('Comment' in parm2type, 24 | msg='AMIE default not loaded') 25 | 26 | def test_get_packet_keys(self): 27 | packet_dict = RAC_PKT_1 28 | packet = Packet.from_dict(packet_dict) 29 | 30 | tid = packet.transaction_id 31 | os = packet.originating_site_name 32 | rs = packet.remote_site_name 33 | ls = packet.local_site_name 34 | expected_atrid = f"{os}:{rs}:{ls}:{tid}" 35 | expected_job_id = str(packet.packet_rec_id) 36 | expected_pid = str(packet.packet_id) 37 | job_id, atrid, pid = get_packet_keys(packet) 38 | 39 | self.assertEqual(expected_job_id,job_id, 40 | msg="AMIE Packet did not yield expected job_id") 41 | self.assertEqual(expected_atrid,atrid, 42 | msg="AMIE Packet did not yield expected atrid") 43 | self.assertEqual(expected_pid,pid, 44 | msg="AMIE Packet did not yield expected pid") 45 | 46 | packet_dict = RPC_PKT_1 47 | packet = Packet.from_dict(packet_dict) 48 | tid = packet.transaction_id 49 | os = packet.originating_site_name 50 | rs = packet.remote_site_name 51 | ls = packet.local_site_name 52 | prid = packet.packet_rec_id 53 | rid = packet.RecordID 54 | expected_atrid = f"{os}:{rs}:{ls}:{tid}" 55 | expected_job_id = str(packet.packet_rec_id) 56 | expected_pid = str(packet.packet_id) 57 | job_id, atrid, pid = get_packet_keys(packet) 58 | 59 | self.assertEqual(expected_job_d,job_id, 60 | msg="AMIE Packet did not yield expected job_id") 61 | self.assertEqual(expected_atrid,atrid, 62 | msg="AMIE Packet did not yield expected atrid") 63 | self.assertEqual(expected_pid,pid, 64 | msg="AMIE Packet did not yield expected pid") 65 | 66 | nonpacket_dict = { 67 | 'job_id': '12345', 68 | 'amie_transaction_id': 'os:rs:ls:tid', 69 | 'packet_id': 'PID', 70 | } 71 | job_id, atrid, pid = get_packet_keys(nonpacket_dict) 72 | self.assertEqual(nonpacket_dict['job_id'], job_id, 73 | msg="non-Packet dict did not yield expected job_id") 74 | self.assertEqual(nonpacket_dict['amie_transaction_id'],atrid, 75 | msg="AMIE Packet did not yield expected atrid") 76 | self.assertEqual(nonpacket_dict['packet_id'],pid, 77 | msg="AMIE Packet did not yield expected pid") 78 | 79 | def test_parse_atrid(self): 80 | atrid = 'OS:RS:LS:TID' 81 | os, rs, ls, tid = parse_atrid(atrid) 82 | self.assertEqual(os,"OS", 83 | msg="parse_atrid did not extract expected os") 84 | self.assertEqual(rs,"RS", 85 | msg="parse_atrid did not extract expected rs") 86 | self.assertEqual(ls,"LS", 87 | msg="parse_atrid did not extract expected ls") 88 | self.assertEqual(tid,"TID", 89 | msg="parse_atrid did not extract expected tid") 90 | 91 | def test_strip_key_prefix(self): 92 | in_dict={ 93 | 'UserFirstName': 'John', 94 | 'UserLastName': 'Doe', 95 | 'Username': 'johndoe', 96 | 'user_name': 'john_doe', 97 | 'FavoriteColor': 'Blue', 98 | } 99 | out_dict = strip_key_prefix("User",in_dict) 100 | self.assertEqual(len(out_dict),5, 101 | msg="wrong number of elements copied") 102 | self.assertEqual(out_dict['FirstName'],'John', 103 | msg="FirstName not copied") 104 | self.assertEqual(out_dict['LastName'],'Doe', 105 | msg="LastName not copied") 106 | self.assertEqual(out_dict['Username'],'johndoe', 107 | msg="Username not copied") 108 | self.assertEqual(out_dict['name'],'john_doe', 109 | msg="name not copied") 110 | self.assertEqual(out_dict['FavoriteColor'],'Blue', 111 | msg="FavoriteColor not copied") 112 | in_dict={ 113 | 'FavouriteColour': 'Blue. No - AHHH', 114 | } 115 | out_dict = strip_key_prefix("User",in_dict) 116 | self.assertFalse(out_dict is in_dict, 117 | msg="copy not made") 118 | self.assertEqual(out_dict['FavouriteColour'],'Blue. No - AHHH', 119 | msg="FavouriteColour not copied") 120 | 121 | if __name__ == '__main__': 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /bin/test-scenario: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys, os, getopt, logging 3 | import requests 4 | import pprintpp 5 | pprintpp.monkeypatch() 6 | import pprint 7 | from pathlib import Path 8 | from config import ConfigLoader 9 | from miscfuncs import truthy 10 | from amieclient import AMIEClient 11 | from serviceprovider import ServiceProvider 12 | from mediator import AMIEMediator 13 | 14 | PROG = "test-scenario" 15 | PROG_UNL = "=============" 16 | DESC = "Create or reset an AMIE API test scenario" 17 | USAGE = os.environ.get('USAGE',None) 18 | USAGE1 = PROG + " [-c|--configfile=] [-s|--site=] scenario" 19 | USAGE2 = PROG + " -h|--help" 20 | USAGE = f''' 21 | {USAGE1} 22 | 23 | or 24 | 25 | {USAGE2}''' 26 | 27 | AMIE_TEST_URL = "https://a3mdev.xsede.org/amie-api-test" 28 | SCENARIOS = [ 29 | "request_project_reactivate", 30 | "request_account_reactivate", 31 | "request_person_merge", 32 | "request_user_modify" 33 | ] 34 | indent = " " 35 | sep = "``\n" + indent + "``" 36 | SCENARIOS_HELP_LIST = "``" + sep.join(SCENARIOS) + "``" 37 | 38 | INTRO_TEXT = f''' 39 | The ``AMIE-API-Testing.pdf`` document describes how to test your site's AMIE 40 | code using a set of "test scenarios". This utility will create a test scenario 41 | as described in the document, or reset test data. 42 | ''' 43 | LOCALSITE_OPTIONS_TEXT = f''' 44 | The ``-c|--configuration`` and ``-s|--site`` arguments are treated just as the 45 | bin/amie program treats them. Run ``amie --help`` for more details. 46 | 47 | ``-c|--configfile=`` *file* 48 | Configuration (``ini``) file. If not specified, the ``CONFIG_INI`` 49 | environment variable will be checked for the name of a file; otherwise, 50 | ``./config.ini`` is assumed. The ``amie_url`` parameter in the 51 | ``[amieclient]`` section must be: ``{AMIE_TEST_URL}`` 52 | 53 | ``-s|--site=`` *site* 54 | Local site name. If not specified, the configuration file must have a 55 | ``site`` property in a ``[amieclient]`` section.''' 56 | OPTIONS_TEXT = f''' 57 | ``-h|--help`` 58 | Display help test and quit scenario. 59 | 60 | ``scenario`` 61 | The name of the test scenario to create, or the word ``reset``. Known 62 | test scenarios are: 63 | 64 | {SCENARIOS_HELP_LIST} 65 | ''' 66 | 67 | envUSAGE = os.environ.get('USAGE',None) 68 | if envUSAGE is not None: 69 | USAGE = envUSAGE 70 | 71 | envINTRO_TEXT = os.environ.get('INTRO_TEXT',None) 72 | if envINTRO_TEXT is not None: 73 | INTRO_TEXT = envINTRO_TEXT 74 | 75 | envLOCALSITE_OPTIONS_TEXT = os.environ.get('LOCALSITE_OPTIONS_TEXT',None) 76 | if envLOCALSITE_OPTIONS_TEXT is not None: 77 | LOCALSITE_OPTIONS_TEXT = envLOCALSITE_OPTIONS_TEXT 78 | 79 | def help(): 80 | help_text = f''' 81 | {PROG} 82 | {PROG_UNL} 83 | {DESC} 84 | 85 | Usage: {USAGE} 86 | {INTRO_TEXT} 87 | Options 88 | -------{LOCALSITE_OPTIONS_TEXT} 89 | {OPTIONS_TEXT} 90 | ''' 91 | print(help_text) 92 | 93 | def main(argv): 94 | run_info = process_command_line_and_configuration(argv) 95 | 96 | combined_config = run_info['config'] 97 | 98 | scenario = run_info['scenario'] 99 | global_config = combined_config['global'] 100 | amie_config = combined_config['amieclient'] 101 | amie_config.update(global_config) 102 | if amie_config['amie_url'] != AMIE_TEST_URL: 103 | prog_err('[amieconfig] amie_url parameter must be:\n '+\ 104 | AMIE_TEST_URL) 105 | sys.exit(3) 106 | 107 | site = amie_config['site_name'] 108 | api_key = amie_config['api_key'] 109 | 110 | 111 | url = AMIE_TEST_URL + '/test/' + site + '/' 112 | if scenario == 'reset': 113 | url = url + 'reset' 114 | else: 115 | url = url + 'scenarios?type=' + scenario 116 | 117 | post(site, api_key, url) 118 | 119 | sys.exit(0) 120 | 121 | def process_command_line_and_configuration(argv): 122 | run_info = process_command_line(argv) 123 | configfile = run_info['configfile'] 124 | site = run_info['site'] 125 | 126 | config = ConfigLoader.loadConfig(configfile) 127 | if site is not None: 128 | config['amieclient']['site_name'] = site; 129 | 130 | run_info['config'] = config 131 | run_info['site'] = site 132 | 133 | return run_info 134 | 135 | def process_command_line(argv): 136 | argv.pop(0) 137 | configfile = os.environ.get('CONFIG_INI','config.ini') 138 | site = None 139 | 140 | try: 141 | opts,args = getopt.getopt(argv,"hc:s:", 142 | [ 143 | "help", 144 | "configfile=", 145 | "site="]) 146 | except getopt.GetoptError as e: 147 | prog_err(e) 148 | print_err(USAGE) 149 | sys.exit(2) 150 | 151 | for opt, arg in opts: 152 | if opt in ("-h","--help"): 153 | help() 154 | sys.exit(0) 155 | elif opt in ("-c","--configfile"): 156 | configfile = arg 157 | elif opt in ("-s","--site"): 158 | site = arg 159 | 160 | if not Path(configfile).is_file(): 161 | prog_err(configfile + ": no such file") 162 | sys.exit(2) 163 | 164 | if len(args) == 0: 165 | prog_err("scenario name (or 'reset') argument is required") 166 | sys.exit(2) 167 | 168 | scenario = args[0] 169 | if scenario != "reset": 170 | valid_scenario = None 171 | for s in SCENARIOS: 172 | if scenario == s: 173 | valid_scenario = s 174 | break 175 | if not valid_scenario: 176 | prog_err("scenario name '"+scenario+"' is invalid") 177 | sys.exit(2) 178 | 179 | 180 | return { 181 | 'configfile': configfile, 182 | 'site': site, 183 | 'scenario': scenario 184 | } 185 | 186 | def print_err(*args, **kwargs): 187 | print(*args, file=sys.stderr, **kwargs) 188 | 189 | def prog_err(*args, **kwargs): 190 | sys.stderr.write(PROG + ": ") 191 | print_err(*args, **kwargs) 192 | 193 | def post(site, api_key, url): 194 | global VERIFY_SSL 195 | headers = { 196 | 'XA-Site': site, 197 | 'XA-API-Key': api_key 198 | } 199 | 200 | result = None 201 | print("POSTing to "+url) 202 | result = requests.post(url, headers=headers, data={}, timeout=60) 203 | print("Status code = "+str(result.status_code)) 204 | 205 | if __name__ == '__main__': 206 | main(sys.argv) 207 | -------------------------------------------------------------------------------- /src/handler/request_project_create.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logdumper import LogDumper 3 | from packethandler import PacketHandler 4 | from misctypes import DateTime 5 | from taskstatus import TaskStatus 6 | from miscfuncs import (truthy, get_first_nonEmpty) 7 | import handler.subtasks as sub 8 | 9 | class RequestProjectCreate(PacketHandler, packet_type="request_project_create"): 10 | 11 | def work(self, apacket): 12 | """Handle a "request_project_create" packet 13 | :param apacket: dict with extended packet data 14 | :type apacket: ActionablePacket 15 | :return: A TaskStatus if there is an uncompleted task, or an AMIEPacket 16 | to be sent back 17 | """ 18 | logger = logging.getLogger("handler") 19 | logdumper = LogDumper(logger) 20 | 21 | spa = self.sp_adapter 22 | 23 | # 24 | # The lower-case parameters we collect initially (e.g. "person_id", 25 | # "org_code", etc) represent the dynamic state of this request. The 26 | # first time work() is called, none of these "dynamic state" parameters 27 | # will be defined. On re-entry to work(), it is possible that some or 28 | # all or them will be defined. 29 | # 30 | 31 | # We want to check up front for RecordID 32 | recordID = apacket.get('RecordID',None) 33 | if recordID is not None: 34 | ts = sub.lookup_project_task(spa, apacket) 35 | if ts: 36 | person_id = apacket.get("PiPersonID",None); 37 | apacket['PersonID'] = person_id; 38 | project_id = apacket.get('ProjectID',None) 39 | if project_id: 40 | npc = self.build_reply(apacket) 41 | return npc 42 | 43 | org_code = apacket.get('org_code',None) 44 | if org_code is None: 45 | ts = sub.define_org_code(spa, apacket, 'Pi') 46 | if ts: 47 | return ts 48 | org_code = apacket['org_code'] 49 | 50 | person_id = apacket.get('person_id',None) 51 | if person_id is None: 52 | ts = sub.define_person(spa, apacket,"Pi") 53 | if ts: 54 | return ts 55 | person_id = apacket['person_id'] 56 | site_org = apacket['site_org'] 57 | person_active = apacket.get('person_active',False) 58 | apacket['PersonID'] = person_id 59 | apacket['PiPersonID'] = person_id 60 | apacket['pi_person_id'] = person_id 61 | 62 | if not person_active: 63 | ts = sub.activate_person(spa, apacket, "Pi") 64 | if ts: 65 | return ts 66 | person_active = apacket['person_active'] 67 | 68 | # Next we want to check if there is already a project for the given 69 | # GrantNumber - this determines the serviceprovider tasks 70 | grantNumber = apacket.get('GrantNumber',None) 71 | if grantNumber is not None: 72 | project_id = sub.lookup_project_by_grant_number(spa, apacket) 73 | 74 | if not project_id: 75 | 76 | local_fos = apacket.get('local_fos',None) 77 | if local_fos is None: 78 | ts = sub.define_local_fos(spa, apacket) 79 | if ts: 80 | return ts 81 | local_fos = apacket['local_fos'] 82 | 83 | contract_number = apacket.get('contract_number',None) 84 | if contract_number is None: 85 | ts = sub.define_contract_number(spa, apacket) 86 | if ts: 87 | return ts 88 | contract_number = apacket['contract_number'] 89 | 90 | project_name_base = apacket.get('project_name_base',None) 91 | if project_name_base is None: 92 | ts = sub.define_project_name_base(spa, apacket) 93 | if ts: 94 | return ts 95 | project_name_base = apacket['project_name_base'] 96 | 97 | project_id = apacket.get('project_id',None) 98 | if project_id is None: 99 | ts = sub.define_project(spa, apacket) 100 | if ts: 101 | return ts 102 | project_id = apacket['project_id'] 103 | service_units_allocated = apacket.get('service_units_allocated',None) 104 | remote_site_login = apacket['remote_site_login'] 105 | 106 | else: 107 | project_id = get_first_nonEmpty(apacket,'project_id','ProjectID') 108 | service_units_allocated = apacket.get('service_units_allocated',None) 109 | if service_units_allocated is None: 110 | ts = sub.define_allocation(spa, apacket) 111 | if ts: 112 | return ts 113 | service_units_allocated = apacket['service_units_allocated'] 114 | 115 | spa.logdumper.debug("request_project_create normalizing: ",apacket) 116 | ts = self.normalize_packet(apacket) 117 | spa.logdumper.debug("request_project_create normalized: ",apacket) 118 | if ts: 119 | return ts 120 | 121 | npc = self.build_reply(apacket) 122 | return npc 123 | 124 | def normalize_packet(self,apacket): 125 | spa = self.sp_adapter 126 | person_id = get_first_nonEmpty(apacket,'person_id','PersonID') 127 | apacket['PersonID'] = person_id 128 | project_id = get_first_nonEmpty(apacket,'project_id','ProjectID') 129 | apacket['ProjectID'] = project_id 130 | sua = get_first_nonEmpty(apacket,'service_units_allocated', 131 | 'ServiceUnitsAllocated') 132 | apacket['ServiceUnitsAllocated'] = sua 133 | user_notified = apacket.get('user_notified',None) 134 | if not truthy(user_notified): 135 | ts = sub.notify_user(spa, apacket) 136 | if ts: 137 | return ts 138 | else: 139 | return None 140 | 141 | def build_reply(self,apacket): 142 | npc = apacket.create_reply_packet() 143 | npc.GrantNumber = apacket['GrantNumber'] 144 | npc.PfosNumber = apacket['PfosNumber'] 145 | npc.PiOrgCode = apacket['PiOrgCode'] 146 | npc.ProjectTitle = apacket['ProjectTitle'] 147 | npc.ResourceList = apacket['ResourceList'] 148 | 149 | npc.PiPersonID = apacket['PersonID'] 150 | npc.PiRemoteSiteLogin = apacket['PersonID'] 151 | npc.ProjectID = apacket['ProjectID'] 152 | npc.ServiceUnitsAllocated = float(apacket['ServiceUnitsAllocated']) 153 | start_date = get_first_nonEmpty(apacket,'start_date','StartDate') 154 | npc.StartDate = DateTime(start_date).datetime() 155 | end_date = get_first_nonEmpty(apacket,'end_date','EndDate') 156 | npc.EndDate = DateTime(end_date).datetime() 157 | 158 | return npc 159 | -------------------------------------------------------------------------------- /src/person.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, process_parms) 2 | 3 | class AMIEPerson(AMIEParmDescAware,dict): 4 | """ 5 | A class used to describe operations on AMIE's concept of an 6 | 'person'. 7 | """ 8 | 9 | @process_parms( 10 | allowed=[ 11 | 'PersonID', 12 | 'GlobalID', 13 | 'active', 14 | 'FirstName', 15 | 'MiddleName', 16 | 'LastName', 17 | 'Email', 18 | 'AcademicDegree', 19 | 'BusinessPhoneComment', 20 | 'BusinessPhoneExtension', 21 | 'BusinessPhoneNumber', 22 | 'CitizenshipList', 23 | 'City', 24 | 'Country', 25 | 'Department', 26 | 'Fax', 27 | 'HomePhoneComment', 28 | 'HomePhoneExtension', 29 | 'HomePhoneNumber', 30 | 'Organization', 31 | 'OrgCode', 32 | 'RemoteSiteLogin', 33 | 'State', 34 | 'StreetAddress', 35 | 'StreetAddress2', 36 | 'Title', 37 | 'Username', 38 | 'site_org', 39 | 'Zip', 40 | ], 41 | required=[ 42 | 'PersonID', 43 | ['FirstName','LastName'], 44 | ]) 45 | def __init__(self, *args, **kwargs): 46 | """lookup_person() result object 47 | 48 | The site client implementation should use this to create the result of 49 | lookup_person(). 50 | """ 51 | 52 | dict.__init__(self,**kwargs) 53 | 54 | class LookupPerson(AMIEParmDescAware,dict): 55 | 56 | @process_parms( 57 | allowed=[ 58 | 'PersonID', 59 | 'GlobalID', 60 | ], 61 | required=[ 62 | ]) 63 | def __init__(self, *args, **kwargs) -> dict: 64 | """Validate, filter, and transform arguments to ``lookup_person()``""" 65 | return dict.__init__(self,**kwargs) 66 | 67 | class ChooseOrAddPerson(AMIEParmDescAware,dict): 68 | """ 69 | A class used when specifying a local person who will be known to AMIE 70 | """ 71 | 72 | @process_parms( 73 | allowed=[ 74 | 'amie_transaction_id', 75 | 'amie_packet_id', 76 | 'job_id', 77 | 'amie_packet_type', 78 | 'task_name', 79 | 'timestamp', 80 | 81 | 'AcademicDegree', 82 | 'BusinessPhoneComment', 83 | 'BusinessPhoneExtension', 84 | 'BusinessPhoneNumber', 85 | 'CitizenshipList', 86 | 'City', 87 | 'Country', 88 | 'Department', 89 | 'Email', 90 | 'FirstName', 91 | 'GlobalID', 92 | 'HomePhoneComment', 93 | 'HomePhoneExtension', 94 | 'HomePhoneNumber', 95 | 'LastName', 96 | 'MiddleName', 97 | 'Organization', 98 | 'OrgCode', 99 | 'SitePersonID', 100 | 'State', 101 | 'Title', 102 | 'RemoteSiteLogin', 103 | 'RequestedLoginList', 104 | ], 105 | required=[ 106 | 'amie_transaction_id', 107 | 'amie_packet_id', 108 | 'job_id', 109 | 'amie_packet_type', 110 | 'task_name', 111 | 'timestamp', 112 | 113 | ['FirstName', 'LastName'], 114 | 'Organization', 115 | ]) 116 | def __init__(self, **kwargs) -> dict: 117 | """Validate, filter, and transform arguments to ``choose_or_add_person()``""" 118 | dict.__init__(self, **kwargs) 119 | 120 | class UpdatePersonDNs(AMIEParmDescAware,dict): 121 | """ 122 | A class used when specifying a DNs to update for a person 123 | """ 124 | 125 | @process_parms( 126 | allowed=[ 127 | 'amie_transaction_id', 128 | 'amie_packet_id', 129 | 'job_id', 130 | 'amie_packet_type', 131 | 'task_name', 132 | 'timestamp', 133 | 134 | 'PersonID', 135 | 'DnList', 136 | ], 137 | required=[ 138 | 'amie_transaction_id', 139 | 'amie_packet_id', 140 | 'job_id', 141 | 'amie_packet_type', 142 | 'task_name', 143 | 'timestamp', 144 | 145 | 'PersonID', 146 | ]) 147 | # 'DnList', 148 | 149 | def __init__(self, **kwargs) -> dict: 150 | """Validate, filter, and transform arguments to ``update_person_DNs()``""" 151 | dict.__init__(self, **kwargs) 152 | 153 | class ActivatePerson(AMIEParmDescAware,dict): 154 | """ 155 | A class used when specifying a local person to activate 156 | """ 157 | 158 | @process_parms( 159 | allowed=[ 160 | 'amie_transaction_id', 161 | 'amie_packet_id', 162 | 'job_id', 163 | 'amie_packet_type', 164 | 'task_name', 165 | 'timestamp', 166 | 167 | 'Email', 168 | 'FirstName', 169 | 'GlobalID', 170 | 'LastName', 171 | 'MiddleName', 172 | 'PersonID', 173 | 'SitePersonID', 174 | 'RemoteSiteLogin', 175 | ], 176 | required=[ 177 | 'amie_transaction_id', 178 | 'amie_packet_id', 179 | 'job_id', 180 | 'amie_packet_type', 181 | 'task_name', 182 | 'timestamp', 183 | 184 | 'PersonID', 185 | ]) 186 | def __init__(self, **kwargs) -> dict: 187 | """Validate, filter, and transform arguments to ``activate_person()``""" 188 | dict.__init__(self, **kwargs) 189 | 190 | class MergePerson(AMIEParmDescAware,dict): 191 | """ 192 | A class used when specifying person records to merge 193 | """ 194 | 195 | @process_parms( 196 | allowed=[ 197 | 'amie_transaction_id', 198 | 'amie_packet_id', 199 | 'job_id', 200 | 'amie_packet_type', 201 | 'task_name', 202 | 'timestamp', 203 | 204 | 'KeepGlobalID', 205 | 'DeleteGlobalID', 206 | 'KeepPersonID', 207 | 'DeletePersonID', 208 | 'KeepPortalLogin', 209 | 'DeletePortalLogin', 210 | ], 211 | required=[ 212 | 'amie_transaction_id', 213 | 'amie_packet_id', 214 | 'job_id', 215 | 'amie_packet_type', 216 | 'task_name', 217 | 'timestamp', 218 | 219 | 'KeepGlobalID', 220 | 'DeleteGlobalID', 221 | 'KeepPersonID', 222 | 'DeletePersonID', 223 | ]) 224 | def __init__(self, **kwargs) -> dict: 225 | """Validate, filter, and transform arguments to ``merge_person()``""" 226 | dict.__init__(self, **kwargs) 227 | 228 | -------------------------------------------------------------------------------- /wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | WRAPPERNAME="wrapper.sh" 3 | PROG=`basename $0` 4 | if [ ":${PROG}" = ":${WRAPPERNAME}" ] ; then 5 | cat < 9 | 10 | Amiemediator programs ("amie", "test-scenario", "viewpackets") require a 11 | site-specific configuration file and externally-defined code to be at all 12 | useful. This script allows you to easily provide a configuration file and other 13 | information tailored to your site. 14 | 15 | This script is not meant to be run as "${WRAPPERNAME}". Instead, it is meant to 16 | be linked to another file with a name matching that of an amiemediator program 17 | and run using that name. For example, the main amiemediator program in called 18 | "amie". To use ${WRAPPERNAME} as a wrapper for "amie", set up a directory for 19 | your custom environment, copy ${WRAPPERNAME} to that directory, and create a 20 | link to ${WRAPPERNAME} called "amie". For example, if your site-specific files 21 | are all under directory "mysite", you could do this: 22 | 23 | \$ mkdir -p mysite/bin 24 | \$ cp ${WRAPPERNAME} mysite/bin 25 | \$ chmod +x mysite/bin/${WRAPPERNAME} 26 | \$ cd mysite/bin 27 | \$ ln -s ${WRAPPERNAME} amie 28 | 29 | In addition to creating a link to your target program, you will want to 30 | create a file in the same directory called ".rc", where is the 31 | name of the target program (e.g. "amie.rc"). ${WRAPPERNAME} will source this 32 | file before doing anything else. 33 | 34 | There are five environment variables that ${WRAPPERNAME} will subsequently use 35 | if they are set and not empty: CONFIG_INI, CONFIG_DIR, PACKAGE_DIR, RUN_ENV, 36 | and AMIEMEDIATOR_DIR. 37 | 38 | CONFIG_INI is assumed to name the application configuration file. CONFIG_DIR 39 | is assumed to name a directory containing configuration files. PACKAGE_DIR is 40 | assumed to name a directory containing all of your site-specific files. (In 41 | the example above, it would be "mydir".) RUN_ENV is assumed to name a 42 | subdirector under \${PACKAGE_DIR} containing run-time configuration files; it 43 | is typically something like "test", "prod", or "dev", for example. 44 | AMIEMEDIATOR_DIR is the top-level directory where the amiemediator package is 45 | installed. 46 | 47 | If CONFIG_INI is not set or does not name a readable file, ${WRAPPERNAME} will 48 | search for a file called \"config.init\" or \"config.ini\" in the following 49 | directories (assuming the variables used to build the paths are set): 50 | \${CONFIG_DIR} 51 | \${PACKAGE_DIR} 52 | \${PACKAGE_DIR}/${RUN_ENV} 53 | \${SCRIPTDIR}/.. 54 | \${SCRIPTDIR}/../.. 55 | \${SCRIPTDIR} 56 | . 57 | .. 58 | 59 | The SCRIPTDIR variable is set internally to the directory containing the 60 | running script itself. 61 | under \${CONFIG_DIR}.it under \$PACKAGE_DIR/\$RUN_ENV. 62 | 63 | If PACKAGE_DIR is not set, ${WRAPPERNAME} will search for a reasonable 64 | default: it will look in the parent directory of the ${WRAPPERNAME} script, 65 | the grandparent directory of the script, the script directory, the current 66 | directory, the parent of the current directory, in order. Specifically, it 67 | will look for a "config.init" or "config.ini" file in each of these directories 68 | and in the \$RUN_ENV subdirectory of these directories. 69 | 70 | If AMIEMEDIATOR_DIR is not set, ${WRAPPERNAME} will search 71 | \$PACKAGE_DIR/../amiemediator and \$PACKAGE_DIR/../../amiemediator. 72 | 73 | All amiemediator programs ("amie", "viewpackets", etc.) support environment 74 | variables CONFIG_INI, USAGE, LOCALSITE_INTRO_TEXT, INTRO_TEXT, 75 | LOCALSITE_CONFIG_TEXT, and LOCAL_SITE_ENV_TEXT. CONFIG_INI identifies the 76 | configuration file, while the other variables are used by the "--help" 77 | command-line option if they are set. 78 | 79 | EOF 80 | echo $PROG 81 | exit 0 82 | fi 83 | 84 | SCRIPTDIR=`cd \`dirname $0\`; /bin/pwd` 85 | 86 | if [ -f "${SCRIPTDIR}/${PROG}.rc" ] ; then 87 | . "${SCRIPTDIR}/${PROG}.rc" 88 | fi 89 | 90 | conf_base_candidates="config.init config.ini" 91 | run_env_candidates=". ${RUN_ENV}" 92 | 93 | pkg_dir_candidates=" 94 | ${PACKAGE_DIR} 95 | ${SCRIPTDIR}/.. 96 | ${SCRIPTDIR}/../.. 97 | ${SCRIPTDIR} 98 | . 99 | .. 100 | " 101 | 102 | amie_dir_candidates="${AMIEMEDIATOR_DIR}" 103 | config_dir_candidates="${CONFIG_DIR}" 104 | for pkg_dir_candidate in ${pkg_dir_candidates} ; do 105 | amie_dir_candidates="${amie_dir_candidates} ${pkg_dir_candidate}" 106 | for run_env_candidate in ${run_env_candidates} ; do 107 | pr_cand="${pkg_dir_candidate}/${run_env_candidate}" 108 | config_dir_candidates="${config_dir_candidates} ${pr_cand}" 109 | done 110 | done 111 | 112 | config_file_candidates="${CONFIG_INI}" 113 | for config_dir_candidate in ${config_dir_candidates} ; do 114 | for conf_base_candidate in ${conf_base_candidates} ; do 115 | cf="${config_dir_candidate}/${conf_base_candidate}" 116 | config_file_candidates="${config_file_candidates} ${cf}" 117 | done 118 | done 119 | CONFIG_FILE= 120 | CONF_ERR_LOG= 121 | for config_file_candidate in ${config_file_candidates} ; do 122 | if [ ! -f "${config_file_candidate}" ] ; then 123 | CONF_ERR_LOG="${CONF_ERR_LOG} ${config_file_candidate}: no such file 124 | " 125 | elif [ ! -r "${config_file_candidate}" ] ; then 126 | CONF_ERR_LOG="${CONF_ERR_LOG} ${config_file_candidate}: file not readable 127 | " 128 | else 129 | CONFIG_FILE="${config_file_candidate}" 130 | fi 131 | done 132 | if [ ":${CONFIG_FILE}" = ":" ] ; then 133 | echo "$PROG: unable to determine application configuration file:" >&2 134 | echo "${CONF_ERR_LOG}" >&2 135 | exit 1 136 | fi 137 | CONFIG_INI="${CONFIG_FILE}" 138 | export CONFIG_INI 139 | 140 | amie_dir_candidates="${AMIEMEDIATOR_DIR}" 141 | for pkg_dir_candidate in ${pkg_dir_candidates} ; do 142 | for run_env_candidate in ${run_env_candidates} ; do 143 | pd="${pkg_dir_candidate}/${run_env_candidate}" 144 | amie_dir_candidates="${amie_dir_candidates} ${pd}/../amiemediator" 145 | amie_dir_candidates="${amie_dir_candidates} ${pd}/../../amiemediator" 146 | done 147 | done 148 | 149 | amiemediator_dir= 150 | amie_file=src/mediator.py 151 | for ad in ${amie_dir_candidates} ; do 152 | af="${ad}/${amie_file}" 153 | if [ -f "${af}" ] && [ -r "${af}" ] ; then 154 | amiemediator_dir="${ad}" 155 | break 156 | fi 157 | done 158 | if [ ":${amiemediator_dir}" = ":" ] ; then 159 | echo "$PROG: unable to determine location of amiemediator package" >&2 160 | exit 1 161 | fi 162 | AMIEMEDIATOR_DIR=`cd ${amiemediator_dir} ; /bin/pwd` 163 | PACKAGE_DIR=`cd ${PACKAGE_DIR} ; /bin/pwd` 164 | PYTHONPATH="${PACKAGE_DIR}/src:${AMIEMEDIATOR_DIR}/src" 165 | export AMIEMEDIATOR_DIR PYTHONPATH 166 | 167 | 168 | PYPROG="${AMIEMEDIATOR_DIR}/bin/${PROG}" 169 | if [ ! -f ${PYPROG} ] || [ ! -x ${PYPROG} ] ; then 170 | echo "${PROG}: ${PYPROG} does not exist or is not executable" >&2 171 | exit 1 172 | fi 173 | export PYPROG 174 | 175 | CMD="${PYPROG} $@" 176 | exec ${CMD} 177 | exit 255 178 | -------------------------------------------------------------------------------- /tests/fixtures/request_project_create.py: -------------------------------------------------------------------------------- 1 | RPC_PKT_1 = { 2 | 'DATA_TYPE': 'Packet', 3 | 'type_id': 7, 4 | 'type': 'request_project_create', 5 | 'header': { 6 | 'expected_reply_list': [ 7 | {'type': 'data_project_create', 'timeout': 30240} 8 | ], 9 | 'packet_id': 2, 10 | 'trans_rec_id': 87139098, 11 | 'transaction_id': 244207, 12 | 'packet_rec_id': 174709746, 13 | 'local_site_name': 'PSC', 14 | 'remote_site_name': 'SDSC', 15 | 'originating_site_name': 'SDSC', 16 | 'outgoing_flag': False, 17 | 'transaction_state': 'in-progress', 18 | 'packet_state': 'in-progress', 19 | 'packet_timestamp': '2021-08-24T14:47:52.600Z', 20 | }, 21 | 'body': { 22 | 'Abstract': 'Investigate something', 23 | 'AcademicDegree': [ 24 | {'Field': 'Computer and Computation Research', 'Degree': 'MS'} 25 | ], 26 | 'AllocatedResource': 'comet-gpu.sdsc.xsede', 27 | 'AllocationType': 'new', 28 | 'EndDate': '2022-09-30T23:59:59Z', 29 | 'GrantNumber': 'IRI120015', 30 | 'NsfStatusCode': 'GS', 31 | 'PfosNumber': 21, 32 | 'PiCity': 'Pittsburgh', 33 | 'PiCountry': '9US', 34 | 'PiDepartment': 'SCS', 35 | 'PiEmail': 'vraunak@andrew.cmu.edu', 36 | 'PiFirstName': 'Vikas', 37 | 'PiGlobalID': '71691', 38 | 'PiLastName': 'Raunak', 39 | 'PiMiddleName': '', 40 | 'PiOrgCode': '0032425', 41 | 'PiOrganization': 'Carnegie Mellon University', 42 | 'PiState': 'PA', 43 | 'PiStreetAddress': 'Craig Street, Carnegie Mellon University, Pittsburgh 15213', 44 | 'PiTitle': '', 45 | 'PiZip': '15213', 46 | 'PiBusinessPhoneNumber': '4124781149', 47 | 'PiRequestedLoginList': [''], 48 | 'RecordID': 12345, 49 | 'ResourceList': ['comet-gpu.sdsc.xsede'], 50 | 'ServiceUnitsAllocated': 20000, 51 | 'StartDate': '2021-10-01T00:00:00Z', 52 | } 53 | } 54 | 55 | RPC_PKT_2 = { 56 | 'DATA_TYPE': 'Packet', 57 | 'type_id': 7, 58 | 'type': 'request_project_create', 59 | 'header': { 60 | 'expected_reply_list': [ 61 | {'type': 'data_project_create', 'timeout': 30240} 62 | ], 63 | 'packet_id': 3, 64 | 'trans_rec_id': 87139099, 65 | 'transaction_id': 244208, 66 | 'packet_rec_id': 174709747, 67 | 'local_site_name': 'PSC', 68 | 'remote_site_name': 'SDSC', 69 | 'originating_site_name': 'SDSC', 70 | 'outgoing_flag': False, 71 | 'transaction_state': 'in-progress', 72 | 'packet_state': 'in-progress', 73 | 'packet_timestamp': '2021-08-24T15:47:52.600Z', 74 | }, 75 | 'body': { 76 | 'Abstract': 'Investigate something interesting', 77 | 'AcademicDegree': [ 78 | {'Field': 'Computer and Computation Research', 'Degree': 'MS'} 79 | ], 80 | 'AllocatedResource': 'comet-gpu.sdsc.xsede', 81 | 'AllocationType': 'new', 82 | 'EndDate': '2022-09-30T23:59:59Z', 83 | 'GrantNumber': 'IRI120015', 84 | 'NsfStatusCode': 'GS', 85 | 'PfosNumber': 21, 86 | 'PiCity': 'Pittsburgh', 87 | 'PiCountry': '9US', 88 | 'PiDepartment': 'SCS', 89 | 'PiEmail': 'vraunak@andrew.cmu.edu', 90 | 'PiFirstName': 'Vikas', 91 | 'PiGlobalID': '71691', 92 | 'PiLastName': 'Raunak', 93 | 'PiMiddleName': '', 94 | 'PiOrgCode': '0032425', 95 | 'PiOrganization': 'Carnegie Mellon University', 96 | 'PiState': 'PA', 97 | 'PiStreetAddress': 'Craig Street, Carnegie Mellon University, Pittsburgh 15213', 98 | 'PiTitle': '', 99 | 'PiZip': '15213', 100 | 'PiBusinessPhoneNumber': '4124781149', 101 | 'PiRequestedLoginList': [''], 102 | 'RecordID': 12345, 103 | 'ResourceList': ['comet-gpu.sdsc.xsede'], 104 | 'ServiceUnitsAllocated': 20000, 105 | 'StartDate': '2021-10-01T00:00:00Z', 106 | } 107 | } 108 | 109 | RPC_PKT_SCENARIO_RPR = { 110 | "DATA_TYPE": "Packet", 111 | "type": "request_project_create", 112 | "header": { 113 | "expected_reply_list": [ 114 | { 115 | "type": "notify_project_create", 116 | "timeout": 30240 117 | } 118 | ], 119 | "packet_id": 1, 120 | "trans_rec_id": 116811798, 121 | "transaction_id": 20897, 122 | "remote_site_name": "NCAR", 123 | "local_site_name": "TGCDB", 124 | "originating_site_name": "TGCDB", 125 | "outgoing_flag": 1, 126 | "packet_rec_id": 233441576, 127 | "packet_timestamp": "2023-08-03T20:34:45.920Z", 128 | "client_state": None, 129 | "packet_state": "in-progress", 130 | "client_json": None, 131 | "transaction_state": "in-progress" 132 | }, 133 | "body": { 134 | "AllocationType": "new", 135 | "BoardType": "Startup", 136 | "RequestType": "new", 137 | "Abstract": "Lorem ipsum dolor est...", 138 | "ChargeNumber": "TG-NNT237423", 139 | "GrantNumber": "NNT237423", 140 | "ProposalNumber": "NNT237423", 141 | "PfosNumber": "21000", 142 | "PiGlobalID": "32657", 143 | "PiBusinessPhoneNumber": "7202352981", 144 | "PiEmail": "jll1062+xsede@phys.psu.edu", 145 | "PiCity": "University Park", 146 | "PiStreetAddress": "Department of Physics", 147 | "PiStreetAddress2": "104 Davey Lab, Box 166", 148 | "PiZip": "16802", 149 | "PiState": "PA", 150 | "PiCountry": "9US", 151 | "PiFirstName": "Justin", 152 | "PiMiddleName": "", 153 | "PiLastName": "Lanfranchi", 154 | "PiDepartment": "Physics", 155 | "PiTitle": "", 156 | "PiOrganization": "Pennsylvania State University", 157 | "PiOrgCode": "0088138", 158 | "NsfStatusCode": "GS", 159 | "PiDnList": [ 160 | "/C=US/O=National Center for Supercomputing Applications/CN=Justin Lanfranchi", 161 | "/C=US/O=Pittsburgh Supercomputing Center/CN=Justin Lanfranchi" 162 | ], 163 | "SitePersonId": [ 164 | { 165 | "Site": "XD-ALLOCATIONS", 166 | "PersonID": "lanfranj" 167 | }, 168 | { 169 | "Site": "X-PORTAL", 170 | "PersonID": "lanfranj" 171 | } 172 | ], 173 | "AcademicDegree": [ 174 | { 175 | "Degree": "BS", 176 | "Field": "Engineering" 177 | } 178 | ], 179 | "ProjectTitle": "Lorem Ipsum", 180 | "RecordID": "XRAS-110809-test-resource1.ncar.xsede", 181 | "Sfos": [ 182 | { 183 | "Number": "0" 184 | } 185 | ], 186 | "StartDate": "2023-08-03", 187 | "EndDate": "2024-08-03", 188 | "ServiceUnitsAllocated": "1", 189 | "ResourceList": [ 190 | "test-resource1.ncar.xsede" 191 | ], 192 | "PiRequestedLoginList": [ 193 | "lanfranj" 194 | ], 195 | "AllocatedResource": "test-resource1.ncar.xsede" 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/t_spsession.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | from datetime import (datetime, timedelta) 4 | from misctypes import (DateTime, TimeUtil) 5 | from retryingproxy import (RetryingServiceProxyError, RetryingServiceProxy) 6 | from serviceprovider import (SPSession, ServiceProviderError, 7 | ServiceProviderTimeout, 8 | ServiceProviderTemporaryError, 9 | ServiceProviderRequestFailed) 10 | 11 | class MockTimeUtil(TimeUtil): 12 | def __init__(self): 13 | self.currtime = datetime.fromisoformat("1970-01-01T00:00:00+00:00") 14 | self.basetime = self.currtime 15 | self.sleep_arg = None 16 | self.called_sleep = False 17 | self.called_now = False 18 | 19 | def sleep(self, secs): 20 | self.currtime += timedelta(seconds=secs) 21 | self.sleep_arg = secs 22 | self.called_sleep = True 23 | 24 | def now(self): 25 | self.called_now = True 26 | return self.currtime 27 | 28 | def clear(self): 29 | self.sleep_arg = None 30 | self.called_sleep = False 31 | self.called_now = False 32 | 33 | class MockServiceProvider(object): 34 | def dotest(self, exc=None): 35 | if exc is not None: 36 | raise exc() 37 | 38 | class TestSPSession(unittest.TestCase): 39 | def setUp(self): 40 | self.timeutil = MockTimeUtil() 41 | self.sp = MockServiceProvider() 42 | SPSession.configure(sp=self.sp, 43 | min_retry_delay=1, max_retry_delay=30, 44 | retry_time_max=90, 45 | time_util=self.timeutil) 46 | 47 | def test_configure(self): 48 | SPSession.configure(sp=None, 49 | min_retry_delay=1, max_retry_delay=30, 50 | retry_time_max=90) 51 | cls = SPSession 52 | self.assertEqual(cls.svc,None, 53 | msg="svc set") 54 | self.assertEqual(cls.min_retry_delay,1, 55 | msg="min_retry_delay not set") 56 | self.assertEqual(cls.max_retry_delay,30, 57 | msg="max_retry_delay not set") 58 | self.assertEqual(cls.retry_time_max,90, 59 | msg="retry_time_max not set") 60 | self.assertTrue(isinstance(cls.time_util,TimeUtil), 61 | msg="default time_util set") 62 | 63 | 64 | SPSession.configure(sp=MockServiceProvider(), 65 | min_retry_delay=1, max_retry_delay=30, 66 | retry_time_max=90, 67 | time_util=self.timeutil) 68 | self.assertTrue(isinstance(cls.svc,MockServiceProvider), 69 | msg="svc not set") 70 | self.assertTrue(isinstance(cls.time_util,MockTimeUtil), 71 | msg="explicit time_util not set") 72 | 73 | 74 | def test_no_temp_errors(self): 75 | sps = SPSession 76 | with sps() as sp: 77 | sp.dotest() 78 | 79 | self.assertEqual(sps.retry_delay,None, 80 | msg="after clear run, sps has retry_delay value") 81 | self.assertEqual(sps.retry_deadline,None, 82 | msg="after clean run, sp has retry_deadline value") 83 | 84 | self.assertFalse(self.timeutil.called_sleep, 85 | msg="sleep() called in clean run") 86 | self.assertFalse(self.timeutil.called_now, 87 | msg="now() called in clean run") 88 | 89 | 90 | def test_ServiceProviderTemporaryError(self): 91 | wrong_retry_delay_msg="after temp err, wrong retry_delay value" 92 | wrong_retry_deadline_msg="after temp err, wrong retry_deadline value" 93 | sps = SPSession 94 | spsi = sps() 95 | with self.assertRaises(ServiceProviderTemporaryError, 96 | msg="TemporaryError not propagated"): 97 | with spsi as sp: 98 | sp.dotest(ServiceProviderTemporaryError) 99 | 100 | self.assertEqual(sps.retry_delay,1, 101 | msg=wrong_retry_delay_msg) 102 | expected_deadline = self.timeutil.basetime + timedelta(seconds=90) 103 | self.assertEqual(sps.retry_deadline,expected_deadline, 104 | msg=wrong_retry_deadline_msg) 105 | 106 | self.assertFalse(self.timeutil.called_sleep, 107 | msg="sleep() called after first tmp err") 108 | self.assertTrue(self.timeutil.called_now, 109 | msg="now() not after tmp err") 110 | self.timeutil.clear() 111 | 112 | for sa in (1,2,4,8): 113 | new_delay = sa * 2 114 | 115 | with self.assertRaises(ServiceProviderTemporaryError, 116 | msg="TemporaryError not propagated"): 117 | with spsi as sp: 118 | sp.dotest(ServiceProviderTemporaryError) 119 | 120 | self.assertEqual(sps.retry_delay,new_delay, 121 | msg=wrong_retry_delay_msg) 122 | self.assertEqual(sps.retry_deadline, 123 | expected_deadline, 124 | msg=wrong_retry_deadline_msg) 125 | 126 | self.assertTrue(self.timeutil.called_sleep, 127 | msg="sleep() called after first tmp err") 128 | self.assertEqual(self.timeutil.sleep_arg, sa, 129 | msg="sleep() called with wrong value") 130 | self.assertTrue(self.timeutil.called_now, 131 | msg="now() not after tmp err") 132 | self.timeutil.clear() 133 | 134 | for sa in (16,30): 135 | with self.assertRaises(ServiceProviderTemporaryError, 136 | msg="TemporaryError not propagated"): 137 | with spsi as sp: 138 | sp.dotest(ServiceProviderTemporaryError) 139 | 140 | self.assertEqual(sps.retry_delay,30, 141 | msg=wrong_retry_delay_msg) 142 | self.assertEqual(sps.retry_deadline, 143 | expected_deadline, 144 | msg=wrong_retry_deadline_msg) 145 | 146 | self.assertTrue(self.timeutil.called_sleep, 147 | msg="sleep() called after first tmp err") 148 | self.assertEqual(self.timeutil.sleep_arg, sa, 149 | msg="sleep() called with wrong value") 150 | self.assertTrue(self.timeutil.called_now, 151 | msg="now() not after tmp err") 152 | self.timeutil.clear() 153 | 154 | with self.assertRaises(ServiceProviderTimeout, 155 | msg="timeout exceeded error not raised"): 156 | with spsi as sp: 157 | sp.dotest(ServiceProviderTemporaryError) 158 | 159 | self.assertEqual(sps.retry_delay,None, 160 | msg=wrong_retry_delay_msg) 161 | self.assertEqual(sps.retry_deadline,None, 162 | msg=wrong_retry_deadline_msg) 163 | 164 | self.assertTrue(self.timeutil.called_sleep, 165 | msg="sleep() called after first tmp err") 166 | self.assertEqual(self.timeutil.sleep_arg, 30, 167 | msg="sleep() called with wrong value") 168 | self.assertTrue(self.timeutil.called_now, 169 | msg="now() not after tmp err") 170 | self.timeutil.clear() 171 | 172 | 173 | if __name__ == '__main__': 174 | unittest.main() 175 | -------------------------------------------------------------------------------- /tests/t_retryingproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | from requests.exceptions import ConnectionError 4 | from datetime import (datetime, timedelta) 5 | from misctypes import (DateTime, TimeUtil) 6 | from retryingproxy import (RetryingServiceProxyError, MaxRetryError, 7 | RetryingServiceProxy) 8 | 9 | class MockTimeUtil(TimeUtil): 10 | def __init__(self): 11 | self.currtime = datetime.fromisoformat("1970-01-01T00:00:00+00:00") 12 | self.basetime = self.currtime 13 | self.sleep_arg = None 14 | self.called_sleep = False 15 | self.called_now = False 16 | 17 | def sleep(self, secs): 18 | self.currtime += timedelta(seconds=secs) 19 | self.sleep_arg = secs 20 | self.called_sleep = True 21 | 22 | def now(self): 23 | self.called_now = True 24 | return self.currtime 25 | 26 | def clear(self): 27 | self.sleep_arg = None 28 | self.called_sleep = False 29 | self.called_now = False 30 | 31 | class MockServiceProvider(object): 32 | def dotest(self, exc=None): 33 | if exc is not None: 34 | raise exc() 35 | 36 | class TestRetryingServiceProxy(unittest.TestCase): 37 | def setUp(self): 38 | self.timeutil = MockTimeUtil() 39 | self.svc = MockServiceProvider() 40 | RetryingServiceProxy.configure(svc=self.svc, 41 | min_retry_delay=1, max_retry_delay=30, 42 | retry_time_max=90, 43 | time_util=self.timeutil) 44 | 45 | def test_configure(self): 46 | RetryingServiceProxy.configure(svc=None, 47 | min_retry_delay=1, max_retry_delay=30, 48 | retry_time_max=90) 49 | cls = RetryingServiceProxy 50 | self.assertEqual(cls.svc,None, 51 | msg="svc set") 52 | self.assertEqual(cls.min_retry_delay,1, 53 | msg="min_retry_delay not set") 54 | self.assertEqual(cls.max_retry_delay,30, 55 | msg="max_retry_delay not set") 56 | self.assertEqual(cls.retry_time_max,90, 57 | msg="retry_time_max not set") 58 | self.assertTrue(isinstance(cls.time_util,TimeUtil), 59 | msg="default time_util set") 60 | self.assertEqual(cls.max_retry_exception,MaxRetryError, 61 | msg="max_retry_exception not set") 62 | 63 | 64 | RetryingServiceProxy.configure(svc=MockServiceProvider(), 65 | min_retry_delay=1, max_retry_delay=30, 66 | retry_time_max=90, 67 | time_util=self.timeutil) 68 | self.assertTrue(isinstance(cls.svc,MockServiceProvider), 69 | msg="svc not set") 70 | self.assertTrue(isinstance(cls.time_util,MockTimeUtil), 71 | msg="explicit time_util not set") 72 | 73 | def test_no_temp_errors(self): 74 | rsp = RetryingServiceProxy 75 | with rsp() as sp: 76 | sp.dotest() 77 | 78 | self.assertEqual(rsp.retry_delay,None, 79 | msg="after clear run, rsp has retry_delay value") 80 | self.assertEqual(rsp.retry_deadline,None, 81 | msg="after clean run, rsp has retry_deadline value") 82 | 83 | self.assertFalse(self.timeutil.called_sleep, 84 | msg="sleep() called in clean run") 85 | self.assertFalse(self.timeutil.called_now, 86 | msg="now() called in clean run") 87 | 88 | 89 | def test_ServiceProviderTemporaryError(self): 90 | wrong_retry_delay_msg="after temp err, wrong retry_delay value" 91 | wrong_retry_deadline_msg="after temp err, wrong retry_deadline value" 92 | rsp = RetryingServiceProxy 93 | rspi = rsp() 94 | with self.assertRaises(ConnectionError, 95 | msg="TemporaryError not propagated"): 96 | with rspi as sp: 97 | sp.dotest(ConnectionError) 98 | 99 | self.assertEqual(rsp.retry_delay,1, 100 | msg=wrong_retry_delay_msg) 101 | expected_deadline = self.timeutil.basetime + timedelta(seconds=90) 102 | self.assertEqual(rsp.retry_deadline,expected_deadline, 103 | msg=wrong_retry_deadline_msg) 104 | 105 | self.assertFalse(self.timeutil.called_sleep, 106 | msg="sleep() called after first tmp err") 107 | self.assertTrue(self.timeutil.called_now, 108 | msg="now() not after tmp err") 109 | self.timeutil.clear() 110 | 111 | for sa in (1,2,4,8): 112 | new_delay = sa * 2 113 | 114 | with self.assertRaises(ConnectionError, 115 | msg="ConnectionError not propagated"): 116 | with rspi as sp: 117 | sp.dotest(ConnectionError) 118 | 119 | self.assertEqual(rsp.retry_delay,new_delay, 120 | msg=wrong_retry_delay_msg) 121 | self.assertEqual(rsp.retry_deadline, 122 | expected_deadline, 123 | msg=wrong_retry_deadline_msg) 124 | 125 | self.assertTrue(self.timeutil.called_sleep, 126 | msg="sleep() called after first tmp err") 127 | self.assertEqual(self.timeutil.sleep_arg, sa, 128 | msg="sleep() called with wrong value") 129 | self.assertTrue(self.timeutil.called_now, 130 | msg="now() not after tmp err") 131 | self.timeutil.clear() 132 | 133 | for sa in (16,30): 134 | with self.assertRaises(ConnectionError, 135 | msg="ConnectionError not propagated"): 136 | with rspi as sp: 137 | sp.dotest(ConnectionError) 138 | 139 | self.assertEqual(rsp.retry_delay,30, 140 | msg=wrong_retry_delay_msg) 141 | self.assertEqual(rsp.retry_deadline, 142 | expected_deadline, 143 | msg=wrong_retry_deadline_msg) 144 | 145 | self.assertTrue(self.timeutil.called_sleep, 146 | msg="sleep() called after first tmp err") 147 | self.assertEqual(self.timeutil.sleep_arg, sa, 148 | msg="sleep() called with wrong value") 149 | self.assertTrue(self.timeutil.called_now, 150 | msg="now() not after tmp err") 151 | self.timeutil.clear() 152 | 153 | with self.assertRaises(MaxRetryError, 154 | msg="timeout exceeded error not raised"): 155 | with rspi as sp: 156 | sp.dotest(ConnectionError) 157 | 158 | self.assertEqual(rsp.retry_delay,None, 159 | msg=wrong_retry_delay_msg) 160 | self.assertEqual(rsp.retry_deadline,None, 161 | msg=wrong_retry_deadline_msg) 162 | 163 | self.assertTrue(self.timeutil.called_sleep, 164 | msg="sleep() called after first tmp err") 165 | self.assertEqual(self.timeutil.sleep_arg, 30, 166 | msg="sleep() called with wrong value") 167 | self.assertTrue(self.timeutil.called_now, 168 | msg="now() not after tmp err") 169 | self.timeutil.clear() 170 | 171 | 172 | if __name__ == '__main__': 173 | unittest.main() 174 | -------------------------------------------------------------------------------- /src/snapshot.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | import json 4 | from stat import * 5 | import time 6 | from filewait import FileWaiter 7 | 8 | class Snapshots(object): 9 | def __init__(self, dir, mode='r', purge_writeable=True): 10 | """Set up a directory as a "snapshot" directory 11 | 12 | A "snapshot" is a JSON-serialized image of an object at an instant 13 | in time. A Snapshots instance is created as either a "writer" or a 14 | "reader". 15 | 16 | Each snapshot has a key unique to the object. The key should be a 17 | readable string that does not start with '.' or contain '/'. The key 18 | is used as the name of the snapshot file. 19 | 20 | :param dir: The directory containing snapshot files. 21 | :type dir: str 22 | :param mode: The mode: either 'r' or 'w' 23 | :type mode: str 24 | """ 25 | if (mode != 'r') and (mode != 'w'): 26 | raise ValueError("mode must be 'r' or 'w'") 27 | 28 | Path(dir).mkdir(parents=True, exist_ok=True) 29 | self.dir = dir 30 | waitfile = str(Path(dir,".WAITFILE")) 31 | self.filewaiter = FileWaiter(waitfile) 32 | if mode == 'r': 33 | self.images = None 34 | else: 35 | self.images = dict() 36 | entry_gen = os.scandir(self.dir) 37 | for dir_entry in entry_gen: 38 | if not dir_entry.name.startswith('.') and dir_entry.is_file(): 39 | if purge_writeable: 40 | Path(dir_entry.path).unlink(missing_ok=True) 41 | else: 42 | with open(dir_entry.path,'r') as f: 43 | jdata = f.read() 44 | self.images[dir_entry.name] = jdata 45 | 46 | def mode(self): 47 | """Return the mode ('r' or 'w')""" 48 | 49 | return 'r' if self.images is None else 'w' 50 | 51 | def update(self, key, data): 52 | """Update the data associated with the given key 53 | 54 | The Snapshot object must be in 'w' mode. 55 | 56 | :param key: The key 57 | :type key: str 58 | :param data: The object data 59 | :type data: Any JSON-serializeable value or object 60 | """ 61 | 62 | if self.mode() == 'r': 63 | raise TypeError("Snapshots.update() not supported in 'r' mode") 64 | jdata = json.dumps(data) 65 | image = self.images.get(key, None) 66 | if jdata != image: 67 | fpath = Path(self.dir,key) 68 | with open(fpath,'w') as f: 69 | f.write(jdata) 70 | self.images[key] = jdata 71 | self.filewaiter.release() 72 | 73 | def release(self): 74 | """Release any process waiting on the snapshots 75 | 76 | The Snapshot object must be in 'w' mode. 77 | """ 78 | 79 | if self.mode() == 'r': 80 | raise TypeError("Snapshots.update() not supported in 'r' mode") 81 | 82 | self.filewaiter.release() 83 | 84 | def delete(self, key): 85 | """Delete the data associated with the given key 86 | 87 | The Snapshot object must be in 'w' mode. 88 | 89 | :param key: The key 90 | :type key: str 91 | :param data: The object data 92 | :type data: Any JSON-serializeable value or object 93 | """ 94 | 95 | if self.mode() == 'r': 96 | raise TypeError("Snapshots.delete() not supported in 'r' mode") 97 | fpath = Path(self.dir, key) 98 | if os.path.exists(fpath): 99 | Path(fpath).unlink(missing_ok=True) 100 | self.filewaiter.release() 101 | self.images.pop(key, None) 102 | 103 | def list(self): 104 | """Return all snapshots 105 | 106 | The Snapshot object can be in either 'r' or 'w' mode. 107 | 108 | :return: A list of unserialized data objects 109 | """ 110 | 111 | if self.mode() == 'r': 112 | images = dict() 113 | entry_gen = os.scandir(self.dir) 114 | for dir_entry in entry_gen: 115 | if not dir_entry.name.startswith('.') and dir_entry.is_file(): 116 | key = dir_entry.name 117 | with open(dir_entry.path,'r') as f: 118 | jdata = f.read() 119 | images[key] = jdata 120 | else: 121 | images = self.images 122 | 123 | keys = list(images.keys()) 124 | keys.sort() 125 | objs = list() 126 | for key in keys: 127 | jdata = images[key] 128 | objs.append(json.loads(jdata)) 129 | return objs 130 | 131 | def list_when_updated(self, max_wait=0): 132 | """Return all snapshots when something is updated or after max_wait sec 133 | 134 | The Snapshot object must be in 'r' mode. 135 | 136 | :param key: The target key 137 | :type key: str 138 | :param max_wait: The maximum seconds to wait; if nothing is updated in 139 | this number of seconds, all snapshots are returned (default=0) 140 | :type max_wait: int 141 | :return: A list of unserialized data objects 142 | """ 143 | 144 | if self.mode() == 'w': 145 | raise TypeError("Snapshots.list_when_updated() " +\ 146 | "not supported in 'w' mode") 147 | if max_wait > 0: 148 | self.filewaiter.wait(max_wait) 149 | 150 | return self.list() 151 | 152 | def get(self, key): 153 | """Get the snapshot data for the given key 154 | 155 | The Snapshot object can be in either 'r' or 'w' mode. 156 | 157 | :param key: The target key 158 | :type key: str 159 | :return: The unserialized data object from the snapshot 160 | """ 161 | 162 | if self.mode() == 'r': 163 | (jdata, token) = self._get_from_file(key) 164 | else: 165 | jdata = self.images.get(key,None) 166 | if jdata is None: 167 | return None 168 | return json.loads(jdata) 169 | 170 | def get_when_updated(self, key, max_wait=0, token=None): 171 | """Get the snapshot data for the given key when it is updated 172 | 173 | The Snapshot object must be in 'r' mode. 174 | 175 | :param key: The target key 176 | :type key: str 177 | :param max_wait: The maximum seconds to wait 178 | :type max_wait: int 179 | :param token: A token used to track when the file has changed; if None, 180 | the function returns immediately 181 | :type token: list 182 | :return: A (data, token) pair; the token is None if the key does not 183 | exist, and data is None if the operation times out 184 | """ 185 | if self.mode() == 'w': 186 | raise TypeError("Snapshots.get_when_updated() " +\ 187 | "not supported in 'w' mode") 188 | (jdata, curr_token) = self._get_from_file(key) 189 | if (token is None) or (token != curr_token): 190 | return (jdata, curr_token) 191 | 192 | curr_time = int(time.time()) 193 | expire_time = curr_time + max_wait 194 | if curr_time < expire_time: 195 | remaining_time = expire_time - curr_time 196 | self.filewaiter.wait(remaining_time) 197 | (jdata, curr_token) = self._get_from_file(key) 198 | if (token is None) or (token != curr_token): 199 | return (jdata, curr_token) 200 | return (None, curr_token) 201 | 202 | 203 | def _get_from_file(self, key): 204 | fpath = Path(self.dir, key) 205 | if not os.path.exists(fpath): 206 | return (None, None) 207 | with open(fpath,'r') as f: 208 | jdata = f.read() 209 | fd = f.fileno() 210 | statinfo = os.fstat(fd) 211 | token = (statinfo.st_mtime, statinfo.st_size) 212 | data = json.loads(jdata) 213 | return (data, token); 214 | -------------------------------------------------------------------------------- /src/actionablepacket.py: -------------------------------------------------------------------------------- 1 | from amieclient.packet.base import Packet as AMIEPacket 2 | from miscfuncs import (Prettifiable, pformat, to_expanded_string) 3 | from misctypes import DateTime 4 | from amieparms import (get_packet_keys, parse_atrid) 5 | from taskstatus import (State, TaskStatus, TaskStatusList) 6 | 7 | class ActionablePacket(Prettifiable,dict): 8 | 9 | @staticmethod 10 | def create_reply(packet)-> AMIEPacket: 11 | """Create a reply AMIEPacket for given AMIEPacket""" 12 | 13 | reply_packet = packet.reply_packet() 14 | packet_type = reply_packet.__class__._packet_type 15 | if packet_type == 'inform_transaction_complete': 16 | reply_packet.DetailCode = 1 17 | reply_packet.Message = "Success" 18 | reply_packet.StatusCode = "Success" 19 | ActionablePacket._fill_in_reply(packet, reply_packet) 20 | return reply_packet 21 | 22 | @staticmethod 23 | def create_failure_reply(packet, *args, **kwargs) -> AMIEPacket: 24 | """Create a reply ITC failure packet for given AMIEPacket""" 25 | 26 | reply_packet = packet.reply_with_failure(*args, **kwargs) 27 | ActionablePacket._fill_in_reply(packet, reply_packet) 28 | return reply_packet 29 | 30 | @staticmethod 31 | def _fill_in_reply(packet, reply_packet): 32 | # The reply packets created by amieclient's Packet.reply_packet() and 33 | # Packet.reply_with_failure do not have the transaction_id, 34 | # originating_site_name, remote_site_name, local_site_name, packet_id, 35 | # and (sometimes) packet_rec_id fields set, because they are not needed 36 | # when the in_reply_to attribute is set. However, the mediator needs 37 | # these fields set to extract needed keys from the reply packet. 38 | # 39 | # The transaction_id, packet_id, and packet_rec_id are created by the 40 | # originating site. The originating site, remote site, and local site 41 | # are set when the transaction is created, and along with the 42 | # transaction_id do not change for the lifetime of the transaction. The 43 | # packet_id is set by the site creating the packet, and must be unique 44 | # for the creating site within the transaction. We just assume the AMIE 45 | # server is allocating its packet_ids appropriately, and add a big 46 | # number to it, because we know it will be unique for us. 47 | # 48 | # The packet_rec_id is globally unique for the AMIE server and is 49 | # unset in the reply packet. 50 | # 51 | reply_packet.transaction_id = packet.transaction_id 52 | reply_packet.originating_site_name = packet.originating_site_name 53 | reply_packet.remote_site_name = packet.remote_site_name 54 | reply_packet.local_site_name = packet.local_site_name 55 | reply_packet.packet_id = packet.packet_id + 1000 56 | 57 | def __init__(self, amie_packet, tasks=None): 58 | """Create a packet dict usable by ServiceProviders from an AMIE packet 59 | 60 | The dict will contain the following entries:: 61 | job_id : globally-unique packet id for AMIE - the 62 | Packet.packet_rec_id (not to be confused 63 | with Packet.packet_id). Since the packet 64 | is associated with a set of tasks in the 65 | ServiceProvider, we call this job_id in 66 | that context to avoid confusion with 67 | the amie_packet_id 68 | amie_packet_type : AMIE packet type 69 | amie_packet_timestamp : packet update timestamp 70 | amie_transaction_id : string that uniquely identifies the AMIE 71 | transaction 72 | amie_packet_id : string that uniquely identifies the packet 73 | within the AMIE transaction 74 | amie_packet : AMIE Packet 75 | tasks : TaskStatusList for local tasks 76 | : entries from the Packet body 77 | 78 | :param amie_packet: An AMIE Packet 79 | :type amie_packet: amieclient.packet.base.Packet 80 | :param tasks: task data for the packet 81 | :type tasks: TaskStatusList, optional 82 | """ 83 | 84 | apdict = self._amiepacket_to_dict(amie_packet) 85 | job_id, atrid, pid = get_packet_keys(amie_packet) 86 | packet_type = amie_packet.__class__._packet_type 87 | dt = DateTime(amie_packet.packet_timestamp) 88 | timestamp = dt.datetime().timestamp() 89 | if tasks is None: 90 | tasks = TaskStatusList() 91 | 92 | apdict['amie_packet'] = amie_packet 93 | apdict['amie_packet_type'] = packet_type 94 | apdict['job_id'] = job_id 95 | apdict['amie_transaction_id'] = atrid 96 | apdict['amie_packet_id'] = pid 97 | apdict['amie_packet_timestamp'] = timestamp 98 | apdict['timestamp'] = timestamp 99 | apdict['tasks'] = tasks 100 | 101 | dict.__init__(self,apdict) 102 | 103 | def _amiepacket_to_dict(self, amie_packet): 104 | packet_dict = amie_packet.as_dict()['body'] 105 | 106 | # For some stange reason, "ResourceList" never has more than one 107 | # resource, so we will make it a scalar to make things easier for 108 | # the service provider. 109 | if 'ResourceList' in packet_dict: 110 | resources = packet_dict['ResourceList'] 111 | packet_dict['Resource'] = resources[0] if resources else None 112 | 113 | return packet_dict 114 | 115 | def mk_name(self): 116 | """Return a convenient name for the ActionablePacket 117 | 118 | This is a normal method, but since ActionablePacket subclasses dict, 119 | it can be used as a static method on any dict with with the expected 120 | attributes ('amie_packet_type', 'amie_transaction_id', and 121 | 'amie_packet_id'): 122 | 123 | name = ActionablePacket.mk_name(my_compatible_dict) 124 | 125 | """ 126 | 127 | ptype = self['amie_packet_type'] 128 | atrid = self['amie_transaction_id'] 129 | pid = self['amie_packet_id'] 130 | return '{}.{}.{}'.format(ptype,atrid,pid) 131 | 132 | def update(self, packet, task_status_list): 133 | """Update a ActionablePacket from AMIE packet and task list 134 | 135 | :param packet: AMIE packet 136 | :type packet: amieclient.packet.base.Packet 137 | :type task_status_list: TaskStatusList 138 | :param task_status_list: The list 139 | :type task_status_list: TaskStatusList 140 | """ 141 | if task_status_list is None: 142 | return 143 | dt = DateTime(packet.packet_timestamp) 144 | timestamp = dt.datetime().timestamp() 145 | for ts in task_status_list: 146 | self['tasks'].put(ts) 147 | if ts['timestamp'] > timestamp: 148 | timestamp = ts['timestamp'] 149 | if timestamp > self['timestamp']: 150 | self['timestamp'] = timestamp 151 | 152 | def add_or_update_task(self, task_status): 153 | """Add/update a task 154 | 155 | :param task_status: The task 156 | :type task_status: TaskStatus 157 | """ 158 | 159 | self['tasks'].put(task_status) 160 | if task_status['timestamp'] > self['timestamp']: 161 | self['timestamp'] = task_status['timestamp'] 162 | 163 | def get_tasks(self) -> TaskStatusList: 164 | return self['tasks'] 165 | 166 | def find_active_task(self): 167 | return self['tasks'].find_active_task() 168 | 169 | def create_reply_packet(self): 170 | packet = self['amie_packet'] 171 | return ActionablePacket.create_reply(packet) 172 | 173 | def create_failure_reply_packet(self, *args, **kwargs): 174 | packet = self['amie_packet'] 175 | return ActionablePacket.create_failure_reply(packet, *args, **kwargs) 176 | 177 | 178 | EXCLUDE_PFORMAT_KEYS = { 179 | 'amie_transaction_id', 180 | 'amie_packet_id', 181 | 'amie_packet_type' 182 | } 183 | 184 | def pformat(self): 185 | # exclude amie_transaction_id, amie_packet_id, and amie_packet_type, 186 | # which are redundant because of mk_name() 187 | # 188 | outdict = dict() 189 | for key in self.keys(): 190 | if key not in ActionablePacket.EXCLUDE_PFORMAT_KEYS: 191 | outdict[key] = self[key] 192 | return "ActionablePacket(" + self.mk_name() + "):\n" + \ 193 | pformat(outdict) 194 | 195 | -------------------------------------------------------------------------------- /src/project.py: -------------------------------------------------------------------------------- 1 | from amieparms import (AMIEParmDescAware, process_parms) 2 | 3 | class LookupProjectTask(AMIEParmDescAware,dict): 4 | @process_parms( 5 | allowed=[ 6 | 'RecordID', 7 | ], 8 | required=[ 9 | 'RecordID', 10 | ]) 11 | def __init__(self, *args, **kwargs): 12 | """Validate, filter, and transform arguments to ``lookup_project_task()``""" 13 | dict.__init__(self, **kwargs) 14 | 15 | 16 | class LookupProjectByGrantNumber(AMIEParmDescAware,dict): 17 | 18 | @process_parms( 19 | allowed=[ 20 | 'GrantNumber', 21 | ], 22 | required=[ 23 | 'GrantNumber', 24 | ]) 25 | def __init__(self, *args, **kwargs) -> dict: 26 | """Validate, filter, and transform arguments to ``lookup_project_by_grant_number()``""" 27 | return dict.__init__(self,**kwargs) 28 | 29 | 30 | class LookupLocalFos(AMIEParmDescAware,dict): 31 | @process_parms( 32 | allowed=[ 33 | 'PfosNumber', 34 | ], 35 | required=[ 36 | 'PfosNumber', 37 | ]) 38 | def __init__(self, *args, **kwargs): 39 | """Validate, filter, and transform arguments to ``lookup_local_fos()``""" 40 | dict.__init__(self, **kwargs) 41 | 42 | class ChooseOrAddLocalFos(AMIEParmDescAware,dict): 43 | @process_parms( 44 | allowed=[ 45 | 'amie_transaction_id', 46 | 'amie_packet_id', 47 | 'job_id', 48 | 'amie_packet_type', 49 | 'task_name', 50 | 'timestamp', 51 | 52 | 'Abstract', 53 | 'GrantNumber', 54 | 'PfosNumber', 55 | 'ProjectTitle', 56 | 'PiDepartment', 57 | ], 58 | required=[ 59 | 'amie_transaction_id', 60 | 'amie_packet_id', 61 | 'job_id', 62 | 'amie_packet_type', 63 | 'task_name', 64 | 'timestamp', 65 | 66 | 'PfosNumber', 67 | ]) 68 | def __init__(self, *args, **kwargs): 69 | """Validate, filter, and transform arguments to ``choose_local_fos()``""" 70 | dict.__init__(self, **kwargs) 71 | 72 | class LookupProjectNameBase(AMIEParmDescAware,dict): 73 | @process_parms( 74 | allowed=[ 75 | 'BoardType', 76 | 'ChargeNumber', 77 | 'GrantNumber', 78 | 'GrantType', 79 | 'NsfStatusCode', 80 | 'PfosNumber', 81 | 'PiCity', 82 | 'PiCountry', 83 | 'PiPersonID', 84 | 'PiOrganization', 85 | 'PiOrgCode', 86 | 'PiState', 87 | 'ProjectID', 88 | 'ProjectTitle', 89 | 'contract_number', 90 | 'site_org', 91 | ], 92 | required=[ 93 | 'GrantNumber', 94 | 'PfosNumber', 95 | 'PiOrganization', 96 | ['PiOrgCode', 'site_org'], 97 | 'contract_number', 98 | ]) 99 | def __init__(self, *args, **kwargs): 100 | """Validate, filter, and transform arguments to ``lookup_project_name_base()``""" 101 | dict.__init__(self, **kwargs) 102 | 103 | class ChooseOrAddProjectNameBase(AMIEParmDescAware,dict): 104 | @process_parms( 105 | allowed=[ 106 | 'amie_transaction_id', 107 | 'amie_packet_id', 108 | 'job_id', 109 | 'amie_packet_type', 110 | 'task_name', 111 | 'timestamp', 112 | 113 | 'BoardType', 114 | 'ChargeNumber', 115 | 'GrantNumber', 116 | 'GrantType', 117 | 'NsfStatusCode', 118 | 'PiCity', 119 | 'PiCountry', 120 | 'PfosNumber', 121 | 'PiPersonID', 122 | 'PiOrganization', 123 | 'PiOrgCode', 124 | 'PiState', 125 | 'ProjectID', 126 | 'ProjectTitle', 127 | 'contract_number', 128 | 'site_org', 129 | ], 130 | required=[ 131 | 'amie_transaction_id', 132 | 'amie_packet_id', 133 | 'job_id', 134 | 'amie_packet_type', 135 | 'task_name', 136 | 'timestamp', 137 | 138 | 'GrantNumber', 139 | 'PfosNumber', 140 | 'PiOrganization', 141 | 'PiOrgCode', 142 | 'contract_number', 143 | ]) 144 | def __init__(self, *args, **kwargs): 145 | """Validate, filter, and transform arguments to ``choose_or_add_project_name_base()``""" 146 | dict.__init__(self, **kwargs) 147 | 148 | 149 | class CreateProject(AMIEParmDescAware,dict): 150 | 151 | @process_parms( 152 | allowed=[ 153 | 'amie_transaction_id', 154 | 'amie_packet_id', 155 | 'job_id', 156 | 'amie_packet_type', 157 | 'task_name', 158 | 'timestamp', 159 | 160 | 'AllocationType', 161 | 'contract_number', 162 | 'local_fos', 163 | 'project_name_base', 164 | 'site_org', 165 | 'Abstract', 166 | 'AllocatedResource', 167 | 'BoardType', 168 | 'ChargeNumber', 169 | 'EndDate', 170 | 'GrantNumber', 171 | 'GrantType', 172 | 'NsfStatusCode', 173 | 'PfosNumber', 174 | 'PiPersonID', 175 | 'ProjectID', 176 | 'ProjectTitle', 177 | 'RecordID', 178 | 'RemoteSiteLogin', 179 | 'RequestType', 180 | 'Resource', 181 | 'RoleList', 182 | 'ServiceUnitsAllocated', 183 | 'Sfos', 184 | 'StartDate', 185 | ], 186 | required=[ 187 | 'amie_transaction_id', 188 | 'amie_packet_id', 189 | 'job_id', 190 | 'amie_packet_type', 191 | 'task_name', 192 | 'timestamp', 193 | 194 | 'AllocationType', 195 | 'local_fos', 196 | 'project_name_base', 197 | 'contract_number', 198 | 'EndDate', 199 | 'GrantNumber', 200 | 'PfosNumber', 201 | 'PiPersonID', 202 | 'RemoteSiteLogin', 203 | 'Resource', 204 | 'ServiceUnitsAllocated', 205 | 'StartDate', 206 | ]) 207 | def __init__(self, *args, **kwargs) -> dict: 208 | """Validate, filter, and transform arguments to ``create_project()``""" 209 | dict.__init__(self, **kwargs) 210 | 211 | 212 | class InactivateProject(AMIEParmDescAware,dict): 213 | @process_parms( 214 | allowed=[ 215 | 'amie_transaction_id', 216 | 'amie_packet_id', 217 | 'job_id', 218 | 'amie_packet_type', 219 | 'task_name', 220 | 'timestamp', 221 | 222 | 'contract_number', 223 | 'GrantNumber', 224 | 'AllocatedResource', 225 | 'Comment', 226 | 'EndDate', 227 | 'ProjectID', 228 | 'Resource', 229 | 'ServiceUnitsAllocated', 230 | 'ServiceUnitsRemaining', 231 | 'StartDate', 232 | ], 233 | required=[ 234 | 'amie_transaction_id', 235 | 'amie_packet_id', 236 | 'job_id', 237 | 'amie_packet_type', 238 | 'task_name', 239 | 'timestamp', 240 | 241 | 'ProjectID', 242 | 'Resource', 243 | ]) 244 | def __init__(self, *args, **kwargs) -> dict: 245 | """Validate, filter, and transform arguments to ``inactivate_project()``""" 246 | dict.__init__(self, **kwargs) 247 | 248 | class ReactivateProject(AMIEParmDescAware,dict): 249 | @process_parms( 250 | allowed=[ 251 | 'amie_transaction_id', 252 | 'amie_packet_id', 253 | 'job_id', 254 | 'amie_packet_type', 255 | 'task_name', 256 | 'timestamp', 257 | 258 | 'GrantNumber', 259 | 'AllocatedResource', 260 | 'Comment', 261 | 'EndDate', 262 | 'PiPersonID', 263 | 'ProjectID', 264 | 'Resource', 265 | 'ServiceUnitsAllocated', 266 | 'ServiceUnitsRemaining', 267 | 'StartDate', 268 | ], 269 | required=[ 270 | 'amie_transaction_id', 271 | 'amie_packet_id', 272 | 'job_id', 273 | 'amie_packet_type', 274 | 'task_name', 275 | 'timestamp', 276 | 277 | 'ProjectID', 278 | 'Resource', 279 | ]) 280 | def __init__(self, *args, **kwargs) -> dict: 281 | """Validate, filter, and transform arguments to ``reactivate_project()``""" 282 | dict.__init__(self, **kwargs) 283 | 284 | -------------------------------------------------------------------------------- /src/handler/subtasks.py: -------------------------------------------------------------------------------- 1 | from packethandler import PacketHandler 2 | from misctypes import DateTime 3 | from taskstatus import TaskStatus 4 | from miscfuncs import (truthy, to_expanded_string) 5 | 6 | def define_org_code(spa, apacket, prefix): 7 | amie_org = spa.lookup_org(apacket,prefix) 8 | if amie_org is None: 9 | org_ts = spa.choose_or_add_org(apacket, prefix) 10 | if org_ts['task_state'] == "successful": 11 | org_code = org_ts.get_product_value('OrgCode') 12 | else: 13 | return org_ts 14 | else: 15 | org_code = amie_org['OrgCode'] 16 | apacket['org_code'] = org_code 17 | return None 18 | 19 | def define_person(spa, apacket, prefix): 20 | amie_person = spa.lookup_person(apacket,prefix) 21 | if amie_person is None: 22 | spa.logdumper.debug("define_person: apacket=",apacket) 23 | person_ts = spa.choose_or_add_person(apacket, prefix) 24 | if person_ts['task_state'] == "successful": 25 | person_id = person_ts.get_product_value('PersonID') 26 | site_org = person_ts.get_product_value('site_org') 27 | person_active = person_ts.get_product_value('active') 28 | else: 29 | return person_ts 30 | else: 31 | person_id = amie_person['PersonID'] 32 | username = amie_person['RemoteSiteLogin'] 33 | site_org = amie_person.get('site_org',None) 34 | person_active = amie_person['active'] 35 | 36 | spa.logdumper.debug("define_person: amie_person=",amie_person) 37 | apacket['person_id'] = person_id 38 | apacket['RemoteSiteLogin'] = username 39 | apacket['PersonID'] = person_id 40 | apacket['site_org'] = site_org 41 | apacket['person_active'] = truthy(person_active) 42 | return None 43 | 44 | def update_person_DNs(spa, apacket, prefix): 45 | spa.logdumper.debug("update_person_DNs: apacket=", apacket) 46 | updn_ts = spa.update_person_DNs(apacket, prefix) 47 | if updn_ts['task_state'] == "successful": 48 | return None 49 | else: 50 | return updn_ts 51 | 52 | def activate_person(spa, apacket, prefix): 53 | person_active = truthy(apacket.get('person_active','0')) 54 | if not person_active: 55 | spa.logdumper.debug("activate_person: prefix="+prefix+" apacket=", 56 | apacket) 57 | active_ts = spa.activate_person(apacket, prefix) 58 | if active_ts['task_state'] == "successful": 59 | person_active = active_ts.get_product_value('active') 60 | else: 61 | return active_ts 62 | 63 | spa.logger.debug("activate_person: person_active="+person_active) 64 | apacket['person_active'] = person_active 65 | return None 66 | 67 | def merge_person(spa, apacket): 68 | spa.logdumper.debug("merge_person: apacket=", apacket) 69 | merge_ts = spa.merge_person(apacket) 70 | if merge_ts['task_state'] == "successful": 71 | return None 72 | else: 73 | return merge_ts 74 | 75 | def define_contract_number(spa, apacket): 76 | spa.logdumper.debug("define_contract_number: apacket=", apacket) 77 | cts = spa.choose_or_add_contract_number(apacket) 78 | if cts['task_state'] == "successful": 79 | contract_number = cts.get_product_value('contract_number') 80 | else: 81 | return cts 82 | spa.logger.debug("define_contract_number contract_number=" + \ 83 | str(contract_number or "")) 84 | apacket['contract_number'] = contract_number 85 | return None 86 | 87 | def define_local_fos(spa, apacket): 88 | local_fos = spa.lookup_local_fos(apacket) 89 | if local_fos is None: 90 | spa.logdumper.debug("define_local_fos: apacket=", apacket) 91 | lf_ts = spa.choose_or_add_local_fos(apacket) 92 | if lf_ts['task_state'] == "successful": 93 | local_fos = lf_ts.get_product_value('areaOfInterest') 94 | else: 95 | return lf_ts 96 | spa.logger.debug("define_local_fos local_fos="+local_fos) 97 | apacket['local_fos'] = local_fos 98 | return None 99 | 100 | def lookup_project_by_grant_number(spa, apacket): 101 | spa.logdumper.debug("lookup_project_by_grant_number: apacket=", apacket) 102 | project_id = spa.lookup_project_by_grant_number(apacket) 103 | if project_id: 104 | apacket['project_id'] = project_id 105 | return project_id 106 | 107 | def define_project_name_base(spa, apacket): 108 | project_name_base = spa.lookup_project_name_base(apacket) 109 | if project_name_base is None: 110 | spa.logdumper.debug("define_project_name_base: apacket=", apacket) 111 | pnb_ts = spa.choose_or_add_project_name_base(apacket) 112 | if pnb_ts['task_state'] == "successful": 113 | project_name_base = pnb_ts.get_product_value('project_name_base') 114 | else: 115 | return pnb_ts 116 | spa.logger.debug("define_project_name_base project_name_base="+\ 117 | project_name_base) 118 | apacket['project_name_base'] = project_name_base 119 | return None 120 | 121 | def define_project(spa, apacket): 122 | spa.logdumper.debug("define_project: apacket=", apacket) 123 | project_ts = spa.create_project(apacket) 124 | if project_ts['task_state'] == "successful": 125 | project_id = project_ts.get_product_value('ProjectID') 126 | service_units_allocated = project_ts.get_product_value('ServiceUnitsAllocated') 127 | start_date = project_ts.get_product_value('StartDate') 128 | end_date = project_ts.get_product_value('EndDate') 129 | remote_site_login = project_ts.get_product_value('PiRemoteSiteLogin') 130 | else: 131 | return project_ts 132 | apacket['project_id'] = project_id 133 | apacket['service_units_allocated'] = service_units_allocated 134 | apacket['start_date'] = start_date 135 | apacket['end_date'] = end_date 136 | apacket['remote_site_login'] = remote_site_login 137 | 138 | def lookup_project_task(spa, apacket): 139 | spa.logdumper.debug("lookup_project_task: apacket=", apacket) 140 | ts = spa.lookup_project_task(apacket) 141 | if ts: 142 | apacket['PiPersonID'] = ts.get_product_value('PiPersonID') 143 | apacket['PiRemoteSiteLogin'] = ts.get_product_value('PiRemoteSiteLogin') 144 | apacket['ProjectID'] = ts.get_product_value('ProjectID') 145 | apacket['ServiceUnitsAllocated'] = \ 146 | ts.get_product_value('ServiceUnitsAllocated') 147 | apacket['StartDate'] = ts.get_product_value('StartDate') 148 | apacket['EndDate'] = ts.get_product_value('EndDate') 149 | return ts 150 | 151 | 152 | def define_account(spa, apacket, prefix): 153 | remote_site_login = apacket.get('remote_site_login',None) 154 | if not remote_site_login: 155 | spa.logdumper.debug("define_account: prefix="+prefix+" apacket=", 156 | apacket) 157 | account_ts = spa.create_account(apacket,prefix) 158 | if account_ts['task_state'] == "successful": 159 | remote_site_login = \ 160 | account_ts.get_product_value('RemoteSiteLogin') 161 | account_activity_time = \ 162 | account_ts.get_product_value('AccountActivityTime') 163 | project_id = \ 164 | account_ts.get_product_value('ProjectID') 165 | else: 166 | return account_ts 167 | spa.logger.debug("define_account remote_site_login="+remote_site_login) 168 | apacket['remote_site_login'] = remote_site_login 169 | apacket['account_activity_time'] = account_activity_time 170 | apacket['project_id'] = project_id 171 | 172 | def define_allocation(spa, apacket): 173 | spa.logdumper.debug("define_allocation: apacket=", apacket) 174 | alloc_ts = spa.update_allocation(apacket) 175 | if alloc_ts['task_state'] == "successful": 176 | service_units_allocated = \ 177 | alloc_ts.get_product_value('ServiceUnitsAllocated') 178 | start_date = \ 179 | alloc_ts.get_product_value('StartDate') 180 | end_date = \ 181 | alloc_ts.get_product_value('EndDate') 182 | resource_name = \ 183 | alloc_ts.get_product_value('resource_name') 184 | else: 185 | return alloc_ts 186 | apacket['service_units_allocated'] = service_units_allocated 187 | apacket['start_date'] = start_date 188 | apacket['end_date'] = end_date 189 | apacket['resource_name'] = resource_name 190 | 191 | def modify_user(spa, apacket): 192 | spa.logdumper.debug("modify_user: apacket=", apacket) 193 | user_ts = spa.modify_user(apacket) 194 | if user_ts['task_state'] == "successful": 195 | return None 196 | else: 197 | return user_ts 198 | 199 | def notify_user(spa, apacket): 200 | spa.logdumper.debug("notify_user: apacket=", apacket) 201 | notify_ts = spa.notify_user(apacket) 202 | if notify_ts['task_state'] == "successful": 203 | return None 204 | else: 205 | return notify_ts 206 | --------------------------------------------------------------------------------