Merge branch 'pretzel' into 'dev'

Lots of Alpha 2.5 commits. See details

See merge request crafty-controller/crafty-commander!46
This commit is contained in:
Andrew 2021-08-18 16:37:40 +00:00
commit 85a118da81
22 changed files with 1595 additions and 172 deletions

71
DBCHANGES.md Normal file
View File

@ -0,0 +1,71 @@
# Database change guide for contributors
When updating a database schema modify the schema in `app/classes/shared/models.py` and create a new migration with the `migration add <name>` command (in Crafty's prompt).
A full list of helper functions you can find in `app/classes/shared/models.py`
## Example migration files
### Rename column/field
```py
def migrate(migrator, database, **kwargs):
migrator.rename_column('my_table', 'old_name', 'new_name') # First argument can be model class OR table name
def rollback(migrator, database, **kwargs):
migrator.rename_column('my_table', 'new_name', 'old_name') # First argument can be model class OR table name
```
### Rename table/model
```py
def migrate(migrator, database, **kwargs):
migrator.rename_table('old_name', 'new_name') # First argument can be model class OR table name
def rollback(migrator, database, **kwargs):
migrator.rename_table('new_name', 'old_name') # First argument can be model class OR table name
```
### Create table/model
```py
import peewee
def migrate(migrator, database, **kwargs):
class NewTable(peewee.Model):
my_id = peewee.IntegerField(unique=True, primary_key=True)
class Meta:
table_name = 'new_table'
database = database
create_table(NewTable)
def rollback(migrator, database, **kwargs):
drop_table('new_table') # Can be model class OR table name
```
### Add columns/fields
```py
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns('table_name', new_field_name=peewee.CharField(default="")) # First argument can be model class OR table name
def rollback(migrator, database, **kwargs):
migrator.drop_columns('table_name', ['new_field_name']) # First argument can be model class OR table name
```

View File

@ -22,9 +22,10 @@ except ModuleNotFoundError as e:
class MainPrompt(cmd.Cmd, object):
def __init__(self, tasks_manager):
def __init__(self, tasks_manager, migration_manager):
super().__init__()
self.tasks_manager = tasks_manager
self.migration_manager = migration_manager
# overrides the default Prompt
prompt = "Crafty Controller v{} > ".format(helper.get_version_string())
@ -47,6 +48,27 @@ class MainPrompt(cmd.Cmd, object):
def do_exit(self, line):
self.universal_exit()
def do_migrations(self, line):
if (line == 'up'):
self.migration_manager.up()
elif (line == 'down'):
self.migration_manager.down()
elif (line == 'done'):
console.info(self.migration_manager.done)
elif (line == 'todo'):
console.info(self.migration_manager.todo)
elif (line == 'diff'):
console.info(self.migration_manager.diff)
elif (line == 'info'):
console.info('Done: {}'.format(self.migration_manager.done))
console.info('FS: {}'.format(self.migration_manager.todo))
console.info('Todo: {}'.format(self.migration_manager.diff))
elif (line.startswith('add ')):
migration_name = line[len('add '):]
self.migration_manager.create(migration_name, False)
else:
console.info('Unknown migration command')
def universal_exit(self):
logger.info("Stopping all server daemons / threads")
console.info("Stopping all server daemons / threads - This may take a few seconds")
@ -62,3 +84,7 @@ class MainPrompt(cmd.Cmd, object):
@staticmethod
def help_exit():
console.help("Stops the server if running, Exits the program")
@staticmethod
def help_migrations():
console.help("Only for advanced users. Use with caution")

View File

@ -4,6 +4,8 @@ import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
@ -110,10 +112,15 @@ class Controller:
@staticmethod
def list_authorized_servers(userId):
#servers = db_helper.get_authorized_servers(userId)
servers = db_helper.get_authorized_servers_from_roles(userId)
servers = db_helper.get_authorized_servers(userId)
server_list = []
for item in servers:
server_list.append(item)
role_servers = db_helper.get_authorized_servers_from_roles(userId)
for item in role_servers:
server_list.append(item)
logger.debug("servers list = {}".format(servers))
return servers
return server_list
def get_server_data(self, server_id):
for s in self.servers_list:
@ -276,8 +283,32 @@ class Controller:
if helper.check_file_perms(zip_path):
helper.ensure_dir_exists(new_server_dir)
helper.ensure_dir_exists(backup_path)
tempDir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(new_server_dir)
zip_ref.extractall(tempDir)
test = zip_ref.filelist[1].filename
path_list = test.split('/')
root_path = path_list[0]
if len(path_list) > 1:
for i in range(len(path_list)-2):
root_path = os.path.join(root_path, path_list[i+1])
full_root_path = os.path.join(tempDir, root_path)
has_properties = False
for item in os.listdir(full_root_path):
if str(item) == 'server.properties':
has_properties = True
try:
shutil.move(os.path.join(full_root_path, item), os.path.join(new_server_dir, item))
except Exception as ex:
logger.error('ERROR IN ZIP IMPORT: {}'.format(ex))
if not has_properties:
logger.info("No server.properties found on zip file import. Creating one with port selection of {}".format(str(port)))
with open(os.path.join(new_server_dir, "server.properties"), "w") as f:
f.write("server-port={}".format(port))
f.close()
zip_ref.close()
else:
return "false"
@ -313,7 +344,7 @@ class Controller:
return new_id
def remove_server(self, server_id):
def remove_server(self, server_id, files):
counter = 0
for s in self.servers_list:
@ -330,7 +361,8 @@ class Controller:
if running:
self.stop_server(server_id)
if files:
shutil.rmtree(db_helper.get_server_data_by_id(server_id)['path'])
# remove the server from the DB
db_helper.remove_server(server_id)

View File

@ -12,6 +12,7 @@ import logging
import html
import zipfile
import pathlib
import shutil
from datetime import datetime
from socket import gethostname
@ -39,6 +40,7 @@ class Helpers:
self.webroot = os.path.join(self.root_dir, 'app', 'frontend')
self.servers_dir = os.path.join(self.root_dir, 'servers')
self.backup_path = os.path.join(self.root_dir, 'backups')
self.migration_dir = os.path.join(self.root_dir, 'app', 'migrations')
self.session_file = os.path.join(self.root_dir, 'app', 'config', 'session.lock')
self.settings_file = os.path.join(self.root_dir, 'app', 'config', 'config.json')
@ -564,6 +566,32 @@ class Helpers:
zf.write(os.path.join(root, file),
os.path.relpath(os.path.join(root, file),
os.path.join(path, '..')))
@staticmethod
def copy_files(source, dest):
if os.path.isfile(source):
shutil.copyfile(source, dest)
logger.info("Copying jar %s to %s", source, dest)
else:
logger.info("Source jar does not exist.")
@staticmethod
def download_file(executable_url, jar_path):
try:
r = requests.get(executable_url, timeout=5)
except Exception as ex:
logger.error("Could not download executable: %s", ex)
return False
if r.status_code != 200:
logger.error("Unable to download file from %s", executable_url)
return False
try:
open(jar_path, "wb").write(r.content)
except Exception as e:
logger.error("Unable to finish executable download. Error: %s", e)
return False
return True
@staticmethod
def remove_prefix(text, prefix):

View File

@ -0,0 +1,532 @@
from datetime import datetime
import logging
import typing as t
import sys
import os
import re
from importlib import import_module
from functools import wraps
try:
from functools import cached_property
except ImportError:
from cached_property import cached_property
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
logger = logging.getLogger(__name__)
try:
import peewee
from playhouse.migrate import (
SchemaMigrator as ScM,
SqliteMigrator as SqM,
Operation, SQL, operation, SqliteDatabase,
make_index_name, Context
)
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(
e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
class MigrateHistory(peewee.Model):
"""
Presents the migration history in a database.
"""
name = peewee.CharField(unique=True)
migrated_at = peewee.DateTimeField(default=datetime.utcnow)
def __unicode__(self) -> str:
"""
String representation of this migration
"""
return self.name
MIGRATE_TABLE = 'migratehistory'
MIGRATE_TEMPLATE = '''# Generated by database migrator
def migrate(migrator, database, **kwargs):
"""
Write your migrations here.
"""
{migrate}
def rollback(migrator, database, **kwargs):
"""
Write your rollback migrations here.
"""
{rollback}'''
VOID: t.Callable = lambda m, d: None
def get_model(method):
"""
Convert string to model class.
"""
@wraps(method)
def wrapper(migrator, model, *args, **kwargs):
if isinstance(model, str):
return method(migrator, migrator.orm[model], *args, **kwargs)
return method(migrator, model, *args, **kwargs)
return wrapper
class Migrator(object):
def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]):
"""
Initializes the migrator
"""
if isinstance(database, peewee.Proxy):
database = database.obj
self.database: SqliteDatabase = database
self.orm: t.Dict[str, peewee.Model] = {}
self.operations: t.List[Operation] = []
self.migrator = SqliteMigrator(database)
def run(self):
"""
Runs operations.
"""
for op in self.operations:
if isinstance(op, Operation):
op.run()
else:
op()
self.clean()
def clean(self):
"""
Cleans the operations.
"""
self.operations = list()
def sql(self, sql: str, *params):
"""
Executes raw SQL.
"""
self.operations.append(self.migrator.sql(sql, *params))
def create_table(self, model: peewee.Model) -> peewee.Model:
"""
Creates model and table in database.
"""
self.orm[model._meta.table_name] = model
model._meta.database = self.database
self.operations.append(model.create_table)
return model
@get_model
def drop_table(self, model: peewee.Model):
"""
Drops model and table from database.
"""
del self.orm[model._meta.table_name]
self.operations.append(self.migrator.drop_table(model))
@get_model
def add_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model:
"""
Creates new fields.
"""
for name, field in fields.items():
model._meta.add_field(name, field)
self.operations.append(self.migrator.add_column(
model._meta.table_name, field.column_name, field))
if field.unique:
self.operations.append(self.migrator.add_index(
model._meta.table_name, (field.column_name,), unique=True))
return model
@get_model
def change_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model:
"""
Changes fields.
"""
for name, field in fields.items():
old_field = model._meta.fields.get(name, field)
old_column_name = old_field and old_field.column_name
model._meta.add_field(name, field)
if isinstance(old_field, peewee.ForeignKeyField):
self.operations.append(self.migrator.drop_foreign_key_constraint(
model._meta.table_name, old_column_name))
if old_column_name != field.column_name:
self.operations.append(
self.migrator.rename_column(
model._meta.table_name, old_column_name, field.column_name))
if isinstance(field, peewee.ForeignKeyField):
on_delete = field.on_delete if field.on_delete else 'RESTRICT'
on_update = field.on_update if field.on_update else 'RESTRICT'
self.operations.append(self.migrator.add_foreign_key_constraint(
model._meta.table_name, field.column_name,
field.rel_model._meta.table_name, field.rel_field.name,
on_delete, on_update))
continue
self.operations.append(self.migrator.change_column(
model._meta.table_name, field.column_name, field))
if field.unique == old_field.unique:
continue
if field.unique:
index = (field.column_name,), field.unique
self.operations.append(self.migrator.add_index(
model._meta.table_name, *index))
model._meta.indexes.append(index)
else:
index = (field.column_name,), old_field.unique
self.operations.append(self.migrator.drop_index(
model._meta.table_name, *index))
model._meta.indexes.remove(index)
return model
@get_model
def drop_columns(self, model: peewee.Model, names: str, **kwargs) -> peewee.Model:
"""
Removes fields from model.
"""
fields = [field for field in model._meta.fields.values()
if field.name in names]
cascade = kwargs.pop('cascade', True)
for field in fields:
self.__del_field__(model, field)
if field.unique:
index_name = make_index_name(
model._meta.table_name, [field.column_name])
self.operations.append(self.migrator.drop_index(
model._meta.table_name, index_name))
self.operations.append(
self.migrator.drop_column(
model._meta.table_name, field.column_name, cascade=False))
return model
def __del_field__(self, model: peewee.Model, field: peewee.Field):
"""
Deletes field from model.
"""
model._meta.remove_field(field.name)
delattr(model, field.name)
if isinstance(field, peewee.ForeignKeyField):
obj_id_name = field.column_name
if field.column_name == field.name:
obj_id_name += '_id'
delattr(model, obj_id_name)
delattr(field.rel_model, field.backref)
@get_model
def rename_column(self, model: peewee.Model, old_name: str, new_name: str) -> peewee.Model:
"""
Renames field in model.
"""
field = model._meta.fields[old_name]
if isinstance(field, peewee.ForeignKeyField):
old_name = field.column_name
self.__del_field__(model, field)
field.name = field.column_name = new_name
model._meta.add_field(new_name, field)
if isinstance(field, peewee.ForeignKeyField):
field.column_name = new_name = field.column_name + '_id'
self.operations.append(self.migrator.rename_column(
model._meta.table_name, old_name, new_name))
return model
@get_model
def rename_table(self, model: peewee.Model, new_name: str) -> peewee.Model:
"""
Renames table in database.
"""
old_name = model._meta.table_name
del self.orm[model._meta.table_name]
model._meta.table_name = new_name
self.orm[model._meta.table_name] = model
self.operations.append(self.migrator.rename_table(old_name, new_name))
return model
@get_model
def add_index(self, model: peewee.Model, *columns: str, **kwargs) -> peewee.Model:
"""Create indexes."""
unique = kwargs.pop('unique', False)
model._meta.indexes.append((columns, unique))
columns_ = []
for col in columns:
field = model._meta.fields.get(col)
if len(columns) == 1:
field.unique = unique
field.index = not unique
if isinstance(field, peewee.ForeignKeyField):
col = col + '_id'
columns_.append(col)
self.operations.append(self.migrator.add_index(
model._meta.table_name, columns_, unique=unique))
return model
@get_model
def drop_index(self, model: peewee.Model, *columns: str) -> peewee.Model:
"""Drop indexes."""
columns_ = []
for col in columns:
field = model._meta.fields.get(col)
if not field:
continue
if len(columns) == 1:
field.unique = field.index = False
if isinstance(field, peewee.ForeignKeyField):
col = col + '_id'
columns_.append(col)
index_name = make_index_name(model._meta.table_name, columns_)
model._meta.indexes = [(cols, _) for (
cols, _) in model._meta.indexes if columns != cols]
self.operations.append(self.migrator.drop_index(
model._meta.table_name, index_name))
return model
@get_model
def add_not_null(self, model: peewee.Model, *names: str) -> peewee.Model:
"""Add not null."""
for name in names:
field = model._meta.fields[name]
field.null = False
self.operations.append(self.migrator.add_not_null(
model._meta.table_name, field.column_name))
return model
@get_model
def drop_not_null(self, model: peewee.Model, *names: str) -> peewee.Model:
"""Drop not null."""
for name in names:
field = model._meta.fields[name]
field.null = True
self.operations.append(self.migrator.drop_not_null(
model._meta.table_name, field.column_name))
return model
@get_model
def add_default(self, model: peewee.Model, name: str, default: t.Any) -> peewee.Model:
"""Add default."""
field = model._meta.fields[name]
model._meta.defaults[field] = field.default = default
self.operations.append(self.migrator.apply_default(
model._meta.table_name, name, field))
return model
class SqliteMigrator(SqM):
def drop_table(self, model):
return lambda: model.drop_table(cascade=False)
@operation
def change_column(self, table: str, column_name: str, field: peewee.Field):
operations = [self.alter_change_column(table, column_name, field)]
if not field.null:
operations.extend([self.add_not_null(table, column_name)])
return operations
def alter_change_column(self, table: str, column_name: str, field: peewee.Field) -> Operation:
return self._update_column(table, column_name, lambda x, y: y)
@operation
def sql(self, sql: str, *params) -> SQL:
"""
Executes raw SQL.
"""
return SQL(sql, *params)
def alter_add_column(
self, table: str, column_name: str, field: peewee.Field, **kwargs) -> Operation:
"""
Fixes field name for ForeignKeys.
"""
name = field.name
op = super().alter_add_column(
table, column_name, field, **kwargs)
if isinstance(field, peewee.ForeignKeyField):
field.name = name
return op
class MigrationManager(object):
filemask = re.compile(r"[\d]+_[^\.]+\.py$")
def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]):
"""
Initializes the migration manager.
"""
if not isinstance(database, (peewee.Database, peewee.Proxy)):
raise RuntimeError('Invalid database: {}'.format(database))
self.database = database
@cached_property
def model(self) -> peewee.Model:
"""
Initialize and cache the MigrationHistory model.
"""
MigrateHistory._meta.database = self.database
MigrateHistory._meta.table_name = 'migratehistory'
MigrateHistory._meta.schema = None
MigrateHistory.create_table(True)
return MigrateHistory
@property
def done(self) -> t.List[str]:
"""
Scans migrations in the database.
"""
return [mm.name for mm in self.model.select().order_by(self.model.id)]
@property
def todo(self):
"""
Scans migrations in the file system.
"""
if not os.path.exists(helper.migration_dir):
logger.warning('Migration directory: {} does not exist.'.format(
helper.migration_dir))
os.makedirs(helper.migration_dir)
return sorted(f[:-3] for f in os.listdir(helper.migration_dir) if self.filemask.match(f))
@property
def diff(self) -> t.List[str]:
"""
Calculates difference between the filesystem and the database.
"""
done = set(self.done)
return [name for name in self.todo if name not in done]
@cached_property
def migrator(self) -> Migrator:
"""
Create migrator and setup it with fake migrations.
"""
migrator = Migrator(self.database)
for name in self.done:
self.up_one(name, migrator)
return migrator
def compile(self, name, migrate='', rollback=''):
"""
Compiles a migration.
"""
name = datetime.utcnow().strftime('%Y%m%d%H%M%S') + '_' + name
filename = name + '.py'
path = os.path.join(helper.migration_dir, filename)
with open(path, 'w') as f:
f.write(MIGRATE_TEMPLATE.format(
migrate=migrate, rollback=rollback, name=filename))
return name
def create(self, name: str = 'auto', auto: bool = False) -> t.Optional[str]:
"""
Creates a migration.
"""
migrate = rollback = ''
if auto:
raise NotImplementedError
logger.info('Creating migration "{}"'.format(name))
name = self.compile(name, migrate, rollback)
logger.info('Migration has been created as "{}"'.format(name))
return name
def clear(self):
"""Clear migrations."""
self.model.delete().execute()
def up(self, name: t.Optional[str] = None):
"""
Runs all unapplied migrations.
"""
logger.info('Starting migrations')
console.info('Starting migrations')
done = []
diff = self.diff
if not diff:
logger.info('There is nothing to migrate')
console.info('There is nothing to migrate')
return done
migrator = self.migrator
for mname in diff:
done.append(self.up_one(mname, migrator))
if name and name == mname:
break
return done
def read(self, name: str):
"""
Reads a migration from a file.
"""
call_params = dict()
if os.name == 'nt' and sys.version_info >= (3, 0):
# if system is windows - force utf-8 encoding
call_params['encoding'] = 'utf-8'
with open(os.path.join(helper.migration_dir, name + '.py'), **call_params) as f:
code = f.read()
scope = {}
code = compile(code, '<string>', 'exec', dont_inherit=True)
exec(code, scope, None)
return scope.get('migrate', VOID), scope.get('rollback', VOID)
def up_one(self, name: str, migrator: Migrator,
rollback: bool = False) -> str:
"""
Runs a migration with a given name.
"""
try:
migrate_fn, rollback_fn = self.read(name)
with self.database.transaction():
if rollback:
logger.info('Rolling back "{}"'.format(name))
rollback_fn(migrator, self.database)
migrator.run()
self.model.delete().where(self.model.name == name).execute()
else:
logger.info('Migrate "{}"'.format(name))
migrate_fn(migrator, self.database)
migrator.run()
if name not in self.done:
self.model.create(name=name)
logger.info('Done "{}"'.format(name))
return name
except Exception:
self.database.rollback()
operation = 'Rollback' if rollback else 'Migration'
logger.exception('{} failed: {}'.format(operation, name))
raise
def down(self, name: t.Optional[str] = None):
"""
Rolls back migrations.
"""
if not self.done:
raise RuntimeError('No migrations are found.')
name = self.done[-1]
migrator = self.migrator
self.up_one(name, migrator, True)
logger.warning('Rolled back migration: {}'.format(name))

