Merge pull request #7 from technikamateur/beta

merging v1.0.1
This commit is contained in:
Daniel 2019-10-18 11:17:20 +02:00 committed by GitHub
commit f4cc0bc354
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 295 additions and 180 deletions

View file

@ -1,4 +1,6 @@
from flask_wtf import csrf, CSRFProtect
import datetime
from flask_wtf import CSRFProtect
import dirkules.config as config
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
@ -12,6 +14,7 @@ csrf.init_app(app)
app_version = app.config["VERSION"]
import dirkules.models
import dirkules.samba.models
# create db if not exists
db.create_all()
@ -21,3 +24,15 @@ scheduler.init_app(app)
scheduler.start()
# import views
import dirkules.views
from dirkules.samba import bp_samba as bp_samba
app.register_blueprint(bp_samba, url_prefix='/samba')
from dirkules.models import Drive
@app.before_request
def check_drives():
if Drive.query.first() is None:
scheduler.get_job("refresh_disks").modify(next_run_time=datetime.datetime.now())

View file

@ -5,7 +5,7 @@ from logging.config import dictConfig
from apscheduler.jobstores.base import ConflictingIdError
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
VERSION = "1.0"
VERSION = "1.0.1"
baseDir = os.path.abspath(os.path.dirname(__file__))
staticDir = os.path.join(baseDir, 'static')

View file

@ -66,10 +66,10 @@ def part_for_disk(device):
# lsblk /dev/sdd -b -o NAME,LABEL,FSTYPE,SIZE,UUID,MOUNTPOINT
parts = []
part_dict = list()
keys = ['name', 'label', 'fs', 'size', 'uuid', 'mount']
keys = ['size', 'name', 'label', 'fs', 'uuid', 'mount']
device = "/dev/" + device
lsblk = subprocess.Popen(
["sudo lsblk " + device + " -l -b -o NAME,LABEL,FSTYPE,SIZE,UUID,MOUNTPOINT"],
["sudo lsblk " + device + " -l -b -o SIZE,NAME,LABEL,FSTYPE,UUID,MOUNTPOINT"],
stdout=subprocess.PIPE,
shell=True,
universal_newlines=True)
@ -83,26 +83,24 @@ def part_for_disk(device):
del parts[1]
element_length = list()
counter = 0
last_letter = 0
pre_value = " "
for char in parts[0]:
if char != " " and pre_value == " ":
element_length.append(counter)
if len(element_length) == 0:
element_length.append(0)
else:
element_length.append(counter)
counter += 1
pre_value = char
# size ist rechtsbuendig. Extra Behandlung
# TODO: Besser machen
if char == "S" and parts[0][last_letter] == "E":
del element_length[-1]
element_length.append((last_letter + 2))
if char != " ":
last_letter = counter - 1
element_length.append(len(parts[0]))
del parts[0]
for part in parts:
values = list()
for start, end in zip(element_length, element_length[1:]):
values.append(part[start:(end - 1)].strip())
for start, next_start in zip(element_length, element_length[1:]):
if next_start == element_length[-1:][0]:
values.append(part[start:len(part)].strip())
else:
values.append(part[start:(next_start - 1)].strip())
part_dict.append(dict(zip(keys, values)))
return part_dict

View file

@ -2,13 +2,10 @@
import subprocess
# This file should read btrfs pools
# Storage: sudo btrfs fi usage -b -T /media/data-raid
def get_free_space(name):
def get_free_space(mount_point):
lines = list()
df = subprocess.Popen(
["df -B K /dev/" + name],
["df -B K " + mount_point],
stdout=subprocess.PIPE,
shell=True,
universal_newlines=True)
@ -20,7 +17,7 @@ def get_free_space(name):
break
df.stdout.close()
for line in lines:
newLine = ' '.join(line.split())
newLine = newLine.split(" ")
if name in newLine[0]:
return int(newLine[3][:-1]) * 1024
new_line = ' '.join(line.split())
new_line = new_line.split(" ")
if mount_point in new_line:
return int(new_line[3][:-1]) * 1024

View file

