feat: implement Flask web UI for VM backup management and scheduling

This commit is contained in:
Rizqi 2026-06-21 12:21:09 +07:00
parent 7f306c713c
commit 7b4688b792
4 changed files with 151 additions and 14 deletions

View File

@ -261,10 +261,11 @@ def create_and_start_job(
vm_name, dest, compress, no_verify_ssl,
sftp_host, sftp_user, sftp_password,
schedule_type, schedule_time, weekly_day, interval_hours,
label='', disk_filter=None
label='', disk_filter=None, monthly_day=1
):
"""Create a job entry and either run immediately or register schedule.
disk_filter: list of VMDK path strings to include, or None for all.
monthly_day: day of month (1-28) for monthly schedule.
"""
jid = datetime.now().strftime('%Y%m%d%H%M%S') + '-' + uuid.uuid4().hex[:6]
job_dir = JOBS_DIR / jid
@ -307,6 +308,12 @@ def create_and_start_job(
day_of_week=int(weekly_day),
hour=int(hour), minute=int(minute)
)
elif schedule_type == 'monthly':
hour, minute = (schedule_time.split(':') + ['00'])[:2]
trigger = CronTrigger(
day=max(1, min(28, int(monthly_day or 1))),
hour=int(hour), minute=int(minute)
)
elif schedule_type == 'interval':
trigger = IntervalTrigger(hours=max(1, int(interval_hours or 24)))
@ -441,6 +448,8 @@ def create_job():
daily_time = request.form.get('daily_time', '02:00')
weekly_day = request.form.get('weekly_day', '0')
weekly_time = request.form.get('weekly_time', '02:00')
monthly_day = request.form.get('monthly_day', '1')
monthly_time = request.form.get('monthly_time', '02:00')
interval_hrs = request.form.get('interval_hours', '24')
label = request.form.get('job_label', '').strip()
@ -453,6 +462,8 @@ def create_job():
sched_time = daily_time
elif schedule_type == 'weekly':
sched_time = weekly_time
elif schedule_type == 'monthly':
sched_time = monthly_time
else:
sched_time = ''
@ -478,11 +489,13 @@ def create_job():
interval_hours=interval_hrs,
label=label,
disk_filter=disk_filter,
monthly_day=monthly_day,
)
n_disks = len(disk_filter) if disk_filter is not None else 'all'
flash(f'Job created — {n_disks} disk(s) selected.', 'success')
return redirect(url_for('job_detail', jobid=jid))
# GET: load VM list for the dropdown
selected_vm = request.args.get('vm', '')
show_schedule = bool(request.args.get('schedule', ''))
@ -522,8 +535,21 @@ def batch_jobs():
disk_strategy = request.form.get('disk_strategy', 'all')
schedule_type = request.form.get('schedule_type', 'now')
daily_time = request.form.get('daily_time', '02:00')
weekly_day = request.form.get('weekly_day', '0')
weekly_time = request.form.get('weekly_time', '02:00')
monthly_day = request.form.get('monthly_day', '1')
monthly_time = request.form.get('monthly_time', '02:00')
interval_hrs = request.form.get('interval_hours', '24')
label_prefix = request.form.get('job_label', '').strip()
sched_time = daily_time if schedule_type == 'daily' else ''
if schedule_type == 'daily':
sched_time = daily_time
elif schedule_type == 'weekly':
sched_time = weekly_time
elif schedule_type == 'monthly':
sched_time = monthly_time
else:
sched_time = ''
if not vm_names:
flash('No VMs selected.', 'danger')
@ -533,14 +559,13 @@ def batch_jobs():
for vm_name in vm_names:
# Resolve disk_filter from strategy
if disk_strategy == 'os':
# Smallest disk = OS disk
vm_info = vms_by_name.get(vm_name, {})
disks = sorted(vm_info.get('disks', []), key=lambda d: d.get('size_gb', 0))
disk_filter = [disks[0]['path']] if disks else None
elif disk_strategy == 'vmx':
disk_filter = [] # empty list = VMX only
disk_filter = []
else:
disk_filter = None # all disks
disk_filter = None
label = f'{label_prefix}{vm_name}' if label_prefix else vm_name
@ -554,10 +579,11 @@ def batch_jobs():
sftp_password=None,
schedule_type=schedule_type,
schedule_time=sched_time,
weekly_day='0',
interval_hours='24',
weekly_day=weekly_day,
interval_hours=interval_hrs,
label=label,
disk_filter=disk_filter,
monthly_day=monthly_day,
)
created.append(jid)
@ -565,6 +591,7 @@ def batch_jobs():
flash(f'{len(created)} backup job{"s" if len(created)!=1 else ""} created ({strat_label}).', 'success')
return redirect(url_for('jobs'))
# GET: show batch config form
vm_names = request.args.getlist('vms')
if not vm_names:

View File

