From 7b4688b79227c13388b2a9266e0d3d823e1eb8c8 Mon Sep 17 00:00:00 2001 From: Rizqi Date: Sun, 21 Jun 2026 12:21:09 +0700 Subject: [PATCH] feat: implement Flask web UI for VM backup management and scheduling --- .../__pycache__/gui_app.cpython-310.pyc | Bin 19215 -> 19728 bytes vsphere_backup/gui_app.py | 41 ++++++-- vsphere_backup/templates/batch_job.html | 93 ++++++++++++++++-- vsphere_backup/templates/create_job.html | 31 +++++- 4 files changed, 151 insertions(+), 14 deletions(-) diff --git a/vsphere_backup/__pycache__/gui_app.cpython-310.pyc b/vsphere_backup/__pycache__/gui_app.cpython-310.pyc index 9f937a0f7e982a35de5495f849a39a364172c47e..9ecdf3c63faac17390b2806790947aa4185b77f5 100644 GIT binary patch delta 6130 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 diff --git a/vsphere_backup/gui_app.py b/vsphere_backup/gui_app.py index b46b4ee..5601b19 100644 --- a/vsphere_backup/gui_app.py +++ b/vsphere_backup/gui_app.py @@ -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: diff --git a/vsphere_backup/templates/batch_job.html b/vsphere_backup/templates/batch_job.html index bb100a3..b2ddb0a 100644 --- a/vsphere_backup/templates/batch_job.html +++ b/vsphere_backup/templates/batch_job.html @@ -259,28 +259,60 @@ Schedule
-
+
+ + +
+ +
@@ -289,6 +321,52 @@
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+ e.g. 6 = every 6h · 12 = twice daily · 168 = weekly +
+
+
+
+
+ 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(() => {}); {% endblock %} + diff --git a/vsphere_backup/templates/create_job.html b/vsphere_backup/templates/create_job.html index 6575182..8dc71a7 100644 --- a/vsphere_backup/templates/create_job.html +++ b/vsphere_backup/templates/create_job.html @@ -285,6 +285,17 @@
+ +
@@ -326,6 +337,24 @@ +
+ e.g. 6 = every 6h · 12 = twice daily · 168 = weekly +
+
+ + + + +
+
+
+ + +
+
+ +
@@ -357,7 +386,7 @@ {% block scripts %}