from werkzeug.security import generate_password_hash, check_password_hash from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from flask import current_app, request, url_for from flask_login import UserMixin, AnonymousUserMixin from app.exceptions import ValidationError from . import db, lm import os import base64 import hashlib from decimal import Decimal from datetime import date, time, datetime, timedelta import json from sortedcontainers import SortedDict import requests import onetimepass class Permission: DEPLOY = 0x01 ADMINISTER = 0x80 class Role(db.Model): __tablename__ = 'roles' pid = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) default = db.Column(db.Boolean, default=False, index=True) permissions = db.Column(db.Integer) users = db.relationship('User', backref='role', lazy='dynamic') @staticmethod def insert_roles(): roles = { 'User': (Permission.DEPLOY, True), 'Administrator': (0xff, False) } for r in roles: role = Role.query.filter_by(name=r).first() if role is None: role = Role(name=r) role.permissions = roles[r][0] role.default = roles[r][1] db.session.add(role) db.session.commit() def __repr__(self): return '' % self.name class User(db.Model, UserMixin): __tablename__ = 'users' pid = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(64), unique=True, index=True) role_id = db.Column(db.Integer, db.ForeignKey('roles.pid')) #FK password_hash = db.Column(db.String(128)) confirmed = db.Column(db.Boolean, default=False) active = db.Column(db.Boolean, default=True) member_since = db.Column(db.DateTime(), default=datetime.utcnow) last_seen = db.Column(db.DateTime(), default=datetime.utcnow) last_ip = db.Column(db.String(128)) twofactor = db.Column(db.Boolean, default=False) #optional 2factor auth otp_secret = db.Column(db.String(16)) avatar_hash = db.Column(db.String(32)) name = db.Column(db.Unicode(256)) address = db.Column(db.Unicode(256)) city = db.Column(db.Unicode(64)) postcode = db.Column(db.String(10)) country = db.Column(db.String(64), default='BG') phone = db.Column(db.String(64)) org_account = db.Column(db.Boolean, default=False) org_companyname = db.Column(db.Unicode(64)) org_regaddress = db.Column(db.Unicode(128)) org_responsible = db.Column(db.Unicode(128)) org_bulstat = db.Column(db.String(16)) org_vat = db.Column(db.Boolean, default=False) org_vatnum = db.Column(db.String(16)) group = db.Column(db.String(24), default='nogroup') language = db.Column(db.String(2), default='BG') wallet = db.Column(db.Float, default=0.0) currency = db.Column(db.String(3), default='BGN') inv_deployments = db.relationship('Deployment', backref='owner', lazy='dynamic') inv_contracts = db.relationship('Contract', backref='owner', lazy='dynamic') inv_domains = db.relationship('Domain', backref='owner', lazy='dynamic') inv_address = db.relationship('Address', backref='owner', lazy='dynamic') def __init__(self, **kwargs): super(User, self).__init__(**kwargs) if self.role is None: if self.email == current_app.config['ADMIN_EMAIL']: #if email match config admin name create admin user self.role = Role.query.filter_by(permissions=0xff).first() if self.role is None: #if role is stil not set, create default user role self.role = Role.query.filter_by(default=True).first() if self.email is not None and self.avatar_hash is None: self.avatar_hash = hashlib.md5(self.email.encode('utf-8')).hexdigest() if self.otp_secret is None: # generate a random secret self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') @property def password(self): raise AttributeError('password is not a readable attribute') @password.setter def password(self, password): self.password_hash = generate_password_hash(password) def verify_password(self, password): return check_password_hash(self.password_hash, password) def get_totp_uri(self): return 'otpauth://totp/DataPanel:{0}?secret={1}&issuer=datapanel'.format(self.email, self.otp_secret) def verify_totp(self, token): return onetimepass.valid_totp(token, self.otp_secret) def generate_confirmation_token(self, expiration=86400): s = Serializer(current_app.config['SECRET_KEY'], expiration) return s.dumps({'confirm': self.pid}) def confirm(self, token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return False if data.get('confirm') != self.pid: return False self.confirmed = True db.session.add(self) db.session.commit() return True def generate_reset_token(self, expiration=86400): s = Serializer(current_app.config['SECRET_KEY'], expiration) return s.dumps({'reset': self.pid}) def reset_password(self, token, new_password): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return False if data.get('reset') != self.pid: return False self.password = new_password db.session.add(self) db.session.commit() return True def can(self, permissions): return self.role is not None and (self.role.permissions & permissions) == permissions def is_administrator(self): return self.can(Permission.ADMINISTER) def ping(self): self.last_seen = datetime.utcnow() db.session.add(self) db.session.commit() def gravatar(self, size=100, default='identicon', rating='g'): #this check is disabled because it didnt work for me but forcing https to gravatar is okay. #if request.is_secure: # url = 'https://secure.gravatar.com/avatar' #else: # url = 'http://www.gravatar.com/avatar' url = 'https://secure.gravatar.com/avatar' hash = self.avatar_hash or hashlib.md5(self.email.encode('utf-8')).hexdigest() return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating) def is_authenticated(self): return self.is_authenticated def get_id(self): return str(self.pid) def __repr__(self): return '' % self.email class AnonymousUser(AnonymousUserMixin): def can(self, permissions): return False def is_administrator(self): return False lm.anonymous_user = AnonymousUser @lm.user_loader def load_user(user_id): return User.query.get(int(user_id)) def contact_proxmaster(data, method, cubeid=0): url = current_app.config['PROXMASTER_URL'] data['apikey'] = current_app.config['APIKEY'] data_json = json.dumps(data) #print('--> {}'.format(data)) if method == 'vmcreate': url = '{}/{}'.format(url, method) else: url = '{}/{}/{}'.format(url, method, cubeid) db_result = requests.post( url, data=data_json, headers={"content-type": "application/json"}, timeout=30 ) try: proxjson = db_result.json() #print('proxmaster query {}'.format(str(proxjson))) return proxjson except: return None #TEMPLATE CLASSES class Product(db.Model): __tablename__ = 'products' pid = db.Column(db.Integer, primary_key=True) #PK group = db.Column(db.Integer) name = db.Column(db.String(64)) image = db.Column(db.String(128)) description = db.Column(db.String(128)) cpu = db.Column(db.Integer) #default cpu mem = db.Column(db.Integer) #default mem hdd = db.Column(db.Integer) #default hdd recipe = db.Column(db.String(128)) #defaut template name enabled = db.Column(db.Boolean) @staticmethod def insert_products(): products = current_app.config['PRODUCTS'] for p in products: product = Product.query.filter_by(pid=p).first() if product is None: product = Product(name=p) #insert default values product.group = products[p][0] product.name = products[p][1] product.image = products[p][2] product.description = products[p][3] product.cpu = products[p][4] product.mem = products[p][5] product.hdd = products[p][6] product.recipe = products[p][7] product.enabled = products[p][8] db.session.add(product) db.session.commit() @staticmethod def get_products(): result = Product.query.all() products = {} for product in result: if product.enabled == True: products[int(product.pid)] = { 'group': product.group, 'img': '/static/images/' + product.image, 'name': product.name, 'description': product.description, 'cpu': product.cpu, 'mem': product.mem, 'hdd': product.hdd, 'recipe': product.recipe } return products class Service(db.Model): __tablename__ = 'services' pid = db.Column(db.Integer, primary_key=True) #PK name = db.Column(db.String(64)) image = db.Column(db.String(128)) description = db.Column(db.Unicode(128)) unitprice = db.Column(db.Float) enabled = db.Column(db.Boolean) @staticmethod def insert_services(): services = current_app.config['SERVICES'] for s in services: service = Service.query.filter_by(pid=p).first() if service is None: service = Service(name=s) #insert default values service.name = products[p][1] service.image = products[p][2] service.description = products[p][3] service.unitprice = products[p][4] service.enabled = products[p][5] db.session.add(service) db.session.commit() @staticmethod def get_services(): result = Service.query.all() services = {} for service in result: if service.enabled == True: services[int(service.pid)] = {'img': '/static/images/' + service.image, 'name': service.name, 'description': service.description, 'unitprice': service.unitprice } return services #INVENTORY CLASSES class Deployment(db.Model): __tablename__ = 'deployments' pid = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK product_id = db.Column(db.Integer, db.ForeignKey('products.pid')) #FK date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) date_expire = db.Column(db.DateTime) enabled = db.Column(db.Boolean) machine_id = db.Column(db.BigInteger) #cubeid machine_alias = db.Column(db.String) #dns name machine_cpu = db.Column(db.Integer) machine_mem = db.Column(db.Integer) machine_hdd = db.Column(db.Integer) def charge(): result = Deployment.query.all() for deploy in result: if deploy.enabled == True: managed_user = User.query.get(deploy.user_id) db.session.add(managed_user) current_product = Product.query.get(int(deploy.product_id)) cpu_cost = deploy.machine_cpu * current_app.config['CPU_RATIO'] mem_cost = ( deploy.machine_mem / 1024 ) * current_app.config['MEM_RATIO'] hdd_cost = deploy.machine_hdd * current_app.config['HDD_RATIO'] total = cpu_cost + mem_cost + hdd_cost if managed_user.wallet - total > 0: managed_user.wallet -= total print('{}> Charging deployment #{} with {}. Wallet now is: {}'.format(managed_user.email, deploy.machine_id, total, managed_user.walet)) else: print('{}> Deployment #{} cannot be charged with {}. Not enough money in the wallet ({}). Notifying admin...'.format(managed_user.email, deploy.machine_id, total, managed_user.wallet)) #TODO: Send emails here. db.session.commit() class Contract(db.Model): __tablename__ = 'contracts' pid = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK service_id = db.Column(db.Integer, db.ForeignKey('services.pid')) #FK date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) date_expire = db.Column(db.DateTime) enabled = db.Column(db.Boolean) description = db.Column(db.Unicode) units = db.Column(db.Integer) discount = db.Column(db.Integer) #percent def charge(): result = Contract.query.all() for contract in result: managed_user = User.query.get(contract.user_id) db.session.add(contract) #if datetime.utcnow.date() > (contract.date_expire - timedelta(days=10)): if contract.enabled == True: print('{}> Contract {} will expire in 10 days at {}. Creating new order...'.format(managed_user.email, contract.pid, contract.date_expire)) current_service = Service.query.get(int(contract.product_id)) transaction = Transaction(user_id=managed_user.id, units=contract.units, unitvalue=(current_service.unitprice * contract.units), description=current_service.name) db.session.add(transaction) contract.data_expire = datetime.utcnow.date() + timedelta(days=30) db.session.commit() return True class Domain(db.Model): __tablename__ = 'domains' pid = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) date_expire = db.Column(db.DateTime) enabled = db.Column(db.Boolean) fqdn = db.Column(db.String, unique=True) auto_update = db.Column(db.Boolean) def charge(): result = Domain.query.all() for domain in result: managed_user = User.query.get(domain.user_id) db.session.add(domain) #if datetime.utcnow.date() > (domain.date_expire - timedelta(days=60)): if domain.enabled == True: print('{}> Domain {} will expire in 60 days at {}. Creating new order...'.format(managed_user.email, domain.fqdn, domain.date_expire)) transaction = Transaction(user_id=managed_user.id, unitvalue=25, description=domain.fqdn) db.session.add(transaction) db.session.commit() return True class Address(db.Model): __tablename__ = 'address' pid = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) ipaddr = db.Column(db.String(128)) macaddr = db.Column(db.String(128)) #UINVOICE class Transaction(db.Model): __tablename__ = 'transaction' pid = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) currency = db.Column(db.String, default='BGN') value = db.Column(db.Float) class Invoice(db.Model): __tablename__ = 'invoice' pid = db.Column(db.Integer, primary_key=True) invoice_number = db.Column(db.Integer, unique=True) user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) currency = db.Column(db.String, default='BGN') tax = db.Column(db.Float) #VAT description = db.Column(db.Unicode(255)) def sub_total(self): items = self.items sub_total = 0 for item in items: sub_total += item.total() return sub_total def tax_amount(self): if not self.tax: return 0 return self.sub_total() * self.tax / 100.0 def grand_total(self): amount_str = str(self.sub_total() + self.tax_amount()) amount = Decimal(amount_str) rounder = Decimal("0.05") # precision for rounding return amount - amount.remainder_near(rounder) class InvoiceItem(db.Model): __tablename__ = 'invoice_item' pid = db.Column(db.Integer, primary_key=True) item_number = db.Column(db.Integer) invoice_id = db.Column(db.Integer, db.ForeignKey('invoice.pid')) #FK item_title = db.Column(db.Unicode(255)) item_quantity = db.Column(db.Float) item_price = db.Column(db.Float) amount = db.Column(db.Float) invoice = db.relationship(Invoice, backref=db.backref('items', order_by=item_number, cascade="delete")) def total(self): return self.item_quantity * self.item_price