View File

@ -22,32 +22,12 @@ except ModuleNotFoundError as e:
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
schema_version = (0, 1, 0) # major, minor, patch semver
database = SqliteDatabase(helper.db_path, pragmas={
'journal_mode': 'wal',
'cache_size': -1024 * 10})
class BaseModel(Model):
class Meta:
database = database
class SchemaVersion(BaseModel):
# DO NOT EVER CHANGE THE SCHEMA OF THIS TABLE
# (unless we have a REALLY good reason to)
# There will only ever be one row, and it allows the database loader to detect
# what it needs to do on major version upgrades so you don't have to wipe the DB
# every time you upgrade
schema_major = IntegerField()
schema_minor = IntegerField()
schema_patch = IntegerField()
class Meta:
table_name = 'schema_version'
primary_key = CompositeKey('schema_major', 'schema_minor', 'schema_patch')
class Users(BaseModel):
class Users(Model):
user_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
last_login = DateTimeField(default=datetime.datetime.now)
@ -61,9 +41,10 @@ class Users(BaseModel):
class Meta:
table_name = "users"
database = database
class Roles(BaseModel):
class Roles(Model):
role_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
last_update = DateTimeField(default=datetime.datetime.now)
@ -71,18 +52,20 @@ class Roles(BaseModel):
class Meta:
table_name = "roles"
database = database
class User_Roles(BaseModel):
class User_Roles(Model):
user_id = ForeignKeyField(Users, backref='user_role')
role_id = ForeignKeyField(Roles, backref='user_role')
class Meta:
table_name = 'user_roles'
primary_key = CompositeKey('user_id', 'role_id')
database = database
class Audit_Log(BaseModel):
class Audit_Log(Model):
audit_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
user_name = CharField(default="")
@ -91,8 +74,11 @@ class Audit_Log(BaseModel):
server_id = IntegerField(default=None, index=True) # When auditing global events, use server ID 0
log_msg = TextField(default='')
class Meta:
database = database
class Host_Stats(BaseModel):
class Host_Stats(Model):
time = DateTimeField(default=datetime.datetime.now, index=True)
boot_time = CharField(default="")
cpu_usage = FloatField(default=0)
@ -106,9 +92,10 @@ class Host_Stats(BaseModel):
class Meta:
table_name = "host_stats"
database = database
class Servers(BaseModel):
class Servers(Model):
server_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
server_uuid = CharField(default="", index=True)
@ -122,23 +109,37 @@ class Servers(BaseModel):
auto_start_delay = IntegerField(default=10)
crash_detection = BooleanField(default=0)
stop_command = CharField(default="stop")
executable_update_url = CharField(default="")
server_ip = CharField(default="127.0.0.1")
server_port = IntegerField(default=25565)
logs_delete_after = IntegerField(default=0)
class Meta:
table_name = "servers"
database = database
class Role_Servers(BaseModel):
class User_Servers(Model):
user_id = ForeignKeyField(Users, backref='user_server')
server_id = ForeignKeyField(Servers, backref='user_server')
class Meta:
table_name = 'user_servers'
primary_key = CompositeKey('user_id', 'server_id')
database = database
class Role_Servers(Model):
role_id = ForeignKeyField(Roles, backref='role_server')
server_id = ForeignKeyField(Servers, backref='role_server')
class Meta:
table_name = 'role_servers'
primary_key = CompositeKey('role_id', 'server_id')
database = database
class Server_Stats(BaseModel):
class Server_Stats(Model):
stats_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
server_id = ForeignKeyField(Servers, backref='server', index=True)
@ -156,13 +157,15 @@ class Server_Stats(BaseModel):
players = CharField(default="")
desc = CharField(default="Unable to Connect")
version = CharField(default="")
updating = BooleanField(default=False)
class Meta:
table_name = "server_stats"
database = database
class Commands(BaseModel):
class Commands(Model):
command_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
server_id = ForeignKeyField(Servers, backref='server', index=True)
@ -173,9 +176,10 @@ class Commands(BaseModel):
class Meta:
table_name = "commands"
database = database
class Webhooks(BaseModel):
class Webhooks(Model):
id = AutoField()
name = CharField(max_length=64, unique=True, index=True)
method = CharField(default="POST")
@ -185,8 +189,10 @@ class Webhooks(BaseModel):
class Meta:
table_name = "webhooks"
database = database
class Schedules(BaseModel):
class Schedules(Model):
schedule_id = IntegerField(unique=True, primary_key=True)
server_id = ForeignKeyField(Servers, backref='schedule_server')
enabled = BooleanField()
@ -199,8 +205,10 @@ class Schedules(BaseModel):
class Meta:
table_name = 'schedules'
database = database
class Backups(BaseModel):
class Backups(Model):
directories = CharField(null=True)
max_backups = IntegerField()
server_id = ForeignKeyField(Servers, backref='backups_server')
@ -208,38 +216,15 @@ class Backups(BaseModel):
class Meta:
table_name = 'backups'
database = database
class db_builder:
@staticmethod
def create_tables():
with database:
database.create_tables([
Backups,
Users,
Roles,
User_Roles,
Host_Stats,
Webhooks,
Servers,
Role_Servers,
Server_Stats,
Commands,
Audit_Log,
SchemaVersion,
Schedules
])
@staticmethod
def default_settings():
logger.info("Fresh Install Detected - Creating Default Settings")
console.info("Fresh Install Detected - Creating Default Settings")
SchemaVersion.insert({
SchemaVersion.schema_major: schema_version[0],
SchemaVersion.schema_minor: schema_version[1],
SchemaVersion.schema_patch: schema_version[2]
}).execute()
default_data = helper.find_default_password()
username = default_data.get("username", 'admin')
@ -266,39 +251,8 @@ class db_builder:
return True
pass
@staticmethod
def check_schema_version():
svs = SchemaVersion.select().execute()
if len(svs) != 1:
raise exceptions.SchemaError("Multiple or no schema versions detected - potentially a failed upgrade?")
sv = svs[0]
svt = (sv.schema_major, sv.schema_minor, sv.schema_patch)
logger.debug("Schema: found {}, expected {}".format(svt, schema_version))
console.debug("Schema: found {}, expected {}".format(svt, schema_version))
if sv.schema_major > schema_version[0]:
raise exceptions.SchemaError("Major version mismatch - possible code reversion")
elif sv.schema_major < schema_version[0]:
db_shortcuts.upgrade_schema()
if sv.schema_minor > schema_version[1]:
logger.warning("Schema minor mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version))
console.warning("Schema minor mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version))
elif sv.schema_minor < schema_version[1]:
db_shortcuts.upgrade_schema()
if sv.schema_patch > schema_version[2]:
logger.info("Schema patch mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version))
console.info("Schema patch mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version))
elif sv.schema_patch < schema_version[2]:
db_shortcuts.upgrade_schema()
logger.info("Schema validation successful! {}".format(schema_version))
class db_shortcuts:
@staticmethod
def upgrade_schema():
raise NotImplemented("I don't know who you are or how you reached this code, but this should NOT have happened. Please report it to the developer with due haste.")
@staticmethod
def return_rows(query):
rows = []
@ -334,6 +288,7 @@ class db_shortcuts:
def remove_server(server_id):
with database.atomic():
Role_Servers.delete().where(Role_Servers.server_id == server_id).execute()
User_Servers.delete().where(User_Servers.server_id == server_id).execute()
Servers.delete().where(Servers.server_id == server_id).execute()
@staticmethod
@ -358,7 +313,37 @@ class db_shortcuts:
server_data.append(db_helper.get_server_data_by_id(u.server_id))
return server_data
@staticmethod
def get_all_authorized_servers(user_id):
user_servers = User_Servers.select().where(User_Servers.user_id == user_id)
user_roles = User_Roles.select().where(User_Roles.user_id == user_id)
server_data = []
roles_list = []
role_server = []
server_data = []
for u in user_servers:
server_data.append(db_helper.get_server_data_by_id(u.server_id))
for u in user_roles:
roles_list.append(db_helper.get_role(u.role_id))
for r in roles_list:
role_test = Role_Servers.select().where(Role_Servers.role_id == r.get('role_id'))
for t in role_test:
role_server.append(t)
for s in role_server:
found = False
for item in user_servers:
if s.server_id == item.server_id:
found = True
if not found:
server_data.append(db_helper.get_server_data_by_id(s.server_id))
return server_data
@staticmethod
def get_authorized_servers_from_roles(user_id):
user_roles = User_Roles.select().where(User_Roles.user_id == user_id)
@ -394,16 +379,44 @@ class db_shortcuts:
user_servers = User_Servers.select().where(User_Servers.user_id == user_id)
authorized_servers = []
server_data = []
user_roles = User_Roles.select().where(User_Roles.user_id == user_id)
roles_list = []
role_server = []
for u in user_servers:
authorized_servers.append(db_helper.get_server_data_by_id(u.server_id))
for u in user_roles:
roles_list.append(db_helper.get_role(u.role_id))
for r in roles_list:
role_test = Role_Servers.select().where(Role_Servers.role_id == r.get('role_id'))
for t in role_test:
role_server.append(t)
for s in role_server:
found = False
for item in user_servers:
if s.server_id == item.server_id:
found = True
if not found:
authorized_servers.append(db_helper.get_server_data_by_id(s.server_id))
for s in authorized_servers:
latest = Server_Stats.select().where(Server_Stats.server_id == s.get('server_id')).order_by(Server_Stats.created.desc()).limit(1)
server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)})
latest = Server_Stats.select().where(Server_Stats.server_id == s.get('server_id')).order_by(
Server_Stats.created.desc()).limit(1)
server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0]})
return server_data
@staticmethod
def get_user_roles_names(user_id):
roles_list = []
roles = User_Roles.select().where(User_Roles.user_id == user_id)
for r in roles:
roles_list.append(db_helper.get_role(r.role_id)['role_name'])
return roles_list
@staticmethod
def get_authorized_servers_stats_from_roles(user_id):
user_roles = User_Roles.select().where(User_Roles.user_id == user_id)
@ -529,16 +542,34 @@ class db_shortcuts:
roles = set()
for r in roles_query:
roles.add(r.role_id.role_id)
#servers_query = User_Servers.select().join(Servers, JOIN.INNER).where(User_Servers.user_id == user_id)
servers_query = User_Servers.select().join(Servers, JOIN.INNER).where(User_Servers.user_id == user_id)
## TODO: this query needs to be narrower
servers = set()
#for s in servers_query:
# servers.add(s.server_id.server_id)
for s in servers_query:
servers.add(s.server_id.server_id)
user['roles'] = roles
#user['servers'] = servers
user['servers'] = servers
#logger.debug("user: ({}) {}".format(user_id, user))
return user
@staticmethod
def add_user_server(server_id, user_id):
servers = User_Servers.insert({User_Servers.server_id: server_id, User_Servers.user_id: user_id}).execute()
return servers
@staticmethod
def user_query(user_id):
user_query = Users.select().where(Users.user_id == user_id)
return user_query
@staticmethod
def user_role_query(user_id):
user_query = User_Roles.select().where(User_Roles.user_id == user_id)
query = Roles.select().where(Roles.role_id == -1)
for u in user_query:
query = Roles.select().where(Roles.role_id == u.role_id)
return query
@staticmethod
def get_user(user_id):
@ -555,7 +586,7 @@ class db_shortcuts:
superuser: False,
api_token: None,
roles: [],
servers: []
servers: [],
}
user = model_to_dict(Users.get(Users.user_id == user_id))
@ -581,9 +612,9 @@ class db_shortcuts:
elif key == "roles":
added_roles = user_data['roles'].difference(base_data['roles'])
removed_roles = base_data['roles'].difference(user_data['roles'])
#elif key == "servers":
# added_servers = user_data['servers'].difference(base_data['servers'])
# removed_servers = base_data['servers'].difference(user_data['servers'])
elif key == "servers":
added_servers = user_data['servers'].difference(base_data['servers'])
removed_servers = base_data['servers'].difference(user_data['servers'])
elif key == "regen_api":
if user_data['regen_api']:
up_data['api_token'] = db_shortcuts.new_api_token()
@ -600,10 +631,10 @@ class db_shortcuts:
# TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point
User_Roles.delete().where(User_Roles.user_id == user_id).where(User_Roles.role_id.in_(removed_roles)).execute()
#for server in added_servers:
# User_Servers.get_or_create(user_id=user_id, server_id=server)
for server in added_servers:
User_Servers.get_or_create(user_id=user_id, server_id=server)
# # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point
#User_Servers.delete().where(User_Servers.user_id == user_id).where(User_Servers.server_id.in_(removed_servers)).execute()
User_Servers.delete().where(User_Servers.user_id == user_id).where(User_Servers.server_id.in_(removed_servers)).execute()
if up_data:
Users.update(up_data).where(Users.user_id == user_id).execute()
@ -842,6 +873,15 @@ class db_shortcuts:
}
return conf
@staticmethod
def set_update(server_id, value):
try:
row = Server_Stats.select().where(Server_Stats.server_id == server_id)
except Exception as ex:
logger.error("Database entry not found. ".format(ex))
with database.atomic():
Server_Stats.update(updating=value).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True):
logger.debug("Updating server {} backup config with {}".format(server_id, locals()))

