├── .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 |
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 | ACS Device Category |
11 | {% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %} {{ acs_device_category.tag }} |
12 |
13 |
14 | Name |
15 | {{ acs_device_category.name }} |
16 |
17 |
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 | ACS Device Category |
11 | Name |
12 | Models |
13 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %}{{ acs_device_category.tag }} |
21 | {{ acs_device_category.name }} |
22 | {{ acs_device_category.acsdevicemodels.count }} |
23 |
24 | {% include 'includes/details-button.html' with url=acs_device_category_detail_url %}
25 | |
26 |
27 | {% endfor %}
28 |
29 |
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 | ACS Device |
13 | {% include 'includes/small-details-button.html' with url=acs_device_detail_url %} {{ acs_device }} |
14 |
15 |
16 | Vendor |
17 | {% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %} {{ acs_device.model.vendor }} |
18 |
19 |
20 | Model |
21 | {% include 'includes/small-details-button.html' with url=acs_device_type_detail_url %} {{ acs_device.model }} |
22 |
23 |
24 | Serial |
25 | {{ acs_device.serial }} |
26 |
27 |
28 | Related Device |
29 | {% include 'includes/small-details-button.html' with url=acs_device.get_related_device.get_absolute_url %}{{ acs_device.get_related_device }} |
30 |
31 |
32 | Latest Inform |
33 | {{ acs_device.acs_latest_inform | default:"N/A" }} |
34 |
35 |
36 | Latest Inform Result |
37 | {{ acs_device.acs_latest_session_result }} |
38 |
39 |
40 | Number of Informs |
41 | {{ acs_device.acs_inform_count }} |
42 |
43 |
44 | Current Config Level |
45 | {{ acs_device.current_config_level | default:"N/A" }} |
46 |
47 |
48 | Desired Config Level |
49 | {{ acs_device.get_desired_config_level | default:"N/A" }} {% if acs_device.desired_config_level %}(LOCAL OVERRIDE){% endif %} |
50 |
51 |
52 | Current Software Version |
53 | {{ acs_device.current_software_version | default:"N/A" }} |
54 |
55 |
56 | Desired Software Version |
57 | {{ acs_device.get_desired_software_version | default:"N/A" }} {% if acs_device.desired_software_version %}(LOCAL OVERRIDE){% endif %} |
58 |
59 |
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 | ACS Device |
11 | Model |
12 | Vendor |
13 | Serial |
14 | Related Device |
15 | Inform |
16 | Informs |
17 | Config Level |
18 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acsdevice_detail_url %}{{ ad.tag }} |
28 | {% include 'includes/small-details-button.html' with url=acsdevicemodel_detail_url %}{{ ad.model }} |
29 | {% include 'includes/small-details-button.html' with url=acsdevicevendor_detail_url %}{{ ad.model.vendor }} |
30 | {{ ad.serial }} |
31 | {% 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 | |
37 | {{ ad.acs_latest_inform }} {{ ad.acs_latest_session_result|truefalseicon|safe }} |
38 | {{ ad.acs_inform_count }} |
39 | {{ ad.current_config_level }}{% if ad.desired_config_level %}(desired: {{ ad.desired_config_level }}){% endif %} |
40 |
41 | {% include 'includes/details-button.html' with url=acsdevice_detail_url %}
42 | |
43 |
44 | {% endfor %}
45 |
46 |
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 | ACS Device Model |
13 | {% include 'includes/small-details-button.html' with url=acs_device_model_detail_url %} {{ acs_device_model.tag }} |
14 |
15 |
16 | Name |
17 | {{ acs_device_model.name }} |
18 |
19 |
20 | Vendor |
21 | {% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %} {{ acs_device_model.vendor }} |
22 |
23 |
24 | Device Category |
25 | {% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %} {{ acs_device_model.category }} |
26 |
27 |
28 | Desired Config Level |
29 | {{ acs_device_model.desired_config_level|default:"N/A" }} |
30 |
31 |
32 | Desired Software Version |
33 | {{ acs_device_model.desired_software_version|default:"N/A" }} |
34 |
35 |
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 | ACS Device Model |
11 | Name |
12 | Vendor |
13 | Category |
14 | Desired Config Level |
15 | Desired Software Version |
16 | Devices |
17 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_device_model_detail_url %}{{ acs_device_model.tag }} |
27 | {{ acs_device_model.name }} |
28 | {% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %}{{ acs_device_model.vendor }} |
29 | {% include 'includes/small-details-button.html' with url=acs_device_category_detail_url %}{{ acs_device_model.category }} |
30 | {{ acs_device_model.desired_config_level|default:"N/A" }} |
31 | {{ acs_device_model.desired_software_version|default:"N/A" }} |
32 | {{ acs_device_model.acsdevices.count }} |
33 |
34 | {% include 'includes/details-button.html' with url=acs_device_model_detail_url %}
35 | |
36 |
37 | {% endfor %}
38 |
39 |
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 |
9 |
10 |
11 |
12 | Parameter |
13 | Type |
14 | Writable |
15 | Notification |
16 | Access list |
17 | Value |
18 |
19 |
20 |
21 | {% for key, value in acs_device.acs_parameter_dict.items %}
22 |
23 | {{ key }} |
24 | {{ value.type }} |
25 | {{ value.writable }} |
26 | {{ value.notification }} |
27 | {{ value.accesslist }} |
28 | {{ value.value | linebreaksbr }} |
29 |
30 | {% endfor %}
31 |
32 |
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 | ACS Device Vendor |
11 | {% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %} {{ acs_device_vendor.tag }} |
12 |
13 |
14 | Name |
15 | {{ acs_device_vendor.name }} |
16 |
17 |
18 | OUI |
19 | {{ acs_device_vendor.oui }} |
20 |
21 |
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 | ACS Device Vendor |
11 | Name |
12 | MAC OUI |
13 | Models |
14 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_device_vendor_detail_url %}{{ acs_device_vendor }} |
22 | {{ acs_device_vendor.name }} |
23 | {{ acs_device_vendor.oui }} |
24 | {{ acs_device_vendor.acsdevicemodels.count }} |
25 |
26 | {% include 'includes/details-button.html' with url=acs_device_vendor_detail_url %}
27 | |
28 |
29 | {% endfor %}
30 |
31 |
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 | Reqresp |
11 | Direction |
12 | When |
13 | RPC Method |
14 | Headers |
15 | Body |
16 | RPC Response To |
17 | RPC Response |
18 | ACS Queue Job |
19 | Actions |
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 |
46 | {% include 'includes/small-details-button.html' with url=acs_http_detail_url %}{{ acs_http }}
47 | |
48 |
49 | {% if acs_http.is_request %}
50 | HTTP Request
51 | {% else %}
52 | HTTP response
53 | {% endif %}
54 | |
55 | {{ acs_http.created_date }} |
56 | {{ acs_http.soap_element_ns }} |
57 | {% if acs_http.is_request %}{{ acs_http.request_headers|length }} bytes{% else %}N/A{% endif %} |
58 | {{ acs_http.body|length }} bytes |
59 |
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 | |
66 |
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 | |
73 |
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 | |
80 |
81 | {% include 'includes/details-button.html' with url=acs_http_detail_url %}
82 | |
83 |
84 | {% endfor %}
85 |
86 |
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 | ACS HTTP Request |
19 | {% include 'includes/small-details-button.html' with url=acs_http_request_detail_url %} {{ acs_http_request.tag }} |
20 |
21 |
22 | ACS Session |
23 | {% include 'includes/small-details-button.html' with url=acs_session_detail_url %} {{ acs_http_request.acs_session }} |
24 |
25 |
26 | When |
27 | {{ acs_http_request.created_date }} |
28 |
29 |
30 | RPC Method |
31 | {{ acs_http_request.cwmp_rpc_method }} |
32 |
33 |
34 | HTTP Request Headers |
35 | {{ acs_http_request.request_headers|prettyprintjson }} |
36 |
37 |
38 | HTTP Request Body |
39 | {% if acs_http_request.body %}{{ acs_http_request.body|prettyprintxml }} {% else %}(empty body){% endif %} |
40 |
41 |
42 | RPC Response To |
43 |
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 | |
50 |
51 |
52 | RPC Response |
53 |
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 | |
60 |
61 |
62 | HTTP Response |
63 | {% include 'includes/small-details-button.html' with url=http_response_detail_url %}{{ acs_http_request.acs_http_response }} |
64 |
65 |
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 | ACS HTTP Request |
11 | ACS Session |
12 | When |
13 | RPC Method |
14 | Headers |
15 | Body |
16 | RPC Response To |
17 | RPC Response |
18 | HTTP Response |
19 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_http_request_detail_url %}{{ acs_http_request }} |
35 | {% include 'includes/small-details-button.html' with url=acs_session_detail_url %}{{ acs_session }} |
36 | {{ acs_http_request.created_date }} |
37 | {{ acs_http_request.cwmp_rpc_method }} |
38 | {{ acs_http_request.request_headers|length }} bytes |
39 | {{ acs_http_request.body|length }} bytes |
40 |
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 }} |
43 | {% else %}
44 | N/A
45 | {% endif %}
46 |
47 |
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 | |
54 | {% include 'includes/small-details-button.html' with url=http_response_detail_url %}{{ acs_http_request.http_response.tag }} |
55 |
56 | {% include 'includes/details-button.html' with url=acs_http_detail_url %}
57 | |
58 |
59 | {% endfor %}
60 |
61 |
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 | ACS HTTP Response |
19 | {% include 'includes/small-details-button.html' with url=acs_http_response_detail_url %} {{ acs_http_response.tag }} |
20 |
21 |
22 | ACS Session |
23 | {% include 'includes/small-details-button.html' with url=acs_session_detail_url %} {{ acs_http_response.http_request.acs_session }} |
24 |
25 |
26 | When |
27 | {{ acs_http_response.created_date }} |
28 |
29 |
30 | RPC Method |
31 | {{ acs_http_response.cwmp_rpc_method }} |
32 |
33 |
34 | HTTP Response Body |
35 | {% if acs_http_response.body %}{{ acs_http_response.body|prettyprintxml }} {% else %}(empty body){% endif %} |
36 |
37 |
38 | RPC Response To |
39 | {% include 'includes/small-details-button.html' with url=rpc_response_to_detail_url %}{{ acs_http_response.rpc_response_to }} |
40 |
41 |
42 | RPC Response |
43 | {% include 'includes/small-details-button.html' with url=rpc_response_detail_url %}{{ acs_http_response.rpc_response }} |
44 |
45 |
46 | HTTP Request |
47 | {% include 'includes/small-details-button.html' with url=http_response_detail_url %}{{ acs_http_response.http_request }} |
48 |
49 |
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 | ACS HTTP Response |
11 | ACS Session |
12 | HTTP Request |
13 | When |
14 | RPC Method |
15 | Body |
16 | RPC Response To |
17 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_http_response_detail_url %}{{ acs_http_response }} |
33 | {% include 'includes/small-details-button.html' with url=acs_session_detail_url %}{{ acs_http_response.http_request.acs_session }} |
34 | {% include 'includes/small-details-button.html' with url=http_request_detail_url %}{{ acs_http_response.http_request.tag }} |
35 | {{ acs_http_response.created_date }} |
36 | {{ acs_http_response.cwmp_rpc_method }} |
37 | {{ acs_http_response.body|length }} bytes |
38 |
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 | |
45 |
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 | |
52 |
53 | {% include 'includes/details-button.html' with url=acs_http_response_detail_url %}
54 | |
55 |
56 | {% endfor %}
57 |
58 |
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 | ACS Queue Job |
15 | {% include 'includes/small-details-button.html' with url=acs_queue_job_detail_url %} {{ aqj.tag }} |
16 |
17 |
18 | ACS Device |
19 | {% include 'includes/small-details-button.html' with url=acsdevice_detail_url %} {{ aqj.acs_device }} |
20 |
21 |
22 | RPC Method |
23 | {{ aqj.cwmp_rpc_method }} |
24 |
25 | {% if aqj.handled_in %}
26 |
27 | Handled in HTTP Response |
28 | {% include 'includes/small-details-button.html' with url=handled_in_detail_url %}{{ aqj.handled_in }} |
29 |
30 | {% endif %}
31 |
32 | Processed |
33 | {{ aqj.processed|truefalseicon|safe }} |
34 |
35 |
36 | Reason |
37 | {{ aqj.reason }} |
38 |
39 |
40 | Automatic? |
41 | {{ aqj.automatic|truefalseicon|safe }} |
42 |
43 |
44 | Urgent? |
45 | {{ aqj.urgent|truefalseicon|safe }} |
46 |
47 |
48 | Notification Sent? |
49 | {{ aqj.notification_sent|truefalseicon|safe }} |
50 |
51 |
52 | XML |
53 | {{ aqj.cwmp_rpc_object_xml|prettyprintxml }} |
54 |
55 |
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 | ACS Queue Job |
11 | ACS Device |
12 | Created |
13 | RPC Method |
14 | Handled In |
15 | Processed |
16 | Reason |
17 | Automatic? |
18 | Urgent? |
19 | Notification Sent? |
20 | XML |
21 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_queue_job_detail_url %}{{ aqj.tag }} |
33 | {% include 'includes/small-details-button.html' with url=acsdevice_detail_url %}{{ aqj.acs_device.tag }} |
34 | {{ aqj.created_date }} |
35 | {{ aqj.cwmp_rpc_method }} |
36 |
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 | |
43 | {{ aqj.processed|truefalseicon|safe }} |
44 | {{ aqj.reason }} |
45 | {{ aqj.automatic|truefalseicon|safe }} |
46 | {{ aqj.urgent|truefalseicon|safe }} |
47 | {{ aqj.notification_sent|truefalseicon|safe }} |
48 | {{ aqj.cwmp_rpc_object_xml|length }} bytes |
49 |
50 | {% include 'includes/details-button.html' with url=acs_queue_job_detail_url %}
51 | |
52 |
53 | {% endfor %}
54 |
55 |
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 | ACS Session |
12 | {% include 'includes/small-details-button.html' with url=acs_http_detail_url %} {{ acs_session.tag }} |
13 |
14 |
15 | ACS Device |
16 | {% include 'includes/small-details-button.html' with url=acs_device_detail_url %} {{ acs_session.acs_device }} |
17 |
18 |
19 | Client IP |
20 | {{ acs_session.client_ip }} ({% if acs_session.client_ip_verified %}verified{% else %}not verified{% endif %}) |
21 |
22 |
23 | ACS Session ID |
24 | {{ acs_session.acs_session_id.hex }} |
25 |
26 |
27 | Inform Event Codes |
28 | {% for iec in acs_session.inform_eventcodes %}{{ iec }} {% endfor %} |
29 |
30 |
31 | ACS Session Result |
32 | {{ acs_session.session_result|truefalseicon|safe }} |
33 |
34 |
35 | ACS Device Uptime |
36 | {{ acs_session.device_uptime | datetimerange_as_pretty_delta }} |
37 |
38 |
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 | ACS Session |
21 | ACS Device |
22 | Client IP |
23 | Client IP Verified |
24 | Inform Event Codes |
25 | HTTP Request |
26 | HTTP Response |
27 | Start |
28 | Duration |
29 | Device Uptime |
30 | Last RPC Method |
31 | Bytes Received |
32 | Bytes Sent |
33 | Actions |
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 | {% include 'includes/small-details-button.html' with url=acs_session_detail_url %}{{ acs_session.tag }} |
42 | {% include 'includes/small-details-button.html' with url=acs_device_detail_url %}{{ acs_session.acs_device.tag }} |
43 | {{ acs_session.client_ip }} |
44 | {{ acs_session.client_ip_verified|truefalseicon|safe }} |
45 | {% for iec in acs_session.inform_eventcodes %}{{ iec }} {% endfor %} |
46 | {{ acs_session.acs_http_requests.count }} |
47 | {{ acs_session.acs_http_responses.count }} |
48 | {{ acs_session.start }} |
49 | {{ acs_session.duration | pretty_timedelta }} |
50 | {{ acs_session.device_uptime | datetimerange_as_pretty_delta }} |
51 | {{ acs_session.get_latest_http_tx.soap_element_ns }} |
52 | {{ acs_session.bytes_in }} bytes |
53 | {{ acs_session.bytes_out }} bytes |
54 |
55 | {% include 'includes/details-button.html' with url=acs_session_detail_url %}
56 | |
57 |
58 | {% endfor %}
59 |
60 |
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 |
--------------------------------------------------------------------------------