Initial unit tests done

This commit is contained in:
Fergal Moran
2020-11-12 19:44:52 +00:00
parent 0af3a1bfe8
commit 04ffc26647
41 changed files with 729 additions and 205 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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',

View File

@@ -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']

View File

@@ -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

View 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()

View File

@@ -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

View File

View File

@@ -0,0 +1,4 @@
# class ZoneSerializerMixin(SerializerMixin):
# serialize_types = (
# (UUID, lambda x: str(x)),
# )

View File

@@ -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')

View File

View 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()

View 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

View File

@@ -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.

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -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

View File

@@ -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'

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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');

View 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>

View File

@@ -0,0 +1,6 @@
export interface DnsHost {
id: number;
host: string;
ip: string;
created_on: Date;
}

View File

@@ -0,0 +1,4 @@
export interface DnsZone {
id: number;
zone: string;
}

View File

@@ -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';

View File

@@ -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
View File

@@ -0,0 +1 @@
BitchMin

View File

@@ -0,0 +1 @@
ASDAKSJH

View File

@@ -0,0 +1 @@
twisted

View File

View 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,
)

View 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
View 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())

View File

View 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)