View File

@ -10,6 +10,7 @@ import threading
import schedule
import logging.config
import zipfile
from threading import Thread
import html
@ -86,11 +87,14 @@ class Server:
self.settings = None
self.updating = False
self.server_id = None
self.jar_update_url = None
self.name = None
self.is_crashed = False
self.restart_count = 0
self.crash_watcher_schedule = None
self.stats = stats
self.backup_thread = threading.Thread(target=self.a_backup_server, daemon=True, name="backup")
self.is_backingup = False
def reload_server_settings(self):
server_data = db_helper.get_server_data_by_id(self.server_id)
@ -155,11 +159,16 @@ class Server:
def start_server(self):
logger.info("Start command detected. Reloading settings from DB for server {}".format(self.name))
self.setup_server_run_command()
# fail safe in case we try to start something already running
if self.check_running():
logger.error("Server is already running - Cancelling Startup")
console.error("Server is already running - Cancelling Startup")
return False
if self.check_update():
logger.error("Server is updating. Terminating startup.")
return False
logger.info("Launching Server {} with command {}".format(self.name, self.server_command))
console.info("Launching Server {} with command {}".format(self.name, self.server_command))
@ -171,6 +180,19 @@ class Server:
logger.info("Linux Detected")
logger.info("Starting server in {p} with command: {c}".format(p=self.server_path, c=self.server_command))
<<<<<<< app/classes/shared/server.py
try:
self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None)
except Exception as ex:
msg = "Server {} failed to start with error code: {}".format(self.name, ex)
logger.error(msg)
websocket_helper.broadcast('send_start_error', {
'error': msg
})
return False
websocket_helper.broadcast('send_start_reload', {
})
=======
self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding='utf-8')
out_buf = ServerOutBuf(self.process, self.server_id)
@ -178,6 +200,7 @@ class Server:
logger.debug('Starting virtual terminal listener for server {}'.format(self.name))
threading.Thread(target=out_buf.check, daemon=True, name='{}_virtual_terminal'.format(self.server_id)).start()
>>>>>>> app/classes/shared/server.py
self.is_crashed = False
self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'))
@ -368,20 +391,50 @@ class Server:
console.info("Removing old crash detection watcher thread")
schedule.clear(self.name)
def is_backup_running(self):
if self.is_backingup:
return True
else:
return False
def backup_server(self):
backup_thread = threading.Thread(target=self.a_backup_server, daemon=True, name="backup")
logger.info("Starting Backup Thread for server {}.".format(self.settings['server_name']))
#checks if the backup thread is currently alive for this server
if not self.is_backingup:
try:
backup_thread.start()
except Exception as ex:
logger.error("Failed to start backup: {}".format(ex))
return False
else:
logger.error("Backup is already being processed for server {}. Canceling backup request".format(self.settings['server_name']))
return False
logger.info("Backup Thread started for server {}.".format(self.settings['server_name']))
def a_backup_server(self):
logger.info("Starting server {} (ID {}) backup".format(self.name, self.server_id))
self.is_backingup = True
conf = db_helper.get_backup_config(self.server_id)
try:
backup_filename = "{}/{}.zip".format(conf['backup_path'], datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
backup_filename = "{}/{}.zip".format(self.settings['backup_path'], datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
logger.info("Creating backup of server '{}' (ID#{}) at '{}'".format(self.settings['server_name'], self.server_id, backup_filename))
helper.zip_directory(backup_filename, self.server_path)
backup_list = self.list_backups()
if len(self.list_backups()) > conf["max_backups"]:
while len(self.list_backups()) > conf["max_backups"]:
backup_list = self.list_backups()
oldfile = backup_list[0]
backup_path = self.settings['backup_path']
old_file_name = oldfile['path']
back_old_file = os.path.join(backup_path, old_file_name)
logger.info("Removing old backup '{}'".format(oldfile))
os.remove(oldfile)
os.remove(back_old_file)
self.is_backingup = False
logger.info("Backup of server: {} completed".format(self.name))
return
except:
logger.exception("Failed to create backup of server {} (ID {})".format(self.name, self.server_id))
self.is_backingup = False
return
def list_backups(self):
conf = db_helper.get_backup_config(self.server_id)
@ -390,3 +443,89 @@ class Server:
return [{"path": os.path.relpath(f['path'], start=conf['backup_path']), "size": f["size"]} for f in files]
else:
return []
def jar_update(self):
db_helper.set_update(self.server_id, True)
update_thread = threading.Thread(target=self.a_jar_update, daemon=True, name="exe_update")
update_thread.start()
def check_update(self):
server_stats = db_helper.get_server_stats_by_id(self.server_id)
if server_stats['updating']:
return True
else:
return False
def a_jar_update(self):
error = False
wasStarted = "-1"
self.backup_server()
#checks if server is running. Calls shutdown if it is running.
if self.check_running():
wasStarted = True
logger.info("Server with PID {} is running. Sending shutdown command".format(self.PID))
self.stop_threaded_server()
else:
wasStarted = False
if len(websocket_helper.clients) > 0:
# There are clients
self.check_update()
message = '<a data-id="'+str(self.server_id)+'" class=""> UPDATING...</i></a>'
websocket_helper.broadcast('update_button_status', {
'isUpdating': self.check_update(),
'server_id': self.server_id,
'wasRunning': wasStarted,
'string': message
})
backup_dir = os.path.join(self.settings['path'], 'crafty_executable_backups')
#checks if backup directory already exists
if os.path.isdir(backup_dir):
backup_executable = os.path.join(backup_dir, 'old_server.jar')
else:
logger.info("Executable backup directory not found for Server: {}. Creating one.".format(self.name))
os.mkdir(backup_dir)
backup_executable = os.path.join(backup_dir, 'old_server.jar')
if os.path.isfile(backup_executable):
#removes old backup
logger.info("Old backup found for server: {}. Removing...".format(self.name))
os.remove(backup_executable)
logger.info("Old backup removed for server: {}.".format(self.name))
else:
logger.info("No old backups found for server: {}".format(self.name))
current_executable = os.path.join(self.settings['path'], self.settings['executable'])
#copies to backup dir
helper.copy_files(current_executable, backup_executable)
#boolean returns true for false for success
downloaded = helper.download_file(self.settings['executable_update_url'], current_executable)
while db_helper.get_server_stats_by_id(self.server_id)['updating']:
if downloaded and not self.is_backingup:
logger.info("Executable updated successfully. Starting Server")
db_helper.set_update(self.server_id, False)
if len(websocket_helper.clients) > 0:
# There are clients
self.check_update()
websocket_helper.broadcast('notification', "Executable update finished for " + self.name)
time.sleep(3)
websocket_helper.broadcast('update_button_status', {
'isUpdating': self.check_update(),
'server_id': self.server_id,
'wasRunning': wasStarted
})
websocket_helper.broadcast('notification', "Executable update finished for "+self.name)
db_helper.add_to_audit_log_raw('Alert', '-1', self.server_id, "Executable update finished for "+self.name, self.settings['server_ip'])
if wasStarted:
self.start_server()
elif not downloaded and not self.is_backingup:
time.sleep(5)
db_helper.set_update(self.server_id, False)
websocket_helper.broadcast('notification',
"Executable update failed for " + self.name + ". Check log file for details.")
logger.error("Executable download failed.")
pass

View File

@ -5,6 +5,7 @@ import time
import logging
import threading
import asyncio
import shutil
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
@ -110,6 +111,18 @@ class TasksManager:
elif command == "backup_server":
svr.backup_server()
elif command == "update_executable":
svr.jar_update()
elif command == "delete_server":
logger.info(
"Removing server from panel for server: {}".format(c['server_id']['server_name']))
self.controller.remove_server(c['server_id']['server_id'], False)
elif command == "delete_server_files":
logger.info(
"Removing server and all associated files for server: {}".format(c['server_id']['server_name']))
self.controller.remove_server(c['server_id']['server_id'], True)
db_helper.mark_command_complete(c.get('command_id', None))
time.sleep(1)

View File

@ -213,7 +213,7 @@ class AjaxHandler(BaseHandler):
dir_path = self.get_body_argument('dir_path', default=None, strip=True)
server_id = self.get_argument('id', None)
console.warning("delete {} for server {}".format(file_path, server_id))
console.warning("delete {} for server {}".format(dir_path, server_id))
if not self.check_server_id(server_id, 'del_dir'): return
else: server_id = bleach.clean(server_id)

View File

@ -7,6 +7,8 @@ import time
import datetime
import os
from tornado import iostream
from app.classes.shared.console import console
from app.classes.shared.models import Users, installer
from app.classes.web.base_handler import BaseHandler
@ -36,11 +38,11 @@ class PanelHandler(BaseHandler):
defined_servers = self.controller.list_defined_servers()
exec_user_role.add("Super User")
else:
defined_servers = self.controller.list_authorized_servers(exec_user_id)
logger.debug(exec_user['roles'])
for r in exec_user['roles']:
role = db_helper.get_role(r)
exec_user_role.add(role['role_name'])
defined_servers = db_helper.get_all_authorized_servers(exec_user_id)
page_data = {
# todo: make this actually pull and compare version data
@ -109,10 +111,9 @@ class PanelHandler(BaseHandler):
if exec_user['superuser'] == 1:
page_data['servers'] = db_helper.get_all_servers_stats()
else:
#page_data['servers'] = db_helper.get_authorized_servers_stats(exec_user_id)
ras = db_helper.get_authorized_servers_stats_from_roles(exec_user_id)
logger.debug("ASFR: {}".format(ras))
page_data['servers'] = ras
user_auth = db_helper.get_authorized_servers_stats(exec_user_id)
logger.debug("ASFR: {}".format(user_auth))
page_data['servers'] = user_auth
for s in page_data['servers']:
try:
@ -137,10 +138,10 @@ class PanelHandler(BaseHandler):
return
if exec_user['superuser'] != 1:
#if not db_helper.server_id_authorized(server_id, exec_user_id):
if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id):
self.redirect("/panel/error?error=Invalid Server ID")
return
if not db_helper.server_id_authorized(server_id, exec_user_id):
if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id):
self.redirect("/panel/error?error=Invalid Server ID")
return False
valid_subpages = ['term', 'logs', 'backup', 'config', 'files', 'admin_controls']
@ -255,8 +256,43 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id))
elif page == 'panel_config':
page_data['users'] = db_helper.get_all_users()
page_data['roles'] = db_helper.get_all_roles()
auth_servers = {}
auth_role_servers = {}
users_list = []
role_users = {}
roles = db_helper.get_all_roles()
role_servers = []
user_roles = {}
for user in db_helper.get_all_users():
user_roles_list = db_helper.get_user_roles_names(user.user_id)
user_servers = db_helper.get_all_authorized_servers(user.user_id)
servers = []
for server in user_servers:
servers.append(server['server_name'])
new_item = {user.user_id: servers}
auth_servers.update(new_item)
data = {user.user_id: user_roles_list}
user_roles.update(data)
for role in roles:
role_servers = []
role = db_helper.get_role(role.role_id)
for serv_id in role['servers']:
role_servers.append(db_helper.get_server_data_by_id(serv_id)['server_name'])
data = {role['role_id']: role_servers}
auth_role_servers.update(data)
page_data['auth-servers'] = auth_servers
page_data['role-servers'] = auth_role_servers
page_data['user-roles'] = user_roles
if exec_user['superuser'] == 1:
page_data['users'] = db_helper.get_all_users()
page_data['roles'] = db_helper.get_all_roles()
else:
page_data['users'] = db_helper.user_query(exec_user['user_id'])
page_data['roles'] = db_helper.user_role_query(exec_user['user_id'])
for user in page_data['users']:
if user.user_id != exec_user['user_id']:
user.api_token = "********"
@ -283,22 +319,39 @@ class PanelHandler(BaseHandler):
page_data['roles_all'] = db_helper.get_all_roles()
page_data['servers'] = []
page_data['servers_all'] = self.controller.list_defined_servers()
page_data['role-servers'] = []
template = "panel/panel_edit_user.html"
elif page == "edit_user":
page_data['new_user'] = False
user_id = self.get_argument('id', None)
user_servers = db_helper.get_authorized_servers(user_id)
role_servers = db_helper.get_authorized_servers_from_roles(user_id)
page_role_servers = []
servers = set()
for server in user_servers:
flag = False
for rserver in role_servers:
if rserver['server_id'] == server['server_id']:
flag = True
if not flag:
servers.add(server['server_id'])
for server in role_servers:
page_role_servers.append(server['server_id'])
page_data['new_user'] = False
page_data['user'] = db_helper.get_user(user_id)
page_data['servers'] = db_helper.get_authorized_servers_stats_from_roles(user_id)
page_data['servers'] = servers
page_data['role-servers'] = page_role_servers
page_data['roles_all'] = db_helper.get_all_roles()
page_data['servers_all'] = self.controller.list_defined_servers()
if not exec_user['superuser']:
self.redirect("/panel/error?error=Unauthorized access: not superuser")
return
elif user_id is None:
if user_id is None:
self.redirect("/panel/error?error=Invalid User ID")
return
elif not exec_user['superuser']:
page_data['servers'] = []
page_data['role-servers'] = []
page_data['roles_all'] = []
page_data['servers_all'] = []
if exec_user['user_id'] != page_data['user']['user_id']:
page_data['user']['api_token'] = "********"
@ -332,6 +385,12 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/panel_config")
elif page == "add_role":
user_roles = {}
for user in db_helper.get_all_users():
user_roles_list = db_helper.get_user_roles_names(user.user_id)
user_servers = db_helper.get_all_authorized_servers(user.user_id)
data = {user.user_id: user_roles_list}
user_roles.update(data)
page_data['new_role'] = True
page_data['role'] = {}
page_data['role']['role_name'] = ""
@ -339,6 +398,8 @@ class PanelHandler(BaseHandler):
page_data['role']['created'] = "N/A"
page_data['role']['last_update'] = "N/A"
page_data['role']['servers'] = set()
page_data['user-roles'] = user_roles
page_data['users'] = db_helper.get_all_users()
if not exec_user['superuser']:
self.redirect("/panel/error?error=Unauthorized access: not superuser")
@ -348,10 +409,20 @@ class PanelHandler(BaseHandler):
template = "panel/panel_edit_role.html"
elif page == "edit_role":
auth_servers = {}
user_roles = {}
for user in db_helper.get_all_users():
user_roles_list = db_helper.get_user_roles_names(user.user_id)
user_servers = db_helper.get_all_authorized_servers(user.user_id)
data = {user.user_id: user_roles_list}
user_roles.update(data)
page_data['new_role'] = False
role_id = self.get_argument('id', None)
page_data['role'] = db_helper.get_role(role_id)
page_data['servers_all'] = self.controller.list_defined_servers()
page_data['user-roles'] = user_roles
page_data['users'] = db_helper.get_all_users()
if not exec_user['superuser']:
self.redirect("/panel/error?error=Unauthorized access: not superuser")
@ -426,14 +497,17 @@ class PanelHandler(BaseHandler):
auto_start_delay = self.get_argument('auto_start_delay', '10')
server_ip = self.get_argument('server_ip', None)
server_port = self.get_argument('server_port', None)
executable_update_url = self.get_argument('executable_update_url', None)
auto_start = int(float(self.get_argument('auto_start', '0')))
crash_detection = int(float(self.get_argument('crash_detection', '0')))
logs_delete_after = int(float(self.get_argument('logs_delete_after', '0')))
subpage = self.get_argument('subpage', None)
if not exec_user['superuser']:
self.redirect("/panel/error?error=Unauthorized access: not superuser")
return
if not db_helper.server_id_authorized(server_id, exec_user_id):
if not db_helper.server_id_authorized_from_roles(server_id, exec_user_id):
self.redirect("/panel/error?error=Unauthorized access: invalid server id")
return
elif server_id is None:
self.redirect("/panel/error?error=Invalid Server ID")
return
@ -454,6 +528,7 @@ class PanelHandler(BaseHandler):
Servers.server_ip: server_ip,
Servers.server_port: server_port,
Servers.auto_start: auto_start,
Servers.executable_update_url: executable_update_url,
Servers.crash_detection: crash_detection,
Servers.logs_delete_after: logs_delete_after,
}).where(Servers.server_id == server_id).execute()
@ -508,7 +583,18 @@ class PanelHandler(BaseHandler):
regen_api = int(float(self.get_argument('regen_api', '0')))
if not exec_user['superuser']:
self.redirect("/panel/error?error=Unauthorized access: not superuser")
user_data = {
"username": username,
"password": password0,
}
db_helper.update_user(user_id, user_data=user_data)
db_helper.add_to_audit_log(exec_user['user_id'],
"Edited user {} (UID:{}) password".format(username,
user_id),
server_id=0,
source_ip=self.get_remote_ip())
self.redirect("/panel/panel_config")
return
elif username is None or username == "":
self.redirect("/panel/error?error=Invalid username")
@ -536,17 +622,28 @@ class PanelHandler(BaseHandler):
if argument:
roles.add(role.role_id)
servers = set()
for server in self.controller.list_defined_servers():
argument = int(float(
bleach.clean(
self.get_argument('server_{}_access'.format(server['server_id']), '0')
)
))
if argument:
servers.add(server['server_id'])
user_data = {
"username": username,
"password": password0,
"enabled": enabled,
"regen_api": regen_api,
"roles": roles,
"servers": servers,
}
db_helper.update_user(user_id, user_data=user_data)
db_helper.add_to_audit_log(exec_user['user_id'],
"Edited user {} (UID:{}) with roles {}".format(username, user_id, roles),
"Edited user {} (UID:{}) with roles {} and servers {}".format(username, user_id, roles, servers),
server_id=0,
source_ip=self.get_remote_ip())
self.redirect("/panel/panel_config")
@ -595,7 +692,11 @@ class PanelHandler(BaseHandler):
servers.add(server['server_id'])
user_id = db_helper.add_user(username, password=password0, enabled=enabled)
db_helper.update_user(user_id, {"roles":roles})
user_data = {
"roles": roles,
"servers": servers,
}
db_helper.update_user(user_id, user_data)
db_helper.add_to_audit_log(exec_user['user_id'],
"Added user {} (UID:{})".format(username, user_id),

View File

@ -196,6 +196,7 @@ class ServerHandler(BaseHandler):
server_type, server_version = server_parts
# todo: add server type check here and call the correct server add functions if not a jar
new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port)
db_helper.add_user_server(new_server_id, exec_user_id)
db_helper.add_to_audit_log(exec_user_data['user_id'],
"created a {} {} server named \"{}\"".format(server_version, str(server_type).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,

View File

@ -220,6 +220,25 @@
var webSocket;
// {% end%}
if (webSocket) {
webSocket.on('send_start_error', function (start_error) {
var x = document.querySelector('.bootbox');
if(x){
x.remove()}
var x = document.querySelector('.modal-backdrop');
if(x){
x.remove()}
bootbox.alert(start_error.error);
});
}
if (webSocket) {
webSocket.on('send_start_reload', function (start_error) {
location.reload()
});
}
function warn(message) {
var closeEl = document.createElement('span');
var strongEL = document.createElement('strong');

View File

@ -123,14 +123,15 @@
</a>
</td>
<td class="actions_serverlist">
<td id="controls{{server['server_data']['server_id']}}" class="actions_serverlist">
{% if server['stats']['running'] %}
<a class="stop_button" data-id="{{server['server_data']['server_id']}}"> <i class="fas fa-stop"></i></a> &nbsp;
<a class="restart_button" data-id="{{server['server_data']['server_id']}}"> <i class="fas fa-sync"></i></a> &nbsp;
{% elif server['stats']['updating']%}
<a data-id="{{server['server_data']['server_id']}}" class=""> UPDATING...</i></a>
{% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button"><i class="fas fa-play"></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button"> <i class="fas fa-clone"></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="play_button"><i class="fas fa-play"></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button"> <i class="fas fa-clone"></i></a>&nbsp;
{% end %}
</td>
@ -228,7 +229,11 @@ function send_command (server_id, command){
success: function(data){
console.log("got response:");
console.log(data);
setTimeout(function(){ location.reload(); }, 10000);
setTimeout(function(){
if (command != 'start_server'){
location.reload();
}
}, 10000);
}
});
@ -281,6 +286,21 @@ $( document ).ready(function() {
mem_percent.textContent = hostStats.mem_percent + '%';
});
}
if (webSocket) {
webSocket.on('update_button_status', function (updateButton) {
var id = 'controls';
var dataId = updateButton.server_id;
var string = updateButton.string
var id = id.concat(updateButton.server_id);
if (updateButton.isUpdating){
console.log(updateButton.isUpdating)
document.getElementById(id).innerHTML = string;
}
else{
window.location.reload()
}
});
}
$( ".clone_button" ).click(function() {
server_id = $(this).attr("data-id");

View File

@ -45,6 +45,7 @@
<th>Enabled</th>
<th>API Token</th>
<th>Allowed Servers</th>
<th>Assigned Roles</th>
<th>Edit</th>
</tr>
</thead>
@ -66,7 +67,16 @@
</td>
<td>{{ user.api_token }}</td>
<td>{{ [] }}</td>
<td class="server_list_{{user.user_id}}"><ul id="{{user.user_id}}">
{% for item in data['auth-servers'][user.user_id] %}
<li>{{item}}</li>
{% end %}
</ul></td>
<td class="role_list_{{user.user_id}}"><ul>
{% for item in data['user-roles'][user.user_id] %}
<li>{{item}}</li>
{% end %}
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
</tr>
{% end %}
@ -95,6 +105,7 @@
<tr class="rounded">
<th>Role</th>
<th>Allowed Servers</th>
<th>Role Users</th>
<th>Edit</th>
</tr>
</thead>
@ -102,7 +113,21 @@
{% for role in data['roles'] %}
<tr>
<td>{{ role.role_name }}</td>
<td>{{ [] }}</td>
<td class="role_list_{{role.role_id}}"><ul id="{{role.role_id}}">
{% for item in data['role-servers'][role.role_id] %}
<li>{{item}}</li>
{% end %}
</ul></td>
<td><ul>
{% for user in data['users'] %}
{% for ruser in data['user-roles'][user.user_id] %}
{% if ruser == role.role_name %}
<li>{{ user.username }}</li>
{% end %}
{% end %}
{% end %}
</ul>
</td>
<td><a href="/panel/edit_role?id={{role.role_id}}"><i class="fas fa-pencil-alt"></i></a></td>
</tr>
{% end %}

View File

@ -40,15 +40,14 @@
<div class="card-body pt-0">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item">
<a class="nav-link active" href="/panel/edit_role?id={{ data['role']['role_name'] }}&subpage=config" role="tab" aria-selected="true">
<a class="nav-link active" href="/panel/edit_role?id={{ data['role']['role_id'] }}&subpage=config" role="tab" aria-selected="true">
<i class="fas fa-cogs"></i>Config</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/panel/edit_role?id={{ data['role']['role_name'] }}&subpage=other" role="tab" aria-selected="false">
<a class="nav-link" href="/panel/edit_role?id={{ data['role']['role_id'] }}&subpage=other" role="tab" aria-selected="false">
<i class="fas fa-folder-tree"></i>Other</a>
</li>
</ul>
<div class="row">
<div class="col-md-6 col-sm-12">
{% if data['new_role'] %}
@ -95,11 +94,24 @@
</div>
<button type="submit" class="btn btn-success mr-2">Save</button>
<button type="reset" class="btn btn-light">Cancel</button>
<button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light">Cancel</button>
</form>
</div>
<div class="col-md-6 col-sm-12">
</div>
<div class="col-md-3 col-sm-12">
<h6>Users Assigned to Role:</h6>
<ul>
{% for user in data['users'] %}
{% for ruser in data['user-roles'][user.user_id] %}
{% if ruser == data['role']['role_name'] %}
<li>{{ user.username }}</li>
{% end %}
{% end %}
{% end %}
</ul>
<br>
</div>
<div class="col-md-3 col-sm-12">
<div class="card">
<div class="card-body">
<h4 class="card-title">Role Config Area</h4>
@ -114,7 +126,9 @@
</blockquote>
</div>
</div>
<div class="text-center">
</div>
</div>
<div class="text-center">
{% if data['new_role'] %}
<a class="btn btn-sm btn-danger disabled">Delete Role</a><br />
<small>You cannot delete something that does not yet exist</small>
@ -123,8 +137,6 @@
{% end %}
</div>
</div>
</div>
</div>
</div>

View File

@ -76,7 +76,7 @@
<label for="password1">Repeat Password <small class="text-muted ml-1"></small> </label>
<input type="password" class="form-control" name="password1" id="password1" value="" placeholder="Repeat Password" >
</div>
{% if len(data['servers_all']) > 0 %}
<div class="form-group">
<label for="role_membership">Roles <small class="text-muted ml-1"> - the roles this user is a member of</small> </label>
<div class="table-responsive">
@ -106,7 +106,6 @@
</table>
</div>
</div>
<div class="form-group">
<label for="server_membership">Servers <small class="text-muted ml-1"> - servers this user is allowed to access </small> </label>
<div class="table-responsive">
@ -123,9 +122,11 @@
<td>{{ server['server_name'] }}</td>
<td>
{% if server['server_id'] in data['servers'] %}
<input type="checkbox" class="form-check-input" id="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access" checked="" disabled>
<input type="checkbox" class="form-check-input" id="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access" checked="" value="1">
{% elif server['server_id'] in data['role-servers'] %}
<input type="checkbox" class="form-check-input" id="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access" checked="" value="" disabled>
{% else %}
<input type="checkbox" class="form-check-input" id="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access" disabled>
<input type="checkbox" class="form-check-input" id="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access" value="1">
{% end %}
</td>
</tr>
@ -136,6 +137,7 @@
</div>
</div>
<div class="form-check-flat">
<label for="enabled" class="form-check-label ml-4 mb-4">
{% if data['user']['enabled'] %}
@ -162,9 +164,9 @@
</label>
</div>
{% end %}
<button type="submit" class="btn btn-success mr-2">Save</button>
<button type="reset" class="btn btn-light">Cancel</button>
<button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light">Cancel</button>
</form>
</div>

View File

@ -106,13 +106,18 @@
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay" value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10" >
</div>
<div class="form-group">
<label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc') }}</small> </label>
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url" value="{{ data['server_stats']['server_id']['executable_update_url'] }}" placeholder="{{ translate('serverConfig', 'exeUpdateURL') }}" >
</div>
<div class="form-group">
<label for="server_ip">{{ translate('serverConfig', 'serverPort') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc') }}</small> </label>
<label for="server_ip">{{ translate('serverConfig', 'serverIP') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc') }}</small> </label>
<input type="text" class="form-control" name="server_ip" id="server_ip" value="{{ data['server_stats']['server_id']['server_ip'] }}">
</div>
<div class="form-group">
<label for="server_port">{{ translate('serverConfig', 'serverIP') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverIPDesc') }}</small> </label>
<label for="server_port">{{ translate('serverConfig', 'serverPort') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverIPDesc') }}</small> </label>
<input type="number" class="form-control" name="server_port" id="server_port" value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1" >
</div>
@ -159,10 +164,12 @@
</div>
<div class="text-center">
{% if data['server_stats']['running'] %}
<button onclick="send_command(server_id, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig', 'update') }}</button>
<a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer') }}</a><br />
<small>{{ translate('serverConfig', 'stopBeforeDeleting') }}</small>
{% else %}
<a href="/panel/remove_server?id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-sm btn-danger">{{ translate('serverConfig', 'deleteServer') }}</a>
<button onclick="send_command(server_id, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig', 'update') }}</button>
<button onclick="deleteConfirm()" class="btn btn-sm btn-danger">{{ translate('serverConfig', 'deleteServer') }}</button>
{% end %}
</div>
@ -196,6 +203,105 @@
});
let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
function send_command (server_id, command){
<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/server/command?command=' + command + '&id=' + server_id,
success: function(data){
console.log("got response:");
console.log(data);
setTimeout(function(){ location.reload(); }, 10000);
}
});
if(command != "delete_server" && command != "delete_server_files"){
bootbox.alert({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingRequest") %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientUpdate") %} </div>'
});
}
}
function deleteServer (){
path = "{{data['server_stats']['server_id']['path']}}";
name = "{{data['server_stats']['server_id']['server_name']}}";
bootbox.confirm({
size: "",
title: "{% raw translate('serverConfig', 'deleteFilesQuestion') %}",
closeButton: false,
message: "{% raw translate('serverConfig', 'deleteFilesQuestionMessage') %}",
buttons: {
confirm: {
label: "{{ translate('serverConfig', 'yesDeleteFiles') }}",
className: 'btn-danger',
},
cancel: {
label: "{{ translate('serverConfig', 'noDeleteFiles') }}",
className: 'btn-link',
}
},
callback: function(result) {
if (!result){
send_command(server_id, 'delete_server');
setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingDelete") %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDelete") %} </div>',
closeButton: false
})
return;}
else{
send_command(server_id, 'delete_server_files');
setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingDelete") %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDeleteFiles") %} </div>',
closeButton: false
})
}
}
});
}
function deleteConfirm (){
path = "{{data['server_stats']['server_id']['path']}}";
name = "{{data['server_stats']['server_id']['server_name']}}";
bootbox.confirm({
size: "",
title: "{% raw translate('serverConfig', 'deleteServerQuestion') %}",
closeButton: false,
message: "{% raw translate('serverConfig', 'deleteServerQuestionMessage') %}",
buttons: {
confirm: {
label: "{{ translate('serverConfig', 'yesDelete') }}",
className: 'btn-danger',
},
cancel: {
label: "{{ translate('serverConfig', 'noDelete') }}",
className: 'btn-link',
}
},
callback: function(result) {
if (!result){
return;
return;}
else{
deleteServer();
}
}
});
}
</script>