@ -80,6 +80,7 @@ def get_drives():
def pool_gen():
part_dict = dict()
Pool.query.delete()
# creates map uuid is key, partitions are values
for part in Partitions.query.all():
if part.uuid in part_dict:
@ -97,7 +98,9 @@ def pool_gen():
drives = drives + str(Drive.query.get(part.drive_id)) + ","
drives = drives[:-1]
value = value[0]
Pool.query.delete()
missing = absent_drive(drives)
if missing is not None:
missing = ",".join(str(x.name) for x in missing)
if value.fs == "btrfs":
if value.mountpoint:
memory_map = btrfsTools.get_space(value.mountpoint)
@ -108,16 +111,16 @@ def pool_gen():
['unbekannt', '1.00', 'unbekannt', '1.00'])))
pool_obj = Pool(value.label, memory_map.get("total"), memory_map.get("free"), raid_map.get("data_raid"),
raid_map.get("data_ratio"), raid_map.get("meta_raid"), raid_map.get("meta_ratio"), value.fs,
value.mountpoint, "not implemented", drives, get_pool_health(drives))
value.mountpoint, "not implemented", drives, get_pool_health(drives), missing)
db.session.add(pool_obj)
elif value.fs == "ext4":
if value.mountpoint:
free_space = ext4Tools.get_free_space(value.name)
free_space = ext4Tools.get_free_space(value.mountpoint)
else:
free_space = 2
pool_obj = Pool(value.label, value.size, free_space, raid, 1.00, raid, 1.00, value.fs, value.mountpoint,
"not implemented", drives)
"not implemented", drives, get_pool_health(drives), missing)
db.session.add(pool_obj)
db.session.commit()
@ -137,6 +140,23 @@ def get_pool_health(drive_list):
return True
def absent_drive(drive_list):
"""
:param drive_list: contains drives which belongs to pool
:return: List of absent drives or None
"""
missing = list()
drive_split = drive_list.split(",")
for drive in drive_split:
db_drive = db.session.query(Drive).filter(Drive.name == drive).scalar()
if db_drive.missing:
missing.append(db_drive)
if not missing:
return None
else:
return missing
def delete_drive(drive):
"""
removes a given drive object (including cascades) from db

View file

@ -78,9 +78,10 @@ class Pool(db.Model):
mountopt = db.Column(db.String)
drives = db.Column(db.String)
healthy = db.Column(db.Boolean)
missing = db.Column(db.String)
def __init__(self, label, size, free, data_raid, data_ratio, meta_raid, meta_ratio, fs, mountpoint, mountopt,
drives, healthy):
drives, healthy, missing):
self.label = label
self.size = size
self.free = free
@ -93,6 +94,7 @@ class Pool(db.Model):
self.mountopt = mountopt
self.drives = drives
self.healthy = healthy
self.missing = missing
class Time(db.Model):
@ -118,24 +120,3 @@ class Cleaning(db.Model):
self.path = path
self.state = state
class SambaShare(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
writeable = db.Column(db.Boolean)
recycling = db.Column(db.Boolean)
btrfs = db.Column(db.Boolean)
options = db.relationship('SambaOptions', order_by="SambaOptions.id", backref="samba_share", lazy="select")
def __init__(self, name, writeable=False, recycling=False, btrfs=False):
self.name = name
self.writeable = writeable
self.recycling = recycling
self.btrfs = btrfs
class SambaOptions(db.Model):
id = db.Column(db.Integer, primary_key=True)
option = db.Column(db.String, nullable=False)
value = db.Column(db.String, nullable=False)
sambashare_id = db.Column(db.Integer, db.ForeignKey('samba_share.id'), nullable=False)

View file

@ -0,0 +1,4 @@
from flask import Blueprint
bp_samba = Blueprint('samba', __name__, template_folder='templates')
from dirkules.samba import views

32
dirkules/samba/manager.py Normal file
View file

@ -0,0 +1,32 @@
from dirkules.config import staticDir
from dirkules import db, app_version
from dirkules.samba.models import SambaGlobal
def set_samba_global(workgroup, name):
SambaGlobal.query.delete()
workgroup = SambaGlobal("workgroup", workgroup)
name = SambaGlobal("server string", "%h {}".format(name))
db.session.add(workgroup)
db.session.add(name)
db.session.commit()
def generate_smb():
if SambaGlobal.query.first() is None:
workgroup = 'WORKGROUP'
server_string = '%h dirkules'
else:
workgroup = SambaGlobal.query.get(1)
server_string = SambaGlobal.query.get(2)
f = open("smb.conf.txt", "w")
samba_global = open(staticDir + "/conf/samba_global.conf", "r")
f.write("# This file was generated by dirkules v{}".format(app_version))
f.write()
f.write("# Global Config")
f.write("server string = {}".format(server_string))
f.write("workgroup = {}".format(workgroup))
f.write(samba_global.read())
f.close()

34
dirkules/samba/models.py Normal file
View file

@ -0,0 +1,34 @@
from dirkules import db
class SambaGlobal(db.Model):
id = db.Column(db.Integer, primary_key=True)
element = db.Column(db.String)
value = db.Column(db.String)
def __init__(self, element, value):
self.element = element
self.value = value
class SambaShare(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
writeable = db.Column(db.Boolean)
recycling = db.Column(db.Boolean)
btrfs = db.Column(db.Boolean)
options = db.relationship('SambaOptions', order_by="SambaOptions.id", backref="samba_share", lazy="select",
cascade="all, delete-orphan")
def __init__(self, name, writeable=False, recycling=False, btrfs=False):
self.name = name
self.writeable = writeable
self.recycling = recycling
self.btrfs = btrfs
class SambaOptions(db.Model):
id = db.Column(db.Integer, primary_key=True)
option = db.Column(db.String, nullable=False)
value = db.Column(db.String, nullable=False)
sambashare_id = db.Column(db.Integer, db.ForeignKey('samba_share.id'), nullable=False)

View file

@ -5,18 +5,19 @@
{% endblock %}
{% block body %}
<div class="ui container">
<t2>Feste Bestandteile der<i> Samba global conf</i></t2>
<t2>Feste Bestandteile der<i> Samba global conf</i></t2>
<div class="ui segment">
{% for line in conf %}
{{ line }}<br>
{% endfor %}
</div>
<t2>Veränderbare Bestandteile der<i> Samba global conf</i></t2>
{% from "_formhelpers.html" import render_field %}
<form method=post class="ui form error">
<t2>Veränderbare Bestandteile der<i> Samba global conf</i></t2>
{% from "_formhelpers.html" import render_field %}
<form method=post class="ui form error">
{{ form.hidden_tag() }}
{{ render_field(form.workgroup) }}
{{ render_field(form.server_string) }}
<button class="ui button" type="submit"><i class="save icon"></i> Ändern</button>
</form>
{{ render_field(form.submit) }}
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Samba{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block body %}
<div class="topspacer"></div>
<div class="ui container">
<a href="{{ url_for('.config') }}">
<div class="ui primary labeled icon button">
<i class="edit icon"></i> Konfigurieren
</div>
</a>
<a href="{{ url_for('.add') }}">
<div class="ui primary labeled icon button">
<i class="plus icon"></i> Freigabe hinzufügen
</div>
</a>
<a href="{{ url_for('.generate') }}">
<div class="ui primary labeled icon button">
<i class="magic icon"></i> Generieren
</div>
</a>
<table class="ui celled fixed table">
<thead>
<tr>
<th>Name</th>
<th>Schreibzugriff</th>
<th>Papierkorb</th>
<th>BtrFS Optimierungen</th>
</tr>
</thead>
<tbody>
{% for share in shares %}
<tr>
<td>{{ share.name }}</td>
<td>{{ share.writeable }}</td>
<td>{{ share.recycling }}</td>
<td>{{ share.btrfs }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -0,0 +1,38 @@
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SelectField, IntegerField, RadioField, validators, SubmitField
class SambaConfigForm(FlaskForm):
workgroup = StringField("workgroup", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Regexp('^[a-z]+$', message="Bitte nur Kleinbuchstaben eingeben."),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Nichts..."})
server_string = StringField("server string", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Regexp('^[a-z]+$',
message="Bitte nur Kleinbuchstaben eingeben."),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Nichts..."})
submit = SubmitField("Speichern")
class SambaAddForm(FlaskForm):
name = StringField("Name der Freigabe", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Bilder"})
writeable = BooleanField("Schreibzugriff")
recycling = BooleanField("Papierkorb")
btrfs = BooleanField("BtrFS Optimierungen (Vorsicht!)")
# additional
path = SelectField("Pfad", choices=[("Value1", "Label1"), ("Value2", "Label2")])
user = StringField("Berechtigte Nutzer", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "sambadaniel"})
create_mask = IntegerField("Dateimaske", [validators.Optional(),
validators.NumberRange(min=4, max=4,
message="Bitte 4 Zahlen eingeben!")],
render_kw={"placeholder": "0600"})
dir_mask = IntegerField("Ordnermaske", [validators.Optional(),
validators.NumberRange(min=4, max=4,
message="Bitte 4 Zahlen eingeben!")],
render_kw={"placeholder": "0700"})
submit = SubmitField("Speichern")

44
dirkules/samba/views.py Normal file
View file

@ -0,0 +1,44 @@
from dirkules import db
from dirkules.config import staticDir
from flask import render_template, url_for, request, redirect
from dirkules.samba import bp_samba
from dirkules.samba.manager import set_samba_global
from dirkules.samba.models import SambaShare
from dirkules.samba.validation import SambaConfigForm, SambaAddForm
@bp_samba.route('/', methods=['GET'])
def index():
shares = SambaShare.query.order_by(db.asc(db.collate(SambaShare.name, 'NOCASE'))).all()
return render_template('samba/index.html', shares=shares)
@bp_samba.route('/config', methods=['GET', 'POST'])
def config():
form = SambaConfigForm(request.form)
if request.method == 'POST' and form.validate():
set_samba_global(form.workgroup.data, form.server_string.data)
return redirect(url_for('.index'))
file = open(staticDir + "/conf/samba_global.conf", "r")
conf = list()
while True:
line = file.readline()
if line != '':
conf.append(line.rstrip())
else:
break
return render_template('samba/config.html', form=form, conf=conf)
@bp_samba.route('/add', methods=['GET', 'POST'])
def add():
form = SambaAddForm(request.form)
if request.method == 'POST' and form.validate():
return redirect(url_for('.index'))
return render_template('samba/add.html', form=form)
@bp_samba.route('/generate')
def generate():
return redirect(url_for('.index'))

View file

@ -23,7 +23,7 @@
<a class="item" href="{{ url_for('drives') }}"><i class="hdd icon"></i> Festplatten</a>
<a class="item" href="{{ url_for('pools') }}"><i class="server icon"></i> Pools</a>
<a class="item" href="{{ url_for('cleaning') }}"><i class="trash icon"></i> Cleaning</a>
<a class="item" href="{{ url_for('samba') }}"><i class="share alternate icon"></i> Samba</a>
<a class="item" href="{{ url_for('samba.index') }}"><i class="share alternate icon"></i> Samba</a>
<div class="right menu">
<a class="ui item" href="{{ url_for('about') }}">über Dirkules</a>
</div>

View file

@ -81,7 +81,7 @@
</tr>
<tr>
<td>Gesundheit</td>
{% if pool.healthy %}
{% if pool.healthy and pool.missing is none %}
<td class="positive"><i class="icon checkmark"></i>Gesund</td>
{% else %}
<td class="negative"><i class="attention icon"></i>Kritisch</td>
@ -99,13 +99,14 @@
</div>
<p>
Die RAID Level von Data und Metadata unterscheiden sich. Normalerweise sollten beide gleich
konfiguriert werden, da es sonst unter Umständen zu <b>Datenverlust</b> kommen kann, wenn ein
konfiguriert werden, da es sonst unter Umständen zu <b>Datenverlust</b> kommen kann, wenn
ein
Laufwerk ausfällt.<br>
Solltest du einen guten Grund dafür haben, ignoriere einfach diese Meldung.
</p>
</div>
{% endif %}
{% if pool.healthy %}
{% if pool.healthy and pool.missing is none %}
<div class="ui positive message">
<i class="close icon"></i>
<div class="header">
@ -116,16 +117,28 @@
</p>
</div>
{% else %}
<div class="ui negative message">
<i class="close icon"></i>
<div class="header">
Kritischer Zustand
{% if not pool.healthy %}
<div class="ui negative message">
<i class="close icon"></i>
<div class="header">
Kritischer Zustand
</div>
<p>
Mindetstens ein Laufwerk befindet sich in einem schlechten Zustand! Du solltest
dieses schnellstmöglich tauschen.
</p>
</div>
<p>
Mindetstens ein Laufwerk befindet sich in einem schlechten Zustand! Du solltest
dieses schnellstmöglich tauschen.
</p>
</div>
{% else %}
<div class="ui negative message">
<i class="close icon"></i>
<div class="header">
Kritischer Zustand
</div>
<p>
Folgende Laufwerke wurden nicht gefunden: {{ pool.missing }}
</p>
</div>
{% endif %}
{% endif %}
</div>
</div>

View file

@ -1,39 +0,0 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block body %}
<a href="{{ url_for('samba_global') }}">
<div class="ui primary labeled icon button">
<i class="plus icon"></i> Samba Global conf
</div>
</a>
<a href="{{ url_for('samba_add') }}">
<div class="ui primary labeled icon button">
<i class="plus icon"></i> Samba add
</div>
</a>
<table class="ui celled fixed table">
<thead>
<tr>
<th>Name</th>
<th>Schreibzugriff</th>
<th>Papierkorb</th>
<th>BtrFS Optimierungen</th>
</tr>
</thead>
<tbody>
{% for share in shares %}
<tr>
<td>{{ share.name }}</td>
<td>{{ share.writeable }}</td>
<td>{{ share.recycling }}</td>
<td>{{ share.btrfs }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -17,42 +17,6 @@ class CleaningForm(FlaskForm):
submit = SubmitField("Job speichern")
class SambaCleaningForm(FlaskForm):
workgroup = StringField("workgroup", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Regexp('^[a-z]+$', message="Bitte nur Kleinbuchstaben eingeben."),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Nichts..."})
server_string = StringField("server string", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Regexp('^[a-z]+$',
message="Bitte nur Kleinbuchstaben eingeben."),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Nichts..."})
class SambaAddForm(FlaskForm):
name = StringField("Name der Freigabe", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Bilder"})
writeable = BooleanField("Schreibzugriff")
recycling = BooleanField("Papierkorb")
btrfs = BooleanField("BtrFS Optimierungen (Vorsicht!)")
# additional
path = SelectField("Pfad", choices=[("Value1", "Label1"), ("Value2", "Label2")])
user = StringField("Berechtigte Nutzer", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "sambadaniel"})
create_mask = IntegerField("Dateimaske", [validators.Optional(),
validators.NumberRange(min=4, max=4,
message="Bitte 4 Zahlen eingeben!")],
render_kw={"placeholder": "0600"})
dir_mask = IntegerField("Ordnermaske", [validators.Optional(),
validators.NumberRange(min=4, max=4,
message="Bitte 4 Zahlen eingeben!")],
render_kw={"placeholder": "0700"})
class SemanticMultiSelectField(SelectField):
def pre_validate(self, form):
if self.choices is not None:

View file

@ -6,10 +6,9 @@ from dirkules import app, db, scheduler, app_version
import dirkules.manager.serviceManager as servMan
import dirkules.manager.driveManager as driveMan
import dirkules.manager.cleaning as cleaningMan
from dirkules.models import Drive, Cleaning, SambaShare, Pool
from dirkules.models import Drive, Cleaning, Pool
import dirkules.manager.viewManager as viewManager
from dirkules.validation.validators import CleaningForm, SambaCleaningForm, SambaAddForm, PoolAddForm
from dirkules.config import staticDir
from dirkules.validation.validators import CleaningForm, PoolAddForm
@app.errorhandler(404)
@ -146,36 +145,3 @@ def add_cleaning():
viewManager.create_cleaning_obj(form.jobname.data, form.path.data, form.active.data)
return redirect(url_for('cleaning'))
return render_template('add_cleaning.html', form=form)
@app.route('/samba', methods=['GET'])
def samba():
shares = SambaShare.query.order_by(db.asc(db.collate(SambaShare.name, 'NOCASE'))).all()
return render_template('samba.html', shares=shares)
@app.route('/samba/global', methods=['GET', 'POST'])
def samba_global():
form = SambaCleaningForm(request.form)
if request.method == 'POST' and form.validate():
print("Input:")
print(form.workgroup.data)
print(form.server_string.data)
return redirect(url_for('samba_global'))
file = open(staticDir + "/conf/samba_global.conf")
conf = list()
while True:
line = file.readline()
if line != '':
conf.append(line.rstrip())
else:
break
return render_template('samba_global.html', form=form, conf=conf)
@app.route('/samba/add', methods=['GET', 'POST'])
def samba_add():
form = SambaAddForm(request.form)
if request.method == 'POST' and form.validate():
return redirect(url_for('samba'))
return render_template('samba_add.html', form=form)