├── .gitignore ├── LICENSE ├── README.md ├── acs ├── __init__.py ├── admin.py ├── apps.py ├── conf.py ├── default_acs_parametermap.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── acs_xmpp_bot.py │ │ └── acs_xmpp_connreq_worker.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20190218_2224.py │ ├── 0003_auto_20190220_1202.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── acs_basemodel.py │ ├── acs_device.py │ ├── acs_device_basemodel.py │ ├── acs_device_category.py │ ├── acs_device_model.py │ ├── acs_device_vendor.py │ ├── acs_http_basemodel.py │ ├── acs_http_request.py │ ├── acs_http_response.py │ ├── acs_queue_job.py │ ├── acs_session.py │ └── cwmp_data_model.py ├── response.py ├── urls.py ├── utils.py ├── views.py └── workers │ ├── __init__.py │ └── acs_http_connreq_worker.py ├── acsadmin ├── __init__.py ├── apps.py ├── forms.py ├── templates │ ├── acs_device_category_detail.html │ ├── acs_device_category_list.html │ ├── acs_device_detail.html │ ├── acs_device_list.html │ ├── acs_device_model_detail.html │ ├── acs_device_model_list.html │ ├── acs_device_vendor_detail.html │ ├── acs_device_vendor_list.html │ ├── acs_http_request_detail.html │ ├── acs_http_request_list.html │ ├── acs_http_response_detail.html │ ├── acs_http_response_list.html │ ├── acs_queue_job_create.html │ ├── acs_queue_job_detail.html │ ├── acs_queue_job_list.html │ ├── acs_session_detail.html │ ├── acs_session_list.html │ └── includes │ │ ├── acs_device_category_detail_panel.html │ │ ├── acs_device_category_list_panel.html │ │ ├── acs_device_detail_panel.html │ │ ├── acs_device_list_panel.html │ │ ├── acs_device_model_detail_panel.html │ │ ├── acs_device_model_list_panel.html │ │ ├── acs_device_parameters_panel.html │ │ ├── acs_device_vendor_detail_panel.html │ │ ├── acs_device_vendor_list_panel.html │ │ ├── acs_http_conversation_panel.html │ │ ├── acs_http_request_detail_panel.html │ │ ├── acs_http_request_list_panel.html │ │ ├── acs_http_response_detail_panel.html │ │ ├── acs_http_response_list_panel.html │ │ ├── acs_queue_job_detail_panel.html │ │ ├── acs_queue_job_list_panel.html │ │ ├── acs_session_detail_panel.html │ │ └── acs_session_list_panel.html ├── templatetags │ ├── __init__.py │ └── custom_template_filters.py ├── urls.py ├── utils.py └── views.py ├── manifest.in └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | *.swp 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Thomas Steen Rasmussen 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-acs 2 | Django ACS Server 3 | 4 | NOTE: THIS IS VERY MUCH A WORK IN PROGRESS. EXPECT NOTHING TO WORK UNTIL I GET SOME MORE STUFF DONE. 5 | 6 | 7 | ## XML Storage Model 8 | django-acs saves the full history of the ACS sessions. For this purpose it needs a model for XML storage. You need to create this model, and a manager. 9 | 10 | The XML Storage Model must have a property called "document" which returns the XML as a string. It must also have a manager with a .create_xml_document() method. This method should accept a single argument named xml, which is the bytes representing the XML, and it must return an instance of the configured XML Storage Model. 11 | 12 | For example: 13 | 14 | class XMLDocumentManager(models.Manager): 15 | """ 16 | A custom manager with the required create_xml_document() method 17 | """ 18 | def create_xml_document(self, xml): 19 | f = io.BytesIO() 20 | f.write(xml) 21 | xmlfile = File(f) 22 | xmldoc = self.model.objects.create() 23 | xmldoc.file_document.save("%s.xml" % str(xmldoc.uuid), xmlfile) 24 | return xmldoc 25 | 26 | class XMLDocument(models.Model): 27 | """ 28 | The XMLDocument model saves XML files to disk and keeps only a reference to the filename in the database. 29 | """ 30 | objects = XMLDocumentManager() 31 | xml_document = models.FileField( 32 | upload_to='xmlarchive/', 33 | null=True, 34 | blank=True, 35 | help_text='The XML Document FileField' 36 | ) 37 | 38 | @cached_property 39 | def document(self): 40 | if self.file_document: 41 | try: 42 | return self.xml_document.read().decode('utf-8') 43 | except Exception as E: 44 | logger.info("got exception while reading XML file %s: %s" % (self.xml_document.path, E)) 45 | # no file or error reading 46 | return None 47 | 48 | 49 | You can start out with just a TextField to save the XML, but for any type of large scale operation you should use the filesystem or an object store which can handle millions of files. 50 | 51 | 52 | Finally configure django-acs to use the class like so: 53 | 54 | DJANGO_ACS={ 55 | 'xml_storage_model': 'xmlarchive.XMLDocument' 56 | } 57 | 58 | 59 | # Settings 60 | An alphabetical list of all supported settings. 61 | 62 | ## inform_interval (optional) 63 | The inform interval to configure for ACS devices, in seconds. 64 | 65 | Default: 3600 66 | 67 | ## inform_limit_per_interval (optional) 68 | The maximum number of Informs (ACS sessions really) we allow a single ACS device to do in the span of a single informinterval. Any more sessions will be rejected with HTTP 420. 69 | 70 | Default: 2 71 | 72 | ## xml_storage_model (required) 73 | Sets the model used to store XML documents. See the section XML Storage Model above. 74 | 75 | No default. 76 | 77 | -------------------------------------------------------------------------------- /acs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acs/__init__.py -------------------------------------------------------------------------------- /acs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from simple_history.admin import SimpleHistoryAdmin 3 | from .models import * 4 | 5 | class SearchSimpleHistoryAdmin(SimpleHistoryAdmin): 6 | search_fields = ['id'] 7 | ordering = ['id'] 8 | 9 | admin.site.register(AcsSession, SearchSimpleHistoryAdmin) 10 | admin.site.register(AcsDeviceCategory, SearchSimpleHistoryAdmin) 11 | admin.site.register(AcsDeviceVendor, SearchSimpleHistoryAdmin) 12 | admin.site.register(AcsDeviceModel, SearchSimpleHistoryAdmin) 13 | admin.site.register(AcsDevice, SearchSimpleHistoryAdmin) 14 | admin.site.register(CwmpDataModel) 15 | 16 | @admin.register(AcsHttpRequest) 17 | class AcsHttpRequestAdmin(SimpleHistoryAdmin): 18 | readonly_fields = ('acs_session', 'rpc_response_to', 'fk_body') 19 | 20 | 21 | @admin.register(AcsHttpResponse) 22 | class AcsHttpResponseAdmin(SimpleHistoryAdmin): 23 | readonly_fields = ('http_request', 'rpc_response_to', 'fk_body') 24 | 25 | @admin.register(AcsQueueJob) 26 | class AcsQueueJobAdmin(SimpleHistoryAdmin): 27 | readonly_fields = ('acs_device', 'handled_in') 28 | 29 | -------------------------------------------------------------------------------- /acs/apps.py: -------------------------------------------------------------------------------- 1 | 2 | from django.apps import AppConfig 3 | from django.db.models.signals import post_save 4 | 5 | 6 | class AcsConfig(AppConfig): 7 | name = 'acs' 8 | 9 | -------------------------------------------------------------------------------- /acs/conf.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | logger = logging.getLogger('django_acs.%s' % __name__) 7 | 8 | 9 | class AcsSettingsBuilder: 10 | """ 11 | All django-acs settings are used through this module. 12 | 13 | Usage: 14 | 15 | from django_acs.conf import acs_settings 16 | print(acs_settings.INFORM_INTERVAL) 17 | """ 18 | def __init__(self, *args, **kwargs): 19 | """ 20 | Populate our acs_settings module 21 | """ 22 | if not hasattr(settings, "DJANGO_ACS"): 23 | raise ImproperlyConfigured("DJANGO_ACS not found in settings.py") 24 | 25 | # First get all settings from the normal django settings.py 26 | for setting in settings.DJANGO_ACS: 27 | setattr(self, setting, settings.DJANGO_ACS[setting]) 28 | 29 | # Then get whatever we are missing from default_acs_settings 30 | for setting in dir(AcsDefaultSettings): 31 | if setting not in dir(self): 32 | setattr(self, setting, getattr(AcsDefaultSettings, setting)) 33 | 34 | # finally check if all the required settings are there 35 | if not hasattr(self, 'XML_STORAGE_MODEL'): 36 | raise ImproperlyConfigured("settings.DJANGO_ACS['XML_STORAGE_MODEL'] was not found. It is a required setting for django-acs. Bailing out.") 37 | 38 | 39 | class AcsDefaultSettings: 40 | """ 41 | Default settings for django-acs. Do not change anything here, 42 | set eg. DJANGO_ACS['INFORM_INTERVAL'] in settings.py instead. 43 | """ 44 | INFORM_LIMIT_PER_INTERVAL = 2 45 | INFORM_INTERVAL = 3600 46 | 47 | # supported CWMP namespaces 48 | CWMP_NAMESPACES = [ 49 | 'urn:dslforum-org:cwmp-1-0', 50 | 'urn:dslforum-org:cwmp-1-1', 51 | 'urn:dslforum-org:cwmp-1-2', 52 | 'urn:dslforum-org:cwmp-1-3', 53 | 'urn:dslforum-org:cwmp-1-4', 54 | ] 55 | 56 | # SOAP namespaces used in django-acs 57 | SOAP_NAMESPACES = { 58 | 'soap-env': 'http://schemas.xmlsoap.org/soap/envelope/', 59 | 'soap-enc': 'http://schemas.xmlsoap.org/soap/encoding/', 60 | 'xsd': 'http://www.w3.org/2001/XMLSchema', 61 | 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', 62 | } 63 | 64 | 65 | # initiate our settings object 66 | acs_settings = AcsSettingsBuilder() 67 | 68 | -------------------------------------------------------------------------------- /acs/default_acs_parametermap.py: -------------------------------------------------------------------------------- 1 | # This dict contains the default mapping between the internal django-acs representation of attributes, 2 | # and the actual name of the parameter in the xml tree. 3 | 4 | default_acs_device_parametermap = { 5 | # acs server stuff 6 | "django_acs.acs.informinterval": "ManagementServer.PeriodicInformInterval", 7 | "django_acs.acs.acs_managed_upgrades": "ManagementServer.UpgradesManaged", 8 | "django_acs.acs.connection_request_user": "ManagementServer.ConnectionRequestUsername", 9 | "django_acs.acs.connection_request_password": "ManagementServer.ConnectionRequestPassword", 10 | "django_acs.acs.parameterkey": "ManagementServer.ParameterKey", 11 | "django_acs.acs.connrequrl": "ManagementServer.ConnectionRequestURL", 12 | 13 | # acs server xmpp stuff 14 | "django_acs.acs.xmpp_server": "XMPP.Connection.1.Server.1.ServerAddress", 15 | "django_acs.acs.xmpp_server_port": "XMPP.Connection.1.Server.1.Port", 16 | "django_acs.acs.xmpp_connection_enable": "XMPP.Connection.1.Enable", 17 | "django_acs.acs.xmpp_connection_username": "XMPP.Connection.1.Username", 18 | "django_acs.acs.xmpp_connection_password": "XMPP.Connection.1.Password", 19 | "django_acs.acs.xmpp_connection_domain": "XMPP.Connection.1.Domain", 20 | "django_acs.acs.xmpp_connection_usetls": "XMPP.Connection.1.UseTLS", 21 | "django_acs.acs.xmpp_connreq_connection": "ManagementServer.ConnReqXMPPConnection", 22 | 23 | # device info 24 | "django_acs.deviceinfo.softwareversion": "DeviceInfo.SoftwareVersion", 25 | "django_acs.deviceinfo.uptime": "DeviceInfo.UpTime", 26 | 27 | # wifi 2.4g 28 | "django_acs.wifi.bg_ssid": "WiFi.SSID.1.SSID", 29 | "django_acs.wifi.bg_wpapsk": "WiFi.AccessPoint.1.Security.KeyPassphrase", 30 | "django_acs.wifi.bg_autochannel": "WiFi.Radio.1.AutoChannelEnable", 31 | "django_acs.wifi.bg_channel": "WiFi.Radio.1.Channel", 32 | 33 | # wifi 5g 34 | "django_acs.wifi.n_ssid": "WiFi.SSID.5.SSID", 35 | "django_acs.wifi.n_wpapsk": "WiFi.AccessPoint.5.Security.KeyPassphrase", 36 | "django_acs.wifi.n_autochannel": "WiFi.Radio.2.AutoChannelEnable", 37 | "django_acs.wifi.n_channel": "WiFi.Radio.2.Channel", 38 | } 39 | 40 | -------------------------------------------------------------------------------- /acs/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acs/management/__init__.py -------------------------------------------------------------------------------- /acs/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acs/management/commands/__init__.py -------------------------------------------------------------------------------- /acs/management/commands/acs_xmpp_bot.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.conf import settings 3 | from sleekxmpp import ClientXMPP 4 | from sleekxmpp.exceptions import IqError, IqTimeout 5 | import logging 6 | 7 | 8 | class Command(BaseCommand): 9 | args = 'none' 10 | help = 'Connects to XMPP server and sends/receives messages' 11 | 12 | def handle(self, *args, **options): 13 | ### Initiate logging 14 | logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') 15 | 16 | ### create AcsXmppBot instance 17 | xmpp = AcsXmppBot(settings.ACS_XMPP_JABBERID, settings.ACS_XMPP_PASSWORD) 18 | xmpp.connect(address=settings.ACS_XMPP_SERVERTUPLE) 19 | xmpp.process(block=True) 20 | 21 | 22 | class AcsXmppBot(ClientXMPP): 23 | def __init__(self, jid, password): 24 | ClientXMPP.__init__(self, jid, password) 25 | self.add_event_handler("session_start", self.session_start) 26 | self.add_event_handler("message", self.message) 27 | 28 | def session_start(self, event): 29 | self.send_presence() 30 | self.get_roster() 31 | 32 | def message(self, msg): 33 | if msg['type'] in ('chat', 'normal'): 34 | msg.reply("Thanks for sending\n%(body)s" % msg).send() 35 | 36 | -------------------------------------------------------------------------------- /acs/management/commands/acs_xmpp_connreq_worker.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.utils import timezone 3 | from time import sleep 4 | from acs.models import AcsQueueJob 5 | import sys, sleekxmpp 6 | from django.conf import settings 7 | import logging 8 | 9 | logger = logging.getLogger('django_acs.%s' % __name__) 10 | 11 | class Command(BaseCommand): 12 | args = 'none' 13 | help = 'Loop through unprocessed AcsQueueJob entries marked urgent and do an XMPP ConnectionRequest for each' 14 | 15 | def __init__(self): 16 | self.xmpp = sleekxmpp.ClientXMPP(settings.ACS_XMPP_JABBERID, settings.ACS_XMPP_PASSWORD) 17 | self.xmpp.add_event_handler('session_start', self.xmpp_init) 18 | 19 | def xmpp_init(self, event): 20 | self.xmpp.send_presence() 21 | self.xmpp.get_roster() 22 | 23 | def handle(self, *args, **options): 24 | if self.xmpp.connect(): 25 | xmpp.schedule('Run pending XMPP ConnectionRequests', 5, check_for_messages, repeat=True) 26 | logger.info('-----------------------------') 27 | logger.info(str(timezone.localtime(timezone.now()))) 28 | logger.info('Entering main xmpp loop...') 29 | xmpp.process(block=True) 30 | 31 | def check_for_messages(self): 32 | logger.info('-----------------------------') 33 | logger.info(str(timezone.localtime(timezone.now()))) 34 | logger.info('Checking for pending XMPP ConnectionRequests...') 35 | 36 | -------------------------------------------------------------------------------- /acs/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2019-02-01 13:30 2 | 3 | import django.contrib.postgres.fields 4 | import django.contrib.postgres.fields.jsonb 5 | import django.contrib.postgres.fields.ranges 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import uuid 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('xmlarchive', '0001_initial'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='AcsDevice', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 25 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 26 | ('serial', models.CharField(max_length=100)), 27 | ('current_config_level', models.DateTimeField(blank=True, null=True)), 28 | ('desired_config_level', models.DateTimeField(blank=True, null=True)), 29 | ('current_software_version', models.CharField(blank=True, max_length=50)), 30 | ('desired_software_version', models.CharField(blank=True, max_length=50)), 31 | ('acs_xmpp_password', models.CharField(blank=True, max_length=50)), 32 | ('acs_latest_inform', models.DateTimeField(blank=True, null=True)), 33 | ('acs_parameters', models.TextField(blank=True)), 34 | ('acs_parameters_time', models.DateTimeField(blank=True, null=True)), 35 | ('imported', models.BooleanField(default=False)), 36 | ('acs_connectionrequest_password', models.CharField(blank=True, max_length=50)), 37 | ], 38 | ), 39 | migrations.CreateModel( 40 | name='AcsDeviceCategory', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 44 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 45 | ('name', models.CharField(choices=[('UNKNOWN', 'Unknown'), ('WIFI', 'Wifi Device'), ('SETTOPBOX', 'Settop Box')], default='UNKNOWN', max_length=100, unique=True)), 46 | ], 47 | options={ 48 | 'abstract': False, 49 | }, 50 | ), 51 | migrations.CreateModel( 52 | name='AcsDeviceModel', 53 | fields=[ 54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 56 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 57 | ('name', models.CharField(max_length=50)), 58 | ('desired_config_level', models.DateTimeField(blank=True, null=True)), 59 | ('desired_software_version', models.CharField(blank=True, max_length=50)), 60 | ('acs_parameter_map_overrides', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), 61 | ('category', models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='acsdevicemodels', to='acs.AcsDeviceCategory')), 62 | ], 63 | options={ 64 | 'abstract': False, 65 | }, 66 | ), 67 | migrations.CreateModel( 68 | name='AcsDeviceVendor', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 72 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 73 | ('name', models.CharField(max_length=100)), 74 | ('oui', models.CharField(max_length=6)), 75 | ], 76 | options={ 77 | 'abstract': False, 78 | }, 79 | ), 80 | migrations.CreateModel( 81 | name='AcsHttpRequest', 82 | fields=[ 83 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 84 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 85 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 86 | ('cwmp_id', models.CharField(blank=True, max_length=100)), 87 | ('cwmp_rpc_method', models.CharField(blank=True, max_length=100)), 88 | ('request_headers', models.TextField(blank=True)), 89 | ('request_xml_valid', models.BooleanField(default=False)), 90 | ('request_soap_valid', models.BooleanField(default=False)), 91 | ], 92 | options={ 93 | 'ordering': ['-created_date'], 94 | }, 95 | ), 96 | migrations.CreateModel( 97 | name='AcsHttpResponse', 98 | fields=[ 99 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 100 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 101 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 102 | ('cwmp_id', models.CharField(blank=True, max_length=100)), 103 | ('cwmp_rpc_method', models.CharField(blank=True, max_length=100)), 104 | ('fk_body', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='xmlarchive.XMLDocument')), 105 | ('http_request', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='acs_http_response', to='acs.AcsHttpRequest')), 106 | ('rpc_response_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rpc_responses', to='acs.AcsHttpRequest')), 107 | ], 108 | options={ 109 | 'ordering': ['-created_date'], 110 | }, 111 | ), 112 | migrations.CreateModel( 113 | name='AcsQueueJob', 114 | fields=[ 115 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 116 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 117 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 118 | ('cwmp_rpc_object_xml', models.TextField()), 119 | ('reason', models.CharField(max_length=200)), 120 | ('automatic', models.BooleanField(default=False)), 121 | ('urgent', models.BooleanField(default=False)), 122 | ('notification_sent', models.BooleanField(default=False)), 123 | ('processed', models.BooleanField(default=False)), 124 | ('acs_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='acs_queue_jobs', to='acs.AcsDevice')), 125 | ('handled_in', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='queuejobs', to='acs.AcsHttpResponse')), 126 | ], 127 | options={ 128 | 'ordering': ['-created_date'], 129 | }, 130 | ), 131 | migrations.CreateModel( 132 | name='AcsSession', 133 | fields=[ 134 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 135 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 136 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 137 | ('acs_session_id', models.UUIDField(default=uuid.uuid4)), 138 | ('client_ip', models.GenericIPAddressField()), 139 | ('client_ip_verified', models.BooleanField(default=False)), 140 | ('reference', models.CharField(blank=True, default='', max_length=100)), 141 | ('session_result', models.BooleanField(default=False)), 142 | ('latest_rpc_method', models.CharField(blank=True, default='', max_length=100)), 143 | ('session_end', models.DateTimeField(blank=True, null=True)), 144 | ('_device_uptime', django.contrib.postgres.fields.ranges.DateTimeRangeField(blank=True, null=True)), 145 | ('inform_eventcodes', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=list, size=None)), 146 | ('cwmp_namespace', models.CharField(blank=True, default='', max_length=100)), 147 | ('acs_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='acs_sessions', to='acs.AcsDevice')), 148 | ], 149 | options={ 150 | 'ordering': ['-created_date'], 151 | }, 152 | ), 153 | migrations.CreateModel( 154 | name='CwmpDataModel', 155 | fields=[ 156 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 157 | ('created_date', models.DateTimeField(auto_now_add=True, db_index=True)), 158 | ('modified_date', models.DateTimeField(auto_now=True, db_index=True)), 159 | ('name', models.TextField()), 160 | ('xml_filename', models.TextField(blank=True)), 161 | ('html_link', models.TextField(blank=True)), 162 | ], 163 | options={ 164 | 'abstract': False, 165 | }, 166 | ), 167 | migrations.AddField( 168 | model_name='acssession', 169 | name='root_data_model', 170 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='acs_sessions', to='acs.CwmpDataModel'), 171 | ), 172 | migrations.AddField( 173 | model_name='acshttprequest', 174 | name='acs_session', 175 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='acs_http_requests', to='acs.AcsSession'), 176 | ), 177 | migrations.AddField( 178 | model_name='acshttprequest', 179 | name='fk_body', 180 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='xmlarchive.XMLDocument'), 181 | ), 182 | migrations.AddField( 183 | model_name='acshttprequest', 184 | name='rpc_response_to', 185 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rpc_responses', to='acs.AcsHttpResponse'), 186 | ), 187 | migrations.AddField( 188 | model_name='acsdevicemodel', 189 | name='vendor', 190 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='acsdevicemodels', to='acs.AcsDeviceVendor'), 191 | ), 192 | migrations.AddField( 193 | model_name='acsdevice', 194 | name='model', 195 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='acsdevices', to='acs.AcsDeviceModel'), 196 | ), 197 | migrations.AlterUniqueTogether( 198 | name='acssession', 199 | unique_together={('acs_session_id', 'client_ip')}, 200 | ), 201 | migrations.AlterUniqueTogether( 202 | name='acsdevice', 203 | unique_together={('serial', 'model')}, 204 | ), 205 | ] 206 | -------------------------------------------------------------------------------- /acs/migrations/0002_auto_20190218_2224.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-18 21:24 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('acs', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='acshttprequest', 15 | old_name='cwmp_rpc_method', 16 | new_name='soap_element', 17 | ), 18 | migrations.RenameField( 19 | model_name='acshttpresponse', 20 | old_name='cwmp_rpc_method', 21 | new_name='soap_element', 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /acs/migrations/0003_auto_20190220_1202.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-20 11:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('acs', '0002_auto_20190218_2224'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='acsdevice', 15 | name='acs_inform_count', 16 | field=models.PositiveIntegerField(default=0), 17 | ), 18 | migrations.AddField( 19 | model_name='acsdevice', 20 | name='acs_latest_session_result', 21 | field=models.BooleanField(blank=True, default=None, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /acs/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acs/migrations/__init__.py -------------------------------------------------------------------------------- /acs/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .acs_basemodel import AcsBaseModel 2 | from .acs_device_basemodel import AcsDeviceBaseModel 3 | from .acs_device_category import AcsDeviceCategory 4 | from .acs_device_model import AcsDeviceModel 5 | from .acs_device import AcsDevice 6 | from .acs_device_vendor import AcsDeviceVendor 7 | from .acs_http_basemodel import AcsHttpBaseModel 8 | from .acs_http_request import AcsHttpRequest 9 | from .acs_http_response import AcsHttpResponse 10 | from .acs_queue_job import AcsQueueJob 11 | from .acs_session import AcsSession 12 | from .cwmp_data_model import CwmpDataModel 13 | 14 | -------------------------------------------------------------------------------- /acs/models/acs_basemodel.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class AcsBaseModel(models.Model): 4 | created_date = models.DateTimeField(auto_now_add=True, db_index=True) 5 | modified_date = models.DateTimeField(auto_now=True, db_index=True) 6 | 7 | class Meta: 8 | abstract = True 9 | 10 | @property 11 | def tag(self): 12 | return "%s#%s" % (self.__class__.__name__, self.pk) 13 | 14 | -------------------------------------------------------------------------------- /acs/models/acs_device.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | from lxml import etree 3 | import requests 4 | from defusedxml.lxml import fromstring 5 | from os import urandom 6 | from collections import OrderedDict 7 | import logging 8 | from random import choice 9 | 10 | from django.db import models 11 | from django.conf import settings 12 | from django.urls import reverse 13 | from django.utils import timezone 14 | from django.apps import apps 15 | from django.core.exceptions import ObjectDoesNotExist 16 | 17 | from acs.response import get_soap_xml_object 18 | from acs.utils import run_ssh_command, get_value_from_parameterlist 19 | from acs.conf import acs_settings 20 | 21 | logger = logging.getLogger('django_acs.%s' % __name__) 22 | 23 | 24 | class AcsDevice(AcsBaseModel): 25 | model = models.ForeignKey('acs.AcsDeviceModel', related_name="acsdevices", on_delete=models.PROTECT) 26 | serial = models.CharField(max_length=100) 27 | current_config_level = models.DateTimeField(null=True, blank=True) 28 | desired_config_level = models.DateTimeField(null=True, blank=True) 29 | current_software_version = models.CharField(max_length=50, blank=True) 30 | desired_software_version = models.CharField(max_length=50, blank=True) 31 | acs_xmpp_password = models.CharField(max_length=50, blank=True) 32 | acs_latest_inform = models.DateTimeField(null=True, blank=True) 33 | acs_latest_session_result = models.BooleanField(default=None, null=True, blank=True) 34 | acs_inform_count = models.PositiveIntegerField(default=0) 35 | acs_parameters = models.TextField(blank=True) 36 | acs_parameters_time = models.DateTimeField(null=True, blank=True) 37 | imported = models.BooleanField(default=False) 38 | acs_connectionrequest_password = models.CharField(max_length=50, blank=True) 39 | 40 | class Meta: 41 | # make sure we only ever have one device of a given model with a given serial number 42 | unique_together=('serial', 'model') 43 | 44 | def __str__(self): 45 | return str(self.tag) 46 | 47 | def get_absolute_url(self): 48 | return reverse('acs_device_detail', kwargs={'pk': self.pk}) 49 | 50 | @property 51 | def latest_acs_session(self): 52 | try: 53 | return self.acs_sessions.latest('created_date') 54 | except ObjectDoesNotExist: 55 | return False 56 | 57 | def handle_user_config_changes(self): 58 | ''' 59 | Check self.acs_parameters for changes to settings configurable by the user 60 | ''' 61 | # check for device specific changes 62 | #changelist = self.get_related_device().get_user_config_changelist() 63 | changelist = None 64 | if changelist: 65 | # one or more device specific configuration elements has changed, update our end 66 | logger.warning("Wanted to reconfigure acs device %s due to user config changes, disabled waiting for TA approval. Changed items: %s" % (self, changelist)) 67 | #self.get_related_device().handle_user_config_change(changelist) 68 | pass 69 | 70 | def get_desired_config_level(self): 71 | ''' 72 | Return the desired config level (from the device where relevant, otherwise from the device model) 73 | ''' 74 | if self.desired_config_level: 75 | return self.desired_config_level 76 | else: 77 | return self.model.desired_config_level 78 | 79 | def get_desired_software_version(self): 80 | ''' 81 | Return the desired software version (from the device where relevant, otherwise from the device model) 82 | ''' 83 | if self.desired_software_version: 84 | return self.desired_software_version 85 | else: 86 | return self.model.desired_software_version 87 | 88 | def get_software_url(self, version): 89 | ''' 90 | Return the full URL to the specified software version for the model of this acs device. 91 | Must be plaintext http because our 4920 devices https support is nonexistant. 92 | Must be on an alternative port because our 4920 devices do not speak http/1.1 properly. 93 | Hostname hardcoded... not sure why. 94 | ''' 95 | return '%s/%s-%s' % (settings.ACS_DOWNLOAD_URL, self.model.tag.replace('#', '_'), version) 96 | 97 | @property 98 | def acs_xmpp_username(self): 99 | return self.tag.replace("#", "") 100 | 101 | @property 102 | def acs_connectionrequest_username(self): 103 | return self.tag.replace("#", "") 104 | 105 | def create_xmpp_user(self): 106 | if self.acs_xmpp_password: 107 | ### we already have an xmpp password, bail out 108 | return False 109 | 110 | ### generate new xmpp password 111 | password = ''.join(choice(settings.PASSWORD_ALPHABET) for i in range(settings.ACS_XMPP_PASSWORD_LENGTH)) 112 | 113 | ### create xmpp account for this acs device 114 | result = run_ssh_command( 115 | server=settings.ACS_XMPP_SSH_SERVER, 116 | username=settings.ACS_XMPP_SSH_USER, 117 | private_key=settings.ACS_XMPP_SSH_PRIVKEY, 118 | command='register %(user)s %(domain)s %(password)s' % { 119 | 'user': self.tag, 120 | 'domain': settings.ACS_XMPP_DOMAIN, 121 | 'password': password, 122 | } 123 | ) 124 | 125 | if result['result'] and result['exit_status'] == 0: 126 | self.acs_xmpp_password = password 127 | self.save() 128 | return True 129 | else: 130 | ### got exception or non-0 exit code 131 | if 'exception' in result: 132 | logger.error("got exception while creating xmpp user for %s: %s" % (self.tag, result['exception'])) 133 | else: 134 | logger.error("got non-0 exit code while creating xmpp user for %s: %s and the output: %s" % (self.tag, result['exit_status'], result['errorlines'])) 135 | return False 136 | 137 | def create_connreq_password(self): 138 | # generate a random 50 chars connreq password for this acs device 139 | self.acs_connectionrequest_password = ''.join(choice(settings.PASSWORD_ALPHABET) for i in range(50)) 140 | self.save() 141 | 142 | def update_acs_parameters(self, attributes_rpc_response): 143 | """ 144 | This method takes an AcsHttpRequest object with a GetParameterAtrributesResponse inside. It checks if the GetParameterAtrributesResponse is a response to 145 | a GetParameterAtrributes for the root object (like Device.) and if so, checks the acs session for a GetParameterNames RPC request for the whole tree (aka parampath="" and nextlevel=0), 146 | and a GetParameterAttributes RPC request for the whole tree (aka "Device."). It then uses the info in the three RPC responses to build an XML tree and save it in self.acs_parameters 147 | """ 148 | # get the acs_session for convenience 149 | acs_session = attributes_rpc_response.acs_session 150 | starttime = timezone.now() 151 | 152 | logger.debug(attributes_rpc_response) 153 | 154 | # 1) check if this GetParameterAtrributesResponse is a response to a GetParameterAtrributes RPC call for the whole tree... 155 | if attributes_rpc_response.rpc_response_to.soap_body.find('cwmp:GetParameterAttributes', acs_session.soap_namespaces).xpath('.//string[text()="%s."]' % acs_session.root_data_model.root_object) is None: 156 | acs_session.acs_log("GetParameterAttributesResponse seen, but it is not a response to a GetParameterAttributes for '%s.' - not updating acs_device.acs_parameter" % acs_session.root_data_model.root_object) 157 | return False 158 | 159 | ### check if we have the other two required responses in this acs session, so we can build the tree for this device. 160 | ### 2) Did we also complete a GetParameterValues call for the full tree in this acs session? 161 | values_rpc_response = acs_session.get_all_values_rpc_response() 162 | if not values_rpc_response: 163 | acs_session.acs_log("No GetParameterValues for the full tree found in this session, not updating acs_device.acs_parameters") 164 | return False 165 | 166 | ### 3) Did we also complete a GetParameterNames RPC with parampath='' and nextlevel=0 in this acs session? 167 | names_rpc_response = acs_session.get_all_names_rpc_response() 168 | 169 | if not names_rpc_response: 170 | acs_session.acs_log("No GetParameterNames for the full tree found in this session, not updating acs_device.acs_parameters") 171 | return False 172 | 173 | ### OK, ready to update the parameters... 174 | 175 | # get ParameterList from namesrequest and build a dict 176 | names_param_list = names_rpc_response.soap_body.find('cwmp:GetParameterNamesResponse', acs_session.soap_namespaces).find('ParameterList') 177 | writabledict = {} 178 | for child in names_param_list.iterchildren(): 179 | writabledict[child.xpath("./Name")[0].text] = child.xpath("./Writable")[0].text 180 | 181 | # get ParameterList from GetParameterAttributesResponse and build a dict of lists of attributes 182 | attributes_param_list = attributes_rpc_response.soap_body.find('cwmp:GetParameterAttributesResponse', acs_session.soap_namespaces).find('ParameterList') 183 | attributedict = {} 184 | for child in attributes_param_list.iterchildren(): 185 | attribs = [] 186 | name = child.xpath("Name")[0] 187 | for attrib in name.itersiblings(): 188 | attribs.append(attrib) 189 | attributedict[child.xpath("Name")[0].text] = attribs 190 | 191 | root = etree.Element("DjangoAcsParameterCache") 192 | ### loop through all params in valuesrequest 193 | paramcount = 0 194 | for param in values_rpc_response.soap_body.find('cwmp:GetParameterValuesResponse', acs_session.soap_namespaces).find('ParameterList').getchildren(): 195 | paramname = param.xpath("Name")[0].text 196 | paramcount += 1 197 | 198 | writable = etree.Element("Writable") 199 | if paramname in writabledict: 200 | # add writable value if we can find it 201 | writable.text = writabledict[paramname] 202 | param.append(writable) 203 | 204 | # append acl and notification (and any future attributes that may appear) to our tree 205 | for attrib in attributedict[paramname]: 206 | param.append(attrib) 207 | 208 | # append this to the tree 209 | root.append(param) 210 | 211 | ### alright, save the tree 212 | self.acs_parameters = etree.tostring(root, xml_declaration=True).decode('utf-8') 213 | self.acs_parameters_time = timezone.now() 214 | self.save() 215 | acs_session.acs_log("Finished processing %s acs parameters for device %s" % (paramcount, self)) 216 | return True 217 | 218 | @property 219 | def acs_parameter_dict(self): 220 | if not self.acs_parameters: 221 | return False 222 | xmlroot = fromstring(bytes(self.acs_parameters, 'utf-8')) 223 | paramdict = {} 224 | for child in xmlroot.iterchildren(): 225 | value = child.find('Value') 226 | paramdict[child.find('Name').text] = { 227 | 'type': value.attrib['{%s}type' % acs_settings.SOAP_NAMESPACES['xsi']], 228 | 'value': value.text, 229 | 'writable': child.find('Writable').text, 230 | 'notification': child.find('Notification').text if child.find('Notification') is not None else "N/A", 231 | 'accesslist': ",".join([acl.text for acl in child.find('AccessList').getchildren()]) if child.find('AccessList') is not None else "N/A", 232 | } 233 | return OrderedDict(sorted(paramdict.items())) 234 | 235 | @property 236 | def acs_connection_request_url(self): 237 | if not self.latest_acs_session: 238 | # we have not had any acs sessions for this device 239 | return False 240 | root_object = self.latest_acs_session.root_data_model.root_object 241 | return self.acs_get_parameter_value('%s.ManagementServer.ConnectionRequestURL' % root_object) 242 | 243 | def acs_get_parameter_value(self, parameterpath): 244 | if not self.acs_parameters or not parameterpath: 245 | return False 246 | xmlroot = fromstring(bytes(self.acs_parameters, 'utf-8')) 247 | pvslist = xmlroot.xpath('./ParameterValueStruct/Name[text()="%s"]/..' % parameterpath) 248 | if pvslist: 249 | valuelist = pvslist[0].xpath('./Value') 250 | if not valuelist: 251 | return False 252 | return valuelist[0].text 253 | else: 254 | logger.error("unable to find %s in acs_parameters :(" % parameterpath) 255 | return False 256 | 257 | def acs_http_connection_request(self): 258 | ### get what we need 259 | url = self.acs_connection_request_url 260 | if not url or not self.acs_connectionrequest_password: 261 | logger.error("unable to make a connectionrequest without url or credentials") 262 | return False 263 | 264 | ### do the request 265 | try: 266 | return requests.get(url, auth=requests.auth.HTTPBasicAuth(self.acs_connectionrequest_username, self.acs_connectionrequest_password)) 267 | except requests.exceptions.ConnectionError as E: 268 | ### catching this exception is neccesary because requests does not catch the exception which httplib returns, 269 | ### because our HTTP servers are closing connection "too fast" without ever sending an HTTP response 270 | logger.exception("got exception %s while running HTTP request" % E) 271 | return False 272 | 273 | @property 274 | def nonautomatic_acs_queue_jobs(self): 275 | return self.acs_queue_jobs.filter(automatic=False) 276 | 277 | @property 278 | def unprocessed_acs_queue_jobs(self): 279 | return self.acs_queue_jobs.filter(processed=False) 280 | 281 | def get_related_device(self): 282 | """ 283 | Loops over models in settings.ACS_DEVICE_DJANGO_MODELS and checks each to find 284 | the related device (if any). 285 | Returns the related device or None 286 | """ 287 | for acsmodel in settings.ACS_DEVICE_DJANGO_MODELS: 288 | devicemodel = apps.get_model(acsmodel['app'], acsmodel['model']) 289 | kwargs = { 290 | acsmodel['acsdevice_relation_field']: self 291 | } 292 | try: 293 | return devicemodel.objects.get(**kwargs) 294 | except devicemodel.DoesNotExist: 295 | # no match in this model, try the next 296 | pass 297 | 298 | @property 299 | def latest_client_ip(self): 300 | """Return the client ip address of the latest acs session for this acs device (if any)""" 301 | if self.acs_sessions.exists(): 302 | return self.acs_sessions.latest('id').client_ip 303 | else: 304 | return False 305 | 306 | def associate_with_related_device(self): 307 | """ 308 | Find the real device which belongs to this acs device (if any), 309 | and create the foreignkey to this model as needed. 310 | """ 311 | if self.get_related_device(): 312 | # this acs_device is already associated with a device 313 | return 314 | 315 | # loop over configured acs models, checking each for a match for this acs device 316 | for acsmodel in settings.ACS_DEVICE_DJANGO_MODELS: 317 | devicemodel = apps.get_model(acsmodel['app'], acsmodel['model']) 318 | args = { 319 | acsmodel['serial_field']: self.serial, 320 | acsmodel['model_name_field']: self.model.name, 321 | acsmodel['vendor_name_field']: self.model.vendor.name, 322 | '%s__isnull' % acsmodel['acsdevice_relation_field']: True 323 | } 324 | try: 325 | device = devicemodel.objects.get(**args) 326 | setattr(device, acsmodel['acsdevice_relation_field'], self) 327 | device.save() 328 | # we are done here 329 | break 330 | except devicemodel.DoesNotExist: 331 | # no match found, this is fine 332 | pass 333 | 334 | 335 | -------------------------------------------------------------------------------- /acs/models/acs_device_basemodel.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | 3 | 4 | class AcsDeviceBaseModel(AcsBaseModel): 5 | class Meta: 6 | abstract = True 7 | 8 | def verify_acs_client_ip(self, ip): 9 | """ Method to verify the ACS client IP, override in your own models. """ 10 | raise NotImplementedError 11 | 12 | def is_configurable(self): 13 | """ Method to determine if an acsdevice is configurable, override in your own models. """ 14 | raise NotImplementedError 15 | 16 | def acs_session_pre_verify_hook(self): 17 | """ This method is called every time an ACS device runs an ACS session, before verify_acs_client_ip() is called. Override in your own models as needed. """ 18 | return False 19 | 20 | def acs_session_post_verify_hook(self): 21 | """ This method is called every time an ACS device runs an ACS session, after verify_acs_client_ip() is called. Override in your own models as needed. """ 22 | return False 23 | 24 | def get_acs_config(self): 25 | """ This method is called while configuring an ACS device. Override in your own models to add device specific config.""" 26 | raise NotImplementedError 27 | 28 | def get_user_config_changelist(self): 29 | """This method should acs_device.acs_parameters versus the local records and returns a list of changed elements, if any. Shoukd return an empty list if everything in acs_parameters matches the local records.""" 30 | raise NotImplementedError 31 | 32 | def handle_user_config_change(self): 33 | """Called whenever the configuration on an ACS device is different from what we configured on it.""" 34 | raise NotImplementedError 35 | 36 | -------------------------------------------------------------------------------- /acs/models/acs_device_category.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | from django.db import models 3 | from django.urls import reverse 4 | 5 | 6 | class AcsDeviceCategory(AcsBaseModel): 7 | """ The category of an ACS device model. Used to determine what to do with the device. """ 8 | WIFI = "WIFI" 9 | SETTOPBOX = "SETTOPBOX" 10 | UNKNOWN = "UNKNOWN" 11 | 12 | CATEGORY_CHOICES = ( 13 | (UNKNOWN, 'Unknown'), 14 | (WIFI, 'Wifi Device'), 15 | (SETTOPBOX, 'Settop Box') 16 | ) 17 | 18 | name = models.CharField(max_length=100, choices=CATEGORY_CHOICES, default=UNKNOWN, unique=True) 19 | 20 | def __str__(self): 21 | return str("%s - %s" % (self.tag, self.name)) 22 | 23 | def get_absolute_url(self): 24 | return reverse('acsdevicecategory_detail', kwargs={'pk': self.pk}) 25 | 26 | 27 | -------------------------------------------------------------------------------- /acs/models/acs_device_model.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | from django.urls import reverse 3 | from django.contrib.postgres.fields import JSONField 4 | from django.conf import settings 5 | from django.db import models 6 | from acs.default_acs_parametermap import default_acs_device_parametermap 7 | 8 | class AcsDeviceModel(AcsBaseModel): 9 | vendor = models.ForeignKey('acs.AcsDeviceVendor', related_name='acsdevicemodels', on_delete=models.PROTECT) 10 | category = models.ForeignKey('acs.AcsDeviceCategory', related_name='acsdevicemodels', default=1, on_delete=models.PROTECT) 11 | name = models.CharField(max_length=50) 12 | desired_config_level = models.DateTimeField(null=True, blank=True) 13 | desired_software_version = models.CharField(max_length=50, blank=True) 14 | acs_parameter_map_overrides = JSONField(null=True, blank=True) 15 | 16 | def __str__(self): 17 | return str("%s - %s" % (self.tag, self.name)) 18 | 19 | def get_absolute_url(self): 20 | return reverse('acsdevicemodel_detail', kwargs={'pk': self.pk}) 21 | 22 | @property 23 | def acs_parameter_map(self): 24 | # return the default_acs_device_parametermap with the overrides for this specific device 25 | if self.acs_parameter_map_overrides: 26 | default_acs_device_parametermap.update(self.acs_parameter_map_overrides) 27 | return default_acs_device_parametermap 28 | 29 | def get_active_notification_parameterlist(self, root_object): 30 | """ 31 | Return the list of parameters which needs active notifications, 32 | based on the category of devicemodel 33 | """ 34 | parameterlist = [] 35 | if self.category.name == "WIFI": 36 | #This acs device category needs notifications for the whole Wifi tree 37 | parameterlist.append("%s.Wifi." % root_object) 38 | return parameterlist 39 | 40 | -------------------------------------------------------------------------------- /acs/models/acs_device_vendor.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | from django.urls import reverse 3 | from django.db import models 4 | 5 | 6 | class AcsDeviceVendor(AcsBaseModel): 7 | name = models.CharField(max_length=100) 8 | oui = models.CharField(max_length=6) 9 | 10 | def __str__(self): 11 | return str('%s - %s' % (self.tag, self.name)) 12 | 13 | def get_absolute_url(self): 14 | return reverse('acsdevicevendor_detail', kwargs={'pk': self.pk}) 15 | 16 | -------------------------------------------------------------------------------- /acs/models/acs_http_basemodel.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from defusedxml.lxml import fromstring 4 | 5 | from django.db import models 6 | from django.conf import settings 7 | from django.utils.functional import cached_property 8 | 9 | from acs.models import AcsBaseModel 10 | from acs.conf import acs_settings 11 | 12 | 13 | class AcsHttpBaseModel(AcsBaseModel): 14 | """ 15 | This abstract model is used for the AcsHttpRequest and 16 | AcsHttpResponse models. It contains the common fields and 17 | methods shared by the two models. 18 | """ 19 | fk_body = models.ForeignKey( 20 | settings.DJANGO_ACS['XML_STORAGE_MODEL'], 21 | null=True, 22 | blank=True, 23 | on_delete=models.PROTECT, 24 | ) 25 | 26 | cwmp_id = models.CharField( 27 | max_length=100, 28 | blank=True 29 | ) 30 | 31 | soap_element = models.CharField( 32 | max_length=100, 33 | blank=True 34 | ) 35 | 36 | class Meta: 37 | abstract = True 38 | 39 | @cached_property 40 | def soap_element_ns(self): 41 | """ 42 | Return a namespaced version of the soap_element, so 43 | "{urn:dslforum-org:cwmp-1-0}Inform" becomes "cwmp:Inform" 44 | Takes the cwmp version into account. 45 | """ 46 | if not self.acs_session.soap_namespaces: 47 | # unable to get soap namespaces for this acs session, return unchanged 48 | return self.soap_element 49 | 50 | # loop over namespaces and find the right one 51 | for namespace, uri in self.acs_session.soap_namespaces.items(): 52 | if uri in self.soap_element: 53 | # found the matching namespace 54 | return self.soap_element.replace("{%s}" % uri, "%s:" % namespace) 55 | 56 | # this is either an unknown uri or a non-namespaced soap element 57 | return self.soap_element 58 | 59 | @cached_property 60 | def soap_element_tuple(self): 61 | """ 62 | Parse a soap element like {urn:dslforum-org:cwmp-1-0}Inform into a tuple like ("urn:dslforum-org:cwmp-1-0", "Inform") 63 | """ 64 | match = re.match("{(.+)}(.+)", self.soap_element) 65 | if match: 66 | return match.groups() 67 | else: 68 | # unable to parse namespace from soap element, this must be non-namespaced 69 | return (None, self.soap_element) 70 | 71 | @cached_property 72 | def cwmp_rpc_method(self): 73 | """ 74 | If this is a cwmp object return the cwmp_rpc_method 75 | """ 76 | namespace, method = self.soap_element_tuple 77 | if namespace in acs_settings.CWMP_NAMESPACES: 78 | return method 79 | else: 80 | return False 81 | 82 | @cached_property 83 | def soap_body(self): 84 | if not self.body: 85 | return False 86 | try: 87 | xmlroot = fromstring(bytes(self.body, 'utf-8')) 88 | # use acs_settings.SOAP_NAMESPACES directly here (rather than self.acs_session.soap_namespaces) since the namespace 'soap-env' does not depend on cwmp version 89 | return xmlroot.find('soap-env:Body', acs_settings.SOAP_NAMESPACES) 90 | except Exception: 91 | return False 92 | 93 | @cached_property 94 | def rpc_response(self): 95 | return self.rpc_responses.all().first() 96 | 97 | @cached_property 98 | def body(self): 99 | return self.fk_body.document 100 | 101 | -------------------------------------------------------------------------------- /acs/models/acs_http_request.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | from defusedxml.lxml import fromstring 3 | import uuid 4 | 5 | from django.db import models 6 | from django.http import HttpResponse 7 | from django.urls import reverse 8 | from django.core.exceptions import ObjectDoesNotExist 9 | 10 | from acs.response import get_soap_envelope 11 | from acs.models import AcsHttpBaseModel 12 | from acs.utils import create_xml_document 13 | 14 | class AcsHttpRequest(AcsHttpBaseModel): 15 | """ Every HTTP request received on the ACS server URL is saved as an instance 16 | of this model. """ 17 | 18 | acs_session = models.ForeignKey('acs.AcsSession', related_name='acs_http_requests', on_delete=models.PROTECT) 19 | rpc_response_to = models.ForeignKey('acs.AcsHttpResponse', related_name='rpc_responses', null=True, blank=True, on_delete=models.PROTECT) # a foreignkey to the http response containing the acs rpc request which triggered the current http request (where relevant) 20 | request_headers = models.TextField(blank=True) 21 | request_xml_valid = models.BooleanField(default=False) 22 | request_soap_valid = models.BooleanField(default=False) 23 | 24 | class Meta: 25 | ordering = ['-created_date'] 26 | 27 | def __str__(self): 28 | return str(self.tag) 29 | 30 | def get_absolute_url(self): 31 | return reverse('acshttprequest_detail', kwargs={'pk': self.pk}) 32 | 33 | @property 34 | def is_request(self): 35 | return True 36 | 37 | @property 38 | def is_response(self): 39 | return False 40 | 41 | def get_response(self, empty_response=False): 42 | ''' 43 | get_response() is called when the CPE is waiting for the ACS 44 | to do something. This happens after the CPE does an empty POST, or after 45 | the CPE has responded to an RPC call initiated by the ACS. It simply pops 46 | a job from the queue and returns it in a http response. 47 | ''' 48 | job = False 49 | if not empty_response: 50 | ### get the first job from the queue (if any) 51 | #jobs = AcsQueueJob.objects.filter(acs_device=self.acs_session.acs_device, processed=False).order_by('created_date') 52 | jobs = self.acs_session.acs_device.acs_queue_jobs.filter(processed=False).order_by('created_date') 53 | self.acs_session.acs_log("Found %s unprocessed acs queue jobs for the device %s" % (jobs.count(), self.acs_session.acs_device)) 54 | if jobs: 55 | job = jobs.first() 56 | self.acs_session.acs_log("Picked job %s" % job) 57 | 58 | if not empty_response and job: 59 | ### get blank SOAP response envelope 60 | response_cwmp_id = uuid.uuid4().hex 61 | root, body = get_soap_envelope(response_cwmp_id, self.acs_session) 62 | 63 | ### add the cwmp soap object to the soap body 64 | cwmpobj = fromstring(job.cwmp_rpc_object_xml.encode('utf-8')) 65 | body.append(cwmpobj) 66 | 67 | ### get the rpc method 68 | response_cwmp_rpc_method = job.cwmp_rpc_method 69 | 70 | ### put HTTP response together 71 | output = etree.tostring(root, encoding='utf-8', xml_declaration=True) 72 | response = HttpResponse(output, content_type='text/xml; charset=utf-8') 73 | else: 74 | ### no jobs in queue for this acs device (or an empty response was requested), so return empty body to end this cwmp session 75 | response = HttpResponse(status=204) 76 | response_cwmp_rpc_method = '(empty response body)' 77 | response_cwmp_id = '' 78 | 79 | ### save the http response 80 | from acs.models import AcsHttpResponse 81 | acs_http_response = AcsHttpResponse.objects.create( 82 | http_request=self, 83 | fk_body=create_xml_document(xml=response.content), 84 | cwmp_id=response_cwmp_id, 85 | soap_element="{%s}%s" % (self.acs_session.soap_namespaces['cwmp'], response_cwmp_rpc_method), 86 | ) 87 | self.acs_session.acs_log("Created ACS HTTP response %s" % acs_http_response) 88 | 89 | if job: 90 | self.acs_session.acs_log("Saving AcsQueueJob %s" % job) 91 | ### save job 92 | job.handled_in = acs_http_response 93 | job.processed = True 94 | job.save() 95 | 96 | ### all good, return response 97 | self.acs_session.acs_log("Responding to CPE %s with %s" % (self.acs_session.acs_device, response_cwmp_rpc_method)) 98 | return response 99 | 100 | -------------------------------------------------------------------------------- /acs/models/acs_http_response.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from acs.models import AcsHttpBaseModel 4 | 5 | 6 | class AcsHttpResponse(AcsHttpBaseModel): 7 | """ Every HTTP response given by the ACS server is saved as an instance of this model. """ 8 | http_request = models.OneToOneField('acs.AcsHttpRequest', related_name='acs_http_response', unique=True, on_delete=models.PROTECT) # a foreignkey to the http request which triggered this http response 9 | rpc_response_to = models.ForeignKey('acs.AcsHttpRequest', related_name='rpc_responses', null=True, blank=True, on_delete=models.PROTECT) # a foreignkey to the http request containing the acs rpc request which triggered the current http response (where relevant) 10 | 11 | class Meta: 12 | ordering = ['-created_date'] 13 | 14 | def __str__(self): 15 | return str(self.tag) 16 | 17 | def get_absolute_url(self): 18 | return reverse('acshttpresponse_detail', kwargs={'pk': self.pk}) 19 | 20 | @property 21 | def is_request(self): 22 | return False 23 | 24 | @property 25 | def is_response(self): 26 | return True 27 | 28 | @property 29 | def queuejob(self): 30 | return self.queuejobs.all().first() 31 | 32 | @property 33 | def acs_session(self): 34 | return self.http_request.acs_session 35 | 36 | -------------------------------------------------------------------------------- /acs/models/acs_queue_job.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | from django.urls import reverse 3 | from django.db import models 4 | from lxml import etree 5 | from defusedxml.lxml import fromstring 6 | 7 | 8 | class AcsQueueJob(AcsBaseModel): 9 | cwmp_rpc_object_xml = models.TextField() 10 | acs_device = models.ForeignKey('acs.AcsDevice', null=True, blank=True, related_name='acs_queue_jobs', on_delete=models.PROTECT) 11 | reason = models.CharField(max_length=200) 12 | automatic = models.BooleanField(default=False) 13 | urgent = models.BooleanField(default=False) 14 | notification_sent = models.BooleanField(default=False) 15 | processed = models.BooleanField(default=False) 16 | handled_in = models.ForeignKey('acs.AcsHttpResponse', null=True, blank=True, related_name='queuejobs', on_delete=models.PROTECT) 17 | 18 | class Meta: 19 | ordering = ['-created_date'] 20 | 21 | def __str__(self): 22 | return str(self.tag) 23 | 24 | def get_absolute_url(self): 25 | return reverse('acs_queue_job_detail', kwargs={'pk': self.pk}) 26 | 27 | @property 28 | def cwmp_rpc_method(self): 29 | ### parse XML to etree object 30 | cwmpobj = fromstring(bytes(self.cwmp_rpc_object_xml, 'utf-8')) 31 | 32 | ### returns 'FooMethod' from string '{urn:dslforum-org:cwmp-1-0}FooMethod' 33 | try: 34 | method = cwmpobj.tag.replace('{%s}' % cwmpobj.nsmap[cwmpobj.prefix], '') 35 | except KeyError: 36 | method = None 37 | return method 38 | 39 | def add_job_tag_as_command_key(self): 40 | element = fromstring(bytes(self.cwmp_rpc_object_xml, 'utf-8')) 41 | cmdkey = etree.SubElement(element, 'CommandKey') 42 | cmdkey.text = self.tag 43 | self.cwmp_rpc_object_xml=etree.tostring(element, xml_declaration=True).decode('utf-8') 44 | self.save() 45 | 46 | -------------------------------------------------------------------------------- /acs/models/acs_session.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import uuid, logging 3 | from psycopg2.extras import DateTimeTZRange 4 | 5 | from acs.models import AcsBaseModel 6 | from django.urls import reverse 7 | from django.contrib.postgres.fields import ArrayField 8 | from django.contrib.postgres.fields import DateTimeRangeField 9 | from django.utils import timezone 10 | from django.conf import settings 11 | from django.db import models 12 | 13 | from acs.models import AcsHttpResponse, AcsQueueJob 14 | from acs.utils import * 15 | from acs.response import get_soap_xml_object 16 | from acs.default_acs_parametermap import default_acs_device_parametermap 17 | from acs.conf import acs_settings 18 | 19 | logger = logging.getLogger('django_acs.%s' % __name__) 20 | 21 | class AcsSession(AcsBaseModel): 22 | """ An ACSSession model instance represents a single ACS session with 23 | an ACS device. Every HTTP request and HTTP response in that session 24 | is linked to the ACS session. The reference field can be used to record 25 | freehand info about the session.""" 26 | 27 | acs_device = models.ForeignKey('acs.AcsDevice', null=True, blank=True, related_name='acs_sessions', on_delete=models.PROTECT) 28 | acs_session_id = models.UUIDField(default=uuid.uuid4) 29 | client_ip = models.GenericIPAddressField() 30 | client_ip_verified = models.BooleanField(default=False) 31 | reference = models.CharField(max_length=100, default='', blank=True) 32 | session_result = models.BooleanField(default=False) 33 | latest_rpc_method = models.CharField(max_length=100, default='', blank=True) 34 | session_end = models.DateTimeField(null=True, blank=True) 35 | _device_uptime = DateTimeRangeField(null=True, blank=True) # use the property device_uptime instead 36 | inform_eventcodes = ArrayField(models.TextField(), default=list, blank=True) 37 | cwmp_namespace = models.CharField(max_length=100, default='', blank=True) 38 | root_data_model = models.ForeignKey('acs.CwmpDataModel', null=True, blank=True, related_name='acs_sessions', on_delete=models.PROTECT) 39 | 40 | class Meta: 41 | ordering = ['-created_date'] 42 | unique_together = (('acs_session_id', 'client_ip'),) 43 | 44 | def __str__(self): 45 | return str('%s (%s)' % (self.tag, self.hexid)) 46 | 47 | def get_absolute_url(self): 48 | return reverse('acssession_detail', kwargs={'pk': self.pk}) 49 | 50 | def add_acs_queue_job(self, cwmp_rpc_object_xml, reason, automatic=False, urgent=False): 51 | ''' 52 | add_acs_queue_job() adds an AcsQueueJob or returns an existing if an 53 | identical job already exists (not taking 'reason' into account) 54 | ''' 55 | 56 | ### do we have any XML? 57 | if not cwmp_rpc_object_xml: 58 | self.acs_log('unable to add acs queue job for acs_device %s, reason "%s": no XML found!' % (self.acs_device.tag, reason)) 59 | return False 60 | ### get or create job and return it 61 | try: 62 | job, created = AcsQueueJob.objects.get_or_create( 63 | acs_device=self.acs_device, 64 | cwmp_rpc_object_xml=cwmp_rpc_object_xml.decode('utf-8'), 65 | processed=False, 66 | urgent=urgent, 67 | defaults={ 68 | 'reason': reason, 69 | 'automatic': automatic, 70 | } 71 | ) 72 | except AcsQueueJob.MultipleObjectsReturned: 73 | job = AcsQueueJob.objects.filter( 74 | acs_device=self.acs_device, 75 | cwmp_rpc_object_xml=cwmp_rpc_object_xml, 76 | processed=False, 77 | urgent=urgent, 78 | ).first() 79 | created = False 80 | 81 | self.acs_log('%(action)s acs queue job %(job)s for acs_device %(acs_device)s method %(method)s reason: "%(reason)s" urgent: %(urgent)s' % { 82 | 'action': 'created' if created else 'returning', 83 | 'job': job.tag, 84 | 'acs_device': self.acs_device, 85 | 'method': job.cwmp_rpc_method, 86 | 'reason': job.reason, 87 | 'urgent': job.urgent, 88 | }) 89 | return job 90 | 91 | @property 92 | def configuration_done(self): 93 | """ 94 | Check if we have done both SetParameterValues and SetParameterAttributes in this session 95 | """ 96 | if self.acs_http_responses.filter(soap_element="{%s}SetParameterValues" % self.cwmp_namespace).exists() and self.acs_http_responses.filter(soap_element="{%s}SetParameterAttributes" % self.cwmp_namespace).exists(): 97 | return True 98 | return False 99 | 100 | @property 101 | def post_configuration_collection_done(self): 102 | """ 103 | After configuring a device we queue a GetParameterNames, GetParameterValues and GetParameterAttributes call. 104 | Check if this has been done or not. 105 | """ 106 | # first get the id of the SetParameterValues call 107 | if not self.acs_http_responses.filter(soap_element="{%s}SetParameterValues" % self.cwmp_namespace).exists(): 108 | # no SetParameterValues call found 109 | return False 110 | configcall = self.acs_http_responses.filter(soap_element="{%s}SetParameterValues" % self.cwmp_namespace).first() 111 | if self.acs_http_responses.filter(soap_element="{%s}GetParameterNames" % self.cwmp_namespace, id__gt=configcall.id).exists() and self.acs_http_responses.filter(soap_element="{%s}GetParameterValues" % self.cwmp_namespace, id__gt=configcall.id).exists() and self.acs_http_responses.filter(soap_element="{%s}GetParameterAttributes" % self.cwmp_namespace, id__gt=configcall.id).exists(): 112 | return True 113 | return False 114 | 115 | def get_device_parameterdict(self, configdict): 116 | """ 117 | Loop over keys in configdict and find the ACS parameter we need to set, 118 | add each to the parameterdict and return. 119 | """ 120 | parameterdict = {} 121 | for key in list(configdict.keys()): 122 | parameterdict[self.get_acs_parameter_name(key)] = configdict[key] 123 | return parameterdict 124 | 125 | def configure_device_parameter_attributes(self, reason, parameterlist, update_parameterkey=False): 126 | """Takes a list of parameter names and configures them for active notification""" 127 | parameterdict = {} 128 | for param in parameterlist: 129 | parameterdict[param] = 1 # 2=active notification 130 | return self.acs_rpc_set_parameter_attributes( 131 | parameterdict=parameterdict, 132 | reason='configuring wifi device notifications (reason: %s)' % reason, 133 | automatic=True, 134 | update_parameterkey=update_parameterkey 135 | ) 136 | 137 | def configure_device_parameter_values(self, reason, update_parameterkey=False): 138 | """ Configures ACS device """ 139 | # initialize empty configdict 140 | configdict = {} 141 | 142 | # set InformInterval to 2 hours 143 | configdict['django_acs.acs.informinterval'] = acs_settings.INFORM_INTERVAL 144 | 145 | # enable ACS managed firmware upgrades (disables manufacturer/provider upgrades) 146 | configdict['django_acs.acs.acs_managed_upgrades'] = True 147 | 148 | # add acs client xmpp settings (only if we have an xmpp account for this device) 149 | if self.acs_device.acs_xmpp_password: 150 | configdict['django_acs.acs.xmpp_server'] = settings.ACS_XMPP_SERVERTUPLE[0] 151 | configdict['django_acs.acs.xmpp_server_port'] = settings.ACS_XMPP_SERVERTUPLE[1] 152 | configdict['django_acs.acs.xmpp_connection_enable'] = True 153 | configdict['django_acs.acs.xmpp_connection_username'] = self.acs_device.acs_xmpp_username 154 | configdict['django_acs.acs.xmpp_connection_password'] = self.acs_device.acs_xmpp_password 155 | configdict['django_acs.acs.xmpp_connection_domain'] = settings.ACS_XMPP_DOMAIN # all ACS clients connect to the same XMPP server domain for now 156 | configdict['django_acs.acs.xmpp_connection_usetls'] = True 157 | configdict['django_acs.acs.xmpp_connreq_connection'] = '%s.XMPP.Connection.1.' % self.root_data_model.root_object 158 | 159 | # set connectionrequest credentials? 160 | if self.acs_device.acs_connectionrequest_username: 161 | configdict['django_acs.acs.connection_request_user'] = self.acs_device.acs_connectionrequest_username 162 | configdict['django_acs.acs.connection_request_password'] = self.acs_device.acs_connectionrequest_password 163 | 164 | # get device specific config from the related device and add it to the configdict 165 | if self.acs_device.get_related_device(): 166 | configdict.update(self.acs_device.get_related_device().get_acs_config()) 167 | 168 | # get a parameterdict suitable for this device by feeding our configdict to this devices get_parameterdict_from_configdict() method 169 | parameterdict = self.get_device_parameterdict(configdict) 170 | 171 | # queue and return the job 172 | return self.acs_rpc_set_parameter_values( 173 | parameterdict=parameterdict, 174 | reason='configuring wifi device (reason: %s)' % reason, 175 | automatic=True, 176 | update_parameterkey=update_parameterkey 177 | ) 178 | 179 | def configure_device(self): 180 | """ 181 | Method to determine if the acs device should be configured. 182 | Returns True if device does not need config, or cannot be configured because we don't have enough information. 183 | Return False if an attempt to queue jobs to configure the device fails. 184 | """ 185 | if not self.acs_device.get_related_device(): 186 | self.acs_log("Not configuring %s: We refuse to put configuration on an acs device if it has no related device" % self.acs_device) 187 | return True 188 | 189 | if not self.acs_device.get_related_device().is_configurable(): 190 | self.acs_log("Not configuring %s: The real device which this acs_device is related to is not configurable" % self.acs_device) 191 | return True 192 | 193 | if not self.client_ip_verified: 194 | self.acs_log("Not configuring %s: This acs_session is not ip_verified" % self.acs_device) 195 | return True 196 | 197 | if self.acs_device.current_config_level and self.acs_device.current_config_level >= self.acs_device.get_desired_config_level(): 198 | self.acs_log("Not configuring %s: This acs_device is already at least at the config_level we want it to be at" % self.acs_device) 199 | return True 200 | 201 | # we need to configure this device! but what is the reason? 202 | if self.acs_device.current_config_level: 203 | reason="Device config level is wrong (it is %s but it should be %s)" % (self.acs_device.current_config_level, self.acs_device.get_desired_config_level()) 204 | else: 205 | reason="Device is unconfigured/has been factory defaulted" 206 | 207 | # we need to update parameterkey in both calls even though we have more work to do, 208 | # because it appears the parameterkey is not updated after a SetParameterAttributes call for some reason 209 | if not self.configure_device_parameter_values(reason=reason, update_parameterkey=True): 210 | self.acs_log('error, unable to create acs job to configure device parameter values for %s' % self.acs_device) 211 | return False 212 | 213 | # get the list of parameters we want to set notification on 214 | parameterlist = self.acs_device.model.get_active_notification_parameterlist(self.root_data_model.root_object) 215 | 216 | # queue the job to set active notifications 217 | if not self.configure_device_parameter_attributes(reason=reason, parameterlist=parameterlist, update_parameterkey=True): 218 | self.acs_log('error, unable to create acs job to configure parameter attributes for %s' % self.acs_device) 219 | return False 220 | 221 | # all done 222 | return True 223 | 224 | def device_firmware_upgrade(self): 225 | if not self.acs_device.get_related_device(): 226 | # we refuse to upgrade acs devices with no related device 227 | return True 228 | 229 | if not self.acs_device.get_related_device().is_configurable(): 230 | # the related device is not configurable 231 | return True 232 | 233 | if not self.client_ip_verified: 234 | # this acs session is not ip verified 235 | return True 236 | 237 | if not self.acs_device.get_desired_software_version(): 238 | # we cannot upgrade a device if we dont know its current software version 239 | return True 240 | 241 | if self.acs_device.get_desired_software_version() == self.acs_device.current_software_version: 242 | # this device has the software version we want it to 243 | return True 244 | 245 | # check if we have already done a Download RPC call in this session 246 | if self.acs_http_responses.filter(soap_element='{%s}Download' % self.cwmp_namespace).exists(): 247 | # we already have a Download job in this session, one must be enough 248 | return True 249 | 250 | # OK, queue the firmware upgrade 251 | self.acs_log("%s has wrong software version (current: %s - desired: %s), queueing download job" % (self.acs_device, self.acs_device.current_software_version, self.acs_device.get_desired_software_version())) 252 | job = self.acs_rpc_download( 253 | parameterdict={ 254 | 'url': self.acs_device.get_software_url(version=self.acs_device.get_desired_software_version()) 255 | }, 256 | reason='Current software version %s differs from desired software version %s' % (self.acs_device.current_software_version, self.acs_device.get_desired_software_version()), 257 | automatic=True, 258 | ) 259 | if not job: 260 | message = 'error, unable to create acs queue job for %s' % self.acs_device 261 | self.acs_log(message) 262 | return False 263 | 264 | # all good 265 | return True 266 | 267 | def collect_device_info(self, reason): 268 | """ 269 | Called in the beginning of the Inform, and after configuring device, to gather all information we can from the acs device 270 | """ 271 | if not self.get_all_parameter_names(reason): 272 | self.acs_log('error, unable to create GetParameterNames acs queue job for %s' % self.acs_device) 273 | return False 274 | 275 | if not self.get_all_parameter_values(reason): 276 | self.acs_log('error, unable to create GetParameterValues acs queue job for %s' % self.acs_device) 277 | return False 278 | 279 | if not self.get_all_parameter_attributes(reason): 280 | self.acs_log('error, unable to create GetParameterAttributes acs queue job for %s' % self.acs_device) 281 | return False 282 | 283 | # all good 284 | return True 285 | 286 | def get_all_parameter_names(self, reason): 287 | # get all parameter names in the tree 288 | return self.acs_rpc_get_parameter_names( 289 | reason=reason, 290 | parampath='', 291 | nextlevel='0', 292 | automatic=True, 293 | ) 294 | 295 | def get_all_parameter_values(self, reason): 296 | # get all parameter values under the root element 297 | return self.acs_rpc_get_parameter_values( 298 | reason=reason, 299 | parameterlist=['%s.' % self.root_data_model.root_object], 300 | automatic=True, 301 | ) 302 | 303 | def get_all_parameter_attributes(self, reason): 304 | # get all parameter attributes under Device. 305 | return self.acs_rpc_get_parameter_attributes( 306 | reason=reason, 307 | parameterlist=['%s.' % self.root_data_model.root_object], 308 | automatic=True, 309 | ) 310 | 311 | def get_acs_parameter_name(self, parametername): 312 | """ 313 | Converts something like django_acs.acs.parameterkey to Device.ManagementServer.ParameterKey based on the root_data_model in use in this session. 314 | """ 315 | # TODO: Does not currently support device overrides! 316 | if not self.root_data_model: 317 | return False 318 | root_object = self.root_data_model.root_object 319 | element = default_acs_device_parametermap[parametername] 320 | return "%s.%s" % (root_object, element) 321 | 322 | def get_inform_eventcodes(self, inform, acshttprequest): 323 | # get Event element from Inform request 324 | event = inform.find('Event') 325 | if event is None: 326 | message = 'Invalid Inform, Event missing from request %s' % acshttprequest 327 | self.acs_log(message) 328 | return False 329 | 330 | # get the EventCode(s) for this inform 331 | eventcodes = [] 332 | for es in event.findall('EventStruct'): 333 | message = 'Found EventStruct with EventCode %s inside Inform' % es.find('EventCode').text 334 | self.acs_log(message) 335 | eventcodes.append(es.find('EventCode').text) 336 | 337 | if not eventcodes: 338 | message = 'EventStruct sections mising from Event in request %s' % acshttprequest 339 | self.acs_log(message) 340 | else: 341 | self.inform_eventcodes = eventcodes 342 | 343 | # return true even if we didn't find any eventcodes 344 | return True 345 | 346 | def get_all_values_rpc_response(self): 347 | """ 348 | Loop over the GetParameterValuesResponse http requests in this acs session and find 349 | one asking for the whole device tree 350 | """ 351 | for httpreq in self.acs_http_requests.filter(soap_element='{%s}GetParameterValuesResponse' % self.cwmp_namespace): 352 | if httpreq.soap_body.find('cwmp:GetParameterValuesResponse', self.soap_namespaces).xpath('.//string[text()="%s."]' % self.root_data_model.root_object) is not None: 353 | # one of the requested values is Device. - great! return this request 354 | return httpreq 355 | # nothing found 356 | return False 357 | 358 | def get_all_names_rpc_response(self): 359 | """ 360 | Loop over the GetParameterNamesResponse http requests in this acs session and find 361 | one asking for the whole device tree 362 | """ 363 | for httpreq in self.acs_http_requests.filter(soap_element='{%s}GetParameterNamesResponse' % self.cwmp_namespace): 364 | if httpreq.soap_body.find('cwmp:GetParameterNamesResponse', self.soap_namespaces).xpath('.//string[text()="%s."]' % self.root_data_model.root_object) is not None: 365 | # one of the requested values is Device. - great! return this request 366 | return httpreq 367 | # nothing found 368 | return False 369 | 370 | 371 | def determine_data_model(self, inform): 372 | """ 373 | Find the tr-069 data model in use based on the information in the Inform 374 | This would be a bit of a faff if the devices all followed the cwmp specs. 375 | Since the devices don't follow the specs this is actually a massive faff. 376 | """ 377 | # get parameterlist 378 | parameterlist = inform.find('ParameterList') 379 | 380 | # try getting Device.DeviceSummary 381 | summary = get_value_from_parameterlist(parameterlist, 'Device.DeviceSummary') 382 | if summary: 383 | # this is some version of an Device:1 device, we can get the exact datamodel version from Device.DeviceSummary 384 | datamodel = get_datamodel_from_devicesummary(summary) 385 | if datamodel: 386 | return datamodel 387 | 388 | # try getting InternetGatewayDevice.DeviceSummary 389 | summary = get_value_from_parameterlist(parameterlist, 'InternetGatewayDevice.DeviceSummary') 390 | if summary: 391 | # this is some version of an InternetGatewayDevice:1 device, we can get the exact datamodel version from InternetGatewayDevice.DeviceSummary 392 | datamodel = get_datamodel_from_devicesummary(summary) 393 | if datamodel: 394 | return datamodel 395 | 396 | # this might be InternetGatewayDevice:1.0 which did not have InternetGatewayDevice.DeviceSummary. 397 | # if we have some value under InternetGatewayDevice and we got this far, this must be data model "InternetGatewayDevice:1.0" 398 | if get_value_from_parameterlist(parameterlist, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion'): 399 | return "InternetGatewayDevice:1.0" 400 | 401 | # this must be a Device:2.x data model, check if we have Device.RootDataModelVersion 402 | if get_value_from_parameterlist(parameterlist, 'Device.RootDataModelVersion'): 403 | return "Device:%s" % get_value_from_parameterlist(parameterlist, 'Device.RootDataModelVersion') 404 | 405 | # this device datamodel is between Device:2.0 and Device:2.3, 406 | # no idea how to figure out which one until we see a device of this type, 407 | # to be conservative and assume the lowest version 408 | return "Device:2.0" 409 | 410 | @property 411 | def cwmp_version(self): 412 | if self.cwmp_namespace == "urn:dslforum-org:cwmp-1-0": 413 | return "1.0" 414 | elif self.cwmp_namespace == "urn:dslforum-org:cwmp-1-1": 415 | return "1.1" 416 | elif self.cwmp_namespace == "urn:dslforum-org:cwmp-1-2": 417 | return "1.2" 418 | elif self.cwmp_namespace == "urn:dslforum-org:cwmp-1-3": 419 | return "1.3" 420 | elif self.cwmp_namespace == "urn:dslforum-org:cwmp-1-4": 421 | return "1.4" 422 | else: 423 | return False 424 | 425 | def acs_log(self, message): 426 | logger.info('acs session %s: %s' % (self.tag, message)) 427 | 428 | @property 429 | def device_uptime(self): 430 | """ 431 | Lazily populated fake field, added february 2018. Returns the value straight from the model if one is present, 432 | otherwise extracts the value from the device acs_parameters XML and saves it in the session model, and then returns it 433 | """ 434 | if self._device_uptime: 435 | return self._device_uptime 436 | else: 437 | return self.update_device_uptime() 438 | 439 | def update_device_uptime(self): 440 | """ 441 | Update _device_uptime by finding the relevant value in the acs parameters of the device 442 | """ 443 | if not self.acs_device: 444 | return False 445 | uptime_seconds = self.acs_device.acs_get_parameter_value(self.get_acs_parameter_name('django_acs.deviceinfo.uptime')) 446 | if not uptime_seconds: 447 | return False 448 | self._device_uptime = DateTimeTZRange(timezone.now()-timedelta(seconds=int(uptime_seconds)), timezone.now()) 449 | self.save() 450 | return self._device_uptime 451 | 452 | @property 453 | def hexid(self): 454 | return self.acs_session_id.hex 455 | 456 | @property 457 | def soap_namespaces(self): 458 | if not self.cwmp_namespace: 459 | # cannot determine namespaces without knowing which cwmp version we are using 460 | return False 461 | 462 | # add the cwmp namespace to the acs_settings.SOAP_NAMESPACES dict and return 463 | namespaces = acs_settings.SOAP_NAMESPACES 464 | namespaces.update({ 465 | 'cwmp': self.cwmp_namespace, 466 | }) 467 | return namespaces 468 | 469 | @property 470 | def start(self): 471 | return self.created_date 472 | 473 | @property 474 | def end(self): 475 | if not self.session_end: 476 | self.session_end = self.acs_http_responses.latest('created_date').created_date 477 | self.save() 478 | return self.session_end 479 | 480 | @property 481 | def duration(self): 482 | return self.end - self.start 483 | 484 | @property 485 | def bytes_in(self): 486 | bytes_in = 0 487 | for http in self.acs_http_requests.all(): 488 | bytes_in += len(http.body) 489 | return bytes_in 490 | 491 | @property 492 | def bytes_out(self): 493 | ### this could probably be done really elegantly with .aggregate and Sum somehow 494 | bytes_out = 0 495 | for resp in self.acs_http_responses.all(): 496 | bytes_out += len(resp.body) 497 | return bytes_out 498 | 499 | @property 500 | def acs_http_responses(self): 501 | return AcsHttpResponse.objects.filter(http_request__acs_session=self).order_by('-created_date') 502 | 503 | @property 504 | def acs_http_conversationlist(self): 505 | conversationlist = [] 506 | for req in self.acs_http_requests.all(): 507 | if hasattr(req, 'acs_http_response'): 508 | conversationlist.append(req.acs_http_response) 509 | conversationlist.append(req) 510 | return conversationlist 511 | 512 | def get_latest_http_tx(self): 513 | # this might need to be wrapped in a try/except for weird cases 514 | try: 515 | return self.acs_http_conversationlist[0] 516 | except IndexError: 517 | return False 518 | 519 | def update_session_result(self): 520 | latest_tx = self.get_latest_http_tx() 521 | self.latest_rpc_method = latest_tx.soap_element 522 | self.session_end = latest_tx.created_date 523 | if self.latest_rpc_method != '{%s}(empty response body)' % self.cwmp_namespace or latest_tx.is_request: 524 | # the last http tx in this acs session is not an http response with the cwmp_rpc_method '(empty response body)' so something is fucky 525 | self.session_result=False 526 | else: 527 | self.session_result=True 528 | self.save() 529 | 530 | # update acs device 531 | ad = self.acs_device 532 | ad.acs_latest_session_result=self.session_result 533 | ad.save() 534 | 535 | 536 | ########################################################################################################### 537 | ### ACS RPC METHODS BELOW HERE 538 | 539 | ### GetRPCMethods 540 | def acs_rpc_get_rpc_methods(self, reason, automatic=False, urgent=False): 541 | job = self.add_acs_queue_job( 542 | cwmp_rpc_object_xml=get_soap_xml_object( 543 | 'GetRPCMethods' 544 | ), 545 | reason=reason, 546 | urgent=urgent, 547 | automatic=automatic, 548 | ) 549 | return job 550 | 551 | ### SetParameterValues 552 | def acs_rpc_set_parameter_values(self, reason, parameterdict, automatic=False, urgent=False, update_parameterkey=False): 553 | job = self.add_acs_queue_job( 554 | cwmp_rpc_object_xml=get_soap_xml_object( 555 | 'SetParameterValues', 556 | datadict=parameterdict, 557 | update_parameterkey=update_parameterkey 558 | ), 559 | reason=reason, 560 | urgent=urgent, 561 | automatic=automatic, 562 | ) 563 | return job 564 | 565 | ### GetParameterValues 566 | def acs_rpc_get_parameter_values(self, reason, parameterlist, automatic=False, urgent=False): 567 | job = self.add_acs_queue_job( 568 | cwmp_rpc_object_xml=get_soap_xml_object( 569 | 'GetParameterValues', datadict={ 570 | 'parameterlist': parameterlist 571 | } 572 | ), 573 | reason=reason, 574 | urgent=urgent, 575 | automatic=automatic, 576 | ) 577 | return job 578 | 579 | 580 | ### GetParameterNames 581 | def acs_rpc_get_parameter_names(self, reason, parampath='', nextlevel='0', automatic=False, urgent=False): 582 | job = self.add_acs_queue_job( 583 | cwmp_rpc_object_xml=get_soap_xml_object( 584 | 'GetParameterNames', datadict={ 585 | 'parampath': parampath, 586 | 'nextlevel': nextlevel, 587 | } 588 | ), 589 | reason=reason, 590 | urgent=urgent, 591 | automatic=automatic, 592 | ) 593 | return job 594 | 595 | ### GetParameterAttributes 596 | def acs_rpc_get_parameter_attributes(self, reason, parameterlist, automatic=False, urgent=False): 597 | job = self.add_acs_queue_job( 598 | cwmp_rpc_object_xml=get_soap_xml_object( 599 | 'GetParameterAttributes', datadict={ 600 | 'parameterlist': parameterlist 601 | } 602 | ), 603 | reason=reason, 604 | urgent=urgent, 605 | automatic=automatic, 606 | ) 607 | return job 608 | 609 | ### SetParameterAttributes 610 | def acs_rpc_set_parameter_attributes(self, reason, parameterdict, automatic=False, urgent=False, update_parameterkey=False): 611 | job = self.add_acs_queue_job( 612 | cwmp_rpc_object_xml=get_soap_xml_object( 613 | 'SetParameterAttributes', 614 | datadict=parameterdict, 615 | update_parameterkey=update_parameterkey 616 | ), 617 | reason=reason, 618 | urgent=urgent, 619 | automatic=automatic, 620 | ) 621 | return job 622 | 623 | ### AddObject 624 | def acs_rpc_add_object(self, reason, objectname, automatic=False, urgent=False, update_parameterkey=False): 625 | job = self.add_acs_queue_job( 626 | cwmp_rpc_object_xml=get_soap_xml_object( 627 | 'AddObject', 628 | datadict={ 629 | 'objectname': objectname 630 | }, 631 | update_parameterkey=update_parameterkey 632 | ), 633 | reason=reason, 634 | urgent=urgent, 635 | automatic=automatic, 636 | ) 637 | return job 638 | 639 | ### DeleteObject 640 | def acs_rpc_delete_object(self, reason, objectname, automatic=False, urgent=False, update_parameterkey=False): 641 | job = self.add_acs_queue_job( 642 | cwmp_rpc_object_xml=get_soap_xml_object( 643 | 'DeleteObject', 644 | datadict={ 645 | 'objectname': objectname 646 | }, 647 | update_parameterkey=update_parameterkey 648 | ), 649 | reason=reason, 650 | urgent=urgent, 651 | automatic=automatic, 652 | ) 653 | return job 654 | 655 | ### Reboot 656 | def acs_rpc_reboot(self, reason, automatic=False, urgent=False): 657 | job = self.add_acs_queue_job( 658 | cwmp_rpc_object_xml=get_soap_xml_object('Reboot'), 659 | reason=reason, 660 | urgent=urgent, 661 | automatic=automatic, 662 | ) 663 | job.add_job_tag_as_command_key() 664 | return job 665 | 666 | ### Download 667 | def acs_rpc_download(self, reason, parameterdict, automatic=False, urgent=False): 668 | job = self.add_acs_queue_job( 669 | cwmp_rpc_object_xml=get_soap_xml_object('Download', datadict=parameterdict), 670 | reason=reason, 671 | urgent=urgent, 672 | automatic=automatic, 673 | ) 674 | job.add_job_tag_as_command_key() 675 | return job 676 | 677 | ### Upload 678 | def acs_rpc_upload(self, reason, parameterdict, automatic=False, urgent=False): 679 | job = self.add_acs_queue_job( 680 | cwmp_rpc_object_xml=get_soap_xml_object('Upload', datadict=parameterdict), 681 | reason=reason, 682 | urgent=urgent, 683 | automatic=automatic, 684 | ) 685 | return job 686 | 687 | ### FactoryReset 688 | def acs_rpc_factory_reset(self, reason, automatic=False, urgent=False): 689 | job = self.add_acs_queue_job( 690 | cwmp_rpc_object_xml=get_soap_xml_object('FactoryReset'), 691 | reason=reason, 692 | urgent=urgent, 693 | automatic=automatic, 694 | ) 695 | return job 696 | 697 | ### ScheduleInform 698 | def acs_rpc_schedule_inform(self, reason, parameterdictdict, automatic=False, urgent=False): 699 | job = self.add_acs_queue_job( 700 | cwmp_rpc_object_xml=get_soap_xml_object('ScheduleInform', datadict=parameterdict), 701 | reason=reason, 702 | urgent=urgent, 703 | automatic=automatic, 704 | ) 705 | return job 706 | 707 | -------------------------------------------------------------------------------- /acs/models/cwmp_data_model.py: -------------------------------------------------------------------------------- 1 | from acs.models import AcsBaseModel 2 | from django.db import models 3 | 4 | 5 | class CwmpDataModel(AcsBaseModel): 6 | name = models.TextField() 7 | xml_filename = models.TextField(blank=True) 8 | html_link = models.TextField(blank=True) 9 | 10 | def __str__(self): 11 | return str("%s - %s - %s" % (self.tag, self.name, self.xml_filename)) 12 | 13 | @property 14 | def root_object(self): 15 | """ 16 | Return "Device" from "Device:2.11" 17 | """ 18 | return self.name.split(":")[0] 19 | 20 | -------------------------------------------------------------------------------- /acs/response.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.utils import timezone 6 | from django.core.exceptions import MultipleObjectsReturned 7 | 8 | from acs.conf import acs_settings 9 | 10 | logger = logging.getLogger('django_acs.%s' % __name__) 11 | 12 | 13 | def add_parameter_names(soapobject, parameterlist): 14 | ### add empty ParameterNames element 15 | paramnames = etree.SubElement(soapobject, 'ParameterNames') 16 | for parameter in parameterlist: 17 | param = etree.SubElement(paramnames, 'string') 18 | param.text = parameter 19 | 20 | ### finally add the total number of elements 21 | paramnames.set(nse('soap-enc', 'arrayType'), "xsd:string[%s]" % len(parameterlist)) 22 | 23 | 24 | def get_soap_xml_object(cwmp_rpc_method, datadict=None, update_parameterkey=False): 25 | ### first get the outer soap object 26 | soapobject = etree.Element(nse('cwmp', cwmp_rpc_method)) 27 | 28 | ### determine what method we are working with 29 | cwmp_rpc_method = str(cwmp_rpc_method) 30 | if cwmp_rpc_method=='GetRPCMethods': 31 | ### nothing else to add for this call 32 | pass 33 | 34 | elif cwmp_rpc_method=='SetParameterValues': 35 | if not datadict: 36 | logger.error('datadict needed for SetParameterValues') 37 | return False 38 | paramlist = etree.SubElement(soapobject, 'ParameterList') 39 | for key, value in datadict.items(): 40 | add_pvs(element=paramlist, key=key, value=value) 41 | paramlist.set(nse('soap-enc', 'arrayType'), "cwmp:ParameterValueStruct[%s]" % len(paramlist)) 42 | paramkey = etree.SubElement(soapobject, 'ParameterKey') 43 | if update_parameterkey: 44 | paramkey.text = str(timezone.now()) 45 | else: 46 | paramkey.text = str(settings.CWMP_CONFIG_INCOMPLETE_PARAMETERKEY_DATE) 47 | 48 | elif cwmp_rpc_method=='GetParameterValues': 49 | if not datadict: 50 | logger.error('datadict needed for GetParameterValues') 51 | return False 52 | 53 | ### add ParameterNames 54 | add_parameter_names(soapobject, datadict['parameterlist']) 55 | 56 | elif cwmp_rpc_method=='GetParameterNames': 57 | if not datadict: 58 | datadict = { 59 | 'parampath': '', 60 | 'nextlevel': '0' 61 | } 62 | ### add the inner response elements, but without XML namespace (according to cwmp spec!) 63 | parampath = etree.SubElement(soapobject, 'ParameterPath') 64 | parampath.text = datadict['parampath'] 65 | nextlevel = etree.SubElement(soapobject, 'NextLevel') 66 | nextlevel.text = datadict['nextlevel'] 67 | 68 | elif cwmp_rpc_method=='GetParameterAttributes': 69 | if not datadict: 70 | logger.error('datadict needed for GetParameterAttributes, returning False') 71 | return False 72 | 73 | ### add ParameterNames 74 | add_parameter_names(soapobject, datadict['parameterlist']) 75 | 76 | elif cwmp_rpc_method=='SetParameterAttributes': 77 | if not datadict: 78 | logger.error('datadict needed for SetParameterValues, returning False') 79 | return False 80 | paramlist = etree.SubElement(soapobject, 'ParameterList') 81 | for name, value in datadict.items(): 82 | add_pas(element=paramlist, name=name, value=value) 83 | paramlist.set(nse('soap-enc', 'arrayType'), "cwmp:SetParameterAttributeStruct[%s]" % len(paramlist)) 84 | paramkey = etree.SubElement(soapobject, 'ParameterKey') 85 | if update_parameterkey: 86 | paramkey.text = str(timezone.now()) 87 | else: 88 | paramkey.text = str(settings.CWMP_CONFIG_INCOMPLETE_PARAMETERKEY_DATE) 89 | 90 | elif cwmp_rpc_method=='AddObject': 91 | if not datadict: 92 | logger.error('datadict needed for AddObject, returning False') 93 | return False 94 | objname = etree.SubElement(soapobject, 'ObjectName') 95 | objname.text = datadict['objectname'] 96 | paramkey = etree.SubElement(soapobject, 'ParameterKey') 97 | if update_parameterkey: 98 | paramkey.text = str(timezone.now()) 99 | else: 100 | paramkey.text = str(settings.CWMP_CONFIG_INCOMPLETE_PARAMETERKEY_DATE) 101 | 102 | elif cwmp_rpc_method=='DeleteObject': 103 | if not datadict: 104 | logger.error('datadict needed for DeleteObject, returning False') 105 | return False 106 | objname = etree.SubElement(soapobject, 'ObjectName') 107 | objname.text = datadict['objectname'] 108 | paramkey = etree.SubElement(soapobject, 'ParameterKey') 109 | if update_parameterkey: 110 | paramkey.text = str(timezone.now()) 111 | else: 112 | paramkey.text = str(settings.CWMP_CONFIG_INCOMPLETE_PARAMETERKEY_DATE) 113 | 114 | elif cwmp_rpc_method=='Reboot': 115 | ### nothing else to add for this call 116 | pass 117 | 118 | elif cwmp_rpc_method=='Download': 119 | if not datadict: 120 | logger.error('datadict needed for Download, returning False') 121 | return False 122 | filetype = etree.SubElement(soapobject, 'FileType') 123 | filetype.text = "1 Firmware Upgrade Image" 124 | url = etree.SubElement(soapobject, 'URL') 125 | url.text = datadict['url'] 126 | 127 | elif cwmp_rpc_method=='Upload': 128 | logger.error("Unimplemented") 129 | return False 130 | 131 | elif cwmp_rpc_method=='FactoryReset': 132 | ### nothing else to add for this call 133 | pass 134 | 135 | elif cwmp_rpc_method=='ScheduleInform': 136 | logger.error("Unimplemented") 137 | return False 138 | 139 | else: 140 | ### unsupported for now 141 | logger.error('unsupported xml object: %s - returning False' % cwmp_rpc_method) 142 | return False 143 | 144 | ### return a string representation of the XML 145 | return etree.tostring(soapobject, encoding='utf-8', xml_declaration=True) 146 | 147 | 148 | def get_soap_envelope(cwmp_id, acs_session): 149 | ''' 150 | Returns an lxml.etree element representing an empty soap XML envelopes 151 | Returns both root and body (for convenience). 152 | ''' 153 | ### begin SOAP envelope 154 | root = etree.Element(nse('soap-env', 'Envelope'), nsmap=acs_session.soap_namespaces) 155 | 156 | ### add SOAP Header 157 | header = etree.SubElement(root, nse('soap-env', 'Header')) 158 | 159 | ### add cwmp id to Header 160 | cwmpid = etree.SubElement(header, nse('cwmp', 'ID')) 161 | cwmpid.set(nse('soap-env', 'mustUnderstand'), "1") 162 | cwmpid.text = cwmp_id 163 | 164 | ### add SOAP Body 165 | body = etree.SubElement(root, nse('soap-env', 'Body')) 166 | 167 | ### return 168 | return root, body 169 | 170 | 171 | def nse(namespace, element): 172 | ''' 173 | Return a namespaced element based on the acs_settings.SOAP_NAMESPACES 174 | ''' 175 | if namespace=='cwmp' and namespace not in acs_settings.SOAP_NAMESPACES: 176 | # default to cwmp1.0 if we have no session to decide from 177 | return '{urn:dslforum-org:cwmp-1-0}%s' % element 178 | return '{%s}%s' % (acs_settings.SOAP_NAMESPACES[namespace], element) 179 | 180 | 181 | def add_pvs(element, key, value): 182 | ''' 183 | Given an etree Element, a key, and a value; 184 | add_pvs() will add a cwmp:ParameterValueStruct (without the namespace) 185 | containing a Name and a Value element. Only supports int bool and str/unicode for now. 186 | ''' 187 | struct = etree.SubElement(element, 'ParameterValueStruct') 188 | nameobj = etree.SubElement(struct, 'Name') 189 | nameobj.text = key 190 | valueobj = etree.SubElement(struct, 'Value') 191 | 192 | if isinstance(value, bool): 193 | valueobj.set(nse('xsi', 'type'), "xsd:boolean") 194 | valueobj.text = "true" if value else "false" 195 | elif isinstance(value, int): 196 | valueobj.set(nse('xsi', 'type'), "xsd:unsignedInt") 197 | valueobj.text = str(value) 198 | elif isinstance(value, str) or isinstance(value, str): 199 | valueobj.set(nse('xsi', 'type'), "xsd:string") 200 | valueobj.text = value 201 | 202 | def add_pas(element, name, value): 203 | ''' 204 | Given an etree Element, a name and a value; 205 | add_pas() will add a cwmp:SetParameterAttributeStruct (without the namespace, according to spec) 206 | containing a Name and Notification and a few more elements. 207 | ''' 208 | struct = etree.SubElement(element, 'SetParameterAttributeStruct') 209 | # Name element 210 | nameobj = etree.SubElement(struct, 'Name') 211 | nameobj.text = name 212 | 213 | # Notification element 214 | notifobj = etree.SubElement(struct, 'Notification') 215 | #notifobj.set(nse('xsi', 'type'), "xsd:unsignedInt") 216 | notifobj.text = str(value) 217 | 218 | # NotificationChange element 219 | notifchobj = etree.SubElement(struct, 'NotificationChange') 220 | #notifchobj.set(nse('xsi', 'type'), "xsd:boolean") 221 | notifchobj.text = "true" 222 | 223 | # AccessListChange element 224 | notifchobj = etree.SubElement(struct, 'AccessListChange') 225 | #notifchobj.set(nse('xsi', 'type'), "xsd:boolean") 226 | notifchobj.text = "false" 227 | 228 | # AccessList element 229 | notifchobj = etree.SubElement(struct, 'AccessList') 230 | notifchobj.set(nse('soap-enc', 'arrayType'), "xsd:string[0]") 231 | 232 | -------------------------------------------------------------------------------- /acs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | import acs.views 4 | 5 | app_name = 'acs' 6 | 7 | urlpatterns = [ 8 | path('', acs.views.AcsServerView.as_view(), name='acs_server'), 9 | ] 10 | 11 | -------------------------------------------------------------------------------- /acs/utils.py: -------------------------------------------------------------------------------- 1 | import io, paramiko, re 2 | 3 | from django.apps import apps 4 | 5 | from radius.models import Radacct 6 | from acs.conf import acs_settings 7 | 8 | 9 | def create_xml_document(xml): 10 | """ 11 | Takes some bytes representing an XML document and returns an instance 12 | of the configured XML storage model. 13 | """ 14 | return get_xml_storage_model().objects.create_xml_document(xml=xml) 15 | 16 | 17 | def get_xml_storage_model(): 18 | """ 19 | Return the configured xml storage model class using apps.get_model() 20 | """ 21 | app, model = acs_settings.XML_STORAGE_MODEL.split(".") 22 | return apps.get_model( 23 | app_label=app, 24 | model_name=model 25 | ) 26 | 27 | 28 | def get_value_from_parameterlist(parameterlist, key): 29 | ''' 30 | Uses lxml.etree xpath to extract the text inside a Value element, 31 | given the 'lookup key' inside the Name element, and the following XML structure: 32 | 33 | 34 | Device.DeviceInfo.HardwareVersion 35 | TW_0.7 36 | 37 | 38 | Device.DeviceInfo.SoftwareVersion 39 | 1.23.4.6.2969 40 | 41 | 42 | Device.DeviceInfo.ProvisioningCode 43 | 44 | 45 | 46 | ''' 47 | elementlist = parameterlist.xpath('.//Name[text()="%s"]/following-sibling::Value' % key) 48 | if elementlist: 49 | element = elementlist[0] 50 | else: 51 | return False 52 | 53 | # return int() for integers 54 | if element.attrib['{http://www.w3.org/2001/XMLSchema-instance}type'] == 'xsd:unsignedInt': 55 | return int(element.text) 56 | else: 57 | return element.text 58 | 59 | 60 | def run_ssh_command(server, username, private_key, command): 61 | try: 62 | private_key_file = io.StringIO(private_key) 63 | private_key = paramiko.RSAKey.from_private_key(private_key_file) 64 | ssh = paramiko.SSHClient() 65 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 66 | ssh.connect(server, username=username, pkey=private_key, timeout=15) 67 | stdin, stdout, stderr = ssh.exec_command(command) 68 | exit_status = stdout.channel.recv_exit_status() 69 | ssh.close() 70 | except Exception as E: 71 | return { 72 | 'result': False, 73 | 'exception': E, 74 | } 75 | 76 | return { 77 | 'result': True, 78 | 'output': stdout.readlines(), 79 | 'errorlines': stderr.readlines(), 80 | 'exit_status': exit_status, 81 | } 82 | 83 | def get_datamodel_from_devicesummary(summary): 84 | """ 85 | Regex to return "Device:1.0" from "Device:1.0[](Baseline:1), ABCService:1.0[1](Baseline:1), XYZService:1.0[1](Baseline:1)" 86 | """ 87 | match = re.search('(.*:\d.\d)\[\]', summary) 88 | if match: 89 | return match.group(1) 90 | else: 91 | return False 92 | 93 | -------------------------------------------------------------------------------- /acs/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lxml import etree 3 | from ipware.ip import get_ip 4 | from defusedxml.lxml import fromstring 5 | from datetime import timedelta 6 | 7 | from django.views.decorators.csrf import csrf_exempt 8 | from django.utils.decorators import method_decorator 9 | from django.views.generic import View 10 | from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseServerError 11 | from django.conf import settings 12 | from django.utils import timezone 13 | from django.utils.dateparse import parse_datetime 14 | from django.db.models import F 15 | 16 | from .models import * 17 | from .utils import get_value_from_parameterlist, create_xml_document 18 | from .response import nse, get_soap_envelope 19 | from .conf import acs_settings 20 | 21 | 22 | class AcsServerView(View): 23 | @method_decorator(csrf_exempt) 24 | def dispatch(self, *args, **kwargs): 25 | ''' 26 | Like a general in a war, this dispatch method is only here to get decorated 27 | ''' 28 | return super(AcsServerView, self).dispatch(*args, **kwargs) 29 | 30 | def post(self, request, *args, **kwargs): 31 | ### get the client IP from the request 32 | ip = get_ip(request) 33 | informinterval = acs_settings.INFORM_INTERVAL 34 | 35 | ### check if we have an acs session id in a cookie 36 | if 'acs_session_id' in request.COOKIES: 37 | hexid = request.COOKIES['acs_session_id'] 38 | try: 39 | acs_session = AcsSession.objects.get(acs_session_id=hexid) 40 | acs_session.acs_log("got acs_session_id from acs_session_id cookie") 41 | except AcsSession.DoesNotExist: 42 | ### create a new AcsSession? only if we haven't already got enough sessions from this client ip 43 | sessions_since_informinterval = AcsSession.objects.filter( 44 | client_ip=ip, 45 | created_date__gt=timezone.now()-timedelta(seconds=informinterval), 46 | ).count() 47 | 48 | if sessions_since_informinterval > acs_settings.INFORM_LIMIT_PER_INTERVAL: 49 | message = "acs session DENIED: the IP %s already has %s sessions the last %s seconds, no thanks (limit is %s)" % (ip, sessions_since_informinterval, informinterval, acs_settings.INFORM_LIMIT_PER_INTERVAL) 50 | print(message) 51 | return HttpResponse(status=420) 52 | 53 | acs_session = AcsSession.objects.create( 54 | client_ip=ip, 55 | ) 56 | hexid = acs_session.hexid 57 | acs_session.acs_log("got invalid acs_session_id %s from acs_session_id cookie, new acs session created" % request.COOKIES['acs_session_id']) 58 | else: 59 | ### no acs_session_id cookie seen, create a new AcsSession? only if we haven't already got enough sessions from this client ip 60 | sessions_since_informinterval = AcsSession.objects.filter( 61 | client_ip=ip, 62 | created_date__gt=timezone.now()-timedelta(seconds=informinterval), 63 | ).count() 64 | 65 | if sessions_since_informinterval > acs_settings.INFORM_LIMIT_PER_INTERVAL: 66 | message = "acs session DENIED: the IP %s already has %s sessions the last %s seconds, no thanks (limit is %s)" % (ip, sessions_since_informinterval, informinterval, acs_settings.INFORM_LIMIT_PER_INTERVAL) 67 | print(message) 68 | return HttpResponse(status=420) 69 | 70 | acs_session = AcsSession.objects.create( 71 | client_ip=ip, 72 | ) 73 | ### and save the acs session ID (uuid.hex()) in the django session for later use 74 | hexid = acs_session.acs_session_id.hex 75 | acs_session.acs_log("created new acs session (had %s sessions in the latest informinterval)" % sessions_since_informinterval) 76 | 77 | ### do we have a body in this http request? attempt parsing it as XML if so 78 | validxml=False 79 | if request.body: 80 | try: 81 | xmlroot = fromstring(request.body) 82 | validxml=True 83 | except Exception as E: 84 | acs_session.acs_log('got exception parsing ACS XML: %s' % E) 85 | 86 | ### get all HTTP headers for this request 87 | headerdict = {} 88 | for key, value in request.META.items(): 89 | ### in django all HTTP headers are prefixed with HTTP_ in request.META 90 | if key[:5] == 'HTTP_': 91 | headerdict[key] = value 92 | 93 | ### save this HTTP request to DB 94 | acs_http_request = AcsHttpRequest.objects.create( 95 | acs_session=acs_session, 96 | request_headers=json.dumps(headerdict), 97 | request_xml_valid=validxml, 98 | fk_body=create_xml_document(xml=request.body), 99 | ) 100 | acs_session.acs_log("saved acs http request %s to db" % acs_http_request) 101 | 102 | if request.body: 103 | ### bail out if we have a bad xml body 104 | if not validxml: 105 | message = 'Invalid XML body posted by client %s' % ip 106 | acs_session.acs_log(message) 107 | return HttpResponseBadRequest(message) 108 | 109 | ### figure out which cwmp version we are speaking (if any) 110 | if not 'cwmp' in xmlroot.nsmap: 111 | message = 'No cwmp namespace found in soap envelope, this is not a valid CWMP request posted by client %s' % ip 112 | acs_session.acs_log(message) 113 | return HttpResponseBadRequest(message) 114 | else: 115 | acs_session.cwmp_namespace = xmlroot.nsmap['cwmp'] 116 | acs_session.save() 117 | 118 | ### parse soap header and body 119 | soap_header = xmlroot.find('soap-env:Header', acs_session.soap_namespaces) 120 | soap_body = xmlroot.find('soap-env:Body', acs_session.soap_namespaces) 121 | if soap_body is None: 122 | # a soap body is required.. 123 | message = 'Unable to find SOAP body in xml posted by client %s' % ip 124 | acs_session.acs_log(message) 125 | return HttpResponseBadRequest(message) 126 | 127 | if soap_header is not None: 128 | ### parse the cwmp id from the soap header 129 | acs_http_request.cwmp_id = soap_header.find('cwmp:ID', acs_session.soap_namespaces).text 130 | 131 | ### do we have exactly one soap object in this soap body? 132 | if len(list(soap_body)) != 1: 133 | acs_http_request.save() 134 | message = 'Only one cwmp object per soap envelope please (client: %s)' % ip 135 | acs_session.acs_log(message) 136 | return HttpResponseBadRequest(message) 137 | else: 138 | ### this appears (for now) to be a valid soap envelope 139 | acs_http_request.request_soap_valid = True 140 | 141 | ### get the soap element in the format {namespace}Method 142 | acs_http_request.soap_element = list(soap_body)[0].tag 143 | 144 | else: 145 | ### empty request body, this means that the CPE is done for now 146 | acs_http_request.cwmp_id = '' 147 | acs_http_request.soap_element = '{%s}(empty request body)' % acs_http_request.acs_session.soap_namespaces['cwmp'] 148 | 149 | ### save the http request 150 | acs_http_request.save() 151 | 152 | ################# http request saved to acs session, now we have to put a response together ################## 153 | ################# at this point we still have not associated the acs session with an acs device, ############# 154 | ################# and we can only do so if we have a valid inform with vendor, serial etc. for the device #### 155 | if not acs_session.acs_device: 156 | # we only permit Inform requests when we have no device 157 | if acs_http_request.cwmp_rpc_method != "Inform": 158 | message = 'An ACS session must begin with an Inform, not %s' % acs_http_request.cwmp_rpc_method 159 | acs_session.acs_log(message) 160 | return HttpResponseBadRequest(message) 161 | 162 | ### initialize a variable 163 | empty_response=False 164 | ### first things first, do we have a body in the http request? 165 | if request.body: 166 | if acs_http_request.cwmp_rpc_method in settings.CWMP_ACS_VALID_RPC_METHODS: 167 | #################################################################################################### 168 | acs_session.acs_log('the ACS client %s is calling a valid RPC method on the ACS server: %s' % (ip, acs_http_request.cwmp_rpc_method)) 169 | 170 | ### get SOAP response envelope 171 | root, body = get_soap_envelope(acs_http_request.cwmp_id, acs_session) 172 | 173 | ### set a few variables used when saving the HTTP response to db 174 | response_cwmp_rpc_method = '%sResponse' % acs_http_request.cwmp_rpc_method 175 | response_cwmp_id = acs_http_request.cwmp_id 176 | 177 | ### parse the soap request (which ACS RPC method is the CPE calling?) 178 | if acs_http_request.cwmp_rpc_method == 'Inform': 179 | ### get Inform xml element 180 | inform = soap_body.find('cwmp:Inform', acs_session.soap_namespaces) 181 | 182 | ### determine which data model version this device is using 183 | datamodel, created = CwmpDataModel.objects.get_or_create( 184 | name=acs_session.determine_data_model(inform) 185 | ) 186 | acs_session.acs_log("ACS client is using data model %s" % datamodel) 187 | acs_session.root_data_model = datamodel 188 | 189 | ######################################################################### 190 | ### get deviceid element from Inform request 191 | deviceid = inform.find('DeviceId') 192 | if deviceid is None: 193 | message = 'Invalid Inform, DeviceID missing from request %s' % request 194 | acs_session.acs_log(message) 195 | return HttpResponseBadRequest(message) 196 | 197 | serial = deviceid.find('SerialNumber').text 198 | if not serial: 199 | message = 'Invalid Inform, SerialNumber missing from request %s' % request 200 | acs_session.acs_log(message) 201 | return HttpResponseBadRequest(message) 202 | 203 | vendor = deviceid.find('Manufacturer').text 204 | if not vendor: 205 | message = 'Invalid Inform, Manufacturer missing from request %s' % request 206 | acs_session.acs_log(message) 207 | return HttpResponseBadRequest(message) 208 | 209 | model = deviceid.find('ProductClass').text 210 | if not model: 211 | message = 'Invalid Inform, ProductClass missing from request %s' % request 212 | acs_session.acs_log(message) 213 | return HttpResponseBadRequest(message) 214 | 215 | oui = deviceid.find('OUI').text 216 | if not oui: 217 | message = 'Invalid Inform, OUI missing from request %s' % request 218 | acs_session.acs_log(message) 219 | return HttpResponseBadRequest(message) 220 | 221 | ### find or create acs devicevendor (using Manufacturer and OUI) 222 | acs_devicevendor, created = AcsDeviceVendor.objects.get_or_create( 223 | name = vendor, 224 | oui = oui, 225 | ) 226 | 227 | ### find or create acs devicetype (using ProductClass) 228 | acs_devicemodel, created = AcsDeviceModel.objects.get_or_create( 229 | vendor = acs_devicevendor, 230 | name = model, 231 | ) 232 | 233 | ### find or create acs device (using serial number and acs devicetype) 234 | acs_device, created = AcsDevice.objects.get_or_create( 235 | model = acs_devicemodel, 236 | serial = serial 237 | ) 238 | 239 | ### set latest session result to False and increase inform count 240 | acs_device.acs_latest_session_result = False 241 | acs_device.acs_inform_count = F('acs_inform_count') + 1 242 | acs_device.save() 243 | 244 | # save acs_device to acs_session 245 | acs_session.acs_device = acs_device 246 | acs_session.save() 247 | 248 | # attempt acs device association 249 | if not acs_device.get_related_device(): 250 | acs_device.associate_with_related_device() 251 | 252 | if not acs_device.acs_xmpp_password: 253 | acs_device.create_xmpp_user() 254 | 255 | if not acs_device.acs_connectionrequest_password: 256 | acs_device.create_connreq_password() 257 | 258 | if not acs_session.get_inform_eventcodes(inform, acs_http_request): 259 | # the event section is missing from this Inform 260 | return HttpResponseBadRequest() 261 | 262 | ######################################################### 263 | # refresh from db to make any changes above visible 264 | acs_device.refresh_from_db() 265 | 266 | # if this acs device is associated with a real device we can call that devices verify_acs_client_ip() method 267 | # and possibly mark this acs session as client_ip_verified=True (which is required before we give out any secrets like ssid in the session) 268 | if acs_device.get_related_device(): 269 | ### run acs pre-ip-verified session hook 270 | acs_device.get_related_device().acs_session_pre_verify_hook() 271 | 272 | # set acs_session.client_ip_verified based on the outcome of verify_acs_client_ip(acs_session.client_ip) 273 | acs_session.client_ip_verified = acs_device.get_related_device().verify_acs_client_ip(acs_session.client_ip) 274 | message = "client_ip_verified set to %s after running acs_device.get_related_device().verify_acs_client_ip(%s)" % (acs_session.client_ip_verified, acs_session.client_ip) 275 | acs_session.acs_log(message) 276 | acs_session.save() 277 | 278 | ### run acs post-ip-verified session hook 279 | acs_device.get_related_device().acs_session_post_verify_hook() 280 | 281 | # refresh from db to make any changes above visible 282 | acs_device.refresh_from_db() 283 | 284 | ########################################################## 285 | ### this is a good place to check for different Inform EventCodes or use 286 | ### other data from the Inform 287 | 288 | # first we clean up any old unprocessed automatic jobs. 289 | # these might be lingering from earlier sessions that may have failed (for any number of reasons) 290 | deleted, info = acs_session.acs_device.acs_queue_jobs.filter(automatic=True, processed=False).delete() 291 | if deleted: 292 | acs_session.acs_log("Cleanup: Deleted %s old unprocessed automatic AcsQueueJobs for this device" % deleted) 293 | 294 | ### get parameterlist from the Inform payload 295 | parameterlist = inform.find('ParameterList') 296 | 297 | ### update current_config_level from Device.ManagementServer.ParameterKey 298 | parameterkey = get_value_from_parameterlist(parameterlist, acs_session.get_acs_parameter_name('django_acs.acs.parameterkey')) 299 | if not parameterkey: 300 | acs_device.current_config_level = None 301 | else: 302 | acs_device.current_config_level = parse_datetime(parameterkey) 303 | 304 | ### update latest_inform time 305 | acs_device.acs_latest_inform = timezone.now() 306 | 307 | ### update current_software_version 308 | acs_device.current_software_version = get_value_from_parameterlist(parameterlist, acs_session.get_acs_parameter_name('django_acs.deviceinfo.softwareversion')) 309 | 310 | ### save acs device 311 | acs_device.save() 312 | 313 | ############################################### 314 | ### This is where we do things we want do _after_ an Inform session. 315 | ### Queue jobs here before sending InformResponse and they will be run in the same session. 316 | 317 | # queue GetParameterNames, GetParameterValues, GetParameterAttributes 318 | if not acs_session.collect_device_info("Collecting information triggered by Inform"): 319 | # unable to queue neccesary job 320 | return HttpResponseServerError() 321 | 322 | ## Queue a firmware upgrade job? 323 | if not acs_session.device_firmware_upgrade(): 324 | # we wanted to queue a firmware upgrade job, but failed 325 | return HttpResponseServerError() 326 | 327 | ############################################### 328 | ### we are done processing the Inform RPC request, and ready to return the InformResponse, 329 | ### so add the outer response element 330 | cwmp = etree.SubElement(body, nse('cwmp', 'InformResponse')) 331 | ### add the inner response elements, without namespace (according to cwmp spec!) 332 | maxenv = etree.SubElement(cwmp, 'MaxEnvelopes') 333 | maxenv.text = '1' 334 | 335 | elif acs_http_request.cwmp_rpc_method == 'TransferComplete': 336 | ### handle TransferComplete RPC call 337 | cwmp = etree.SubElement(body, nse('cwmp', 'TransferCompleteResponse')) 338 | 339 | else: 340 | message = 'Unimplemented cwmp method %s called by the client %s' % (acs_http_request.cwmp_rpc_method, acs_device) 341 | acs_session.acs_log(message) 342 | return HttpResponseBadRequest(message) 343 | 344 | ##################################################################################################### 345 | ### we are done processing the http request, put HTTP response together 346 | output = etree.tostring(root, encoding='utf-8', xml_declaration=True) 347 | response = HttpResponse(output, content_type='text/xml; charset=utf-8') 348 | 349 | ### save the HTTP response 350 | acs_http_response = AcsHttpResponse.objects.create( 351 | http_request=acs_http_request, 352 | fk_body=create_xml_document(xml=response.content), 353 | cwmp_id=response_cwmp_id, 354 | soap_element="{%s}%s" % (acs_session.soap_namespaces['cwmp'], response_cwmp_rpc_method), 355 | rpc_response_to=acs_http_request, 356 | ) 357 | acs_session.acs_log("responding to CPE %s with %s" % (acs_session.acs_device, response_cwmp_rpc_method)) 358 | 359 | elif acs_http_request.cwmp_rpc_method and acs_http_request.cwmp_rpc_method[:-8] in settings.CWMP_CPE_VALID_RPC_METHODS: 360 | ##################################################################################################### 361 | acs_session.acs_log('the CPE %s is responding to an RPC call from the ACS: %s' % (acs_session.acs_device, acs_http_request.cwmp_rpc_method)) 362 | ### first link this http request to the related rpc request (which is in a http response), 363 | ### find it by looking for the same rpc method and cwmp id in http responses in this acs session 364 | match = False 365 | for httpresponse in acs_session.acs_http_responses: 366 | if httpresponse.cwmp_rpc_method == acs_http_request.cwmp_rpc_method[:-8] and httpresponse.cwmp_id == acs_http_request.cwmp_id: 367 | acs_http_request.rpc_response_to = httpresponse 368 | acs_http_request.save() 369 | match = True 370 | if not match: 371 | message = 'Unable to find the HTTP response containing the RPC request being responded to :(' 372 | acs_session.acs_log(message) 373 | return HttpResponseServerError(message) 374 | 375 | ### parse the cwmp object from the soap body 376 | rpcresponsexml = soap_body.find('cwmp:%s' % acs_http_request.cwmp_rpc_method, acs_session.soap_namespaces) 377 | 378 | if acs_http_request.cwmp_rpc_method == 'GetParameterNamesResponse': 379 | ### do nothing for now, the response will be used when the GetParameterValuesResponse comes in later 380 | pass 381 | 382 | elif acs_http_request.cwmp_rpc_method == 'GetParameterValuesResponse': 383 | # nothing here for now 384 | pass 385 | 386 | elif acs_http_request.cwmp_rpc_method == 'GetParameterAttributesResponse': 387 | # this is a GetParameterAttributesResponse, attempt to update the device acs parameters 388 | if acs_session.acs_device.update_acs_parameters(acs_http_request): 389 | ################################################################################################# 390 | ### this is where we do things to and with the recently fetched acs parameters from the device, 391 | ### like configuring the device or handling user config changes 392 | ### Queue jobs here before sending GetParameterAttributesResponse and they will be run in the same session. 393 | 394 | # extract device uptime from acs_device.acs_parameters and save it to acs_session.device_uptime 395 | acs_session.update_device_uptime() 396 | 397 | # check if we need to call the handle_user_config_changes() method on the acs_device, 398 | # we only check for user changes if a device has been configured by us already, and doesn't need any more config at the moment 399 | if acs_session.acs_device.current_config_level and acs_session.acs_device.current_config_level > acs_session.acs_device.get_desired_config_level(): 400 | # device is already configured, and doesn't need additional config from us right now, so check if the user changed anything on the device, and act accordingly 401 | acs_session.acs_device.handle_user_config_changes() 402 | 403 | # refresh to get any changes from above 404 | acs_session.refresh_from_db() 405 | 406 | # if this device has been reconfigured in this session we collect data again, 407 | # if not, we reconfigure it if needed 408 | if acs_session.configuration_done: 409 | # device has been configured, so collect data again so we have the latest (unless we have already done so) 410 | if not acs_session.post_configuration_collection_done: 411 | if not acs_session.collect_device_info(reason="Device has been reconfigured"): 412 | acs_session.acs_log("Unable to queue one or more jobs to collect info after configuration") 413 | return HttpResponseServerError() 414 | else: 415 | # this device has not been configured in this ACS session. This is where we check if we need to configure it now. 416 | # acs_session.configure_device returns False if there was a problem configuring the device, and true if 417 | # the device was configured, or did not need to be configured 418 | if not acs_session.configure_device(): 419 | # there was a problem creating configure jobs for the device 420 | return HttpResponseServerError() 421 | 422 | elif acs_http_request.cwmp_rpc_method == 'GetRPCMethodsResponse': 423 | pass 424 | 425 | elif acs_http_request.cwmp_rpc_method == 'SetParameterValuesResponse': 426 | ### find status 427 | status = rpcresponsexml.find('Status').text 428 | if status != '0': 429 | ### ACS client failed to apply all our settings, fuckery is afoot! 430 | message = 'The ACS device %s failed to apply our SetParameterValues settings, something is wrong!' % acs_device 431 | acs_session.acs_log(message) 432 | return HttpResponseBadRequest(message) 433 | 434 | ### find the parameterkey and update the acs_device so we know its current_config_level 435 | ### since this is a SetParameterValuesResponse we will probably get settings.CWMP_CONFIG_INCOMPLETE_PARAMETERKEY_DATE here, 436 | ### which is fine(tm) 437 | parameterkey = acs_http_request.rpc_response_to.soap_body.find('cwmp:SetParameterValues', acs_session.soap_namespaces).find('ParameterKey').text 438 | acs_session.acs_device.current_config_level = parse_datetime(parameterkey) 439 | 440 | elif acs_http_request.cwmp_rpc_method == 'SetParameterAttributesResponse': 441 | ### find the parameterkey and update the acs_device so we know its current_config_level 442 | parameterkey = acs_http_request.rpc_response_to.soap_body.find('cwmp:SetParameterAttributes', acs_session.soap_namespaces).find('ParameterKey').text 443 | acs_session.acs_device.current_config_level = parse_datetime(parameterkey) 444 | # in case we have a local desired_config_level on the acs device, unset it now as the configuration has been done 445 | if acs_session.acs_device.desired_config_level: 446 | acs_session.acs_device.desired_config_level = None 447 | acs_session.acs_device.save() 448 | 449 | elif acs_http_request.cwmp_rpc_method == 'FactoryResetResponse': 450 | empty_response=True 451 | 452 | ### we are done processing the clients response, do we have anything else? 453 | response = acs_http_request.get_response(empty_response=empty_response) 454 | else: 455 | ##################################################################################################### 456 | ### TODO: insert some code to handle soapfault here so we dont hit the "Unknown cwmp object/method" bit below when a soapfault happens 457 | 458 | ##################################################################################################### 459 | acs_session.acs_log('unknown cwmp object/method received from %s: %s' % (acs_session.acs_device, acs_http_request.cwmp_rpc_method)) 460 | return HttpResponseBadRequest('unknown cwmp object/method received') 461 | 462 | else: 463 | # this http request has an empty body 464 | acs_session.acs_log('the CPE %s is done and posted an empty body to the ACS' % acs_session.acs_device) 465 | ### get a response for the client - if we have nothing queued it will be an empty response 466 | response = acs_http_request.get_response() 467 | 468 | ### all done, update the acs session with result before returning response 469 | acs_session.update_session_result() 470 | 471 | ### set the acs session cookie 472 | # we have to set this cookie manually because some stupid ACS client cannot parse expires in a http cookie 473 | # and Django always sets exipires in cookies, no even it the expires argument is set to None, 474 | # to be compatible with old IE clients yay 475 | #response.set_cookie(key='acs_session_id', value=max_age=60, expires=None, path='/') 476 | response['Set-Cookie'] = "acs_session_id=%s; Max-Age=60; Path=/" % hexid 477 | return response 478 | 479 | -------------------------------------------------------------------------------- /acs/workers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acs/workers/__init__.py -------------------------------------------------------------------------------- /acs/workers/acs_http_connreq_worker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.utils import timezone 4 | from acs.models import AcsQueueJob 5 | 6 | logger = logging.getLogger('django_acs.%s' % __name__) 7 | 8 | def do_work(): 9 | """ 10 | The acs_http_connreq_worker loops through unprocessed AcsQueueJob entries marked as urgent and does a HTTP ConnectionRequest for each unique ACS device found 11 | """ 12 | for aqj in AcsQueueJob.objects.filter(urgent=True, processed=False, notification_sent=False).order_by('acs_device', 'created_date').distinct('acs_device'): 13 | logger.info('Processing HTTP connectionrequest for acs device %s (because job %s is urgent)' % (aqj.acs_device, aqj.tag)) 14 | 15 | # do the http connection request 16 | r = aqj.acs_device.acs_http_connection_request() 17 | if not r: 18 | logger.info('Unable to send HTTP connectionrequest for acs device %s (job %s is urgent)' % (aqj.acs_device, aqj.tag)) 19 | else: 20 | logger.info('Called ACS ConnectionRequest url for acs device %s because job %s is urgent: result HTTP %s' % (aqj.wifidevice, aqj.tag, r.status_code)) 21 | # all good, update and save aqj 22 | aqj.notification_sent=True 23 | aqj.save() 24 | 25 | -------------------------------------------------------------------------------- /acsadmin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acsadmin/__init__.py -------------------------------------------------------------------------------- /acsadmin/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class AcsConfig(AppConfig): 4 | name = 'acsadmin' 5 | 6 | -------------------------------------------------------------------------------- /acsadmin/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | from defusedxml.lxml import fromstring 4 | from django.core.exceptions import ValidationError 5 | 6 | class AcsDeviceActionForm(forms.Form): 7 | action = forms.ChoiceField( 8 | widget=forms.RadioSelect, 9 | choices=[(x, x) for x in settings.CWMP_CPE_VALID_RPC_METHODS] 10 | ) 11 | 12 | xml = forms.CharField( 13 | widget=forms.Textarea, 14 | label="XML payload", 15 | help_text="Optionally (when needed) input the XML payload of the RPC call.", 16 | required=False 17 | ) 18 | 19 | reason = forms.CharField( 20 | label="Reason", 21 | help_text="Please specify the reason why you are adding this ACS RPC call", 22 | ) 23 | 24 | urgent = forms.BooleanField( 25 | label="Urgent?", 26 | help_text='Check to make this job urgent. Leave unchecked to wait until next inform.', 27 | required=False 28 | ) 29 | 30 | def clean(self): 31 | cleaned_data = super(AcsDeviceActionForm, self).clean() 32 | errorlist = [] 33 | if cleaned_data['xml']: 34 | try: 35 | # fromstring takes bytes, so convert the string from our form to bytes, utf-8 encoded of course 36 | xmlroot = fromstring(cleaned_data['xml'].encode('utf-8')) 37 | except Exception as E: 38 | errorlist.append(ValidationError('XML not valid: %s' % E, code='invalid_xml')) 39 | 40 | # any errors to report? 41 | if errorlist: 42 | raise ValidationError(errorlist) 43 | 44 | return cleaned_data 45 | 46 | 47 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_category_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_category_detail_panel.html' with acs_device_category=object %} 5 | {% include 'includes/acs_device_model_list_panel.html' with acs_device_model_list=object.acsdevicemodels.all headline='ACS Device Models Under This Category' %} 6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_category_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS Device Categories{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_category_list_panel.html' with acs_device_category_list=object_list %} 5 | {% endblock content %} 6 | 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_detail_panel.html' with acs_device=object %} 5 | {% include 'includes/acs_device_parameters_panel.html' with acs_device=object collapsed=True %} 6 | {% include 'includes/acs_session_list_panel.html' with acs_device=object acs_session_list=object.acs_sessions.all|slice:"10" included=True %} 7 | {% include 'includes/acs_queue_job_list_panel.html' with acs_queue_job_list=object.unprocessed_acs_queue_jobs.all headline="All unprocessed ACS queue jobs for this acs device" %} 8 | {% endblock content %} 9 | 10 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS Devices{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_list_panel.html' with acs_device_list=object_list %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_model_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_model_detail_panel.html' with acs_device_model=object %} 5 | {% include 'includes/acs_device_list_panel.html' with acs_device_list=object.acsdevices.all headline='ACS Devices of This Model' %} 6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_model_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS Device Models{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_model_list_panel.html' with acs_device_model_list=object_list %} 5 | {% endblock content %} 6 | 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_vendor_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_vendor_detail_panel.html' with acs_device_vendor=object %} 5 | {% include 'includes/acs_device_model_list_panel.html' with acs_device_model_list=object.acsdevicemodels.all headline='ACS Device Models by This Vendor' %} 6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_device_vendor_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS Device Vendors{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_device_vendor_list_panel.html' with acs_device_vendor_list=object_list %} 5 | {% endblock content %} 6 | 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_http_request_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_http_request_detail_panel.html' with acs_http_request=object %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_http_request_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS HTTP Request List{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_http_request_list_panel.html' with acs_http_request_list=object_list %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_http_response_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_http_response_detail_panel.html' with acs_http_response=object %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_http_response_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS HTTP Response List{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_http_response_list_panel.html' with acs_http_response_list=object_list %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_queue_job_create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap3 %} 3 | {% block head_title %}Create ACS Queue Job{% endblock head_title %} 4 | {% block extra_head %} 5 | {% endblock extra_head %} 6 | {% block content %} 7 |
8 |