View File

@ -83,12 +83,19 @@
<button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand') }}</button>
</span>
</div>
<div class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0">
{% if data['server_stats']['updating']%}
<div id="update_control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'updating') }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart') %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop') }}</button>
</div>
{% else %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="send_command(server_id, 'start_server');" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start') }}</button>
<button onclick="send_command(server_id, 'restart_server');" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart') %}</button>
<button onclick="send_command(server_id, 'stop_server');" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop') }}</button>
</div>
{% end %}
</div>
@ -120,12 +127,27 @@
success: function(data){
console.log("got response:");
console.log(data);
setTimeout(function(){ location.reload(); }, 10000);
setTimeout(function(){
if (command != 'start_server'){
location.reload();
}
}, 10000);
}
});
}
if (webSocket) {
webSocket.on('update_button_status', function (updateButton) {
if (updateButton.isUpdating){
console.log(updateButton.isUpdating)
document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating") }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart") %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop") }}</button>';
}
else{
window.location.reload()
document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(server_id, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start") }}</button><button onclick="send_command(server_id, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart") %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop") }}</button>';
}
});
}
// Convert running to lower case (example: 'True' converts to 'true') and
// then to boolean via JSON.parse()
let online = JSON.parse('{{ data['server_stats']['running'] }}'.toLowerCase());

