Some checks failed
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>
223 lines
6.7 KiB
Python
223 lines
6.7 KiB
Python
#!/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/>.
|
|
#
|
|
# -----------------------------------------------------------------------
|
|
|
|
"""Utility functions for date/time formatting, number formatting, and display helpers."""
|
|
|
|
import datetime
|
|
import os
|
|
|
|
|
|
def datetime2isodate(adatetime=None):
|
|
"""Convert datetime to ISO format string."""
|
|
if not adatetime:
|
|
adatetime = datetime.datetime.now()
|
|
assert isinstance(adatetime, datetime.datetime)
|
|
return adatetime.isoformat()
|
|
|
|
|
|
def isodate2datetime(isodatestr):
|
|
"""Convert ISO format string to datetime."""
|
|
# we remove the microseconds part as it is not working for python2.5 strptime
|
|
return datetime.datetime.strptime(isodatestr.split(".")[0], "%Y-%m-%dT%H:%M:%S")
|
|
|
|
|
|
def time2display(adatetime):
|
|
"""Format datetime for display."""
|
|
return adatetime.strftime("%Y-%m-%d %H:%M")
|
|
|
|
|
|
def hours_minutes(hours):
|
|
"""Convert decimal hours to HH:MM format."""
|
|
if hours is None:
|
|
return None
|
|
else:
|
|
return "%02i:%02i" % (int(hours), int((hours - int(hours)) * 60.0))
|
|
|
|
|
|
def fileisodate(filename):
|
|
"""Get file modification time as ISO date string."""
|
|
return datetime.datetime.fromtimestamp(os.stat(filename).st_mtime).isoformat()
|
|
|
|
|
|
def dateof(adatetime):
|
|
"""Get date part of datetime (midnight)."""
|
|
return adatetime.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
|
|
#####################################
|
|
# http://code.activestate.com/recipes/498181-add-thousands-separator-commas-to-formatted-number/
|
|
# Code from Michael Robellard's comment made 28 Feb 2010
|
|
# Modified for leading +, -, space on 1 Mar 2010 by Glenn Linderman
|
|
#
|
|
# Tail recursion removed and leading garbage handled on March 12 2010, Alessandro Forghieri
|
|
def splitThousands(s, tSep=",", dSep="."):
|
|
"""Splits a general float on thousands. GIGO on general input"""
|
|
if s is None:
|
|
return 0
|
|
if not isinstance(s, str):
|
|
s = str(s)
|
|
|
|
cnt = 0
|
|
numChars = dSep + "0123456789"
|
|
ls = len(s)
|
|
while cnt < ls and s[cnt] not in numChars:
|
|
cnt += 1
|
|
|
|
lhs = s[0:cnt]
|
|
s = s[cnt:]
|
|
if dSep == "":
|
|
cnt = -1
|
|
else:
|
|
cnt = s.rfind(dSep)
|
|
if cnt > 0:
|
|
rhs = dSep + s[cnt + 1 :]
|
|
s = s[:cnt]
|
|
else:
|
|
rhs = ""
|
|
|
|
splt = ""
|
|
while s != "":
|
|
splt = s[-3:] + tSep + splt
|
|
s = s[:-3]
|
|
|
|
return lhs + splt[:-1] + rhs
|
|
|
|
|
|
def convert_bytes(bytes):
|
|
"""Convert bytes to human-readable format (T/G/M/K/b)."""
|
|
if bytes is None:
|
|
return None
|
|
else:
|
|
bytes = float(bytes)
|
|
if bytes >= 1099511627776:
|
|
terabytes = bytes / 1099511627776
|
|
size = "%.2fT" % terabytes
|
|
elif bytes >= 1073741824:
|
|
gigabytes = bytes / 1073741824
|
|
size = "%.2fG" % gigabytes
|
|
elif bytes >= 1048576:
|
|
megabytes = bytes / 1048576
|
|
size = "%.2fM" % megabytes
|
|
elif bytes >= 1024:
|
|
kilobytes = bytes / 1024
|
|
size = "%.2fK" % kilobytes
|
|
else:
|
|
size = "%.2fb" % bytes
|
|
return size
|
|
|
|
|
|
def check_string(test_string):
|
|
"""Check if string contains only alphanumeric characters, dots, dashes, and underscores."""
|
|
import re
|
|
|
|
pattern = r"[^\.A-Za-z0-9\-_]"
|
|
if re.search(pattern, test_string):
|
|
# Character other then . a-z 0-9 was found
|
|
print(("Invalid : %r" % (test_string,)))
|
|
|
|
|
|
def str2bool(val):
|
|
"""Convert string to boolean."""
|
|
if not isinstance(type(val), bool):
|
|
return val.lower() in ("yes", "true", "t", "1")
|
|
|
|
|
|
## {{{ http://code.activestate.com/recipes/81189/ (r2)
|
|
def pp(cursor, data=None, rowlens=0, callback=None):
|
|
"""
|
|
pretty print a query result as a table
|
|
callback is a function called for each field (fieldname,value) to format the output
|
|
"""
|
|
|
|
def defaultcb(fieldname, value):
|
|
return value
|
|
|
|
if not callback:
|
|
callback = defaultcb
|
|
|
|
d = cursor.description
|
|
if not d:
|
|
return "#### NO RESULTS ###"
|
|
names = []
|
|
lengths = []
|
|
rules = []
|
|
if not data:
|
|
data = cursor.fetchall()
|
|
for dd in d: # iterate over description
|
|
l = dd[1]
|
|
if not l:
|
|
l = 12 # or default arg ...
|
|
l = max(l, len(dd[0])) # handle long names
|
|
names.append(dd[0])
|
|
lengths.append(l)
|
|
for col in range(len(lengths)):
|
|
if rowlens:
|
|
rls = [len(str(callback(d[col][0], row[col]))) for row in data if row[col]]
|
|
lengths[col] = max([lengths[col]] + rls)
|
|
rules.append("-" * lengths[col])
|
|
format = " ".join(["%%-%ss" % l for l in lengths])
|
|
result = [format % tuple(names)]
|
|
result.append(format % tuple(rules))
|
|
for row in data:
|
|
row_cb = []
|
|
for col in range(len(d)):
|
|
row_cb.append(callback(d[col][0], row[col]))
|
|
result.append(format % tuple(row_cb))
|
|
return "\n".join(result)
|
|
|
|
|
|
## end of http://code.activestate.com/recipes/81189/ }}}
|
|
|
|
|
|
def html_table(cur, callback=None):
|
|
"""
|
|
cur est un cursor issu d'une requete
|
|
callback est une fonction qui prend (rowmap,fieldname,value)
|
|
et renvoie une representation texte
|
|
"""
|
|
|
|
def safe_unicode(iso):
|
|
if iso is None:
|
|
return None
|
|
elif isinstance(iso, str):
|
|
return iso # .decode()
|
|
else:
|
|
return iso
|
|
|
|
def itermap(cur):
|
|
for row in cur:
|
|
yield dict((cur.description[idx][0], value) for idx, value in enumerate(row))
|
|
|
|
head = "<tr>" + "".join(["<th>" + c[0] + "</th>" for c in cur.description]) + "</tr>"
|
|
lines = ""
|
|
if callback:
|
|
for r in itermap(cur):
|
|
lines = (
|
|
lines
|
|
+ "<tr>"
|
|
+ "".join(["<td>" + str(callback(r, c[0], safe_unicode(r[c[0]]))) + "</td>" for c in cur.description])
|
|
+ "</tr>"
|
|
)
|
|
else:
|
|
for r in cur:
|
|
lines = lines + "<tr>" + "".join(["<td>" + safe_unicode(c) + "</td>" for c in r]) + "</tr>"
|
|
|
|
return "<table border=1 cellpadding=2 cellspacing=0>%s%s</table>" % (head, lines)
|