Create ACS Queue Job

9 |
10 |
11 | {% csrf_token %} 12 | {% bootstrap_form form %} 13 | {% include 'includes/submit-button.html' %} 14 |
15 |
16 |
17 | 18 | {% endblock content %} 19 | 20 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_queue_job_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_queue_job_detail_panel.html' with aqj=object %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_queue_job_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS Queue Job List{% endblock head_title %} 3 | {% block content %} 4 |

5 | {% url 'acs_queue_job_list' as show_all_url %} 6 | {% include 'includes/button.html' with url=show_all_url icon='fa-list' buttontext='Show All' %} 7 | 8 | {% url 'acs_queue_job_list_hideautomatic' as hide_automatic_url %} 9 | {% include 'includes/button.html' with url=hide_automatic_url icon='fa-list' buttontext='Hide Automatic' %} 10 |

11 | {% include 'includes/acs_queue_job_list_panel.html' with acs_queue_job_list=object_list %} 12 | {% endblock content %} 13 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_session_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}{{ object }}{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_session_detail_panel.html' with acs_session=object %} 5 | {% include 'includes/acs_http_conversation_panel.html' with acs_http_conversationlist=object.acs_http_conversationlist headline='ACS Session HTTP Conversation' %} 6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /acsadmin/templates/acs_session_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}ACS Session List{% endblock head_title %} 3 | {% block content %} 4 | {% include 'includes/acs_session_list_panel.html' with acs_session_list=object_list included=False %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_category_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS Device Category Details for "|addstr:acs_device_category.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_device_category_detail' pk=acs_device_category.id as acs_device_category_detail_url %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
ACS Device Category{% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %} {{ acs_device_category.tag }}
Name{{ acs_device_category.name }}
18 |
19 |
20 |
21 | {% include 'includes/detail_panel_footer.html' %} 22 |
23 | 24 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_category_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsDeviceCategoryList' headline=headline|default:"ACS Device Category List" %} 3 | {% if acs_device_category_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for acs_device_category in acs_device_category_list %} 18 | {% url 'acsadmin:acs_device_category_detail' pk=acs_device_category.id as acs_device_category_detail_url %} 19 | 20 | 21 | 22 | 23 | 26 | 27 | {% endfor %} 28 | 29 |
ACS Device CategoryNameModelsActions
{% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %}{{ acs_device_category.tag }}{{ acs_device_category.name }}{{ acs_device_category.acsdevicemodels.count }} 24 | {% include 'includes/details-button.html' with url=acs_device_category_detail_url %} 25 |
30 | {% if is_paginated %} 31 | {% include 'includes/pagination-footer.html' %} 32 | {% endif %} 33 | {% else %} 34 |

No Acs Device Categories found!

35 | {% endif %} 36 | {% include 'includes/collapsable-panel-footer.html' %} 37 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS Device Details for "|addstr:acs_device.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_device_detail' pk=acs_device.id as acs_device_detail_url %} 8 | {% url 'acsadmin:acs_device_vendor_detail' pk=acs_device.model.vendor.id as acs_device_vendor_detail_url %} 9 | {% url 'acsadmin:acs_device_model_detail' pk=acs_device.model.id as acs_device_model_detail_url %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
ACS Device{% include 'includes/small-details-button.html' with url=acs_device_detail_url %} {{ acs_device }}
Vendor{% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %} {{ acs_device.model.vendor }}
Model{% include 'includes/small-details-button.html' with url=acs_device_type_detail_url %} {{ acs_device.model }}
Serial{{ acs_device.serial }}
Related Device{% include 'includes/small-details-button.html' with url=acs_device.get_related_device.get_absolute_url %}{{ acs_device.get_related_device }}
Latest Inform{{ acs_device.acs_latest_inform | default:"N/A" }}
Latest Inform Result{{ acs_device.acs_latest_session_result }}
Number of Informs{{ acs_device.acs_inform_count }}
Current Config Level{{ acs_device.current_config_level | default:"N/A" }}
Desired Config Level{{ acs_device.get_desired_config_level | default:"N/A" }} {% if acs_device.desired_config_level %}(LOCAL OVERRIDE){% endif %}
Current Software Version{{ acs_device.current_software_version | default:"N/A" }}
Desired Software Version{{ acs_device.get_desired_software_version | default:"N/A" }} {% if acs_device.desired_software_version %}(LOCAL OVERRIDE){% endif %}
60 |
61 |
62 |
63 |
64 | {% url 'acsadmin:acs_queue_job_create' pk=acs_device.id as acs_queue_job_create_url %} 65 |

{% include 'includes/create-button.html' with url=acs_queue_job_create_url buttontext='Schedule ACS RPC Job' %}

66 |
67 |
68 |
69 |
70 |
71 | {% include 'includes/detail_panel_footer.html' %} 72 |
73 | 74 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsDeviceList' headline=headline|default:"ACS Device List" %} 3 | {% if acs_device_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for ad in acs_device_list %} 23 | {% url 'acsadmin:acs_device_detail' pk=ad.id as acsdevice_detail_url %} 24 | {% url 'acsadmin:acs_device_model_detail' pk=ad.model.id as acsdevicemodel_detail_url %} 25 | {% url 'acsadmin:acs_device_vendor_detail' pk=ad.model.vendor.id as acsdevicvendor_detail_url %} 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 44 | {% endfor %} 45 | 46 |
ACS DeviceModelVendorSerialRelated DeviceInformInformsConfig LevelActions
{% include 'includes/small-details-button.html' with url=acsdevice_detail_url %}{{ ad.tag }}{% include 'includes/small-details-button.html' with url=acsdevicemodel_detail_url %}{{ ad.model }}{% include 'includes/small-details-button.html' with url=acsdevicevendor_detail_url %}{{ ad.model.vendor }}{{ ad.serial }}{% if ad.get_related_device %} 32 | {% include 'includes/small-details-button.html' with url=ad.get_related_device.get_absolute_url %}{{ ad.get_related_device }} 33 | {% else %} 34 | N/A 35 | {% endif %} 36 | {{ ad.acs_latest_inform }} {{ ad.acs_latest_session_result|truefalseicon|safe }}{{ ad.acs_inform_count }}{{ ad.current_config_level }}{% if ad.desired_config_level %}(desired: {{ ad.desired_config_level }}){% endif %} 41 | {% include 'includes/details-button.html' with url=acsdevice_detail_url %} 42 |
47 | {% if is_paginated %} 48 | {% include 'includes/pagination-footer.html' %} 49 | {% endif %} 50 | {% else %} 51 |

No acs devices found!

52 | {% endif %} 53 | {% include 'includes/collapsable-panel-footer.html' %} 54 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_model_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS Device Model Details for "|addstr:acs_device_model.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_device_model_detail' pk=acs_device_model.id as acs_device_model_detail_url %} 8 | {% url 'acsadmin:acs_device_vendor_detail' pk=acs_device_model.vendor.id as acs_device_vendor_detail_url %} 9 | {% url 'acsadmin:acs_device_category_detail' pk=acs_device_model.category.id as acs_device_category_detail_url %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
ACS Device Model{% include 'includes/small-details-button.html' with url=acs_device_model_detail_url %} {{ acs_device_model.tag }}
Name{{ acs_device_model.name }}
Vendor{% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %} {{ acs_device_model.vendor }}
Device Category{% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %} {{ acs_device_model.category }}
Desired Config Level{{ acs_device_model.desired_config_level|default:"N/A" }}
Desired Software Version{{ acs_device_model.desired_software_version|default:"N/A" }}
36 |
37 |
38 |
39 | {% include 'includes/detail_panel_footer.html' %} 40 |
41 | 42 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_model_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsDeviceModelList' headline=headline|default:"ACS Device Model List" %} 3 | {% if acs_device_model_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for acs_device_model in acs_device_model_list %} 22 | {% url 'acsadmin:acs_device_model_detail' pk=acs_device_model.id as acs_device_model_detail_url %} 23 | {% url 'acsadmin:acs_device_vendor_detail' pk=acs_device_model.vendor.id as acs_device_vendor_detail_url %} 24 | {% url 'acsadmin:acs_device_category_detail' pk=acs_device_model.category.id as acs_device_category_detail_url %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | {% endfor %} 38 | 39 |
ACS Device ModelNameVendorCategoryDesired Config LevelDesired Software VersionDevicesActions
{% include 'includes/small-details-button.html' with url=acs_device_model_detail_url %}{{ acs_device_model.tag }}{{ acs_device_model.name }}{% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %}{{ acs_device_model.vendor }}{% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %}{{ acs_device_model.category }}{{ acs_device_model.desired_config_level|default:"N/A" }}{{ acs_device_model.desired_software_version|default:"N/A" }}{{ acs_device_model.acsdevices.count }} 34 | {% include 'includes/details-button.html' with url=acs_device_model_detail_url %} 35 |
40 | {% if is_paginated %} 41 | {% include 'includes/pagination-footer.html' %} 42 | {% endif %} 43 | {% else %} 44 |

No Acs Device Models found!

45 | {% endif %} 46 | {% include 'includes/collapsable-panel-footer.html' %} 47 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_parameters_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='WifiDeviceParametersList' headline=headline|default:"WiFi Device ACS Parameters Last Updated: "|addstr:acs_device.acs_parameters_time %} 3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for key, value in acs_device.acs_parameter_dict.items %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 | 32 |
ParameterTypeWritableNotificationAccess listValue
{{ key }}{{ value.type }}{{ value.writable }}{{ value.notification }}{{ value.accesslist }}{{ value.value | linebreaksbr }}
33 | 34 | 44 | 45 | {% include 'includes/collapsable-panel-footer.html' %} 46 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_vendor_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS Device Vendor Details for "|addstr:acs_device_vendor.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_device_vendor_detail' pk=acs_device_vendor.id as acs_device_vendor_detail_url %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
ACS Device Vendor{% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %} {{ acs_device_vendor.tag }}
Name{{ acs_device_vendor.name }}
OUI{{ acs_device_vendor.oui }}
22 |
23 |
24 |
25 | {% include 'includes/detail_panel_footer.html' %} 26 |
27 | 28 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_device_vendor_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsDeviceVendorList' headline=headline|default:"ACS Device Vendor List" %} 3 | {% if acs_device_vendor_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for acs_device_vendor in acs_device_vendor_list %} 19 | {% url 'acsadmin:acs_device_vendor_detail' pk=acs_device_vendor.id as acs_device_vendor_detail_url %} 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | {% endfor %} 30 | 31 |
ACS Device VendorNameMAC OUIModelsActions
{% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %}{{ acs_device_vendor }}{{ acs_device_vendor.name }}{{ acs_device_vendor.oui }}{{ acs_device_vendor.acsdevicemodels.count }} 26 | {% include 'includes/details-button.html' with url=acs_device_vendor_detail_url %} 27 |
32 | {% if is_paginated %} 33 | {% include 'includes/pagination-footer.html' %} 34 | {% endif %} 35 | {% else %} 36 |