View File

@ -0,0 +1,215 @@
import peewee
import datetime
def migrate(migrator, database, **kwargs):
db = database
class Users(peewee.Model):
user_id = peewee.AutoField()
created = peewee.DateTimeField(default=datetime.datetime.now)
last_login = peewee.DateTimeField(default=datetime.datetime.now)
last_update = peewee.DateTimeField(default=datetime.datetime.now)
last_ip = peewee.CharField(default="")
username = peewee.CharField(default="", unique=True, index=True)
password = peewee.CharField(default="")
enabled = peewee.BooleanField(default=True)
superuser = peewee.BooleanField(default=False)
# we may need to revisit this
api_token = peewee.CharField(default="", unique=True, index=True)
class Meta:
table_name = "users"
database = db
class Roles(peewee.Model):
role_id = peewee.AutoField()
created = peewee.DateTimeField(default=datetime.datetime.now)
last_update = peewee.DateTimeField(default=datetime.datetime.now)
role_name = peewee.CharField(default="", unique=True, index=True)
class Meta:
table_name = "roles"
database = db
class User_Roles(peewee.Model):
user_id = peewee.ForeignKeyField(Users, backref='user_role')
role_id = peewee.ForeignKeyField(Roles, backref='user_role')
class Meta:
table_name = 'user_roles'
primary_key = peewee.CompositeKey('user_id', 'role_id')
database = db
class Audit_Log(peewee.Model):
audit_id = peewee.AutoField()
created = peewee.DateTimeField(default=datetime.datetime.now)
user_name = peewee.CharField(default="")
user_id = peewee.IntegerField(default=0, index=True)
source_ip = peewee.CharField(default='127.0.0.1')
# When auditing global events, use server ID 0
server_id = peewee.IntegerField(default=None, index=True)
log_msg = peewee.TextField(default='')
class Meta:
database = db
class Host_Stats(peewee.Model):
time = peewee.DateTimeField(default=datetime.datetime.now, index=True)
boot_time = peewee.CharField(default="")
cpu_usage = peewee.FloatField(default=0)
cpu_cores = peewee.IntegerField(default=0)
cpu_cur_freq = peewee.FloatField(default=0)
cpu_max_freq = peewee.FloatField(default=0)
mem_percent = peewee.FloatField(default=0)
mem_usage = peewee.CharField(default="")
mem_total = peewee.CharField(default="")
disk_json = peewee.TextField(default="")
class Meta:
table_name = "host_stats"
database = db
class Servers(peewee.Model):
server_id = peewee.AutoField()
created = peewee.DateTimeField(default=datetime.datetime.now)
server_uuid = peewee.CharField(default="", index=True)
server_name = peewee.CharField(default="Server", index=True)
path = peewee.CharField(default="")
backup_path = peewee.CharField(default="")
executable = peewee.CharField(default="")
log_path = peewee.CharField(default="")
execution_command = peewee.CharField(default="")
auto_start = peewee.BooleanField(default=0)
auto_start_delay = peewee.IntegerField(default=10)
crash_detection = peewee.BooleanField(default=0)
stop_command = peewee.CharField(default="stop")
executable_update_url = peewee.CharField(default="")
server_ip = peewee.CharField(default="127.0.0.1")
server_port = peewee.IntegerField(default=25565)
logs_delete_after = peewee.IntegerField(default=0)
class Meta:
table_name = "servers"
database = db
class User_Servers(peewee.Model):
user_id = peewee.ForeignKeyField(Users, backref='user_server')
server_id = peewee.ForeignKeyField(Servers, backref='user_server')
class Meta:
table_name = 'user_servers'
primary_key = peewee.CompositeKey('user_id', 'server_id')
database = db
class Role_Servers(peewee.Model):
role_id = peewee.ForeignKeyField(Roles, backref='role_server')
server_id = peewee.ForeignKeyField(Servers, backref='role_server')
class Meta:
table_name = 'role_servers'
primary_key = peewee.CompositeKey('role_id', 'server_id')
database = db
class Server_Stats(peewee.Model):
stats_id = peewee.AutoField()
created = peewee.DateTimeField(default=datetime.datetime.now)
server_id = peewee.ForeignKeyField(Servers, backref='server', index=True)
started = peewee.CharField(default="")
running = peewee.BooleanField(default=False)
cpu = peewee.FloatField(default=0)
mem = peewee.FloatField(default=0)
mem_percent = peewee.FloatField(default=0)
world_name = peewee.CharField(default="")
world_size = peewee.CharField(default="")
server_port = peewee.IntegerField(default=25565)
int_ping_results = peewee.CharField(default="")
online = peewee.IntegerField(default=0)
max = peewee.IntegerField(default=0)
players = peewee.CharField(default="")
desc = peewee.CharField(default="Unable to Connect")
version = peewee.CharField(default="")
updating = peewee.BooleanField(default=False)
class Meta:
table_name = "server_stats"
database = db
class Commands(peewee.Model):
command_id = peewee.AutoField()
created = peewee.DateTimeField(default=datetime.datetime.now)
server_id = peewee.ForeignKeyField(Servers, backref='server', index=True)
user = peewee.ForeignKeyField(Users, backref='user', index=True)
source_ip = peewee.CharField(default='127.0.0.1')
command = peewee.CharField(default='')
executed = peewee.BooleanField(default=False)
class Meta:
table_name = "commands"
database = db
class Webhooks(peewee.Model):
id = peewee.AutoField()
name = peewee.CharField(max_length=64, unique=True, index=True)
method = peewee.CharField(default="POST")
url = peewee.CharField(unique=True)
event = peewee.CharField(default="")
send_data = peewee.BooleanField(default=True)
class Meta:
table_name = "webhooks"
database = db
class Schedules(peewee.Model):
schedule_id = peewee.IntegerField(unique=True, primary_key=True)
server_id = peewee.ForeignKeyField(Servers, backref='schedule_server')
enabled = peewee.BooleanField()
action = peewee.CharField()
interval = peewee.IntegerField()
interval_type = peewee.CharField()
start_time = peewee.CharField(null=True)
command = peewee.CharField(null=True)
comment = peewee.CharField()
class Meta:
table_name = 'schedules'
database = db
class Backups(peewee.Model):
directories = peewee.CharField(null=True)
max_backups = peewee.IntegerField()
server_id = peewee.ForeignKeyField(Servers, backref='backups_server')
schedule_id = peewee.ForeignKeyField(Schedules, backref='backups_schedule')
class Meta:
table_name = 'backups'
database = db
migrator.create_table(Backups)
migrator.create_table(Users)
migrator.create_table(Roles)
migrator.create_table(User_Roles)
migrator.create_table(User_Servers)
migrator.create_table(Host_Stats)
migrator.create_table(Webhooks)
migrator.create_table(Servers)
migrator.create_table(Role_Servers)
migrator.create_table(Server_Stats)
migrator.create_table(Commands)
migrator.create_table(Audit_Log)
migrator.create_table(Schedules)
def rollback(migrator, database, **kwargs):
migrator.drop_table('users')
migrator.drop_table('roles')
migrator.drop_table('user_roles')
migrator.drop_table('audit_log') # ? Not 100% sure of the table name, please specify in the schema
migrator.drop_table('host_stats')
migrator.drop_table('servers')
migrator.drop_table('user_servers')
migrator.drop_table('role_servers')
migrator.drop_table('server_stats')
migrator.drop_table('commands')
migrator.drop_table('webhooks')
migrator.drop_table('schedules')
migrator.drop_table('backups')

