feat: implement Flask web UI for VM backup management and scheduling
This commit is contained in:
parent
7f306c713c
commit
7b4688b792
Binary file not shown.
@ -261,10 +261,11 @@ def create_and_start_job(
|
|||||||
vm_name, dest, compress, no_verify_ssl,
|
vm_name, dest, compress, no_verify_ssl,
|
||||||
sftp_host, sftp_user, sftp_password,
|
sftp_host, sftp_user, sftp_password,
|
||||||
schedule_type, schedule_time, weekly_day, interval_hours,
|
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.
|
"""Create a job entry and either run immediately or register schedule.
|
||||||
disk_filter: list of VMDK path strings to include, or None for all.
|
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]
|
jid = datetime.now().strftime('%Y%m%d%H%M%S') + '-' + uuid.uuid4().hex[:6]
|
||||||
job_dir = JOBS_DIR / jid
|
job_dir = JOBS_DIR / jid
|
||||||
@ -307,6 +308,12 @@ def create_and_start_job(
|
|||||||
day_of_week=int(weekly_day),
|
day_of_week=int(weekly_day),
|
||||||
hour=int(hour), minute=int(minute)
|
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':
|
elif schedule_type == 'interval':
|
||||||
trigger = IntervalTrigger(hours=max(1, int(interval_hours or 24)))
|
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')
|
daily_time = request.form.get('daily_time', '02:00')
|
||||||
weekly_day = request.form.get('weekly_day', '0')
|
weekly_day = request.form.get('weekly_day', '0')
|
||||||
weekly_time = request.form.get('weekly_time', '02:00')
|
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')
|
interval_hrs = request.form.get('interval_hours', '24')
|
||||||
label = request.form.get('job_label', '').strip()
|
label = request.form.get('job_label', '').strip()
|
||||||
|
|
||||||
@ -453,6 +462,8 @@ def create_job():
|
|||||||
sched_time = daily_time
|
sched_time = daily_time
|
||||||
elif schedule_type == 'weekly':
|
elif schedule_type == 'weekly':
|
||||||
sched_time = weekly_time
|
sched_time = weekly_time
|
||||||
|
elif schedule_type == 'monthly':
|
||||||
|
sched_time = monthly_time
|
||||||
else:
|
else:
|
||||||
sched_time = ''
|
sched_time = ''
|
||||||
|
|
||||||
@ -478,11 +489,13 @@ def create_job():
|
|||||||
interval_hours=interval_hrs,
|
interval_hours=interval_hrs,
|
||||||
label=label,
|
label=label,
|
||||||
disk_filter=disk_filter,
|
disk_filter=disk_filter,
|
||||||
|
monthly_day=monthly_day,
|
||||||
)
|
)
|
||||||
n_disks = len(disk_filter) if disk_filter is not None else 'all'
|
n_disks = len(disk_filter) if disk_filter is not None else 'all'
|
||||||
flash(f'Job created — {n_disks} disk(s) selected.', 'success')
|
flash(f'Job created — {n_disks} disk(s) selected.', 'success')
|
||||||
return redirect(url_for('job_detail', jobid=jid))
|
return redirect(url_for('job_detail', jobid=jid))
|
||||||
|
|
||||||
|
|
||||||
# GET: load VM list for the dropdown
|
# GET: load VM list for the dropdown
|
||||||
selected_vm = request.args.get('vm', '')
|
selected_vm = request.args.get('vm', '')
|
||||||
show_schedule = bool(request.args.get('schedule', ''))
|
show_schedule = bool(request.args.get('schedule', ''))
|
||||||
@ -522,8 +535,21 @@ def batch_jobs():
|
|||||||
disk_strategy = request.form.get('disk_strategy', 'all')
|
disk_strategy = request.form.get('disk_strategy', 'all')
|
||||||
schedule_type = request.form.get('schedule_type', 'now')
|
schedule_type = request.form.get('schedule_type', 'now')
|
||||||
daily_time = request.form.get('daily_time', '02:00')
|
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()
|
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:
|
if not vm_names:
|
||||||
flash('No VMs selected.', 'danger')
|
flash('No VMs selected.', 'danger')
|
||||||
@ -533,14 +559,13 @@ def batch_jobs():
|
|||||||
for vm_name in vm_names:
|
for vm_name in vm_names:
|
||||||
# Resolve disk_filter from strategy
|
# Resolve disk_filter from strategy
|
||||||
if disk_strategy == 'os':
|
if disk_strategy == 'os':
|
||||||
# Smallest disk = OS disk
|
|
||||||
vm_info = vms_by_name.get(vm_name, {})
|
vm_info = vms_by_name.get(vm_name, {})
|
||||||
disks = sorted(vm_info.get('disks', []), key=lambda d: d.get('size_gb', 0))
|
disks = sorted(vm_info.get('disks', []), key=lambda d: d.get('size_gb', 0))
|
||||||
disk_filter = [disks[0]['path']] if disks else None
|
disk_filter = [disks[0]['path']] if disks else None
|
||||||
elif disk_strategy == 'vmx':
|
elif disk_strategy == 'vmx':
|
||||||
disk_filter = [] # empty list = VMX only
|
disk_filter = []
|
||||||
else:
|
else:
|
||||||
disk_filter = None # all disks
|
disk_filter = None
|
||||||
|
|
||||||
label = f'{label_prefix} — {vm_name}' if label_prefix else vm_name
|
label = f'{label_prefix} — {vm_name}' if label_prefix else vm_name
|
||||||
|
|
||||||
@ -554,10 +579,11 @@ def batch_jobs():
|
|||||||
sftp_password=None,
|
sftp_password=None,
|
||||||
schedule_type=schedule_type,
|
schedule_type=schedule_type,
|
||||||
schedule_time=sched_time,
|
schedule_time=sched_time,
|
||||||
weekly_day='0',
|
weekly_day=weekly_day,
|
||||||
interval_hours='24',
|
interval_hours=interval_hrs,
|
||||||
label=label,
|
label=label,
|
||||||
disk_filter=disk_filter,
|
disk_filter=disk_filter,
|
||||||
|
monthly_day=monthly_day,
|
||||||
)
|
)
|
||||||
created.append(jid)
|
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')
|
flash(f'{len(created)} backup job{"s" if len(created)!=1 else ""} created ({strat_label}).', 'success')
|
||||||
return redirect(url_for('jobs'))
|
return redirect(url_for('jobs'))
|
||||||
|
|
||||||
|
|
||||||
# GET: show batch config form
|
# GET: show batch config form
|
||||||
vm_names = request.args.getlist('vms')
|
vm_names = request.args.getlist('vms')
|
||||||
if not vm_names:
|
if not vm_names:
|
||||||
|
|||||||
@ -259,28 +259,60 @@
|
|||||||
Schedule
|
Schedule
|
||||||
</div>
|
</div>
|
||||||
<div class="section-card-body">
|
<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')">
|
<label class="schedule-opt selected" id="opt-now" onclick="selectSchedule('now')">
|
||||||
<input type="radio" name="schedule_type" value="now" checked />
|
<input type="radio" name="schedule_type" value="now" checked />
|
||||||
<div>
|
<div>
|
||||||
<div class="schedule-opt-icon">
|
<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>
|
||||||
<div class="schedule-opt-title">Run Now</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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="schedule-opt" id="opt-daily" onclick="selectSchedule('daily')">
|
<label class="schedule-opt" id="opt-daily" onclick="selectSchedule('daily')">
|
||||||
<input type="radio" name="schedule_type" value="daily" />
|
<input type="radio" name="schedule_type" value="daily" />
|
||||||
<div>
|
<div>
|
||||||
<div class="schedule-opt-icon">
|
<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>
|
||||||
<div class="schedule-opt-title">Daily</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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail panels -->
|
||||||
<div class="schedule-detail" id="detail-daily">
|
<div class="schedule-detail" id="detail-daily">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group" style="margin:0;">
|
<div class="form-group" style="margin:0;">
|
||||||
@ -289,6 +321,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 (1–28)</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;">
|
<div class="form-group" style="margin-top:16px; margin-bottom:0;">
|
||||||
<label class="form-label" for="job_label">Label prefix (optional)</label>
|
<label class="form-label" for="job_label">Label prefix (optional)</label>
|
||||||
<input id="job_label" class="form-control" type="text" name="job_label"
|
<input id="job_label" class="form-control" type="text" name="job_label"
|
||||||
@ -315,8 +393,10 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
const ALL_SCHED = ['now','daily','weekly','monthly','interval'];
|
||||||
|
|
||||||
function selectSchedule(type) {
|
function selectSchedule(type) {
|
||||||
['now','daily'].forEach(t => {
|
ALL_SCHED.forEach(t => {
|
||||||
document.getElementById('opt-' + t).classList.remove('selected');
|
document.getElementById('opt-' + t).classList.remove('selected');
|
||||||
const d = document.getElementById('detail-' + t);
|
const d = document.getElementById('detail-' + t);
|
||||||
if (d) d.classList.remove('visible');
|
if (d) d.classList.remove('visible');
|
||||||
@ -369,3 +449,4 @@
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -285,6 +285,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Daily detail -->
|
<!-- Daily detail -->
|
||||||
@ -326,6 +337,24 @@
|
|||||||
<label class="form-label" for="interval_hours">Every (hours)</label>
|
<label class="form-label" for="interval_hours">Every (hours)</label>
|
||||||
<input id="interval_hours" class="form-control" type="number"
|
<input id="interval_hours" class="form-control" type="number"
|
||||||
name="interval_hours" value="24" min="1" max="8760" />
|
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 · 12 = twice daily · 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 (1–28)</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>
|
||||||
</div>
|
</div>
|
||||||
@ -357,7 +386,7 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
function selectSchedule(type) {
|
function selectSchedule(type) {
|
||||||
['now','daily','weekly','interval'].forEach(t => {
|
['now','daily','weekly','monthly','interval'].forEach(t => {
|
||||||
document.getElementById('opt-' + t).classList.remove('selected');
|
document.getElementById('opt-' + t).classList.remove('selected');
|
||||||
const d = document.getElementById('detail-' + t);
|
const d = document.getElementById('detail-' + t);
|
||||||
if (d) d.classList.remove('visible');
|
if (d) d.classList.remove('visible');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user