proxadmin/app/models.py
2017-05-24 17:37:52 +03:00

463 lines
17 KiB
Python

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 '<Role %r>' % 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='User')
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 '<User %r>' % 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