No Acs Device Vendors found!

37 | {% endif %} 38 | {% include 'includes/collapsable-panel-footer.html' %} 39 | 40 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_http_conversation_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsHttpConversation' headline=headline|default:"ACS HTTP Conversation" %} 3 | {% if acs_http_conversationlist %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for acs_http in acs_http_conversationlist %} 24 | 25 | {% if acs_http.is_request %} 26 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http.id as acs_http_detail_url %} 27 | {% if acs_http.rpc_response_to %} 28 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http.rpc_response_to.id as response_to_detail_url %} 29 | {% endif %} 30 | {% if acs_http.rpc_response %} 31 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http.rpc_response.id as rpc_response_detail_url %} 32 | {% endif %} 33 | {% else %} 34 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http.id as acs_http_detail_url %} 35 | {% if acs_http.rpc_response_to %} 36 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http.rpc_response_to.id as response_to_detail_url %} 37 | {% endif %} 38 | {% if acs_http.rpc_response %} 39 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http.rpc_response.id as rpc_response_detail_url %} 40 | {% endif %} 41 | {% if acs_http.queuejob %} 42 | {% url 'acsadmin:acs_queue_job_detail' pk=acs_http.queuejob.id as queue_job_detail_url %} 43 | {% endif %} 44 | {% endif %} 45 | 48 | 55 | 56 | 57 | 58 | 59 | 66 | 73 | 80 | 83 | 84 | {% endfor %} 85 | 86 |
ReqrespDirectionWhenRPC MethodHeadersBodyRPC Response ToRPC ResponseACS Queue JobActions
46 | {% include 'includes/small-details-button.html' with url=acs_http_detail_url %}{{ acs_http }} 47 | 49 | {% if acs_http.is_request %} 50 | HTTP Request 51 | {% else %} 52 | HTTP response 53 | {% endif %} 54 | {{ acs_http.created_date }}{{ acs_http.soap_element_ns }}{% if acs_http.is_request %}{{ acs_http.request_headers|length }} bytes{% else %}N/A{% endif %}{{ acs_http.body|length }} bytes 60 | {% if acs_http.rpc_response_to %} 61 | {% include 'includes/small-details-button.html' with url=response_to_detail_url %}{{ acs_http.rpc_response_to.tag }} 62 | {% else %} 63 | N/A 64 | {% endif %} 65 | 67 | {% if acs_http.rpc_response %} 68 | {% include 'includes/small-details-button.html' with url=rpc_response_detail_url %}{{ acs_http.rpc_response.tag }} 69 | {% else %} 70 | N/A 71 | {% endif %} 72 | 74 | {% if acs_http.queuejob %} 75 | {% include 'includes/small-details-button.html' with url=queue_job_detail_url %}{{ acs_http.queuejob.tag }} 76 | {% else %} 77 | N/A 78 | {% endif %} 79 | 81 | {% include 'includes/details-button.html' with url=acs_http_detail_url %} 82 |
87 | {% if is_paginated %} 88 | {% include 'includes/pagination-footer.html' %} 89 | {% endif %} 90 | {% else %} 91 |