@ -259,28 +259,60 @@
Schedule
</div>
<div class="section-card-body">
<div class="schedule-options">
<div class="schedule-options" style="grid-template-columns: repeat(3, 1fr);">
<label class="schedule-opt selected" id="opt-now" onclick="selectSchedule('now')">
<input type="radio" name="schedule_type" value="now" checked />
<div>
<div class="schedule-opt-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<div class="schedule-opt-title">Run Now</div>
<div class="schedule-opt-desc">Start all backups immediately</div>
<div class="schedule-opt-desc">Start immediately</div>
</div>
</label>
<label class="schedule-opt" id="opt-daily" onclick="selectSchedule('daily')">
<input type="radio" name="schedule_type" value="daily" />
<div>
<div class="schedule-opt-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
<div class="schedule-opt-title">Daily</div>
<div class="schedule-opt-desc">All VMs at the same time each day</div>
<div class="schedule-opt-desc">Every day at set time</div>
</div>
</label>
<label class="schedule-opt" id="opt-weekly" onclick="selectSchedule('weekly')">
<input type="radio" name="schedule_type" value="weekly" />
<div>
<div class="schedule-opt-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/></svg>
</div>
<div class="schedule-opt-title">Weekly</div>
<div class="schedule-opt-desc">Specific day each week</div>
</div>
</label>
<label class="schedule-opt" id="opt-monthly" onclick="selectSchedule('monthly')">
<input type="radio" name="schedule_type" value="monthly" />
<div>
<div class="schedule-opt-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h4"/><path d="M8 18h2"/></svg>
</div>
<div class="schedule-opt-title">Monthly</div>
<div class="schedule-opt-desc">Specific day each month</div>
</div>
</label>
<label class="schedule-opt" id="opt-interval" onclick="selectSchedule('interval')">
<input type="radio" name="schedule_type" value="interval" />
<div>
<div class="schedule-opt-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
</div>
<div class="schedule-opt-title">Every N Hours</div>
<div class="schedule-opt-desc">Repeat on an interval</div>
</div>
</label>
</div>
<!-- Detail panels -->
<div class="schedule-detail" id="detail-daily">
<div class="form-row">
<div class="form-group" style="margin:0;">
@ -289,6 +321,52 @@
</div>
</div>
</div>
<div class="schedule-detail" id="detail-weekly">
<div class="form-row">
<div class="form-group" style="margin:0;">
<label class="form-label" for="weekly_day">Day of week</label>
<select id="weekly_day" class="form-control" name="weekly_day">
<option value="0">Monday</option>
<option value="1">Tuesday</option>
<option value="2">Wednesday</option>
<option value="3">Thursday</option>
<option value="4">Friday</option>
<option value="5">Saturday</option>
<option value="6">Sunday</option>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" for="weekly_time">Time (24h)</label>
<input id="weekly_time" class="form-control" type="time" name="weekly_time" value="02:00" />
</div>
</div>
</div>
<div class="schedule-detail" id="detail-monthly">
<div class="form-row">
<div class="form-group" style="margin:0;">
<label class="form-label" for="monthly_day">Day of month (128)</label>
<input id="monthly_day" class="form-control" type="number"
name="monthly_day" min="1" max="28" value="1" />
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" for="monthly_time">Time (24h)</label>
<input id="monthly_time" class="form-control" type="time" name="monthly_time" value="02:00" />
</div>
</div>
</div>
<div class="schedule-detail" id="detail-interval">
<div class="form-row">
<div class="form-group" style="margin:0;">
<label class="form-label" for="interval_hours">Repeat every (hours)</label>
<input id="interval_hours" class="form-control" type="number"
name="interval_hours" min="1" max="8760" value="24" />
<div style="font-size:12px;color:var(--text-muted);margin-top:4px;">
e.g. 6 = every 6h · 12 = twice daily · 168 = weekly
</div>
</div>
</div>
</div>
<div class="form-group" style="margin-top:16px; margin-bottom:0;">
<label class="form-label" for="job_label">Label prefix (optional)</label>
<input id="job_label" class="form-control" type="text" name="job_label"
@ -315,8 +393,10 @@
{% block scripts %}
<script>
const ALL_SCHED = ['now','daily','weekly','monthly','interval'];
function selectSchedule(type) {
['now','daily'].forEach(t => {
ALL_SCHED.forEach(t => {
document.getElementById('opt-' + t).classList.remove('selected');
const d = document.getElementById('detail-' + t);
if (d) d.classList.remove('visible');
@ -369,3 +449,4 @@
.catch(() => {});
</script>
{% endblock %}

View File

@ -285,6 +285,17 @@
</div>
</label>
<label class="schedule-opt" id="opt-monthly" onclick="selectSchedule('monthly')">
<input type="radio" name="schedule_type" value="monthly" />
<div>
<div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h4"/><path d="M8 18h2"/></svg>
</div>
<div class="schedule-opt-title">Monthly</div>
<div class="schedule-opt-desc">Specific day each month</div>
</div>
</label>
</div>
<!-- Daily detail -->
@ -326,6 +337,24 @@
<label class="form-label" for="interval_hours">Every (hours)</label>
<input id="interval_hours" class="form-control" type="number"
name="interval_hours" value="24" min="1" max="8760" />
<div style="font-size:12px;color:var(--text-muted);margin-top:4px;">
e.g. 6 = every 6h &middot; 12 = twice daily &middot; 168 = weekly
</div>
</div>
</div>
</div>
<!-- Monthly detail -->
<div class="schedule-detail" id="detail-monthly">
<div class="form-row">
<div class="form-group" style="margin:0;">
<label class="form-label" for="monthly_day">Day of month (128)</label>
<input id="monthly_day" class="form-control" type="number"
name="monthly_day" min="1" max="28" value="1" />
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" for="monthly_time">Time (24h)</label>
<input id="monthly_time" class="form-control" type="time" name="monthly_time" value="02:00" />
</div>
</div>
</div>
@ -357,7 +386,7 @@
{% block scripts %}
<script>
function selectSchedule(type) {
['now','daily','weekly','interval'].forEach(t => {
['now','daily','weekly','monthly','interval'].forEach(t => {
document.getElementById('opt-' + t).classList.remove('selected');
const d = document.getElementById('detail-' + t);
if (d) d.classList.remove('visible');