refactor(drivers): organize backup modules into drivers subfolder
lint / docker (push) Has been cancelled
lint / docker (push) Has been cancelled
- Move all backup_*.py files to libtisbackup/drivers/ subdirectory - Move XenAPI.py and copy_vm_xcp.py to drivers/ (driver-specific) - Create drivers/__init__.py with automatic driver imports - Update tisbackup.py imports to use new structure - Add pyvmomi>=8.0.0 as mandatory dependency - Sync requirements.txt with pyproject.toml dependencies - Add pylint>=3.0.0 and pytest-cov>=6.0.0 to dev dependencies - Configure pylint and coverage tools in pyproject.toml - Add conventional commits guidelines to CLAUDE.md - Enhance .gitignore with comprehensive patterns for Python, IDEs, testing, and secrets - Update CLAUDE.md documentation with new structure and tooling Breaking Changes: - Drivers must now be imported from libtisbackup.drivers instead of libtisbackup - All backup driver files relocated to drivers/ subdirectory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# -----------------------------------------------------------------------
|
||||
# This file is part of TISBackup
|
||||
#
|
||||
# TISBackup is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# TISBackup is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"""Database management for backup statistics and history."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from .utils import (
|
||||
convert_bytes,
|
||||
datetime2isodate,
|
||||
hours_minutes,
|
||||
html_table,
|
||||
isodate2datetime,
|
||||
pp,
|
||||
splitThousands,
|
||||
time2display,
|
||||
)
|
||||
|
||||
|
||||
class BackupStat:
|
||||
"""Manages SQLite database for backup statistics and history."""
|
||||
|
||||
dbpath = ""
|
||||
db = None
|
||||
logger = logging.getLogger("tisbackup")
|
||||
|
||||
def __init__(self, dbpath):
|
||||
self.dbpath = dbpath
|
||||
if not os.path.isfile(self.dbpath):
|
||||
self.db = sqlite3.connect(self.dbpath)
|
||||
self.initdb()
|
||||
else:
|
||||
self.db = sqlite3.connect(self.dbpath, check_same_thread=False)
|
||||
if "'TYPE'" not in str(self.db.execute("select * from stats").description):
|
||||
self.updatedb()
|
||||
|
||||
def updatedb(self):
|
||||
"""Update database schema to add TYPE column if missing."""
|
||||
self.logger.debug("Update stat database")
|
||||
self.db.execute("alter table stats add column TYPE TEXT;")
|
||||
self.db.execute("update stats set TYPE='BACKUP';")
|
||||
self.db.commit()
|
||||
|
||||
def initdb(self):
|
||||
"""Initialize database schema."""
|
||||
assert isinstance(self.db, sqlite3.Connection)
|
||||
self.logger.debug("Initialize stat database")
|
||||
self.db.execute("""
|
||||
create table stats (
|
||||
backup_name TEXT,
|
||||
server_name TEXT,
|
||||
description TEXT,
|
||||
backup_start TEXT,
|
||||
backup_end TEXT,
|
||||
backup_duration NUMERIC,
|
||||
total_files_count INT,
|
||||
written_files_count INT,
|
||||
total_bytes INT,
|
||||
written_bytes INT,
|
||||
status TEXT,
|
||||
log TEXT,
|
||||
backup_location TEXT,
|
||||
TYPE TEXT)""")
|
||||
self.db.execute("""
|
||||
create index idx_stats_backup_name on stats(backup_name);""")
|
||||
self.db.execute("""
|
||||
create index idx_stats_backup_location on stats(backup_location);""")
|
||||
self.db.execute("""
|
||||
CREATE INDEX idx_stats_backup_name_start on stats(backup_name,backup_start);""")
|
||||
self.db.commit()
|
||||
|
||||
def start(self, backup_name, server_name, TYPE, description="", backup_location=None):
|
||||
"""Add in stat DB a record for the newly running backup"""
|
||||
return self.add(
|
||||
backup_name=backup_name,
|
||||
server_name=server_name,
|
||||
description=description,
|
||||
backup_start=datetime2isodate(),
|
||||
status="Running",
|
||||
TYPE=TYPE,
|
||||
)
|
||||
|
||||
def finish(
|
||||
self,
|
||||
rowid,
|
||||
total_files_count=None,
|
||||
written_files_count=None,
|
||||
total_bytes=None,
|
||||
written_bytes=None,
|
||||
log=None,
|
||||
status="OK",
|
||||
backup_end=None,
|
||||
backup_duration=None,
|
||||
backup_location=None,
|
||||
):
|
||||
"""Update record in stat DB for the finished backup"""
|
||||
if not backup_end:
|
||||
backup_end = datetime2isodate()
|
||||
if backup_duration is None:
|
||||
try:
|
||||
# get duration using start of backup datetime
|
||||
backup_duration = (
|
||||
isodate2datetime(backup_end)
|
||||
- isodate2datetime(self.query("select backup_start from stats where rowid=?", (rowid,))[0]["backup_start"])
|
||||
).seconds / 3600.0
|
||||
except:
|
||||
backup_duration = None
|
||||
|
||||
# update stat record
|
||||
self.db.execute(
|
||||
"""\
|
||||
update stats set
|
||||
total_files_count=?,written_files_count=?,total_bytes=?,written_bytes=?,log=?,status=?,backup_end=?,backup_duration=?,backup_location=?
|
||||
where
|
||||
rowid = ?
|
||||
""",
|
||||
(
|
||||
total_files_count,
|
||||
written_files_count,
|
||||
total_bytes,
|
||||
written_bytes,
|
||||
log,
|
||||
status,
|
||||
backup_end,
|
||||
backup_duration,
|
||||
backup_location,
|
||||
rowid,
|
||||
),
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
def add(
|
||||
self,
|
||||
backup_name="",
|
||||
server_name="",
|
||||
description="",
|
||||
backup_start=None,
|
||||
backup_end=None,
|
||||
backup_duration=None,
|
||||
total_files_count=None,
|
||||
written_files_count=None,
|
||||
total_bytes=None,
|
||||
written_bytes=None,
|
||||
status="draft",
|
||||
log="",
|
||||
TYPE="",
|
||||
backup_location=None,
|
||||
):
|
||||
"""Add a new backup record to the database."""
|
||||
if not backup_start:
|
||||
backup_start = datetime2isodate()
|
||||
if not backup_end:
|
||||
backup_end = datetime2isodate()
|
||||
|
||||
cur = self.db.execute(
|
||||
"""\
|
||||
insert into stats (
|
||||
backup_name,
|
||||
server_name,
|
||||
description,
|
||||
backup_start,
|
||||
backup_end,
|
||||
backup_duration,
|
||||
total_files_count,
|
||||
written_files_count,
|
||||
total_bytes,
|
||||
written_bytes,
|
||||
status,
|
||||
log,
|
||||
backup_location,
|
||||
TYPE) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
backup_name,
|
||||
server_name,
|
||||
description,
|
||||
backup_start,
|
||||
backup_end,
|
||||
backup_duration,
|
||||
total_files_count,
|
||||
written_files_count,
|
||||
total_bytes,
|
||||
written_bytes,
|
||||
status,
|
||||
log,
|
||||
backup_location,
|
||||
TYPE,
|
||||
),
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def query(self, query, args=(), one=False):
|
||||
"""
|
||||
execute la requete query sur la db et renvoie un tableau de dictionnaires
|
||||
"""
|
||||
cur = self.db.execute(query, args)
|
||||
rv = [dict((cur.description[idx][0], value) for idx, value in enumerate(row)) for row in cur.fetchall()]
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
def last_backups(self, backup_name, count=30):
|
||||
"""Display last N backups for a given backup_name."""
|
||||
if backup_name:
|
||||
cur = self.db.execute("select * from stats where backup_name=? order by backup_end desc limit ?", (backup_name, count))
|
||||
else:
|
||||
cur = self.db.execute("select * from stats order by backup_end desc limit ?", (count,))
|
||||
|
||||
def fcb(fieldname, value):
|
||||
if fieldname in ("backup_start", "backup_end"):
|
||||
return time2display(isodate2datetime(value))
|
||||
elif "bytes" in fieldname:
|
||||
return convert_bytes(value)
|
||||
elif "count" in fieldname:
|
||||
return splitThousands(value, " ", ".")
|
||||
elif "backup_duration" in fieldname:
|
||||
return hours_minutes(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
# for r in self.query('select * from stats where backup_name=? order by backup_end desc limit ?',(backup_name,count)):
|
||||
print((pp(cur, None, 1, fcb)))
|
||||
|
||||
def fcb(self, fields, fieldname, value):
|
||||
"""Format callback for HTML table display."""
|
||||
if fieldname in ("backup_start", "backup_end"):
|
||||
return time2display(isodate2datetime(value))
|
||||
elif "bytes" in fieldname:
|
||||
return convert_bytes(value)
|
||||
elif "count" in fieldname:
|
||||
return splitThousands(value, " ", ".")
|
||||
elif "backup_duration" in fieldname:
|
||||
return hours_minutes(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def as_html(self, cur):
|
||||
"""Convert cursor to HTML table."""
|
||||
if cur:
|
||||
return html_table(cur, self.fcb)
|
||||
else:
|
||||
return html_table(self.db.execute("select * from stats order by backup_start asc"), self.fcb)
|
||||
Reference in New Issue
Block a user