shadowsocks-manager 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shadowsocks_manager/__init__.py +0 -0
- shadowsocks_manager/__main__.py +45 -0
- shadowsocks_manager/args_formatter/__init__.py +74 -0
- shadowsocks_manager/args_formatter/tests.py +42 -0
- shadowsocks_manager/domain/__init__.py +0 -0
- shadowsocks_manager/domain/admin.py +78 -0
- shadowsocks_manager/domain/apps.py +10 -0
- shadowsocks_manager/domain/migrations/0001_initial.py +79 -0
- shadowsocks_manager/domain/migrations/__init__.py +0 -0
- shadowsocks_manager/domain/models.py +265 -0
- shadowsocks_manager/domain/serializers.py +25 -0
- shadowsocks_manager/domain/tests.py +138 -0
- shadowsocks_manager/domain/urls.py +18 -0
- shadowsocks_manager/domain/views.py +38 -0
- shadowsocks_manager/dynamicmethod/__init__.py +0 -0
- shadowsocks_manager/dynamicmethod/admin.py +8 -0
- shadowsocks_manager/dynamicmethod/apps.py +10 -0
- shadowsocks_manager/dynamicmethod/migrations/__init__.py +0 -0
- shadowsocks_manager/dynamicmethod/models.py +96 -0
- shadowsocks_manager/dynamicmethod/tests.py +8 -0
- shadowsocks_manager/dynamicmethod/views.py +8 -0
- shadowsocks_manager/manage.py +29 -0
- shadowsocks_manager/notification/__init__.py +0 -0
- shadowsocks_manager/notification/admin.py +18 -0
- shadowsocks_manager/notification/apps.py +10 -0
- shadowsocks_manager/notification/migrations/0001_initial.py +40 -0
- shadowsocks_manager/notification/migrations/__init__.py +0 -0
- shadowsocks_manager/notification/models.py +66 -0
- shadowsocks_manager/notification/serializers.py +13 -0
- shadowsocks_manager/notification/tests.py +93 -0
- shadowsocks_manager/notification/urls.py +16 -0
- shadowsocks_manager/notification/views.py +19 -0
- shadowsocks_manager/retry/__init__.py +68 -0
- shadowsocks_manager/retry/tests.py +42 -0
- shadowsocks_manager/shadowsocks_manager/__init__.py +6 -0
- shadowsocks_manager/shadowsocks_manager/celery.py +27 -0
- shadowsocks_manager/shadowsocks_manager/settings.py +217 -0
- shadowsocks_manager/shadowsocks_manager/urls.py +31 -0
- shadowsocks_manager/shadowsocks_manager/wsgi.py +19 -0
- shadowsocks_manager/shadowsocksz/__init__.py +0 -0
- shadowsocks_manager/shadowsocksz/admin.py +188 -0
- shadowsocks_manager/shadowsocksz/apps.py +10 -0
- shadowsocks_manager/shadowsocksz/management/__init__.py +0 -0
- shadowsocks_manager/shadowsocksz/management/commands/__init__.py +0 -0
- shadowsocks_manager/shadowsocksz/management/commands/shadowsocks_config.py +24 -0
- shadowsocks_manager/shadowsocksz/migrations/0001_initial.py +114 -0
- shadowsocks_manager/shadowsocksz/migrations/__init__.py +0 -0
- shadowsocks_manager/shadowsocksz/models.py +1024 -0
- shadowsocks_manager/shadowsocksz/serializers.py +41 -0
- shadowsocks_manager/shadowsocksz/tasks.py +21 -0
- shadowsocks_manager/shadowsocksz/tests.py +470 -0
- shadowsocks_manager/shadowsocksz/urls.py +20 -0
- shadowsocks_manager/shadowsocksz/views.py +53 -0
- shadowsocks_manager/singleton/__init__.py +0 -0
- shadowsocks_manager/singleton/admin.py +8 -0
- shadowsocks_manager/singleton/apps.py +10 -0
- shadowsocks_manager/singleton/migrations/__init__.py +0 -0
- shadowsocks_manager/singleton/models.py +46 -0
- shadowsocks_manager/singleton/tests.py +8 -0
- shadowsocks_manager/singleton/views.py +8 -0
- shadowsocks_manager/statistic/__init__.py +0 -0
- shadowsocks_manager/statistic/admin.py +78 -0
- shadowsocks_manager/statistic/apps.py +10 -0
- shadowsocks_manager/statistic/migrations/0001_initial.py +57 -0
- shadowsocks_manager/statistic/migrations/0002_auto_20210313_1419.py +19 -0
- shadowsocks_manager/statistic/migrations/__init__.py +0 -0
- shadowsocks_manager/statistic/models.py +308 -0
- shadowsocks_manager/statistic/serializers.py +19 -0
- shadowsocks_manager/statistic/tasks.py +17 -0
- shadowsocks_manager/statistic/tests.py +99 -0
- shadowsocks_manager/statistic/urls.py +16 -0
- shadowsocks_manager/statistic/views.py +26 -0
- shadowsocks_manager/utils/__init__.py +0 -0
- shadowsocks_manager/utils/celery.py +28 -0
- shadowsocks_manager/utils/createsuperuser.py +57 -0
- shadowsocks_manager/utils/dotenv.py +70 -0
- shadowsocks_manager/utils/manage.py +29 -0
- shadowsocks_manager/utils/uwsgi.py +27 -0
- shadowsocks_manager-0.1.1.data/scripts/ssm-dev-start +34 -0
- shadowsocks_manager-0.1.1.data/scripts/ssm-dev-stop +20 -0
- shadowsocks_manager-0.1.1.data/scripts/ssm-setup +221 -0
- shadowsocks_manager-0.1.1.data/scripts/ssm-test +113 -0
- shadowsocks_manager-0.1.1.dist-info/LICENSE +21 -0
- shadowsocks_manager-0.1.1.dist-info/METADATA +475 -0
- shadowsocks_manager-0.1.1.dist-info/RECORD +88 -0
- shadowsocks_manager-0.1.1.dist-info/WHEEL +5 -0
- shadowsocks_manager-0.1.1.dist-info/entry_points.txt +7 -0
- shadowsocks_manager-0.1.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
from docopt import docopt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
"""
|
|
10
|
+
Description:
|
|
11
|
+
Make the proxy call after adding the django root to the python path and changing the current directory to the django root.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
ssm COMMAND [OPTIONS]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
COMMAND The command to be run.
|
|
18
|
+
OPTIONS All options are transparently passing to the called command.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
None
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
$ ssm python manage.py runserver
|
|
25
|
+
$ ssm uwsgi --ini uwsgi.ini
|
|
26
|
+
$ ssm celery -A shadowsocks_manager worker -l info
|
|
27
|
+
"""
|
|
28
|
+
#docopt(main.__doc__)
|
|
29
|
+
|
|
30
|
+
django_root = os.path.dirname(os.path.abspath(__file__))
|
|
31
|
+
|
|
32
|
+
# add the django root to the python path to allow django commands to be run from any directory
|
|
33
|
+
if django_root not in sys.path:
|
|
34
|
+
sys.path.insert(0, django_root)
|
|
35
|
+
|
|
36
|
+
# change dir to the django root to allow the dir-sensitive commands(such as loaddata) to be run from any directory
|
|
37
|
+
os.chdir(django_root)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# make the proxy call
|
|
41
|
+
return subprocess.call(sys.argv[1:])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == '__main__':
|
|
45
|
+
sys.exit(main())
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# py2.7 and py3 compatibility imports
|
|
2
|
+
from __future__ import unicode_literals
|
|
3
|
+
from builtins import range
|
|
4
|
+
from builtins import object
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Formatter(object):
|
|
9
|
+
"""
|
|
10
|
+
Return the input-syntax string presentation of args and kwargs list, delimited by comma `, `.
|
|
11
|
+
The value of the args and kwargs are formatted as the canonical string presentation.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
>>> f = Formatter(*args, **kwargs)
|
|
15
|
+
>>> print(f)
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> print(Formatter('foo', 1, x='bar', y=2))
|
|
19
|
+
'foo', 1, x='bar', y=2
|
|
20
|
+
|
|
21
|
+
Use Case:
|
|
22
|
+
# log the input arguments of methods.
|
|
23
|
+
def foo(*args, **kwargs):
|
|
24
|
+
message = Formatter(*args, **kwargs).to_string())
|
|
25
|
+
logger.debug('foo: {}'.format(message))
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, *args, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the Formatter to have two members: args, kwargs
|
|
31
|
+
"""
|
|
32
|
+
self.args = args
|
|
33
|
+
self.kwargs = kwargs
|
|
34
|
+
|
|
35
|
+
def __str__(self, *args, **kwargs):
|
|
36
|
+
return self.to_string(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
def args_to_string(self):
|
|
39
|
+
"""
|
|
40
|
+
Return the canonical string presentation of args list, delimited by comma `, `.
|
|
41
|
+
"""
|
|
42
|
+
if self.args:
|
|
43
|
+
self.args = [repr(arg) for arg in self.args]
|
|
44
|
+
formatter = ['{}' for count in range(len(self.args))]
|
|
45
|
+
return ', '.join(formatter).format(*self.args)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def kwargs_to_string(self):
|
|
49
|
+
"""
|
|
50
|
+
Return the input-syntax string presentation of kwargs list, delimited by comma `, `.
|
|
51
|
+
The value of the kwargs are formatted as the canonical string presentation.
|
|
52
|
+
"""
|
|
53
|
+
if self.kwargs:
|
|
54
|
+
formatter = ['{}={}' for count in range(len(self.kwargs))]
|
|
55
|
+
return ', '.join(formatter).format(
|
|
56
|
+
*reduce((lambda x, y: x + y), [list([k, repr(v)]) for k,v in list(self.kwargs.items())]))
|
|
57
|
+
|
|
58
|
+
def to_string(self):
|
|
59
|
+
"""
|
|
60
|
+
Return the input-syntax string presentation of args and kwargs list, delimited by comma `, `.
|
|
61
|
+
The value of the args and kwargs are formatted as the canonical string presentation.
|
|
62
|
+
Be noted to the differences:
|
|
63
|
+
* An empty string `` is returned if no args and kwargs found.
|
|
64
|
+
* The input empty string `` is returned as the literal "``" with the additional quoting.
|
|
65
|
+
* The input NoneType None is returned as the literal `None` without additional quoting.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> Formatter('foo', 1, x='bar', y=2).to_string()
|
|
69
|
+
"'foo', 1, x='bar', y=2"
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
items = [self.args_to_string(), self.kwargs_to_string()]
|
|
73
|
+
items = [item for item in items if item is not None]
|
|
74
|
+
return ', '.join(items)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# py2.7 and py3 compatibility imports
|
|
4
|
+
from __future__ import unicode_literals
|
|
5
|
+
from __future__ import absolute_import
|
|
6
|
+
from builtins import str
|
|
7
|
+
|
|
8
|
+
from unittest import TestCase
|
|
9
|
+
|
|
10
|
+
from . import Formatter
|
|
11
|
+
|
|
12
|
+
# Create your tests here.
|
|
13
|
+
class FormatterTestCase(TestCase):
|
|
14
|
+
def test(self):
|
|
15
|
+
# no args and kwargs
|
|
16
|
+
self.assertEqual(Formatter().to_string(), '')
|
|
17
|
+
# one arg as None
|
|
18
|
+
self.assertEqual(Formatter(None).to_string(), 'None')
|
|
19
|
+
# one arg as empty string
|
|
20
|
+
self.assertEqual(Formatter('').to_string(), repr(''))
|
|
21
|
+
# two simple args
|
|
22
|
+
self.assertEqual(Formatter('foo', 1).to_string(), '{}, 1'.format(repr('foo')))
|
|
23
|
+
# one arg as list
|
|
24
|
+
self.assertEqual(Formatter(['foo', 1]).to_string(), '[{}, 1]'.format(repr('foo')))
|
|
25
|
+
# one arg as dict
|
|
26
|
+
self.assertEqual(Formatter({'foo': 1}).to_string(), repr({'foo': 1}))
|
|
27
|
+
# on arg as tuple
|
|
28
|
+
self.assertEqual(Formatter(('foo', 1)).to_string(), repr(('foo', 1)))
|
|
29
|
+
# two simple kwargs, take care of the order, so works with both py2 and py3
|
|
30
|
+
self.assertEqual(Formatter(y=2, x='bar').to_string(), 'y=2, x={}'.format(repr('bar')))
|
|
31
|
+
# one kwarg as list
|
|
32
|
+
self.assertEqual(Formatter(x=['bar', 2]).to_string(), 'x={}'.format(repr(['bar', 2])))
|
|
33
|
+
# one kwarg as dict
|
|
34
|
+
self.assertEqual(Formatter(x={'bar': 2}).to_string(), 'x={}'.format(repr({'bar': 2})))
|
|
35
|
+
# one kwarg as tuple
|
|
36
|
+
self.assertEqual(Formatter(x=('bar', 2)).to_string(), 'x={}'.format(repr(('bar', 2))))
|
|
37
|
+
# two simple args and two simple kwargs, take care of the order, so works with both py2 and py3
|
|
38
|
+
self.assertEqual(
|
|
39
|
+
Formatter('foo', 1, y=2, x='bar').to_string(),
|
|
40
|
+
'{}, 1, y=2, x={}'.format(repr('foo'), repr('bar')))
|
|
41
|
+
# __str__()
|
|
42
|
+
self.assertEqual(str(Formatter(None)), 'None')
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# py2.7 and py3 compatibility imports
|
|
4
|
+
from __future__ import unicode_literals
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from django.contrib import admin, messages
|
|
8
|
+
|
|
9
|
+
from .models import NameServer, Domain, Record
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Register your models here.
|
|
13
|
+
|
|
14
|
+
@admin.register(NameServer)
|
|
15
|
+
class NameServerAdmin(admin.ModelAdmin):
|
|
16
|
+
fields = ('name', 'api_cls_name', 'user', 'credential',
|
|
17
|
+
'dt_created', 'dt_updated')
|
|
18
|
+
|
|
19
|
+
readonly_fields = ('dt_created', 'dt_updated')
|
|
20
|
+
|
|
21
|
+
list_display = ('name', 'api_cls_name', 'user', 'is_api_accessible',
|
|
22
|
+
'dt_created', 'dt_updated')
|
|
23
|
+
|
|
24
|
+
def is_api_accessible(self, obj):
|
|
25
|
+
return obj.is_api_accessible
|
|
26
|
+
|
|
27
|
+
is_api_accessible.boolean = True
|
|
28
|
+
is_api_accessible.short_description = 'API'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@admin.register(Domain)
|
|
32
|
+
class DomainAdmin(admin.ModelAdmin):
|
|
33
|
+
fields = ('name', 'nameserver',
|
|
34
|
+
'dt_created', 'dt_updated')
|
|
35
|
+
|
|
36
|
+
readonly_fields = ('dt_created', 'dt_updated')
|
|
37
|
+
|
|
38
|
+
list_display = ('name', 'nameserver',
|
|
39
|
+
'dt_created', 'dt_updated')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@admin.register(Record)
|
|
43
|
+
class RecordAdmin(admin.ModelAdmin):
|
|
44
|
+
fields = ('host', 'domain', 'type', 'answer', 'site',
|
|
45
|
+
'dt_created', 'dt_updated')
|
|
46
|
+
|
|
47
|
+
readonly_fields = ('dt_created', 'dt_updated')
|
|
48
|
+
|
|
49
|
+
list_display = ('host', 'domain', 'type', 'answer', 'answer_from_dns_api', 'is_matching_dns_api',
|
|
50
|
+
'answer_from_dns_query', 'is_matching_dns_query', 'site',
|
|
51
|
+
'dt_created', 'dt_updated')
|
|
52
|
+
|
|
53
|
+
def answer_from_dns_api(self, obj):
|
|
54
|
+
return list(obj.answer_from_dns_api) if obj.answer_from_dns_api is not None else obj.answer_from_dns_api
|
|
55
|
+
|
|
56
|
+
def answer_from_dns_query(self, obj):
|
|
57
|
+
return list(obj.answer_from_dns_query) if obj.answer_from_dns_query is not None else obj.answer_from_dns_query
|
|
58
|
+
|
|
59
|
+
def is_matching_dns_api(self, obj):
|
|
60
|
+
return obj.is_matching_dns_api
|
|
61
|
+
|
|
62
|
+
is_matching_dns_api.boolean = True
|
|
63
|
+
is_matching_dns_api.short_description = 'DNS API'
|
|
64
|
+
|
|
65
|
+
def is_matching_dns_query(self, obj):
|
|
66
|
+
return obj.is_matching_dns_query
|
|
67
|
+
|
|
68
|
+
is_matching_dns_query.boolean = True
|
|
69
|
+
is_matching_dns_query.short_description = 'DNS Query'
|
|
70
|
+
|
|
71
|
+
def sync_to_dns(self, request, queryset):
|
|
72
|
+
for obj in queryset:
|
|
73
|
+
result = obj.sync_to_dns()
|
|
74
|
+
messages.info(request, '{0}: {1}'.format(obj.host, json.dumps(result)))
|
|
75
|
+
|
|
76
|
+
sync_to_dns.short_description = 'Synchronize DNS records to DNS server for Selected Domain'
|
|
77
|
+
|
|
78
|
+
actions = (sync_to_dns,)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by Django 1.11.29 on 2024-04-21 20:23
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
import django.contrib.sites.models
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
import django.db.models.deletion
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
|
|
12
|
+
initial = True
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
('sites', '0002_alter_domain_unique'),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='Domain',
|
|
21
|
+
fields=[
|
|
22
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
23
|
+
('name', models.CharField(help_text='Root domain name. Example: yourdomain.com.', max_length=64, unique=True)),
|
|
24
|
+
('dt_created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
|
25
|
+
('dt_updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
|
26
|
+
],
|
|
27
|
+
),
|
|
28
|
+
migrations.CreateModel(
|
|
29
|
+
name='NameServer',
|
|
30
|
+
fields=[
|
|
31
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
32
|
+
('name', models.CharField(help_text='The name for the Nameserver, name it as your wish. Example: name.com.', max_length=64, unique=True)),
|
|
33
|
+
('api_cls_name', models.CharField(choices=[('NameNsApi', 'NameNsApi')], help_text='Select the API class name for the Nameserver.', max_length=32, verbose_name='API Class')),
|
|
34
|
+
('user', models.CharField(blank=True, help_text='User identity for the Nameserver API service.', max_length=64, null=True)),
|
|
35
|
+
('credential', models.CharField(blank=True, help_text='User credential/token for the Nameserver API service.', max_length=128, null=True)),
|
|
36
|
+
('dt_created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
|
37
|
+
('dt_updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
|
38
|
+
],
|
|
39
|
+
),
|
|
40
|
+
migrations.CreateModel(
|
|
41
|
+
name='Record',
|
|
42
|
+
fields=[
|
|
43
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
44
|
+
('host', models.CharField(help_text='Host name. Example: vpn.', max_length=64)),
|
|
45
|
+
('type', models.CharField(blank=True, choices=[('A', 'A'), ('MX', 'MX'), ('CNAME', 'CNAME'), ('TXT', 'TXT'), ('SRV', 'SRV'), ('AAAA', 'AAAA'), ('NS', 'NS'), ('ANAME', 'ANAME')], max_length=8, null=True)),
|
|
46
|
+
('answer', models.CharField(blank=True, help_text='Answer for the host name, comma "," is the delimiter for multiple answers.', max_length=512, null=True)),
|
|
47
|
+
('dt_created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
|
48
|
+
('dt_updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
|
49
|
+
('domain', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='domain.Domain')),
|
|
50
|
+
],
|
|
51
|
+
),
|
|
52
|
+
migrations.CreateModel(
|
|
53
|
+
name='Site',
|
|
54
|
+
fields=[
|
|
55
|
+
],
|
|
56
|
+
options={
|
|
57
|
+
'proxy': True,
|
|
58
|
+
'indexes': [],
|
|
59
|
+
},
|
|
60
|
+
bases=('sites.site',),
|
|
61
|
+
managers=[
|
|
62
|
+
('objects', django.contrib.sites.models.SiteManager()),
|
|
63
|
+
],
|
|
64
|
+
),
|
|
65
|
+
migrations.AddField(
|
|
66
|
+
model_name='record',
|
|
67
|
+
name='site',
|
|
68
|
+
field=models.ForeignKey(blank=True, help_text="The record with a site will be dynamically added to Django's ALLOWED_HOSTS.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='records', to='domain.Site'),
|
|
69
|
+
),
|
|
70
|
+
migrations.AddField(
|
|
71
|
+
model_name='domain',
|
|
72
|
+
name='nameserver',
|
|
73
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='domain.NameServer'),
|
|
74
|
+
),
|
|
75
|
+
migrations.AlterUniqueTogether(
|
|
76
|
+
name='record',
|
|
77
|
+
unique_together=set([('host', 'domain')]),
|
|
78
|
+
),
|
|
79
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# py2.7 and py3 compatibility imports
|
|
4
|
+
from __future__ import unicode_literals
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import socket, requests, json
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.db.models.signals import post_save, post_delete
|
|
11
|
+
from django.dispatch import receiver
|
|
12
|
+
from django.contrib.sites.models import Site
|
|
13
|
+
from django.conf import settings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger('django')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Create your models here.
|
|
20
|
+
|
|
21
|
+
class Site(Site):
|
|
22
|
+
|
|
23
|
+
class Meta:
|
|
24
|
+
proxy = True
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return self.name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseNsApi(object):
|
|
31
|
+
|
|
32
|
+
def __init__(self, user, credential, *args, **kwargs):
|
|
33
|
+
super(BaseNsApi, self).__init__(*args, **kwargs)
|
|
34
|
+
self.user = user
|
|
35
|
+
self.credential = credential
|
|
36
|
+
|
|
37
|
+
def call_api(self, url, method='get', data=None):
|
|
38
|
+
response = getattr(requests, method)(
|
|
39
|
+
url,
|
|
40
|
+
auth=(self.user, self.credential),
|
|
41
|
+
json=data
|
|
42
|
+
)
|
|
43
|
+
return response.json()
|
|
44
|
+
|
|
45
|
+
def create_record(self, *args, **kwargs):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def list_records(self, *args, **kwargs):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def delete_records(self, *args, **kwargs):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NameNsApi(BaseNsApi):
|
|
56
|
+
|
|
57
|
+
api_base_url = 'https://api.name.com/v4'
|
|
58
|
+
|
|
59
|
+
def create_record(self, domain, type, host, answer, ttl=300):
|
|
60
|
+
url = "/".join([self.api_base_url, 'domains', domain, 'records'])
|
|
61
|
+
data = {"host": host, "type": type, "answer": answer, "ttl": ttl}
|
|
62
|
+
|
|
63
|
+
return self.call_api(url, method='post', data=data)
|
|
64
|
+
|
|
65
|
+
def list_records(self, domain, type, host):
|
|
66
|
+
url = "/".join([self.api_base_url, 'domains', domain, 'records'])
|
|
67
|
+
|
|
68
|
+
records = self.call_api(url, method='get') or {}
|
|
69
|
+
return [item for item in records.get('records', [])
|
|
70
|
+
if item.get('type') == type and item.get('host') == host]
|
|
71
|
+
|
|
72
|
+
def delete_records(self, domain, type, host):
|
|
73
|
+
records = self.list_records(domain, type, host)
|
|
74
|
+
|
|
75
|
+
for item in records:
|
|
76
|
+
url = "/".join([self.api_base_url, 'domains', domain, 'records', str(item.get('id'))])
|
|
77
|
+
self.call_api(url, method='delete')
|
|
78
|
+
|
|
79
|
+
return records
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def is_accessible(self):
|
|
83
|
+
try:
|
|
84
|
+
url = "/".join([self.api_base_url, 'hello'])
|
|
85
|
+
return self.call_api(url, method='get').get('username') == self.user
|
|
86
|
+
except:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class NameServer(models.Model):
|
|
91
|
+
API_CLASS_NAME = [
|
|
92
|
+
('NameNsApi', 'NameNsApi')
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
name = models.CharField(unique=True, max_length=64,
|
|
96
|
+
help_text='The name for the Nameserver, name it as your wish. Example: name.com.')
|
|
97
|
+
api_cls_name = models.CharField('API Class', max_length=32, choices=API_CLASS_NAME,
|
|
98
|
+
help_text='Select the API class name for the Nameserver.')
|
|
99
|
+
user = models.CharField(max_length=64, null=True, blank=True,
|
|
100
|
+
help_text='User identity for the Nameserver API service.')
|
|
101
|
+
credential = models.CharField(max_length=128, null=True, blank=True,
|
|
102
|
+
help_text='User credential/token for the Nameserver API service.')
|
|
103
|
+
dt_created = models.DateTimeField('Created', auto_now_add=True)
|
|
104
|
+
dt_updated = models.DateTimeField('Updated', auto_now=True)
|
|
105
|
+
|
|
106
|
+
def __str__(self):
|
|
107
|
+
return self.name
|
|
108
|
+
|
|
109
|
+
def __init__(self, *args, **kwargs):
|
|
110
|
+
super(NameServer, self).__init__(*args, **kwargs)
|
|
111
|
+
|
|
112
|
+
self.api_cls = globals().get(self.api_cls_name)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def api(self):
|
|
116
|
+
if self.api_cls and self.user and self.credential:
|
|
117
|
+
return self.api_cls(self.user, self.credential)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def is_api_accessible(self):
|
|
121
|
+
return self.api.is_accessible if self.api else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Domain(models.Model):
|
|
125
|
+
name = models.CharField(unique=True, max_length=64,
|
|
126
|
+
help_text='Root domain name. Example: yourdomain.com.')
|
|
127
|
+
nameserver = models.ForeignKey(NameServer, null=True, blank=True, on_delete=models.SET_NULL)
|
|
128
|
+
dt_created = models.DateTimeField('Created', auto_now_add=True)
|
|
129
|
+
dt_updated = models.DateTimeField('Updated', auto_now=True)
|
|
130
|
+
|
|
131
|
+
def __str__(self):
|
|
132
|
+
return self.name
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def ns_api(self):
|
|
136
|
+
return self.nameserver.api if self.nameserver else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Record(models.Model):
|
|
140
|
+
TYPE = [
|
|
141
|
+
('A', 'A'),
|
|
142
|
+
('MX', 'MX'),
|
|
143
|
+
('CNAME', 'CNAME'),
|
|
144
|
+
('TXT', 'TXT'),
|
|
145
|
+
('SRV', 'SRV'),
|
|
146
|
+
('AAAA', 'AAAA'),
|
|
147
|
+
('NS', 'NS'),
|
|
148
|
+
('ANAME', 'ANAME'),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
host = models.CharField(max_length=64,
|
|
152
|
+
help_text='Host name. Example: vpn.')
|
|
153
|
+
domain = models.ForeignKey(Domain, on_delete=models.PROTECT)
|
|
154
|
+
type = models.CharField(max_length=8, null=True, blank=True, choices=TYPE)
|
|
155
|
+
answer = models.CharField(max_length=512, null=True, blank=True,
|
|
156
|
+
help_text='Answer for the host name, comma "," is the delimiter for multiple answers.')
|
|
157
|
+
site = models.ForeignKey(Site, null=True, blank=True, on_delete=models.SET_NULL, related_name='records',
|
|
158
|
+
help_text="The record with a site will be dynamically added to Django's ALLOWED_HOSTS.")
|
|
159
|
+
dt_created = models.DateTimeField('Created', auto_now_add=True)
|
|
160
|
+
dt_updated = models.DateTimeField('Updated', auto_now=True)
|
|
161
|
+
|
|
162
|
+
class Meta:
|
|
163
|
+
unique_together = ('host', 'domain')
|
|
164
|
+
|
|
165
|
+
def __str__(self):
|
|
166
|
+
return self.fqdn
|
|
167
|
+
|
|
168
|
+
def save(self, *args, **kwargs):
|
|
169
|
+
super(Record, self).save(*args, **kwargs)
|
|
170
|
+
if self.site:
|
|
171
|
+
self.site.domain = self.fqdn
|
|
172
|
+
self.site.save()
|
|
173
|
+
settings.ALLOWED_HOSTS.update_cache()
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def fqdn(self):
|
|
177
|
+
return '.'.join([self.host, self.domain.name])
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def answers(self):
|
|
181
|
+
"""
|
|
182
|
+
Return the record.answer in lowercase and split as Set.
|
|
183
|
+
"""
|
|
184
|
+
return {
|
|
185
|
+
item.lower()
|
|
186
|
+
for item in (self.answer.split(',') if self.answer else [])
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def answer_from_dns_api(self):
|
|
191
|
+
"""
|
|
192
|
+
Return the answers from DNS API, in lowercase and as Set.
|
|
193
|
+
"""
|
|
194
|
+
if self.domain.ns_api:
|
|
195
|
+
return {
|
|
196
|
+
record.get('answer').lower()
|
|
197
|
+
for record in self.domain.ns_api.list_records(
|
|
198
|
+
self.domain.name, self.type, self.host
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def answer_from_dns_query(self):
|
|
204
|
+
"""
|
|
205
|
+
Return the answers from DNS query, in lowercase and as Set.
|
|
206
|
+
"""
|
|
207
|
+
ips = []
|
|
208
|
+
try:
|
|
209
|
+
truename, alias, ips = socket.gethostbyname_ex(self.fqdn)
|
|
210
|
+
except socket.gaierror:
|
|
211
|
+
# not found the host
|
|
212
|
+
pass
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(e)
|
|
215
|
+
return {item.lower() for item in ips}
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def is_matching_dns_api(self):
|
|
219
|
+
"""
|
|
220
|
+
Test if the record.answer matches the DNS API query.
|
|
221
|
+
"""
|
|
222
|
+
return self.answers == self.answer_from_dns_api
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def is_matching_dns_query(self):
|
|
226
|
+
"""
|
|
227
|
+
Test if the record.answer matches the DNS query.
|
|
228
|
+
"""
|
|
229
|
+
return self.answers == self.answer_from_dns_query
|
|
230
|
+
|
|
231
|
+
def sync_to_dns(self):
|
|
232
|
+
"""
|
|
233
|
+
Sync the record to DNS server through DNS API.
|
|
234
|
+
"""
|
|
235
|
+
ret = defaultdict(list)
|
|
236
|
+
if self.domain.ns_api:
|
|
237
|
+
if self.is_matching_dns_api:
|
|
238
|
+
ret['message'] = 'No need to synchronize.'
|
|
239
|
+
return ret
|
|
240
|
+
|
|
241
|
+
ret['deleted'] = self.delete_from_dns()
|
|
242
|
+
for answer in (self.answer or '').split(','):
|
|
243
|
+
ret['created'].append(
|
|
244
|
+
self.domain.ns_api.create_record(self.domain.name, self.type, self.host, answer))
|
|
245
|
+
else:
|
|
246
|
+
ret['message'] = 'Please configure Nameserver and its User and Credential for the domain first.'
|
|
247
|
+
|
|
248
|
+
return ret
|
|
249
|
+
|
|
250
|
+
def delete_from_dns(self):
|
|
251
|
+
"""
|
|
252
|
+
Delete the record from DNS server through DNS API.
|
|
253
|
+
"""
|
|
254
|
+
if self.domain.ns_api:
|
|
255
|
+
return self.domain.ns_api.delete_records(self.domain.name, self.type, self.host)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@receiver(post_save, sender=Record)
|
|
259
|
+
def record_sync_to_dns(sender, instance, **kwargs):
|
|
260
|
+
instance.sync_to_dns()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@receiver(post_delete, sender=Record)
|
|
264
|
+
def record_delete_from_dns(sender, instance, **kwargs):
|
|
265
|
+
instance.delete_from_dns()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# py2.7 and py3 compatibility imports
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
|
|
7
|
+
from . import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NameServerSerializer(serializers.ModelSerializer):
|
|
11
|
+
class Meta:
|
|
12
|
+
model = models.NameServer
|
|
13
|
+
fields = ('id', 'name', 'api_cls_name', 'user', 'credential', 'dt_created', 'dt_updated')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DomainSerializer(serializers.ModelSerializer):
|
|
17
|
+
class Meta:
|
|
18
|
+
model = models.Domain
|
|
19
|
+
fields = ('id', 'name', 'nameserver', 'dt_created', 'dt_updated')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RecordSerializer(serializers.ModelSerializer):
|
|
23
|
+
class Meta:
|
|
24
|
+
model = models.Record
|
|
25
|
+
fields = ('id', 'host', 'domain', 'type', 'answer', 'site', 'dt_created', 'dt_updated')
|