View File

@ -125,8 +125,9 @@
"sendCommand": "Send command",
"start": "Start",
"restart": "Restart",
"stop": "Stop"
},
"stop": "Stop",
"updating": "Updating..."
},
"serverPlayerManagement": {
"players": "Players",
"bannedPlayers": "Banned Players",
@ -199,7 +200,23 @@
"save": "Save",
"cancel": "Cancel",
"deleteServer": "Delete Server",
"stopBeforeDeleting": "Please stop the server before deleting it"
"stopBeforeDeleting": "Please stop the server before deleting it",
"exeUpdateURLDesc": "Direct Download URL for updates.",
"exeUpdateURL": "Server Executable Update URL",
"update": "Update Executable",
"bePatientUpdate": "Please be patient while we update the server. Download times can vary depending upon your internet speeds.<br /> This screen will refresh in a moment",
"sendingRequest": "Sending your request...",
"deleteServerQuestion": "Delete Server?",
"deleteServerQuestionMessage": "Are you sure you want to delete this server? After this there is no going back...",
"yesDelete": "Yes, delete",
"noDelete": "No, go back",
"deleteFilesQuestion": "Delete server files from machine?",
"deleteFilesQuestionMessage": "Would you like Crafty to delete all server files from the host machine?",
"yesDeleteFiles": "Yes, delete files",
"noDeleteFiles": "No, just remove from panel",
"sendingDelete": "Deleting Server",
"bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.",
"bePatientDeleteFiles" : "Please be patient while we remove your server from the Crafty panel and delete all files. This screen will close in a few moments."
},
"serverConfigHelp": {
"title": "Server Config Area",

View File

@ -9,10 +9,11 @@ import signal
""" Our custom classes / pip packages """
from app.classes.shared.console import console
from app.classes.shared.helpers import helper
from app.classes.shared.models import installer
from app.classes.shared.models import installer, database
from app.classes.shared.tasks import TasksManager
from app.classes.shared.controller import Controller
from app.classes.shared.migration import MigrationManager
from app.classes.shared.cmd import MainPrompt
@ -91,16 +92,18 @@ if __name__ == '__main__':
# our session file, helps prevent multiple controller agents on the same machine.
helper.create_session_file(ignore=args.ignore)
migration_manager = MigrationManager(database)
migration_manager.up() # Automatically runs migrations
# do our installer stuff
fresh_install = installer.is_fresh_install()
if fresh_install:
console.debug("Fresh install detected")
installer.create_tables()
installer.default_settings()
else:
console.debug("Existing install detected")
installer.check_schema_version()
# now the tables are created, we can load the tasks_manger and server controller
controller = Controller()

View File

@ -23,4 +23,3 @@ termcolor==1.1.0
tornado==6.0.4
urllib3==1.25.10
webencodings==0.5.1