├── .gitignore ├── LICENSE ├── README.md ├── jasmin_api ├── jasmin_api │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── rest_api │ ├── __init__.py │ ├── exceptions.py │ ├── middleware.py │ ├── migrations │ │ └── __init__.py │ ├── tests.py │ ├── tools.py │ └── views │ │ ├── __init__.py │ │ ├── filters.py │ │ ├── groups.py │ │ ├── httpccm.py │ │ ├── morouter.py │ │ ├── mtrouter.py │ │ ├── smppccm.py │ │ └── users.py └── run_cherrypy.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | jasmin_api/db.sqlite3 53 | jasmin_api/jasmin_api/local_settings.py 54 | /jasmin_api/static/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jasmin-api 2 | Jasmin restful management API 3 | 4 | ##Documentation 5 | 6 | * Installation and cofiguration of the API below 7 | * General Jasmin documentation [http://docs.jasminsms.com/en/latest/index.html](http://docs.jasminsms.com/en/latest/index.html) 8 | * Jasmin management CLI, which is wrapped by the rest API [http://docs.jasminsms.com/en/latest/management/jcli/modules.html](http://docs.jasminsms.com/en/latest/management/jcli/modules.html) 9 | * Swagger documentation will be on the path /docs/ If you run locally with default settings this will be [http://localhost:8000/docs/](http://localhost:8000/docs/) 10 | 11 | 12 | ##Settings 13 | 14 | Requires local_settings.py in jasmin_api/jasmin_api 15 | (same directory as settings.py) which should contain: 16 | 17 | DEBUG = False 18 | SECRET_KEY = '[some random string]' 19 | 20 | set DEBUG = True for testing 21 | 22 | By default a SQLite database will be used for storing authentication data. You can use a different database by adding a [DATABASES setting](https://docs.djangoproject.com/en/1.8/ref/settings/#databases) to local_settings.py 23 | 24 | You can hide the Swagger documentation, which is shown by default, by adding: 25 | 26 | SHOW_SWAGGER = False 27 | 28 | You can also override the default settings for the telnet connection in local_settings.py. These settings with their defaults are: 29 | 30 | TELNET_HOST = '127.0.0.1' 31 | TELNET_PORT = 8990 32 | TELNET_USERNAME = 'jcliadmin' 33 | TELNET_PW = 'jclipwd' 34 | 35 | ##Installing 36 | 37 | We recommend installing in a virtualenv 38 | 39 | 1. Install dependencies: 40 | 41 | $ pip install -r requirements.txt 42 | 43 | 2. cd to jasmin_api and run: 44 | 45 | $ ./manage.py migrate 46 | $ ./manage.py createsuperuser 47 | $ ./manage.py collectstatic 48 | 49 | The last is only needed if you are running the production server (see below) 50 | rather than the Django dev server. It should be run again on any upgrade that changes static files. If in doubt, run it. 51 | 52 | ##Running 53 | 54 | To run for testing and development: 55 | cd jasmin_api;./manage.py runserver 56 | 57 | This is slower, requires DEBUG=True, and is **much less secure** 58 | 59 | To run on production: 60 | 61 | cd jasmin_api;run_cherrypy.py 62 | 63 | This requires that you run the collectstatic command (see above) and you should 64 | have DEBUG=False 65 | 66 | ## Dependencies and requirements 67 | * Python 2.7 required, use of virtualenv recommended 68 | * A command line telnet client should be installed - this is a usual with Unix type OSes 69 | * See requirements.txt for packages installable from pypi 70 | * Optional: libyaml would improve performance of Django REST Swagger 71 | -------------------------------------------------------------------------------- /jasmin_api/jasmin_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jookies/jasmin-api/0e34527f6b3e476332fbba154792ab8ee20ee770/jasmin_api/jasmin_api/__init__.py -------------------------------------------------------------------------------- /jasmin_api/jasmin_api/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__)) 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | ################################################################################ 8 | # Settings most likely to need overriding in local_settings.py # 9 | ################################################################################ 10 | 11 | #Jasmin telnet defaults, override in local_settings.py 12 | TELNET_HOST = '127.0.0.1' 13 | TELNET_PORT = 8990 14 | TELNET_USERNAME = 'jcliadmin' 15 | TELNET_PW = 'jclipwd' # no alternative storing as plain text 16 | TELNET_TIMEOUT = 10 # reasonable value for intranet. 17 | 18 | 19 | REST_FRAMEWORK = { 20 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 21 | 'rest_framework.authentication.BasicAuthentication', 22 | ), 23 | 'DEFAULT_PERMISSION_CLASSES': ( 24 | 'rest_framework.permissions.IsAuthenticated', 25 | ), 26 | } 27 | 28 | ################################################################################ 29 | # Other settings # 30 | ################################################################################ 31 | 32 | 33 | STANDARD_PROMPT = 'jcli : ' # There should be no need to change this 34 | INTERACTIVE_PROMPT ='> ' # Prompt for interactive commands 35 | 36 | #This should be OK for REST API - we are not generating URLs 37 | #see https://www.djangoproject.com/weblog/2013/feb/19/security/#s-issue-host-header-poisoning 38 | ALLOWED_HOSTS = ['*'] 39 | 40 | SWAGGER_SETTINGS = { 41 | 'exclude_namespaces': [], 42 | 'api_version': '', 43 | 'is_authenticated': False, 44 | 'is_superuser': False, 45 | 'info': { 46 | 'description': 'A REST API for managing Jasmin SMS Gateway', 47 | 'title': 'Jasim Management REST API', 48 | }, 49 | } 50 | 51 | 52 | # Application definition 53 | 54 | INSTALLED_APPS = ( 55 | 'django.contrib.auth', 56 | 'django.contrib.contenttypes', 57 | 'django.contrib.sessions', 58 | 'django.contrib.messages', 59 | 'django.contrib.staticfiles', 60 | 'rest_framework', 61 | 'rest_api', 62 | ) 63 | 64 | MIDDLEWARE_CLASSES = ( 65 | 'django.contrib.sessions.middleware.SessionMiddleware', 66 | 'django.middleware.common.CommonMiddleware', 67 | 'django.middleware.csrf.CsrfViewMiddleware', 68 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 69 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 70 | 'django.contrib.messages.middleware.MessageMiddleware', 71 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 72 | 'django.middleware.security.SecurityMiddleware', 73 | 'rest_api.middleware.TelnetConnectionMiddleware' 74 | ) 75 | 76 | ROOT_URLCONF = 'jasmin_api.urls' 77 | 78 | TEMPLATES = [ 79 | { 80 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 81 | 'DIRS': [], 82 | 'APP_DIRS': True, 83 | 'OPTIONS': { 84 | 'context_processors': [ 85 | 'django.template.context_processors.debug', 86 | 'django.template.context_processors.request', 87 | 'django.contrib.auth.context_processors.auth', 88 | 'django.contrib.messages.context_processors.messages', 89 | ], 90 | }, 91 | }, 92 | ] 93 | 94 | WSGI_APPLICATION = 'jasmin_api.wsgi.application' 95 | 96 | 97 | # Database 98 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 99 | 100 | DATABASES = { 101 | 'default': { 102 | 'ENGINE': 'django.db.backends.sqlite3', 103 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 104 | } 105 | } 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 127 | 128 | #Simplify config to show/hide Swagger docs 129 | SHOW_SWAGGER = True 130 | 131 | with open(os.path.join(SETTINGS_DIR, 'local_settings.py')) as f: 132 | exec(f.read()) 133 | 134 | if SHOW_SWAGGER: 135 | INSTALLED_APPS += ('rest_framework_swagger',) 136 | -------------------------------------------------------------------------------- /jasmin_api/jasmin_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.conf import settings 3 | 4 | from rest_framework.routers import DefaultRouter 5 | 6 | from rest_api.views import ( 7 | GroupViewSet, UserViewSet, MORouterViewSet, SMPPCCMViewSet, HTTPCCMViewSet, MTRouterViewSet, FiltersViewSet 8 | ) 9 | 10 | router = DefaultRouter() 11 | router.register(r'groups', GroupViewSet, base_name='groups') 12 | router.register(r'users', UserViewSet, base_name='users') 13 | router.register(r'morouters', MORouterViewSet, base_name='morouters') 14 | router.register(r'mtrouters', MTRouterViewSet, base_name='mtrouters') 15 | router.register(r'smppsconns', SMPPCCMViewSet, base_name='smppcons') 16 | router.register(r'httpsconns', HTTPCCMViewSet, base_name='httpcons') 17 | router.register(r'filters', FiltersViewSet, base_name='filters') 18 | 19 | urlpatterns = [ 20 | url(r'^api/', include(router.urls)), 21 | ] 22 | 23 | 24 | if settings.SHOW_SWAGGER: 25 | urlpatterns += [url(r'^docs/', include('rest_framework_swagger.urls'))] 26 | 27 | 28 | -------------------------------------------------------------------------------- /jasmin_api/jasmin_api/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for jasmin_api project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jasmin_api.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /jasmin_api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jasmin_api.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jookies/jasmin-api/0e34527f6b3e476332fbba154792ab8ee20ee770/jasmin_api/rest_api/__init__.py -------------------------------------------------------------------------------- /jasmin_api/rest_api/exceptions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import APIException 2 | 3 | class TelnetUnexpectedResponse(APIException): 4 | status_code = 500 5 | default_detail = 'Unexpected response from Jasmin' 6 | 7 | class TelnetConnectionTimeout(APIException): 8 | status_code = 500 9 | default_detail = 'Connection to jcli timed out' 10 | 11 | class TelnetLoginFailed(APIException): 12 | status_code = 403 13 | default_detail = 'Jasmin login failed' 14 | 15 | class CanNotModifyError(APIException): 16 | status_code = 400 17 | default_detail = 'Can not modify a key' 18 | 19 | class JasminSyntaxError(APIException): 20 | status_code = 400 21 | default_detail = 'Can not modify a key' 22 | 23 | class JasminError(APIException): 24 | status_code = 400 25 | default_detail = 'Jasmin error' 26 | 27 | class UnknownError(APIException): 28 | status_code = 404 29 | default_detail = 'object not known' 30 | 31 | class MissingKeyError(APIException): 32 | status_code = 400 33 | default_detail = 'A mandatory key is missing' 34 | 35 | class MutipleValuesRequiredKeyError(APIException): 36 | status_code = 400 37 | default_detail = 'Multiple values are required fro this key' 38 | 39 | class ActionFailed(APIException): 40 | status_code = 400 41 | default_detail = 'Action failed' 42 | 43 | class ObjectNotFoundError(APIException): 44 | status_code = 404 45 | default_detail = 'Object not found' 46 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/middleware.py: -------------------------------------------------------------------------------- 1 | import pexpect 2 | 3 | from django.conf import settings 4 | 5 | from .exceptions import TelnetUnexpectedResponse, TelnetConnectionTimeout, TelnetLoginFailed 6 | 7 | class TelnetConnectionMiddleware(object): 8 | def process_request(self, request): 9 | """Add a telnet connection to all request paths that start with /api/ 10 | assuming we only need to connect for these means we avoid unecessary 11 | overhead on any other functionality we add, and keeps URL path clear 12 | for it. 13 | """ 14 | if not request.path.startswith('/api/'): 15 | return None 16 | try: 17 | telnet = pexpect.spawn( 18 | "telnet %s %s" % 19 | (settings.TELNET_HOST, settings.TELNET_PORT), 20 | timeout=settings.TELNET_TIMEOUT, 21 | ) 22 | telnet.expect_exact('Username: ') 23 | telnet.sendline(settings.TELNET_USERNAME) 24 | telnet.expect_exact('Password: ') 25 | telnet.sendline(settings.TELNET_PW) 26 | except pexpect.EOF: 27 | raise TelnetUnexpectedResponse 28 | except pexpect.TIMEOUT: 29 | raise TelnetConnectionTimeout 30 | 31 | try: 32 | telnet.expect_exact(settings.STANDARD_PROMPT) 33 | except pexpect.EOF: 34 | raise TelnetLoginFailed 35 | else: 36 | request.telnet = telnet 37 | return None 38 | 39 | def process_response(self, request, response): 40 | "Make sure telnet connection is closed when unleashing response back to client" 41 | if hasattr(request, 'telnet'): 42 | try: 43 | request.telnet.sendline('quit') 44 | except pexpect.ExceptionPexpect: 45 | request.telnet.kill(9) 46 | return response 47 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jookies/jasmin-api/0e34527f6b3e476332fbba154792ab8ee20ee770/jasmin_api/rest_api/migrations/__init__.py -------------------------------------------------------------------------------- /jasmin_api/rest_api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/tools.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from .exceptions import (CanNotModifyError, JasminSyntaxError, 4 | JasminError, UnknownError) 5 | 6 | STANDARD_PROMPT = settings.STANDARD_PROMPT 7 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 8 | 9 | 10 | def set_ikeys(telnet, keys2vals): 11 | "set multiple keys for interactive command" 12 | for key, val in keys2vals.items(): 13 | print key, val 14 | telnet.sendline("%s %s" % (key, val)) 15 | matched_index = telnet.expect([ 16 | r'.*(Unknown .*)' + INTERACTIVE_PROMPT, 17 | r'(.*) can not be modified.*' + INTERACTIVE_PROMPT, 18 | r'(.*)' + INTERACTIVE_PROMPT 19 | ]) 20 | result = telnet.match.group(1).strip() 21 | if matched_index == 0: 22 | raise UnknownError(result) 23 | if matched_index == 1: 24 | raise CanNotModifyError(result) 25 | telnet.sendline('ok') 26 | ok_index = telnet.expect([ 27 | r'ok(.* syntax is invalid).*' + INTERACTIVE_PROMPT, 28 | r'.*' + STANDARD_PROMPT, 29 | ]) 30 | if ok_index == 0: 31 | #remove whitespace and return error 32 | raise JasminSyntaxError(" ".join(telnet.match.group(1).split())) 33 | return 34 | 35 | 36 | def split_cols(lines): 37 | "split columns into lists, skipping blank and non-data lines" 38 | parsed = [] 39 | for line in lines: 40 | raw_split = line.split() 41 | fields = [s for s in raw_split if (s and raw_split[0][0] == '#')] 42 | parsed.append(fields) 43 | return parsed 44 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .groups import GroupViewSet 2 | from .users import UserViewSet 3 | from .morouter import MORouterViewSet 4 | from .mtrouter import MTRouterViewSet 5 | from .smppccm import SMPPCCMViewSet 6 | from .httpccm import HTTPCCMViewSet 7 | from .filters import FiltersViewSet 8 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/filters.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.conf import settings 4 | from django.http import JsonResponse 5 | from django.utils.datastructures import MultiValueDictKeyError 6 | 7 | from rest_framework.viewsets import ViewSet 8 | from rest_framework.decorators import list_route 9 | 10 | from rest_api.tools import set_ikeys, split_cols 11 | from rest_api.exceptions import (JasminSyntaxError, JasminError, 12 | UnknownError, MissingKeyError, 13 | MutipleValuesRequiredKeyError, ObjectNotFoundError) 14 | 15 | STANDARD_PROMPT = settings.STANDARD_PROMPT 16 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 17 | 18 | class FiltersViewSet(ViewSet): 19 | "Viewset for managing Filters" 20 | lookup_field = 'fid' 21 | 22 | def _list(self, telnet): 23 | "List Filters as python dict" 24 | telnet.sendline('filter -l') 25 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 26 | result = telnet.match.group(0).strip().replace("\r", '').split("\n") 27 | if len(result) < 3: 28 | return {'filters': []} 29 | results = [l.replace(', ', ',').replace('(!)', '') 30 | for l in result[2:-2] if l] 31 | filters = split_cols(results) 32 | return { 33 | 'filters': 34 | [ 35 | { 36 | 'fid': f[0].strip().lstrip('#'), 37 | 'type': f[1], 38 | 'routes': f[2] + ' ' + f[3], 39 | 'description': ' '.join(f[4:]) 40 | } for f in filters 41 | ] 42 | } 43 | 44 | def list(self, request): 45 | "List Filters. No parameters" 46 | return JsonResponse(self._list(request.telnet)) 47 | 48 | def get_filter(self, telnet, fid): 49 | "Return data for one filter as Python dict" 50 | filters = self._list(telnet)['filters'] 51 | try: 52 | return {'filter': 53 | next((m for m in filters if m['fid'] == fid), None) 54 | } 55 | except StopIteration: 56 | raise ObjectNotFoundError('No Filter with fid: %s' % fid) 57 | 58 | def retrieve(self, request, fid): 59 | "Details for one Filter by fid (integer)" 60 | return JsonResponse(self.get_filter(request.telnet, fid)) 61 | 62 | def create(self, request): 63 | """Create Filter. 64 | Required parameters: type, fid, parameters 65 | --- 66 | # YAML 67 | omit_serializer: true 68 | parameters: 69 | - name: type 70 | description: One of TransparentFilter, ConnectorFilter, UserFilter, GroupFilter, SourceAddrFilter, DestinationAddrFilter, ShortMessageFilter, DateIntervalFilter, TimeIntervalFilter, TagFilter, EvalPyFilter 71 | required: true 72 | type: string 73 | paramType: form 74 | - name: fid 75 | description: Filter id, used to identify filter 76 | required: true 77 | type: string 78 | paramType: form 79 | - name: parameter 80 | description: Parameter 81 | required: false 82 | type: string 83 | paramType: form 84 | """ 85 | telnet = request.telnet 86 | data = request.data 87 | try: 88 | ftype, fid = data['type'], data['fid'] 89 | except IndexError: 90 | raise MissingKeyError( 91 | 'Missing parameter: type or fid required') 92 | ftype = ftype.lower() 93 | telnet.sendline('filter -a') 94 | telnet.expect(r'Adding a new Filter(.+)\n' + INTERACTIVE_PROMPT) 95 | ikeys = OrderedDict({'type': ftype, 'fid': fid}) 96 | if ftype != 'transparentfilter': 97 | try: 98 | parameter = data['parameter'] 99 | except MultiValueDictKeyError: 100 | raise MissingKeyError('%s filter requires parameter' % ftype) 101 | if ftype == 'connectorfilter': 102 | ikeys['cid'] = parameter 103 | elif ftype == 'userfilter': 104 | ikeys['uid'] = parameter 105 | elif ftype == 'groupfilter': 106 | ikeys['gid'] = parameter 107 | elif ftype == 'sourceaddrfilter': 108 | ikeys['source_addr'] = parameter 109 | elif ftype == 'destinationaddrfilter': 110 | ikeys['destination_addr'] = parameter 111 | elif ftype == 'shortmessagefilter': 112 | ikeys['short_message'] = parameter 113 | elif ftype == 'dateintervalfilter': 114 | ikeys['dateInterval'] = parameter 115 | elif ftype == 'timeintervalfilter': 116 | ikeys['timeInterval'] = parameter 117 | elif ftype == 'tagfilter': 118 | ikeys['tag'] = parameter 119 | elif ftype == 'evalpyfilter': 120 | ikeys['pyCode'] = parameter 121 | print ikeys 122 | set_ikeys(telnet, ikeys) 123 | telnet.sendline('persist\n') 124 | telnet.expect(r'.*' + STANDARD_PROMPT) 125 | return JsonResponse({'filter': self.get_filter(telnet, fid)}) 126 | 127 | def simple_filter_action(self, telnet, action, fid, return_filter=True): 128 | telnet.sendline('filter -%s %s' % (action, fid)) 129 | matched_index = telnet.expect([ 130 | r'.+Successfully(.+)' + STANDARD_PROMPT, 131 | r'.+Unknown Filter: (.+)' + STANDARD_PROMPT, 132 | r'.+(.*)' + STANDARD_PROMPT, 133 | ]) 134 | if matched_index == 0: 135 | telnet.sendline('persist\n') 136 | if return_filter: 137 | telnet.expect(r'.*' + STANDARD_PROMPT) 138 | return JsonResponse({'filter': self.get_filter(telnet, fid)}) 139 | else: 140 | return JsonResponse({'fid': fid}) 141 | elif matched_index == 1: 142 | raise UnknownError(detail='No filter:' + fid) 143 | else: 144 | raise JasminError(telnet.match.group(1)) 145 | 146 | def destroy(self, request, fid): 147 | """Delete a filter. One parameter required, the filter identifier (a string) 148 | 149 | HTTP codes indicate result as follows 150 | 151 | - 200: successful deletion 152 | - 404: nonexistent filter 153 | - 400: other error 154 | """ 155 | return self.simple_filter_action( 156 | request.telnet, 'r', fid, return_filter=False) 157 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/groups.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import JsonResponse 3 | 4 | from rest_framework.viewsets import ViewSet 5 | from rest_framework.decorators import detail_route 6 | 7 | from rest_api.exceptions import MissingKeyError, ActionFailed, ObjectNotFoundError 8 | 9 | STANDARD_PROMPT = settings.STANDARD_PROMPT 10 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 11 | 12 | class GroupViewSet(ViewSet): 13 | "ViewSet for managing *Jasmin* user groups (*not* Django auth groups)" 14 | lookup_field = 'gid' 15 | 16 | def list(self, request): 17 | "List groups. No request parameters provided or required." 18 | telnet = request.telnet 19 | telnet.sendline('group -l') 20 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 21 | result = telnet.match.group(0).strip().replace("\r", '').split("\n") 22 | if len(result) < 3: 23 | return JsonResponse({'groups': []}) 24 | groups = result[2:-2] 25 | return JsonResponse( 26 | { 27 | 'groups': 28 | [ 29 | { 30 | 'name': g.strip().lstrip('!#'), 'status': ( 31 | 'disabled' if g[1] == '!' else 'enabled' 32 | ) 33 | } for g in groups 34 | ] 35 | } 36 | ) 37 | 38 | def create(self, request): 39 | """Create a group. 40 | One POST parameter required, the group identifier (a string) 41 | --- 42 | # YAML 43 | omit_serializer: true 44 | parameters: 45 | - name: gid 46 | description: Group identifier 47 | required: true 48 | type: string 49 | paramType: form 50 | """ 51 | telnet = request.telnet 52 | telnet.sendline('group -a') 53 | telnet.expect(r'Adding a new Group(.+)\n' + INTERACTIVE_PROMPT) 54 | if not 'gid' in request.data: 55 | raise MissingKeyError('Missing gid (group identifier)') 56 | telnet.sendline('gid ' + request.data['gid'] + '\n') 57 | telnet.expect(INTERACTIVE_PROMPT) 58 | telnet.sendline('ok\n') 59 | 60 | matched_index = telnet.expect([ 61 | r'.+Successfully added(.+)\[(.+)\][\n\r]+' + STANDARD_PROMPT, 62 | r'.+Error: (.+)[\n\r]+' + INTERACTIVE_PROMPT, 63 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 64 | ]) 65 | if matched_index == 0: 66 | gid = telnet.match.group(2).strip() 67 | telnet.sendline('persist\n') 68 | return JsonResponse({'name': gid}) 69 | else: 70 | raise ActionFailed(telnet.match.group(1)) 71 | 72 | def simple_group_action(self, telnet, action, gid): 73 | telnet.sendline('group -%s %s' % (action, gid)) 74 | matched_index = telnet.expect([ 75 | r'.+Successfully(.+)' + STANDARD_PROMPT, 76 | r'.+Unknown Group: (.+)' + STANDARD_PROMPT, 77 | r'.+(.*)' + STANDARD_PROMPT, 78 | ]) 79 | if matched_index == 0: 80 | telnet.sendline('persist\n') 81 | return JsonResponse({'name': gid}) 82 | elif matched_index == 1: 83 | raise ObjectNotFoundError('Unknown group: %s' % gid) 84 | else: 85 | raise ActionFailed(telnet.match.group(1)) 86 | 87 | def destroy(self, request, gid): 88 | """Delete a group. One parameter required, the group identifier (a string) 89 | 90 | HTTP codes indicate result as follows 91 | 92 | - 200: successful deletion 93 | - 404: nonexistent group 94 | - 400: other error 95 | """ 96 | return self.simple_group_action(request.telnet, 'r', gid) 97 | 98 | @detail_route(methods=['put']) 99 | def enable(self, request, gid): 100 | """Enable a group. One parameter required, the group identifier (a string) 101 | 102 | HTTP codes indicate result as follows 103 | 104 | - 200: successful deletion 105 | - 404: nonexistent group 106 | - 400: other error 107 | """ 108 | return self.simple_group_action(request.telnet, 'e', gid) 109 | 110 | 111 | @detail_route(methods=['put']) 112 | def disable(self, request, gid): 113 | """Disable a group. 114 | 115 | One parameter required, the group identifier (a string) 116 | 117 | HTTP codes indicate result as follows 118 | 119 | - 200: successful deletion 120 | - 404: nonexistent group 121 | - 400: other error 122 | """ 123 | return self.simple_group_action(request.telnet, 'd', gid) 124 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/httpccm.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import JsonResponse 3 | 4 | from rest_framework.viewsets import ViewSet 5 | from rest_framework.parsers import JSONParser 6 | from rest_framework.decorators import detail_route, parser_classes 7 | from rest_api.tools import set_ikeys, split_cols 8 | from rest_api.exceptions import ( 9 | JasminSyntaxError, JasminError, ActionFailed, 10 | ObjectNotFoundError, UnknownError, 11 | ) 12 | 13 | STANDARD_PROMPT = settings.STANDARD_PROMPT 14 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 15 | 16 | 17 | class HTTPCCMViewSet(ViewSet): 18 | "Viewset for managing HTTP Client Connectors" 19 | lookup_field = 'cid' 20 | 21 | def get_httpccm(self, telnet, cid, silent=False): 22 | #Some of this could be abstracted out - similar pattern in users.py 23 | telnet.sendline('httpccm -s ' + cid) 24 | matched_index = telnet.expect([ 25 | r'.+Unknown connector:.*' + STANDARD_PROMPT, 26 | r'.+Usage:.*' + STANDARD_PROMPT, 27 | r'(.+)\n' + STANDARD_PROMPT, 28 | ]) 29 | if matched_index != 2: 30 | if silent: 31 | return 32 | else: 33 | raise ObjectNotFoundError('Unknown connector: %s' % cid) 34 | result = telnet.match.group(1) 35 | httpccm = {} 36 | for line in result.splitlines(): 37 | d = [x for x in line.split() if x] 38 | if len(d) == 2: 39 | httpccm[d[0]] = d[1] 40 | return httpccm 41 | 42 | def get_connector_list(self, telnet): 43 | telnet.sendline('httpccm -l') 44 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 45 | result = telnet.match.group(0).strip().replace("\r", '').split("\n") 46 | if len(result) < 3: 47 | return [] 48 | return split_cols(result[2:-2]) 49 | 50 | def simple_httpccm_action(self, telnet, action, cid): 51 | telnet.sendline('httpccm -%s %s' % (action, cid)) 52 | matched_index = telnet.expect([ 53 | r'.+Successfully(.+)' + STANDARD_PROMPT, 54 | r'.+Unknown connector: (.+)' + STANDARD_PROMPT, 55 | r'(.*)' + STANDARD_PROMPT, 56 | ]) 57 | if matched_index == 0: 58 | telnet.sendline('persist\n') 59 | return JsonResponse({'name': cid}) 60 | elif matched_index == 1: 61 | raise ObjectNotFoundError('Unknown HTTP Connector: %s' % cid) 62 | else: 63 | raise ActionFailed(telnet.match.group(1)) 64 | 65 | def list(self, request): 66 | """List HTTP Client Connectors. No parameters 67 | Differs from slightly from telent CLI names and values: 68 | 69 | 1. the "service" column is called "status" 70 | 2. the cid is the full connector id of the form https(cid) 71 | """ 72 | telnet = request.telnet 73 | connector_list = self.get_connector_list(telnet) 74 | connectors = [] 75 | for raw_data in connector_list: 76 | if raw_data[0][0] == '#': 77 | cid = raw_data[0][1:] 78 | connector = self.get_httpccm(telnet, cid, True) 79 | connector.update( 80 | cid=cid, 81 | type=raw_data[1], 82 | method=raw_data[2], 83 | url=raw_data[3] 84 | ) 85 | connectors.append(connector) 86 | return JsonResponse({'connectors': connectors}) 87 | 88 | def retrieve(self, request, cid): 89 | """Retreive data for one connector 90 | Required parameter: cid (connector id)""" 91 | telnet = request.telnet 92 | connector = self.get_httpccm(telnet, cid, silent=False) 93 | connector_list = self.get_connector_list(telnet) 94 | list_data = next( 95 | (raw_data for raw_data in connector_list if 96 | raw_data[0] == '#' + cid), 97 | None 98 | ) 99 | if not list_data: 100 | raise ObjectNotFoundError('Unknown connector: %s' % cid) 101 | connector.update( 102 | cid=cid, 103 | type=list_data[1], 104 | method=list_data[2], 105 | url=list_data[3] 106 | ) 107 | return JsonResponse({'connector': connector}) 108 | 109 | def create(self, request): 110 | """Create an HTTP Client Connector. 111 | Required parameter: cid (connector id) 112 | --- 113 | # YAML 114 | omit_serializer: true 115 | parameters: 116 | - name: cid 117 | description: Connector identifier 118 | required: true 119 | type: string 120 | paramType: form 121 | - name: url 122 | description: URL to be called with message parameters 123 | required: true 124 | type: string 125 | paramType: form 126 | - name: method 127 | description: Calling method (GET or POST) 128 | required: true 129 | type: string 130 | paramType: form 131 | """ 132 | telnet = request.telnet 133 | 134 | telnet.sendline('httpccm -a') 135 | data = request.data 136 | 137 | for k, v in data.items(): 138 | telnet.sendline("%s %s" % (k, v)) 139 | telnet.sendline('ok') 140 | matched_index = telnet.expect([ 141 | r'.*(HttpConnector url syntax is invalid.*)' + INTERACTIVE_PROMPT, 142 | r'.*(HttpConnector method syntax is invalid, must be GET or POST.*)' + INTERACTIVE_PROMPT, 143 | r'.*' + INTERACTIVE_PROMPT, 144 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 145 | ]) 146 | if matched_index != 2: 147 | raise JasminSyntaxError( 148 | detail=" ".join(telnet.match.group(1).split())) 149 | telnet.sendline('persist\n') 150 | telnet.expect(r'.*' + STANDARD_PROMPT) 151 | return JsonResponse({'cid': request.data['cid']}) 152 | 153 | def destroy(self, request, cid): 154 | """Delete an http connector. 155 | One parameter required, the connector identifier 156 | 157 | HTTP codes indicate result as follows 158 | 159 | - 200: successful deletion 160 | - 404: nonexistent group 161 | - 400: other error 162 | """ 163 | return self.simple_httpccm_action(request.telnet, 'r', cid) 164 | 165 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/morouter.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.conf import settings 4 | from django.http import JsonResponse 5 | from django.utils.datastructures import MultiValueDictKeyError 6 | 7 | from rest_framework.viewsets import ViewSet 8 | from rest_framework.decorators import list_route 9 | 10 | from rest_api.tools import set_ikeys, split_cols 11 | from rest_api.exceptions import (JasminSyntaxError, JasminError, 12 | UnknownError, MissingKeyError, 13 | MutipleValuesRequiredKeyError, ObjectNotFoundError) 14 | 15 | STANDARD_PROMPT = settings.STANDARD_PROMPT 16 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 17 | 18 | 19 | class MORouterViewSet(ViewSet): 20 | "Viewset for managing MO Routes" 21 | lookup_field = 'order' 22 | 23 | def _list(self, telnet): 24 | "List MO router as python dict" 25 | telnet.sendline('morouter -l') 26 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 27 | result = telnet.match.group(0).strip().replace("\r", '').split("\n") 28 | if len(result) < 3: 29 | return {'morouters': []} 30 | results = [l.replace(', ', ',').replace('(!)', '') 31 | for l in result[2:-2] if l] 32 | routers = split_cols(results) 33 | print routers 34 | return { 35 | 'morouters': 36 | [ 37 | { 38 | 'order': r[0].strip().lstrip('#'), 39 | 'type': r[1], 40 | 'connectors': [c.strip() for c in r[2].split(',')], 41 | 'filters': [c.strip() for c in ' '.join(r[3:]).split(',') 42 | ] if len(r) > 3 else [] 43 | } for r in routers 44 | ] 45 | } 46 | 47 | def list(self, request): 48 | "List MO routers. No parameters" 49 | return JsonResponse(self._list(request.telnet)) 50 | 51 | def get_router(self, telnet, order): 52 | "Return data for one morouter as Python dict" 53 | morouters = self._list(telnet)['morouters'] 54 | try: 55 | return {'morouter': 56 | next((m for m in morouters if m['order'] == order), None) 57 | } 58 | except StopIteration: 59 | raise ObjectNotFoundError('No MoROuter with order: %s' % order) 60 | 61 | def retrieve(self, request, order): 62 | "Details for one MORouter by order (integer)" 63 | return JsonResponse(self.get_router(request.telnet, order)) 64 | 65 | 66 | @list_route(methods=['delete']) 67 | def flush(self, request): 68 | "Flush entire routing table" 69 | telnet = request.telnet 70 | telnet.sendline('morouter -f') 71 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 72 | telnet.sendline('persist\n') 73 | telnet.expect(r'.*' + STANDARD_PROMPT) 74 | return JsonResponse({'morouters': []}) 75 | 76 | def create(self, request): 77 | """Create MORouter. 78 | Required parameters: type, order, smppconnectors, httpconnectors 79 | More than one connector is allowed only for RandomRoundrobinMORoute 80 | --- 81 | # YAML 82 | omit_serializer: true 83 | parameters: 84 | - name: type 85 | description: One of DefaultRoute, StaticMORoute, RandomRoundrobinMORoute 86 | required: true 87 | type: string 88 | paramType: form 89 | - name: order 90 | description: Router order, also used to identify router 91 | required: true 92 | type: string 93 | paramType: form 94 | - name: smppconnectors 95 | description: List of SMPP connector ids. 96 | required: false 97 | type: array 98 | paramType: form 99 | - name: httpconnectors 100 | description: List of HTTP connector ids. 101 | required: false 102 | type: array 103 | paramType: form 104 | - name: filters 105 | description: List of filters, required except for DefaultRoute 106 | required: false 107 | type: array 108 | paramType: form 109 | """ 110 | telnet = request.telnet 111 | data = request.data 112 | try: 113 | rtype, order = data['type'], data['order'] 114 | except IndexError: 115 | raise MissingKeyError( 116 | 'Missing parameter: type or order required') 117 | rtype = rtype.lower() 118 | telnet.sendline('morouter -a') 119 | telnet.expect(r'Adding a new MO Route(.+)\n' + INTERACTIVE_PROMPT) 120 | ikeys = OrderedDict({'type': rtype}) 121 | if rtype != 'defaultroute': 122 | try: 123 | filters = data['filters'].split(',') 124 | except MultiValueDictKeyError: 125 | raise MissingKeyError('%s router requires filters' % rtype) 126 | ikeys['filters'] = ';'.join(filters) 127 | ikeys['order'] = order 128 | print(ikeys) 129 | smppconnectors = data.get('smppconnectors', '') 130 | httpconnectors = data.get('httpconnectors', '') 131 | connectors = ['smpps(%s)' % c.strip() 132 | for c in smppconnectors.split(',') if c.strip() 133 | ] + ['http(%s)' % c for c in httpconnectors.split(',') if c.strip()] 134 | if rtype == 'randomroundrobinmoroute': 135 | if len(connectors) < 2: 136 | raise MutipleValuesRequiredKeyError( 137 | 'Round Robin route requires at least two connectors') 138 | ikeys['connectors'] = ';'.join(connectors) 139 | else: 140 | if len(connectors) != 1: 141 | raise MissingKeyError('one and only one connector required') 142 | ikeys['connector'] = connectors[0] 143 | set_ikeys(telnet, ikeys) 144 | telnet.sendline('persist\n') 145 | telnet.expect(r'.*' + STANDARD_PROMPT) 146 | return JsonResponse({'morouter': self.get_router(telnet, order)}) 147 | 148 | def simple_morouter_action(self, telnet, action, order, return_moroute=True): 149 | telnet.sendline('morouter -%s %s' % (action, order)) 150 | matched_index = telnet.expect([ 151 | r'.+Successfully(.+)' + STANDARD_PROMPT, 152 | r'.+Unknown MO Route: (.+)' + STANDARD_PROMPT, 153 | r'.+(.*)' + STANDARD_PROMPT, 154 | ]) 155 | if matched_index == 0: 156 | telnet.sendline('persist\n') 157 | if return_moroute: 158 | telnet.expect(r'.*' + STANDARD_PROMPT) 159 | return JsonResponse({'morouter': self.get_router(telnet, fid)}) 160 | else: 161 | return JsonResponse({'order': order}) 162 | elif matched_index == 1: 163 | raise UnknownError(detail='No router:' + order) 164 | else: 165 | raise JasminError(telnet.match.group(1)) 166 | 167 | def destroy(self, request, order): 168 | """Delete a morouter. One parameter required, the router identifier (a string) 169 | 170 | HTTP codes indicate result as follows 171 | 172 | - 200: successful deletion 173 | - 404: nonexistent router 174 | - 400: other error 175 | """ 176 | return self.simple_morouter_action( 177 | request.telnet, 'r', order, return_moroute=False) 178 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/mtrouter.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.conf import settings 4 | from django.http import JsonResponse 5 | from django.utils.datastructures import MultiValueDictKeyError 6 | 7 | from rest_framework.viewsets import ViewSet 8 | from rest_framework.decorators import list_route 9 | 10 | from rest_api.tools import set_ikeys, split_cols 11 | from rest_api.exceptions import (JasminSyntaxError, JasminError, 12 | UnknownError, MissingKeyError, 13 | MutipleValuesRequiredKeyError, ObjectNotFoundError) 14 | 15 | STANDARD_PROMPT = settings.STANDARD_PROMPT 16 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 17 | 18 | class MTRouterViewSet(ViewSet): 19 | "Viewset for managing MT Routes" 20 | lookup_field = 'order' 21 | 22 | def _list(self, telnet): 23 | "List MT router as python dict" 24 | telnet.sendline('mtrouter -l') 25 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 26 | result = telnet.match.group(0).strip().replace("\r", '').split("\n") 27 | if len(result) < 3: 28 | return {'mtrouters': []} 29 | results = [l.replace(', ', ',').replace('(!)', '') 30 | for l in result[2:-2] if l] 31 | routers = split_cols(results) 32 | return { 33 | 'mtrouters': 34 | [ 35 | { 36 | 'order': r[0].strip().lstrip('#'), 37 | 'type': r[1], 38 | 'rate': r[2], 39 | 'connectors': [c.strip() for c in r[3].split(',')], 40 | 'filters': [c.strip() for c in ' '.join(r[4:]).split(',') 41 | ] if len(r) > 3 else [] 42 | } for r in routers 43 | ] 44 | } 45 | 46 | def list(self, request): 47 | "List MT Routers. No parameters" 48 | return JsonResponse(self._list(request.telnet)) 49 | 50 | def get_router(self, telnet, order): 51 | "Return data for one mtrouter as Python dict" 52 | routers = self._list(telnet)['mtrouters'] 53 | try: 54 | return {'mtrouter': 55 | next((m for m in routers if m['order'] == order), None) 56 | } 57 | except StopIteration: 58 | raise ObjectNotFoundError('No MTRouter with order: %s' % order) 59 | 60 | def retrieve(self, request, order): 61 | "Details for one MTRouter by order (integer)" 62 | return JsonResponse(self.get_router(request.telnet, order)) 63 | 64 | 65 | @list_route(methods=['delete']) 66 | def flush(self, request): 67 | "Flush entire routing table" 68 | telnet = request.telnet 69 | telnet.sendline('mtrouter -f') 70 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 71 | telnet.sendline('persist\n') 72 | telnet.expect(r'.*' + STANDARD_PROMPT) 73 | return JsonResponse({'mtrouters': []}) 74 | 75 | def create(self, request): 76 | """Create MTRouter. 77 | Required parameters: type, order, smppconnectors, httpconnectors 78 | More than one connector is allowed only for RandomRoundrobinMTRoute 79 | --- 80 | # YAML 81 | omit_serializer: true 82 | parameters: 83 | - name: type 84 | description: One of DefaultRoute, StaticMTRoute, RandomRoundrobinMTRoute 85 | required: true 86 | type: string 87 | paramType: form 88 | - name: order 89 | description: Router order, also used to identify router 90 | required: true 91 | type: string 92 | paramType: form 93 | - name: rate 94 | description: Router rate, may be zero for free 95 | required: true 96 | type: float 97 | paramType: form 98 | - name: smppconnectors 99 | description: List of SMPP connector ids. 100 | required: false 101 | type: array 102 | paramType: form 103 | - name: httpconnectors 104 | description: List of HTTP connector ids. 105 | required: false 106 | type: array 107 | paramType: form 108 | - name: filters 109 | description: List of filters, required except for DefaultRoute 110 | required: false 111 | type: array 112 | paramType: form 113 | """ 114 | telnet = request.telnet 115 | data = request.data 116 | try: 117 | rtype, order, rate = data['type'], data['order'], data['rate'] 118 | except IndexError: 119 | raise MissingKeyError( 120 | 'Missing parameter: type or order required') 121 | rtype = rtype.lower() 122 | telnet.sendline('mtrouter -a') 123 | telnet.expect(r'Adding a new MT Route(.+)\n' + INTERACTIVE_PROMPT) 124 | ikeys = OrderedDict({'type': rtype}) 125 | if rtype != 'defaultroute': 126 | try: 127 | filters = data['filters'].split(',') 128 | except MultiValueDictKeyError: 129 | raise MissingKeyError('%s router requires filters' % rtype) 130 | ikeys['filters'] = ';'.join(filters) 131 | ikeys['order'] = order 132 | smppconnectors = data.get('smppconnectors', '') 133 | httpconnectors = data.get('httpconnectors', '') 134 | connectors = ['smppc(%s)' % c.strip() 135 | for c in smppconnectors.split(',') if c.strip() 136 | ] + ['http(%s)' % c for c in httpconnectors.split(',') if c.strip()] 137 | if rtype == 'randomroundrobinmtroute': 138 | if len(connectors) < 2: 139 | raise MutipleValuesRequiredKeyError( 140 | 'Round Robin route requires at least two connectors') 141 | ikeys['connectors'] = ';'.join(connectors) 142 | else: 143 | if len(connectors) != 1: 144 | raise MissingKeyError('one and only one connector required') 145 | ikeys['connector'] = connectors[0] 146 | ikeys['rate'] = rate 147 | print ikeys 148 | set_ikeys(telnet, ikeys) 149 | telnet.sendline('persist\n') 150 | telnet.expect(r'.*' + STANDARD_PROMPT) 151 | return JsonResponse({'mtrouter': self.get_router(telnet, order)}) 152 | 153 | def simple_mtrouter_action(self, telnet, action, order, return_mtroute=True): 154 | telnet.sendline('mtrouter -%s %s' % (action, order)) 155 | matched_index = telnet.expect([ 156 | r'.+Successfully(.+)' + STANDARD_PROMPT, 157 | r'.+Unknown MT Route: (.+)' + STANDARD_PROMPT, 158 | r'.+(.*)' + STANDARD_PROMPT, 159 | ]) 160 | if matched_index == 0: 161 | telnet.sendline('persist\n') 162 | if return_mtroute: 163 | telnet.expect(r'.*' + STANDARD_PROMPT) 164 | return JsonResponse({'mtrouter': self.get_router(telnet, fid)}) 165 | else: 166 | return JsonResponse({'order': order}) 167 | elif matched_index == 1: 168 | raise UnknownError(detail='No router:' + order) 169 | else: 170 | raise JasminError(telnet.match.group(1)) 171 | 172 | def destroy(self, request, order): 173 | """Delete a mtrouter. One parameter required, the router identifier (a string) 174 | 175 | HTTP codes indicate result as follows 176 | 177 | - 200: successful deletion 178 | - 404: nonexistent router 179 | - 400: other error 180 | """ 181 | return self.simple_mtrouter_action( 182 | request.telnet, 'r', order, return_mtroute=False) 183 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/smppccm.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import JsonResponse 3 | 4 | from rest_framework.viewsets import ViewSet 5 | from rest_framework.parsers import JSONParser 6 | from rest_framework.decorators import detail_route, parser_classes 7 | from rest_api.tools import set_ikeys, split_cols 8 | from rest_api.exceptions import ( 9 | JasminSyntaxError, JasminError, ActionFailed, 10 | ObjectNotFoundError, UnknownError, 11 | ) 12 | 13 | STANDARD_PROMPT = settings.STANDARD_PROMPT 14 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 15 | 16 | 17 | class SMPPCCMViewSet(ViewSet): 18 | "Viewset for managing SMPP Client Connectors" 19 | lookup_field = 'cid' 20 | 21 | def get_smppccm(self, telnet, cid, silent=False): 22 | #Some of this could be abstracted out - similar pattern in users.py 23 | telnet.sendline('smppccm -s ' + cid) 24 | matched_index = telnet.expect([ 25 | r'.+Unknown connector:.*' + STANDARD_PROMPT, 26 | r'.+Usage:.*' + STANDARD_PROMPT, 27 | r'(.+)\n' + STANDARD_PROMPT, 28 | ]) 29 | if matched_index != 2: 30 | if silent: 31 | return 32 | else: 33 | raise ObjectNotFoundError('Unknown connector: %s' % cid) 34 | result = telnet.match.group(1) 35 | smppccm = {} 36 | for line in result.splitlines(): 37 | d = [x for x in line.split() if x] 38 | if len(d) == 2: 39 | smppccm[d[0]] = d[1] 40 | return smppccm 41 | 42 | def get_connector_list(self, telnet): 43 | telnet.sendline('smppccm -l') 44 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 45 | result = telnet.match.group(0).strip().replace("\r", '').split("\n") 46 | if len(result) < 3: 47 | return [] 48 | return split_cols(result[2:-2]) 49 | 50 | def simple_smppccm_action(self, telnet, action, cid): 51 | telnet.sendline('smppccm -%s %s' % (action, cid)) 52 | matched_index = telnet.expect([ 53 | r'.+Successfully(.+)' + STANDARD_PROMPT, 54 | r'.+Unknown connector: (.+)' + STANDARD_PROMPT, 55 | r'(.*)' + STANDARD_PROMPT, 56 | ]) 57 | if matched_index == 0: 58 | telnet.sendline('persist\n') 59 | return JsonResponse({'name': cid}) 60 | elif matched_index == 1: 61 | raise ObjectNotFoundError('Unknown SMPP Connector: %s' % cid) 62 | else: 63 | raise ActionFailed(telnet.match.group(1)) 64 | 65 | def list(self, request): 66 | """List SMPP Client Connectors. No parameters 67 | Differs from slightly from telent CLI names and values: 68 | 69 | 1. the "service" column is called "status" 70 | 2. the cid is the full connector id of the form smpps(cid) 71 | """ 72 | telnet = request.telnet 73 | connector_list = self.get_connector_list(telnet) 74 | connectors = [] 75 | for raw_data in connector_list: 76 | if raw_data[0][0] == '#': 77 | cid = raw_data[0][1:] 78 | connector = self.get_smppccm(telnet, cid, True) 79 | connector.update( 80 | cid=cid, 81 | status=raw_data[1], 82 | session=raw_data[2], 83 | starts=raw_data[3], 84 | stops=raw_data[4] 85 | ) 86 | connectors.append(connector) 87 | return JsonResponse({'connectors': connectors}) 88 | 89 | def retrieve(self, request, cid): 90 | """Retreive data for one connector 91 | Required parameter: cid (connector id)""" 92 | telnet = request.telnet 93 | connector = self.get_smppccm(telnet, cid, silent=False) 94 | connector_list = self.get_connector_list(telnet) 95 | list_data = next( 96 | (raw_data for raw_data in connector_list if 97 | raw_data[0] == '#' + cid), 98 | None 99 | ) 100 | connector.update( 101 | cid=cid, 102 | status=list_data[1], 103 | session=list_data[2], 104 | starts=list_data[3], 105 | stops=list_data[4] 106 | ) 107 | return JsonResponse({'connector': connector}) 108 | 109 | def create(self, request): 110 | """Create an SMPP Client Connector. 111 | Required parameter: cid (connector id) 112 | --- 113 | # YAML 114 | omit_serializer: true 115 | parameters: 116 | - name: cid 117 | description: Connector identifier 118 | required: true 119 | type: string 120 | paramType: form 121 | """ 122 | telnet = request.telnet 123 | 124 | telnet.sendline('smppccm -a') 125 | updates = request.data 126 | for k, v in updates.items(): 127 | if not ((type(updates) is dict) and (len(updates) >= 1)): 128 | raise JasminSyntaxError('updates should be a a key value array') 129 | telnet.sendline("%s %s" % (k, v)) 130 | matched_index = telnet.expect([ 131 | r'.*(Unknown SMPPClientConfig key:.*)' + INTERACTIVE_PROMPT, 132 | r'.*(Error:.*)' + STANDARD_PROMPT, 133 | r'.*' + INTERACTIVE_PROMPT, 134 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 135 | ]) 136 | if matched_index != 2: 137 | raise JasminSyntaxError( 138 | detail=" ".join(telnet.match.group(1).split())) 139 | telnet.sendline('ok') 140 | telnet.sendline('persist\n') 141 | telnet.expect(r'.*' + STANDARD_PROMPT) 142 | return JsonResponse({'cid': request.data['cid']}) 143 | 144 | def destroy(self, request, cid): 145 | """Delete an smpp connector. 146 | One parameter required, the connector identifier 147 | 148 | HTTP codes indicate result as follows 149 | 150 | - 200: successful deletion 151 | - 404: nonexistent group 152 | - 400: other error 153 | """ 154 | return self.simple_smppccm_action(request.telnet, 'r', cid) 155 | 156 | @parser_classes((JSONParser,)) 157 | def partial_update(self, request, cid): 158 | """Update some SMPP connector attributes 159 | 160 | JSON requests only. The updates parameter is a key value array 161 | --- 162 | # YAML 163 | omit_serializer: true 164 | parameters: 165 | - name: updates 166 | description: Items to update 167 | required: true 168 | type: array 169 | paramType: body 170 | """ 171 | telnet = request.telnet 172 | telnet.sendline('smppccm -u ' + cid) 173 | matched_index = telnet.expect([ 174 | r'.*Updating connector(.*)' + INTERACTIVE_PROMPT, 175 | r'.*Unknown connector: (.*)' + STANDARD_PROMPT, 176 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 177 | ]) 178 | if matched_index == 1: 179 | raise UnknownError(detail='Unknown connector:' + cid) 180 | if matched_index != 0: 181 | raise JasminError(detail=" ".join(telnet.match.group(0).split())) 182 | updates = request.data 183 | for k, v in updates.items(): 184 | if not ((type(updates) is dict) and (len(updates) >= 1)): 185 | raise JasminSyntaxError('updates should be a a key value array') 186 | telnet.sendline("%s %s" % (k, v)) 187 | matched_index = telnet.expect([ 188 | r'.*(Unknown SMPPClientConfig key:.*)' + INTERACTIVE_PROMPT, 189 | r'.*(Error:.*)' + STANDARD_PROMPT, 190 | r'.*' + INTERACTIVE_PROMPT, 191 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 192 | ]) 193 | if matched_index != 2: 194 | raise JasminSyntaxError( 195 | detail=" ".join(telnet.match.group(1).split())) 196 | telnet.sendline('ok') 197 | ok_index = telnet.expect([ 198 | r'.*(Error:.*)' + STANDARD_PROMPT, 199 | r'(.*)' + INTERACTIVE_PROMPT, 200 | r'.*' + STANDARD_PROMPT, 201 | ]) 202 | if ok_index == 0: 203 | raise JasminSyntaxError( 204 | detail=" ".join(telnet.match.group(1).split())) 205 | telnet.sendline('persist\n') 206 | #Not sure why this needs to be repeated, just as with user 207 | telnet.expect(r'.*' + STANDARD_PROMPT) 208 | 209 | return JsonResponse( 210 | {'connector': self.get_smppccm(telnet, cid, silent=False)}) 211 | 212 | @detail_route(methods=['put']) 213 | def start(self, request, cid): 214 | """Start SMPP Connector 215 | 216 | One parameter required, the connector identifier 217 | 218 | HTTP codes indicate result as follows 219 | 220 | - 200: successful start 221 | - 404: nonexistent connector 222 | - 400: other error - this includes failure to start because it is started. 223 | """ 224 | return self.simple_smppccm_action(request.telnet, '1', cid) 225 | 226 | @detail_route(methods=['put']) 227 | def stop(self, request, cid): 228 | """Start SMPP Connector 229 | 230 | One parameter required, the connector identifier 231 | 232 | HTTP codes indicate result as follows 233 | 234 | - 200: successful start 235 | - 404: nonexistent connector 236 | - 400: other error - this includes failure to stop because it is stopped. 237 | """ 238 | return self.simple_smppccm_action(request.telnet, '0', cid) 239 | -------------------------------------------------------------------------------- /jasmin_api/rest_api/views/users.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import JsonResponse 3 | 4 | from rest_framework.viewsets import ViewSet 5 | from rest_framework.parsers import JSONParser 6 | from rest_framework.decorators import detail_route, parser_classes 7 | 8 | from rest_api.tools import set_ikeys 9 | from rest_api.exceptions import (JasminSyntaxError, JasminError, 10 | UnknownError, MissingKeyError, 11 | ObjectNotFoundError) 12 | 13 | STANDARD_PROMPT = settings.STANDARD_PROMPT 14 | INTERACTIVE_PROMPT = settings.INTERACTIVE_PROMPT 15 | 16 | class UserViewSet(ViewSet): 17 | "ViewSet for managing *Jasmin* users (*not* Django auth users)" 18 | lookup_field = 'uid' 19 | 20 | def get_user(self, telnet, uid, silent=False): 21 | """Gets a single users data 22 | silent supresses Http404 exception if user not found""" 23 | telnet.sendline('user -s ' + uid) 24 | matched_index = telnet.expect([ 25 | r'.+Unknown User:.*' + STANDARD_PROMPT, 26 | r'.+Usage: user.*' + STANDARD_PROMPT, 27 | r'(.+)\n' + STANDARD_PROMPT, 28 | ]) 29 | if matched_index != 2: 30 | if silent: 31 | return 32 | else: 33 | raise ObjectNotFoundError('Unknown user: %s' % uid) 34 | result = telnet.match.group(1) 35 | user = {} 36 | for line in [l for l in result.splitlines() if l][1:]: 37 | d = [x for x in line.split() if x] 38 | if len(d) == 2: 39 | user[d[0]] = d[1] 40 | elif len(d) == 4: 41 | #Not DRY, could be more elegant 42 | if not d[0] in user: 43 | user[d[0]] = {} 44 | if not d[1] in user[d[0]]: 45 | user[d[0]][d[1]] = {} 46 | if not d[2] in user[d[0]][d[1]]: 47 | user[d[0]][d[1]][d[2]] = {} 48 | user[d[0]][d[1]][d[2]] = d[3] 49 | #each line has two or four lines so above exhaustive 50 | return user 51 | 52 | def retrieve(self, request, uid): 53 | "Retrieve data for one user" 54 | return JsonResponse({'user': self.get_user(request.telnet, uid)}) 55 | 56 | def list(self, request): 57 | "List users. No parameters" 58 | telnet = request.telnet 59 | telnet.sendline('user -l') 60 | telnet.expect([r'(.+)\n' + STANDARD_PROMPT]) 61 | result = telnet.match.group(0).strip() 62 | if len(result) < 3: 63 | return JsonResponse({'users': []}) 64 | 65 | results = [l for l in result.splitlines() if l] 66 | annotated_uids = [u.split(None, 1)[0][1:] for u in results[2:-2]] 67 | users = [] 68 | for auid in annotated_uids: 69 | if auid[0] == '!': 70 | udata = self.get_user(telnet, auid[1:], True) 71 | udata['status'] = 'disabled' 72 | else: 73 | udata = self.get_user(telnet, auid, True) 74 | udata['status'] = 'enabled' 75 | users.append(udata) 76 | return JsonResponse( 77 | { 78 | #return users skipping None (== nonexistent user) 79 | 'users': [u for u in users if u] 80 | } 81 | ) 82 | 83 | def create(self, request): 84 | """Create a User. 85 | Required parameters: username, password, uid (user identifier), gid (group identifier), 86 | --- 87 | # YAML 88 | omit_serializer: true 89 | parameters: 90 | - name: uid 91 | description: Username identifier 92 | required: true 93 | type: string 94 | paramType: form 95 | - name: gid 96 | description: Group identifier 97 | required: true 98 | type: string 99 | paramType: form 100 | - name: username 101 | description: Username 102 | required: true 103 | type: string 104 | paramType: form 105 | - name: password 106 | description: Password 107 | required: true 108 | type: string 109 | paramType: form 110 | """ 111 | telnet = request.telnet 112 | data = request.data 113 | try: 114 | uid, gid, username, password = \ 115 | data['uid'], data['gid'], data['username'], data['password'] 116 | except Exception: 117 | raise MissingKeyError('Missing parameter: uid, gid, username and/or password required') 118 | telnet.sendline('user -a') 119 | telnet.expect(r'Adding a new User(.+)\n' + INTERACTIVE_PROMPT) 120 | set_ikeys( 121 | telnet, 122 | { 123 | 'uid': uid, 'gid': gid, 'username': username, 124 | 'password': password 125 | } 126 | ) 127 | telnet.sendline('persist\n') 128 | telnet.expect(r'.*' + STANDARD_PROMPT) 129 | return JsonResponse({'user': self.get_user(telnet, uid)}) 130 | 131 | @parser_classes((JSONParser,)) 132 | def partial_update(self, request, uid): 133 | """Update some user attributes 134 | 135 | JSON requests only. The updates parameter is a list of lists. 136 | Each list is a list of valid arguments to user update. For example: 137 | 138 | * ["gid", "mygroup"] will set the user's group to mygroup 139 | * ["mt_messaging_cred", "authorization", "smpps_send", "False"] 140 | will remove the user privilege to send SMSs through the SMPP API. 141 | --- 142 | # YAML 143 | omit_serializer: true 144 | parameters: 145 | - name: updates 146 | description: Items to update 147 | required: true 148 | type: array 149 | paramType: body 150 | """ 151 | telnet = request.telnet 152 | telnet.sendline('user -u ' + uid) 153 | matched_index = telnet.expect([ 154 | r'.*Updating User(.*)' + INTERACTIVE_PROMPT, 155 | r'.*Unknown User: (.*)' + STANDARD_PROMPT, 156 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 157 | ]) 158 | if matched_index == 1: 159 | raise UnknownError(detail='Unknown user:' + uid) 160 | if matched_index != 0: 161 | raise JasminError(detail=" ".join(telnet.match.group(0).split())) 162 | updates = request.data 163 | if not ((type(updates) is list) and (len(updates) >= 1)): 164 | raise JasminSyntaxError('updates should be a list') 165 | for update in updates: 166 | if not ((type(update) is list) and (len(update) >= 1)): 167 | raise JasminSyntaxError("Not a list: %s" % update) 168 | telnet.sendline(" ".join([x for x in update])) 169 | matched_index = telnet.expect([ 170 | r'.*(Unknown User key:.*)' + INTERACTIVE_PROMPT, 171 | r'.*(Error:.*)' + STANDARD_PROMPT, 172 | r'.*' + INTERACTIVE_PROMPT, 173 | r'.+(.*)(' + INTERACTIVE_PROMPT + '|' + STANDARD_PROMPT + ')', 174 | ]) 175 | if matched_index != 2: 176 | raise JasminSyntaxError( 177 | detail=" ".join(telnet.match.group(1).split())) 178 | telnet.sendline('ok') 179 | ok_index = telnet.expect([ 180 | r'(.*)' + INTERACTIVE_PROMPT, 181 | r'.*' + STANDARD_PROMPT, 182 | ]) 183 | if ok_index == 0: 184 | raise JasminSyntaxError( 185 | detail=" ".join(telnet.match.group(1).split())) 186 | telnet.sendline('persist\n') 187 | #Not sure why this needs to be repeated 188 | telnet.expect(r'.*' + STANDARD_PROMPT) 189 | return JsonResponse({'user': self.get_user(telnet, uid)}) 190 | 191 | def simple_user_action(self, telnet, action, uid, return_user=True): 192 | telnet.sendline('user -%s %s' % (action, uid)) 193 | matched_index = telnet.expect([ 194 | r'.+Successfully(.+)' + STANDARD_PROMPT, 195 | r'.+Unknown User: (.+)' + STANDARD_PROMPT, 196 | r'.+(.*)' + STANDARD_PROMPT, 197 | ]) 198 | if matched_index == 0: 199 | telnet.sendline('persist\n') 200 | if return_user: 201 | telnet.expect(r'.*' + STANDARD_PROMPT) 202 | return JsonResponse({'user': self.get_user(telnet, uid)}) 203 | else: 204 | return JsonResponse({'uid': uid}) 205 | elif matched_index == 1: 206 | raise UnknownError(detail='No user:' + uid) 207 | else: 208 | raise JasminError(telnet.match.group(1)) 209 | 210 | def destroy(self, request, uid): 211 | """Delete a user. One parameter required, the user identifier (a string) 212 | 213 | HTTP codes indicate result as follows 214 | 215 | - 200: successful deletion 216 | - 404: nonexistent user 217 | - 400: other error 218 | """ 219 | return self.simple_user_action( 220 | request.telnet, 'r', uid, return_user=False) 221 | 222 | @detail_route(methods=['put']) 223 | def enable(self, request, uid): 224 | """Enable a user. One parameter required, the user identifier (a string) 225 | 226 | HTTP codes indicate result as follows 227 | 228 | - 200: successful deletion 229 | - 404: nonexistent user 230 | - 400: other error 231 | """ 232 | return self.simple_user_action(request.telnet, 'e', uid) 233 | 234 | @detail_route(methods=['put']) 235 | def disable(self, request, uid): 236 | """Disable a user. 237 | 238 | One parameter required, the user identifier (a string) 239 | 240 | HTTP codes indicate result as follows 241 | 242 | - 200: successful deletion 243 | - 404: nonexistent user 244 | - 400: other error 245 | """ 246 | return self.simple_user_action(request.telnet, 'd', uid) 247 | 248 | @detail_route(methods=['put']) 249 | def smpp_unbind(self, request, uid): 250 | """Unbind user from smpp server 251 | 252 | One parameter required, the user identifier (a string) 253 | 254 | HTTP codes indicate result as follows 255 | 256 | - 200: successful unbind 257 | - 404: nonexistent user 258 | - 400: other error 259 | """ 260 | return self.simple_user_action(request.telnet, '-smpp-unbind', uid) 261 | 262 | @detail_route(methods=['put']) 263 | def smpp_ban(self, request, uid): 264 | """Unbind and ban user from smpp server 265 | 266 | One parameter required, the user identifier (a string) 267 | 268 | HTTP codes indicate result as follows 269 | 270 | - 200: successful ban and unbind 271 | - 404: nonexistent user 272 | - 400: other error 273 | """ 274 | return self.simple_user_action(request.telnet, '-smpp-ban', uid) 275 | -------------------------------------------------------------------------------- /jasmin_api/run_cherrypy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from jasmin_api.wsgi import application 4 | 5 | from django.conf import settings 6 | 7 | import cherrypy 8 | 9 | cherrypy.tree.graft(application, "/") 10 | 11 | cherrypy.tree.mount(None, settings.STATIC_URL, {'/': { 12 | 'tools.staticdir.on': True, 13 | 'tools.staticdir.dir': settings.STATIC_ROOT, 14 | 'tools.expires.on': True, 15 | 'tools.expires.secs': 86400 16 | } 17 | }) 18 | 19 | server = cherrypy._cpserver.Server() 20 | 21 | server.socket_host = "0.0.0.0" 22 | server.socket_port = 8000 23 | server.thread_pool = 10 24 | 25 | server.subscribe() 26 | 27 | cherrypy.engine.start() 28 | cherrypy.engine.block() 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.8,<=1.8.99 2 | djangorestframework==3.3.2 3 | django-rest-swagger==0.3.4 4 | Markdown==2 5 | CherryPy>=4.0.0 6 | pexpect>=4.0 7 | --------------------------------------------------------------------------------