No ACS HTTP Requests Found!

92 | {% endif %} 93 | {% include 'includes/collapsable-panel-footer.html' %} 94 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_http_request_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS HTTP Request Details for "|addstr:acs_http_request.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_request.id as acs_http_request_detail_url %} 8 | {% url 'acsadmin:acs_session_detail' pk=acs_http_request.acs_session.id as acs_session_detail_url %} 9 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_request.acs_http_response.id as http_response_detail_url %} 10 | {% if acs_http_request.rpc_response_to %} 11 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_request.rpc_response_to.id as response_to_detail_url %} 12 | {% endif %} 13 | {% if acs_http_request.rpc_response %} 14 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_request.rpc_response.id as rpc_response_detail_url %} 15 | {% endif %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 51 | 52 | 53 | 60 | 61 | 62 | 63 | 64 | 65 |
ACS HTTP Request{% include 'includes/small-details-button.html' with url=acs_http_request_detail_url %} {{ acs_http_request.tag }}
ACS Session{% include 'includes/small-details-button.html' with url=acs_session_detail_url %} {{ acs_http_request.acs_session }}
When{{ acs_http_request.created_date }}
RPC Method{{ acs_http_request.cwmp_rpc_method }}
HTTP Request Headers
{{ acs_http_request.request_headers|prettyprintjson }}
HTTP Request Body{% if acs_http_request.body %}
{{ acs_http_request.body|prettyprintxml }}
{% else %}(empty body){% endif %}
RPC Response To 44 | {% if acs_http_request.rpc_response_to %} 45 | {% include 'includes/small-details-button.html' with url=response_to_detail_url %}{{ acs_http_request.rpc_response_to }} 46 | {% else %} 47 | N/A 48 | {% endif %} 49 |
RPC Response 54 | {% if acs_http_request.rpc_response %} 55 | {% include 'includes/small-details-button.html' with url=rpc_response_detail_url %}{{ acs_http_request.rpc_response }} 56 | {% else %} 57 | N/A 58 | {% endif %} 59 |
HTTP Response{% include 'includes/small-details-button.html' with url=http_response_detail_url %}{{ acs_http_request.acs_http_response }}
66 |
67 |
68 |
69 | {% include 'includes/detail_panel_footer.html' %} 70 |
71 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_http_request_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsHttpRequestList' headline=headline|default:"ACS HTTP Request List" %} 3 | {% if acs_http_request_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for acs_http_request in acs_http_request_list %} 24 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_request.id as acs_http_request_detail_url %} 25 | {% url 'acsadmin:acs_session_detail' pk=acs_http_request.acs_session.id as acs_session_detail_url %} 26 | {% if acs_http_request.rpc_response_to %} 27 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_request.rpc_response_to.id as response_to_detail_url %} 28 | {% endif %} 29 | {% if acs_http_request.rpc_response %} 30 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_request.rpc_response.id as rpc_response_detail_url %} 31 | {% endif %} 32 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_request.http_response.id as http_response_detail_url %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | {% else %} 44 | N/A 45 | {% endif %} 46 | 47 | 54 | 55 | 58 | 59 | {% endfor %} 60 | 61 |
ACS HTTP RequestACS SessionWhenRPC MethodHeadersBodyRPC Response ToRPC ResponseHTTP ResponseActions
{% include 'includes/small-details-button.html' with url=acs_http_request_detail_url %}{{ acs_http_request }}{% include 'includes/small-details-button.html' with url=acs_session_detail_url %}{{ acs_session }}{{ acs_http_request.created_date }}{{ acs_http_request.cwmp_rpc_method }}{{ acs_http_request.request_headers|length }} bytes{{ acs_http_request.body|length }} bytes 41 | {% if acs_http_request.rpc_response_to %} 42 | {% include 'includes/small-details-button.html' with url=response_to_detail_url %}{{ acs_http_request.rpc_response_to.tag }} 48 | {% if acs_http_request.rpc_response %} 49 | {% include 'includes/small-details-button.html' with url=rpc_response_detail_url %}{{ acs_http_request.rpc_response }} 50 | {% else %} 51 | N/A 52 | {% endif %} 53 | {% include 'includes/small-details-button.html' with url=http_response_detail_url %}{{ acs_http_request.http_response.tag }} 56 | {% include 'includes/details-button.html' with url=acs_http_detail_url %} 57 |
62 | {% if is_paginated %} 63 | {% include 'includes/pagination-footer.html' %} 64 | {% endif %} 65 | {% else %} 66 |

