mirror of
https://github.com/fergalmoran/bitchmin.git
synced 2025-12-22 09:27:53 +00:00
Initial unit tests done
This commit is contained in:
@@ -52,7 +52,7 @@ def login():
|
|||||||
'accessToken': access_token,
|
'accessToken': access_token,
|
||||||
'refreshToken': refresh_token,
|
'refreshToken': refresh_token,
|
||||||
'user': {
|
'user': {
|
||||||
'fullName': user.fullName
|
'fullName': user.full_name
|
||||||
}
|
}
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from IPy import IP
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.api import api
|
from app.api import api
|
||||||
from app.models import DnsUpdate
|
from app.models import User, DnsHost, DnsZone
|
||||||
from app.models import User
|
|
||||||
from app.utils import dnsupdate
|
from app.utils import dnsupdate
|
||||||
from app.utils.dnsupdate import delete_record
|
from app.utils.dnsupdate import delete_record
|
||||||
from app.utils.dnsutils import get_dns_records
|
from app.utils.dnsutils import get_dns_records
|
||||||
@@ -18,6 +17,11 @@ from app.utils.iputils import is_valid_ip, is_valid_hostname
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@api.route('/dns/hosts')
|
||||||
|
def get_hosts():
|
||||||
|
return DnsZone.get_delete_put_post()
|
||||||
|
|
||||||
|
|
||||||
@api.route('/dns/refresh', methods=['POST'])
|
@api.route('/dns/refresh', methods=['POST'])
|
||||||
@jwt_required
|
@jwt_required
|
||||||
def refresh_dns():
|
def refresh_dns():
|
||||||
@@ -94,7 +98,7 @@ def delete_dns_record():
|
|||||||
os.getenv('DNS_KEY'),
|
os.getenv('DNS_KEY'),
|
||||||
host)
|
host)
|
||||||
|
|
||||||
records = DnsUpdate.query.filter_by(host=host).all()
|
records = db.session(DnsHost).query.filter_by(host=host).all()
|
||||||
for record in records:
|
for record in records:
|
||||||
db.session.delete(record)
|
db.session.delete(record)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -135,7 +139,7 @@ def update_dns():
|
|||||||
'payload': '{} is not a valid IP address'.format(hostname)
|
'payload': '{} is not a valid IP address'.format(hostname)
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
count = DnsUpdate.query.filter_by(host=hostname).count()
|
count = db.session(DnsHost).query.filter_by(host=hostname).count()
|
||||||
if count != 0:
|
if count != 0:
|
||||||
logger.warning('HOST {} is already in the system'.format(hostname))
|
logger.warning('HOST {} is already in the system'.format(hostname))
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -151,7 +155,7 @@ def update_dns():
|
|||||||
args['ip'])
|
args['ip'])
|
||||||
|
|
||||||
if update_result:
|
if update_result:
|
||||||
update = DnsUpdate(args['host'], ip, user)
|
update = db.session(DnsHost)(args['host'], ip, user)
|
||||||
db.session.add(update)
|
db.session.add(update)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -169,7 +173,7 @@ def update_dns():
|
|||||||
@jwt_required
|
@jwt_required
|
||||||
def get_dns_list():
|
def get_dns_list():
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
updates = DnsUpdate \
|
updates = db.session(DnsHost) \
|
||||||
.query \
|
.query \
|
||||||
.filter(User.id == user.id) \
|
.filter(User.id == user.id) \
|
||||||
.all()
|
.all()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def get_user():
|
|||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
if user:
|
if user:
|
||||||
user = {
|
user = {
|
||||||
'fullName': user.fullName,
|
'fullName': user.full_name,
|
||||||
}
|
}
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ basedir = os.path.abspath(os.path.dirname(__file__))
|
|||||||
load_dotenv(os.path.join(basedir, '../.env'))
|
load_dotenv(os.path.join(basedir, '../.env'))
|
||||||
|
|
||||||
ISDEV = os.getenv('FLASK_ENV') == 'development'
|
ISDEV = os.getenv('FLASK_ENV') == 'development'
|
||||||
|
DEBUG_CONNECTION = 'postgresql+psycopg2://bitchmin:bitchmin@localhost/bitchmin'
|
||||||
|
# DEBUG_CONNECTION = 'sqlite:///' + os.path.join(basedir, '../app.db')
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
ISDEV = ISDEV
|
ISDEV = ISDEV
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY') or 'you-will-never-guess'
|
SECRET_KEY = os.getenv('SECRET_KEY') or 'you-will-never-guess'
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or DEBUG_CONNECTION
|
||||||
'postgresql+psycopg2://bitchmin:bitchmin@localhost/bitchmin'
|
|
||||||
# 'sqlite:///' + os.path.join(basedir, '../app.db')
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
LOG_TO_STDOUT = os.getenv('LOG_TO_STDOUT')
|
LOG_TO_STDOUT = os.getenv('LOG_TO_STDOUT')
|
||||||
ADMINS = ['Ferg@lMoran.me']
|
ADMINS = ['Ferg@lMoran.me']
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from .dnsupdate import DnsUpdate
|
from .dns import DnsZone, DnsNameServer, DnsHost
|
||||||
from .user import User
|
from .user import User
|
||||||
from .bindstate import BindState
|
|
||||||
89
bitchmin-api/app/models/dns.py
Normal file
89
bitchmin-api/app/models/dns.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask_serialize import FlaskSerializeMixin
|
||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.orm import relationship, validates, MapperExtension
|
||||||
|
from sqlalchemy_utils import IPAddressType
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models._basemodel import _BaseModelMixin
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DnsNameServer(db.Model, _BaseModelMixin, FlaskSerializeMixin):
|
||||||
|
__tablename__ = 'dns_nameservers'
|
||||||
|
|
||||||
|
host = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
|
ip = db.Column(IPAddressType(255), nullable=False)
|
||||||
|
zone_id = db.Column(db.Integer, db.ForeignKey('dns_zones.id'))
|
||||||
|
zone = relationship("DnsZone", back_populates="nameservers")
|
||||||
|
|
||||||
|
def __init__(self, zone, host, ip):
|
||||||
|
self.zone = zone
|
||||||
|
self.host = host
|
||||||
|
self.ip = ip
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DnsZone(db.Model, _BaseModelMixin, FlaskSerializeMixin):
|
||||||
|
__tablename__ = 'dns_zones'
|
||||||
|
relationship_fields = ['nameservers', 'hosts']
|
||||||
|
|
||||||
|
def __init__(self, zone_name):
|
||||||
|
self.zone_name = zone_name
|
||||||
|
self.serial = self._create_serial()
|
||||||
|
|
||||||
|
def get_serial_increment(self):
|
||||||
|
return int(str(self.serial)[8:])
|
||||||
|
|
||||||
|
def _create_serial(self):
|
||||||
|
return datetime.today().strftime('%Y%m%d{:02d}'.format(0))
|
||||||
|
|
||||||
|
def increment_serial(self):
|
||||||
|
current = str(self.serial)
|
||||||
|
if current and len(current) == 10:
|
||||||
|
inc = self.get_serial_increment()
|
||||||
|
return datetime.today().strftime('%Y%m%d{:02d}'.format(inc + 1))
|
||||||
|
|
||||||
|
return self._create_serial()
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
zone_name = db.Column(db.String(253), unique=True, nullable=False)
|
||||||
|
serial = db.Column(db.Integer, default=_create_serial)
|
||||||
|
|
||||||
|
hosts = relationship("DnsHost", back_populates="zone")
|
||||||
|
nameservers = relationship("DnsNameServer", back_populates="zone")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DnsHost(db.Model, _BaseModelMixin, FlaskSerializeMixin):
|
||||||
|
__tablename__ = 'dns_hosts'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
zone_id = db.Column(db.Integer, db.ForeignKey('dns_zones.id'))
|
||||||
|
zone = relationship("DnsZone", back_populates="hosts")
|
||||||
|
|
||||||
|
host = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
|
ip = db.Column(IPAddressType(255), nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, zone, host, ip):
|
||||||
|
self.zone = zone
|
||||||
|
self.host = host
|
||||||
|
self.ip = ip
|
||||||
|
pass
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(id=self.id, host=self.host, ip=self.ip)
|
||||||
|
|
||||||
|
|
||||||
|
# @event.listens_for(DnsZone.hosts, 'append')
|
||||||
|
# @event.listens_for(DnsZone.hosts, 'remove')
|
||||||
|
# def increment_zone_serial_handler(target, value, initiator):
|
||||||
|
# target.serial = target.increment_serial()
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(DnsHost.ip, 'set', active_history=True)
|
||||||
|
def increment_zone_serial_handler(target, value, old, initiator):
|
||||||
|
if target.zone:
|
||||||
|
target.zone.serial = target.zone.increment_serial()
|
||||||
@@ -12,17 +12,15 @@ from app.models._basemodel import _BaseModelMixin
|
|||||||
class User(db.Model, _BaseModelMixin):
|
class User(db.Model, _BaseModelMixin):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
id: str
|
id: str
|
||||||
fullName: str
|
full_name: str
|
||||||
|
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
fullName = db.Column(db.String(120), unique=False, nullable=True)
|
full_name = db.Column(db.String(120), unique=False, nullable=True)
|
||||||
password = db.Column(db.String(255), nullable=False)
|
password = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
dns_updates = relationship("DnsUpdate")
|
def __init__(self, email, full_name, password):
|
||||||
|
|
||||||
def __init__(self, email, fullName, password):
|
|
||||||
self.email = email
|
self.email = email
|
||||||
self.fullName = fullName
|
self.full_name = full_name
|
||||||
self.password = generate_password_hash(password, method='sha256')
|
self.password = generate_password_hash(password, method='sha256')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
0
bitchmin-api/app/serialisers/__init__.py
Normal file
0
bitchmin-api/app/serialisers/__init__.py
Normal file
4
bitchmin-api/app/serialisers/zone.py
Normal file
4
bitchmin-api/app/serialisers/zone.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# class ZoneSerializerMixin(SerializerMixin):
|
||||||
|
# serialize_types = (
|
||||||
|
# (UUID, lambda x: str(x)),
|
||||||
|
# )
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
from ipaddress import ip_address
|
import logging
|
||||||
|
|
||||||
from flask_mail import Message
|
|
||||||
import os
|
import os
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from app import create_celery_app, mail, db
|
from app import create_celery_app, db
|
||||||
from app.models import DnsUpdate, BindState, dnsupdate
|
from app.models import DnsHost, DnsNameServer
|
||||||
import logging
|
|
||||||
from app.utils import dnsupdate, iputils
|
from app.utils import dnsupdate, iputils
|
||||||
from app.utils.twilio import send_sms
|
from app.utils.twilio import send_sms
|
||||||
|
|
||||||
@@ -22,67 +20,76 @@ def check_host_records():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info('Checking for existing state')
|
logger.info('Checking for existing state')
|
||||||
bind_state = BindState.query.one()
|
bind_states = db.session.query(DnsNameServer)
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
bind_state = BindState(
|
send_sms(
|
||||||
nameserver_1_ip=platform_ip,
|
os.getenv('SMS_NOTIFY_TO'),
|
||||||
nameserver_1_host=os.getenv('DNSro_SERVER')
|
os.getenv('SMS_NOTIFY_FROM'),
|
||||||
)
|
'BITCHMIN: No hosts found\nPlatform IP {}'.format(
|
||||||
db.session.add(bind_state)
|
platform_ip
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
previous_ip = bind_state.nameserver_1_ip
|
|
||||||
if ip_address(platform_ip) != previous_ip:
|
|
||||||
logger.info('External IP has changed')
|
|
||||||
bind_state.nameserver_1_ip = platform_ip
|
|
||||||
|
|
||||||
logger.info('Updating nameserver record')
|
|
||||||
dnsupdate.update_dns(
|
|
||||||
os.getenv('DNS_SERVER'),
|
|
||||||
os.getenv('DNS_ZONE'),
|
|
||||||
os.getenv('DNS_KEY'),
|
|
||||||
bind_state.nameserver_1_host,
|
|
||||||
platform_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info('Checking host records')
|
|
||||||
hosts = DnsUpdate.query.filter_by(ip=previous_ip)
|
|
||||||
result = 'Here are the hosts we have\n'
|
|
||||||
for host in hosts:
|
|
||||||
result += '\tHost: {} IP: {} Date Updated: {}'.format(
|
|
||||||
host.host,
|
|
||||||
host.ip,
|
|
||||||
host.updated_on
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
# bind_state = DnsNameServer(
|
||||||
|
# ip=platform_ip,
|
||||||
|
# host=os.getenv('DNS_SERVER')
|
||||||
|
# )
|
||||||
|
# db.session.add(bind_state)
|
||||||
|
# db.session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
for bind_state in bind_states:
|
||||||
|
previous_ip = bind_state.nameserver_1_ip
|
||||||
|
if ip_address(platform_ip) != previous_ip:
|
||||||
|
logger.info('External IP has changed')
|
||||||
|
bind_state.nameserver_1_ip = platform_ip
|
||||||
|
|
||||||
|
logger.info('Updating nameserver record')
|
||||||
dnsupdate.update_dns(
|
dnsupdate.update_dns(
|
||||||
os.getenv('DNS_SERVER'),
|
os.getenv('DNS_SERVER'),
|
||||||
os.getenv('DNS_ZONE'),
|
os.getenv('DNS_ZONE'),
|
||||||
os.getenv('DNS_KEY'),
|
os.getenv('DNS_KEY'),
|
||||||
host.host,
|
bind_state.nameserver_1_host,
|
||||||
platform_ip
|
platform_ip
|
||||||
)
|
)
|
||||||
host.ip = platform_ip
|
|
||||||
|
|
||||||
logger.info('Saving host details')
|
logger.info('Checking host records')
|
||||||
db.session.commit()
|
hosts = db.session(DnsHost).query.filter_by(ip=previous_ip)
|
||||||
|
result = 'Here are the hosts we have\n'
|
||||||
result = 'End of hosts'
|
for host in hosts:
|
||||||
logger.debug(result)
|
result += '\tHost: {} IP: {} Date Updated: {}'.format(
|
||||||
logger.info('Sending mail')
|
host.host,
|
||||||
try:
|
host.ip,
|
||||||
send_sms(
|
host.updated_on
|
||||||
os.getenv('SMS_NOTIFY_TO'),
|
)
|
||||||
os.getenv('SMS_NOTIFY_FROM'),
|
dnsupdate.update_dns(
|
||||||
'IP address for {} changed from {} to {}'.format(
|
os.getenv('DNS_SERVER'),
|
||||||
bind_state.nameserver_1_host,
|
os.getenv('DNS_ZONE'),
|
||||||
previous_ip,
|
os.getenv('DNS_KEY'),
|
||||||
|
host.host,
|
||||||
platform_ip
|
platform_ip
|
||||||
)
|
)
|
||||||
)
|
host.ip = platform_ip
|
||||||
|
|
||||||
logger.info('Mail sent successfully')
|
logger.info('Saving host details')
|
||||||
except Exception as e:
|
db.session.commit()
|
||||||
logger.error('Unable to send mail report')
|
|
||||||
logger.error(e)
|
result = 'End of hosts'
|
||||||
else:
|
logger.debug(result)
|
||||||
logger.info('No IP changes detected')
|
logger.info('Sending mail')
|
||||||
|
try:
|
||||||
|
send_sms(
|
||||||
|
os.getenv('SMS_NOTIFY_TO'),
|
||||||
|
os.getenv('SMS_NOTIFY_FROM'),
|
||||||
|
'IP address for {} changed from {} to {}'.format(
|
||||||
|
bind_state.nameserver_1_host,
|
||||||
|
previous_ip,
|
||||||
|
platform_ip
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info('Mail sent successfully')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Unable to send mail report')
|
||||||
|
logger.error(e)
|
||||||
|
else:
|
||||||
|
logger.info('No IP changes detected')
|
||||||
|
|||||||
0
bitchmin-api/app/tests/__init__.py
Normal file
0
bitchmin-api/app/tests/__init__.py
Normal file
68
bitchmin-api/app/tests/scaffolder.py
Normal file
68
bitchmin-api/app/tests/scaffolder.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import DnsZone, User, DnsHost, DnsNameServer
|
||||||
|
|
||||||
|
|
||||||
|
class Scaffolder(object):
|
||||||
|
engine = create_engine(
|
||||||
|
'postgresql+psycopg2://bitchmin:bitchmin@localhost/bitchmin',
|
||||||
|
poolclass=StaticPool)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
def _create_user(self):
|
||||||
|
user = User(
|
||||||
|
'fergal.moran@gmail.com',
|
||||||
|
'Fergal Moran',
|
||||||
|
'topsecret'
|
||||||
|
)
|
||||||
|
self.session.add(user)
|
||||||
|
|
||||||
|
def _create_zone(self):
|
||||||
|
zone = DnsZone('bitchmints.com')
|
||||||
|
self.session.add(zone)
|
||||||
|
return zone
|
||||||
|
|
||||||
|
def _create_nameservers(self, zone):
|
||||||
|
for i in range(1, 3):
|
||||||
|
ns = DnsNameServer(
|
||||||
|
zone,
|
||||||
|
'host-{}'.format(i),
|
||||||
|
'10.1.1.10{}'.format(i)
|
||||||
|
|
||||||
|
)
|
||||||
|
self.session.add(ns)
|
||||||
|
|
||||||
|
def _create_hosts(self, zone):
|
||||||
|
for i in range(1, 11):
|
||||||
|
host = DnsHost(
|
||||||
|
zone,
|
||||||
|
'host-{}'.format(i),
|
||||||
|
'10.1.1.{}'.format(i)
|
||||||
|
)
|
||||||
|
self.session.add(host)
|
||||||
|
|
||||||
|
def scaffold(self):
|
||||||
|
db.metadata.drop_all(self.engine)
|
||||||
|
db.metadata.create_all(self.engine)
|
||||||
|
|
||||||
|
self._create_user()
|
||||||
|
z = self._create_zone()
|
||||||
|
self._create_nameservers(z)
|
||||||
|
self._create_hosts(z)
|
||||||
|
|
||||||
|
self.session.commit()
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
db.metadata.drop_all(self.engine)
|
||||||
|
self.engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
Scaffolder().scaffold()
|
||||||
57
bitchmin-api/app/tests/test_db_updates.py
Normal file
57
bitchmin-api/app/tests/test_db_updates.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
from app.models import User, DnsZone, DnsNameServer, DnsHost
|
||||||
|
from scaffolder import Scaffolder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function") # or "module" (to teardown at a module level)
|
||||||
|
def db():
|
||||||
|
scaffolder = Scaffolder()
|
||||||
|
scaffolder.teardown()
|
||||||
|
scaffolder.scaffold()
|
||||||
|
|
||||||
|
yield scaffolder.session
|
||||||
|
|
||||||
|
scaffolder.teardown()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDatabaseUpdate:
|
||||||
|
def test_load(self, db) -> None:
|
||||||
|
assert db.query(User).count() == 1
|
||||||
|
assert db.query(DnsZone).count() == 1
|
||||||
|
assert db.query(DnsNameServer).count() == 2
|
||||||
|
assert db.query(DnsHost).count() == 10
|
||||||
|
|
||||||
|
def test_serial_on_create(self, db) -> None:
|
||||||
|
zone = db.query(DnsZone).first()
|
||||||
|
assert len(str(zone.serial)) == 10
|
||||||
|
assert zone.get_serial_increment() == 10
|
||||||
|
|
||||||
|
def test_serial_on_add_host(self, db) -> None:
|
||||||
|
zone = db.query(DnsZone).first()
|
||||||
|
serial = zone.get_serial_increment()
|
||||||
|
|
||||||
|
DnsHost(
|
||||||
|
zone=zone,
|
||||||
|
host=str(uuid.uuid4()),
|
||||||
|
ip='99.99.99.99'
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
"""
|
||||||
|
Probably better to test that the new serial is > old serial...
|
||||||
|
Not hugely important that it's only ONE more than it
|
||||||
|
"""
|
||||||
|
assert zone.get_serial_increment() > serial
|
||||||
|
|
||||||
|
def test_serial_on_update_host(self, db) -> None:
|
||||||
|
host = db.query(DnsHost).get(4)
|
||||||
|
serial = host.zone.get_serial_increment()
|
||||||
|
|
||||||
|
host.ip = '99.99.99.99'
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
assert host.zone.get_serial_increment() > serial
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ Create Date: ${create_date}
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy_utils
|
||||||
|
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
"""Added Bind State
|
|
||||||
|
|
||||||
Revision ID: 7de781bdfffe
|
|
||||||
Revises: e4b3d04da7fc
|
|
||||||
Create Date: 2020-09-04 21:47:57.400947
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import sqlalchemy_utils
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '7de781bdfffe'
|
|
||||||
down_revision = 'e4b3d04da7fc'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('bind_state',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
||||||
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
||||||
sa.Column('nameserver_1_ip', sqlalchemy_utils.types.ip_address.IPAddressType(length=255), nullable=False),
|
|
||||||
sa.Column('nameserver_1_host', sa.String(length=255), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('nameserver_1_host')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('bind_state')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""Initial migration.
|
||||||
|
|
||||||
|
Revision ID: ca46ac8d1ba5
|
||||||
|
Revises:
|
||||||
|
Create Date: 2020-11-09 18:33:08.250178
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy_utils
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ca46ac8d1ba5'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('dns_zones',
|
||||||
|
sa.Column('created_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('zone_name', sa.String(length=253), nullable=False),
|
||||||
|
sa.Column('serial', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('zone_name')
|
||||||
|
)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=120), nullable=False),
|
||||||
|
sa.Column('full_name', sa.String(length=120), nullable=True),
|
||||||
|
sa.Column('password', sa.String(length=255), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('email')
|
||||||
|
)
|
||||||
|
op.create_table('dns_hosts',
|
||||||
|
sa.Column('created_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('zone_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('host', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('ip', sqlalchemy_utils.types.ip_address.IPAddressType(length=255), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['zone_id'], ['dns_zones.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('host')
|
||||||
|
)
|
||||||
|
op.create_table('dns_nameservers',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('ip', sqlalchemy_utils.types.ip_address.IPAddressType(length=255), nullable=False),
|
||||||
|
sa.Column('host', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('zone_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['zone_id'], ['dns_zones.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('host')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('dns_nameservers')
|
||||||
|
op.drop_table('dns_hosts')
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_table('dns_zones')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""Initial
|
|
||||||
|
|
||||||
Revision ID: e4b3d04da7fc
|
|
||||||
Revises:
|
|
||||||
Create Date: 2020-08-30 22:21:56.289213
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import sqlalchemy_utils
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'e4b3d04da7fc'
|
|
||||||
down_revision = None
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('users',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
||||||
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
||||||
sa.Column('email', sa.String(length=120), nullable=False),
|
|
||||||
sa.Column('fullName', sa.String(length=120), nullable=True),
|
|
||||||
sa.Column('password', sa.String(length=255), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('email')
|
|
||||||
)
|
|
||||||
op.create_table('dns_updates',
|
|
||||||
sa.Column('created_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
||||||
sa.Column('updated_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('host', sa.String(length=120), nullable=False),
|
|
||||||
sa.Column('ip', sqlalchemy_utils.types.ip_address.IPAddressType(length=255), nullable=False),
|
|
||||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('host')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('dns_updates')
|
|
||||||
op.drop_table('users')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -4,6 +4,7 @@ Flask-SQLAlchemy
|
|||||||
Flask_Migrate
|
Flask_Migrate
|
||||||
flask_jwt_extended
|
flask_jwt_extended
|
||||||
Flask-Mail
|
Flask-Mail
|
||||||
|
Flask-Serialize
|
||||||
sqlalchemy_utils
|
sqlalchemy_utils
|
||||||
phue
|
phue
|
||||||
python-dotenv
|
python-dotenv
|
||||||
@@ -15,9 +16,11 @@ werkzeug
|
|||||||
celery
|
celery
|
||||||
redis
|
redis
|
||||||
flower
|
flower
|
||||||
|
sqlalchemy
|
||||||
requests
|
requests
|
||||||
IPy
|
IPy
|
||||||
pydig
|
pydig
|
||||||
tinder
|
tinder
|
||||||
twilio
|
twilio
|
||||||
flask_monitoringdashboard
|
flask_monitoringdashboard
|
||||||
|
pytest
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
docker run -e POSTGRES_PASSWORD=docker library/postgres
|
|
||||||
|
|
||||||
|
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name bitchmin-postgres \
|
--name bitchmin-postgres \
|
||||||
-e POSTGRES_PASSWORD=postgres \
|
-e POSTGRES_PASSWORD=postgres \
|
||||||
-p 5432:5432 \
|
-p 5432:5432 \
|
||||||
-e /opt/postgres/bitchmin=/var/lib/postgresql/data \
|
-e /home/fergalm/dev/BitchMin/bitchmin-api/.working/pgdata=/var/lib/postgresql \
|
||||||
-d library/postgres
|
-d library/postgres
|
||||||
|
|
||||||
docker exec -it bitchmin-postgres psql \
|
docker exec -it bitchmin-postgres psql \
|
||||||
@@ -22,19 +19,4 @@ docker exec -it bitchmin-postgres psql \
|
|||||||
-U postgres \
|
-U postgres \
|
||||||
-c "grant all privileges on database bitchmin to bitchmin;"
|
-c "grant all privileges on database bitchmin to bitchmin;"
|
||||||
|
|
||||||
kubectl exec -it postgres-5f4895b95d-4xz92 \
|
|
||||||
'psql -U postgres -c "CREATE DATABASE bitchmin;"'
|
|
||||||
|
|
||||||
kubectl exec -it postgres-5f4895b95d-4xz92 \
|
|
||||||
"psql -U postgres -c $"CREATE USER bitchmin WITH PASSWORD \'bitchmin\';'"
|
|
||||||
|
|
||||||
psql -U postgres -c 'psql -c "CREATE USER bitchmin WITH PASSWORD '\''bitchmin'\'';"'
|
|
||||||
|
|
||||||
kubectl exec -it postgres-5f4895b95d-4xz92 \
|
|
||||||
'psql -U postgres -c "grant all privileges on database bitchmin to bitchmin;"'
|
|
||||||
su - postgres -c 'psql -c "grant all privileges on database bitchmin to bitchmin;"'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# DATABASE_URL='postgresql+psycopg2://bitchmin:bitchmin@10.1.1.1/bitchmin'
|
|
||||||
# psql postgres -c "CREATE DATABASE BitchMin WITH ENCODING 'UTF8'
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Api } from '@/api/apiBase';
|
|||||||
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import { apiConfig } from '@/api/config';
|
import { apiConfig } from '@/api/config';
|
||||||
import { ApiResult, DataApiResult } from '@/api/apiResult';
|
import { ApiResult, DataApiResult } from '@/api/apiResult';
|
||||||
import { DnsRecord } from '@/models/dnsRecord';
|
import { DnsHost } from '@/models/dnsHost';
|
||||||
|
|
||||||
export class DnsApi extends Api {
|
export class DnsApi extends Api {
|
||||||
constructor(config: AxiosRequestConfig) {
|
constructor(config: AxiosRequestConfig) {
|
||||||
@@ -13,11 +13,11 @@ export class DnsApi extends Api {
|
|||||||
public async updateDnsRecord(
|
public async updateDnsRecord(
|
||||||
hostName: string,
|
hostName: string,
|
||||||
ipAddress: string,
|
ipAddress: string,
|
||||||
): Promise<DataApiResult<DnsRecord>> {
|
): Promise<DataApiResult<DnsHost>> {
|
||||||
const result = await this.post<
|
const result = await this.post<
|
||||||
ApiResult,
|
ApiResult,
|
||||||
any,
|
any,
|
||||||
AxiosResponse<DataApiResult<DnsRecord>>
|
AxiosResponse<DataApiResult<DnsHost>>
|
||||||
>('/dns/', {
|
>('/dns/', {
|
||||||
host: hostName,
|
host: hostName,
|
||||||
ip: ipAddress,
|
ip: ipAddress,
|
||||||
@@ -35,11 +35,11 @@ export class DnsApi extends Api {
|
|||||||
public async refreshDnsRecord(
|
public async refreshDnsRecord(
|
||||||
hostName: string,
|
hostName: string,
|
||||||
ip: string,
|
ip: string,
|
||||||
): Promise<DataApiResult<DnsRecord>> {
|
): Promise<DataApiResult<DnsHost>> {
|
||||||
const result = await this.post<
|
const result = await this.post<
|
||||||
ApiResult,
|
ApiResult,
|
||||||
any,
|
any,
|
||||||
AxiosResponse<DataApiResult<DnsRecord>>
|
AxiosResponse<DataApiResult<DnsHost>>
|
||||||
>('/dns/refresh', {
|
>('/dns/refresh', {
|
||||||
host: hostName,
|
host: hostName,
|
||||||
ip,
|
ip,
|
||||||
@@ -62,8 +62,8 @@ export class DnsApi extends Api {
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDnsRecords(): Promise<DnsRecord[]> {
|
public async getDnsRecords(): Promise<DnsHost[]> {
|
||||||
const result = await this.get<DnsRecord[]>('/dns/list');
|
const result = await this.get<DnsHost[]>('/dns/list');
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, PropSync, Vue } from 'vue-property-decorator';
|
import { Component, PropSync, Vue } from 'vue-property-decorator';
|
||||||
import { dnsApi } from '@/api';
|
import { dnsApi } from '@/api';
|
||||||
import { DnsRecord } from '@/models/dnsRecord';
|
import { DnsHost } from '@/models/dnsHost';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@@ -78,13 +78,13 @@ import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|||||||
})
|
})
|
||||||
export default class DnsRecordsList extends Vue {
|
export default class DnsRecordsList extends Vue {
|
||||||
@PropSync('inrecords')
|
@PropSync('inrecords')
|
||||||
public records!: DnsRecord[];
|
public records!: DnsHost[];
|
||||||
|
|
||||||
private callInProgress = false;
|
private callInProgress = false;
|
||||||
|
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
|
|
||||||
async refreshRecord(host: DnsRecord) {
|
async refreshRecord(host: DnsHost) {
|
||||||
this.callInProgress = true;
|
this.callInProgress = true;
|
||||||
console.log('DnsRecordsList', 'refreshRecord', host);
|
console.log('DnsRecordsList', 'refreshRecord', host);
|
||||||
const result = await dnsApi.refreshDnsRecord(host.host, host.ip);
|
const result = await dnsApi.refreshDnsRecord(host.host, host.ip);
|
||||||
@@ -94,7 +94,7 @@ export default class DnsRecordsList extends Vue {
|
|||||||
this.callInProgress = false;
|
this.callInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyRecord(record: DnsRecord) {
|
async verifyRecord(record: DnsHost) {
|
||||||
this.callInProgress = true;
|
this.callInProgress = true;
|
||||||
const result = await dnsApi.verifyDnsRecord(record.host, record.ip);
|
const result = await dnsApi.verifyDnsRecord(record.host, record.ip);
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
@@ -106,7 +106,7 @@ export default class DnsRecordsList extends Vue {
|
|||||||
this.callInProgress = false;
|
this.callInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteRecord(record: DnsRecord) {
|
async deleteRecord(record: DnsHost) {
|
||||||
this.callInProgress = true;
|
this.callInProgress = true;
|
||||||
const result = await dnsApi.deleteDnsRecord(record.host);
|
const result = await dnsApi.deleteDnsRecord(record.host);
|
||||||
if (result === 200) {
|
if (result === 200) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="mx-auto" outlined>
|
<v-card class="mx-auto" >
|
||||||
<template slot="progress">
|
<template slot="progress">
|
||||||
<v-progress-linear color="deep-purple" height="10" indeterminate></v-progress-linear>
|
<v-progress-linear color="deep-purple" height="10" indeterminate></v-progress-linear>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, PropSync, Vue } from 'vue-property-decorator';
|
import { Component, PropSync, Vue } from 'vue-property-decorator';
|
||||||
import { dnsApi } from '@/api';
|
import { dnsApi } from '@/api';
|
||||||
import { DnsRecord } from '@/models/dnsRecord';
|
import { DnsHost } from '@/models/dnsHost';
|
||||||
import { DataApiResult } from '@/api/apiResult';
|
import { DataApiResult } from '@/api/apiResult';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -67,7 +67,7 @@ export default class DnsUpdateForm extends Vue {
|
|||||||
];
|
];
|
||||||
|
|
||||||
@PropSync('inrecords')
|
@PropSync('inrecords')
|
||||||
public records!: DnsRecord[];
|
public records!: DnsHost[];
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
// this.$refs.form.validate();
|
// this.$refs.form.validate();
|
||||||
@@ -76,7 +76,7 @@ export default class DnsUpdateForm extends Vue {
|
|||||||
processUpdate() {
|
processUpdate() {
|
||||||
dnsApi
|
dnsApi
|
||||||
.updateDnsRecord(this.hostName, this.ipAddress)
|
.updateDnsRecord(this.hostName, this.ipAddress)
|
||||||
.then((r: DataApiResult<DnsRecord>) => {
|
.then((r: DataApiResult<DnsHost>) => {
|
||||||
if (r.status === 'success') {
|
if (r.status === 'success') {
|
||||||
this.error = '';
|
this.error = '';
|
||||||
Vue.toasted.success('Update successful');
|
Vue.toasted.success('Update successful');
|
||||||
|
|||||||
28
bitchmin-client/src/components/Dns/DnsZonesList.vue
Normal file
28
bitchmin-client/src/components/Dns/DnsZonesList.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="mx-auto">
|
||||||
|
<v-card-title>Choose Zone</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-select :options="zones"
|
||||||
|
item-text="zone"
|
||||||
|
item-value="id"
|
||||||
|
></v-select>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, PropSync, Vue } from 'vue-property-decorator';
|
||||||
|
import { DnsZone } from '@/models/dnsZone';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'DnsHostsList'
|
||||||
|
})
|
||||||
|
export default class DnsZonesList extends Vue {
|
||||||
|
|
||||||
|
@PropSync('inhosts')
|
||||||
|
public zones!: DnsZone[];
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
console.log('DnsZonesList', 'mounted', this.zones);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
6
bitchmin-client/src/models/dnsHost.ts
Normal file
6
bitchmin-client/src/models/dnsHost.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface DnsHost {
|
||||||
|
id: number;
|
||||||
|
host: string;
|
||||||
|
ip: string;
|
||||||
|
created_on: Date;
|
||||||
|
}
|
||||||
4
bitchmin-client/src/models/dnsZone.ts
Normal file
4
bitchmin-client/src/models/dnsZone.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface DnsZone {
|
||||||
|
id: number;
|
||||||
|
zone: string;
|
||||||
|
}
|
||||||
@@ -4,4 +4,4 @@ export * from './userLoginModel';
|
|||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './light';
|
export * from './light';
|
||||||
export * from './dnsRecord';
|
export * from './dnsHost';
|
||||||
|
|||||||
@@ -2,35 +2,50 @@
|
|||||||
<v-container id="dashboard" fluid tag="section">
|
<v-container id="dashboard" fluid tag="section">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" sm="4" lg="4">
|
<v-col cols="12" sm="4" lg="4">
|
||||||
<DnsUpdateForm :inrecords="dnsRecords" />
|
<DnsZonesList :inhosts="dnsZones" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="8" lg="8">
|
<v-col cols="12" sm="8" lg="8">
|
||||||
|
<DnsUpdateForm :inrecords="dnsRecords" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="12" lg="12">
|
||||||
<DnsRecordsList :inrecords="dnsRecords" />
|
<DnsRecordsList :inrecords="dnsRecords" />
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
|
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
|
||||||
|
import DnsZonesList from '@/components/Dns/DnsZonesList.vue';
|
||||||
import DnsRecordsList from '@/components/Dns/DnsRecordsList.vue';
|
import DnsRecordsList from '@/components/Dns/DnsRecordsList.vue';
|
||||||
import DnsUpdateForm from '@/components/Dns/DnsUpdateForm.vue';
|
import DnsUpdateForm from '@/components/Dns/DnsUpdateForm.vue';
|
||||||
import { DnsRecord } from '@/models';
|
import { DnsHost } from '@/models';
|
||||||
import { dnsApi } from '@/api';
|
import { dnsApi } from '@/api';
|
||||||
|
import { DnsZone } from '@/models/dnsZone';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
HelloWorld,
|
HelloWorld,
|
||||||
|
DnsZonesList,
|
||||||
DnsRecordsList,
|
DnsRecordsList,
|
||||||
DnsUpdateForm,
|
DnsUpdateForm
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
export default class BitchNS extends Vue {
|
export default class BitchNS extends Vue {
|
||||||
dnsRecords: DnsRecord[] = [];
|
dnsZones: DnsZone[] = [];
|
||||||
|
dnsRecords: DnsHost[] = [];
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.dnsRecords = await dnsApi.getDnsRecords();
|
this.dnsZones = [
|
||||||
|
{ id: 1, zone: 'bitchmints.com' },
|
||||||
|
{ id: 2, zone: 'fergl.ie' }
|
||||||
|
];
|
||||||
|
// this.dnsHosts = await dnsApi.getHosts();
|
||||||
|
// this.dnsRecords = await dnsApi.getDnsRecords();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
1
bitchmin-server/.venv
Normal file
1
bitchmin-server/.venv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
BitchMin
|
||||||
1
bitchmin-server/commands.txt
Normal file
1
bitchmin-server/commands.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ASDAKSJH
|
||||||
1
bitchmin-server/requirements.txt
Normal file
1
bitchmin-server/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
twisted
|
||||||
0
bitchmin-server/resolvers/__init__.py
Normal file
0
bitchmin-server/resolvers/__init__.py
Normal file
BIN
bitchmin-server/resolvers/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
bitchmin-server/resolvers/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
bitchmin-server/resolvers/__pycache__/commands.cpython-38.pyc
Normal file
BIN
bitchmin-server/resolvers/__pycache__/commands.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
19
bitchmin-server/resolvers/commands.py
Normal file
19
bitchmin-server/resolvers/commands.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
class Command:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AddRecordCommand(Command):
|
||||||
|
def __init__(self, record_type, zone, host, ip, ttl):
|
||||||
|
self.type = record_type
|
||||||
|
self.zone = zone
|
||||||
|
self.host = host
|
||||||
|
self.ip = ip
|
||||||
|
self.ttl = ttl
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'AddRecordCommand({}) - Host: {} IP: {}'.format(
|
||||||
|
self.type,
|
||||||
|
self.zone,
|
||||||
|
self.host,
|
||||||
|
self.ip,
|
||||||
|
)
|
||||||
115
bitchmin-server/resolvers/memory_resolver.py
Normal file
115
bitchmin-server/resolvers/memory_resolver.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from twisted.internet import defer
|
||||||
|
from twisted.names import dns, error
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidZoneException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Resolver:
|
||||||
|
def _build_host(self, record_type, host, ip, ttl):
|
||||||
|
return {
|
||||||
|
host: {
|
||||||
|
'type': record_type,
|
||||||
|
'ttl': ttl,
|
||||||
|
'ip': ip
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryResolver(Resolver):
|
||||||
|
"""
|
||||||
|
A resolver which calculates the answers to certain queries based on the
|
||||||
|
query type and name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, zones):
|
||||||
|
self._zones = zones
|
||||||
|
|
||||||
|
def add_host(self, record_type, zone, host, ip, ttl):
|
||||||
|
if self._zones[zone]:
|
||||||
|
# 'ns1': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.7'},
|
||||||
|
new = {
|
||||||
|
host: {
|
||||||
|
'type': record_type,
|
||||||
|
'ttl': ttl,
|
||||||
|
'ip': ip
|
||||||
|
},
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self._zones[zone]['hosts'].update(new)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
else:
|
||||||
|
raise InvalidZoneException('Zone {} does not exist'.format(zone))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_host(zone, host):
|
||||||
|
parsed = [x for x in host.split('.') if x not in zone.split('.')]
|
||||||
|
return '.'.join(parsed) if len(parsed) != 0 else ''
|
||||||
|
|
||||||
|
def _get_authoritative_zone(self, query):
|
||||||
|
"""
|
||||||
|
split on "." and keep removing from left until we find a zone or we run out of string
|
||||||
|
"""
|
||||||
|
parts = str(query.name).split('.')
|
||||||
|
while len(parts) != 0:
|
||||||
|
t = '.'.join(parts)
|
||||||
|
if t in self._zones:
|
||||||
|
return t, self._zones[t]
|
||||||
|
parts.pop(0)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_response(self, zone, query):
|
||||||
|
"""
|
||||||
|
Calculate the response to a query.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
host = self._parse_host(zone, str(query.name))
|
||||||
|
record = self._zones[zone]['hosts'][host]
|
||||||
|
|
||||||
|
if query.type == dns.NS:
|
||||||
|
payload = dns.Record_NS(
|
||||||
|
name=zone['nameservers'][0],
|
||||||
|
ttl=record['ttl']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
payload = dns.Record_A(
|
||||||
|
address=record['ip'],
|
||||||
|
ttl=record['ttl']
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = dns.RRHeader(
|
||||||
|
name=zone,
|
||||||
|
payload=payload,
|
||||||
|
ttl=record['ttl']
|
||||||
|
)
|
||||||
|
|
||||||
|
answers = [answer]
|
||||||
|
authority = []
|
||||||
|
additional = []
|
||||||
|
|
||||||
|
return answers, authority, additional
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def query(self, query, timeout=None):
|
||||||
|
"""
|
||||||
|
Check if the query should be answered dynamically, otherwise dispatch to
|
||||||
|
the fallback resolver.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logging.info(query)
|
||||||
|
name, zone = self._get_authoritative_zone(query)
|
||||||
|
if zone and name:
|
||||||
|
logging.debug('BitchNS: Authoritative for {}'.format(name))
|
||||||
|
result = self._get_response(name, query)
|
||||||
|
if result:
|
||||||
|
logging.debug('BitchNS: Resolving {} to {}'.format(name, result))
|
||||||
|
return defer.succeed(result)
|
||||||
|
logging.debug('BitchNS: Host {} not found in zone {}'.format(name, zone))
|
||||||
|
|
||||||
|
return defer.fail(error.DomainError())
|
||||||
73
bitchmin-server/server.py
Normal file
73
bitchmin-server/server.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
from twisted.internet.endpoints import TCP4ServerEndpoint
|
||||||
|
from twisted.names import client, dns, server
|
||||||
|
|
||||||
|
from resolvers.memory_resolver import MemoryResolver
|
||||||
|
from servers.worker_server import WorkerServerFactory
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
PORT = 10053
|
||||||
|
WORKER_PORT = 10054
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
zones = {
|
||||||
|
'bitchmints.com': {
|
||||||
|
'serial': 'BOOO',
|
||||||
|
'admin': 'Ferg@lMoran.me',
|
||||||
|
'nameservers': [
|
||||||
|
'ns1.bitchmints.com',
|
||||||
|
'ns2.bitchmints.com'
|
||||||
|
],
|
||||||
|
'hosts': {
|
||||||
|
'ns1': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.7'},
|
||||||
|
'ns2': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.8'},
|
||||||
|
'host-1': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
|
||||||
|
'host-2': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
|
||||||
|
'host-3': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
|
||||||
|
'host-4': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'fergl.ie': {
|
||||||
|
'serial': 'BOOO',
|
||||||
|
'admin': 'Ferg@lMoran.me',
|
||||||
|
'nameservers': [
|
||||||
|
'ns1.bitchmints.com',
|
||||||
|
'ns2.bitchmints.com'
|
||||||
|
],
|
||||||
|
'hosts': {
|
||||||
|
'farts': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.8'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memory_resolver = MemoryResolver(zones)
|
||||||
|
|
||||||
|
dns_factory = server.DNSServerFactory(
|
||||||
|
clients=[
|
||||||
|
memory_resolver,
|
||||||
|
client.Resolver(resolv='/etc/resolv.conf')]
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
protocol = dns.DNSDatagramProtocol(controller=dns_factory)
|
||||||
|
|
||||||
|
logging.info('BitchNS: Starting UDP/ns listener on {}'.format(PORT))
|
||||||
|
reactor.listenUDP(PORT, protocol)
|
||||||
|
|
||||||
|
logging.info('BitchNS: Starting TCP/ns listener on {}'.format(PORT))
|
||||||
|
reactor.listenTCP(PORT, dns_factory)
|
||||||
|
|
||||||
|
logging.info('BitchNS: Starting TCP/worker listener on {}'.format(WORKER_PORT))
|
||||||
|
worker_factory = TCP4ServerEndpoint(reactor, WORKER_PORT)
|
||||||
|
worker_factory.listen(WorkerServerFactory(memory_resolver))
|
||||||
|
|
||||||
|
reactor.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.debug('BitchNS: Server starting')
|
||||||
|
raise SystemExit(main())
|
||||||
0
bitchmin-server/servers/__init__.py
Normal file
0
bitchmin-server/servers/__init__.py
Normal file
BIN
bitchmin-server/servers/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
bitchmin-server/servers/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
bitchmin-server/servers/__pycache__/worker_server.cpython-38.pyc
Normal file
BIN
bitchmin-server/servers/__pycache__/worker_server.cpython-38.pyc
Normal file
Binary file not shown.
59
bitchmin-server/servers/worker_server.py
Normal file
59
bitchmin-server/servers/worker_server.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from twisted.internet.protocol import Protocol, Factory
|
||||||
|
|
||||||
|
from resolvers.commands import AddRecordCommand
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerServerFactory(Factory):
|
||||||
|
def __init__(self, resolver):
|
||||||
|
self._resolver = resolver
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
return WorkerServer(self._resolver)
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownCommandException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerServer(Protocol):
|
||||||
|
def __init__(self, resolver):
|
||||||
|
self._resolver = resolver
|
||||||
|
|
||||||
|
def dataReceived(self, data):
|
||||||
|
logging.debug('BitchNS: Command received: {}'.format(data))
|
||||||
|
try:
|
||||||
|
command = self.__parse_data(data)
|
||||||
|
logging.debug('BitchNS: {}'.format(command))
|
||||||
|
self._resolver.add_host(
|
||||||
|
record_type=command.type,
|
||||||
|
zone=command.zone,
|
||||||
|
host=command.host,
|
||||||
|
ip=command.ip,
|
||||||
|
ttl=command.ttl
|
||||||
|
)
|
||||||
|
self.__send_response(str(command))
|
||||||
|
|
||||||
|
except UnknownCommandException as e:
|
||||||
|
self.__send_response('Unable to parse command: {0}'.format(e))
|
||||||
|
|
||||||
|
def __send_response(self, response):
|
||||||
|
self.transport.write(bytes(
|
||||||
|
'{}\n'.format(
|
||||||
|
response
|
||||||
|
).encode('utf-8')
|
||||||
|
))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __parse_data(data):
|
||||||
|
parts = data.decode('utf-8').split('|')
|
||||||
|
if parts[0] == 'A':
|
||||||
|
return AddRecordCommand(
|
||||||
|
record_type=parts[1],
|
||||||
|
zone=parts[2],
|
||||||
|
host=parts[3],
|
||||||
|
ip=parts[4],
|
||||||
|
ttl=parts[5]
|
||||||
|
)
|
||||||
|
|
||||||
|
raise UnknownCommandException(data)
|
||||||
Reference in New Issue
Block a user