Add full scheme partitioning support

The initial "Partition & Format" (aka setup) form only allows to modify
one partition at a time. This commit updates it to allow to modify the
whole disk partition schema in one go, without pop-ups and transitions.

This is a remake of the previous form using FieldList de WTForms and
javascript to duplicate / remove FieldList adapted to the attributes
available in WTForms.
multi-ogserver
Javier Sánchez Parra 2021-07-28 17:14:51 +02:00 committed by OpenGnSys Support Team
parent 4b4edf4aee
commit 5dd2b5c6dc
4 changed files with 120 additions and 187 deletions

View File

@ -7,7 +7,7 @@
from wtforms import (
Form, SubmitField, HiddenField, SelectField, BooleanField, IntegerField,
StringField, RadioField
StringField, RadioField, FormField, FieldList
)
from wtforms.validators import InputRequired
from flask_wtf import FlaskForm
@ -21,9 +21,8 @@ class WOLForm(FlaskForm):
submit = SubmitField(label=_('Submit'))
class PartitionForm(FlaskForm):
ips = HiddenField()
disk = HiddenField()
partition = HiddenField()
partition = SelectField(label=_('Partition'),
choices=range(1,10))
part_type = SelectField(label=_('Type'),
choices=[('LINUX', 'Linux'),
('NTFS', 'NTFS'),
@ -31,28 +30,19 @@ class PartitionForm(FlaskForm):
fs = SelectField(label=_('Filesystem'),
choices=[('EXT4', 'EXT4'),
('NTFS', 'NTFS'),
('DISK', 'Disk'),
('EMPTY', 'Empty')])
size = IntegerField(label=_('Size (KB)'))
format_partition = BooleanField(label=_('Format'))
modify = SubmitField(label=_('Modify'))
delete = SubmitField(label=_('Delete'))
class NewPartitionForm(FlaskForm):
class SetupForm(FlaskForm):
ips = HiddenField()
part_type = SelectField(label=_('Type'),
choices=[('LINUX', 'Linux'),
('NTFS', 'NTFS'),
('EMPTY', 'Empty')])
fs = SelectField(label=_('Filesystem'),
choices=[('EXT4', 'EXT4'),
('NTFS', 'NTFS'),
('DISK', 'Disk'),
('EMPTY', 'Empty')])
size = IntegerField(label=_('Size (KB)'))
create = SubmitField(label=_('Create'))
disk = HiddenField()
disk_type = SelectField(label=_('Type'),
choices=[('MSDOS', 'MSDOS'),
('GPT', 'GPT')])
partitions = FieldList(FormField(PartitionForm),
min_entries=1,
max_entries=10)
class HardwareForm(FlaskForm):
ips = HiddenField()

View File

@ -53,3 +53,25 @@ function updateScopes(scopes) {
function unfoldAll() {
$('#scopes .collapse').collapse('show');
}
function AddPartition(evt) {
const target = $($(evt).data("target"));
const oldrow = target.find("[data-toggle=fieldset-entry]:last");
const row = oldrow.clone(true, true);
const elem_id = row.find(":input")[0].id;
const elem_prefix = elem_id.replace(/(.*)-(\d{1,4})/m, '$1')// max 4 digits for ids in list
const elem_num = parseInt(elem_id.replace(/(.*)-(\d{1,4})/m, '$2')) + 1;
// Increment WTForms unique identifiers
row.children(':input').each(function() {
const id = $(this).attr('id').replace(elem_prefix+'-' + (elem_num - 1),
elem_prefix+'-' + (elem_num));
$(this).attr('name', id).attr('id', id).val('').removeAttr("checked");
});
row.show();
oldrow.after(row);
}
function RemovePartition(evt) {
const target = $(evt).parent().parent();
target.remove();
}

View File

@ -4,79 +4,61 @@
<h1 class="m-5">{{_('Partition and Format')}}</h1>
<table class="table">
<thead class="text-center">
<tr>
<th>Type</th>
<th>Filesytem</th>
<th>Size (KB)</th>
<th>Format?</th>
<th colspan="2"></th>
</tr>
</thead>
<form class="form-inline" method="POST" id="setupForm">
<table class="table">
<thead class="text-center">
<tr>
<th>Partition Table</th>
<th>Total Disk Size (KB)</th>
</tr>
</thead>
<tbody>
{% for form in forms %}
<form class="form-inline" method="POST">
<tr>
{{ form.hidden_tag() }}
<td>{{ form.part_type(class_="form-control") }}</td>
<td>{{ form.fs(class_="form-control") }}</td>
<td>{{ form.size(class_="form-control") }}</td>
<td>{{ form.format_partition(class_="form-control") }}</td>
<td>{{ form.modify(class_="form-control btn-primary") }}</td>
<td>{{ form.delete(class_="form-control btn-danger") }}</td>
</tr>
</form>
{% endfor %}
</tbody>
</table>
<tbody data-target="partitons-fieldset" id="setupTable" class="text-center">
<tr>
{{ form.hidden_tag() }}
<td>{{ form.disk_type(class_="form-control") }}</td>
<td>{{ disk_size }}</td>
</tr>
</tbody>
</table>
<table class="table">
<thead class="text-center">
<tr>
<th>Partition</th>
<th>Type</th>
<th>Filesytem</th>
<th>Size (KB)</th>
<th>Format?</th>
<th colspan="2"></th>
</tr>
</thead>
<button class="btn btn-primary" data-toggle="modal" data-target="#newPartitionModal">
{{ _('Add a new Partition') }}
<tbody data-target="partitons-fieldset" id="partitionsTable" class="text-center">
{% for partition in form.partitions %}
<tr data-toggle="fieldset-entry">
{{ partition.hidden_tag() }}
<td>{{ partition.partition(class_="form-control") }}</td>
<td>{{ partition.part_type(class_="form-control") }}</td>
<td>{{ partition.fs(class_="form-control") }}</td>
<td>{{ partition.size(class_="form-control") }}</td>
<td>{{ partition.format_partition(class_="form-control") }}</td>
<td>
<button class="btn btn-danger" onclick="RemovePartition(this)">
{{ _('Remove') }}
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
<button class="btn btn-primary" data-target="#partitionsTable" onclick="AddPartition(this)">
{{ _('Add a new partition') }}
</button>
<!-- Modal -->
<div class="modal fade" id="newPartitionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form class="form" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ _('Create a new partition') }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ new_partition_form.hidden_tag() }}
<div class="form-group">
<label for="{{ new_partition_form.part_type.id }}">{{ new_partition_form.part_type.label }}</label>
{{ new_partition_form.part_type(class_="form-control") }}
</div>
<div class="form-group">
<label for="{{ new_partition_form.fs.id }}">{{ new_partition_form.fs.label }}</label>
{{ new_partition_form.fs(class_="form-control") }}
</div>
<div class="form-group">
<label for="{{ new_partition_form.size.id }}">{{ new_partition_form.size.label }}</label>
{% if new_partition_form.size.errors %}
{{ new_partition_form.size(class_="form-control is-invalid") }}
{% else %}
{{ new_partition_form.size(class_="form-control") }}
{% endif %}
{% for error in new_partition_form.size.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
{{ new_partition_form.create(class_="btn btn-primary") }}
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</form>
</div>
</div>
<button class="btn btn-success" form="setupForm">
{{ _('Accept') }}
</button>
{% endblock %}

View File

@ -9,7 +9,7 @@ from flask import (
g, render_template, url_for, flash, redirect, request, jsonify, make_response
)
from ogcp.forms.action_forms import (
WOLForm, PartitionForm, NewPartitionForm, ClientDetailsForm, HardwareForm,
WOLForm, SetupForm, ClientDetailsForm, HardwareForm,
SessionForm, ImageRestoreForm, ImageCreateForm, SoftwareForm, BootModeForm,
RoomForm, DeleteRoomForm, CenterForm
)
@ -46,6 +46,7 @@ PART_TYPE_CODES = {
}
PART_SCHEME_CODES = {
0: 'EMPTY',
1: 'MSDOS',
2: 'GPT'
}
@ -232,85 +233,65 @@ def action_wol():
@app.route('/action/setup', methods=['GET'])
@login_required
def action_setup_show(ips=None, new_partition_form=None):
def action_setup_show(ips=None):
if not ips:
ips = parse_ips(request.args.to_dict())
db_partitions = get_client_setup(ips)
forms = [PartitionForm() for _ in db_partitions]
forms = list(forms)
form = SetupForm()
if not new_partition_form:
new_partition_form = NewPartitionForm()
new_partition_form.ips.data = " ".join(ips)
new_partition_form.create.render_kw = {"formaction": url_for('action_setup_create')}
form.ips.data = " ".join(ips)
form.disk.data = db_partitions[0]['disk']
# If partition table is empty, set MSDOS
form.disk_type.data = db_partitions[0]['code'] or 1
for form, db_part in zip(forms, db_partitions):
form.ips.data = " ".join(ips)
form.disk.data = db_part['disk']
form.partition.data = db_part['partition']
# XXX: Should separate whole disk form from partition setup form
if db_part['filesystem'] == "DISK":
form.part_type.choices = list(PART_SCHEME_CODES.values())
form.fs.render_kw = {'disabled': ''}
disk_size = db_partitions[0]['size']
form.part_type.data = db_part['code']
form.fs.data = db_part['filesystem']
form.size.data = db_part['size']
form.modify.render_kw = {"formaction": url_for('action_setup_modify')}
form.delete.render_kw = {"formaction": url_for('action_setup_delete')}
# Make form.partition length equal to (db_partitions - 1) length
diff = len(db_partitions) - 1 - len(form.partitions)
[form.partitions.append_entry() for _ in range(diff)]
for partition, db_part in zip(form.partitions, db_partitions[1:]):
partition.partition.data = str(db_part['partition'])
partition.part_type.data = db_part['code']
partition.fs.data = db_part['filesystem']
partition.size.data = db_part['size']
scopes, _clients = get_scopes(ips)
return render_template('actions/setup.html',
forms=forms,
new_partition_form=new_partition_form,
form=form,
disk_size=disk_size,
scopes=scopes)
@app.route('/action/setup/create', methods=['POST'])
@login_required
def action_setup_create():
form = NewPartitionForm(request.form)
ips = form.ips.data.split(' ')
if form.validate():
# TODO: create the real partition
flash(_('Partition created successfully'), category='info')
return redirect(url_for('action_setup_show', ips=ips))
flash(_('Invalid partition configuration'), category='error')
# This return will maintain the new partition form state, but will break
# the F5 behavior in the browser
return action_setup_show(ips, form)
@app.route('/action/setup/modify', methods=['POST'])
@app.route('/action/setup', methods=['POST'])
@login_required
def action_setup_modify():
form = PartitionForm(request.form)
form = SetupForm(request.form)
if form.validate():
ips = form.ips.data.split(' ')
db_partitions = get_client_setup(ips)
payload = {'clients': ips,
'disk': str(form.disk.data),
'type': str(form.disk_type.data),
'cache': str(0),
'cache_size': str(0),
'partition_setup': []}
for db_part in db_partitions:
if db_part['partition'] == 0:
# Set partition scheme
payload['type'] = db_part['code']
continue
partition_setup = {'partition': str(db_part['partition']),
'code': db_part['code'],
'filesystem': db_part['filesystem'],
'size': str(db_part['size']),
'format': str(int(False))}
required_partitions = ["1", "2", "3", "4"]
for partition in form.partitions:
print(partition)
partition_setup = {'partition': str(partition.partition.data),
'code': str(partition.part_type.data),
'filesystem': str(partition.fs.data),
'size': str(partition.size.data),
'format': str(int(partition.format_partition.data))}
payload['partition_setup'].append(partition_setup)
if partition.partition.data in required_partitions:
required_partitions.remove(partition.partition.data)
# Ensure a 4 partition setup
for i in range(len(db_partitions), 5):
for partition in required_partitions:
empty_part = {
'partition': str(i),
'partition': partition,
'code': 'EMPTY',
'filesystem': 'EMPTY',
'size': '0',
@ -318,48 +299,6 @@ def action_setup_modify():
}
payload['partition_setup'].append(empty_part)
modified_part = payload['partition_setup'][int(form.partition.data) - 1]
modified_part['filesystem'] = str(form.fs.data)
modified_part['code'] = str(form.part_type.data)
modified_part['size'] = str(form.size.data)
modified_part['format'] = str(int(form.format_partition.data))
r = g.server.post('/setup', payload=payload)
if r.status_code == requests.codes.ok:
return redirect(url_for("scopes"))
return make_response("400 Bad Request", 400)
@app.route('/action/setup/delete', methods=['POST'])
@login_required
def action_setup_delete():
form = PartitionForm(request.form)
if form.validate():
ips = form.ips.data.split(' ')
db_partitions = get_client_setup(ips)
payload = {'clients': ips,
'disk': str(form.disk.data),
'cache': str(0),
'cache_size': str(0),
'partition_setup': []}
for db_part in db_partitions:
if db_part['partition'] == 0:
# Skip if this is disk setup.
continue
partition_setup = {'partition': str(db_part['partition']),
'code': db_part['code'],
'filesystem': db_part['filesystem'],
'size': str(db_part['size']),
'format': str(int(False))}
payload['partition_setup'].append(partition_setup)
modified_part = payload['partition_setup'][int(form.partition.data) - 1]
modified_part['filesystem'] = FS_CODES[1]
modified_part['code'] = PART_TYPE_CODES[0]
modified_part['size'] = str(0)
modified_part['format'] = str(int(True))
r = g.server.post('/setup', payload=payload)
if r.status_code == requests.codes.ok:
return redirect(url_for("scopes"))