No ACS HTTP Requests Found!

67 | {% endif %} 68 | {% include 'includes/collapsable-panel-footer.html' %} 69 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_http_response_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS HTTP Response Details for "|addstr:acs_http_response.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_response.id as acs_http_response_detail_url %} 8 | {% url 'acsadmin:acs_session_detail' pk=acs_http_response.http_request.acs_session.id as acs_session_detail_url %} 9 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_response.http_request.id as http_request_detail_url %} 10 | {% if acs_http_response.rpc_response_to %} 11 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_response.rpc_response_to.id as rpc_response_to_detail_url %} 12 | {% endif %} 13 | {% if acs_http_response.rpc_response %} 14 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_response.rpc_response.id as rpc_response_detail_url %} 15 | {% endif %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
ACS HTTP Response{% include 'includes/small-details-button.html' with url=acs_http_response_detail_url %} {{ acs_http_response.tag }}
ACS Session{% include 'includes/small-details-button.html' with url=acs_session_detail_url %} {{ acs_http_response.http_request.acs_session }}
When{{ acs_http_response.created_date }}
RPC Method{{ acs_http_response.cwmp_rpc_method }}
HTTP Response Body{% if acs_http_response.body %}
{{ acs_http_response.body|prettyprintxml }}
{% else %}(empty body){% endif %}
RPC Response To{% include 'includes/small-details-button.html' with url=rpc_response_to_detail_url %}{{ acs_http_response.rpc_response_to }}
RPC Response{% include 'includes/small-details-button.html' with url=rpc_response_detail_url %}{{ acs_http_response.rpc_response }}
HTTP Request{% include 'includes/small-details-button.html' with url=http_response_detail_url %}{{ acs_http_response.http_request }}
50 |
51 |
52 |
53 | {% include 'includes/detail_panel_footer.html' %} 54 |
55 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_http_response_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsHttpResponseList' headline=headline|default:"ACS HTTP Response List" %} 3 | {% if acs_http_response_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for acs_http_response in acs_http_response_list %} 22 | {% url 'acsadmin:acs_http_response_detail' pk=acs_http_response.id as acs_http_response_detail_url %} 23 | {% url 'acsadmin:acs_session_detail' pk=acs_http_response.http_request.acs_session.id as acs_session_detail_url %} 24 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_response.http_request.id as http_request_detail_url %} 25 | {% if acs_http_response.rpc_response_to %} 26 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_response.rpc_response_to.id as rpc_response_to_detail_url %} 27 | {% endif %} 28 | {% if acs_http_response.rpc_response %} 29 | {% url 'acsadmin:acs_http_request_detail' pk=acs_http_response.rpc_response.id as rpc_response_detail_url %} 30 | {% endif %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 45 | 52 | 55 | 56 | {% endfor %} 57 | 58 |
ACS HTTP ResponseACS SessionHTTP RequestWhenRPC MethodBodyRPC Response ToActions
{% include 'includes/small-details-button.html' with url=acs_http_response_detail_url %}{{ acs_http_response }}{% include 'includes/small-details-button.html' with url=acs_session_detail_url %}{{ acs_http_response.http_request.acs_session }}{% include 'includes/small-details-button.html' with url=http_request_detail_url %}{{ acs_http_response.http_request.tag }}{{ acs_http_response.created_date }}{{ acs_http_response.cwmp_rpc_method }}{{ acs_http_response.body|length }} bytes 39 | {% if acs_http_response.rpc_response_to %} 40 | {% include 'includes/small-details-button.html' with url=rpc_response_to_detail_url %}{{ acs_http_response.rpc_response_to.tag }} 41 | {% else %} 42 | N/A 43 | {% endif %} 44 | 46 | {% if acs_http_response.rpc_response %} 47 | {% include 'includes/small-details-button.html' with url=rpc_response_detail_url %}{{ acs_http_response.rpc_response.tag }} 48 | {% else %} 49 | N/A 50 | {% endif %} 51 | 53 | {% include 'includes/details-button.html' with url=acs_http_response_detail_url %} 54 |
59 | {% if is_paginated %} 60 | {% include 'includes/pagination-footer.html' %} 61 | {% endif %} 62 | {% else %} 63 |

No ACS HTTP Responses Found!

64 | {% endif %} 65 | {% include 'includes/collapsable-panel-footer.html' %} 66 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_queue_job_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS Queue Job Details for "|addstr:aqj.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_queue_job_detail' pk=aqj.id as acs_queue_job_detail_url %} 8 | {% url 'acsadmin:acs_device_detail' pk=aqj.acs_device.id as acs_device_detail_url %} 9 | {% if aqj.handled_in %} 10 | {% url 'acsadmin:acs_http_response_detail' pk=aqj.handled_in.id as handled_in_detail_url %} 11 | {% endif %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% if aqj.handled_in %} 26 | 27 | 28 | 29 | 30 | {% endif %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
ACS Queue Job{% include 'includes/small-details-button.html' with url=acs_queue_job_detail_url %} {{ aqj.tag }}
ACS Device{% include 'includes/small-details-button.html' with url=acsdevice_detail_url %} {{ aqj.acs_device }}
RPC Method{{ aqj.cwmp_rpc_method }}
Handled in HTTP Response{% include 'includes/small-details-button.html' with url=handled_in_detail_url %}{{ aqj.handled_in }}
Processed{{ aqj.processed|truefalseicon|safe }}
Reason{{ aqj.reason }}
Automatic?{{ aqj.automatic|truefalseicon|safe }}
Urgent?{{ aqj.urgent|truefalseicon|safe }}
Notification Sent?{{ aqj.notification_sent|truefalseicon|safe }}
XML
{{ aqj.cwmp_rpc_object_xml|prettyprintxml }}
56 |
57 |
58 |
59 | {% include 'includes/detail_panel_footer.html' %} 60 |
61 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_queue_job_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsQueueJobList' headline=headline|default:"ACS Queue Job List" %} 3 | {% if acs_queue_job_list %} 4 | {% if is_paginated %} 5 | {% include 'includes/pagination-footer.html' %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for aqj in acs_queue_job_list %} 26 | {% url 'acsadmin:acs_queue_job_detail' pk=aqj.id as acs_queue_job_detail_url %} 27 | {% url 'acsadmin:acs_device_detail' pk=aqj.acs_device.id as acsdevice_detail_url %} 28 | {% if aqj.handled_in %} 29 | {% url 'acsadmin:acs_http_response_detail' pk=aqj.handled_in.id as handled_in_detail_url %} 30 | {% endif %} 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | {% endfor %} 54 | 55 |
ACS Queue JobACS DeviceCreatedRPC MethodHandled InProcessedReasonAutomatic?Urgent?Notification Sent?XMLActions
{% include 'includes/small-details-button.html' with url=acs_queue_job_detail_url %}{{ aqj.tag }}{% include 'includes/small-details-button.html' with url=acsdevice_detail_url %}{{ aqj.acs_device.tag }}{{ aqj.created_date }}{{ aqj.cwmp_rpc_method }} 37 | {% if aqj.handled_in %} 38 | {% include 'includes/small-details-button.html' with url=handled_in_detail_url %}{{ aqj.handled_in }} 39 | {% else %} 40 | N/A 41 | {% endif %} 42 | {{ aqj.processed|truefalseicon|safe }}{{ aqj.reason }}{{ aqj.automatic|truefalseicon|safe }}{{ aqj.urgent|truefalseicon|safe }}{{ aqj.notification_sent|truefalseicon|safe }}{{ aqj.cwmp_rpc_object_xml|length }} bytes 50 | {% include 'includes/details-button.html' with url=acs_queue_job_detail_url %} 51 |
56 | {% if is_paginated %} 57 | {% include 'includes/pagination-footer.html' %} 58 | {% endif %} 59 | {% else %} 60 |

No Acs Queue Jobs found!

61 | {% endif %} 62 | {% include 'includes/collapsable-panel-footer.html' %} 63 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_session_detail_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 |
3 |

{{ headline|default:"ACS Session Details for "|addstr:acs_session.tag }}

4 |
5 |
6 |
7 | {% url 'acsadmin:acs_session_detail' pk=acs_session.id as acs_session_detail_url %} 8 | {% url 'acsadmin:acs_device_detail' pk=acs_session.acs_device.id as acs_device_detail_url %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
ACS Session{% include 'includes/small-details-button.html' with url=acs_http_detail_url %} {{ acs_session.tag }}
ACS Device{% include 'includes/small-details-button.html' with url=acs_device_detail_url %} {{ acs_session.acs_device }}
Client IP{{ acs_session.client_ip }} ({% if acs_session.client_ip_verified %}verified{% else %}not verified{% endif %})
ACS Session ID{{ acs_session.acs_session_id.hex }}
Inform Event Codes{% for iec in acs_session.inform_eventcodes %}{{ iec }}
{% endfor %}
ACS Session Result{{ acs_session.session_result|truefalseicon|safe }}
ACS Device Uptime{{ acs_session.device_uptime | datetimerange_as_pretty_delta }}
39 |
40 |
41 |
42 | {% include 'includes/detail_panel_footer.html' %} 43 |
44 | -------------------------------------------------------------------------------- /acsadmin/templates/includes/acs_session_list_panel.html: -------------------------------------------------------------------------------- 1 | {% load custom_template_filters %} 2 | {% include 'includes/collapsable-panel-header.html' with name='AcsSessionList' headline=headline|default:"ACS Session List" %} 3 | {% if acs_session_list %} 4 | {% if not included %} 5 |

6 | {% url 'acsadmin:acs_session_list' as all_sessions %} 7 | {% url 'acsadmin:acs_session_list_failed' as only_failed %} 8 | {% url 'acsadmin:acs_session_list_failed_verified' as only_failed_verified %} 9 | {% include 'includes/button.html' with url=all_sessions buttontext="Show All Sessions" %} 10 | {% include 'includes/button.html' with url=only_failed buttontext="Show Only Failed Sessions" %} 11 | {% include 'includes/button.html' with url=only_failed_verified buttontext="Show Only Dailed and IP Verified Sessions" %} 12 |

13 | {% endif %} 14 | {% if is_paginated %} 15 | {% include 'includes/pagination-footer.html' %} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for acs_session in acs_session_list %} 38 | {% url 'acsadmin:acs_session_detail' pk=acs_session.id as acs_session_detail_url %} 39 | {% url 'acsadmin:acs_device_detail' pk=acs_session.acs_device.id as acs_device_detail_url %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | {% endfor %} 59 | 60 |
ACS SessionACS DeviceClient IPClient IP VerifiedInform Event CodesHTTP RequestHTTP ResponseStartDurationDevice UptimeLast RPC MethodBytes ReceivedBytes SentActions
{% include 'includes/small-details-button.html' with url=acs_session_detail_url %}{{ acs_session.tag }}{% include 'includes/small-details-button.html' with url=acs_device_detail_url %}{{ acs_session.acs_device.tag }}{{ acs_session.client_ip }}{{ acs_session.client_ip_verified|truefalseicon|safe }}{% for iec in acs_session.inform_eventcodes %}{{ iec }}
{% endfor %}
{{ acs_session.acs_http_requests.count }}{{ acs_session.acs_http_responses.count }}{{ acs_session.start }}{{ acs_session.duration | pretty_timedelta }}{{ acs_session.device_uptime | datetimerange_as_pretty_delta }}{{ acs_session.get_latest_http_tx.soap_element_ns }}{{ acs_session.bytes_in }} bytes{{ acs_session.bytes_out }} bytes 55 | {% include 'includes/details-button.html' with url=acs_session_detail_url %} 56 |
61 | {% if included %} 62 | {% url 'acsadmin:acsdevice_all_acs_sessions' pk=acs_device.id as all_acs_sessions_url %} 63 | {% include 'includes/button.html' with url=all_acs_sessions_url buttontext='Show All ACS Sessions for This ACS Device' %} 64 | {% endif %} 65 | {% if is_paginated %} 66 | {% include 'includes/pagination-footer.html' %} 67 | {% endif %} 68 | {% else %} 69 |

No ACS Sessions Found!

70 | {% endif %} 71 | {% include 'includes/collapsable-panel-footer.html' %} 72 | -------------------------------------------------------------------------------- /acsadmin/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tykling/django-acs/065e944bbce4517e9db74e6dc4098549ac1e2b0b/acsadmin/templatetags/__init__.py -------------------------------------------------------------------------------- /acsadmin/templatetags/custom_template_filters.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from defusedxml.lxml import fromstring 3 | from lxml import etree 4 | import json 5 | 6 | from django import template 7 | 8 | register = template.Library() 9 | 10 | @register.filter 11 | def datetimerange_as_pretty_delta(value): 12 | if value: 13 | return pretty_timedelta(value.upper-value.lower) 14 | 15 | @register.filter 16 | def pretty_timedelta(value): 17 | if value: 18 | # return a pretty printed version without microseconds 19 | return str(timedelta(seconds=int(value.total_seconds()))) 20 | 21 | @register.filter(is_safe=True) 22 | def truefalseicon(value): 23 | '''Returns a icon with a green checkmark or a red X depending on true/false input, requires font-awesome to be any good''' 24 | if value: 25 | return '' 26 | else: 27 | return '' 28 | 29 | @register.filter 30 | def addstr(arg1, arg2): 31 | '''concatenate arg1 & arg2''' 32 | return str(arg1) + str(arg2) 33 | 34 | @register.filter 35 | def prettyprintjson(jsondata): 36 | return json.dumps(json.loads(jsondata), indent=4) if jsondata else 'N/A' 37 | 38 | @register.filter 39 | def prettyprintxml(xml): 40 | ''' 41 | This assumes too much about encoding and stuff. 42 | Should be possible to prettyprint without changing the xml at all. 43 | Catch lxml.etree.XMLSyntaxError so we still return the xml if it has syntax errors, with a message. 44 | ''' 45 | try: 46 | return etree.tostring( 47 | fromstring(xml.encode('utf-8')), 48 | pretty_print=True, 49 | xml_declaration=True, 50 | encoding='utf-8', 51 | ).decode('utf-8') 52 | except etree.XMLSyntaxError: 53 | return "CAUGHT lxml.etree.XMLSyntaxError:\n%s" % xml 54 | 55 | -------------------------------------------------------------------------------- /acsadmin/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | import acsadmin.views 4 | 5 | app_name = 'acsadmin' 6 | 7 | urlpatterns = [ 8 | path('http/requests/', acsadmin.views.AcsHttpRequestList.as_view(), name='acs_http_request_list'), 9 | path('http/requests//', acsadmin.views.AcsHttpRequestDetail.as_view(), name='acs_http_request_detail'), 10 | 11 | path('http/responses/', acsadmin.views.AcsHttpResponseList.as_view(), name='acs_http_response_list'), 12 | path('http/responses//', acsadmin.views.AcsHttpResponseDetail.as_view(), name='acs_http_response_detail'), 13 | 14 | path('sessions/', acsadmin.views.AcsSessionList.as_view(), name='acs_session_list'), 15 | path('sessions/failed/', acsadmin.views.AcsSessionList.as_view(), name='acs_session_list_failed', kwargs={'only_failed': True}), 16 | path('sessions/verifiedfailed/', acsadmin.views.AcsSessionList.as_view(), name='acs_session_list_failed_verified', kwargs={'only_failed': True, 'only_verified': True}), 17 | path('sessions//', acsadmin.views.AcsSessionDetail.as_view(), name='acs_session_detail'), 18 | 19 | path('jobqueue/', acsadmin.views.AcsQueueJobList.as_view(), name='acs_queue_job_list'), 20 | path('jobqueue/hideautomatic/', acsadmin.views.AcsQueueJobList.as_view(), name='acs_queue_job_list_hideautomatic', kwargs={'hideautomatic': True}), 21 | path('jobqueue//', acsadmin.views.AcsQueueJobDetail.as_view(), name='acs_queue_job_detail'), 22 | 23 | path('devicemodels/', acsadmin.views.AcsDeviceModelList.as_view(), name='acs_device_model_list'), 24 | path('devicemodels//', acsadmin.views.AcsDeviceModelDetail.as_view(), name='acs_device_model_detail'), 25 | 26 | path('devicecategories/', acsadmin.views.AcsDeviceCategoryList.as_view(), name='acs_device_category_list'), 27 | path('devicecategories//', acsadmin.views.AcsDeviceCategoryDetail.as_view(), name='acs_device_category_detail'), 28 | 29 | path('devicevendors/', acsadmin.views.AcsDeviceVendorList.as_view(), name='acs_device_vendor_list'), 30 | path('devicevendors//', acsadmin.views.AcsDeviceVendorDetail.as_view(), name='acs_device_vendor_detail'), 31 | 32 | path('devices/', acsadmin.views.AcsDeviceList.as_view(), name='acs_device_list'), 33 | path('devices//', acsadmin.views.AcsDeviceDetail.as_view(), name='acs_device_detail'), 34 | path('devices//create_job/', acsadmin.views.AcsQueueJobCreate.as_view(), name='acs_queue_job_create'), 35 | path('devices//allacssessions/', acsadmin.views.AllAcsSessions.as_view(), name='acsdevice_all_acs_sessions'), 36 | ] 37 | 38 | -------------------------------------------------------------------------------- /acsadmin/utils.py: -------------------------------------------------------------------------------- 1 | import io, paramiko, re 2 | from radius.models import Radacct 3 | 4 | def get_value_from_parameterlist(parameterlist, key): 5 | ''' 6 | Uses lxml.etree xpath to extract the text inside a Value element, 7 | given the 'lookup key' inside the Name element, and the following XML structure: 8 | 9 | 10 | Device.DeviceInfo.HardwareVersion 11 | TW_0.7 12 | 13 | 14 | Device.DeviceInfo.SoftwareVersion 15 | 1.23.4.6.2969 16 | 17 | 18 | Device.DeviceInfo.ProvisioningCode 19 | 20 | 21 | 22 | ''' 23 | elementlist = parameterlist.xpath('.//Name[text()="%s"]/following-sibling::Value' % key) 24 | if elementlist: 25 | element = elementlist[0] 26 | else: 27 | return False 28 | 29 | # return int() for integers 30 | if element.attrib['{http://www.w3.org/2001/XMLSchema-instance}type'] == 'xsd:unsignedInt': 31 | return int(element.text) 32 | else: 33 | return element.text 34 | 35 | 36 | def run_ssh_command(server, username, private_key, command): 37 | try: 38 | private_key_file = io.StringIO(private_key) 39 | private_key = paramiko.RSAKey.from_private_key(private_key_file) 40 | ssh = paramiko.SSHClient() 41 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 42 | ssh.connect(server, username=username, pkey=private_key, timeout=15) 43 | stdin, stdout, stderr = ssh.exec_command(command) 44 | exit_status = stdout.channel.recv_exit_status() 45 | ssh.close() 46 | except Exception as E: 47 | return { 48 | 'result': False, 49 | 'exception': E, 50 | } 51 | 52 | return { 53 | 'result': True, 54 | 'output': stdout.readlines(), 55 | 'errorlines': stderr.readlines(), 56 | 'exit_status': exit_status, 57 | } 58 | 59 | def get_datamodel_from_devicesummary(summary): 60 | """ 61 | Regex to return "Device:1.0" from "Device:1.0[](Baseline:1), ABCService:1.0[1](Baseline:1), XYZService:1.0[1](Baseline:1)" 62 | """ 63 | match = re.search('(.*:\d.\d)\[\]', summary) 64 | if match: 65 | return match.group(1) 66 | else: 67 | return False 68 | 69 | -------------------------------------------------------------------------------- /acsadmin/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView, DetailView 2 | from django.views.generic.edit import FormView, CreateView 3 | from django.shortcuts import get_object_or_404 4 | 5 | from acs.models import * 6 | from .forms import AcsDeviceActionForm 7 | 8 | 9 | class AcsQueueJobList(ListView): 10 | model = AcsQueueJob 11 | template_name = 'acs_queue_job_list.html' 12 | paginate_by = 25 13 | 14 | def get_queryset(self): 15 | queryset = super(AcsQueueJobList, self).get_queryset() 16 | if 'hideautomatic' in self.kwargs and self.kwargs['hideautomatic']: 17 | queryset = queryset.exclude(automatic=True) 18 | return queryset 19 | 20 | 21 | class AcsQueueJobDetail(DetailView): 22 | model = AcsQueueJob 23 | template_name = 'acs_queue_job_detail.html' 24 | 25 | 26 | class AcsQueueJobCreate(CreateView): 27 | model = AcsQueueJob 28 | template_name = 'acs_queue_job_create.html' 29 | fields = ['cwmp_rpc_object_xml', 'reason', 'urgent'] 30 | 31 | def setup(self, *args, **kwargs): 32 | self.acs_device = get_object_or_404( 33 | AcsDevice, 34 | pk=kwargs['pk'], 35 | ) 36 | 37 | def form_valid(self, form): 38 | job = form.save(commit=False) 39 | job.acs_device = self.acs_device 40 | job.save() 41 | return(super().form_valid(form)) 42 | 43 | 44 | class AcsSessionList(ListView): 45 | model = AcsSession 46 | template_name = 'acs_session_list.html' 47 | paginate_by = 25 48 | 49 | def get_queryset(self): 50 | queryset = super(AcsSessionList, self).get_queryset() 51 | #queryset = queryset.get_related() 52 | if 'only_failed' in self.kwargs and self.kwargs['only_failed']: 53 | queryset = queryset.filter(session_result=False) 54 | if 'only_verified' in self.kwargs and self.kwargs['only_verified']: 55 | queryset = queryset.filter(client_ip_verified=True) 56 | return queryset 57 | 58 | 59 | class AcsSessionDetail(DetailView): 60 | model = AcsSession 61 | template_name = 'acs_session_detail.html' 62 | 63 | 64 | class AcsHttpRequestList(ListView): 65 | model = AcsHttpRequest 66 | template_name = 'acs_http_request_list.html' 67 | paginate_by = 25 68 | 69 | 70 | class AcsHttpRequestDetail(DetailView): 71 | model = AcsHttpRequest 72 | template_name = 'acs_http_request_detail.html' 73 | 74 | 75 | class AcsHttpResponseList(ListView): 76 | model = AcsHttpResponse 77 | template_name = 'acs_http_response_list.html' 78 | paginate_by = 25 79 | 80 | 81 | class AcsHttpResponseDetail(DetailView): 82 | model = AcsHttpResponse 83 | template_name = 'acs_http_response_detail.html' 84 | 85 | 86 | class AcsDeviceModelList(ListView): 87 | model = AcsDeviceModel 88 | template_name = 'acs_device_model_list.html' 89 | paginate_by = 25 90 | 91 | 92 | class AcsDeviceModelDetail(DetailView): 93 | model = AcsDeviceModel 94 | template_name = 'acs_device_model_detail.html' 95 | 96 | 97 | class AcsDeviceCategoryList(ListView): 98 | model = AcsDeviceCategory 99 | template_name = 'acs_device_category_list.html' 100 | paginate_by = 25 101 | 102 | 103 | class AcsDeviceCategoryDetail(DetailView): 104 | model = AcsDeviceCategory 105 | template_name = 'acs_device_category_detail.html' 106 | 107 | 108 | class AcsDeviceVendorList(ListView): 109 | model = AcsDeviceVendor 110 | template_name = 'acs_device_vendor_list.html' 111 | paginate_by = 25 112 | 113 | 114 | class AcsDeviceVendorDetail(DetailView): 115 | model = AcsDeviceVendor 116 | template_name = 'acs_device_vendor_detail.html' 117 | 118 | 119 | class AcsDeviceList(ListView): 120 | model = AcsDevice 121 | template_name = 'acs_device_list.html' 122 | paginate_by = 25 123 | 124 | 125 | class AcsDeviceDetail(DetailView): 126 | model = AcsDevice 127 | template_name = 'acs_device_detail.html' 128 | 129 | 130 | class AllAcsSessions(ListView): 131 | model = AcsSession 132 | template_name = 'acs_session_list.html' 133 | paginate_by = 100 134 | 135 | def get_queryset(self): 136 | return AcsSession.objects.filter(acs_device_id=self.kwargs['pk']) 137 | 138 | -------------------------------------------------------------------------------- /manifest.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include acsadmin/templates * 4 | recursive-include docs * 5 | 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 5 | README = readme.read() 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='django-acs', 12 | version='0.1.0', 13 | packages=find_packages(), 14 | include_package_data=True, 15 | license='BSD License', 16 | description='A Django ACS Server.', 17 | long_description=README, 18 | url='https://github.com/tykling/django-acs', 19 | author='Thomas Steen Rasmussen', 20 | author_email='thomas@gibfest.dk', 21 | classifiers=[ 22 | 'Environment :: Web Environment', 23 | 'Framework :: Django', 24 | 'Framework :: Django :: 2.1', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 3.5', 30 | 'Programming Language :: Python :: 3.6', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Topic :: Internet :: WWW/HTTP', 33 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 34 | ], 35 | ) 36 | --------------------------------------------------------------------------------