Compare commits

23 Commits

Author SHA1 Message Date
k3nny e823f65c3c fix(tisbackup): 🐛 remove excess uv/uvx 2025-04-18 23:57:44 +02:00
k3nny 5c627f3a64 fix(tisbackup): 🐛 Dockerfile fix venv uv 2025-04-18 23:48:25 +02:00
k3nny 7b6ce02a93 fix(tisbackup): 🐛 fix dockerignore pyproject.toml absent 2025-04-18 23:36:26 +02:00
k3nny e7d3e1140c fix(tisbackup): using uv is good in Dockerfile maybe 2025-04-18 23:32:15 +02:00
k3nny 6fe3eebf36 fix(tisbackup): using uv is good 2025-04-18 23:11:05 +02:00
k3nny 79d15628bd fix(tisbackup): add elements to .dockerignore - bis
lint / docker (push) Successful in 9m17s
2025-04-14 23:54:51 +02:00
k3nny 3a4f3267eb fix(tisbackup): add elements to .dockerignore
lint / docker (push) Has been cancelled
2025-04-14 23:50:42 +02:00
k3nny 8761a04c40 fix(tisbackup): add .dockerignore
lint / docker (push) Has been cancelled
2025-04-14 23:45:53 +02:00
k3nny 586991bcf1 fix(tisbackup): fix iniparse wrong check
lint / docker (push) Has been cancelled
2025-04-14 23:37:16 +02:00
k3nny ddb5f3716d Fix replace
lint / docker (push) Successful in 9m16s
2025-03-07 22:54:14 +01:00
k3nny b805f8387e Fix re.compile / re.match warnings
lint / docker (push) Has been cancelled
2025-03-07 22:51:20 +01:00
k3nny da50051a3f Python 3.13 + add nginx reverse-proxy
lint / docker (push) Successful in 14m2s
2025-03-07 22:24:27 +01:00
k3nny 8ef9bbde06 improve README.md
lint / docker (push) Successful in 9m15s
2024-11-30 00:20:51 +01:00
k3nny 737f9bea38 fix iniparse
lint / docker (push) Successful in 9m14s
fix code passing ruff linter
pre-commit ruff
pre-commit ruff format
2024-11-29 23:45:40 +01:00
k3nny aa8a68aa80 EOF & whitespace
lint / docker (push) Failing after 4m47s
2024-11-29 00:54:31 +01:00
k3nny 7fcc5afc64 EOF & whitespace 2024-11-29 00:54:09 +01:00
k3nny e7e98d0b47 few fixes and lint compatible 2024-11-29 00:48:59 +01:00
k3nny 8479c378ee fix basic 2024-11-29 00:32:39 +01:00
k3nny 274e1e2e59 requirements.txt 2024-11-29 00:02:24 +01:00
k3nny eb0bdaedbd fix import 2024-11-28 23:59:02 +01:00
k3nny 99dc6e0abf fix import 2024-11-28 23:46:48 +01:00
k3nny e8ba6df102 fix first pass - .gitignore 2024-11-28 23:21:26 +01:00
k3nny ffd9bf3d39 fix first pass 2024-11-28 23:20:19 +01:00
89 changed files with 7785 additions and 6540 deletions
+101
View File
@@ -0,0 +1,101 @@
# TISBackup
rpm/
deb/
.gitea/
.hadolint.yml
.pre-commit-config.yaml
README.md
compose.yml
docs/
docs-sphinx-rst/
samples/
# Git
.git
.gitignore
.gitattributes
# CI
.codeclimate.yml
.travis.yml
.taskcluster.yml
# Docker
docker-compose.yml
Dockerfile
.docker
.dockerignore
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Virtual environment
.env
.venv/
venv/
# PyCharm
.idea
# Python mode for VIM
.ropeproject
**/.ropeproject
# Vim swap files
**/*.swp
# VS Code
.vscode/
+1 -2
View File
@@ -14,12 +14,11 @@ jobs:
- name: Install Python - name: Install Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: '3.12' python-version: '3.13'
cache: 'pip' # caching pip dependencies cache: 'pip' # caching pip dependencies
- run: pip install ruff - run: pip install ruff
- run: | - run: |
ruff check . ruff check .
ruff fix .
# - uses: stefanzweifel/git-auto-commit-action@v4 # - uses: stefanzweifel/git-auto-commit-action@v4
# with: # with:
# commit_message: 'style fixes by ruff' # commit_message: 'style fixes by ruff'
+4
View File
@@ -2,6 +2,10 @@
*.swp *.swp
*~ *~
*.pyc *.pyc
__pycache__/
.venv/
.ruff_cache/
.mypy_cache/
/tasks.sqlite /tasks.sqlite
/tasks.sqlite-wal /tasks.sqlite-wal
/srvinstallation /srvinstallation
+13
View File
@@ -0,0 +1,13 @@
DL3008failure-threshold: warning
format: tty
ignored:
- DL3007
override:
error:
- DL3015
warning:
- DL3015
info:
- DL3008
style:
- DL3015
+16
View File
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.1
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
+5
View File
@@ -0,0 +1,5 @@
{
"conventionalCommits.scopes": [
"tisbackup"
]
}
Executable
+27
View File
@@ -0,0 +1,27 @@
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /opt/tisbackup
COPY entrypoint.sh /entrypoint.sh
COPY . /opt/tisbackup
ENV DEBIAN_FRONTEND=noninteractive
ENV UV_PROJECT_ENVIRONMENT=/usr/local
ENV UV_PYTHON_DOWNLOADS=never
RUN apt-get update && apt-get upgrade -y \
&& apt-get install --no-install-recommends -y rsync ssh cron \
&& rm -rf /var/lib/apt/lists/* \
#&& /usr/local/bin/python3.13 -m pip install --no-cache-dir -r requirements.txt \
&& uv sync --locked --no-dev --no-install-project \
&& rm -f /bin/uv /bin/uvx \
&& mkdir -p /var/spool/cron/crontabs \
&& echo '59 03 * * * root /bin/bash /opt/tisbackup/backup.sh' > /etc/crontab \
&& echo '' >> /etc/crontab \
&& crontab /etc/crontab
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/local/bin/python3.13","/opt/tisbackup/tisbackup_gui.py"]
+139 -4
View File
@@ -1,10 +1,145 @@
# TISBackup
This is the repository of the TISBackup project, licensed under GPLv3. This is the repository of the TISBackup project, licensed under GPLv3.
TISBackup is a python script that the backup server runs TISBackup is a python script to backup servers.
at regular intervals to retrieve different data types on remote hosts
It runs at regular intervals to retrieve different data types on remote hosts
such as database dumps, files, virtual machine images and metadata. such as database dumps, files, virtual machine images and metadata.
:ref:`Tranquil IT <contact_tranquil_it>` is the original author of TISBackup. ## Install using Compose
Clone that repository and build the pod image using the provided `Dockerfile`
```bash
docker build . -t tisbackup:latest
```
In another folder, create subfolders as following
```bash
mkdir -p /var/tisbackup/{backup/log,config,ssh}/
```
Expected structure
```
/var/tisbackup/
└─backup/ <-- backup location
└─config/
├── tisbackup-config.ini <-- backups config
└── tisbackup_gui.ini <-- tisbackup config
└─ssh/
├── id_rsa <-- SSH Key
└── id_rsa.pub <-- SSH PubKey
compose.yaml
```
Adapt the compose.yml file to suits your needs, one pod act as the WebUI front end and the other as the crond scheduler
```yaml
services:
tisbackup_gui:
container_name: tisbackup_gui
image: "tisbackup:latest"
build: .
volumes:
- ./config/:/etc/tis/
- ./backup/:/backup/
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
ports:
- 9980:8080
tisbackup_cron:
container_name: tisbackup_cron
image: "tisbackup:latest"
build: .
volumes:
- ./config/:/etc/tis/
- ./ssh/:/config_ssh/
- ./backup/:/backup/
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
restart: always
command: "/bin/bash /opt/tisbackup/cron.sh"
```
## Configuration
* Provide an SSH key and store it in `./ssh`
* Setup config files in the `./config` directory
**tisbackup-config.ini**
```ini
[global]
backup_base_dir = /backup/
# backup retention in days
backup_retention_time=90
# for nagios check in hours
maximum_backup_age=30
[srvads-poudlard-samba]
type=rsync+ssh
server_name=srvads.poudlard.lan
remote_dir=/var/lib/samba/
compression=True
;exclude_list="/proc/**","/sys/**","/dev/**"
private_key=/config_ssh/id_rsa
ssh_port = 22
```
**tisbackup_gui.ini**
```ini
[general]
config_tisbackup= /etc/tis/tisbackup-config.ini
sections=
ADMIN_EMAIL=josebove@internet.fr
base_config_dir= /etc/tis/
backup_base_dir=/backup/
```
Run!
```bash
docker compose up -d
```
## NGINX reverse-proxy
Sample config file
```nginx
server {
listen 443 ssl http2;
# Remove '#' in the next line to enable IPv6
# listen [::]:443 ssl http2;
server_name tisbackup.poudlard.lan;
ssl_certificate /etc/letsencrypt/live/tisbackup.poudlard.lan/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/tisbackup.poudlard.lan/privkey.pem; # managed by Certbot
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://localhost:9980/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
## About
[Tranquil IT](contact_at_tranquil_it) is the original author of TISBackup.
The documentation is provided under the license CC-BY-SA and can be found The documentation is provided under the license CC-BY-SA and can be found
on `readthedoc <https://tisbackup.readthedocs.io/en/latest/index.html>`_. on [readthedoc](https://tisbackup.readthedocs.io/en/latest/index.html).
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -x
echo "Starting cleanup job for TIS Backup"
/usr/local/bin/python3.13 /opt/tisbackup/tisbackup.py backup
/usr/local/bin/python3.13 /opt/tisbackup/tisbackup.py cleanup
Executable
+41
View File
@@ -0,0 +1,41 @@
services:
tisbackup_gui:
container_name: tisbackup_gui
image: "tisbackup:latest"
build: .
volumes:
- ./config/:/etc/tis/
- ./backup/:/backup/
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
ports:
- 9980:8080
deploy:
resources:
limits:
cpus: 0.50
memory: 512M
reservations:
cpus: 0.25
memory: 128M
tisbackup_cron:
container_name: tisbackup_cron
image: "tisbackup:latest"
build: .
volumes:
- ./config/:/etc/tis/
- ./ssh/:/config_ssh/
- ./backup/:/backup/
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
restart: always
command: "/bin/bash /opt/tisbackup/cron.sh"
deploy:
resources:
limits:
cpus: 0.50
memory: 512M
reservations:
cpus: 0.25
memory: 128M
Regular → Executable
+5 -6
View File
@@ -1,10 +1,9 @@
import os,sys import os
from huey.backends.sqlite_backend import SqliteQueue,SqliteDataStore import sys
from huey.api import Huey, create_task
from huey.contrib.sql_huey import SqlHuey
from huey.storage import SqliteStorage
tisbackup_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__))) tisbackup_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
tasks_db = os.path.join(tisbackup_root_dir, "tasks.sqlite") tasks_db = os.path.join(tisbackup_root_dir, "tasks.sqlite")
queue = SqliteQueue('tisbackups',tasks_db) huey = SqlHuey(name="tisbackups", filename=tasks_db, always_eager=False, storage_class=SqliteStorage)
result_store = SqliteDataStore('tisbackups',tasks_db)
huey = Huey(queue,result_store,always_eager=False)
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -x
echo "Starting cron job for TIS Backup"
cron -f -l 2
-2
View File
@@ -52,5 +52,3 @@ The documentation for tisbackup is here: [tisbackup doc](https://tisbackup.readt
dpkg --force-all --purge tis-tisbackup dpkg --force-all --purge tis-tisbackup
apt autoremove apt autoremove
``` ```
-1
View File
@@ -7,4 +7,3 @@ Depends: unzip, ssh, rsync, python3-paramiko, python3-pyvmomi, python3-pexpect,
Maintainer: Tranquil-IT <technique@tranquil.it> Maintainer: Tranquil-IT <technique@tranquil.it>
Description: TISBackup backup management Description: TISBackup backup management
Homepage: https://www.tranquil.it Homepage: https://www.tranquil.it
-2
View File
@@ -32,5 +32,3 @@ rsync -aP ../samples/tisbackup-config.ini.sample ./builddir/etc/tis/tisbackup-c
chmod 755 ./builddir/opt/tisbackup/tisbackup.py chmod 755 ./builddir/opt/tisbackup/tisbackup.py
dpkg-deb --build builddir tis-tisbackup-1-${VERSION}.deb dpkg-deb --build builddir tis-tisbackup-1-${VERSION}.deb
+43 -44
View File
@@ -30,50 +30,50 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.doctest', "sphinx.ext.doctest",
'sphinx.ext.intersphinx', "sphinx.ext.intersphinx",
'sphinx.ext.todo', "sphinx.ext.todo",
'sphinx.ext.viewcode', "sphinx.ext.viewcode",
'sphinx.ext.githubpages', "sphinx.ext.githubpages",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# #
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = 'TISBackup' project = "TISBackup"
copyright = '2020, Tranquil IT' copyright = "2020, Tranquil IT"
author = 'Tranquil IT' author = "Tranquil IT"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.8' version = "1.8"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.8.2' release = "1.8.2"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = 'en' language = "en"
locale_dirs = ['locale/'] locale_dirs = ["locale/"]
gettext_compact = False gettext_compact = False
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
@@ -110,7 +110,7 @@ exclude_patterns = []
# show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] # modindex_common_prefix = []
@@ -126,18 +126,19 @@ todo_include_todos = True
try: try:
import sphinx_rtd_theme import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme" html_theme = "sphinx_rtd_theme"
html_favicon = "_static/favicon.ico" html_favicon = "_static/favicon.ico"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_context = { html_context = {
'css_files': [ "css_files": [
'_static/css/custom.css', # overrides for wide tables in RTD theme "_static/css/custom.css", # overrides for wide tables in RTD theme
'_static/css/ribbon.css', "_static/css/ribbon.css",
'_static/theme_overrides.css', # override wide tables in RTD theme "_static/theme_overrides.css", # override wide tables in RTD theme
], ],
} }
except ImportError as e: except ImportError as e: # noqa : F841
html_theme = 'alabaster' html_theme = "alabaster"
html_theme_path = [] html_theme_path = []
@@ -178,7 +179,7 @@ except ImportError as e:
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or # Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied # .htaccess) here, relative to this directory. These files are copied
@@ -258,15 +259,13 @@ html_static_path = ['_static']
# html_search_scorer = 'scorer.js' # html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'tisbackupdoc' htmlhelp_basename = "tisbackupdoc"
# -- Linkcheck ------------------- # -- Linkcheck -------------------
# make linkcheck # make linkcheck
# URL patterns to ignore # URL patterns to ignore
linkcheck_ignore = [r'http.*://.*mydomain.lan.*', linkcheck_ignore = [r"http.*://.*mydomain.lan.*", r"http.*://.*host_fqdn.*", r"http://user:pwd@host_fqdn:port"]
r'http.*://.*host_fqdn.*',
r'http://user:pwd@host_fqdn:port']
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@@ -282,20 +281,17 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
'papersize': 'lulupaper', "papersize": "lulupaper",
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
'pointsize': '9pt', "pointsize": "9pt",
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
'preamble': r'\batchmode', "preamble": r"\batchmode",
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
'sphinxsetup': 'hmargin={1.5cm,1.5cm}, vmargin={3cm,3cm}, marginpar=1cm', "sphinxsetup": "hmargin={1.5cm,1.5cm}, vmargin={3cm,3cm}, marginpar=1cm",
} }
@@ -303,7 +299,7 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'tisbackup.tex', 'TISBackup Documentation', 'Tranquil IT', 'manual'), (master_doc, "tisbackup.tex", "TISBackup Documentation", "Tranquil IT", "manual"),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@@ -343,10 +339,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, "tisbackup", "TISBackup Documentation", [author], 1)]
(master_doc, 'tisbackup', 'TISBackup Documentation',
[author], 1)
]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
# #
@@ -359,9 +352,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'tisbackup', 'TISBackup Documentation', (
author, 'Tranquil IT', 'The objective of TISbackup is to benefit from file backups and centralized alert feedback on "reasonable" data volumes.', master_doc,
'Miscellaneous'), "tisbackup",
"TISBackup Documentation",
author,
"Tranquil IT",
'The objective of TISbackup is to benefit from file backups and centralized alert feedback on "reasonable" data volumes.',
"Miscellaneous",
),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
@@ -382,7 +381,7 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None} intersphinx_mapping = {"https://docs.python.org/": None}
# -- Options for Epub output ---------------------------------------------- # -- Options for Epub output ----------------------------------------------
@@ -438,7 +437,7 @@ epub_copyright = copyright
# epub_post_files = [] # epub_post_files = []
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ["search.html"]
# The depth of the table of contents in toc.ncx. # The depth of the table of contents in toc.ncx.
# #
-2
View File
@@ -293,5 +293,3 @@ function splitQuery(query) {
} }
return result; return result;
} }
Executable
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
env >> /etc/environment
# execute CMD
echo "$@"
exec "$@"
+47 -47
View File
@@ -55,15 +55,17 @@
# -------------------------------------------------------------------- # --------------------------------------------------------------------
import gettext import gettext
import six.moves.xmlrpc_client as xmlrpclib
import six.moves.http_client as httplib
import socket import socket
import sys import sys
translation = gettext.translation('xen-xm', fallback = True) import six.moves.http_client as httplib
import six.moves.xmlrpc_client as xmlrpclib
translation = gettext.translation("xen-xm", fallback=True)
API_VERSION_1_1 = "1.1"
API_VERSION_1_2 = "1.2"
API_VERSION_1_1 = '1.1'
API_VERSION_1_2 = '1.2'
class Failure(Exception): class Failure(Exception):
def __init__(self, details): def __init__(self, details):
@@ -78,41 +80,48 @@ class Failure(Exception):
return msg return msg
def _details_map(self): def _details_map(self):
return dict([(str(i), self.details[i]) return dict([(str(i), self.details[i]) for i in range(len(self.details))])
for i in range(len(self.details))])
# Just a "constant" that we use to decide whether to retry the RPC # Just a "constant" that we use to decide whether to retry the RPC
_RECONNECT_AND_RETRY = object() _RECONNECT_AND_RETRY = object()
class UDSHTTPConnection(httplib.HTTPConnection): class UDSHTTPConnection(httplib.HTTPConnection):
"""HTTPConnection subclass to allow HTTP over Unix domain sockets.""" """HTTPConnection subclass to allow HTTP over Unix domain sockets."""
def connect(self): def connect(self):
path = self.host.replace("_", "/") path = self.host.replace("_", "/")
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path) self.sock.connect(path)
class UDSHTTP(httplib.HTTPConnection): class UDSHTTP(httplib.HTTPConnection):
_connection_class = UDSHTTPConnection _connection_class = UDSHTTPConnection
class UDSTransport(xmlrpclib.Transport): class UDSTransport(xmlrpclib.Transport):
def __init__(self, use_datetime=0): def __init__(self, use_datetime=0):
self._use_datetime = use_datetime self._use_datetime = use_datetime
self._extra_headers = [] self._extra_headers = []
self._connection = (None, None) self._connection = (None, None)
def add_extra_header(self, key, value): def add_extra_header(self, key, value):
self._extra_headers += [(key, value)] self._extra_headers += [(key, value)]
def make_connection(self, host): def make_connection(self, host):
# Python 2.4 compatibility # Python 2.4 compatibility
if sys.version_info[0] <= 2 and sys.version_info[1] < 7: if sys.version_info[0] <= 2 and sys.version_info[1] < 7:
return UDSHTTP(host) return UDSHTTP(host)
else: else:
return UDSHTTPConnection(host) return UDSHTTPConnection(host)
def send_request(self, connection, handler, request_body): def send_request(self, connection, handler, request_body):
connection.putrequest("POST", handler) connection.putrequest("POST", handler)
for key, value in self._extra_headers: for key, value in self._extra_headers:
connection.putheader(key, value) connection.putheader(key, value)
class Session(xmlrpclib.ServerProxy): class Session(xmlrpclib.ServerProxy):
"""A server proxy and session manager for communicating with xapi using """A server proxy and session manager for communicating with xapi using
the Xen-API. the Xen-API.
@@ -125,32 +134,27 @@ class Session(xmlrpclib.ServerProxy):
session.xenapi.session.logout() session.xenapi.session.logout()
""" """
def __init__(self, uri, transport=None, encoding=None, verbose=0, def __init__(self, uri, transport=None, encoding=None, verbose=0, allow_none=1, ignore_ssl=False):
allow_none=1, ignore_ssl=False):
# Fix for CA-172901 (+ Python 2.4 compatibility) # Fix for CA-172901 (+ Python 2.4 compatibility)
# Fix for context=ctx ( < Python 2.7.9 compatibility) # Fix for context=ctx ( < Python 2.7.9 compatibility)
if not (sys.version_info[0] <= 2 and sys.version_info[1] <= 7 and sys.version_info[2] <= 9 ) \ if not (sys.version_info[0] <= 2 and sys.version_info[1] <= 7 and sys.version_info[2] <= 9) and ignore_ssl:
and ignore_ssl:
import ssl import ssl
ctx = ssl._create_unverified_context() ctx = ssl._create_unverified_context()
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, verbose, allow_none, context=ctx)
verbose, allow_none, context=ctx)
else: else:
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, verbose, allow_none)
verbose, allow_none)
self.transport = transport self.transport = transport
self._session = None self._session = None
self.last_login_method = None self.last_login_method = None
self.last_login_params = None self.last_login_params = None
self.API_version = API_VERSION_1_1 self.API_version = API_VERSION_1_1
def xenapi_request(self, methodname, params): def xenapi_request(self, methodname, params):
if methodname.startswith('login'): if methodname.startswith("login"):
self._login(methodname, params) self._login(methodname, params)
return None return None
elif methodname == 'logout' or methodname == 'session.logout': elif methodname == "logout" or methodname == "session.logout":
self._logout() self._logout()
return None return None
else: else:
@@ -161,29 +165,25 @@ class Session(xmlrpclib.ServerProxy):
if result is _RECONNECT_AND_RETRY: if result is _RECONNECT_AND_RETRY:
retry_count += 1 retry_count += 1
if self.last_login_method: if self.last_login_method:
self._login(self.last_login_method, self._login(self.last_login_method, self.last_login_params)
self.last_login_params)
else: else:
raise xmlrpclib.Fault(401, 'You must log in') raise xmlrpclib.Fault(401, "You must log in")
else: else:
return result return result
raise xmlrpclib.Fault( raise xmlrpclib.Fault(500, "Tried 3 times to get a valid session, but failed")
500, 'Tried 3 times to get a valid session, but failed')
def _login(self, method, params): def _login(self, method, params):
try: try:
result = _parse_result( result = _parse_result(getattr(self, "session.%s" % method)(*params))
getattr(self, 'session.%s' % method)(*params))
if result is _RECONNECT_AND_RETRY: if result is _RECONNECT_AND_RETRY:
raise xmlrpclib.Fault( raise xmlrpclib.Fault(500, "Received SESSION_INVALID when logging in")
500, 'Received SESSION_INVALID when logging in')
self._session = result self._session = result
self.last_login_method = method self.last_login_method = method
self.last_login_params = params self.last_login_params = params
self.API_version = self._get_api_version() self.API_version = self._get_api_version()
except socket.error as e: except socket.error as e:
if e.errno == socket.errno.ETIMEDOUT: if e.errno == socket.errno.ETIMEDOUT:
raise xmlrpclib.Fault(504, 'The connection timed out') raise xmlrpclib.Fault(504, "The connection timed out")
else: else:
raise e raise e
@@ -207,38 +207,38 @@ class Session(xmlrpclib.ServerProxy):
return "%s.%s" % (major, minor) return "%s.%s" % (major, minor)
def __getattr__(self, name): def __getattr__(self, name):
if name == 'handle': if name == "handle":
return self._session return self._session
elif name == 'xenapi': elif name == "xenapi":
return _Dispatcher(self.API_version, self.xenapi_request, None) return _Dispatcher(self.API_version, self.xenapi_request, None)
elif name.startswith('login') or name.startswith('slave_local'): elif name.startswith("login") or name.startswith("slave_local"):
return lambda *params: self._login(name, params) return lambda *params: self._login(name, params)
elif name == 'logout': elif name == "logout":
return _Dispatcher(self.API_version, self.xenapi_request, "logout") return _Dispatcher(self.API_version, self.xenapi_request, "logout")
else: else:
return xmlrpclib.ServerProxy.__getattr__(self, name) return xmlrpclib.ServerProxy.__getattr__(self, name)
def xapi_local(): def xapi_local():
return Session("http://_var_lib_xcp_xapi/", transport=UDSTransport()) return Session("http://_var_lib_xcp_xapi/", transport=UDSTransport())
def _parse_result(result): def _parse_result(result):
if type(result) != dict or 'Status' not in result: if not isinstance(type(result), dict) or "Status" not in result:
raise xmlrpclib.Fault(500, 'Missing Status in response from server' + result) raise xmlrpclib.Fault(500, "Missing Status in response from server" + result)
if result['Status'] == 'Success': if result["Status"] == "Success":
if 'Value' in result: if "Value" in result:
return result['Value'] return result["Value"]
else: else:
raise xmlrpclib.Fault(500, raise xmlrpclib.Fault(500, "Missing Value in response from server")
'Missing Value in response from server')
else: else:
if 'ErrorDescription' in result: if "ErrorDescription" in result:
if result['ErrorDescription'][0] == 'SESSION_INVALID': if result["ErrorDescription"][0] == "SESSION_INVALID":
return _RECONNECT_AND_RETRY return _RECONNECT_AND_RETRY
else: else:
raise Failure(result['ErrorDescription']) raise Failure(result["ErrorDescription"])
else: else:
raise xmlrpclib.Fault( raise xmlrpclib.Fault(500, "Missing ErrorDescription in response from server")
500, 'Missing ErrorDescription in response from server')
# Based upon _Method from xmlrpclib. # Based upon _Method from xmlrpclib.
@@ -250,9 +250,9 @@ class _Dispatcher:
def __repr__(self): def __repr__(self):
if self.__name: if self.__name:
return '<XenAPI._Dispatcher for %s>' % self.__name return "<XenAPI._Dispatcher for %s>" % self.__name
else: else:
return '<XenAPI._Dispatcher>' return "<XenAPI._Dispatcher>"
def __getattr__(self, name): def __getattr__(self, name):
if self.__name is None: if self.__name is None:
-1
View File
@@ -15,4 +15,3 @@
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>. # along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+75 -55
View File
@@ -19,10 +19,10 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import sys import sys
try: try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko sys.stderr = open("/dev/null") # Silence silly warnings from paramiko
import paramiko import paramiko
except ImportError as e: except ImportError as e:
print(("Error : can not load paramiko library %s" % e)) print(("Error : can not load paramiko library %s" % e))
@@ -32,31 +32,32 @@ sys.stderr = sys.__stderr__
from libtisbackup.common import * from libtisbackup.common import *
class backup_mysql(backup_generic): class backup_mysql(backup_generic):
"""Backup a mysql database as gzipped sql file through ssh""" """Backup a mysql database as gzipped sql file through ssh"""
type = 'mysql+ssh'
required_params = backup_generic.required_params + ['db_user','db_passwd','private_key']
optional_params = backup_generic.optional_params + ['db_name']
db_name='' type = "mysql+ssh"
db_user='' required_params = backup_generic.required_params + ["db_user", "db_passwd", "private_key"]
db_passwd='' optional_params = backup_generic.optional_params + ["db_name"]
db_name = ""
db_user = ""
db_passwd = ""
dest_dir = "" dest_dir = ""
def do_backup(self, stats): def do_backup(self, stats):
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date) self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir): if not os.path.isdir(self.dest_dir):
if not self.dry_run: if not self.dry_run:
os.makedirs(self.dest_dir) os.makedirs(self.dest_dir)
else: else:
print(('mkdir "%s"' % self.dest_dir)) print(('mkdir "%s"' % self.dest_dir))
else: else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir) raise Exception("backup destination directory already exists : %s" % self.dest_dir)
self.logger.debug('[%s] Connecting to %s with user root and key %s',self.backup_name,self.server_name,self.private_key) self.logger.debug("[%s] Connecting to %s with user root and key %s", self.backup_name, self.server_name, self.private_key)
try: try:
mykey = paramiko.RSAKey.from_private_key_file(self.private_key) mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
except paramiko.SSHException: except paramiko.SSHException:
@@ -65,40 +66,48 @@ class backup_mysql(backup_generic):
self.ssh = paramiko.SSHClient() self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name,username='root',pkey = mykey, port=self.ssh_port) self.ssh.connect(self.server_name, username="root", pkey=mykey, port=self.ssh_port)
self.db_passwd=self.db_passwd.replace('$','\$') self.db_passwd = self.db_passwd.replace("$", r"\$")
if not self.db_name: if not self.db_name:
stats['log']= "Successfully backuping processed to the following databases :" stats["log"] = "Successfully backuping processed to the following databases :"
stats['status']='List' stats["status"] = "List"
cmd = 'mysql -N -B -p -e "SHOW DATABASES;" -u ' + self.db_user +' -p' + self.db_passwd + ' 2> /dev/null' cmd = 'mysql -N -B -p -e "SHOW DATABASES;" -u ' + self.db_user + " -p" + self.db_passwd + " 2> /dev/null"
self.logger.debug('[%s] List databases: %s',self.backup_name,cmd) self.logger.debug("[%s] List databases: %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
databases = output.split('\n') databases = output.split("\n")
for database in databases: for database in databases:
if database != "": if database != "":
self.db_name = database.rstrip() self.db_name = database.rstrip()
self.do_mysqldump(stats) self.do_mysqldump(stats)
else: else:
stats['log']= "Successfully backup processed to the following database :" stats["log"] = "Successfully backup processed to the following database :"
self.do_mysqldump(stats) self.do_mysqldump(stats)
def do_mysqldump(self, stats): def do_mysqldump(self, stats):
t = datetime.datetime.now() t = datetime.datetime.now()
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S') backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
# dump db # dump db
stats['status']='Dumping' stats["status"] = "Dumping"
cmd = 'mysqldump --single-transaction -u' + self.db_user +' -p' + self.db_passwd + ' ' + self.db_name + ' > /tmp/' + self.db_name + '-' + backup_start_date + '.sql' cmd = (
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd) "mysqldump --single-transaction -u"
+ self.db_user
+ " -p"
+ self.db_passwd
+ " "
+ self.db_name
+ " > /tmp/"
+ self.db_name
+ "-"
+ backup_start_date
+ ".sql"
)
self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
print(output) print(output)
@@ -107,9 +116,9 @@ class backup_mysql(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# zip the file # zip the file
stats['status']='Zipping' stats["status"] = "Zipping"
cmd = 'gzip /tmp/' + self.db_name + '-' + backup_start_date + '.sql' cmd = "gzip /tmp/" + self.db_name + "-" + backup_start_date + ".sql"
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd) self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
@@ -117,10 +126,10 @@ class backup_mysql(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# get the file # get the file
stats['status']='SFTP' stats["status"] = "SFTP"
filepath = '/tmp/' + self.db_name + '-' + backup_start_date + '.sql.gz' filepath = "/tmp/" + self.db_name + "-" + backup_start_date + ".sql.gz"
localpath = os.path.join(self.dest_dir , self.db_name + '.sql.gz') localpath = os.path.join(self.dest_dir, self.db_name + ".sql.gz")
self.logger.debug('[%s] Get gz backup with sftp on %s from %s to %s',self.backup_name,self.server_name,filepath,localpath) self.logger.debug("[%s] Get gz backup with sftp on %s from %s to %s", self.backup_name, self.server_name, filepath, localpath)
if not self.dry_run: if not self.dry_run:
transport = self.ssh.get_transport() transport = self.ssh.get_transport()
sftp = paramiko.SFTPClient.from_transport(transport) sftp = paramiko.SFTPClient.from_transport(transport)
@@ -128,52 +137,63 @@ class backup_mysql(backup_generic):
sftp.close() sftp.close()
if not self.dry_run: if not self.dry_run:
stats['total_files_count']=1 + stats.get('total_files_count', 0) stats["total_files_count"] = 1 + stats.get("total_files_count", 0)
stats['written_files_count']=1 + stats.get('written_files_count', 0) stats["written_files_count"] = 1 + stats.get("written_files_count", 0)
stats['total_bytes']=os.stat(localpath).st_size + stats.get('total_bytes', 0) stats["total_bytes"] = os.stat(localpath).st_size + stats.get("total_bytes", 0)
stats['written_bytes']=os.stat(localpath).st_size + stats.get('written_bytes', 0) stats["written_bytes"] = os.stat(localpath).st_size + stats.get("written_bytes", 0)
stats['log'] = '%s "%s"' % (stats['log'] ,self.db_name) stats["log"] = '%s "%s"' % (stats["log"], self.db_name)
stats['backup_location'] = self.dest_dir stats["backup_location"] = self.dest_dir
stats['status']='RMTemp' stats["status"] = "RMTemp"
cmd = 'rm -f /tmp/' + self.db_name + '-' + backup_start_date + '.sql.gz' cmd = "rm -f /tmp/" + self.db_name + "-" + backup_start_date + ".sql.gz"
self.logger.debug('[%s] Remove temp gzip : %s',self.backup_name,cmd) self.logger.debug("[%s] Remove temp gzip : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
stats['status']='OK' stats["status"] = "OK"
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') p = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
for item in filelist: for item in filelist:
if p.match(item): if p.match(item):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(item, "%Y%m%d-%Hh%Mm%S").isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
register_driver(backup_mysql) register_driver(backup_mysql)
+12 -5
View File
@@ -18,34 +18,41 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os
import datetime import datetime
import os
from .common import * from .common import *
class backup_null(backup_generic): class backup_null(backup_generic):
"""Null backup to register servers which don't need any backups """Null backup to register servers which don't need any backups
but we still want to know they are taken in account""" but we still want to know they are taken in account"""
type = 'null'
required_params = ['type','server_name','backup_name'] type = "null"
required_params = ["type", "server_name", "backup_name"]
optional_params = [] optional_params = []
def do_backup(self, stats): def do_backup(self, stats):
pass pass
def process_backup(self): def process_backup(self):
pass pass
def cleanup_backup(self): def cleanup_backup(self):
pass pass
def register_existingbackups(self): def register_existingbackups(self):
pass pass
def export_latestbackup(self, destdir): def export_latestbackup(self, destdir):
return {} return {}
def checknagios(self, maxage_hours=30): def checknagios(self, maxage_hours=30):
return (nagiosStateOk, "No backups needs to be performed") return (nagiosStateOk, "No backups needs to be performed")
register_driver(backup_null) register_driver(backup_null)
if __name__=='__main__': if __name__ == "__main__":
pass pass
+73 -52
View File
@@ -18,8 +18,9 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import sys import sys
try: try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko sys.stderr = open("/dev/null") # Silence silly warnings from paramiko
import paramiko import paramiko
except ImportError as e: except ImportError as e:
print(("Error : can not load paramiko library %s" % e)) print(("Error : can not load paramiko library %s" % e))
@@ -27,25 +28,29 @@ except ImportError as e:
sys.stderr = sys.__stderr__ sys.stderr = sys.__stderr__
import datetime
import base64 import base64
import datetime
import os import os
from libtisbackup.common import *
import re import re
from libtisbackup.common import *
class backup_oracle(backup_generic): class backup_oracle(backup_generic):
"""Backup a oracle database as zipped file through ssh""" """Backup a oracle database as zipped file through ssh"""
type = 'oracle+ssh'
required_params = backup_generic.required_params + ['db_name','private_key', 'userid'] type = "oracle+ssh"
optional_params = ['username', 'remote_backup_dir', 'ignore_error_oracle_code'] required_params = backup_generic.required_params + ["db_name", "private_key", "userid"]
db_name='' optional_params = ["username", "remote_backup_dir", "ignore_error_oracle_code"]
username='oracle' db_name = ""
remote_backup_dir = r'/home/oracle/backup' username = "oracle"
remote_backup_dir = r"/home/oracle/backup"
ignore_error_oracle_code = [] ignore_error_oracle_code = []
def do_backup(self, stats): def do_backup(self, stats):
self.logger.debug(
self.logger.debug('[%s] Connecting to %s with user %s and key %s',self.backup_name,self.server_name,self.username,self.private_key) "[%s] Connecting to %s with user %s and key %s", self.backup_name, self.server_name, self.username, self.private_key
)
try: try:
mykey = paramiko.RSAKey.from_private_key_file(self.private_key) mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
except paramiko.SSHException: except paramiko.SSHException:
@@ -57,9 +62,9 @@ class backup_oracle(backup_generic):
self.ssh.connect(self.server_name, username=self.username, pkey=mykey, port=self.ssh_port) self.ssh.connect(self.server_name, username=self.username, pkey=mykey, port=self.ssh_port)
t = datetime.datetime.now() t = datetime.datetime.now()
self.backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S') self.backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
dumpfile= self.remote_backup_dir + '/' + self.db_name + '_' + self.backup_start_date+'.dmp' dumpfile = self.remote_backup_dir + "/" + self.db_name + "_" + self.backup_start_date + ".dmp"
dumplog = self.remote_backup_dir + '/' + self.db_name + '_' + self.backup_start_date+'.log' dumplog = self.remote_backup_dir + "/" + self.db_name + "_" + self.backup_start_date + ".log"
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date) self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir): if not os.path.isdir(self.dest_dir):
@@ -68,17 +73,17 @@ class backup_oracle(backup_generic):
else: else:
print(('mkdir "%s"' % self.dest_dir)) print(('mkdir "%s"' % self.dest_dir))
else: else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir) raise Exception("backup destination directory already exists : %s" % self.dest_dir)
# dump db # dump db
stats['status']='Dumping' stats["status"] = "Dumping"
cmd = "exp '%s' file='%s' grants=y log='%s'" % (self.userid, dumpfile, dumplog) cmd = "exp '%s' file='%s' grants=y log='%s'" % (self.userid, dumpfile, dumplog)
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd) self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
localpath = os.path.join(self.dest_dir , self.db_name + '.log') localpath = os.path.join(self.dest_dir, self.db_name + ".log")
self.logger.debug('[%s] Get log file with sftp on %s from %s to %s',self.backup_name,self.server_name,dumplog,localpath) self.logger.debug("[%s] Get log file with sftp on %s from %s to %s", self.backup_name, self.server_name, dumplog, localpath)
transport = self.ssh.get_transport() transport = self.ssh.get_transport()
sftp = paramiko.SFTPClient.from_transport(transport) sftp = paramiko.SFTPClient.from_transport(transport)
sftp.get(dumplog, localpath) sftp.get(dumplog, localpath)
@@ -86,16 +91,21 @@ class backup_oracle(backup_generic):
file = open(localpath) file = open(localpath)
for line in file: for line in file:
if re.search('EXP-[0-9]+:', line) and not re.match('EXP-[0-9]+:', line).group(0).replace(':','') in self.ignore_error_oracle_code: if (
stats['status']='RMTemp' re.search("EXP-[0-9]+:", line)
and re.match("EXP-[0-9]+:", line).group(0).replace(":", "") not in self.ignore_error_oracle_code
):
stats["status"] = "RMTemp"
self.clean_dumpfiles(dumpfile, dumplog) self.clean_dumpfiles(dumpfile, dumplog)
raise Exception('Aborting, Not null exit code (%s) for "%s"' % (re.match('EXP-[0-9]+:', line).group(0).replace(':',''),cmd)) raise Exception(
'Aborting, Not null exit code (%s) for "%s"' % (re.match("EXP-[0-9]+:", line).group(0).replace(":", ""), cmd)
)
file.close() file.close()
# zip the file # zip the file
stats['status']='Zipping' stats["status"] = "Zipping"
cmd = 'gzip %s' % dumpfile cmd = "gzip %s" % dumpfile
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd) self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
@@ -103,10 +113,10 @@ class backup_oracle(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# get the file # get the file
stats['status']='SFTP' stats["status"] = "SFTP"
filepath = dumpfile + '.gz' filepath = dumpfile + ".gz"
localpath = os.path.join(self.dest_dir , self.db_name + '.dmp.gz') localpath = os.path.join(self.dest_dir, self.db_name + ".dmp.gz")
self.logger.debug('[%s] Get gz backup with sftp on %s from %s to %s',self.backup_name,self.server_name,filepath,localpath) self.logger.debug("[%s] Get gz backup with sftp on %s from %s to %s", self.backup_name, self.server_name, filepath, localpath)
if not self.dry_run: if not self.dry_run:
transport = self.ssh.get_transport() transport = self.ssh.get_transport()
sftp = paramiko.SFTPClient.from_transport(transport) sftp = paramiko.SFTPClient.from_transport(transport)
@@ -114,61 +124,72 @@ class backup_oracle(backup_generic):
sftp.close() sftp.close()
if not self.dry_run: if not self.dry_run:
stats['total_files_count']=1 stats["total_files_count"] = 1
stats['written_files_count']=1 stats["written_files_count"] = 1
stats['total_bytes']=os.stat(localpath).st_size stats["total_bytes"] = os.stat(localpath).st_size
stats['written_bytes']=os.stat(localpath).st_size stats["written_bytes"] = os.stat(localpath).st_size
stats['log']='gzip dump of DB %s:%s (%d bytes) to %s' % (self.server_name,self.db_name, stats['written_bytes'], localpath) stats["log"] = "gzip dump of DB %s:%s (%d bytes) to %s" % (self.server_name, self.db_name, stats["written_bytes"], localpath)
stats['backup_location'] = self.dest_dir stats["backup_location"] = self.dest_dir
stats['status']='RMTemp' stats["status"] = "RMTemp"
self.clean_dumpfiles(dumpfile, dumplog) self.clean_dumpfiles(dumpfile, dumplog)
stats['status']='OK' stats["status"] = "OK"
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') p = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
for item in filelist: for item in filelist:
if p.match(item): if p.match(item):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(item, "%Y%m%d-%Hh%Mm%S").isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
def clean_dumpfiles(self, dumpfile, dumplog): def clean_dumpfiles(self, dumpfile, dumplog):
cmd = 'rm -f "%s.gz" "%s"' % (dumpfile, dumplog) cmd = 'rm -f "%s.gz" "%s"' % (dumpfile, dumplog)
self.logger.debug('[%s] Remove temp gzip : %s',self.backup_name,cmd) self.logger.debug("[%s] Remove temp gzip : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
cmd = 'rm -f '+self.remote_backup_dir + '/' + self.db_name + '_' + self.backup_start_date+'.dmp' cmd = "rm -f " + self.remote_backup_dir + "/" + self.db_name + "_" + self.backup_start_date + ".dmp"
self.logger.debug('[%s] Remove temp dump : %s',self.backup_name,cmd) self.logger.debug("[%s] Remove temp dump : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
register_driver(backup_oracle) register_driver(backup_oracle)
+63 -50
View File
@@ -18,8 +18,9 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import sys import sys
try: try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko sys.stderr = open("/dev/null") # Silence silly warnings from paramiko
import paramiko import paramiko
except ImportError as e: except ImportError as e:
print(("Error : can not load paramiko library %s" % e)) print(("Error : can not load paramiko library %s" % e))
@@ -29,15 +30,17 @@ sys.stderr = sys.__stderr__
from .common import * from .common import *
class backup_pgsql(backup_generic): class backup_pgsql(backup_generic):
"""Backup a postgresql database as gzipped sql file through ssh""" """Backup a postgresql database as gzipped sql file through ssh"""
type = 'pgsql+ssh'
required_params = backup_generic.required_params + ['private_key']
optional_params = backup_generic.optional_params + ['db_name','tmp_dir','encoding']
db_name = '' type = "pgsql+ssh"
tmp_dir = '/tmp' required_params = backup_generic.required_params + ["private_key"]
encoding = 'UTF8' optional_params = backup_generic.optional_params + ["db_name", "tmp_dir", "encoding"]
db_name = ""
tmp_dir = "/tmp"
encoding = "UTF8"
def do_backup(self, stats): def do_backup(self, stats):
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date) self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
@@ -48,7 +51,7 @@ class backup_pgsql(backup_generic):
else: else:
print(('mkdir "%s"' % self.dest_dir)) print(('mkdir "%s"' % self.dest_dir))
else: else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir) raise Exception("backup destination directory already exists : %s" % self.dest_dir)
try: try:
mykey = paramiko.RSAKey.from_private_key_file(self.private_key) mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
@@ -56,48 +59,48 @@ class backup_pgsql(backup_generic):
# mykey = paramiko.DSSKey.from_private_key_file(self.private_key) # mykey = paramiko.DSSKey.from_private_key_file(self.private_key)
mykey = paramiko.Ed25519Key.from_private_key_file(self.private_key) mykey = paramiko.Ed25519Key.from_private_key_file(self.private_key)
self.logger.debug('[%s] Trying to connect to "%s" with username root and key "%s"',self.backup_name,self.server_name,self.private_key) self.logger.debug(
'[%s] Trying to connect to "%s" with username root and key "%s"', self.backup_name, self.server_name, self.private_key
)
self.ssh = paramiko.SSHClient() self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name,username='root',pkey = mykey,port=self.ssh_port) self.ssh.connect(self.server_name, username="root", pkey=mykey, port=self.ssh_port)
if self.db_name: if self.db_name:
stats['log']= "Successfully backup processed to the following database :" stats["log"] = "Successfully backup processed to the following database :"
self.do_pgsqldump(stats) self.do_pgsqldump(stats)
else: else:
stats['log']= "Successfully backuping processed to the following databases :" stats["log"] = "Successfully backuping processed to the following databases :"
stats['status']='List' stats["status"] = "List"
cmd = """su - postgres -c 'psql -A -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;"' 2> /dev/null""" cmd = """su - postgres -c 'psql -A -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;"' 2> /dev/null"""
self.logger.debug('[%s] List databases: %s',self.backup_name,cmd) self.logger.debug("[%s] List databases: %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
databases = output.split('\n') databases = output.split("\n")
for database in databases: for database in databases:
if database.strip() not in ("", "template0", "template1"): if database.strip() not in ("", "template0", "template1"):
self.db_name = database.strip() self.db_name = database.strip()
self.do_pgsqldump(stats) self.do_pgsqldump(stats)
stats["status"] = "OK"
stats['status']='OK'
def do_pgsqldump(self, stats): def do_pgsqldump(self, stats):
t = datetime.datetime.now() t = datetime.datetime.now()
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S') backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
params = { params = {
'encoding':self.encoding, "encoding": self.encoding,
'db_name':self.db_name, "db_name": self.db_name,
'tmp_dir':self.tmp_dir, "tmp_dir": self.tmp_dir,
'dest_dir':self.dest_dir, "dest_dir": self.dest_dir,
'backup_start_date':backup_start_date} "backup_start_date": backup_start_date,
}
# dump db # dump db
filepath = '%(tmp_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz' % params filepath = "%(tmp_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz" % params
cmd = "su - postgres -c 'pg_dump -E %(encoding)s -Z9 %(db_name)s'" % params cmd = "su - postgres -c 'pg_dump -E %(encoding)s -Z9 %(db_name)s'" % params
cmd += ' > ' + filepath cmd += " > " + filepath
self.logger.debug('[%s] %s ',self.backup_name,cmd) self.logger.debug("[%s] %s ", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
@@ -105,7 +108,7 @@ class backup_pgsql(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# get the file # get the file
localpath = '%(dest_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz' % params localpath = "%(dest_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz" % params
self.logger.debug('[%s] get the file using sftp from "%s" to "%s" ', self.backup_name, filepath, localpath) self.logger.debug('[%s] get the file using sftp from "%s" to "%s" ', self.backup_name, filepath, localpath)
if not self.dry_run: if not self.dry_run:
transport = self.ssh.get_transport() transport = self.ssh.get_transport()
@@ -114,51 +117,61 @@ class backup_pgsql(backup_generic):
sftp.close() sftp.close()
if not self.dry_run: if not self.dry_run:
stats['total_files_count']=1 + stats.get('total_files_count', 0) stats["total_files_count"] = 1 + stats.get("total_files_count", 0)
stats['written_files_count']=1 + stats.get('written_files_count', 0) stats["written_files_count"] = 1 + stats.get("written_files_count", 0)
stats['total_bytes']=os.stat(localpath).st_size + stats.get('total_bytes', 0) stats["total_bytes"] = os.stat(localpath).st_size + stats.get("total_bytes", 0)
stats['written_bytes']=os.stat(localpath).st_size + stats.get('written_bytes', 0) stats["written_bytes"] = os.stat(localpath).st_size + stats.get("written_bytes", 0)
stats['log'] = '%s "%s"' % (stats['log'] ,self.db_name) stats["log"] = '%s "%s"' % (stats["log"], self.db_name)
stats['backup_location'] = self.dest_dir stats["backup_location"] = self.dest_dir
cmd = 'rm -f %(tmp_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz' % params cmd = "rm -f %(tmp_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz" % params
self.logger.debug('[%s] %s ',self.backup_name,cmd) self.logger.debug("[%s] %s ", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') p = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
for item in filelist: for item in filelist:
if p.match(item): if p.match(item):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(item, "%Y%m%d-%Hh%Mm%S").isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
register_driver(backup_pgsql) register_driver(backup_pgsql)
+150 -116
View File
@@ -18,43 +18,49 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os
import datetime import datetime
from libtisbackup.common import *
import time
import logging import logging
import re import os
import os.path import os.path
import datetime import re
import time
from libtisbackup.common import *
class backup_rsync(backup_generic): class backup_rsync(backup_generic):
"""Backup a directory on remote server with rsync and rsync protocol (requires running remote rsync daemon)""" """Backup a directory on remote server with rsync and rsync protocol (requires running remote rsync daemon)"""
type = 'rsync'
required_params = backup_generic.required_params + ['remote_user','remote_dir','rsync_module','password_file']
optional_params = backup_generic.optional_params + ['compressionlevel','compression','bwlimit','exclude_list','protect_args','overload_args']
remote_user='root' type = "rsync"
remote_dir='' required_params = backup_generic.required_params + ["remote_user", "remote_dir", "rsync_module", "password_file"]
optional_params = backup_generic.optional_params + [
"compressionlevel",
"compression",
"bwlimit",
"exclude_list",
"protect_args",
"overload_args",
]
exclude_list='' remote_user = "root"
rsync_module='' remote_dir = ""
password_file = ''
compression = '' exclude_list = ""
rsync_module = ""
password_file = ""
compression = ""
bwlimit = 0 bwlimit = 0
protect_args = '1' protect_args = "1"
overload_args = None overload_args = None
compressionlevel = 0 compressionlevel = 0
def read_config(self, iniconf): def read_config(self, iniconf):
assert(isinstance(iniconf,ConfigParser)) assert isinstance(iniconf, ConfigParser)
backup_generic.read_config(self, iniconf) backup_generic.read_config(self, iniconf)
if not self.bwlimit and iniconf.has_option('global','bw_limit'): if not self.bwlimit and iniconf.has_option("global", "bw_limit"):
self.bwlimit = iniconf.getint('global','bw_limit') self.bwlimit = iniconf.getint("global", "bw_limit")
if not self.compressionlevel and iniconf.has_option('global','compression_level'): if not self.compressionlevel and iniconf.has_option("global", "compression_level"):
self.compressionlevel = iniconf.getint('global','compression_level') self.compressionlevel = iniconf.getint("global", "compression_level")
def do_backup(self, stats): def do_backup(self, stats):
if not self.set_lock(): if not self.set_lock():
@@ -63,45 +69,45 @@ class backup_rsync(backup_generic):
try: try:
try: try:
backup_source = 'undefined' backup_source = "undefined"
dest_dir = os.path.join(self.backup_dir,self.backup_start_date+'.rsync/') dest_dir = os.path.join(self.backup_dir, self.backup_start_date + ".rsync/")
if not os.path.isdir(dest_dir): if not os.path.isdir(dest_dir):
if not self.dry_run: if not self.dry_run:
os.makedirs(dest_dir) os.makedirs(dest_dir)
else: else:
print(('mkdir "%s"' % dest_dir)) print(('mkdir "%s"' % dest_dir))
else: else:
raise Exception('backup destination directory already exists : %s' % dest_dir) raise Exception("backup destination directory already exists : %s" % dest_dir)
options = ['-rt','--stats','--delete-excluded','--numeric-ids','--delete-after'] options = ["-rt", "--stats", "--delete-excluded", "--numeric-ids", "--delete-after"]
if self.logger.level: if self.logger.level:
options.append('-P') options.append("-P")
if self.dry_run: if self.dry_run:
options.append('-d') options.append("-d")
if self.overload_args != None: if self.overload_args is not None:
options.append(self.overload_args) options.append(self.overload_args)
elif not "cygdrive" in self.remote_dir: elif "cygdrive" not in self.remote_dir:
# we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful # we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful
options.append('-lpgoD') options.append("-lpgoD")
# the protect-args option is not available in all rsync version # the protect-args option is not available in all rsync version
if not self.protect_args.lower() in ('false','no','0'): if self.protect_args.lower() not in ("false", "no", "0"):
options.append('--protect-args') options.append("--protect-args")
if self.compression.lower() in ('true','yes','1'): if self.compression.lower() in ("true", "yes", "1"):
options.append('-z') options.append("-z")
if self.compressionlevel: if self.compressionlevel:
options.append('--compress-level=%s' % self.compressionlevel) options.append("--compress-level=%s" % self.compressionlevel)
if self.bwlimit: if self.bwlimit:
options.append('--bwlimit %s' % self.bwlimit) options.append("--bwlimit %s" % self.bwlimit)
latest = self.get_latest_backup(self.backup_start_date) latest = self.get_latest_backup(self.backup_start_date)
if latest: if latest:
options.extend(['--link-dest="%s"' % os.path.join('..',b,'') for b in latest]) options.extend(['--link-dest="%s"' % os.path.join("..", b, "") for b in latest])
def strip_quotes(s): def strip_quotes(s):
if s[0] == '"': if s[0] == '"':
@@ -113,56 +119,62 @@ class backup_rsync(backup_generic):
# Add excludes # Add excludes
if "--exclude" in self.exclude_list: if "--exclude" in self.exclude_list:
# old settings with exclude_list=--exclude toto --exclude=titi # old settings with exclude_list=--exclude toto --exclude=titi
excludes = [strip_quotes(s).strip() for s in self.exclude_list.replace('--exclude=','').replace('--exclude ','').split()] excludes = [
strip_quotes(s).strip() for s in self.exclude_list.replace("--exclude=", "").replace("--exclude ", "").split()
]
else: else:
try: try:
# newsettings with exclude_list='too','titi', parsed as a str python list content # newsettings with exclude_list='too','titi', parsed as a str python list content
excludes = eval('[%s]' % self.exclude_list) excludes = eval("[%s]" % self.exclude_list)
except Exception as e: except Exception as e:
raise Exception('Error reading exclude list : value %s, eval error %s (don\'t forget quotes and comma...)' % (self.exclude_list,e)) raise Exception(
"Error reading exclude list : value %s, eval error %s (don't forget quotes and comma...)"
% (self.exclude_list, e)
)
options.extend(['--exclude="%s"' % x for x in excludes]) options.extend(['--exclude="%s"' % x for x in excludes])
if (self.rsync_module and not self.password_file): if self.rsync_module and not self.password_file:
raise Exception('You must specify a password file if you specify a rsync module') raise Exception("You must specify a password file if you specify a rsync module")
if (not self.rsync_module and not self.private_key): if not self.rsync_module and not self.private_key:
raise Exception('If you don''t use SSH, you must specify a rsync module') raise Exception("If you don" "t use SSH, you must specify a rsync module")
#rsync_re = re.compile('(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)') # rsync_re = re.compile(r'(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)')
#ssh_re = re.compile('((?P<user>.*)@)?(?P<server>[^:]*):(?P<path>/.*)') # ssh_re = re.compile(r'((?P<user>.*)@)?(?P<server>[^:]*):(?P<path>/.*)')
# Add ssh connection params # Add ssh connection params
if self.rsync_module: if self.rsync_module:
# Case of rsync exports # Case of rsync exports
if self.password_file: if self.password_file:
options.append('--password-file="%s"' % self.password_file) options.append('--password-file="%s"' % self.password_file)
backup_source = '%s@%s::%s%s' % (self.remote_user, self.server_name, self.rsync_module, self.remote_dir) backup_source = "%s@%s::%s%s" % (self.remote_user, self.server_name, self.rsync_module, self.remote_dir)
else: else:
# case of rsync + ssh # case of rsync + ssh
ssh_params = ['-o StrictHostKeyChecking=no'] ssh_params = ["-o StrictHostKeyChecking=no"]
ssh_params.append('-o BatchMode=yes') ssh_params.append("-o BatchMode=yes")
if self.private_key: if self.private_key:
ssh_params.append('-i %s' % self.private_key) ssh_params.append("-i %s" % self.private_key)
if self.cipher_spec: if self.cipher_spec:
ssh_params.append('-c %s' % self.cipher_spec) ssh_params.append("-c %s" % self.cipher_spec)
if self.ssh_port != 22: if self.ssh_port != 22:
ssh_params.append('-p %i' % self.ssh_port) ssh_params.append("-p %i" % self.ssh_port)
options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params))) options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params)))
backup_source = '%s@%s:%s' % (self.remote_user,self.server_name,self.remote_dir) backup_source = "%s@%s:%s" % (self.remote_user, self.server_name, self.remote_dir)
# ensure there is a slash at end # ensure there is a slash at end
if backup_source[-1] != '/': if backup_source[-1] != "/":
backup_source += '/' backup_source += "/"
options_params = " ".join(options) options_params = " ".join(options)
cmd = '/usr/bin/rsync %s %s %s 2>&1' % (options_params,backup_source,dest_dir) cmd = "/usr/bin/rsync %s %s %s 2>&1" % (options_params, backup_source, dest_dir)
self.logger.debug("[%s] rsync : %s", self.backup_name, cmd) self.logger.debug("[%s] rsync : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
self.line = '' self.line = ""
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
def ondata(data, context): def ondata(data, context):
if context.verbose: if context.verbose:
print(data) print(data)
@@ -170,30 +182,32 @@ class backup_rsync(backup_generic):
log = monitor_stdout(process, ondata, self) log = monitor_stdout(process, ondata, self)
reg_total_files = re.compile('Number of files: (?P<file>\d+)') reg_total_files = re.compile(r"Number of files: (?P<file>\d+)")
reg_transferred_files = re.compile('Number of .*files transferred: (?P<file>\d+)') reg_transferred_files = re.compile(r"Number of .*files transferred: (?P<file>\d+)")
for l in log.splitlines(): for l in log.splitlines():
line = l.replace(',','') line = l.replace(",", "")
m = reg_total_files.match(line) m = reg_total_files.match(line)
if m: if m:
stats['total_files_count'] += int(m.groupdict()['file']) stats["total_files_count"] += int(m.groupdict()["file"])
m = reg_transferred_files.match(line) m = reg_transferred_files.match(line)
if m: if m:
stats['written_files_count'] += int(m.groupdict()['file']) stats["written_files_count"] += int(m.groupdict()["file"])
if line.startswith('Total file size:'): if line.startswith("Total file size:"):
stats['total_bytes'] += int(line.split(':')[1].split()[0]) stats["total_bytes"] += int(line.split(":")[1].split()[0])
if line.startswith('Total transferred file size:'): if line.startswith("Total transferred file size:"):
stats['written_bytes'] += int(line.split(':')[1].split()[0]) stats["written_bytes"] += int(line.split(":")[1].split()[0])
returncode = process.returncode returncode = process.returncode
## deal with exit code 24 (file vanished) ## deal with exit code 24 (file vanished)
if (returncode == 24): if returncode == 24:
self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer") self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer")
elif (returncode == 23): elif returncode == 23:
self.logger.warning("[" + self.backup_name + "] unable so set uid on some files") self.logger.warning("[" + self.backup_name + "] unable so set uid on some files")
elif (returncode != 0): elif returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code " + str(returncode)) self.logger.error("[" + self.backup_name + "] shell program exited with error code " + str(returncode))
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]) raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]
)
else: else:
print(cmd) print(cmd)
@@ -206,16 +220,19 @@ class backup_rsync(backup_generic):
print((os.popen('touch "%s"' % finaldest).read())) print((os.popen('touch "%s"' % finaldest).read()))
else: else:
print(("mv", dest_dir, finaldest)) print(("mv", dest_dir, finaldest))
stats['backup_location'] = finaldest stats["backup_location"] = finaldest
stats['status']='OK' stats["status"] = "OK"
stats['log']='ssh+rsync backup from %s OK, %d bytes written for %d changed files' % (backup_source,stats['written_bytes'],stats['written_files_count']) stats["log"] = "ssh+rsync backup from %s OK, %d bytes written for %d changed files" % (
backup_source,
stats["written_bytes"],
stats["written_files_count"],
)
except BaseException as e: except BaseException as e:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=str(e) stats["log"] = str(e)
raise raise
finally: finally:
self.remove_lock() self.remove_lock()
@@ -224,9 +241,9 @@ class backup_rsync(backup_generic):
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
filelist.reverse() filelist.reverse()
full = '' # full = ''
r_full = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') r_full = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
r_partial = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}.rsync$') r_partial = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}.rsync$")
# we take all latest partials younger than the latest full and the latest full # we take all latest partials younger than the latest full and the latest full
for item in filelist: for item in filelist:
if r_partial.match(item) and item < current: if r_partial.match(item) and item < current:
@@ -237,37 +254,46 @@ class backup_rsync(backup_generic):
return result return result
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') p = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
for item in filelist: for item in filelist:
if p.match(item): if p.match(item):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(item, "%Y%m%d-%Hh%Mm%S").isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
def is_pid_still_running(self, lockfile): def is_pid_still_running(self, lockfile):
f = open(lockfile) f = open(lockfile)
@@ -278,8 +304,8 @@ class backup_rsync(backup_generic):
return False return False
for line in lines: for line in lines:
if line.startswith('pid='): if line.startswith("pid="):
pid = line.split('=')[1].strip() pid = line.split("=")[1].strip()
if os.path.exists("/proc/" + pid): if os.path.exists("/proc/" + pid):
self.logger.info("[" + self.backup_name + "] process still there") self.logger.info("[" + self.backup_name + "] process still there")
return True return True
@@ -290,55 +316,63 @@ class backup_rsync(backup_generic):
self.logger.info("[" + self.backup_name + "] incorrrect lock file : no pid line") self.logger.info("[" + self.backup_name + "] incorrrect lock file : no pid line")
return False return False
def set_lock(self): def set_lock(self):
self.logger.debug("[" + self.backup_name + "] setting lock") self.logger.debug("[" + self.backup_name + "] setting lock")
# TODO: improve for race condition # TODO: improve for race condition
# TODO: also check if process is really there # TODO: also check if process is really there
if os.path.isfile(self.backup_dir + '/lock'): if os.path.isfile(self.backup_dir + "/lock"):
self.logger.debug("[" + self.backup_name + "] File " + self.backup_dir + '/lock already exist') self.logger.debug("[" + self.backup_name + "] File " + self.backup_dir + "/lock already exist")
if self.is_pid_still_running(self.backup_dir + '/lock')==False: if not self.is_pid_still_running(self.backup_dir + "/lock"):
self.logger.info("[" + self.backup_name + "] removing lock file " + self.backup_dir + '/lock') self.logger.info("[" + self.backup_name + "] removing lock file " + self.backup_dir + "/lock")
os.unlink(self.backup_dir + '/lock') os.unlink(self.backup_dir + "/lock")
else: else:
return False return False
lockfile = open(self.backup_dir + '/lock',"w") lockfile = open(self.backup_dir + "/lock", "w")
# Write all the lines at once: # Write all the lines at once:
lockfile.write('pid='+str(os.getpid())) lockfile.write("pid=" + str(os.getpid()))
lockfile.write('\nbackup_time=' + self.backup_start_date) lockfile.write("\nbackup_time=" + self.backup_start_date)
lockfile.close() lockfile.close()
return True return True
def remove_lock(self): def remove_lock(self):
self.logger.debug("[%s] removing lock", self.backup_name) self.logger.debug("[%s] removing lock", self.backup_name)
os.unlink(self.backup_dir + '/lock') os.unlink(self.backup_dir + "/lock")
class backup_rsync_ssh(backup_rsync): class backup_rsync_ssh(backup_rsync):
"""Backup a directory on remote server with rsync and ssh protocol (requires rsync software on remote host)""" """Backup a directory on remote server with rsync and ssh protocol (requires rsync software on remote host)"""
type = 'rsync+ssh'
required_params = backup_generic.required_params + ['remote_user','remote_dir','private_key'] type = "rsync+ssh"
optional_params = backup_generic.optional_params + ['compression','bwlimit','ssh_port','exclude_list','protect_args','overload_args', 'cipher_spec'] required_params = backup_generic.required_params + ["remote_user", "remote_dir", "private_key"]
cipher_spec = '' optional_params = backup_generic.optional_params + [
"compression",
"bwlimit",
"ssh_port",
"exclude_list",
"protect_args",
"overload_args",
"cipher_spec",
]
cipher_spec = ""
register_driver(backup_rsync) register_driver(backup_rsync)
register_driver(backup_rsync_ssh) register_driver(backup_rsync_ssh)
if __name__=='__main__': if __name__ == "__main__":
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
cp = ConfigParser() cp = ConfigParser()
cp.read('/opt/tisbackup/configtest.ini') cp.read("/opt/tisbackup/configtest.ini")
dbstat = BackupStat('/backup/data/log/tisbackup.sqlite') dbstat = BackupStat("/backup/data/log/tisbackup.sqlite")
b = backup_rsync('htouvet','/backup/data/htouvet',dbstat) b = backup_rsync("htouvet", "/backup/data/htouvet", dbstat)
b.read_config(cp) b.read_config(cp)
b.process_backup() b.process_backup()
print((b.checknagios())) print((b.checknagios()))
+155 -122
View File
@@ -18,44 +18,49 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os
import datetime import datetime
from .common import *
import time
import logging import logging
import re import os
import os.path import os.path
import datetime import re
import time
from .common import * from .common import *
class backup_rsync_btrfs(backup_generic): class backup_rsync_btrfs(backup_generic):
"""Backup a directory on remote server with rsync and btrfs protocol (requires running remote rsync daemon)""" """Backup a directory on remote server with rsync and btrfs protocol (requires running remote rsync daemon)"""
type = 'rsync+btrfs'
required_params = backup_generic.required_params + ['remote_user','remote_dir','rsync_module','password_file']
optional_params = backup_generic.optional_params + ['compressionlevel','compression','bwlimit','exclude_list','protect_args','overload_args']
remote_user='root' type = "rsync+btrfs"
remote_dir='' required_params = backup_generic.required_params + ["remote_user", "remote_dir", "rsync_module", "password_file"]
optional_params = backup_generic.optional_params + [
"compressionlevel",
"compression",
"bwlimit",
"exclude_list",
"protect_args",
"overload_args",
]
exclude_list='' remote_user = "root"
rsync_module='' remote_dir = ""
password_file = ''
compression = '' exclude_list = ""
rsync_module = ""
password_file = ""
compression = ""
bwlimit = 0 bwlimit = 0
protect_args = '1' protect_args = "1"
overload_args = None overload_args = None
compressionlevel = 0 compressionlevel = 0
def read_config(self, iniconf): def read_config(self, iniconf):
assert(isinstance(iniconf,ConfigParser)) assert isinstance(iniconf, ConfigParser)
backup_generic.read_config(self, iniconf) backup_generic.read_config(self, iniconf)
if not self.bwlimit and iniconf.has_option('global','bw_limit'): if not self.bwlimit and iniconf.has_option("global", "bw_limit"):
self.bwlimit = iniconf.getint('global','bw_limit') self.bwlimit = iniconf.getint("global", "bw_limit")
if not self.compressionlevel and iniconf.has_option('global','compression_level'): if not self.compressionlevel and iniconf.has_option("global", "compression_level"):
self.compressionlevel = iniconf.getint('global','compression_level') self.compressionlevel = iniconf.getint("global", "compression_level")
def do_backup(self, stats): def do_backup(self, stats):
if not self.set_lock(): if not self.set_lock():
@@ -64,15 +69,15 @@ class backup_rsync_btrfs(backup_generic):
try: try:
try: try:
backup_source = 'undefined' backup_source = "undefined"
dest_dir = os.path.join(self.backup_dir,'last_backup') dest_dir = os.path.join(self.backup_dir, "last_backup")
if not os.path.isdir(dest_dir): if not os.path.isdir(dest_dir):
if not self.dry_run: if not self.dry_run:
cmd = "/bin/btrfs subvolume create %s" % dest_dir cmd = "/bin/btrfs subvolume create %s" % dest_dir
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
log = monitor_stdout(process,'',self) log = monitor_stdout(process, "", self)
returncode = process.returncode returncode = process.returncode
if (returncode != 0): if returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code: %s" % log) self.logger.error("[" + self.backup_name + "] shell program exited with error code: %s" % log)
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd) raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd)
else: else:
@@ -80,35 +85,33 @@ class backup_rsync_btrfs(backup_generic):
else: else:
print(('btrfs subvolume create "%s"' % dest_dir)) print(('btrfs subvolume create "%s"' % dest_dir))
options = ["-rt", "--stats", "--delete-excluded", "--numeric-ids", "--delete-after"]
options = ['-rt','--stats','--delete-excluded','--numeric-ids','--delete-after']
if self.logger.level: if self.logger.level:
options.append('-P') options.append("-P")
if self.dry_run: if self.dry_run:
options.append('-d') options.append("-d")
if self.overload_args != None: if self.overload_args is not None:
options.append(self.overload_args) options.append(self.overload_args)
elif not "cygdrive" in self.remote_dir: elif "cygdrive" not in self.remote_dir:
# we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful # we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful
options.append('-lpgoD') options.append("-lpgoD")
# the protect-args option is not available in all rsync version # the protect-args option is not available in all rsync version
if not self.protect_args.lower() in ('false','no','0'): if self.protect_args.lower() not in ("false", "no", "0"):
options.append('--protect-args') options.append("--protect-args")
if self.compression.lower() in ('true','yes','1'): if self.compression.lower() in ("true", "yes", "1"):
options.append('-z') options.append("-z")
if self.compressionlevel: if self.compressionlevel:
options.append('--compress-level=%s' % self.compressionlevel) options.append("--compress-level=%s" % self.compressionlevel)
if self.bwlimit: if self.bwlimit:
options.append('--bwlimit %s' % self.bwlimit) options.append("--bwlimit %s" % self.bwlimit)
latest = self.get_latest_backup(self.backup_start_date) # latest = self.get_latest_backup(self.backup_start_date)
# remove link-dest replace by btrfs # remove link-dest replace by btrfs
# if latest: # if latest:
# options.extend(['--link-dest="%s"' % os.path.join('..',b,'') for b in latest]) # options.extend(['--link-dest="%s"' % os.path.join('..',b,'') for b in latest])
@@ -123,54 +126,60 @@ class backup_rsync_btrfs(backup_generic):
# Add excludes # Add excludes
if "--exclude" in self.exclude_list: if "--exclude" in self.exclude_list:
# old settings with exclude_list=--exclude toto --exclude=titi # old settings with exclude_list=--exclude toto --exclude=titi
excludes = [strip_quotes(s).strip() for s in self.exclude_list.replace('--exclude=','').replace('--exclude ','').split()] excludes = [
strip_quotes(s).strip() for s in self.exclude_list.replace("--exclude=", "").replace("--exclude ", "").split()
]
else: else:
try: try:
# newsettings with exclude_list='too','titi', parsed as a str python list content # newsettings with exclude_list='too','titi', parsed as a str python list content
excludes = eval('[%s]' % self.exclude_list) excludes = eval("[%s]" % self.exclude_list)
except Exception as e: except Exception as e:
raise Exception('Error reading exclude list : value %s, eval error %s (don\'t forget quotes and comma...)' % (self.exclude_list,e)) raise Exception(
"Error reading exclude list : value %s, eval error %s (don't forget quotes and comma...)"
% (self.exclude_list, e)
)
options.extend(['--exclude="%s"' % x for x in excludes]) options.extend(['--exclude="%s"' % x for x in excludes])
if (self.rsync_module and not self.password_file): if self.rsync_module and not self.password_file:
raise Exception('You must specify a password file if you specify a rsync module') raise Exception("You must specify a password file if you specify a rsync module")
if (not self.rsync_module and not self.private_key): if not self.rsync_module and not self.private_key:
raise Exception('If you don''t use SSH, you must specify a rsync module') raise Exception("If you don" "t use SSH, you must specify a rsync module")
#rsync_re = re.compile('(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)') # rsync_re = re.compile(r'(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)')
#ssh_re = re.compile('((?P<user>.*)@)?(?P<server>[^:]*):(?P<path>/.*)') # ssh_re = re.compile(r'((?P<user>.*)@)?(?P<server>[^:]*):(?P<path>/.*)')
# Add ssh connection params # Add ssh connection params
if self.rsync_module: if self.rsync_module:
# Case of rsync exports # Case of rsync exports
if self.password_file: if self.password_file:
options.append('--password-file="%s"' % self.password_file) options.append('--password-file="%s"' % self.password_file)
backup_source = '%s@%s::%s%s' % (self.remote_user, self.server_name, self.rsync_module, self.remote_dir) backup_source = "%s@%s::%s%s" % (self.remote_user, self.server_name, self.rsync_module, self.remote_dir)
else: else:
# case of rsync + ssh # case of rsync + ssh
ssh_params = ['-o StrictHostKeyChecking=no'] ssh_params = ["-o StrictHostKeyChecking=no"]
if self.private_key: if self.private_key:
ssh_params.append('-i %s' % self.private_key) ssh_params.append("-i %s" % self.private_key)
if self.cipher_spec: if self.cipher_spec:
ssh_params.append('-c %s' % self.cipher_spec) ssh_params.append("-c %s" % self.cipher_spec)
if self.ssh_port != 22: if self.ssh_port != 22:
ssh_params.append('-p %i' % self.ssh_port) ssh_params.append("-p %i" % self.ssh_port)
options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params))) options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params)))
backup_source = '%s@%s:%s' % (self.remote_user,self.server_name,self.remote_dir) backup_source = "%s@%s:%s" % (self.remote_user, self.server_name, self.remote_dir)
# ensure there is a slash at end # ensure there is a slash at end
if backup_source[-1] != '/': if backup_source[-1] != "/":
backup_source += '/' backup_source += "/"
options_params = " ".join(options) options_params = " ".join(options)
cmd = '/usr/bin/rsync %s %s %s 2>&1' % (options_params,backup_source,dest_dir) cmd = "/usr/bin/rsync %s %s %s 2>&1" % (options_params, backup_source, dest_dir)
self.logger.debug("[%s] rsync : %s", self.backup_name, cmd) self.logger.debug("[%s] rsync : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
self.line = '' self.line = ""
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
def ondata(data, context): def ondata(data, context):
if context.verbose: if context.verbose:
print(data) print(data)
@@ -178,30 +187,32 @@ class backup_rsync_btrfs(backup_generic):
log = monitor_stdout(process, ondata, self) log = monitor_stdout(process, ondata, self)
reg_total_files = re.compile('Number of files: (?P<file>\d+)') reg_total_files = re.compile(r"Number of files: (?P<file>\d+)")
reg_transferred_files = re.compile('Number of .*files transferred: (?P<file>\d+)') reg_transferred_files = re.compile(r"Number of .*files transferred: (?P<file>\d+)")
for l in log.splitlines(): for l in log.splitlines():
line = l.replace(',','') line = l.replace(",", "")
m = reg_total_files.match(line) m = reg_total_files.match(line)
if m: if m:
stats['total_files_count'] += int(m.groupdict()['file']) stats["total_files_count"] += int(m.groupdict()["file"])
m = reg_transferred_files.match(line) m = reg_transferred_files.match(line)
if m: if m:
stats['written_files_count'] += int(m.groupdict()['file']) stats["written_files_count"] += int(m.groupdict()["file"])
if line.startswith('Total file size:'): if line.startswith("Total file size:"):
stats['total_bytes'] += int(line.split(':')[1].split()[0]) stats["total_bytes"] += int(line.split(":")[1].split()[0])
if line.startswith('Total transferred file size:'): if line.startswith("Total transferred file size:"):
stats['written_bytes'] += int(line.split(':')[1].split()[0]) stats["written_bytes"] += int(line.split(":")[1].split()[0])
returncode = process.returncode returncode = process.returncode
## deal with exit code 24 (file vanished) ## deal with exit code 24 (file vanished)
if (returncode == 24): if returncode == 24:
self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer") self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer")
elif (returncode == 23): elif returncode == 23:
self.logger.warning("[" + self.backup_name + "] unable so set uid on some files") self.logger.warning("[" + self.backup_name + "] unable so set uid on some files")
elif (returncode != 0): elif returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code ", str(returncode)) self.logger.error("[" + self.backup_name + "] shell program exited with error code ", str(returncode))
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]) raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]
)
else: else:
print(cmd) print(cmd)
@@ -212,29 +223,34 @@ class backup_rsync_btrfs(backup_generic):
if not self.dry_run: if not self.dry_run:
cmd = "/bin/btrfs subvolume snapshot %s %s" % (dest_dir, finaldest) cmd = "/bin/btrfs subvolume snapshot %s %s" % (dest_dir, finaldest)
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
log = monitor_stdout(process,'',self) log = monitor_stdout(process, "", self)
returncode = process.returncode returncode = process.returncode
if (returncode != 0): if returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code " + str(returncode)) self.logger.error("[" + self.backup_name + "] shell program exited with error code " + str(returncode))
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]) raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]
)
else: else:
self.logger.info("[" + self.backup_name + "] snapshot directory created %s" % finaldest) self.logger.info("[" + self.backup_name + "] snapshot directory created %s" % finaldest)
else: else:
print(("btrfs snapshot of %s to %s" % (dest_dir, finaldest))) print(("btrfs snapshot of %s to %s" % (dest_dir, finaldest)))
else: else:
raise Exception('snapshot directory already exists : %s' %finaldest) raise Exception("snapshot directory already exists : %s" % finaldest)
self.logger.debug("[%s] touching datetime of target directory %s", self.backup_name, finaldest) self.logger.debug("[%s] touching datetime of target directory %s", self.backup_name, finaldest)
print((os.popen('touch "%s"' % finaldest).read())) print((os.popen('touch "%s"' % finaldest).read()))
stats['backup_location'] = finaldest stats["backup_location"] = finaldest
stats['status']='OK' stats["status"] = "OK"
stats['log']='ssh+rsync+btrfs backup from %s OK, %d bytes written for %d changed files' % (backup_source,stats['written_bytes'],stats['written_files_count']) stats["log"] = "ssh+rsync+btrfs backup from %s OK, %d bytes written for %d changed files" % (
backup_source,
stats["written_bytes"],
stats["written_files_count"],
)
except BaseException as e: except BaseException as e:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=str(e) stats["log"] = str(e)
raise raise
finally: finally:
self.remove_lock() self.remove_lock()
@@ -243,9 +259,9 @@ class backup_rsync_btrfs(backup_generic):
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
filelist.reverse() filelist.reverse()
full = '' # full = ''
r_full = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') r_full = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
r_partial = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}.rsync$') r_partial = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}.rsync$")
# we take all latest partials younger than the latest full and the latest full # we take all latest partials younger than the latest full and the latest full
for item in filelist: for item in filelist:
if r_partial.match(item) and item < current: if r_partial.match(item) and item < current:
@@ -255,37 +271,46 @@ class backup_rsync_btrfs(backup_generic):
break break
return result return result
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') p = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
for item in filelist: for item in filelist:
if p.match(item): if p.match(item):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(item, "%Y%m%d-%Hh%Mm%S").isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
def is_pid_still_running(self, lockfile): def is_pid_still_running(self, lockfile):
f = open(lockfile) f = open(lockfile)
@@ -296,8 +321,8 @@ class backup_rsync_btrfs(backup_generic):
return False return False
for line in lines: for line in lines:
if line.startswith('pid='): if line.startswith("pid="):
pid = line.split('=')[1].strip() pid = line.split("=")[1].strip()
if os.path.exists("/proc/" + pid): if os.path.exists("/proc/" + pid):
self.logger.info("[" + self.backup_name + "] process still there") self.logger.info("[" + self.backup_name + "] process still there")
return True return True
@@ -308,55 +333,63 @@ class backup_rsync_btrfs(backup_generic):
self.logger.info("[" + self.backup_name + "] incorrrect lock file : no pid line") self.logger.info("[" + self.backup_name + "] incorrrect lock file : no pid line")
return False return False
def set_lock(self): def set_lock(self):
self.logger.debug("[" + self.backup_name + "] setting lock") self.logger.debug("[" + self.backup_name + "] setting lock")
# TODO: improve for race condition # TODO: improve for race condition
# TODO: also check if process is really there # TODO: also check if process is really there
if os.path.isfile(self.backup_dir + '/lock'): if os.path.isfile(self.backup_dir + "/lock"):
self.logger.debug("[" + self.backup_name + "] File " + self.backup_dir + '/lock already exist') self.logger.debug("[" + self.backup_name + "] File " + self.backup_dir + "/lock already exist")
if self.is_pid_still_running(self.backup_dir + '/lock')==False: if not self.is_pid_still_running(self.backup_dir + "/lock"):
self.logger.info("[" + self.backup_name + "] removing lock file " + self.backup_dir + '/lock') self.logger.info("[" + self.backup_name + "] removing lock file " + self.backup_dir + "/lock")
os.unlink(self.backup_dir + '/lock') os.unlink(self.backup_dir + "/lock")
else: else:
return False return False
lockfile = open(self.backup_dir + '/lock',"w") lockfile = open(self.backup_dir + "/lock", "w")
# Write all the lines at once: # Write all the lines at once:
lockfile.write('pid='+str(os.getpid())) lockfile.write("pid=" + str(os.getpid()))
lockfile.write('\nbackup_time=' + self.backup_start_date) lockfile.write("\nbackup_time=" + self.backup_start_date)
lockfile.close() lockfile.close()
return True return True
def remove_lock(self): def remove_lock(self):
self.logger.debug("[%s] removing lock", self.backup_name) self.logger.debug("[%s] removing lock", self.backup_name)
os.unlink(self.backup_dir + '/lock') os.unlink(self.backup_dir + "/lock")
class backup_rsync__btrfs_ssh(backup_rsync_btrfs): class backup_rsync__btrfs_ssh(backup_rsync_btrfs):
"""Backup a directory on remote server with rsync,ssh and btrfs protocol (requires rsync software on remote host)""" """Backup a directory on remote server with rsync,ssh and btrfs protocol (requires rsync software on remote host)"""
type = 'rsync+btrfs+ssh'
required_params = backup_generic.required_params + ['remote_user','remote_dir','private_key'] type = "rsync+btrfs+ssh"
optional_params = backup_generic.optional_params + ['compression','bwlimit','ssh_port','exclude_list','protect_args','overload_args','cipher_spec'] required_params = backup_generic.required_params + ["remote_user", "remote_dir", "private_key"]
cipher_spec = '' optional_params = backup_generic.optional_params + [
"compression",
"bwlimit",
"ssh_port",
"exclude_list",
"protect_args",
"overload_args",
"cipher_spec",
]
cipher_spec = ""
register_driver(backup_rsync_btrfs) register_driver(backup_rsync_btrfs)
register_driver(backup_rsync__btrfs_ssh) register_driver(backup_rsync__btrfs_ssh)
if __name__=='__main__': if __name__ == "__main__":
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
cp = ConfigParser() cp = ConfigParser()
cp.read('/opt/tisbackup/configtest.ini') cp.read("/opt/tisbackup/configtest.ini")
dbstat = BackupStat('/backup/data/log/tisbackup.sqlite') dbstat = BackupStat("/backup/data/log/tisbackup.sqlite")
b = backup_rsync('htouvet','/backup/data/htouvet',dbstat) b = backup_rsync("htouvet", "/backup/data/htouvet", dbstat)
b.read_config(cp) b.read_config(cp)
b.process_backup() b.process_backup()
print((b.checknagios())) print((b.checknagios()))
+59 -47
View File
@@ -19,10 +19,10 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import sys import sys
try: try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko sys.stderr = open("/dev/null") # Silence silly warnings from paramiko
import paramiko import paramiko
except ImportError as e: except ImportError as e:
print("Error : can not load paramiko library %s" % e) print("Error : can not load paramiko library %s" % e)
@@ -32,26 +32,28 @@ sys.stderr = sys.__stderr__
from .common import * from .common import *
class backup_samba4(backup_generic): class backup_samba4(backup_generic):
"""Backup a samba4 databases as gzipped tdbs file through ssh""" """Backup a samba4 databases as gzipped tdbs file through ssh"""
type = 'samba4'
required_params = backup_generic.required_params + ['private_key'] type = "samba4"
optional_params = backup_generic.optional_params + ['root_dir_samba'] required_params = backup_generic.required_params + ["private_key"]
optional_params = backup_generic.optional_params + ["root_dir_samba"]
root_dir_samba = "/var/lib/samba/" root_dir_samba = "/var/lib/samba/"
def do_backup(self, stats): def do_backup(self, stats):
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date) self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir): if not os.path.isdir(self.dest_dir):
if not self.dry_run: if not self.dry_run:
os.makedirs(self.dest_dir) os.makedirs(self.dest_dir)
else: else:
print('mkdir "%s"' % self.dest_dir) print('mkdir "%s"' % self.dest_dir)
else: else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir) raise Exception("backup destination directory already exists : %s" % self.dest_dir)
self.logger.debug('[%s] Connecting to %s with user root and key %s',self.backup_name,self.server_name,self.private_key) self.logger.debug("[%s] Connecting to %s with user root and key %s", self.backup_name, self.server_name, self.private_key)
try: try:
mykey = paramiko.RSAKey.from_private_key_file(self.private_key) mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
except paramiko.SSHException: except paramiko.SSHException:
@@ -60,32 +62,31 @@ class backup_samba4(backup_generic):
self.ssh = paramiko.SSHClient() self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name,username='root',pkey = mykey, port=self.ssh_port) self.ssh.connect(self.server_name, username="root", pkey=mykey, port=self.ssh_port)
stats['log']= "Successfully backuping processed to the following databases :" stats["log"] = "Successfully backuping processed to the following databases :"
stats['status']='List' stats["status"] = "List"
dir_ldbs = os.path.join(self.root_dir_samba+'/private/sam.ldb.d/') dir_ldbs = os.path.join(self.root_dir_samba + "/private/sam.ldb.d/")
cmd = 'ls %s/*.ldb 2> /dev/null' % dir_ldbs cmd = "ls %s/*.ldb 2> /dev/null" % dir_ldbs
self.logger.debug('[%s] List databases: %s',self.backup_name,cmd) self.logger.debug("[%s] List databases: %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
databases = output.split('\n') databases = output.split("\n")
for database in databases: for database in databases:
if database != "": if database != "":
self.db_name = database.rstrip() self.db_name = database.rstrip()
self.do_mysqldump(stats) self.do_mysqldump(stats)
def do_mysqldump(self, stats): def do_mysqldump(self, stats):
t = datetime.datetime.now() # t = datetime.datetime.now()
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S') # backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
# dump db # dump db
stats['status']='Dumping' stats["status"] = "Dumping"
cmd = 'tdbbackup -s .tisbackup ' + self.db_name cmd = "tdbbackup -s .tisbackup " + self.db_name
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd) self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
print(output) print(output)
@@ -94,9 +95,9 @@ class backup_samba4(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# zip the file # zip the file
stats['status']='Zipping' stats["status"] = "Zipping"
cmd = 'gzip -f "%s.tisbackup"' % self.db_name cmd = 'gzip -f "%s.tisbackup"' % self.db_name
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd) self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
@@ -104,10 +105,10 @@ class backup_samba4(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# get the file # get the file
stats['status']='SFTP' stats["status"] = "SFTP"
filepath = self.db_name + '.tisbackup.gz' filepath = self.db_name + ".tisbackup.gz"
localpath = os.path.join(self.dest_dir , os.path.basename(self.db_name) + '.tisbackup.gz') localpath = os.path.join(self.dest_dir, os.path.basename(self.db_name) + ".tisbackup.gz")
self.logger.debug('[%s] Get gz backup with sftp on %s from %s to %s',self.backup_name,self.server_name,filepath,localpath) self.logger.debug("[%s] Get gz backup with sftp on %s from %s to %s", self.backup_name, self.server_name, filepath, localpath)
if not self.dry_run: if not self.dry_run:
transport = self.ssh.get_transport() transport = self.ssh.get_transport()
sftp = paramiko.SFTPClient.from_transport(transport) sftp = paramiko.SFTPClient.from_transport(transport)
@@ -115,52 +116,63 @@ class backup_samba4(backup_generic):
sftp.close() sftp.close()
if not self.dry_run: if not self.dry_run:
stats['total_files_count']=1 + stats.get('total_files_count', 0) stats["total_files_count"] = 1 + stats.get("total_files_count", 0)
stats['written_files_count']=1 + stats.get('written_files_count', 0) stats["written_files_count"] = 1 + stats.get("written_files_count", 0)
stats['total_bytes']=os.stat(localpath).st_size + stats.get('total_bytes', 0) stats["total_bytes"] = os.stat(localpath).st_size + stats.get("total_bytes", 0)
stats['written_bytes']=os.stat(localpath).st_size + stats.get('written_bytes', 0) stats["written_bytes"] = os.stat(localpath).st_size + stats.get("written_bytes", 0)
stats['log'] = '%s "%s"' % (stats['log'] ,self.db_name) stats["log"] = '%s "%s"' % (stats["log"], self.db_name)
stats['backup_location'] = self.dest_dir stats["backup_location"] = self.dest_dir
stats['status']='RMTemp' stats["status"] = "RMTemp"
cmd = 'rm -f "%s"' % filepath cmd = 'rm -f "%s"' % filepath
self.logger.debug('[%s] Remove temp gzip : %s',self.backup_name,cmd) self.logger.debug("[%s] Remove temp gzip : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh) (error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
stats['status']='OK' stats["status"] = "OK"
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$') p = re.compile(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
for item in filelist: for item in filelist:
if p.match(item): if p.match(item):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(item, "%Y%m%d-%Hh%Mm%S").isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
register_driver(backup_samba4) register_driver(backup_samba4)
+66 -49
View File
@@ -19,10 +19,10 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import sys import sys
try: try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko sys.stderr = open("/dev/null") # Silence silly warnings from paramiko
import paramiko import paramiko
except ImportError as e: except ImportError as e:
print("Error : can not load paramiko library %s" % e) print("Error : can not load paramiko library %s" % e)
@@ -30,48 +30,49 @@ except ImportError as e:
sys.stderr = sys.__stderr__ sys.stderr = sys.__stderr__
import datetime
import base64 import base64
import datetime
import os import os
from .common import * from .common import *
class backup_sqlserver(backup_generic): class backup_sqlserver(backup_generic):
"""Backup a SQLSERVER database as gzipped sql file through ssh""" """Backup a SQLSERVER database as gzipped sql file through ssh"""
type = 'sqlserver+ssh'
required_params = backup_generic.required_params + ['db_name','private_key'] type = "sqlserver+ssh"
optional_params = ['username', 'remote_backup_dir', 'sqlserver_before_2005', 'db_server_name', 'db_user', 'db_password'] required_params = backup_generic.required_params + ["db_name", "private_key"]
db_name='' optional_params = ["username", "remote_backup_dir", "sqlserver_before_2005", "db_server_name", "db_user", "db_password"]
db_user='' db_name = ""
db_password='' db_user = ""
db_password = ""
userdb = "-E" userdb = "-E"
username='Administrateur' username = "Administrateur"
remote_backup_dir = r'c:/WINDOWS/Temp/' remote_backup_dir = r"c:/WINDOWS/Temp/"
sqlserver_before_2005 = False sqlserver_before_2005 = False
db_server_name = "localhost" db_server_name = "localhost"
def do_backup(self, stats): def do_backup(self, stats):
try: try:
mykey = paramiko.RSAKey.from_private_key_file(self.private_key) mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
except paramiko.SSHException: except paramiko.SSHException:
# mykey = paramiko.DSSKey.from_private_key_file(self.private_key) # mykey = paramiko.DSSKey.from_private_key_file(self.private_key)
mykey = paramiko.Ed25519Key.from_private_key_file(self.private_key) mykey = paramiko.Ed25519Key.from_private_key_file(self.private_key)
self.logger.debug('[%s] Connecting to %s with user root and key %s',self.backup_name,self.server_name,self.private_key) self.logger.debug("[%s] Connecting to %s with user root and key %s", self.backup_name, self.server_name, self.private_key)
ssh = paramiko.SSHClient() ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(self.server_name, username=self.username, pkey=mykey, port=self.ssh_port) ssh.connect(self.server_name, username=self.username, pkey=mykey, port=self.ssh_port)
t = datetime.datetime.now() t = datetime.datetime.now()
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S') backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
backup_file = self.remote_backup_dir + '/' + self.db_name + '-' + backup_start_date + '.bak' backup_file = self.remote_backup_dir + "/" + self.db_name + "-" + backup_start_date + ".bak"
if not self.db_user == '': if not self.db_user == "":
self.userdb = '-U %s -P %s' % ( self.db_user, self.db_password ) self.userdb = "-U %s -P %s" % (self.db_user, self.db_password)
# dump db # dump db
stats['status']='Dumping' stats["status"] = "Dumping"
if self.sqlserver_before_2005: if self.sqlserver_before_2005:
cmd = """osql -E -Q "BACKUP DATABASE [%s] cmd = """osql -E -Q "BACKUP DATABASE [%s]
TO DISK='%s' TO DISK='%s'
@@ -80,8 +81,14 @@ class backup_sqlserver(backup_generic):
cmd = """sqlcmd %s -S "%s" -d master -Q "BACKUP DATABASE [%s] cmd = """sqlcmd %s -S "%s" -d master -Q "BACKUP DATABASE [%s]
TO DISK = N'%s' TO DISK = N'%s'
WITH INIT, NOUNLOAD , WITH INIT, NOUNLOAD ,
NAME = N'Backup %s', NOSKIP ,STATS = 10, NOFORMAT" """ % (self.userdb, self.db_server_name, self.db_name, backup_file ,self.db_name ) NAME = N'Backup %s', NOSKIP ,STATS = 10, NOFORMAT" """ % (
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd) self.userdb,
self.db_server_name,
self.db_name,
backup_file,
self.db_name,
)
self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
try: try:
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=ssh) (error_code, output) = ssh_exec(cmd, ssh=ssh)
@@ -90,9 +97,9 @@ class backup_sqlserver(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# zip the file # zip the file
stats['status']='Zipping' stats["status"] = "Zipping"
cmd = 'gzip "%s"' % backup_file cmd = 'gzip "%s"' % backup_file
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd) self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=ssh) (error_code, output) = ssh_exec(cmd, ssh=ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
@@ -100,10 +107,10 @@ class backup_sqlserver(backup_generic):
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
# get the file # get the file
stats['status']='SFTP' stats["status"] = "SFTP"
filepath = backup_file + '.gz' filepath = backup_file + ".gz"
localpath = os.path.join(self.backup_dir , self.db_name + '-' + backup_start_date + '.bak.gz') localpath = os.path.join(self.backup_dir, self.db_name + "-" + backup_start_date + ".bak.gz")
self.logger.debug('[%s] Get gz backup with sftp on %s from %s to %s',self.backup_name,self.server_name,filepath,localpath) self.logger.debug("[%s] Get gz backup with sftp on %s from %s to %s", self.backup_name, self.server_name, filepath, localpath)
if not self.dry_run: if not self.dry_run:
transport = ssh.get_transport() transport = ssh.get_transport()
sftp = paramiko.SFTPClient.from_transport(transport) sftp = paramiko.SFTPClient.from_transport(transport)
@@ -111,48 +118,58 @@ class backup_sqlserver(backup_generic):
sftp.close() sftp.close()
if not self.dry_run: if not self.dry_run:
stats['total_files_count']=1 stats["total_files_count"] = 1
stats['written_files_count']=1 stats["written_files_count"] = 1
stats['total_bytes']=os.stat(localpath).st_size stats["total_bytes"] = os.stat(localpath).st_size
stats['written_bytes']=os.stat(localpath).st_size stats["written_bytes"] = os.stat(localpath).st_size
stats['log']='gzip dump of DB %s:%s (%d bytes) to %s' % (self.server_name,self.db_name, stats['written_bytes'], localpath) stats["log"] = "gzip dump of DB %s:%s (%d bytes) to %s" % (self.server_name, self.db_name, stats["written_bytes"], localpath)
stats['backup_location'] = localpath stats["backup_location"] = localpath
finally: finally:
stats['status']='RMTemp' stats["status"] = "RMTemp"
cmd = 'rm -f "%s" "%s"' % ( backup_file + '.gz', backup_file ) cmd = 'rm -f "%s" "%s"' % (backup_file + ".gz", backup_file)
self.logger.debug('[%s] Remove temp gzip : %s',self.backup_name,cmd) self.logger.debug("[%s] Remove temp gzip : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=ssh) (error_code, output) = ssh_exec(cmd, ssh=ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output) self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code: if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd)) raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code, cmd))
stats["status"] = "OK"
stats['status']='OK'
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).bak.gz$' % self.db_name) p = re.compile(r"^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).bak.gz$" % self.db_name)
for item in filelist: for item in filelist:
sr = p.match(item) sr = p.match(item)
if sr: if sr:
file_name = os.path.join(self.backup_dir, item) file_name = os.path.join(self.backup_dir, item)
start = datetime.datetime.strptime(sr.groups()[0],'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(sr.groups()[0], "%Y%m%d-%Hh%Mm%S").isoformat()
if not file_name in registered: if file_name not in registered:
self.logger.info('Registering %s from %s',file_name,fileisodate(file_name)) self.logger.info("Registering %s from %s", file_name, fileisodate(file_name))
size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split("\t")[0])
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end=fileisodate(file_name),status='OK',total_bytes=size_bytes,backup_location=file_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=fileisodate(file_name),
status="OK",
total_bytes=size_bytes,
backup_location=file_name,
)
else: else:
self.logger.info('Skipping %s from %s, already registered',file_name,fileisodate(file_name)) self.logger.info("Skipping %s from %s, already registered", file_name, fileisodate(file_name))
register_driver(backup_sqlserver) register_driver(backup_sqlserver)
+88 -83
View File
@@ -18,36 +18,39 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os
import datetime
from .common import *
from . import XenAPI
import time
import logging
import re
import os.path
import datetime
import select
import urllib.request, urllib.error, urllib.parse, urllib.request, urllib.parse, urllib.error
import base64 import base64
import datetime
import logging
import os
import os.path
import re
import select
import socket import socket
import requests import time
import pexpect import urllib.error
import urllib.parse
import urllib.request
from stat import * from stat import *
import pexpect
import requests
from . import XenAPI
from .common import *
class backup_switch(backup_generic): class backup_switch(backup_generic):
"""Backup a startup-config on a switch""" """Backup a startup-config on a switch"""
type = 'switch'
required_params = backup_generic.required_params + ['switch_ip','switch_type'] type = "switch"
optional_params = backup_generic.optional_params + [ 'switch_user', 'switch_password']
switch_user = '' required_params = backup_generic.required_params + ["switch_ip", "switch_type"]
switch_password = '' optional_params = backup_generic.optional_params + ["switch_user", "switch_password"]
switch_user = ""
switch_password = ""
def switch_hp(self, filename): def switch_hp(self, filename):
s = socket.socket() s = socket.socket()
try: try:
s.connect((self.switch_ip, 23)) s.connect((self.switch_ip, 23))
@@ -55,13 +58,13 @@ class backup_switch(backup_generic):
except: except:
raise raise
child=pexpect.spawn('telnet '+self.switch_ip) child = pexpect.spawn("telnet " + self.switch_ip)
time.sleep(1) time.sleep(1)
if self.switch_user != "": if self.switch_user != "":
child.sendline(self.switch_user) child.sendline(self.switch_user)
child.sendline(self.switch_password+'\r') child.sendline(self.switch_password + "\r")
else: else:
child.sendline(self.switch_password+'\r') child.sendline(self.switch_password + "\r")
try: try:
child.expect("#") child.expect("#")
except: except:
@@ -77,7 +80,7 @@ class backup_switch(backup_generic):
child.expect("#") child.expect("#")
lines += child.before lines += child.before
child.sendline("logout\r") child.sendline("logout\r")
child.send('y\r') child.send("y\r")
for line in lines.split("\n")[1:-1]: for line in lines.split("\n")[1:-1]:
open(filename, "a").write(line.strip() + "\n") open(filename, "a").write(line.strip() + "\n")
@@ -89,19 +92,19 @@ class backup_switch(backup_generic):
except: except:
raise raise
child=pexpect.spawn('telnet '+self.switch_ip) child = pexpect.spawn("telnet " + self.switch_ip)
time.sleep(1) time.sleep(1)
if self.switch_user: if self.switch_user:
child.sendline(self.switch_user) child.sendline(self.switch_user)
child.expect('Password: ') child.expect("Password: ")
child.sendline(self.switch_password+'\r') child.sendline(self.switch_password + "\r")
try: try:
child.expect(">") child.expect(">")
except: except:
raise Exception("Bad Credentials") raise Exception("Bad Credentials")
child.sendline('enable\r') child.sendline("enable\r")
child.expect('Password: ') child.expect("Password: ")
child.sendline(self.switch_password+'\r') child.sendline(self.switch_password + "\r")
try: try:
child.expect("#") child.expect("#")
except: except:
@@ -109,18 +112,17 @@ class backup_switch(backup_generic):
child.sendline("terminal length 0\r") child.sendline("terminal length 0\r")
child.expect("#") child.expect("#")
child.sendline("show run\r") child.sendline("show run\r")
child.expect('Building configuration...') child.expect("Building configuration...")
child.expect("#") child.expect("#")
running_config = child.before running_config = child.before
child.sendline("show vlan\r") child.sendline("show vlan\r")
child.expect('VLAN') child.expect("VLAN")
child.expect("#") child.expect("#")
vlan = 'VLAN'+child.before vlan = "VLAN" + child.before
open(filename,"a").write(running_config+'\n'+vlan) open(filename, "a").write(running_config + "\n" + vlan)
child.send('exit\r') child.send("exit\r")
child.close() child.close()
def switch_linksys_SRW2024(self, filename): def switch_linksys_SRW2024(self, filename):
s = socket.socket() s = socket.socket()
try: try:
@@ -129,23 +131,23 @@ class backup_switch(backup_generic):
except: except:
raise raise
child=pexpect.spawn('telnet '+self.switch_ip) child = pexpect.spawn("telnet " + self.switch_ip)
time.sleep(1) time.sleep(1)
if hasattr(self,'switch_password'): if hasattr(self, "switch_password"):
child.sendline(self.switch_user+'\t') child.sendline(self.switch_user + "\t")
child.sendline(self.switch_password+'\r') child.sendline(self.switch_password + "\r")
else: else:
child.sendline(self.switch_user+'\r') child.sendline(self.switch_user + "\r")
try: try:
child.expect('Menu') child.expect("Menu")
except: except:
raise Exception("Bad Credentials") raise Exception("Bad Credentials")
child.sendline('\032') child.sendline("\032")
child.expect('>') child.expect(">")
child.sendline('lcli') child.sendline("lcli")
child.expect("Name:") child.expect("Name:")
if hasattr(self,'switch_password'): if hasattr(self, "switch_password"):
child.send(self.switch_user+'\r'+self.switch_password+'\r') child.send(self.switch_user + "\r" + self.switch_password + "\r")
else: else:
child.sendline(self.switch_user) child.sendline(self.switch_user)
child.expect(".*#") child.expect(".*#")
@@ -163,14 +165,19 @@ class backup_switch(backup_generic):
for line in lines.split("\n")[1:-1]: for line in lines.split("\n")[1:-1]:
open(filename, "a").write(line.strip() + "\n") open(filename, "a").write(line.strip() + "\n")
def switch_dlink_DGS1210(self, filename): def switch_dlink_DGS1210(self, filename):
login_data = {'Login' : self.switch_user, 'Password' : self.switch_password, 'sellanId' : 0, 'sellan' : 0, 'lang_seqid' : 1} login_data = {"Login": self.switch_user, "Password": self.switch_password, "sellanId": 0, "sellan": 0, "lang_seqid": 1}
resp = requests.post('http://%s/form/formLoginApply' % self.switch_ip, data=login_data, headers={"Referer":'http://%s/www/login.html' % self.switch_ip}) resp = requests.post(
"http://%s/form/formLoginApply" % self.switch_ip,
data=login_data,
headers={"Referer": "http://%s/www/login.html" % self.switch_ip},
)
if "Wrong password" in resp.text: if "Wrong password" in resp.text:
raise Exception("Wrong password") raise Exception("Wrong password")
resp = requests.post("http://%s/BinFile/config.bin" % self.switch_ip, headers={"Referer":'http://%s/www/iss/013_download_cfg.html' % self.switch_ip}) resp = requests.post(
with open(filename, 'w') as f: "http://%s/BinFile/config.bin" % self.switch_ip, headers={"Referer": "http://%s/www/iss/013_download_cfg.html" % self.switch_ip}
)
with open(filename, "w") as f:
f.write(resp.content) f.write(resp.content)
def switch_dlink_DGS1510(self, filename): def switch_dlink_DGS1510(self, filename):
@@ -181,12 +188,12 @@ class backup_switch(backup_generic):
except: except:
raise raise
child = pexpect.spawn('telnet ' + self.switch_ip) child = pexpect.spawn("telnet " + self.switch_ip)
time.sleep(1) time.sleep(1)
if self.switch_user: if self.switch_user:
child.sendline(self.switch_user) child.sendline(self.switch_user)
child.expect('Password:') child.expect("Password:")
child.sendline(self.switch_password + '\r') child.sendline(self.switch_password + "\r")
try: try:
child.expect("#") child.expect("#")
except: except:
@@ -195,68 +202,66 @@ class backup_switch(backup_generic):
child.expect("#") child.expect("#")
child.sendline("show run\r") child.sendline("show run\r")
child.logfile_read = open(filename, "a") child.logfile_read = open(filename, "a")
child.expect('End of configuration file') child.expect("End of configuration file")
child.expect('#--') child.expect("#--")
child.expect("#") child.expect("#")
child.close() child.close()
myre = re.compile("#--+") myre = re.compile(r"#--+")
config = myre.split(open(filename).read())[2] config = myre.split(open(filename).read())[2]
with open(filename,'w') as f: with open(filename, "w") as f:
f.write(config) f.write(config)
def do_backup(self, stats): def do_backup(self, stats):
try: try:
dest_filename = os.path.join(self.backup_dir, "%s-%s" % (self.backup_name, self.backup_start_date)) dest_filename = os.path.join(self.backup_dir, "%s-%s" % (self.backup_name, self.backup_start_date))
options = [] # options = []
options_params = " ".join(options) # options_params = " ".join(options)
if "LINKSYS-SRW2024" == self.switch_type: if "LINKSYS-SRW2024" == self.switch_type:
dest_filename += '.txt' dest_filename += ".txt"
self.switch_linksys_SRW2024(dest_filename) self.switch_linksys_SRW2024(dest_filename)
elif self.switch_type in [ "CISCO", ]: elif self.switch_type in [
dest_filename += '.txt' "CISCO",
]:
dest_filename += ".txt"
self.switch_cisco(dest_filename) self.switch_cisco(dest_filename)
elif self.switch_type in ["HP-PROCURVE-4104GL", "HP-PROCURVE-2524"]: elif self.switch_type in ["HP-PROCURVE-4104GL", "HP-PROCURVE-2524"]:
dest_filename += '.txt' dest_filename += ".txt"
self.switch_hp(dest_filename) self.switch_hp(dest_filename)
elif "DLINK-DGS1210" == self.switch_type: elif "DLINK-DGS1210" == self.switch_type:
dest_filename += '.bin' dest_filename += ".bin"
self.switch_dlink_DGS1210(dest_filename) self.switch_dlink_DGS1210(dest_filename)
elif "DLINK-DGS1510" == self.switch_type: elif "DLINK-DGS1510" == self.switch_type:
dest_filename += '.cfg' dest_filename += ".cfg"
self.switch_dlink_DGS1510(dest_filename) self.switch_dlink_DGS1510(dest_filename)
else: else:
raise Exception("Unknown Switch type") raise Exception("Unknown Switch type")
stats['total_files_count']=1 stats["total_files_count"] = 1
stats['written_files_count']=1 stats["written_files_count"] = 1
stats['total_bytes']= os.stat(dest_filename).st_size stats["total_bytes"] = os.stat(dest_filename).st_size
stats['written_bytes'] = stats['total_bytes'] stats["written_bytes"] = stats["total_bytes"]
stats['backup_location'] = dest_filename stats["backup_location"] = dest_filename
stats['status']='OK' stats["status"] = "OK"
stats['log']='Switch backup from %s OK, %d bytes written' % (self.server_name,stats['written_bytes']) stats["log"] = "Switch backup from %s OK, %d bytes written" % (self.server_name, stats["written_bytes"])
except BaseException as e: except BaseException as e:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=str(e) stats["log"] = str(e)
raise raise
register_driver(backup_switch) register_driver(backup_switch)
if __name__=='__main__': if __name__ == "__main__":
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
cp = ConfigParser() cp = ConfigParser()
cp.read('/opt/tisbackup/configtest.ini') cp.read("/opt/tisbackup/configtest.ini")
b = backup_xva() b = backup_xva()
b.read_config(cp) b.read_config(cp)
+58 -70
View File
@@ -18,33 +18,34 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
from .common import *
import pyVmomi
from pyVmomi import vim
from pyVmomi import vmodl
from pyVim.connect import SmartConnect, Disconnect
from datetime import datetime, date, timedelta
import atexit import atexit
import getpass import getpass
from datetime import date, datetime, timedelta
import pyVmomi
import requests import requests
from pyVim.connect import Disconnect, SmartConnect
from pyVmomi import vim, vmodl
# Disable HTTPS verification warnings. # Disable HTTPS verification warnings.
from requests.packages import urllib3 from requests.packages import urllib3
from .common import *
urllib3.disable_warnings() urllib3.disable_warnings()
import os import os
import time
import tarfile
import re import re
import tarfile
import time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from stat import * from stat import *
class backup_vmdk(backup_generic): class backup_vmdk(backup_generic):
type = 'esx-vmdk' type = "esx-vmdk"
required_params = backup_generic.required_params + ['esxhost','password_file','server_name'] required_params = backup_generic.required_params + ["esxhost", "password_file", "server_name"]
optional_params = backup_generic.optional_params + ['esx_port', 'prefix_clone', 'create_ovafile', 'halt_vm'] optional_params = backup_generic.optional_params + ["esx_port", "prefix_clone", "create_ovafile", "halt_vm"]
esx_port = 443 esx_port = 443
prefix_clone = "clone-" prefix_clone = "clone-"
@@ -54,25 +55,22 @@ class backup_vmdk(backup_generic):
def make_compatible_cookie(self, client_cookie): def make_compatible_cookie(self, client_cookie):
cookie_name = client_cookie.split("=", 1)[0] cookie_name = client_cookie.split("=", 1)[0]
cookie_value = client_cookie.split("=", 1)[1].split(";", 1)[0] cookie_value = client_cookie.split("=", 1)[1].split(";", 1)[0]
cookie_path = client_cookie.split("=", 1)[1].split(";", 1)[1].split( cookie_path = client_cookie.split("=", 1)[1].split(";", 1)[1].split(";", 1)[0].lstrip()
";", 1)[0].lstrip()
cookie_text = " " + cookie_value + "; $" + cookie_path cookie_text = " " + cookie_value + "; $" + cookie_path
# Make a cookie # Make a cookie
cookie = dict() cookie = dict()
cookie[cookie_name] = cookie_text cookie[cookie_name] = cookie_text
return cookie return cookie
def download_file(self, url, local_filename, cookie, headers): def download_file(self, url, local_filename, cookie, headers):
r = requests.get(url, stream=True, headers=headers, cookies=cookie, verify=False) r = requests.get(url, stream=True, headers=headers, cookies=cookie, verify=False)
with open(local_filename, 'wb') as f: with open(local_filename, "wb") as f:
for chunk in r.iter_content(chunk_size=1024 * 1024 * 64): for chunk in r.iter_content(chunk_size=1024 * 1024 * 64):
if chunk: if chunk:
f.write(chunk) f.write(chunk)
f.flush() f.flush()
return local_filename return local_filename
def export_vmdks(self, vm): def export_vmdks(self, vm):
HttpNfcLease = vm.ExportVm() HttpNfcLease = vm.ExportVm()
try: try:
@@ -82,12 +80,12 @@ class backup_vmdk(backup_generic):
for device_url in device_urls: for device_url in device_urls:
deviceId = device_url.key deviceId = device_url.key
deviceUrlStr = device_url.url deviceUrlStr = device_url.url
diskFileName = vm.name.replace(self.prefix_clone,'') + "-" + device_url.targetId diskFileName = vm.name.replace(self.prefix_clone, "") + "-" + device_url.targetId
diskUrlStr = deviceUrlStr.replace("*", self.esxhost) diskUrlStr = deviceUrlStr.replace("*", self.esxhost)
diskLocalPath = './' + diskFileName # diskLocalPath = './' + diskFileName
cookie = self.make_compatible_cookie(si._stub.cookie) cookie = self.make_compatible_cookie(si._stub.cookie)
headers = {'Content-Type': 'application/octet-stream'} headers = {"Content-Type": "application/octet-stream"}
self.logger.debug("[%s] exporting disk: %s" % (self.server_name, diskFileName)) self.logger.debug("[%s] exporting disk: %s" % (self.server_name, diskFileName))
self.download_file(diskUrlStr, diskFileName, cookie, headers) self.download_file(diskUrlStr, diskFileName, cookie, headers)
@@ -96,7 +94,6 @@ class backup_vmdk(backup_generic):
HttpNfcLease.Complete() HttpNfcLease.Complete()
return vmdks return vmdks
def create_ovf(self, vm, vmdks): def create_ovf(self, vm, vmdks):
ovfDescParams = vim.OvfManager.CreateDescriptorParams() ovfDescParams = vim.OvfManager.CreateDescriptorParams()
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams) ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
@@ -104,12 +101,12 @@ class backup_vmdk(backup_generic):
new_id = list(root[0][1].attrib.values())[0][1:3] new_id = list(root[0][1].attrib.values())[0][1:3]
ovfFiles = [] ovfFiles = []
for vmdk in vmdks: for vmdk in vmdks:
old_id = vmdk['id'][1:3] old_id = vmdk["id"][1:3]
id = vmdk['id'].replace(old_id,new_id) id = vmdk["id"].replace(old_id, new_id)
ovfFiles.append(vim.OvfManager.OvfFile(size=os.path.getsize(vmdk['filename']), path=vmdk['filename'], deviceId=id)) ovfFiles.append(vim.OvfManager.OvfFile(size=os.path.getsize(vmdk["filename"]), path=vmdk["filename"], deviceId=id))
ovfDescParams = vim.OvfManager.CreateDescriptorParams() ovfDescParams = vim.OvfManager.CreateDescriptorParams()
ovfDescParams.ovfFiles = ovfFiles; ovfDescParams.ovfFiles = ovfFiles
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams) ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
ovf_filename = vm.name + ".ovf" ovf_filename = vm.name + ".ovf"
@@ -125,25 +122,26 @@ class backup_vmdk(backup_generic):
self.logger.debug("[%s] creating ova file: %s" % (self.server_name, ova_filename)) self.logger.debug("[%s] creating ova file: %s" % (self.server_name, ova_filename))
with tarfile.open(ova_filename, "w") as tar: with tarfile.open(ova_filename, "w") as tar:
for vmdk in vmdks: for vmdk in vmdks:
tar.add(vmdk['filename']) tar.add(vmdk["filename"])
os.unlink(vmdk['filename']) os.unlink(vmdk["filename"])
return ova_filename return ova_filename
def clone_vm(self, vm): def clone_vm(self, vm):
task = self.wait_task(vm.CreateSnapshot_Task(name="backup",description="Automatic backup "+datetime.now().strftime("%Y-%m-%d %H:%M:%s"),memory=False,quiesce=True)) task = self.wait_task(
vm.CreateSnapshot_Task(
name="backup", description="Automatic backup " + datetime.now().strftime("%Y-%m-%d %H:%M:%s"), memory=False, quiesce=True
)
)
snapshot = task.info.result snapshot = task.info.result
prefix_vmclone = self.prefix_clone prefix_vmclone = self.prefix_clone
clone_name = prefix_vmclone + vm.name clone_name = prefix_vmclone + vm.name
datastore = '[%s]' % vm.datastore[0].name datastore = "[%s]" % vm.datastore[0].name
vmx_file = vim.vm.FileInfo(logDirectory=None, snapshotDirectory=None, suspendDirectory=None, vmPathName=datastore)
config = vim.vm.ConfigSpec(
vmx_file = vim.vm.FileInfo(logDirectory=None, name=clone_name, memoryMB=vm.summary.config.memorySizeMB, numCPUs=vm.summary.config.numCpu, files=vmx_file
snapshotDirectory=None, )
suspendDirectory=None,
vmPathName=datastore)
config = vim.vm.ConfigSpec(name=clone_name, memoryMB=vm.summary.config.memorySizeMB, numCPUs=vm.summary.config.numCpu, files=vmx_file)
hosts = datacenter.hostFolder.childEntity hosts = datacenter.hostFolder.childEntity
resource_pool = hosts[0].resourcePool resource_pool = hosts[0].resourcePool
@@ -154,23 +152,22 @@ class backup_vmdk(backup_generic):
controller = vim.vm.device.VirtualDeviceSpec() controller = vim.vm.device.VirtualDeviceSpec()
controller.operation = vim.vm.device.VirtualDeviceSpec.Operation.add controller.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
controller.device = vim.vm.device.VirtualLsiLogicController(busNumber=0,sharedBus='noSharing') controller.device = vim.vm.device.VirtualLsiLogicController(busNumber=0, sharedBus="noSharing")
controller.device.key = 0 controller.device.key = 0
i = 0 i = 0
vm_devices = [] vm_devices = []
clone_folder = "%s/" % "/".join(new_vm.summary.config.vmPathName.split('/')[:-1]) clone_folder = "%s/" % "/".join(new_vm.summary.config.vmPathName.split("/")[:-1])
for device in vm.config.hardware.device: for device in vm.config.hardware.device:
if device.__class__.__name__ == 'vim.vm.device.VirtualDisk': if device.__class__.__name__ == "vim.vm.device.VirtualDisk":
cur_vers = int(re.findall(r'\d{3,6}', device.backing.fileName)[0]) cur_vers = int(re.findall(r"\d{3,6}", device.backing.fileName)[0])
if cur_vers == 1: if cur_vers == 1:
source = device.backing.fileName.replace('-000001','') source = device.backing.fileName.replace("-000001", "")
else: else:
source = device.backing.fileName.replace('%d.' % cur_vers,'%d.' % ( cur_vers - 1 )) source = device.backing.fileName.replace("%d." % cur_vers, "%d." % (cur_vers - 1))
dest = clone_folder + source.split("/")[-1]
dest = clone_folder+source.split('/')[-1]
disk_spec = vim.VirtualDiskManager.VirtualDiskSpec(diskType="sparseMonolithic", adapterType="ide") disk_spec = vim.VirtualDiskManager.VirtualDiskSpec(diskType="sparseMonolithic", adapterType="ide")
self.wait_task(si.content.virtualDiskManager.CopyVirtualDisk_Task(sourceName=source, destName=dest, destSpec=disk_spec)) self.wait_task(si.content.virtualDiskManager.CopyVirtualDisk_Task(sourceName=source, destName=dest, destSpec=disk_spec))
# self.wait_task(si.content.virtualDiskManager.ShrinkVirtualDisk_Task(dest)) # self.wait_task(si.content.virtualDiskManager.ShrinkVirtualDisk_Task(dest))
@@ -187,23 +184,19 @@ class backup_vmdk(backup_generic):
vm_devices.append(vdisk_spec) vm_devices.append(vdisk_spec)
i += 1 i += 1
vm_devices.append(controller) vm_devices.append(controller)
config.deviceChange = vm_devices config.deviceChange = vm_devices
self.wait_task(new_vm.ReconfigVM_Task(config)) self.wait_task(new_vm.ReconfigVM_Task(config))
self.wait_task(snapshot.RemoveSnapshot_Task(removeChildren=True)) self.wait_task(snapshot.RemoveSnapshot_Task(removeChildren=True))
return new_vm return new_vm
def wait_task(self, task): def wait_task(self, task):
while task.info.state in ["queued", "running"]: while task.info.state in ["queued", "running"]:
time.sleep(2) time.sleep(2)
self.logger.debug("[%s] %s", self.server_name, task.info.descriptionId) self.logger.debug("[%s] %s", self.server_name, task.info.descriptionId)
return task return task
def do_backup(self, stats): def do_backup(self, stats):
try: try:
dest_dir = os.path.join(self.backup_dir, "%s" % self.backup_start_date) dest_dir = os.path.join(self.backup_dir, "%s" % self.backup_start_date)
@@ -213,22 +206,21 @@ class backup_vmdk(backup_generic):
else: else:
print('mkdir "%s"' % dest_dir) print('mkdir "%s"' % dest_dir)
else: else:
raise Exception('backup destination directory already exists : %s' % dest_dir) raise Exception("backup destination directory already exists : %s" % dest_dir)
os.chdir(dest_dir) os.chdir(dest_dir)
user_esx, password_esx, null = open(self.password_file).read().split('\n') user_esx, password_esx, null = open(self.password_file).read().split("\n")
global si global si
si = SmartConnect(host=self.esxhost, user=user_esx, pwd=password_esx, port=self.esx_port) si = SmartConnect(host=self.esxhost, user=user_esx, pwd=password_esx, port=self.esx_port)
if not si: if not si:
raise Exception("Could not connect to the specified host using specified " raise Exception("Could not connect to the specified host using specified " "username and password")
"username and password")
atexit.register(Disconnect, si) atexit.register(Disconnect, si)
content = si.RetrieveContent() content = si.RetrieveContent()
for child in content.rootFolder.childEntity: for child in content.rootFolder.childEntity:
if hasattr(child, 'vmFolder'): if hasattr(child, "vmFolder"):
global vmFolder, datacenter global vmFolder, datacenter
datacenter = child datacenter = child
vmFolder = datacenter.vmFolder vmFolder = datacenter.vmFolder
@@ -250,33 +242,29 @@ class backup_vmdk(backup_generic):
self.wait_task(new_vm.Destroy_Task()) self.wait_task(new_vm.Destroy_Task())
if str2bool(self.create_ovafile): if str2bool(self.create_ovafile):
ova_filename = self.create_ova(vm, vmdks, ovf_filename) ova_filename = self.create_ova(vm, vmdks, ovf_filename) # noqa : F841
if str2bool(self.halt_vm): if str2bool(self.halt_vm):
vm.PowerOnVM() vm.PowerOnVM()
if os.path.exists(dest_dir): if os.path.exists(dest_dir):
for file in os.listdir(dest_dir): for file in os.listdir(dest_dir):
stats['written_bytes'] += os.stat(file)[ST_SIZE] stats["written_bytes"] += os.stat(file)[ST_SIZE]
stats['total_files_count'] += 1 stats["total_files_count"] += 1
stats['written_files_count'] += 1 stats["written_files_count"] += 1
stats['total_bytes'] = stats['written_bytes'] stats["total_bytes"] = stats["written_bytes"]
else: else:
stats['written_bytes'] = 0 stats["written_bytes"] = 0
stats['backup_location'] = dest_dir stats["backup_location"] = dest_dir
stats['log']='XVA backup from %s OK, %d bytes written' % (self.server_name,stats['written_bytes'])
stats['status']='OK'
stats["log"] = "XVA backup from %s OK, %d bytes written" % (self.server_name, stats["written_bytes"])
stats["status"] = "OK"
except BaseException as e: except BaseException as e:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=str(e) stats["log"] = str(e)
raise raise
register_driver(backup_vmdk) register_driver(backup_vmdk)
+43 -33
View File
@@ -19,73 +19,83 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import paramiko
from .common import * from .common import *
import paramiko
class backup_xcp_metadata(backup_generic): class backup_xcp_metadata(backup_generic):
"""Backup metatdata of a xcp pool using xe pool-dump-database""" """Backup metatdata of a xcp pool using xe pool-dump-database"""
type = 'xcp-dump-metadata'
required_params = ['type','server_name','private_key','backup_name'] type = "xcp-dump-metadata"
required_params = ["type", "server_name", "private_key", "backup_name"]
def do_backup(self, stats): def do_backup(self, stats):
self.logger.debug("[%s] Connecting to %s with user root and key %s", self.backup_name, self.server_name, self.private_key)
self.logger.debug('[%s] Connecting to %s with user root and key %s',self.backup_name,self.server_name,self.private_key)
t = datetime.datetime.now() t = datetime.datetime.now()
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S') backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
# dump pool medatadata # dump pool medatadata
localpath = os.path.join(self.backup_dir , 'xcp_metadata-' + backup_start_date + '.dump') localpath = os.path.join(self.backup_dir, "xcp_metadata-" + backup_start_date + ".dump")
stats['status']='Dumping' stats["status"] = "Dumping"
if not self.dry_run: if not self.dry_run:
cmd = "/opt/xensource/bin/xe pool-dump-database file-name=" cmd = "/opt/xensource/bin/xe pool-dump-database file-name="
self.logger.debug('[%s] Dump XCP Metadata : %s', self.backup_name, cmd) self.logger.debug("[%s] Dump XCP Metadata : %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, server_name=self.server_name,private_key=self.private_key, remote_user='root') (error_code, output) = ssh_exec(cmd, server_name=self.server_name, private_key=self.private_key, remote_user="root")
with open(localpath, "w") as f: with open(localpath, "w") as f:
f.write(output) f.write(output)
# zip the file # zip the file
stats['status']='Zipping' stats["status"] = "Zipping"
cmd = 'gzip %s ' % localpath cmd = "gzip %s " % localpath
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd) self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run: if not self.dry_run:
call_external_process(cmd) call_external_process(cmd)
localpath += ".gz" localpath += ".gz"
if not self.dry_run: if not self.dry_run:
stats['total_files_count']=1 stats["total_files_count"] = 1
stats['written_files_count']=1 stats["written_files_count"] = 1
stats['total_bytes']=os.stat(localpath).st_size stats["total_bytes"] = os.stat(localpath).st_size
stats['written_bytes']=os.stat(localpath).st_size stats["written_bytes"] = os.stat(localpath).st_size
stats['log']='gzip dump of DB %s:%s (%d bytes) to %s' % (self.server_name,'xcp metadata dump', stats['written_bytes'], localpath) stats["log"] = "gzip dump of DB %s:%s (%d bytes) to %s" % (self.server_name, "xcp metadata dump", stats["written_bytes"], localpath)
stats['backup_location'] = localpath stats["backup_location"] = localpath
stats['status']='OK' stats["status"] = "OK"
def register_existingbackups(self): def register_existingbackups(self):
"""scan metatdata backup files and insert stats in database""" """scan metatdata backup files and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
p = re.compile('^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).dump.gz$' % self.server_name) p = re.compile(r"^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).dump.gz$" % self.server_name)
for item in filelist: for item in filelist:
sr = p.match(item) sr = p.match(item)
if sr: if sr:
file_name = os.path.join(self.backup_dir, item) file_name = os.path.join(self.backup_dir, item)
start = datetime.datetime.strptime(sr.groups()[0],'%Y%m%d-%Hh%Mm%S').isoformat() start = datetime.datetime.strptime(sr.groups()[0], "%Y%m%d-%Hh%Mm%S").isoformat()
if not file_name in registered: if file_name not in registered:
self.logger.info('Registering %s from %s',file_name,fileisodate(file_name)) self.logger.info("Registering %s from %s", file_name, fileisodate(file_name))
size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split("\t")[0])
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end=fileisodate(file_name),status='OK',total_bytes=size_bytes,backup_location=file_name) self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=fileisodate(file_name),
status="OK",
total_bytes=size_bytes,
backup_location=file_name,
)
else: else:
self.logger.info('Skipping %s from %s, already registered',file_name,fileisodate(file_name)) self.logger.info("Skipping %s from %s, already registered", file_name, fileisodate(file_name))
register_driver(backup_xcp_metadata) register_driver(backup_xcp_metadata)
+99 -76
View File
@@ -18,31 +18,42 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import logging
import re
import os
import datetime import datetime
import urllib.request, urllib.parse, urllib.error
import socket
import tarfile
import hashlib import hashlib
from stat import * import logging
import os
import re
import socket
import ssl import ssl
import tarfile
import urllib.error
import urllib.parse
import urllib.request
from stat import *
import requests import requests
from .common import *
from . import XenAPI from . import XenAPI
from .common import *
if hasattr(ssl, '_create_unverified_context'): if hasattr(ssl, "_create_unverified_context"):
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
class backup_xva(backup_generic): class backup_xva(backup_generic):
"""Backup a VM running on a XCP server as a XVA file (requires xe tools and XenAPI)""" """Backup a VM running on a XCP server as a XVA file (requires xe tools and XenAPI)"""
type = 'xen-xva'
required_params = backup_generic.required_params + ['xcphost','password_file','server_name'] type = "xen-xva"
optional_params = backup_generic.optional_params + ['enable_https', 'halt_vm', 'verify_export', 'reuse_snapshot', 'ignore_proxies', 'use_compression' ]
required_params = backup_generic.required_params + ["xcphost", "password_file", "server_name"]
optional_params = backup_generic.optional_params + [
"enable_https",
"halt_vm",
"verify_export",
"reuse_snapshot",
"ignore_proxies",
"use_compression",
]
enable_https = "no" enable_https = "no"
halt_vm = "no" halt_vm = "no"
@@ -52,33 +63,32 @@ class backup_xva(backup_generic):
use_compression = "true" use_compression = "true"
if str2bool(ignore_proxies): if str2bool(ignore_proxies):
os.environ['http_proxy']="" os.environ["http_proxy"] = ""
os.environ['https_proxy']="" os.environ["https_proxy"] = ""
def verify_export_xva(self, filename): def verify_export_xva(self, filename):
self.logger.debug("[%s] Verify xva export integrity", self.server_name) self.logger.debug("[%s] Verify xva export integrity", self.server_name)
tar = tarfile.open(filename) tar = tarfile.open(filename)
members = tar.getmembers() members = tar.getmembers()
for tarinfo in members: for tarinfo in members:
if re.search('^[0-9]*$',os.path.basename(tarinfo.name)): if re.search("^[0-9]*$", os.path.basename(tarinfo.name)):
sha1sum = hashlib.sha1(tar.extractfile(tarinfo).read()).hexdigest() sha1sum = hashlib.sha1(tar.extractfile(tarinfo).read()).hexdigest()
sha1sum2 = tar.extractfile(tarinfo.name+'.checksum').read() sha1sum2 = tar.extractfile(tarinfo.name + ".checksum").read()
if not sha1sum == sha1sum2: if not sha1sum == sha1sum2:
raise Exception("File corrupt") raise Exception("File corrupt")
tar.close() tar.close()
def export_xva(self, vdi_name, filename, halt_vm, dry_run, enable_https=True, reuse_snapshot="no"): def export_xva(self, vdi_name, filename, halt_vm, dry_run, enable_https=True, reuse_snapshot="no"):
user_xen, password_xen, null = open(self.password_file).read().split("\n")
user_xen, password_xen, null = open(self.password_file).read().split('\n') session = XenAPI.Session("https://" + self.xcphost)
session = XenAPI.Session('https://'+self.xcphost)
try: try:
session.login_with_password(user_xen, password_xen) session.login_with_password(user_xen, password_xen)
except XenAPI.Failure as error: except XenAPI.Failure as error:
msg, ip = error.details msg, ip = error.details
if msg == 'HOST_IS_SLAVE': if msg == "HOST_IS_SLAVE":
xcphost = ip xcphost = ip
session = XenAPI.Session('https://'+xcphost) session = XenAPI.Session("https://" + xcphost)
session.login_with_password(user_xen, password_xen) session.login_with_password(user_xen, password_xen)
if not session.xenapi.VM.get_by_name_label(vdi_name): if not session.xenapi.VM.get_by_name_label(vdi_name):
@@ -88,22 +98,20 @@ class backup_xva(backup_generic):
status_vm = session.xenapi.VM.get_power_state(vm) status_vm = session.xenapi.VM.get_power_state(vm)
self.logger.debug("[%s] Check if previous fail backups exist", vdi_name) self.logger.debug("[%s] Check if previous fail backups exist", vdi_name)
backups_fail = files = [f for f in os.listdir(self.backup_dir) if f.startswith(vdi_name) and f.endswith(".tmp")] backups_fail = [f for f in os.listdir(self.backup_dir) if f.startswith(vdi_name) and f.endswith(".tmp")]
for backup_fail in backups_fail: for backup_fail in backups_fail:
self.logger.debug('[%s] Delete backup "%s"', vdi_name, backup_fail) self.logger.debug('[%s] Delete backup "%s"', vdi_name, backup_fail)
os.unlink(os.path.join(self.backup_dir, backup_fail)) os.unlink(os.path.join(self.backup_dir, backup_fail))
# add snapshot option # add snapshot option
if not str2bool(halt_vm): if not str2bool(halt_vm):
self.logger.debug("[%s] Check if previous tisbackups snapshots exist", vdi_name) self.logger.debug("[%s] Check if previous tisbackups snapshots exist", vdi_name)
old_snapshots = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vdi_name)) old_snapshots = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vdi_name))
self.logger.debug("[%s] Old snaps count %s", vdi_name, len(old_snapshots)) self.logger.debug("[%s] Old snaps count %s", vdi_name, len(old_snapshots))
if len(old_snapshots) == 1 and str2bool(reuse_snapshot) == True: if len(old_snapshots) == 1 and str2bool(reuse_snapshot):
snapshot = old_snapshots[0] snapshot = old_snapshots[0]
self.logger.debug("[%s] Reusing snap \"%s\"", vdi_name, session.xenapi.VM.get_name_description(snapshot)) self.logger.debug('[%s] Reusing snap "%s"', vdi_name, session.xenapi.VM.get_name_description(snapshot))
vm = snapshot # vm = session.xenapi.VM.get_by_name_label("tisbackup-%s"%(vdi_name))[0] vm = snapshot # vm = session.xenapi.VM.get_by_name_label("tisbackup-%s"%(vdi_name))[0]
else: else:
self.logger.debug("[%s] Deleting %s old snaps", vdi_name, len(old_snapshots)) self.logger.debug("[%s] Deleting %s old snaps", vdi_name, len(old_snapshots))
@@ -111,15 +119,15 @@ class backup_xva(backup_generic):
self.logger.debug("[%s] Destroy snapshot %s", vdi_name, session.xenapi.VM.get_name_description(old_snapshot)) self.logger.debug("[%s] Destroy snapshot %s", vdi_name, session.xenapi.VM.get_name_description(old_snapshot))
try: try:
for vbd in session.xenapi.VM.get_VBDs(old_snapshot): for vbd in session.xenapi.VM.get_VBDs(old_snapshot):
if session.xenapi.VBD.get_type(vbd) == 'CD' and session.xenapi.VBD.get_record(vbd)['empty'] == False: if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd) session.xenapi.VBD.eject(vbd)
else: else:
vdi = session.xenapi.VBD.get_VDI(vbd) vdi = session.xenapi.VBD.get_VDI(vbd)
if not 'NULL' in vdi: if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi) session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(old_snapshot) session.xenapi.VM.destroy(old_snapshot)
except XenAPI.Failure as error: except XenAPI.Failure as error:
return("error when destroy snapshot %s"%(error)) return "error when destroy snapshot %s" % (error)
now = datetime.datetime.now() now = datetime.datetime.now()
self.logger.debug("[%s] Snapshot in progress", vdi_name) self.logger.debug("[%s] Snapshot in progress", vdi_name)
@@ -127,7 +135,7 @@ class backup_xva(backup_generic):
snapshot = session.xenapi.VM.snapshot(vm, "tisbackup-%s" % (vdi_name)) snapshot = session.xenapi.VM.snapshot(vm, "tisbackup-%s" % (vdi_name))
self.logger.debug("[%s] got snapshot %s", vdi_name, snapshot) self.logger.debug("[%s] got snapshot %s", vdi_name, snapshot)
except XenAPI.Failure as error: except XenAPI.Failure as error:
return("error when snapshot %s"%(error)) return "error when snapshot %s" % (error)
# get snapshot opaqueRef # get snapshot opaqueRef
vm = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vdi_name))[0] vm = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vdi_name))[0]
session.xenapi.VM.set_name_description(snapshot, "snapshot created by tisbackup on: %s" % (now.strftime("%Y-%m-%d %H:%M"))) session.xenapi.VM.set_name_description(snapshot, "snapshot created by tisbackup on: %s" % (now.strftime("%Y-%m-%d %H:%M")))
@@ -149,15 +157,13 @@ class backup_xva(backup_generic):
scheme = "http://" scheme = "http://"
if str2bool(enable_https): if str2bool(enable_https):
scheme = "https://" scheme = "https://"
url = scheme+user_xen+":"+password_xen+"@"+self.xcphost+"/export?use_compression="+self.use_compression+"&uuid="+session.xenapi.VM.get_uuid(vm)
# url = scheme+user_xen+":"+password_xen+"@"+self.xcphost+"/export?use_compression="+self.use_compression+"&uuid="+session.xenapi.VM.get_uuid(vm)
top_level_url = (
scheme + self.xcphost + "/export?use_compression=" + self.use_compression + "&uuid=" + session.xenapi.VM.get_uuid(vm)
top_level_url = scheme+self.xcphost+"/export?use_compression="+self.use_compression+"&uuid="+session.xenapi.VM.get_uuid(vm) )
r = requests.get(top_level_url, auth=(user_xen, password_xen)) r = requests.get(top_level_url, auth=(user_xen, password_xen))
open(filename_temp, 'wb').write(r.content) open(filename_temp, "wb").write(r.content)
except Exception as e: except Exception as e:
self.logger.error("[%s] error when fetching snap: %s", "tisbackup-%s" % (vdi_name), e) self.logger.error("[%s] error when fetching snap: %s", "tisbackup-%s" % (vdi_name), e)
@@ -167,18 +173,18 @@ class backup_xva(backup_generic):
finally: finally:
if not str2bool(halt_vm): if not str2bool(halt_vm):
self.logger.debug("[%s] Destroy snapshot",'tisbackup-%s'%(vdi_name)) self.logger.debug("[%s] Destroy snapshot", "tisbackup-%s" % (vdi_name))
try: try:
for vbd in session.xenapi.VM.get_VBDs(snapshot): for vbd in session.xenapi.VM.get_VBDs(snapshot):
if session.xenapi.VBD.get_type(vbd) == 'CD' and session.xenapi.VBD.get_record(vbd)['empty'] == False: if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd) session.xenapi.VBD.eject(vbd)
else: else:
vdi = session.xenapi.VBD.get_VDI(vbd) vdi = session.xenapi.VBD.get_VDI(vbd)
if not 'NULL' in vdi: if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi) session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(snapshot) session.xenapi.VM.destroy(snapshot)
except XenAPI.Failure as error: except XenAPI.Failure as error:
return("error when destroy snapshot %s"%(error)) return "error when destroy snapshot %s" % (error)
elif status_vm == "Running": elif status_vm == "Running":
self.logger.debug("[%s] Starting in progress", self.backup_name) self.logger.debug("[%s] Starting in progress", self.backup_name)
@@ -193,85 +199,102 @@ class backup_xva(backup_generic):
tar = os.system('tar tf "%s" > /dev/null' % filename_temp) tar = os.system('tar tf "%s" > /dev/null' % filename_temp)
if not tar == 0: if not tar == 0:
os.unlink(filename_temp) os.unlink(filename_temp)
return("Tar error") return "Tar error"
if str2bool(self.verify_export): if str2bool(self.verify_export):
self.verify_export_xva(filename_temp) self.verify_export_xva(filename_temp)
os.rename(filename_temp, filename) os.rename(filename_temp, filename)
return(0) return 0
def do_backup(self, stats): def do_backup(self, stats):
try: try:
dest_filename = os.path.join(self.backup_dir,"%s-%s.%s" % (self.backup_name,self.backup_start_date,'xva')) dest_filename = os.path.join(self.backup_dir, "%s-%s.%s" % (self.backup_name, self.backup_start_date, "xva"))
options = [] # options = []
options_params = " ".join(options) # options_params = " ".join(options)
cmd = self.export_xva( vdi_name= self.server_name,filename= dest_filename, halt_vm= self.halt_vm, enable_https=self.enable_https, dry_run= self.dry_run, reuse_snapshot=self.reuse_snapshot) cmd = self.export_xva(
vdi_name=self.server_name,
filename=dest_filename,
halt_vm=self.halt_vm,
enable_https=self.enable_https,
dry_run=self.dry_run,
reuse_snapshot=self.reuse_snapshot,
)
if os.path.exists(dest_filename): if os.path.exists(dest_filename):
stats['written_bytes'] = os.stat(dest_filename)[ST_SIZE] stats["written_bytes"] = os.stat(dest_filename)[ST_SIZE]
stats['total_files_count'] = 1 stats["total_files_count"] = 1
stats['written_files_count'] = 1 stats["written_files_count"] = 1
stats['total_bytes'] = stats['written_bytes'] stats["total_bytes"] = stats["written_bytes"]
else: else:
stats['written_bytes'] = 0 stats["written_bytes"] = 0
stats['backup_location'] = dest_filename stats["backup_location"] = dest_filename
if cmd == 0: if cmd == 0:
stats['log']='XVA backup from %s OK, %d bytes written' % (self.server_name,stats['written_bytes']) stats["log"] = "XVA backup from %s OK, %d bytes written" % (self.server_name, stats["written_bytes"])
stats['status']='OK' stats["status"] = "OK"
else: else:
raise Exception(cmd) raise Exception(cmd)
except BaseException as e: except BaseException as e:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=str(e) stats["log"] = str(e)
raise raise
def register_existingbackups(self): def register_existingbackups(self):
"""scan backup dir and insert stats in database""" """scan backup dir and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',(self.backup_name,))] registered = [
b["backup_location"]
for b in self.dbstat.query("select distinct backup_location from stats where backup_name=?", (self.backup_name,))
]
filelist = os.listdir(self.backup_dir) filelist = os.listdir(self.backup_dir)
filelist.sort() filelist.sort()
for item in filelist: for item in filelist:
if item.endswith('.xva'): if item.endswith(".xva"):
dir_name = os.path.join(self.backup_dir, item) dir_name = os.path.join(self.backup_dir, item)
if not dir_name in registered: if dir_name not in registered:
start = (datetime.datetime.strptime(item,self.backup_name+'-%Y%m%d-%Hh%Mm%S.xva') + datetime.timedelta(0,30*60)).isoformat() start = (
datetime.datetime.strptime(item, self.backup_name + "-%Y%m%d-%Hh%Mm%S.xva") + datetime.timedelta(0, 30 * 60)
).isoformat()
if fileisodate(dir_name) > start: if fileisodate(dir_name) > start:
stop = fileisodate(dir_name) stop = fileisodate(dir_name)
else: else:
stop = start stop = start
self.logger.info('Registering %s started on %s',dir_name,start) self.logger.info("Registering %s started on %s", dir_name, start)
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name) self.logger.debug(" Disk usage %s", 'du -sb "%s"' % dir_name)
if not self.dry_run: if not self.dry_run:
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0]) size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split("\t")[0])
else: else:
size_bytes = 0 size_bytes = 0
self.logger.debug(' Size in bytes : %i',size_bytes) self.logger.debug(" Size in bytes : %i", size_bytes)
if not self.dry_run: if not self.dry_run:
self.dbstat.add(self.backup_name,self.server_name,'',\ self.dbstat.add(
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name,TYPE='BACKUP') self.backup_name,
self.server_name,
"",
backup_start=start,
backup_end=stop,
status="OK",
total_bytes=size_bytes,
backup_location=dir_name,
TYPE="BACKUP",
)
else: else:
self.logger.info('Skipping %s, already registered',dir_name) self.logger.info("Skipping %s, already registered", dir_name)
register_driver(backup_xva) register_driver(backup_xva)
if __name__=='__main__': if __name__ == "__main__":
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
cp = ConfigParser() cp = ConfigParser()
cp.read('/opt/tisbackup/configtest.ini') cp.read("/opt/tisbackup/configtest.ini")
b = backup_xva() b = backup_xva()
b.read_config(cp) b.read_config(cp)
+352 -242
View File
File diff suppressed because it is too large Load Diff
+74 -82
View File
@@ -18,32 +18,35 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os
import datetime
from .common import *
from . import XenAPI
import time
import logging
import re
import os.path
import os
import datetime
import select
import urllib.request, urllib.error, urllib.parse
import base64 import base64
import datetime
import logging
import os
import os.path
import re
import select
import socket import socket
from stat import *
import ssl import ssl
if hasattr(ssl, '_create_unverified_context'): import time
import urllib.error
import urllib.parse
import urllib.request
from stat import *
from . import XenAPI
from .common import *
if hasattr(ssl, "_create_unverified_context"):
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
class copy_vm_xcp(backup_generic): class copy_vm_xcp(backup_generic):
"""Backup a VM running on a XCP server on a second SR (requires xe tools and XenAPI)""" """Backup a VM running on a XCP server on a second SR (requires xe tools and XenAPI)"""
type = 'copy-vm-xcp'
required_params = backup_generic.required_params + ['server_name','storage_name','password_file','vm_name','network_name'] type = "copy-vm-xcp"
optional_params = backup_generic.optional_params + ['start_vm','max_copies', 'delete_snapshot', 'halt_vm']
required_params = backup_generic.required_params + ["server_name", "storage_name", "password_file", "vm_name", "network_name"]
optional_params = backup_generic.optional_params + ["start_vm", "max_copies", "delete_snapshot", "halt_vm"]
start_vm = "no" start_vm = "no"
max_copies = 1 max_copies = 1
@@ -51,30 +54,28 @@ class copy_vm_xcp(backup_generic):
delete_snapshot = "yes" delete_snapshot = "yes"
def read_config(self, iniconf): def read_config(self, iniconf):
assert(isinstance(iniconf,ConfigParser)) assert isinstance(iniconf, ConfigParser)
backup_generic.read_config(self, iniconf) backup_generic.read_config(self, iniconf)
if self.start_vm in 'no' and iniconf.has_option('global','start_vm'): if self.start_vm in "no" and iniconf.has_option("global", "start_vm"):
self.start_vm = iniconf.get('global','start_vm') self.start_vm = iniconf.get("global", "start_vm")
if self.max_copies == 1 and iniconf.has_option('global','max_copies'): if self.max_copies == 1 and iniconf.has_option("global", "max_copies"):
self.max_copies = iniconf.getint('global','max_copies') self.max_copies = iniconf.getint("global", "max_copies")
if self.delete_snapshot == "yes" and iniconf.has_option('global','delete_snapshot'): if self.delete_snapshot == "yes" and iniconf.has_option("global", "delete_snapshot"):
self.delete_snapshot = iniconf.get('global','delete_snapshot') self.delete_snapshot = iniconf.get("global", "delete_snapshot")
def copy_vm_to_sr(self, vm_name, storage_name, dry_run, delete_snapshot="yes"): def copy_vm_to_sr(self, vm_name, storage_name, dry_run, delete_snapshot="yes"):
user_xen, password_xen, null = open(self.password_file).read().split('\n') user_xen, password_xen, null = open(self.password_file).read().split("\n")
session = XenAPI.Session('https://'+self.server_name) session = XenAPI.Session("https://" + self.server_name)
try: try:
session.login_with_password(user_xen, password_xen) session.login_with_password(user_xen, password_xen)
except XenAPI.Failure as error: except XenAPI.Failure as error:
msg, ip = error.details msg, ip = error.details
if msg == 'HOST_IS_SLAVE': if msg == "HOST_IS_SLAVE":
server_name = ip server_name = ip
session = XenAPI.Session('https://'+server_name) session = XenAPI.Session("https://" + server_name)
session.login_with_password(user_xen, password_xen) session.login_with_password(user_xen, password_xen)
self.logger.debug("[%s] VM (%s) to backup in storage: %s", self.backup_name, vm_name, storage_name) self.logger.debug("[%s] VM (%s) to backup in storage: %s", self.backup_name, vm_name, storage_name)
now = datetime.datetime.now() now = datetime.datetime.now()
@@ -85,7 +86,6 @@ class copy_vm_xcp(backup_generic):
result = (1, "error get SR opaqueref %s" % (error)) result = (1, "error get SR opaqueref %s" % (error))
return result return result
# get vm to copy opaqueRef # get vm to copy opaqueRef
try: try:
vm = session.xenapi.VM.get_by_name_label(vm_name)[0] vm = session.xenapi.VM.get_by_name_label(vm_name)[0]
@@ -119,16 +119,12 @@ class copy_vm_xcp(backup_generic):
result = (1, "error when snapshot %s" % (error)) result = (1, "error when snapshot %s" % (error))
return result return result
# get snapshot opaqueRef # get snapshot opaqueRef
snapshot = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vm_name))[0] snapshot = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vm_name))[0]
session.xenapi.VM.set_name_description(snapshot, "snapshot created by tisbackup on : %s" % (now.strftime("%Y-%m-%d %H:%M"))) session.xenapi.VM.set_name_description(snapshot, "snapshot created by tisbackup on : %s" % (now.strftime("%Y-%m-%d %H:%M")))
vm_backup_name = "zzz-%s-" % (vm_name) vm_backup_name = "zzz-%s-" % (vm_name)
# Check if old backup exit # Check if old backup exit
list_backups = [] list_backups = []
for vm_ref in session.xenapi.VM.get_all(): for vm_ref in session.xenapi.VM.get_all():
@@ -139,10 +135,9 @@ class copy_vm_xcp(backup_generic):
list_backups.sort() list_backups.sort()
if len(list_backups) >= 1: if len(list_backups) >= 1:
# Shutting last backup if started # Shutting last backup if started
last_backup_vm = session.xenapi.VM.get_by_name_label(list_backups[-1])[0] last_backup_vm = session.xenapi.VM.get_by_name_label(list_backups[-1])[0]
if not "Halted" in session.xenapi.VM.get_power_state(last_backup_vm): if "Halted" not in session.xenapi.VM.get_power_state(last_backup_vm):
self.logger.debug("[%s] Shutting down last backup vm : %s", self.backup_name, list_backups[-1]) self.logger.debug("[%s] Shutting down last backup vm : %s", self.backup_name, list_backups[-1])
session.xenapi.VM.hard_shutdown(last_backup_vm) session.xenapi.VM.hard_shutdown(last_backup_vm)
@@ -150,18 +145,18 @@ class copy_vm_xcp(backup_generic):
if len(list_backups) >= int(self.max_copies): if len(list_backups) >= int(self.max_copies):
for i in range(len(list_backups) - int(self.max_copies) + 1): for i in range(len(list_backups) - int(self.max_copies) + 1):
oldest_backup_vm = session.xenapi.VM.get_by_name_label(list_backups[i])[0] oldest_backup_vm = session.xenapi.VM.get_by_name_label(list_backups[i])[0]
if not "Halted" in session.xenapi.VM.get_power_state(oldest_backup_vm): if "Halted" not in session.xenapi.VM.get_power_state(oldest_backup_vm):
self.logger.debug("[%s] Shutting down old vm : %s", self.backup_name, list_backups[i]) self.logger.debug("[%s] Shutting down old vm : %s", self.backup_name, list_backups[i])
session.xenapi.VM.hard_shutdown(oldest_backup_vm) session.xenapi.VM.hard_shutdown(oldest_backup_vm)
try: try:
self.logger.debug("[%s] Deleting old vm : %s", self.backup_name, list_backups[i]) self.logger.debug("[%s] Deleting old vm : %s", self.backup_name, list_backups[i])
for vbd in session.xenapi.VM.get_VBDs(oldest_backup_vm): for vbd in session.xenapi.VM.get_VBDs(oldest_backup_vm):
if session.xenapi.VBD.get_type(vbd) == 'CD'and session.xenapi.VBD.get_record(vbd)['empty'] == False: if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd) session.xenapi.VBD.eject(vbd)
else: else:
vdi = session.xenapi.VBD.get_VDI(vbd) vdi = session.xenapi.VBD.get_VDI(vbd)
if not 'NULL' in vdi: if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi) session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(oldest_backup_vm) session.xenapi.VM.destroy(oldest_backup_vm)
@@ -169,7 +164,6 @@ class copy_vm_xcp(backup_generic):
result = (1, "error when destroy old backup vm %s" % (error)) result = (1, "error when destroy old backup vm %s" % (error))
return result return result
self.logger.debug("[%s] Copy %s in progress on %s", self.backup_name, vm_name, storage_name) self.logger.debug("[%s] Copy %s in progress on %s", self.backup_name, vm_name, storage_name)
try: try:
backup_vm = session.xenapi.VM.copy(snapshot, vm_backup_name + now.strftime("%Y-%m-%d %H:%M"), storage) backup_vm = session.xenapi.VM.copy(snapshot, vm_backup_name + now.strftime("%Y-%m-%d %H:%M"), storage)
@@ -177,7 +171,6 @@ class copy_vm_xcp(backup_generic):
result = (1, "error when copy %s" % (error)) result = (1, "error when copy %s" % (error))
return result return result
# define VM as a template # define VM as a template
session.xenapi.VM.set_is_a_template(backup_vm, False) session.xenapi.VM.set_is_a_template(backup_vm, False)
@@ -188,28 +181,28 @@ class copy_vm_xcp(backup_generic):
result = (1, "error get VIF opaqueref %s" % (error)) result = (1, "error get VIF opaqueref %s" % (error))
return result return result
for i in vifDestroy: for i in vifDestroy:
vifRecord = session.xenapi.VIF.get_record(i) vifRecord = session.xenapi.VIF.get_record(i)
session.xenapi.VIF.destroy(i) session.xenapi.VIF.destroy(i)
data = {'MAC': vifRecord['MAC'], data = {
'MAC_autogenerated': False, "MAC": vifRecord["MAC"],
'MTU': vifRecord['MTU'], "MAC_autogenerated": False,
'VM': backup_vm, "MTU": vifRecord["MTU"],
'current_operations': vifRecord['current_operations'], "VM": backup_vm,
'currently_attached': vifRecord['currently_attached'], "current_operations": vifRecord["current_operations"],
'device': vifRecord['device'], "currently_attached": vifRecord["currently_attached"],
'ipv4_allowed': vifRecord['ipv4_allowed'], "device": vifRecord["device"],
'ipv6_allowed': vifRecord['ipv6_allowed'], "ipv4_allowed": vifRecord["ipv4_allowed"],
'locking_mode': vifRecord['locking_mode'], "ipv6_allowed": vifRecord["ipv6_allowed"],
'network': networkRef, "locking_mode": vifRecord["locking_mode"],
'other_config': vifRecord['other_config'], "network": networkRef,
'qos_algorithm_params': vifRecord['qos_algorithm_params'], "other_config": vifRecord["other_config"],
'qos_algorithm_type': vifRecord['qos_algorithm_type'], "qos_algorithm_params": vifRecord["qos_algorithm_params"],
'qos_supported_algorithms': vifRecord['qos_supported_algorithms'], "qos_algorithm_type": vifRecord["qos_algorithm_type"],
'runtime_properties': vifRecord['runtime_properties'], "qos_supported_algorithms": vifRecord["qos_supported_algorithms"],
'status_code': vifRecord['status_code'], "runtime_properties": vifRecord["runtime_properties"],
'status_detail': vifRecord['status_detail'] "status_code": vifRecord["status_code"],
"status_detail": vifRecord["status_detail"],
} }
try: try:
session.xenapi.VIF.create(data) session.xenapi.VIF.create(data)
@@ -217,38 +210,37 @@ class copy_vm_xcp(backup_generic):
result = (1, error) result = (1, error)
return result return result
if self.start_vm in ["true", "1", "t", "y", "yes", "oui"]:
if self.start_vm in ['true', '1', 't', 'y', 'yes', 'oui']:
session.xenapi.VM.start(backup_vm, False, True) session.xenapi.VM.start(backup_vm, False, True)
session.xenapi.VM.set_name_description(backup_vm, "snapshot created by tisbackup on : %s" % (now.strftime("%Y-%m-%d %H:%M"))) session.xenapi.VM.set_name_description(backup_vm, "snapshot created by tisbackup on : %s" % (now.strftime("%Y-%m-%d %H:%M")))
size_backup = 0 size_backup = 0
for vbd in session.xenapi.VM.get_VBDs(backup_vm): for vbd in session.xenapi.VM.get_VBDs(backup_vm):
if session.xenapi.VBD.get_type(vbd) == 'CD' and session.xenapi.VBD.get_record(vbd)['empty'] == False: if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd) session.xenapi.VBD.eject(vbd)
else: else:
vdi = session.xenapi.VBD.get_VDI(vbd) vdi = session.xenapi.VBD.get_VDI(vbd)
if not 'NULL' in vdi: if "NULL" not in vdi:
size_backup = size_backup + int(session.xenapi.VDI.get_record(vdi)['physical_utilisation']) size_backup = size_backup + int(session.xenapi.VDI.get_record(vdi)["physical_utilisation"])
result = (0, size_backup) result = (0, size_backup)
if self.delete_snapshot == 'no': if self.delete_snapshot == "no":
return result return result
# Disable automatic boot # Disable automatic boot
if 'auto_poweron' in session.xenapi.VM.get_other_config(backup_vm): if "auto_poweron" in session.xenapi.VM.get_other_config(backup_vm):
session.xenapi.VM.remove_from_other_config(backup_vm, "auto_poweron") session.xenapi.VM.remove_from_other_config(backup_vm, "auto_poweron")
if not str2bool(self.halt_vm): if not str2bool(self.halt_vm):
# delete the snapshot # delete the snapshot
try: try:
for vbd in session.xenapi.VM.get_VBDs(snapshot): for vbd in session.xenapi.VM.get_VBDs(snapshot):
if session.xenapi.VBD.get_type(vbd) == 'CD' and session.xenapi.VBD.get_record(vbd)['empty'] == False: if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd) session.xenapi.VBD.eject(vbd)
else: else:
vdi = session.xenapi.VBD.get_VDI(vbd) vdi = session.xenapi.VBD.get_VDI(vbd)
if not 'NULL' in vdi: if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi) session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(snapshot) session.xenapi.VM.destroy(snapshot)
except XenAPI.Failure as error: except XenAPI.Failure as error:
@@ -264,27 +256,26 @@ class copy_vm_xcp(backup_generic):
return result return result
def do_backup(self, stats): def do_backup(self, stats):
try: try:
timestamp = int(time.time()) # timestamp = int(time.time())
cmd = self.copy_vm_to_sr(self.vm_name, self.storage_name, self.dry_run, delete_snapshot=self.delete_snapshot) cmd = self.copy_vm_to_sr(self.vm_name, self.storage_name, self.dry_run, delete_snapshot=self.delete_snapshot)
if cmd[0] == 0: if cmd[0] == 0:
timeExec = int(time.time()) - timestamp # timeExec = int(time.time()) - timestamp
stats['log']='copy of %s to an other storage OK' % (self.backup_name) stats["log"] = "copy of %s to an other storage OK" % (self.backup_name)
stats['status']='OK' stats["status"] = "OK"
stats['total_files_count'] = 1 stats["total_files_count"] = 1
stats['total_bytes'] = cmd[1] stats["total_bytes"] = cmd[1]
stats['backup_location'] = self.storage_name stats["backup_location"] = self.storage_name
else: else:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=cmd[1] stats["log"] = cmd[1]
except BaseException as e: except BaseException as e:
stats['status']='ERROR' stats["status"] = "ERROR"
stats['log']=str(e) stats["log"] = str(e)
raise raise
def register_existingbackups(self): def register_existingbackups(self):
@@ -292,4 +283,5 @@ class copy_vm_xcp(backup_generic):
# This backup is on target server, no data available on this server # This backup is on target server, no data available on this server
pass pass
register_driver(copy_vm_xcp) register_driver(copy_vm_xcp)
+26 -12
View File
@@ -8,18 +8,32 @@ from .config import BasicConfig, ConfigNamespace
from .compat import RawConfigParser, ConfigParser, SafeConfigParser from .compat import RawConfigParser, ConfigParser, SafeConfigParser
from .utils import tidy from .utils import tidy
from .configparser import DuplicateSectionError, \ from .configparser import (
NoSectionError, NoOptionError, \ DuplicateSectionError,
InterpolationMissingOptionError, \ NoSectionError,
InterpolationDepthError, \ NoOptionError,
InterpolationSyntaxError, \ InterpolationMissingOptionError,
DEFAULTSECT, MAX_INTERPOLATION_DEPTH InterpolationDepthError,
InterpolationSyntaxError,
DEFAULTSECT,
MAX_INTERPOLATION_DEPTH,
)
__all__ = [ __all__ = [
'BasicConfig', 'ConfigNamespace', "BasicConfig",
'INIConfig', 'tidy', 'change_comment_syntax', "ConfigNamespace",
'RawConfigParser', 'ConfigParser', 'SafeConfigParser', "INIConfig",
'DuplicateSectionError', 'NoSectionError', 'NoOptionError', "tidy",
'InterpolationMissingOptionError', 'InterpolationDepthError', "change_comment_syntax",
'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH', "RawConfigParser",
"ConfigParser",
"SafeConfigParser",
"DuplicateSectionError",
"NoSectionError",
"NoOptionError",
"InterpolationMissingOptionError",
"InterpolationDepthError",
"InterpolationSyntaxError",
"DEFAULTSECT",
"MAX_INTERPOLATION_DEPTH",
] ]
+87 -73
View File
@@ -12,44 +12,49 @@ The underlying INIConfig object can be accessed as cfg.data
""" """
import re import re
from .configparser import DuplicateSectionError, \ from typing import Dict, List, TextIO, Optional, Type, Union, Tuple
NoSectionError, NoOptionError, \
InterpolationMissingOptionError, \
InterpolationDepthError, \
InterpolationSyntaxError, \
DEFAULTSECT, MAX_INTERPOLATION_DEPTH
# These are imported only for compatiability. from .configparser import (
DuplicateSectionError,
NoSectionError,
NoOptionError,
InterpolationMissingOptionError,
InterpolationDepthError,
InterpolationSyntaxError,
DEFAULTSECT,
MAX_INTERPOLATION_DEPTH,
)
# These are imported only for compatibility.
# The code below does not reference them directly. # The code below does not reference them directly.
from .configparser import Error, InterpolationError, \ from .configparser import Error, InterpolationError, MissingSectionHeaderError, ParsingError
MissingSectionHeaderError, ParsingError
import six
from . import ini from . import ini
class RawConfigParser(object): class RawConfigParser:
def __init__(self, defaults=None, dict_type=dict): def __init__(self, defaults=None, dict_type=dict):
if dict_type != dict: if dict_type is not dict:
raise ValueError('Custom dict types not supported') raise ValueError("Custom dict types not supported")
self.data = ini.INIConfig(defaults=defaults, optionxformsource=self) self.data = ini.INIConfig(defaults=defaults, optionxformsource=self)
def optionxform(self, optionstr): def optionxform(self, optionstr: str) -> str:
return optionstr.lower() return optionstr.lower()
def defaults(self): def defaults(self) -> Dict[str, str]:
d = {} d: Dict[str, str] = {}
secobj = self.data._defaults secobj: ini.INISection = self.data._defaults
name: str
for name in secobj._options: for name in secobj._options:
d[name] = secobj._compat_get(name) d[name] = secobj._compat_get(name)
return d return d
def sections(self): def sections(self) -> List[str]:
"""Return a list of section names, excluding [DEFAULT]""" """Return a list of section names, excluding [DEFAULT]"""
return list(self.data) return list(self.data)
def add_section(self, section): def add_section(self, section: str) -> None:
"""Create a new section in the configuration. """Create a new section in the configuration.
Raise DuplicateSectionError if a section by the specified name Raise DuplicateSectionError if a section by the specified name
@@ -59,28 +64,28 @@ class RawConfigParser(object):
# The default section is the only one that gets the case-insensitive # The default section is the only one that gets the case-insensitive
# treatment - so it is special-cased here. # treatment - so it is special-cased here.
if section.lower() == "default": if section.lower() == "default":
raise ValueError('Invalid section name: %s' % section) raise ValueError("Invalid section name: %s" % section)
if self.has_section(section): if self.has_section(section):
raise DuplicateSectionError(section) raise DuplicateSectionError(section)
else: else:
self.data._new_namespace(section) self.data._new_namespace(section)
def has_section(self, section): def has_section(self, section: str) -> bool:
"""Indicate whether the named section is present in the configuration. """Indicate whether the named section is present in the configuration.
The DEFAULT section is not acknowledged. The DEFAULT section is not acknowledged.
""" """
return section in self.data return section in self.data
def options(self, section): def options(self, section: str) -> List[str]:
"""Return a list of option names for the given section name.""" """Return a list of option names for the given section name."""
if section in self.data: if section in self.data:
return list(self.data[section]) return list(self.data[section])
else: else:
raise NoSectionError(section) raise NoSectionError(section)
def read(self, filenames): def read(self, filenames: Union[List[str], str]) -> List[str]:
"""Read and parse a filename or a list of filenames. """Read and parse a filename or a list of filenames.
Files that cannot be opened are silently ignored; this is Files that cannot be opened are silently ignored; this is
@@ -89,9 +94,11 @@ class RawConfigParser(object):
home directory, systemwide directory), and all existing home directory, systemwide directory), and all existing
configuration files in the list will be read. A single configuration files in the list will be read. A single
filename may also be given. filename may also be given.
Returns the list of files that were read.
""" """
files_read = [] files_read = []
if isinstance(filenames, six.string_types): if isinstance(filenames, str):
filenames = [filenames] filenames = [filenames]
for filename in filenames: for filename in filenames:
try: try:
@@ -103,7 +110,7 @@ class RawConfigParser(object):
fp.close() fp.close()
return files_read return files_read
def readfp(self, fp, filename=None): def readfp(self, fp: TextIO, filename: Optional[str] = None) -> None:
"""Like read() but the argument must be a file-like object. """Like read() but the argument must be a file-like object.
The `fp' argument must have a `readline' method. Optional The `fp' argument must have a `readline' method. Optional
@@ -113,60 +120,70 @@ class RawConfigParser(object):
""" """
self.data._readfp(fp) self.data._readfp(fp)
def get(self, section, option, vars=None): def get(self, section: str, option: str, vars: dict = None) -> str:
if not self.has_section(section): if not self.has_section(section):
raise NoSectionError(section) raise NoSectionError(section)
sec = self.data[section] sec: ini.INISection = self.data[section]
if option in sec: if option in sec:
return sec._compat_get(option) return sec._compat_get(option)
else: else:
raise NoOptionError(option, section) raise NoOptionError(option, section)
def items(self, section): def items(self, section: str) -> List[Tuple[str, str]]:
if section in self.data: if section in self.data:
ans = [] ans = []
opt: str
for opt in self.data[section]: for opt in self.data[section]:
ans.append((opt, self.get(section, opt))) ans.append((opt, self.get(section, opt)))
return ans return ans
else: else:
raise NoSectionError(section) raise NoSectionError(section)
def getint(self, section, option): def getint(self, section: str, option: str) -> int:
return int(self.get(section, option)) return int(self.get(section, option))
def getfloat(self, section, option): def getfloat(self, section: str, option: str) -> float:
return float(self.get(section, option)) return float(self.get(section, option))
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, _boolean_states = {
'0': False, 'no': False, 'false': False, 'off': False} "1": True,
"yes": True,
"true": True,
"on": True,
"0": False,
"no": False,
"false": False,
"off": False,
}
def getboolean(self, section, option): def getboolean(self, section: str, option: str) -> bool:
v = self.get(section, option) v = self.get(section, option)
if v.lower() not in self._boolean_states: if v.lower() not in self._boolean_states:
raise ValueError('Not a boolean: %s' % v) raise ValueError("Not a boolean: %s" % v)
return self._boolean_states[v.lower()] return self._boolean_states[v.lower()]
def has_option(self, section, option): def has_option(self, section: str, option: str) -> bool:
"""Check for the existence of a given option in a given section.""" """Check for the existence of a given option in a given section."""
if section in self.data: if section in self.data:
sec = self.data[section] sec = self.data[section]
else: else:
raise NoSectionError(section) raise NoSectionError(section)
return (option in sec) return option in sec
def set(self, section, option, value): def set(self, section: str, option: str, value: str) -> None:
"""Set an option.""" """Set an option."""
if section in self.data: if section in self.data:
self.data[section][option] = value self.data[section][option] = value
else: else:
raise NoSectionError(section) raise NoSectionError(section)
def write(self, fp): def write(self, fp: TextIO) -> None:
"""Write an .ini-format representation of the configuration state.""" """Write an .ini-format representation of the configuration state."""
fp.write(str(self.data)) fp.write(str(self.data))
def remove_option(self, section, option): # FIXME Return a boolean instead of integer
def remove_option(self, section: str, option: str) -> int:
"""Remove an option.""" """Remove an option."""
if section in self.data: if section in self.data:
sec = self.data[section] sec = self.data[section]
@@ -178,7 +195,7 @@ class RawConfigParser(object):
else: else:
return 0 return 0
def remove_section(self, section): def remove_section(self, section: str) -> bool:
"""Remove a file section.""" """Remove a file section."""
if not self.has_section(section): if not self.has_section(section):
return False return False
@@ -186,15 +203,15 @@ class RawConfigParser(object):
return True return True
class ConfigDict(object): class ConfigDict:
"""Present a dict interface to a ini section.""" """Present a dict interface to an ini section."""
def __init__(self, cfg, section, vars): def __init__(self, cfg: RawConfigParser, section: str, vars: dict):
self.cfg = cfg self.cfg: RawConfigParser = cfg
self.section = section self.section: str = section
self.vars = vars self.vars: dict = vars
def __getitem__(self, key): def __getitem__(self, key: str) -> Union[str, List[Union[int, str]]]:
try: try:
return RawConfigParser.get(self.cfg, self.section, key, self.vars) return RawConfigParser.get(self.cfg, self.section, key, self.vars)
except (NoOptionError, NoSectionError): except (NoOptionError, NoSectionError):
@@ -202,8 +219,13 @@ class ConfigDict(object):
class ConfigParser(RawConfigParser): class ConfigParser(RawConfigParser):
def get(
def get(self, section, option, raw=False, vars=None): self,
section: str,
option: str,
raw: bool = False,
vars: Optional[dict] = None,
) -> object:
"""Get an option value for a given section. """Get an option value for a given section.
All % interpolations are expanded in the return values, based on the All % interpolations are expanded in the return values, based on the
@@ -226,7 +248,7 @@ class ConfigParser(RawConfigParser):
d = ConfigDict(self, section, vars) d = ConfigDict(self, section, vars)
return self._interpolate(section, option, value, d) return self._interpolate(section, option, value, d)
def _interpolate(self, section, option, rawval, vars): def _interpolate(self, section: str, option: str, rawval: object, vars: "ConfigDict"):
# do the string interpolation # do the string interpolation
value = rawval value = rawval
depth = MAX_INTERPOLATION_DEPTH depth = MAX_INTERPOLATION_DEPTH
@@ -236,15 +258,14 @@ class ConfigParser(RawConfigParser):
try: try:
value = value % vars value = value % vars
except KeyError as e: except KeyError as e:
raise InterpolationMissingOptionError( raise InterpolationMissingOptionError(option, section, rawval, e.args[0])
option, section, rawval, e.args[0])
else: else:
break break
if value.find("%(") != -1: if value.find("%(") != -1:
raise InterpolationDepthError(option, section, rawval) raise InterpolationDepthError(option, section, rawval)
return value return value
def items(self, section, raw=False, vars=None): def items(self, section: str, raw: bool = False, vars: Optional[dict] = None):
"""Return a list of tuples with (name, value) for each option """Return a list of tuples with (name, value) for each option
in the section. in the section.
@@ -272,40 +293,37 @@ class ConfigParser(RawConfigParser):
d = ConfigDict(self, section, vars) d = ConfigDict(self, section, vars)
if raw: if raw:
return [(option, d[option]) return [(option, d[option]) for option in options]
for option in options]
else: else:
return [(option, self._interpolate(section, option, d[option], d)) return [(option, self._interpolate(section, option, d[option], d)) for option in options]
for option in options]
class SafeConfigParser(ConfigParser): class SafeConfigParser(ConfigParser):
_interpvar_re = re.compile(r"%\(([^)]+)\)s") _interpvar_re = re.compile(r"%\(([^)]+)\)s")
_badpercent_re = re.compile(r"%[^%]|%$") _badpercent_re = re.compile(r"%[^%]|%$")
def set(self, section, option, value): def set(self, section: str, option: str, value: object) -> None:
if not isinstance(value, six.string_types): if not isinstance(value, str):
raise TypeError("option values must be strings") raise TypeError("option values must be strings")
# check for bad percent signs: # check for bad percent signs:
# first, replace all "good" interpolations # first, replace all "good" interpolations
tmp_value = self._interpvar_re.sub('', value) tmp_value = self._interpvar_re.sub("", value)
# then, check if there's a lone percent sign left # then, check if there's a lone percent sign left
m = self._badpercent_re.search(tmp_value) m = self._badpercent_re.search(tmp_value)
if m: if m:
raise ValueError("invalid interpolation syntax in %r at " raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, m.start()))
"position %d" % (value, m.start()))
ConfigParser.set(self, section, option, value) ConfigParser.set(self, section, option, value)
def _interpolate(self, section, option, rawval, vars): def _interpolate(self, section: str, option: str, rawval: str, vars: ConfigDict):
# do the string interpolation # do the string interpolation
L = [] L = []
self._interpolate_some(option, L, rawval, section, vars, 1) self._interpolate_some(option, L, rawval, section, vars, 1)
return ''.join(L) return "".join(L)
_interpvar_match = re.compile(r"%\(([^)]+)\)s").match _interpvar_match = re.compile(r"%\(([^)]+)\)s").match
def _interpolate_some(self, option, accum, rest, section, map, depth): def _interpolate_some(self, option: str, accum: List[str], rest: str, section: str, map: ConfigDict, depth: int) -> None:
if depth > MAX_INTERPOLATION_DEPTH: if depth > MAX_INTERPOLATION_DEPTH:
raise InterpolationDepthError(option, section, rest) raise InterpolationDepthError(option, section, rest)
while rest: while rest:
@@ -330,14 +348,10 @@ class SafeConfigParser(ConfigParser):
try: try:
v = map[var] v = map[var]
except KeyError: except KeyError:
raise InterpolationMissingOptionError( raise InterpolationMissingOptionError(option, section, rest, var)
option, section, rest, var)
if "%" in v: if "%" in v:
self._interpolate_some(option, accum, v, self._interpolate_some(option, accum, v, section, map, depth + 1)
section, map, depth + 1)
else: else:
accum.append(v) accum.append(v)
else: else:
raise InterpolationSyntaxError( raise InterpolationSyntaxError(option, section, "'%' must be followed by '%' or '(', found: " + repr(rest))
option, section,
"'%' must be followed by '%' or '(', found: " + repr(rest))
+70 -53
View File
@@ -1,4 +1,10 @@
class ConfigNamespace(object): from typing import Dict, Iterable, List, TextIO, Union, TYPE_CHECKING
if TYPE_CHECKING:
from .ini import INIConfig, INISection
class ConfigNamespace:
"""Abstract class representing the interface of Config objects. """Abstract class representing the interface of Config objects.
A ConfigNamespace is a collection of names mapped to values, where A ConfigNamespace is a collection of names mapped to values, where
@@ -12,27 +18,27 @@ class ConfigNamespace(object):
Subclasses must implement the methods for container-like access, Subclasses must implement the methods for container-like access,
and this class will automatically provide dotted access. and this class will automatically provide dotted access.
""" """
# Methods that must be implemented by subclasses # Methods that must be implemented by subclasses
def _getitem(self, key): def _getitem(self, key: str) -> object:
return NotImplementedError(key) return NotImplementedError(key)
def __setitem__(self, key, value): def __setitem__(self, key: str, value: object):
raise NotImplementedError(key, value) raise NotImplementedError(key, value)
def __delitem__(self, key): def __delitem__(self, key: str) -> None:
raise NotImplementedError(key) raise NotImplementedError(key)
def __iter__(self): def __iter__(self) -> Iterable[str]:
# FIXME Raise instead return
return NotImplementedError() return NotImplementedError()
def _new_namespace(self, name): def _new_namespace(self, name: str) -> "ConfigNamespace":
raise NotImplementedError(name) raise NotImplementedError(name)
def __contains__(self, key): def __contains__(self, key: str) -> bool:
try: try:
self._getitem(key) self._getitem(key)
except KeyError: except KeyError:
@@ -44,35 +50,35 @@ class ConfigNamespace(object):
# #
# To distinguish between accesses of class members and namespace # To distinguish between accesses of class members and namespace
# keys, we first call object.__getattribute__(). If that succeeds, # keys, we first call object.__getattribute__(). If that succeeds,
# the name is assumed to be a class member. Otherwise it is # the name is assumed to be a class member. Otherwise, it is
# treated as a namespace key. # treated as a namespace key.
# #
# Therefore, member variables should be defined in the class, # Therefore, member variables should be defined in the class,
# not just in the __init__() function. See BasicNamespace for # not just in the __init__() function. See BasicNamespace for
# an example. # an example.
def __getitem__(self, key): def __getitem__(self, key: str) -> Union[object, "Undefined"]:
try: try:
return self._getitem(key) return self._getitem(key)
except KeyError: except KeyError:
return Undefined(key, self) return Undefined(key, self)
def __getattr__(self, name): def __getattr__(self, name: str) -> Union[object, "Undefined"]:
try: try:
return self._getitem(name) return self._getitem(name)
except KeyError: except KeyError:
if name.startswith('__') and name.endswith('__'): if name.startswith("__") and name.endswith("__"):
raise AttributeError raise AttributeError
return Undefined(name, self) return Undefined(name, self)
def __setattr__(self, name, value): def __setattr__(self, name: str, value: object) -> None:
try: try:
object.__getattribute__(self, name) object.__getattribute__(self, name)
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
except AttributeError: except AttributeError:
self.__setitem__(name, value) self.__setitem__(name, value)
def __delattr__(self, name): def __delattr__(self, name: str) -> None:
try: try:
object.__getattribute__(self, name) object.__getattribute__(self, name)
object.__delattr__(self, name) object.__delattr__(self, name)
@@ -82,12 +88,12 @@ class ConfigNamespace(object):
# During unpickling, Python checks if the class has a __setstate__ # During unpickling, Python checks if the class has a __setstate__
# method. But, the data dicts have not been initialised yet, which # method. But, the data dicts have not been initialised yet, which
# leads to _getitem and hence __getattr__ raising an exception. So # leads to _getitem and hence __getattr__ raising an exception. So
# we explicitly impement default __setstate__ behavior. # we explicitly implement default __setstate__ behavior.
def __setstate__(self, state): def __setstate__(self, state: dict) -> None:
self.__dict__.update(state) self.__dict__.update(state)
class Undefined(object): class Undefined:
"""Helper class used to hold undefined names until assignment. """Helper class used to hold undefined names until assignment.
This class helps create any undefined subsections when an This class helps create any undefined subsections when an
@@ -95,21 +101,24 @@ class Undefined(object):
statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet. statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet.
""" """
def __init__(self, name, namespace): def __init__(self, name: str, namespace: ConfigNamespace):
object.__setattr__(self, 'name', name) # FIXME These assignments into `object` feel very strange.
object.__setattr__(self, 'namespace', namespace) # What's the reason for it?
object.__setattr__(self, "name", name)
object.__setattr__(self, "namespace", namespace)
def __setattr__(self, name, value): def __setattr__(self, name: str, value: object) -> None:
obj = self.namespace._new_namespace(self.name) obj = self.namespace._new_namespace(self.name)
obj[name] = value obj[name] = value
def __setitem__(self, name, value): def __setitem__(self, name, value) -> None:
obj = self.namespace._new_namespace(self.name) obj = self.namespace._new_namespace(self.name)
obj[name] = value obj[name] = value
# ---- Basic implementation of a ConfigNamespace # ---- Basic implementation of a ConfigNamespace
class BasicConfig(ConfigNamespace): class BasicConfig(ConfigNamespace):
"""Represents a hierarchical collection of named values. """Represents a hierarchical collection of named values.
@@ -161,7 +170,7 @@ class BasicConfig(ConfigNamespace):
Finally, values can be read from a file as follows: Finally, values can be read from a file as follows:
>>> from six import StringIO >>> from io import StringIO
>>> sio = StringIO(''' >>> sio = StringIO('''
... # comment ... # comment
... ui.height = 100 ... ui.height = 100
@@ -181,66 +190,73 @@ class BasicConfig(ConfigNamespace):
""" """
# this makes sure that __setattr__ knows this is not a namespace key # this makes sure that __setattr__ knows this is not a namespace key
_data = None _data: Dict[str, str] = None
def __init__(self): def __init__(self):
self._data = {} self._data = {}
def _getitem(self, key): def _getitem(self, key: str) -> str:
return self._data[key] return self._data[key]
def __setitem__(self, key, value): def __setitem__(self, key: str, value: object) -> None:
# FIXME We can add any object as 'value', but when an integer is read
# from a file, it will be a string. Should we explicitly convert
# this 'value' to string, to ensure consistency?
# It will stay the original type until it is written to a file.
self._data[key] = value self._data[key] = value
def __delitem__(self, key): def __delitem__(self, key: str) -> None:
del self._data[key] del self._data[key]
def __iter__(self): def __iter__(self) -> Iterable[str]:
return iter(self._data) return iter(self._data)
def __str__(self, prefix=''): def __str__(self, prefix: str = "") -> str:
lines = [] lines: List[str] = []
keys = list(self._data.keys()) keys: List[str] = list(self._data.keys())
keys.sort() keys.sort()
for name in keys: for name in keys:
value = self._data[name] value: object = self._data[name]
if isinstance(value, ConfigNamespace): if isinstance(value, ConfigNamespace):
lines.append(value.__str__(prefix='%s%s.' % (prefix,name))) lines.append(value.__str__(prefix="%s%s." % (prefix, name)))
else: else:
if value is None: if value is None:
lines.append('%s%s' % (prefix, name)) lines.append("%s%s" % (prefix, name))
else: else:
lines.append('%s%s = %s' % (prefix, name, value)) lines.append("%s%s = %s" % (prefix, name, value))
return '\n'.join(lines) return "\n".join(lines)
def _new_namespace(self, name): def _new_namespace(self, name: str) -> "BasicConfig":
obj = BasicConfig() obj = BasicConfig()
self._data[name] = obj self._data[name] = obj
return obj return obj
def _readfp(self, fp): def _readfp(self, fp: TextIO) -> None:
while True: while True:
line = fp.readline() line: str = fp.readline()
if not line: if not line:
break break
line = line.strip() line = line.strip()
if not line: continue if not line:
if line[0] == '#': continue continue
data = line.split('=', 1) if line[0] == "#":
continue
data: List[str] = line.split("=", 1)
if len(data) == 1: if len(data) == 1:
name = line name = line
value = None value = None
else: else:
name = data[0].strip() name = data[0].strip()
value = data[1].strip() value = data[1].strip()
name_components = name.split('.') name_components = name.split(".")
ns = self ns: ConfigNamespace = self
for n in name_components[:-1]: for n in name_components[:-1]:
if n in ns: if n in ns:
ns = ns[n] maybe_ns: object = ns[n]
if not isinstance(ns, ConfigNamespace): if not isinstance(maybe_ns, ConfigNamespace):
raise TypeError('value-namespace conflict', n) raise TypeError("value-namespace conflict", n)
ns = maybe_ns
else: else:
ns = ns._new_namespace(n) ns = ns._new_namespace(n)
ns[name_components[-1]] = value ns[name_components[-1]] = value
@@ -248,7 +264,8 @@ class BasicConfig(ConfigNamespace):
# ---- Utility functions # ---- Utility functions
def update_config(target, source):
def update_config(target: ConfigNamespace, source: ConfigNamespace):
"""Imports values from source into target. """Imports values from source into target.
Recursively walks the <source> ConfigNamespace and inserts values Recursively walks the <source> ConfigNamespace and inserts values
@@ -276,15 +293,15 @@ def update_config(target, source):
display_clock = True display_clock = True
display_qlength = True display_qlength = True
width = 150 width = 150
""" """
for name in sorted(source): for name in sorted(source):
value = source[name] value: object = source[name]
if isinstance(value, ConfigNamespace): if isinstance(value, ConfigNamespace):
if name in target: if name in target:
myns = target[name] maybe_myns: object = target[name]
if not isinstance(myns, ConfigNamespace): if not isinstance(maybe_myns, ConfigNamespace):
raise TypeError('value-namespace conflict') raise TypeError("value-namespace conflict")
myns = maybe_myns
else: else:
myns = target._new_namespace(name) myns = target._new_namespace(name)
update_config(myns, value) update_config(myns, value)
-5
View File
@@ -1,7 +1,2 @@
try:
from ConfigParser import *
# not all objects get imported with __all__
from ConfigParser import Error, InterpolationMissingOptionError
except ImportError:
from configparser import * from configparser import *
from configparser import Error, InterpolationMissingOptionError from configparser import Error, InterpolationMissingOptionError
+233 -199
View File
@@ -7,7 +7,7 @@
Example: Example:
>>> from six import StringIO >>> from io import StringIO
>>> sio = StringIO('''# configure foo-application >>> sio = StringIO('''# configure foo-application
... [foo] ... [foo]
... bar1 = qualia ... bar1 = qualia
@@ -39,26 +39,31 @@ Example:
# An ini parser that supports ordered sections/options # An ini parser that supports ordered sections/options
# Also supports updates, while preserving structure # Also supports updates, while preserving structure
# Backward-compatiable with ConfigParser # Backward-compatible with ConfigParser
import re import re
from .configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
import six from typing import Any, Callable, Dict, TextIO, Iterator, List, Optional, Set, Union
from typing import TYPE_CHECKING
from .configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
from . import config from . import config
if TYPE_CHECKING:
from compat import RawConfigParser
class LineType(object):
line = None
def __init__(self, line=None): class LineType:
line: Optional[str] = None
def __init__(self, line: Optional[str] = None) -> None:
if line is not None: if line is not None:
self.line = line.strip('\n') self.line = line.strip("\n")
# Return the original line for unmodified objects # Return the original line for unmodified objects
# Otherwise construct using the current attribute values # Otherwise construct using the current attribute values
def __str__(self): def __str__(self) -> str:
if self.line is not None: if self.line is not None:
return self.line return self.line
else: else:
@@ -66,78 +71,87 @@ class LineType(object):
# If an attribute is modified after initialization # If an attribute is modified after initialization
# set line to None since it is no longer accurate. # set line to None since it is no longer accurate.
def __setattr__(self, name, value): def __setattr__(self, name: str, value: object) -> None:
if hasattr(self, name): if hasattr(self, name):
self.__dict__['line'] = None self.__dict__["line"] = None
self.__dict__[name] = value self.__dict__[name] = value
def to_string(self): def to_string(self) -> str:
raise Exception('This method must be overridden in derived classes') # FIXME Raise NotImplementedError instead
raise Exception("This method must be overridden in derived classes")
class SectionLine(LineType): class SectionLine(LineType):
regex = re.compile(r'^\[' regex = re.compile(r"^\[" r"(?P<name>[^]]+)" r"\]\s*" r"((?P<csep>;|#)(?P<comment>.*))?$")
r'(?P<name>[^]]+)'
r'\]\s*'
r'((?P<csep>;|#)(?P<comment>.*))?$')
def __init__(self, name, comment=None, comment_separator=None, def __init__(
comment_offset=-1, line=None): self,
super(SectionLine, self).__init__(line) name: str,
self.name = name comment: Optional[str] = None,
self.comment = comment comment_separator: Optional[str] = None,
self.comment_separator = comment_separator comment_offset: int = -1,
self.comment_offset = comment_offset line: Optional[str] = None,
) -> None:
super().__init__(line)
self.name: str = name
self.comment: Optional[str] = comment
self.comment_separator: Optional[str] = comment_separator
self.comment_offset: int = comment_offset
def to_string(self): def to_string(self) -> str:
out = '[' + self.name + ']' out: str = "[" + self.name + "]"
if self.comment is not None: if self.comment is not None:
# try to preserve indentation of comments # try to preserve indentation of comments
out = (out+' ').ljust(self.comment_offset) out = (out + " ").ljust(self.comment_offset)
out = out + self.comment_separator + self.comment out = out + self.comment_separator + self.comment
return out return out
def parse(cls, line): @classmethod
m = cls.regex.match(line.rstrip()) def parse(cls, line: str) -> Optional["SectionLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None: if m is None:
return None return None
return cls(m.group('name'), m.group('comment'), return cls(m.group("name"), m.group("comment"), m.group("csep"), m.start("csep"), line)
m.group('csep'), m.start('csep'),
line)
parse = classmethod(parse)
class OptionLine(LineType): class OptionLine(LineType):
def __init__(self, name, value, separator=' = ', comment=None, def __init__(
comment_separator=None, comment_offset=-1, line=None): self,
super(OptionLine, self).__init__(line) name: str,
self.name = name value: object,
self.value = value separator: str = " = ",
self.separator = separator comment: Optional[str] = None,
self.comment = comment comment_separator: Optional[str] = None,
self.comment_separator = comment_separator comment_offset: int = -1,
self.comment_offset = comment_offset line: Optional[str] = None,
) -> None:
super().__init__(line)
self.name: str = name
self.value: object = value
self.separator: str = separator
self.comment: Optional[str] = comment
self.comment_separator: Optional[str] = comment_separator
self.comment_offset: int = comment_offset
def to_string(self): def to_string(self) -> str:
out = '%s%s%s' % (self.name, self.separator, self.value) out: str = "%s%s%s" % (self.name, self.separator, self.value)
if self.comment is not None: if self.comment is not None:
# try to preserve indentation of comments # try to preserve indentation of comments
out = (out+' ').ljust(self.comment_offset) out = (out + " ").ljust(self.comment_offset)
out = out + self.comment_separator + self.comment out = out + self.comment_separator + self.comment
return out return out
regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)' regex = re.compile(r"^(?P<name>[^:=\s[][^:=]*)" r"(?P<sep>[:=]\s*)" r"(?P<value>.*)$")
r'(?P<sep>[:=]\s*)'
r'(?P<value>.*)$')
def parse(cls, line): @classmethod
m = cls.regex.match(line.rstrip()) def parse(cls, line: str) -> Optional["OptionLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None: if m is None:
return None return None
name = m.group('name').rstrip() name: str = m.group("name").rstrip()
value = m.group('value') value: str = m.group("value")
sep = m.group('name')[len(name):] + m.group('sep') sep: str = m.group("name")[len(name) :] + m.group("sep")
# comments are not detected in the regex because # comments are not detected in the regex because
# ensuring total compatibility with ConfigParser # ensuring total compatibility with ConfigParser
@@ -150,123 +164,120 @@ class OptionLine(LineType):
# include ';' in the value needs to be addressed. # include ';' in the value needs to be addressed.
# Also, '#' doesn't mark comments in options... # Also, '#' doesn't mark comments in options...
coff = value.find(';') coff: int = value.find(";")
if coff != -1 and value[coff - 1].isspace(): if coff != -1 and value[coff - 1].isspace():
comment = value[coff + 1 :] comment = value[coff + 1 :]
csep = value[coff] csep = value[coff]
value = value[:coff].rstrip() value = value[:coff].rstrip()
coff = m.start('value') + coff coff = m.start("value") + coff
else: else:
comment = None comment = None
csep = None csep = None
coff = -1 coff = -1
return cls(name, value, sep, comment, csep, coff, line) return cls(name, value, sep, comment, csep, coff, line)
parse = classmethod(parse)
def change_comment_syntax(comment_chars='%;#', allow_rem=False): def change_comment_syntax(comment_chars: str = "%;#", allow_rem: bool = False) -> None:
comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars) comment_chars: str = re.sub(r"([\]\-\^])", r"\\\1", comment_chars)
regex = r'^(?P<csep>[%s]' % comment_chars regex: str = r"^(?P<csep>[%s]" % comment_chars
if allow_rem: if allow_rem:
regex += '|[rR][eE][mM]' regex += "|[rR][eE][mM]"
regex += r')(?P<comment>.*)$' regex += r")(?P<comment>.*)$"
CommentLine.regex = re.compile(regex) CommentLine.regex = re.compile(regex)
class CommentLine(LineType): class CommentLine(LineType):
regex = re.compile(r'^(?P<csep>[;#])' regex: re.Pattern = re.compile(r"^(?P<csep>[;#]|[rR][eE][mM])" r"(?P<comment>.*)$")
r'(?P<comment>.*)$')
def __init__(self, comment='', separator='#', line=None): def __init__(self, comment: str = "", separator: str = "#", line: Optional[str] = None) -> None:
super(CommentLine, self).__init__(line) super().__init__(line)
self.comment = comment self.comment: str = comment
self.separator = separator self.separator: str = separator
def to_string(self): def to_string(self) -> str:
return self.separator + self.comment return self.separator + self.comment
def parse(cls, line): @classmethod
m = cls.regex.match(line.rstrip()) def parse(cls, line: str) -> Optional["CommentLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None: if m is None:
return None return None
return cls(m.group('comment'), m.group('csep'), line) return cls(m.group("comment"), m.group("csep"), line)
parse = classmethod(parse)
class EmptyLine(LineType): class EmptyLine(LineType):
# could make this a singleton # could make this a singleton
def to_string(self): def to_string(self) -> str:
return '' return ""
value = property(lambda self: '') value = property(lambda self: "")
def parse(cls, line): @classmethod
def parse(cls, line: str) -> Optional["EmptyLine"]:
if line.strip(): if line.strip():
return None return None
return cls(line) return cls(line)
parse = classmethod(parse)
class ContinuationLine(LineType): class ContinuationLine(LineType):
regex = re.compile(r'^\s+(?P<value>.*)$') regex: re.Pattern = re.compile(r"^\s+(?P<value>.*)$")
def __init__(self, value, value_offset=None, line=None): def __init__(self, value: str, value_offset: Optional[int] = None, line: Optional[str] = None) -> None:
super(ContinuationLine, self).__init__(line) super().__init__(line)
self.value = value self.value = value
if value_offset is None: if value_offset is None:
value_offset = 8 value_offset = 8
self.value_offset = value_offset self.value_offset: int = value_offset
def to_string(self): def to_string(self) -> str:
return ' '*self.value_offset + self.value return " " * self.value_offset + self.value
def parse(cls, line): @classmethod
m = cls.regex.match(line.rstrip()) def parse(cls, line: str) -> Optional["ContinuationLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None: if m is None:
return None return None
return cls(m.group('value'), m.start('value'), line) return cls(m.group("value"), m.start("value"), line)
parse = classmethod(parse)
class LineContainer(object): class LineContainer:
def __init__(self, d=None): def __init__(self, d: Optional[Union[List[LineType], LineType]] = None) -> None:
self.contents = [] self.contents = []
self.orgvalue = None self.orgvalue: str = None
if d: if d:
if isinstance(d, list): self.extend(d) if isinstance(d, list):
else: self.add(d) self.extend(d)
else:
self.add(d)
def add(self, x): def add(self, x: LineType) -> None:
self.contents.append(x) self.contents.append(x)
def extend(self, x): def extend(self, x: List[LineType]) -> None:
for i in x: self.add(i) for i in x:
self.add(i)
def get_name(self): def get_name(self) -> str:
return self.contents[0].name return self.contents[0].name
def set_name(self, data): def set_name(self, data: str) -> None:
self.contents[0].name = data self.contents[0].name = data
def get_value(self): def get_value(self) -> str:
if self.orgvalue is not None: if self.orgvalue is not None:
return self.orgvalue return self.orgvalue
elif len(self.contents) == 1: elif len(self.contents) == 1:
return self.contents[0].value return self.contents[0].value
else: else:
return '\n'.join([('%s' % x.value) for x in self.contents return "\n".join([("%s" % x.value) for x in self.contents if not isinstance(x, CommentLine)])
if not isinstance(x, CommentLine)])
def set_value(self, data): def set_value(self, data: object) -> None:
self.orgvalue = data self.orgvalue = data
lines = ('%s' % data).split('\n') lines: List[str] = ("%s" % data).split("\n")
# If there is an existing ContinuationLine, use its offset # If there is an existing ContinuationLine, use its offset
value_offset = None value_offset: Optional[int] = None
for v in self.contents: for v in self.contents:
if isinstance(v, ContinuationLine): if isinstance(v, ContinuationLine):
value_offset = v.value_offset value_offset = v.value_offset
@@ -282,40 +293,45 @@ class LineContainer(object):
else: else:
self.add(EmptyLine()) self.add(EmptyLine())
def get_line_number(self) -> Optional[int]:
return self.contents[0].line_number if self.contents else None
name = property(get_name, set_name) name = property(get_name, set_name)
value = property(get_value, set_value) value = property(get_value, set_value)
def __str__(self): line_number = property(get_line_number)
s = [x.__str__() for x in self.contents]
return '\n'.join(s)
def finditer(self, key): def __str__(self) -> str:
s: List[str] = [x.__str__() for x in self.contents]
return "\n".join(s)
def finditer(self, key: str) -> Iterator[Union[SectionLine, OptionLine]]:
for x in self.contents[::-1]: for x in self.contents[::-1]:
if hasattr(x, 'name') and x.name==key: if hasattr(x, "name") and x.name == key:
yield x yield x
def find(self, key): def find(self, key: str) -> Union[SectionLine, OptionLine]:
for x in self.finditer(key): for x in self.finditer(key):
return x return x
raise KeyError(key) raise KeyError(key)
def _make_xform_property(myattrname, srcattrname=None): def _make_xform_property(myattrname: str, srcattrname: Optional[str] = None) -> property:
private_attrname = myattrname + 'value' private_attrname: str = myattrname + "value"
private_srcname = myattrname + 'source' private_srcname: str = myattrname + "source"
if srcattrname is None: if srcattrname is None:
srcattrname = myattrname srcattrname = myattrname
def getfn(self): def getfn(self) -> Callable:
srcobj = getattr(self, private_srcname) srcobj: Optional[object] = getattr(self, private_srcname)
if srcobj is not None: if srcobj is not None:
return getattr(srcobj, srcattrname) return getattr(srcobj, srcattrname)
else: else:
return getattr(self, private_attrname) return getattr(self, private_attrname)
def setfn(self, value): def setfn(self, value: Callable) -> None:
srcobj = getattr(self, private_srcname) srcobj: Optional[object] = getattr(self, private_srcname)
if srcobj is not None: if srcobj is not None:
setattr(srcobj, srcattrname, value) setattr(srcobj, srcattrname, value)
else: else:
@@ -325,31 +341,38 @@ def _make_xform_property(myattrname, srcattrname=None):
class INISection(config.ConfigNamespace): class INISection(config.ConfigNamespace):
_lines = None _lines: List[LineContainer] = None
_options = None _options: Dict[str, object] = None
_defaults = None _defaults: Optional["INISection"] = None
_optionxformvalue = None _optionxformvalue: "INIConfig" = None
_optionxformsource = None _optionxformsource: "INIConfig" = None
_compat_skip_empty_lines = set() _compat_skip_empty_lines: Set[str] = set()
def __init__(self, lineobj, defaults=None, optionxformvalue=None, optionxformsource=None): def __init__(
self,
lineobj: LineContainer,
defaults: Optional["INISection"] = None,
optionxformvalue: Optional["INIConfig"] = None,
optionxformsource: Optional["INIConfig"] = None,
) -> None:
self._lines = [lineobj] self._lines = [lineobj]
self._defaults = defaults self._defaults = defaults
self._optionxformvalue = optionxformvalue self._optionxformvalue = optionxformvalue
self._optionxformsource = optionxformsource self._optionxformsource = optionxformsource
self._options = {} self._options = {}
_optionxform = _make_xform_property('_optionxform') _optionxform = _make_xform_property("_optionxform")
def _compat_get(self, key): def _compat_get(self, key: str) -> str:
# identical to __getitem__ except that _compat_XXX # identical to __getitem__ except that _compat_XXX
# is checked for backward-compatible handling # is checked for backward-compatible handling
if key == '__name__': if key == "__name__":
return self._lines[-1].name return self._lines[-1].name
if self._optionxform: key = self._optionxform(key) if self._optionxform:
key = self._optionxform(key)
try: try:
value = self._options[key].value value: str = self._options[key].value
del_empty = key in self._compat_skip_empty_lines del_empty: bool = key in self._compat_skip_empty_lines
except KeyError: except KeyError:
if self._defaults and key in self._defaults._options: if self._defaults and key in self._defaults._options:
value = self._defaults._options[key].value value = self._defaults._options[key].value
@@ -357,13 +380,14 @@ class INISection(config.ConfigNamespace):
else: else:
raise raise
if del_empty: if del_empty:
value = re.sub('\n+', '\n', value) value = re.sub("\n+", "\n", value)
return value return value
def _getitem(self, key): def _getitem(self, key: str) -> object:
if key == '__name__': if key == "__name__":
return self._lines[-1].name return self._lines[-1].name
if self._optionxform: key = self._optionxform(key) if self._optionxform:
key = self._optionxform(key)
try: try:
return self._options[key].value return self._options[key].value
except KeyError: except KeyError:
@@ -372,22 +396,25 @@ class INISection(config.ConfigNamespace):
else: else:
raise raise
def __setitem__(self, key, value): def __setitem__(self, key: str, value: object) -> None:
if self._optionxform: xkey = self._optionxform(key) if self._optionxform:
else: xkey = key xkey = self._optionxform(key)
else:
xkey = key
if xkey in self._compat_skip_empty_lines: if xkey in self._compat_skip_empty_lines:
self._compat_skip_empty_lines.remove(xkey) self._compat_skip_empty_lines.remove(xkey)
if xkey not in self._options: if xkey not in self._options:
# create a dummy object - value may have multiple lines # create a dummy object - value may have multiple lines
obj = LineContainer(OptionLine(key, '')) obj = LineContainer(OptionLine(key, ""))
self._lines[-1].add(obj) self._lines[-1].add(obj)
self._options[xkey] = obj self._options[xkey] = obj
# the set_value() function in LineContainer # the set_value() function in LineContainer
# automatically handles multi-line values # automatically handles multi-line values
self._options[xkey].value = value self._options[xkey].value = value
def __delitem__(self, key): def __delitem__(self, key: str) -> None:
if self._optionxform: key = self._optionxform(key) if self._optionxform:
key = self._optionxform(key)
if key in self._compat_skip_empty_lines: if key in self._compat_skip_empty_lines:
self._compat_skip_empty_lines.remove(key) self._compat_skip_empty_lines.remove(key)
for l in self._lines: for l in self._lines:
@@ -395,14 +422,16 @@ class INISection(config.ConfigNamespace):
for o in l.contents: for o in l.contents:
if isinstance(o, LineContainer): if isinstance(o, LineContainer):
n = o.name n = o.name
if self._optionxform: n = self._optionxform(n) if self._optionxform:
if key != n: remaining.append(o) n = self._optionxform(n)
if key != n:
remaining.append(o)
else: else:
remaining.append(o) remaining.append(o)
l.contents = remaining l.contents = remaining
del self._options[key] del self._options[key]
def __iter__(self): def __iter__(self) -> Iterator[str]:
d = set() d = set()
for l in self._lines: for l in self._lines:
for x in l.contents: for x in l.contents:
@@ -421,26 +450,25 @@ class INISection(config.ConfigNamespace):
d.add(x) d.add(x)
def _new_namespace(self, name): def _new_namespace(self, name):
raise Exception('No sub-sections allowed', name) raise Exception("No sub-sections allowed", name)
def make_comment(line): def make_comment(line: str) -> CommentLine:
return CommentLine(line.rstrip('\n')) return CommentLine(line.rstrip("\n"))
def readline_iterator(f): def readline_iterator(f: TextIO) -> Iterator[str]:
"""iterate over a file by only using the file object's readline method""" """Iterate over a file by only using the file object's readline method."""
have_newline: bool = False
have_newline = False
while True: while True:
line = f.readline() line: Optional[str] = f.readline()
if not line: if not line:
if have_newline: if have_newline:
yield "" yield ""
return return
if line.endswith('\n'): if line.endswith("\n"):
have_newline = True have_newline = True
else: else:
have_newline = False have_newline = False
@@ -448,57 +476,67 @@ def readline_iterator(f):
yield line yield line
def lower(x): def lower(x: str) -> str:
return x.lower() return x.lower()
class INIConfig(config.ConfigNamespace): class INIConfig(config.ConfigNamespace):
_data = None _data: LineContainer = None
_sections = None _sections: Dict[str, object] = None
_defaults = None _defaults: INISection = None
_optionxformvalue = None _optionxformvalue: Callable = None
_optionxformsource = None _optionxformsource: Optional["INIConfig"] = None
_sectionxformvalue = None _sectionxformvalue: Optional["INIConfig"] = None
_sectionxformsource = None _sectionxformsource: Optional["INIConfig"] = None
_parse_exc = None _parse_exc = None
_bom = False _bom = False
def __init__(self, fp=None, defaults=None, parse_exc=True, def __init__(
optionxformvalue=lower, optionxformsource=None, self,
sectionxformvalue=None, sectionxformsource=None): fp: TextIO = None,
defaults: Dict[str, object] = None,
parse_exc: bool = True,
optionxformvalue: Callable = lower,
optionxformsource: Optional[Union["INIConfig", "RawConfigParser"]] = None,
sectionxformvalue: Optional["INIConfig"] = None,
sectionxformsource: Optional["INIConfig"] = None,
) -> None:
self._data = LineContainer() self._data = LineContainer()
self._parse_exc = parse_exc self._parse_exc = parse_exc
self._optionxformvalue = optionxformvalue self._optionxformvalue = optionxformvalue
self._optionxformsource = optionxformsource self._optionxformsource = optionxformsource
self._sectionxformvalue = sectionxformvalue self._sectionxformvalue = sectionxformvalue
self._sectionxformsource = sectionxformsource self._sectionxformsource = sectionxformsource
self._sections = {} self._sections: Dict[str, INISection] = {}
if defaults is None: defaults = {} if defaults is None:
defaults = {}
self._defaults = INISection(LineContainer(), optionxformsource=self) self._defaults = INISection(LineContainer(), optionxformsource=self)
for name, value in defaults.items(): for name, value in defaults.items():
self._defaults[name] = value self._defaults[name] = value
if fp is not None: if fp is not None:
self._readfp(fp) self._readfp(fp)
_optionxform = _make_xform_property('_optionxform', 'optionxform') _optionxform = _make_xform_property("_optionxform", "optionxform")
_sectionxform = _make_xform_property('_sectionxform', 'optionxform') _sectionxform = _make_xform_property("_sectionxform", "optionxform")
def _getitem(self, key): def _getitem(self, key: str) -> INISection:
if key == DEFAULTSECT: if key == DEFAULTSECT:
return self._defaults return self._defaults
if self._sectionxform: key = self._sectionxform(key) if self._sectionxform:
key = self._sectionxform(key)
return self._sections[key] return self._sections[key]
def __setitem__(self, key, value): def __setitem__(self, key: str, value: object):
raise Exception('Values must be inside sections', key, value) raise Exception("Values must be inside sections", key, value)
def __delitem__(self, key): def __delitem__(self, key: str) -> None:
if self._sectionxform: key = self._sectionxform(key) if self._sectionxform:
key = self._sectionxform(key)
for line in self._sections[key]._lines: for line in self._sections[key]._lines:
self._data.contents.remove(line) self._data.contents.remove(line)
del self._sections[key] del self._sections[key]
def __iter__(self): def __iter__(self) -> Iterator[str]:
d = set() d = set()
d.add(DEFAULTSECT) d.add(DEFAULTSECT)
for x in self._data.contents: for x in self._data.contents:
@@ -507,35 +545,31 @@ class INIConfig(config.ConfigNamespace):
yield x.name yield x.name
d.add(x.name) d.add(x.name)
def _new_namespace(self, name): def _new_namespace(self, name: str) -> INISection:
if self._data.contents: if self._data.contents:
self._data.add(EmptyLine()) self._data.add(EmptyLine())
obj = LineContainer(SectionLine(name)) obj = LineContainer(SectionLine(name))
self._data.add(obj) self._data.add(obj)
if self._sectionxform: name = self._sectionxform(name) if self._sectionxform:
name = self._sectionxform(name)
if name in self._sections: if name in self._sections:
ns = self._sections[name] ns = self._sections[name]
ns._lines.append(obj) ns._lines.append(obj)
else: else:
ns = INISection(obj, defaults=self._defaults, ns = INISection(obj, defaults=self._defaults, optionxformsource=self)
optionxformsource=self)
self._sections[name] = ns self._sections[name] = ns
return ns return ns
def __str__(self): def __str__(self) -> str:
if self._bom: if self._bom:
fmt = u'\ufeff%s' fmt = "\ufeff%s"
else: else:
fmt = '%s' fmt = "%s"
return fmt % self._data.__str__() return fmt % self._data.__str__()
__unicode__ = __str__ _line_types = [EmptyLine, CommentLine, SectionLine, OptionLine, ContinuationLine]
_line_types = [EmptyLine, CommentLine, def _parse(self, line: str) -> Any:
SectionLine, OptionLine,
ContinuationLine]
def _parse(self, line):
for linetype in self._line_types: for linetype in self._line_types:
lineobj = linetype.parse(line) lineobj = linetype.parse(line)
if lineobj: if lineobj:
@@ -544,7 +578,7 @@ class INIConfig(config.ConfigNamespace):
# can't parse line # can't parse line
return None return None
def _readfp(self, fp): def _readfp(self, fp: TextIO) -> None:
cur_section = None cur_section = None
cur_option = None cur_option = None
cur_section_name = None cur_section_name = None
@@ -554,21 +588,20 @@ class INIConfig(config.ConfigNamespace):
try: try:
fname = fp.name fname = fp.name
except AttributeError: except AttributeError:
fname = '<???>' fname = "<???>"
line_count = 0 line_count = 0
exc = None exc = None
line = None line = None
for line in readline_iterator(fp): for line in readline_iterator(fp):
# Check for BOM on first line # Check for BOM on first line
if line_count == 0 and isinstance(line, six.text_type): if line_count == 0 and isinstance(line, str):
if line[0] == u'\ufeff': if line[0] == "\ufeff":
line = line[1:] line = line[1:]
self._bom = True self._bom = True
line_obj = self._parse(line) line_obj = self._parse(line)
line_count += 1 line_count += 1
if not cur_section and not isinstance(line_obj, (CommentLine, EmptyLine, SectionLine)): if not cur_section and not isinstance(line_obj, (CommentLine, EmptyLine, SectionLine)):
if self._parse_exc: if self._parse_exc:
raise MissingSectionHeaderError(fname, line_count, line) raise MissingSectionHeaderError(fname, line_count, line)
@@ -588,7 +621,7 @@ class INIConfig(config.ConfigNamespace):
cur_option.extend(pending_lines) cur_option.extend(pending_lines)
pending_lines = [] pending_lines = []
if pending_empty_lines: if pending_empty_lines:
optobj._compat_skip_empty_lines.add(cur_option_name) optobj._compat_skip_empty_lines.add(cur_option_name) # noqa : F821
pending_empty_lines = False pending_empty_lines = False
cur_option.add(line_obj) cur_option.add(line_obj)
else: else:
@@ -633,9 +666,7 @@ class INIConfig(config.ConfigNamespace):
else: else:
cur_section_name = cur_section.name cur_section_name = cur_section.name
if cur_section_name not in self._sections: if cur_section_name not in self._sections:
self._sections[cur_section_name] = \ self._sections[cur_section_name] = INISection(cur_section, defaults=self._defaults, optionxformsource=self)
INISection(cur_section, defaults=self._defaults,
optionxformsource=self)
else: else:
self._sections[cur_section_name]._lines.append(cur_section) self._sections[cur_section_name]._lines.append(cur_section)
@@ -644,8 +675,11 @@ class INIConfig(config.ConfigNamespace):
if isinstance(line_obj, EmptyLine): if isinstance(line_obj, EmptyLine):
pending_empty_lines = True pending_empty_lines = True
if line_obj:
line_obj.line_number = line_count
self._data.extend(pending_lines) self._data.extend(pending_lines)
if line and line[-1] == '\n': if line and line[-1] == "\n":
self._data.add(EmptyLine()) self._data.add(EmptyLine())
if exc: if exc:
+11 -7
View File
@@ -1,8 +1,13 @@
from typing import TYPE_CHECKING, List
from . import compat from . import compat
from .ini import LineContainer, EmptyLine from .ini import EmptyLine, LineContainer
if TYPE_CHECKING:
from .ini import LineType
def tidy(cfg): def tidy(cfg: compat.RawConfigParser):
"""Clean up blank lines. """Clean up blank lines.
This functions makes the configuration look clean and This functions makes the configuration look clean and
@@ -19,8 +24,7 @@ def tidy(cfg):
if isinstance(cont[i], LineContainer): if isinstance(cont[i], LineContainer):
tidy_section(cont[i]) tidy_section(cont[i])
i += 1 i += 1
elif (isinstance(cont[i-1], EmptyLine) and elif isinstance(cont[i - 1], EmptyLine) and isinstance(cont[i], EmptyLine):
isinstance(cont[i], EmptyLine)):
del cont[i] del cont[i]
else: else:
i += 1 i += 1
@@ -34,9 +38,9 @@ def tidy(cfg):
cont.append(EmptyLine()) cont.append(EmptyLine())
def tidy_section(lc): def tidy_section(lc: "LineContainer"):
cont = lc.contents cont: List[LineType] = lc.contents
i = 1 i: int = 1
while i < len(cont): while i < len(cont):
if isinstance(cont[i - 1], EmptyLine) and isinstance(cont[i], EmptyLine): if isinstance(cont[i - 1], EmptyLine) and isinstance(cont[i], EmptyLine):
del cont[i] del cont[i]
+20
View File
@@ -0,0 +1,20 @@
server {
listen 443 ssl http2;
# Remove '#' in the next line to enable IPv6
# listen [::]:443 ssl http2;
server_name tisbackup.poudlard.lan;
ssl_certificate /etc/letsencrypt/live/tisbackup.poudlard.lan/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/tisbackup.poudlard.lan/privkey.pem; # managed by Certbot
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://localhost:9980/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
+30
View File
@@ -0,0 +1,30 @@
[project]
name = "TISbackup"
version = "1.8.0"
description = "Backup server side executed python scripts for managing linux and windows system and application data backups, developed by adminsys for adminsys"
readme = "README.md"
dependencies = [
"flask==3.1.0",
"huey==2.5.3",
"iniparse==0.5",
"paramiko==3.5.1",
"peewee==3.17.9",
"pexpect==4.9.0",
"redis==5.2.1",
"requests==2.32.3",
"simplejson==3.20.1",
"six==1.17.0",
]
requires-python = ">=3.13"
[tool.black]
line-length = 140
[tool.ruff]
# Allow lines to be as long as 120.
line-length = 140
indent-width = 4
[tool.ruff.lint]
ignore = ["F401", "F403", "F405", "E402", "E701", "E722", "E741"]
Regular → Executable
+10 -3
View File
@@ -1,3 +1,10 @@
six flask==3.1.0
requests huey==2.5.3
paramiko iniparse==0.5
paramiko==3.5.1
peewee==3.17.9
pexpect==4.9.0
redis==5.2.1
requests==2.32.3
simplejson==3.20.1
six==1.17.0
-1
View File
@@ -12,4 +12,3 @@ echo $VERSION > __VERSION__
rpmbuild -bb --buildroot $PWD/builddir -v --clean tis-tisbackup.spec rpmbuild -bb --buildroot $PWD/builddir -v --clean tis-tisbackup.spec
cp RPMS/*/*.rpm . cp RPMS/*/*.rpm .
-2
View File
@@ -14,5 +14,3 @@ else
sleep 3 sleep 3
fi fi
echo $(date +%Y-%m-%d\ %H:%M:%S) : Fin Export TISBackup sur Disque USB : $target >> /var/log/tisbackup.log echo $(date +%Y-%m-%d\ %H:%M:%S) : Fin Export TISBackup sur Disque USB : $target >> /var/log/tisbackup.log
-1
View File
@@ -95,4 +95,3 @@ maximum_backup_age=30
;type=xcp-dump-metadata ;type=xcp-dump-metadata
;server_name=srvxen1 ;server_name=srvxen1
;private_key=/root/.ssh/id_rsa ;private_key=/root/.ssh/id_rsa
-1
View File
@@ -18,4 +18,3 @@ password_file=/home/homes/ssamson/tisbackup-pra/xen_passwd
network_name=net-test network_name=net-test
#start_vm=no #start_vm=no
#max_copies=3 #max_copies=3
-1
View File
@@ -4,4 +4,3 @@
# m h dom mon dow user command # m h dom mon dow user command
30 22 * * * root /opt/tisbackup/tisbackup.py -c /etc/tis/tisbackup-config.ini backup >> /var/log/tisbackup.log 2>&1 30 22 * * * root /opt/tisbackup/tisbackup.py -c /etc/tis/tisbackup-config.ini backup >> /var/log/tisbackup.log 2>&1
30 12 * * * root /opt/tisbackup/tisbackup.py -c /etc/tis/tisbackup-config.ini cleanup >> /var/log/tisbackup.log 2>&1 30 12 * * * root /opt/tisbackup/tisbackup.py -c /etc/tis/tisbackup-config.ini cleanup >> /var/log/tisbackup.log 2>&1
-1
View File
@@ -95,4 +95,3 @@ case "$1" in
esac esac
exit 0 exit 0
-1
View File
@@ -95,4 +95,3 @@ case "$1" in
esac esac
exit 0 exit 0
-1
View File
@@ -14948,4 +14948,3 @@
})); }));
}(window, document)); }(window, document));
+12 -6
View File
@@ -1,16 +1,20 @@
from huey import RedisHuey
import os
import logging import logging
import os
from huey import RedisHuey
from tisbackup import tis_backup from tisbackup import tis_backup
huey = RedisHuey('tisbackup', host='localhost') huey = RedisHuey("tisbackup", host="localhost")
@huey.task() @huey.task()
def run_export_backup(base, config_file, mount_point, backup_sections): def run_export_backup(base, config_file, mount_point, backup_sections):
try: try:
# Log # Log
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
@@ -27,16 +31,18 @@ def run_export_backup(base, config_file, mount_point, backup_sections):
mount_point = mount_point mount_point = mount_point
backup.export_backups(backup_sections, mount_point) backup.export_backups(backup_sections, mount_point)
except Exception as e: except Exception as e:
return(str(e)) return str(e)
finally: finally:
os.system("/bin/umount %s" % mount_point) os.system("/bin/umount %s" % mount_point)
os.rmdir(mount_point) os.rmdir(mount_point)
return "ok" return "ok"
def get_task(): def get_task():
return task return task
def set_task(my_task): def set_task(my_task):
global task global task
task = my_task task = my_task
-1
View File
@@ -98,4 +98,3 @@
</script> </script>
</body></html> </body></html>
+129 -108
View File
@@ -18,37 +18,39 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import datetime import datetime
import subprocess import os
import os,sys import sys
from os.path import isfile, join from os.path import isfile, join
tisbackup_root_dir = os.path.dirname(os.path.realpath(__file__)) tisbackup_root_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0,os.path.join(tisbackup_root_dir,'lib')) sys.path.insert(0, os.path.join(tisbackup_root_dir, "lib"))
sys.path.insert(0,os.path.join(tisbackup_root_dir,'libtisbackup')) sys.path.insert(0, os.path.join(tisbackup_root_dir, "libtisbackup"))
from iniparse import ini,ConfigParser
from optparse import OptionParser
import re
import getopt
import os.path
import logging
import errno import errno
from libtisbackup.common import * import logging
import os.path
from optparse import OptionParser
from iniparse import ConfigParser, ini
from libtisbackup.backup_mysql import backup_mysql from libtisbackup.backup_mysql import backup_mysql
from libtisbackup.backup_rsync import backup_rsync
from libtisbackup.backup_rsync import backup_rsync_ssh
#from libtisbackup.backup_oracle import backup_oracle
from libtisbackup.backup_rsync_btrfs import backup_rsync_btrfs
from libtisbackup.backup_rsync_btrfs import backup_rsync__btrfs_ssh
from libtisbackup.backup_pgsql import backup_pgsql
from libtisbackup.backup_xva import backup_xva
# from libtisbackup.backup_vmdk import backup_vmdk # from libtisbackup.backup_vmdk import backup_vmdk
# from libtisbackup.backup_switch import backup_switch # from libtisbackup.backup_switch import backup_switch
from libtisbackup.backup_null import backup_null from libtisbackup.backup_null import backup_null
from libtisbackup.backup_xcp_metadata import backup_xcp_metadata from libtisbackup.backup_pgsql import backup_pgsql
from libtisbackup.copy_vm_xcp import copy_vm_xcp from libtisbackup.backup_rsync import backup_rsync, backup_rsync_ssh
# from libtisbackup.backup_oracle import backup_oracle
from libtisbackup.backup_rsync_btrfs import backup_rsync__btrfs_ssh, backup_rsync_btrfs
# from libtisbackup.backup_sqlserver import backup_sqlserver # from libtisbackup.backup_sqlserver import backup_sqlserver
from libtisbackup.backup_samba4 import backup_samba4 from libtisbackup.backup_samba4 import backup_samba4
from libtisbackup.backup_xcp_metadata import backup_xcp_metadata
from libtisbackup.backup_xva import backup_xva
from libtisbackup.common import *
from libtisbackup.copy_vm_xcp import copy_vm_xcp
__version__ = "2.0" __version__ = "2.0"
@@ -70,23 +72,45 @@ action is either :
version = "VERSION" version = "VERSION"
parser = OptionParser(usage=usage, version="%prog " + version) parser = OptionParser(usage=usage, version="%prog " + version)
parser.add_option("-c","--config", dest="config", default='/etc/tis/tisbackup-config.ini', help="Config file full path (default: %default)") parser.add_option(
parser.add_option("-d","--dry-run", dest="dry_run", default=False, action='store_true', help="Dry run (default: %default)") "-c", "--config", dest="config", default="/etc/tis/tisbackup-config.ini", help="Config file full path (default: %default)"
parser.add_option("-v","--verbose", dest="verbose", default=False, action='store_true', help="More information (default: %default)") )
parser.add_option("-s","--sections", dest="sections", default='', help="Comma separated list of sections (backups) to process (default: All)") parser.add_option("-d", "--dry-run", dest="dry_run", default=False, action="store_true", help="Dry run (default: %default)")
parser.add_option("-l","--loglevel", dest="loglevel", default='info', type='choice', choices=['debug','warning','info','error','critical'], metavar='LOGLEVEL',help="Loglevel (default: %default)") parser.add_option("-v", "--verbose", dest="verbose", default=False, action="store_true", help="More information (default: %default)")
parser.add_option("-n","--len", dest="statscount", default=30, type='int', help="Number of lines to list for dumpstat (default: %default)") parser.add_option(
parser.add_option("-b","--backupdir", dest="backup_base_dir", default='', help="Base directory for all backups (default: [global] backup_base_dir in config file)") "-s", "--sections", dest="sections", default="", help="Comma separated list of sections (backups) to process (default: All)"
parser.add_option("-x","--exportdir", dest="exportdir", default='', help="Directory where to export latest backups with exportbackup (nodefault)") )
parser.add_option(
"-l",
"--loglevel",
dest="loglevel",
default="info",
type="choice",
choices=["debug", "warning", "info", "error", "critical"],
metavar="LOGLEVEL",
help="Loglevel (default: %default)",
)
parser.add_option("-n", "--len", dest="statscount", default=30, type="int", help="Number of lines to list for dumpstat (default: %default)")
parser.add_option(
"-b",
"--backupdir",
dest="backup_base_dir",
default="",
help="Base directory for all backups (default: [global] backup_base_dir in config file)",
)
parser.add_option(
"-x", "--exportdir", dest="exportdir", default="", help="Directory where to export latest backups with exportbackup (nodefault)"
)
class tis_backup: class tis_backup:
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
def __init__(self,dry_run=False,verbose=False,backup_base_dir=''): def __init__(self, dry_run=False, verbose=False, backup_base_dir=""):
self.dry_run = dry_run self.dry_run = dry_run
self.verbose = verbose self.verbose = verbose
self.backup_base_dir = backup_base_dir self.backup_base_dir = backup_base_dir
self.backup_base_dir = '' self.backup_base_dir = ""
self.backup_list = [] self.backup_list = []
self.dry_run = dry_run self.dry_run = dry_run
self.verbose = False self.verbose = False
@@ -97,22 +121,23 @@ class tis_backup:
cp.read(filename) cp.read(filename)
if not self.backup_base_dir: if not self.backup_base_dir:
self.backup_base_dir = cp.get('global','backup_base_dir') self.backup_base_dir = cp.get("global", "backup_base_dir")
if not os.path.isdir(self.backup_base_dir): if not os.path.isdir(self.backup_base_dir):
self.logger.info('Creating backup directory %s' % self.backup_base_dir) self.logger.info("Creating backup directory %s" % self.backup_base_dir)
os.makedirs(self.backup_base_dir) os.makedirs(self.backup_base_dir)
self.logger.debug("backup directory : " + self.backup_base_dir) self.logger.debug("backup directory : " + self.backup_base_dir)
self.dbstat = BackupStat(os.path.join(self.backup_base_dir,'log','tisbackup.sqlite')) self.dbstat = BackupStat(os.path.join(self.backup_base_dir, "log", "tisbackup.sqlite"))
for section in cp.sections(): for section in cp.sections():
if (section != 'global'): if section != "global":
self.logger.debug("reading backup config " + section) self.logger.debug("reading backup config " + section)
backup_item = None backup_item = None
type = cp.get(section,'type') type = cp.get(section, "type")
backup_item = backup_drivers[type](backup_name=section, backup_item = backup_drivers[type](
backup_dir=os.path.join(self.backup_base_dir,section),dbstat=self.dbstat,dry_run=self.dry_run) backup_name=section, backup_dir=os.path.join(self.backup_base_dir, section), dbstat=self.dbstat, dry_run=self.dry_run
)
backup_item.read_config(cp) backup_item.read_config(cp)
backup_item.verbose = self.verbose backup_item.verbose = self.verbose
@@ -122,20 +147,19 @@ class tis_backup:
# TODO socket.gethostbyaddr('64.236.16.20') # TODO socket.gethostbyaddr('64.236.16.20')
# TODO limit backup to one backup on the command line # TODO limit backup to one backup on the command line
def checknagios(self, sections=[]): def checknagios(self, sections=[]):
try: try:
if not sections: if not sections:
sections = [backup_item.backup_name for backup_item in self.backup_list] sections = [backup_item.backup_name for backup_item in self.backup_list]
self.logger.debug('Start of check nagios for %s' % (','.join(sections),)) self.logger.debug("Start of check nagios for %s" % (",".join(sections),))
try: try:
worst_nagiosstatus = None worst_nagiosstatus = None
ok = [] ok = []
warning = [] warning = []
critical = [] critical = []
unknown = [] unknown = []
nagiosoutput = '' nagiosoutput = ""
for backup_item in self.backup_list: for backup_item in self.backup_list:
if not sections or backup_item.backup_name in sections: if not sections or backup_item.backup_name in sections:
(nagiosstatus, log) = backup_item.checknagios() (nagiosstatus, log) = backup_item.checknagios()
@@ -150,7 +174,7 @@ class tis_backup:
self.logger.debug('[%s] nagios:"%i" log: %s', backup_item.backup_name, nagiosstatus, log) self.logger.debug('[%s] nagios:"%i" log: %s', backup_item.backup_name, nagiosstatus, log)
if not ok and not critical and not unknown and not warning: if not ok and not critical and not unknown and not warning:
self.logger.debug('Nothing processed') self.logger.debug("Nothing processed")
worst_nagiosstatus = nagiosStateUnknown worst_nagiosstatus = nagiosStateUnknown
nagiosoutput = 'UNKNOWN : Unknown backup sections "%s"' % sections nagiosoutput = 'UNKNOWN : Unknown backup sections "%s"' % sections
@@ -159,40 +183,39 @@ class tis_backup:
if unknown: if unknown:
if not worst_nagiosstatus: if not worst_nagiosstatus:
worst_nagiosstatus = nagiosStateUnknown worst_nagiosstatus = nagiosStateUnknown
nagiosoutput = 'UNKNOWN status backups %s' % (','.join([b[0] for b in unknown])) nagiosoutput = "UNKNOWN status backups %s" % (",".join([b[0] for b in unknown]))
globallog.extend(unknown) globallog.extend(unknown)
if critical: if critical:
if not worst_nagiosstatus: if not worst_nagiosstatus:
worst_nagiosstatus = nagiosStateCritical worst_nagiosstatus = nagiosStateCritical
nagiosoutput = 'CRITICAL backups %s' % (','.join([b[0] for b in critical])) nagiosoutput = "CRITICAL backups %s" % (",".join([b[0] for b in critical]))
globallog.extend(critical) globallog.extend(critical)
if warning: if warning:
if not worst_nagiosstatus: if not worst_nagiosstatus:
worst_nagiosstatus = nagiosStateWarning worst_nagiosstatus = nagiosStateWarning
nagiosoutput = 'WARNING backups %s' % (','.join([b[0] for b in warning])) nagiosoutput = "WARNING backups %s" % (",".join([b[0] for b in warning]))
globallog.extend(warning) globallog.extend(warning)
if ok: if ok:
if not worst_nagiosstatus: if not worst_nagiosstatus:
worst_nagiosstatus = nagiosStateOk worst_nagiosstatus = nagiosStateOk
nagiosoutput = 'OK backups %s' % (','.join([b[0] for b in ok])) nagiosoutput = "OK backups %s" % (",".join([b[0] for b in ok]))
globallog.extend(ok) globallog.extend(ok)
if worst_nagiosstatus == nagiosStateOk: if worst_nagiosstatus == nagiosStateOk:
nagiosoutput = 'ALL backups OK %s' % (','.join(sections)) nagiosoutput = "ALL backups OK %s" % (",".join(sections))
except BaseException as e: except BaseException as e:
worst_nagiosstatus = nagiosStateCritical worst_nagiosstatus = nagiosStateCritical
nagiosoutput = 'EXCEPTION',"Critical : %s" % str(e) nagiosoutput = "EXCEPTION", "Critical : %s" % str(e)
raise raise
finally: finally:
self.logger.debug('worst nagios status :"%i"', worst_nagiosstatus) self.logger.debug('worst nagios status :"%i"', worst_nagiosstatus)
print('%s (tisbackup V%s)' %(nagiosoutput,version)) print("%s (tisbackup V%s)" % (nagiosoutput, version))
print('\n'.join(["[%s]:%s" % (l[0],l[1]) for l in globallog])) print("\n".join(["[%s]:%s" % (log_elem[0], log_elem[1]) for log_elem in globallog]))
sys.exit(worst_nagiosstatus) sys.exit(worst_nagiosstatus)
def process_backup(self, sections=[]): def process_backup(self, sections=[]):
@@ -201,50 +224,50 @@ class tis_backup:
if not sections: if not sections:
sections = [backup_item.backup_name for backup_item in self.backup_list] sections = [backup_item.backup_name for backup_item in self.backup_list]
self.logger.info('Processing backup for %s' % (','.join(sections)) ) self.logger.info("Processing backup for %s" % (",".join(sections)))
for backup_item in self.backup_list: for backup_item in self.backup_list:
if not sections or backup_item.backup_name in sections: if not sections or backup_item.backup_name in sections:
try: try:
assert(isinstance(backup_item,backup_generic)) assert isinstance(backup_item, backup_generic)
self.logger.info('Processing [%s]',(backup_item.backup_name)) self.logger.info("Processing [%s]", (backup_item.backup_name))
stats = backup_item.process_backup() stats = backup_item.process_backup()
processed.append((backup_item.backup_name, stats)) processed.append((backup_item.backup_name, stats))
except BaseException as e: except BaseException as e:
self.logger.critical('Backup [%s] processed with error : %s',backup_item.backup_name,e) self.logger.critical("Backup [%s] processed with error : %s", backup_item.backup_name, e)
errors.append((backup_item.backup_name, str(e))) errors.append((backup_item.backup_name, str(e)))
if not processed and not errors: if not processed and not errors:
self.logger.critical('No backup properly finished or processed') self.logger.critical("No backup properly finished or processed")
else: else:
if processed: if processed:
self.logger.info('Backup processed : %s' , ",".join([b[0] for b in processed])) self.logger.info("Backup processed : %s", ",".join([b[0] for b in processed]))
if errors: if errors:
self.logger.error('Backup processed with errors: %s' , ",".join([b[0] for b in errors])) self.logger.error("Backup processed with errors: %s", ",".join([b[0] for b in errors]))
def export_backups(self,sections=[],exportdir=''): def export_backups(self, sections=[], exportdir=""):
processed = [] processed = []
errors = [] errors = []
if not sections: if not sections:
sections = [backup_item.backup_name for backup_item in self.backup_list] sections = [backup_item.backup_name for backup_item in self.backup_list]
self.logger.info('Exporting OK backups for %s to %s' % (','.join(sections),exportdir) ) self.logger.info("Exporting OK backups for %s to %s" % (",".join(sections), exportdir))
for backup_item in self.backup_list: for backup_item in self.backup_list:
if backup_item.backup_name in sections: if backup_item.backup_name in sections:
try: try:
assert(isinstance(backup_item,backup_generic)) assert isinstance(backup_item, backup_generic)
self.logger.info('Processing [%s]',(backup_item.backup_name)) self.logger.info("Processing [%s]", (backup_item.backup_name))
stats = backup_item.export_latestbackup(destdir=exportdir) stats = backup_item.export_latestbackup(destdir=exportdir)
processed.append((backup_item.backup_name, stats)) processed.append((backup_item.backup_name, stats))
except BaseException as e: except BaseException as e:
self.logger.critical('Export Backup [%s] processed with error : %s',backup_item.backup_name,e) self.logger.critical("Export Backup [%s] processed with error : %s", backup_item.backup_name, e)
errors.append((backup_item.backup_name, str(e))) errors.append((backup_item.backup_name, str(e)))
if not processed and not errors: if not processed and not errors:
self.logger.critical('No export backup properly finished or processed') self.logger.critical("No export backup properly finished or processed")
else: else:
if processed: if processed:
self.logger.info('Export Backups processed : %s' , ",".join([b[0] for b in processed])) self.logger.info("Export Backups processed : %s", ",".join([b[0] for b in processed]))
if errors: if errors:
self.logger.error('Export Backups processed with errors: %s' , ",".join([b[0] for b in errors])) self.logger.error("Export Backups processed with errors: %s", ",".join([b[0] for b in errors]))
def retry_failed_backups(self, maxage_hours=30): def retry_failed_backups(self, maxage_hours=30):
processed = [] processed = []
@@ -252,63 +275,62 @@ class tis_backup:
# before mindate, backup is too old # before mindate, backup is too old
mindate = datetime2isodate((datetime.datetime.now() - datetime.timedelta(hours=maxage_hours))) mindate = datetime2isodate((datetime.datetime.now() - datetime.timedelta(hours=maxage_hours)))
failed_backups = self.dbstat.query("""\ failed_backups = self.dbstat.query(
"""\
select distinct backup_name as bname select distinct backup_name as bname
from stats from stats
where status="OK" and backup_start>=?""",(mindate,)) where status="OK" and backup_start>=?""",
(mindate,),
)
defined_backups = list(map(lambda f: f.backup_name, [x for x in self.backup_list if not isinstance(x, backup_null)])) defined_backups = list(map(lambda f: f.backup_name, [x for x in self.backup_list if not isinstance(x, backup_null)]))
failed_backups_names = set(defined_backups) - set([b['bname'] for b in failed_backups if b['bname'] in defined_backups]) failed_backups_names = set(defined_backups) - set([b["bname"] for b in failed_backups if b["bname"] in defined_backups])
if failed_backups_names: if failed_backups_names:
self.logger.info('Processing backup for %s',','.join(failed_backups_names)) self.logger.info("Processing backup for %s", ",".join(failed_backups_names))
for backup_item in self.backup_list: for backup_item in self.backup_list:
if backup_item.backup_name in failed_backups_names: if backup_item.backup_name in failed_backups_names:
try: try:
assert(isinstance(backup_item,backup_generic)) assert isinstance(backup_item, backup_generic)
self.logger.info('Processing [%s]',(backup_item.backup_name)) self.logger.info("Processing [%s]", (backup_item.backup_name))
stats = backup_item.process_backup() stats = backup_item.process_backup()
processed.append((backup_item.backup_name, stats)) processed.append((backup_item.backup_name, stats))
except BaseException as e: except BaseException as e:
self.logger.critical('Backup [%s] not processed, error : %s',backup_item.backup_name,e) self.logger.critical("Backup [%s] not processed, error : %s", backup_item.backup_name, e)
errors.append((backup_item.backup_name, str(e))) errors.append((backup_item.backup_name, str(e)))
if not processed and not errors: if not processed and not errors:
self.logger.critical('No backup properly finished or processed') self.logger.critical("No backup properly finished or processed")
else: else:
if processed: if processed:
self.logger.info('Backup processed : %s' , ",".join([b[0] for b in errors])) self.logger.info("Backup processed : %s", ",".join([b[0] for b in errors]))
if errors: if errors:
self.logger.error('Backup processed with errors: %s' , ",".join([b[0] for b in errors])) self.logger.error("Backup processed with errors: %s", ",".join([b[0] for b in errors]))
else: else:
self.logger.info('No recent failed backups found in database') self.logger.info("No recent failed backups found in database")
def cleanup_backup_section(self, sections=[]): def cleanup_backup_section(self, sections=[]):
log = ''
processed = False processed = False
if not sections: if not sections:
sections = [backup_item.backup_name for backup_item in self.backup_list] sections = [backup_item.backup_name for backup_item in self.backup_list]
self.logger.info('Processing cleanup for %s' % (','.join(sections)) ) self.logger.info("Processing cleanup for %s" % (",".join(sections)))
for backup_item in self.backup_list: for backup_item in self.backup_list:
if backup_item.backup_name in sections: if backup_item.backup_name in sections:
try: try:
assert(isinstance(backup_item,backup_generic)) assert isinstance(backup_item, backup_generic)
self.logger.info('Processing cleanup of [%s]',(backup_item.backup_name)) self.logger.info("Processing cleanup of [%s]", (backup_item.backup_name))
backup_item.cleanup_backup() backup_item.cleanup_backup()
processed = True processed = True
except BaseException as e: except BaseException as e:
self.logger.critical('Cleanup of [%s] not processed, error : %s',backup_item.backup_name,e) self.logger.critical("Cleanup of [%s] not processed, error : %s", backup_item.backup_name, e)
if not processed: if not processed:
self.logger.critical('No cleanup properly finished or processed') self.logger.critical("No cleanup properly finished or processed")
def register_existingbackups(self, sections=[]): def register_existingbackups(self, sections=[]):
if not sections: if not sections:
sections = [backup_item.backup_name for backup_item in self.backup_list] sections = [backup_item.backup_name for backup_item in self.backup_list]
self.logger.info('Append existing backups to database...') self.logger.info("Append existing backups to database...")
for backup_item in self.backup_list: for backup_item in self.backup_list:
if backup_item.backup_name in sections: if backup_item.backup_name in sections:
backup_item.register_existingbackups() backup_item.register_existingbackups()
@@ -316,15 +338,15 @@ class tis_backup:
def html_report(self): def html_report(self):
for backup_item in self.backup_list: for backup_item in self.backup_list:
if not section or section == backup_item.backup_name: if not section or section == backup_item.backup_name:
assert(isinstance(backup_item,backup_generic)) assert isinstance(backup_item, backup_generic)
if not maxage_hours: if not maxage_hours:
maxage_hours = backup_item.maximum_backup_age maxage_hours = backup_item.maximum_backup_age
(nagiosstatus, log) = backup_item.checknagios(maxage_hours=maxage_hours) (nagiosstatus, log) = backup_item.checknagios(maxage_hours=maxage_hours)
globallog.append('[%s] %s' % (backup_item.backup_name,log)) globallog.append("[%s] %s" % (backup_item.backup_name, log))
self.logger.debug('[%s] nagios:"%i" log: %s', backup_item.backup_name, nagiosstatus, log) self.logger.debug('[%s] nagios:"%i" log: %s', backup_item.backup_name, nagiosstatus, log)
processed = True # processed = True
if nagiosstatus >= worst_nagiosstatus: # if nagiosstatus >= worst_nagiosstatus:
worst_nagiosstatus = nagiosstatus # worst_nagiosstatus = nagiosstatus
def main(): def main():
@@ -335,7 +357,7 @@ def main():
parser.print_usage() parser.print_usage()
sys.exit(2) sys.exit(2)
backup_start_date = datetime.datetime.now().strftime('%Y%m%d-%Hh%Mm%S') backup_start_date = datetime.datetime.now().strftime("%Y%m%d-%Hh%Mm%S")
# options # options
action = args[0] action = args[0]
@@ -351,16 +373,16 @@ def main():
loglevel = options.loglevel loglevel = options.loglevel
# setup Logger # setup Logger
logger = logging.getLogger('tisbackup') logger = logging.getLogger("tisbackup")
hdlr = logging.StreamHandler() hdlr = logging.StreamHandler()
hdlr.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) hdlr.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(hdlr) logger.addHandler(hdlr)
# set loglevel # set loglevel
if loglevel in ('debug','warning','info','error','critical'): if loglevel in ("debug", "warning", "info", "error", "critical"):
numeric_level = getattr(logging, loglevel.upper(), None) numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int): if not isinstance(numeric_level, int):
raise ValueError('Invalid log level: %s' % loglevel) raise ValueError("Invalid log level: %s" % loglevel)
logger.setLevel(numeric_level) logger.setLevel(numeric_level)
# Config file # Config file
@@ -371,19 +393,19 @@ def main():
cp = ConfigParser() cp = ConfigParser()
cp.read(config_file) cp.read(config_file)
backup_base_dir = options.backup_base_dir or cp.get('global','backup_base_dir') backup_base_dir = options.backup_base_dir or cp.get("global", "backup_base_dir")
log_dir = os.path.join(backup_base_dir,'log') log_dir = os.path.join(backup_base_dir, "log")
if not os.path.exists(log_dir): if not os.path.exists(log_dir):
os.makedirs(log_dir) os.makedirs(log_dir)
# if we run the nagios check, we don't create log file, everything is piped to stdout # if we run the nagios check, we don't create log file, everything is piped to stdout
if action!='checknagios': if action != "checknagios":
try: try:
hdlr = logging.FileHandler(os.path.join(log_dir,'tisbackup_%s.log' % (backup_start_date))) hdlr = logging.FileHandler(os.path.join(log_dir, "tisbackup_%s.log" % (backup_start_date)))
hdlr.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) hdlr.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(hdlr) logger.addHandler(hdlr)
except IOError as e: except IOError as e:
if action == 'cleanup' and e.errno == errno.ENOSPC: if action == "cleanup" and e.errno == errno.ENOSPC:
logger.warning("No space left on device, disabling file logging.") logger.warning("No space left on device, disabling file logging.")
else: else:
raise e raise e
@@ -392,15 +414,15 @@ def main():
backup = tis_backup(dry_run=dry_run, verbose=verbose, backup_base_dir=backup_base_dir) backup = tis_backup(dry_run=dry_run, verbose=verbose, backup_base_dir=backup_base_dir)
backup.read_ini_file(config_file) backup.read_ini_file(config_file)
backup_sections = options.sections.split(',') if options.sections else [] backup_sections = options.sections.split(",") if options.sections else []
all_sections = [backup_item.backup_name for backup_item in backup.backup_list] all_sections = [backup_item.backup_name for backup_item in backup.backup_list]
if not backup_sections: if not backup_sections:
backup_sections = all_sections backup_sections = all_sections
else: else:
for b in backup_sections: for b in backup_sections:
if not b in all_sections: if b not in all_sections:
raise Exception('Section %s is not defined in config file' % b) raise Exception("Section %s is not defined in config file" % b)
if dry_run: if dry_run:
logger.warning("WARNING : DRY RUN, nothing will be done, just printing on screen...") logger.warning("WARNING : DRY RUN, nothing will be done, just printing on screen...")
@@ -409,7 +431,7 @@ def main():
backup.process_backup(backup_sections) backup.process_backup(backup_sections)
elif action == "exportbackup": elif action == "exportbackup":
if not options.exportdir: if not options.exportdir:
raise Exception('No export directory supplied dor exportbackup action') raise Exception("No export directory supplied dor exportbackup action")
backup.export_backups(backup_sections, options.exportdir) backup.export_backups(backup_sections, options.exportdir)
elif action == "cleanup": elif action == "cleanup":
backup.cleanup_backup_section(backup_sections) backup.cleanup_backup_section(backup_sections)
@@ -423,7 +445,6 @@ def main():
elif action == "register_existing": elif action == "register_existing":
backup.register_existingbackups(backup_sections) backup.register_existingbackups(backup_sections)
else: else:
logger.error('Unhandled action "%s", quitting...', action) logger.error('Unhandled action "%s", quitting...', action)
sys.exit(1) sys.exit(1)
+153 -132
View File
@@ -17,52 +17,52 @@
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>. # along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os,sys import os
import sys
from os.path import isfile, join from os.path import isfile, join
tisbackup_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__))) tisbackup_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
sys.path.append(os.path.join(tisbackup_root_dir,'lib')) sys.path.append(os.path.join(tisbackup_root_dir, "lib"))
sys.path.append(os.path.join(tisbackup_root_dir,'libtisbackup')) sys.path.append(os.path.join(tisbackup_root_dir, "libtisbackup"))
from shutil import *
from iniparse import ConfigParser,RawConfigParser
from libtisbackup.common import *
import time
from flask import request, Flask, session, g, appcontext_pushed, redirect, url_for, abort, render_template, flash, jsonify, Response
from urllib.parse import urlparse
import json
import glob import glob
import time import json
from config import huey
from tasks import run_export_backup, get_task, set_task
from tisbackup import tis_backup
import logging import logging
import re import re
import time
from shutil import *
from urllib.parse import urlparse
from flask import Flask, Response, abort, appcontext_pushed, flash, g, jsonify, redirect, render_template, request, session, url_for
from iniparse import ConfigParser, RawConfigParser
from config import huey
from libtisbackup.common import *
from tasks import get_task, run_export_backup, set_task
from tisbackup import tis_backup
cp = ConfigParser() cp = ConfigParser()
cp.read("/etc/tis/tisbackup_gui.ini") cp.read("/etc/tis/tisbackup_gui.ini")
CONFIG = cp.get('general','config_tisbackup').split(",") CONFIG = cp.get("general", "config_tisbackup").split(",")
SECTIONS = cp.get('general','sections') SECTIONS = cp.get("general", "sections")
ADMIN_EMAIL = cp.get('general','ADMIN_EMAIL') ADMIN_EMAIL = cp.get("general", "ADMIN_EMAIL")
BASE_DIR = cp.get('general','base_config_dir') BASE_DIR = cp.get("general", "base_config_dir")
tisbackup_config_file = CONFIG[0] tisbackup_config_file = CONFIG[0]
config_number = 0 config_number = 0
cp = ConfigParser() cp = ConfigParser()
cp.read(tisbackup_config_file) cp.read(tisbackup_config_file)
backup_base_dir = cp.get('global','backup_base_dir') backup_base_dir = cp.get("global", "backup_base_dir")
dbstat = BackupStat(os.path.join(backup_base_dir,'log','tisbackup.sqlite')) dbstat = BackupStat(os.path.join(backup_base_dir, "log", "tisbackup.sqlite"))
mindate = None mindate = None
error = None error = None
info = None info = None
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'fsiqefiuqsefARZ4Zfesfe34234dfzefzfe' app.secret_key = "fsiqefiuqsefARZ4Zfesfe34234dfzefzfe"
app.config['PROPAGATE_EXCEPTIONS'] = True app.config["PROPAGATE_EXCEPTIONS"] = True
tasks_db = os.path.join(tisbackup_root_dir, "tasks.sqlite") tasks_db = os.path.join(tisbackup_root_dir, "tasks.sqlite")
@@ -70,7 +70,7 @@ tasks_db = os.path.join(tisbackup_root_dir,"tasks.sqlite")
def read_all_configs(base_dir): def read_all_configs(base_dir):
raw_configs = [] raw_configs = []
list_config = [] list_config = []
config_base_dir = base_dir # config_base_dir = base_dir
for file in os.listdir(base_dir): for file in os.listdir(base_dir):
if isfile(join(base_dir, file)): if isfile(join(base_dir, file)):
@@ -78,19 +78,19 @@ def read_all_configs(base_dir):
for elem in raw_configs: for elem in raw_configs:
line = open(elem).readline() line = open(elem).readline()
if 'global' in line: if "global" in line:
list_config.append(elem) list_config.append(elem)
backup_dict = {} backup_dict = {}
backup_dict['rsync_ssh_list'] = [] backup_dict["rsync_ssh_list"] = []
backup_dict['rsync_btrfs_list'] = [] backup_dict["rsync_btrfs_list"] = []
backup_dict['rsync_list'] = [] backup_dict["rsync_list"] = []
backup_dict['null_list'] = [] backup_dict["null_list"] = []
backup_dict['pgsql_list'] = [] backup_dict["pgsql_list"] = []
backup_dict['mysql_list'] = [] backup_dict["mysql_list"] = []
# backup_dict['sqlserver_list'] = [] # backup_dict['sqlserver_list'] = []
backup_dict['xva_list'] = [] backup_dict["xva_list"] = []
backup_dict['metadata_list'] = [] backup_dict["metadata_list"] = []
# backup_dict['switch_list'] = [] # backup_dict['switch_list'] = []
# backup_dict['oracle_list'] = [] # backup_dict['oracle_list'] = []
@@ -99,7 +99,7 @@ def read_all_configs(base_dir):
for config_file in list_config: for config_file in list_config:
cp.read(config_file) cp.read(config_file)
backup_base_dir = cp.get('global', 'backup_base_dir') backup_base_dir = cp.get("global", "backup_base_dir")
backup = tis_backup(backup_base_dir=backup_base_dir) backup = tis_backup(backup_base_dir=backup_base_dir)
backup.read_ini_file(config_file) backup.read_ini_file(config_file)
@@ -110,11 +110,12 @@ def read_all_configs(base_dir):
backup_sections = all_sections backup_sections = all_sections
else: else:
for b in backup_sections: for b in backup_sections:
if not b in all_sections: if b not in all_sections:
raise Exception('Section %s is not defined in config file' % b) raise Exception("Section %s is not defined in config file" % b)
if not backup_sections: # never used..
sections = [backup_item.backup_name for backup_item in backup.backup_list] # if not backup_sections:
# sections = [backup_item.backup_name for backup_item in backup.backup_list]
for backup_item in backup.backup_list: for backup_item in backup.backup_list:
if backup_item.backup_name in backup_sections: if backup_item.backup_name in backup_sections:
@@ -125,35 +126,28 @@ def read_all_configs(base_dir):
result.append(b) result.append(b)
for row in result: for row in result:
backup_name = row['backup_name'] backup_name = row["backup_name"]
server_name = row['server_name'] server_name = row["server_name"]
backup_type = row['type'] backup_type = row["type"]
if backup_type == "xcp-dump-metadata": if backup_type == "xcp-dump-metadata":
backup_dict['metadata_list'].append( backup_dict["metadata_list"].append([server_name, backup_name, backup_type, ""])
[server_name, backup_name, backup_type, ""])
if backup_type == "rsync+ssh": if backup_type == "rsync+ssh":
remote_dir = row['remote_dir'] remote_dir = row["remote_dir"]
backup_dict['rsync_ssh_list'].append( backup_dict["rsync_ssh_list"].append([server_name, backup_name, backup_type, remote_dir])
[server_name, backup_name, backup_type, remote_dir])
if backup_type == "rsync+btrfs+ssh": if backup_type == "rsync+btrfs+ssh":
remote_dir = row['remote_dir'] remote_dir = row["remote_dir"]
backup_dict['rsync_btrfs_list'].append( backup_dict["rsync_btrfs_list"].append([server_name, backup_name, backup_type, remote_dir])
[server_name, backup_name, backup_type, remote_dir])
if backup_type == "rsync": if backup_type == "rsync":
remote_dir = row['remote_dir'] remote_dir = row["remote_dir"]
backup_dict['rsync_list'].append( backup_dict["rsync_list"].append([server_name, backup_name, backup_type, remote_dir])
[server_name, backup_name, backup_type, remote_dir])
if backup_type == "null": if backup_type == "null":
backup_dict['null_list'].append( backup_dict["null_list"].append([server_name, backup_name, backup_type, ""])
[server_name, backup_name, backup_type, ""])
if backup_type == "pgsql+ssh": if backup_type == "pgsql+ssh":
db_name = row['db_name'] if len(row['db_name']) > 0 else '*' db_name = row["db_name"] if len(row["db_name"]) > 0 else "*"
backup_dict['pgsql_list'].append( backup_dict["pgsql_list"].append([server_name, backup_name, backup_type, db_name])
[server_name, backup_name, backup_type, db_name])
if backup_type == "mysql+ssh": if backup_type == "mysql+ssh":
db_name = row['db_name'] if len(row['db_name']) > 0 else '*' db_name = row["db_name"] if len(row["db_name"]) > 0 else "*"
backup_dict['mysql_list'].append( backup_dict["mysql_list"].append([server_name, backup_name, backup_type, db_name])
[server_name, backup_name, backup_type, db_name])
# if backup_type == "sqlserver+ssh": # if backup_type == "sqlserver+ssh":
# db_name = row['db_name'] # db_name = row['db_name']
# backup_dict['sqlserver_list'].append( # backup_dict['sqlserver_list'].append(
@@ -163,8 +157,7 @@ def read_all_configs(base_dir):
# backup_dict['oracle_list'].append( # backup_dict['oracle_list'].append(
# [server_name, backup_name, backup_type, db_name]) # [server_name, backup_name, backup_type, db_name])
if backup_type == "xen-xva": if backup_type == "xen-xva":
backup_dict['xva_list'].append( backup_dict["xva_list"].append([server_name, backup_name, backup_type, ""])
[server_name, backup_name, backup_type, ""])
# if backup_type == "switch": # if backup_type == "switch":
# backup_dict['switch_list'].append( # backup_dict['switch_list'].append(
# [server_name, backup_name, backup_type, ""]) # [server_name, backup_name, backup_type, ""])
@@ -177,7 +170,7 @@ def read_config():
cp = ConfigParser() cp = ConfigParser()
cp.read(config_file) cp.read(config_file)
backup_base_dir = cp.get('global','backup_base_dir') backup_base_dir = cp.get("global", "backup_base_dir")
backup = tis_backup(backup_base_dir=backup_base_dir) backup = tis_backup(backup_base_dir=backup_base_dir)
backup.read_ini_file(config_file) backup.read_ini_file(config_file)
@@ -188,12 +181,14 @@ def read_config():
backup_sections = all_sections backup_sections = all_sections
else: else:
for b in backup_sections: for b in backup_sections:
if not b in all_sections: if b not in all_sections:
raise Exception('Section %s is not defined in config file' % b) raise Exception("Section %s is not defined in config file" % b)
result = [] result = []
if not backup_sections:
sections = [backup_item.backup_name for backup_item in backup.backup_list] # not used ...
# if not backup_sections:
# sections = [backup_item.backup_name for backup_item in backup.backup_list]
for backup_item in backup.backup_list: for backup_item in backup.backup_list:
if backup_item.backup_name in backup_sections: if backup_item.backup_name in backup_sections:
@@ -204,40 +199,40 @@ def read_config():
result.append(b) result.append(b)
backup_dict = {} backup_dict = {}
backup_dict['rsync_ssh_list'] = [] backup_dict["rsync_ssh_list"] = []
backup_dict['rsync_btrfs_list'] = [] backup_dict["rsync_btrfs_list"] = []
backup_dict['rsync_list'] = [] backup_dict["rsync_list"] = []
backup_dict['null_list'] = [] backup_dict["null_list"] = []
backup_dict['pgsql_list'] = [] backup_dict["pgsql_list"] = []
backup_dict['mysql_list'] = [] backup_dict["mysql_list"] = []
# backup_dict['sqlserver_list'] = [] # backup_dict['sqlserver_list'] = []
backup_dict['xva_list'] = [] backup_dict["xva_list"] = []
backup_dict['metadata_list'] = [] backup_dict["metadata_list"] = []
# backup_dict['switch_list'] = [] # backup_dict['switch_list'] = []
# backup_dict['oracle_list'] = [] # backup_dict['oracle_list'] = []
for row in result: for row in result:
backup_name = row['backup_name'] backup_name = row["backup_name"]
server_name = row['server_name'] server_name = row["server_name"]
backup_type = row['type'] backup_type = row["type"]
if backup_type == "xcp-dump-metadata": if backup_type == "xcp-dump-metadata":
backup_dict['metadata_list'].append([server_name, backup_name, backup_type, ""]) backup_dict["metadata_list"].append([server_name, backup_name, backup_type, ""])
if backup_type == "rsync+ssh": if backup_type == "rsync+ssh":
remote_dir = row['remote_dir'] remote_dir = row["remote_dir"]
backup_dict['rsync_ssh_list'].append([server_name, backup_name, backup_type,remote_dir]) backup_dict["rsync_ssh_list"].append([server_name, backup_name, backup_type, remote_dir])
if backup_type == "rsync+btrfs+ssh": if backup_type == "rsync+btrfs+ssh":
remote_dir = row['remote_dir'] remote_dir = row["remote_dir"]
backup_dict['rsync_btrfs_list'].append([server_name, backup_name, backup_type,remote_dir]) backup_dict["rsync_btrfs_list"].append([server_name, backup_name, backup_type, remote_dir])
if backup_type == "rsync": if backup_type == "rsync":
remote_dir = row['remote_dir'] remote_dir = row["remote_dir"]
backup_dict['rsync_list'].append([server_name, backup_name, backup_type,remote_dir]) backup_dict["rsync_list"].append([server_name, backup_name, backup_type, remote_dir])
if backup_type == "null": if backup_type == "null":
backup_dict['null_list'].append([server_name, backup_name, backup_type, ""]) backup_dict["null_list"].append([server_name, backup_name, backup_type, ""])
if backup_type == "pgsql+ssh": if backup_type == "pgsql+ssh":
db_name = row['db_name'] if len(row['db_name']) > 0 else '*' db_name = row["db_name"] if len(row["db_name"]) > 0 else "*"
backup_dict['pgsql_list'].append([server_name, backup_name, backup_type, db_name]) backup_dict["pgsql_list"].append([server_name, backup_name, backup_type, db_name])
if backup_type == "mysql+ssh": if backup_type == "mysql+ssh":
db_name = row['db_name'] if len(row['db_name']) > 0 else '*' db_name = row["db_name"] if len(row["db_name"]) > 0 else "*"
backup_dict['mysql_list'].append([server_name, backup_name, backup_type, db_name]) backup_dict["mysql_list"].append([server_name, backup_name, backup_type, db_name])
# if backup_type == "sqlserver+ssh": # if backup_type == "sqlserver+ssh":
# db_name = row['db_name'] # db_name = row['db_name']
# backup_dict['sqlserver_list'].append([server_name, backup_name, backup_type, db_name]) # backup_dict['sqlserver_list'].append([server_name, backup_name, backup_type, db_name])
@@ -245,38 +240,57 @@ def read_config():
# db_name = row['db_name'] # db_name = row['db_name']
# backup_dict['oracle_list'].append([server_name, backup_name, backup_type, db_name]) # backup_dict['oracle_list'].append([server_name, backup_name, backup_type, db_name])
if backup_type == "xen-xva": if backup_type == "xen-xva":
backup_dict['xva_list'].append([server_name, backup_name, backup_type, ""]) backup_dict["xva_list"].append([server_name, backup_name, backup_type, ""])
# if backup_type == "switch": # if backup_type == "switch":
# backup_dict['switch_list'].append([server_name, backup_name, backup_type, ""]) # backup_dict['switch_list'].append([server_name, backup_name, backup_type, ""])
return backup_dict return backup_dict
@app.route('/')
@app.route("/")
def backup_all(): def backup_all():
backup_dict = read_config() backup_dict = read_config()
return render_template('backups.html', backup_list = backup_dict) return render_template("backups.html", backup_list=backup_dict)
@app.route('/config_number/') @app.route("/config_number/")
@app.route('/config_number/<int:id>') @app.route("/config_number/<int:id>")
def set_config_number(id=None): def set_config_number(id=None):
if id != None and len(CONFIG) > id: if id is not None and len(CONFIG) > id:
global config_number global config_number
config_number = id config_number = id
read_config() read_config()
return jsonify(configs=CONFIG, config_number=config_number) return jsonify(configs=CONFIG, config_number=config_number)
@app.route('/all_json') @app.route("/all_json")
def backup_all_json(): def backup_all_json():
backup_dict = read_all_configs(BASE_DIR) backup_dict = read_all_configs(BASE_DIR)
return json.dumps(backup_dict['rsync_list']+backup_dict['rsync_btrfs_list']+backup_dict['rsync_ssh_list']+backup_dict['pgsql_list']+backup_dict['mysql_list']+backup_dict['xva_list']+backup_dict['null_list']+backup_dict['metadata_list']) return json.dumps(
backup_dict["rsync_list"]
+ backup_dict["rsync_btrfs_list"]
+ backup_dict["rsync_ssh_list"]
+ backup_dict["pgsql_list"]
+ backup_dict["mysql_list"]
+ backup_dict["xva_list"]
+ backup_dict["null_list"]
+ backup_dict["metadata_list"]
)
# + backup_dict['switch_list'])+backup_dict['sqlserver_list'] # + backup_dict['switch_list'])+backup_dict['sqlserver_list']
@app.route('/json') @app.route("/json")
def backup_json(): def backup_json():
backup_dict = read_config() backup_dict = read_config()
return json.dumps(backup_dict['rsync_list']+backup_dict['rsync_btrfs_list']+backup_dict['rsync_ssh_list']+backup_dict['pgsql_list']+backup_dict['mysql_list']+backup_dict['xva_list']+backup_dict['null_list']+backup_dict['metadata_list']) return json.dumps(
backup_dict["rsync_list"]
+ backup_dict["rsync_btrfs_list"]
+ backup_dict["rsync_ssh_list"]
+ backup_dict["pgsql_list"]
+ backup_dict["mysql_list"]
+ backup_dict["xva_list"]
+ backup_dict["null_list"]
+ backup_dict["metadata_list"]
)
# + backup_dict['switch_list'])+backup_dict['sqlserver_list'] # + backup_dict['switch_list'])+backup_dict['sqlserver_list']
@@ -284,7 +298,7 @@ def check_usb_disk():
"""This method returns the mounts point of FIRST external disk""" """This method returns the mounts point of FIRST external disk"""
# disk_name = [] # disk_name = []
usb_disk_list = [] usb_disk_list = []
for name in glob.glob('/dev/sd[a-z]'): for name in glob.glob("/dev/sd[a-z]"):
for line in os.popen("udevadm info -q env -n %s" % name): for line in os.popen("udevadm info -q env -n %s" % name):
if re.match("ID_PATH=.*usb.*", line): if re.match("ID_PATH=.*usb.*", line):
usb_disk_list += [name] usb_disk_list += [name]
@@ -296,19 +310,22 @@ def check_usb_disk():
usb_partition_list = [] usb_partition_list = []
for usb_disk in usb_disk_list: for usb_disk in usb_disk_list:
cmd = "udevadm info -q path -n %s" % usb_disk + '1' cmd = "udevadm info -q path -n %s" % usb_disk + "1"
output = os.popen(cmd).read() output = os.popen(cmd).read()
print("cmd : " + cmd) print("cmd : " + cmd)
print("output : " + output) print("output : " + output)
if '/devices/pci' in output: if "/devices/pci" in output:
# flash("partition found: %s1" % usb_disk) # flash("partition found: %s1" % usb_disk)
usb_partition_list.append(usb_disk + "1") usb_partition_list.append(usb_disk + "1")
print(usb_partition_list) print(usb_partition_list)
if len(usb_partition_list) == 0: if len(usb_partition_list) == 0:
raise_error("The drive %s has no partition" % (usb_disk_list[0] ), "You should initialize the usb drive and format an ext4 partition with TISBACKUP label") raise_error(
"The drive %s has no partition" % (usb_disk_list[0]),
"You should initialize the usb drive and format an ext4 partition with TISBACKUP label",
)
return "" return ""
tisbackup_partition_list = [] tisbackup_partition_list = []
@@ -320,44 +337,46 @@ def check_usb_disk():
print(tisbackup_partition_list) print(tisbackup_partition_list)
if len(tisbackup_partition_list) == 0: if len(tisbackup_partition_list) == 0:
raise_error("No tisbackup partition exist on disk %s" % (usb_disk_list[0] ), "You should initialize the usb drive and format an ext4 partition with TISBACKUP label") raise_error(
"No tisbackup partition exist on disk %s" % (usb_disk_list[0]),
"You should initialize the usb drive and format an ext4 partition with TISBACKUP label",
)
return "" return ""
if len(tisbackup_partition_list) > 1: if len(tisbackup_partition_list) > 1:
raise_error("There are many usb disk", "You should plug remove one of them") raise_error("There are many usb disk", "You should plug remove one of them")
return "" return ""
return tisbackup_partition_list[0] return tisbackup_partition_list[0]
def check_already_mount(partition_name, refresh): def check_already_mount(partition_name, refresh):
with open('/proc/mounts') as f: with open("/proc/mounts") as f:
mount_point = "" mount_point = ""
for line in f.readlines(): for line in f.readlines():
if line.startswith(partition_name): if line.startswith(partition_name):
mount_point = line.split(' ')[1] mount_point = line.split(" ")[1]
if not refresh: if not refresh:
run_command("/bin/umount %s" % mount_point) run_command("/bin/umount %s" % mount_point)
os.rmdir(mount_point) os.rmdir(mount_point)
return mount_point return mount_point
def run_command(cmd, info=""): def run_command(cmd, info=""):
flash("Executing: %s" % cmd) flash("Executing: %s" % cmd)
from subprocess import CalledProcessError, check_output from subprocess import CalledProcessError, check_output
result = "" result = ""
try: try:
result = check_output(cmd, stderr=subprocess.STDOUT, shell=True) result = check_output(cmd, stderr=subprocess.STDOUT, shell=True)
except CalledProcessError as e: except CalledProcessError:
raise_error(result, info) raise_error(result, info)
return result return result
def check_mount_disk(partition_name, refresh):
def check_mount_disk(partition_name, refresh):
mount_point = check_already_mount(partition_name, refresh) mount_point = check_already_mount(partition_name, refresh)
if not refresh: if not refresh:
mount_point = "/mnt/TISBACKUP-" + str(time.time()) mount_point = "/mnt/TISBACKUP-" + str(time.time())
os.mkdir(mount_point) os.mkdir(mount_point)
flash("must mount " + partition_name) flash("must mount " + partition_name)
@@ -369,43 +388,41 @@ def check_mount_disk(partition_name, refresh):
return mount_point return mount_point
@app.route('/status.json')
@app.route("/status.json")
def export_backup_status(): def export_backup_status():
exports = dbstat.query('select * from stats where TYPE="EXPORT" and backup_start>="%s"' % mindate) exports = dbstat.query('select * from stats where TYPE="EXPORT" and backup_start>="%s"' % mindate)
error = "" error = ""
finish = not runnings_backups() finish = not runnings_backups()
if get_task() != None and finish: if get_task() is not None and finish:
status = get_task().get() status = get_task().get()
if status != "ok": if status != "ok":
error = "Export failing with error: " + status error = "Export failing with error: " + status
return jsonify(data=exports, finish=finish, error=error) return jsonify(data=exports, finish=finish, error=error)
def runnings_backups(): def runnings_backups():
task = get_task() task = get_task()
is_runnig = (task != None) is_runnig = task is not None
finish = ( is_runnig and task.get() != None) finish = is_runnig and task.get() is not None
return is_runnig and not finish return is_runnig and not finish
@app.route('/backups.json') @app.route("/backups.json")
def last_backup_json(): def last_backup_json():
exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC ') exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC ')
return Response(response=json.dumps(exports), return Response(response=json.dumps(exports), status=200, mimetype="application/json")
status=200,
mimetype="application/json")
@app.route('/last_backups') @app.route("/last_backups")
def last_backup(): def last_backup():
exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC LIMIT 20 ') exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC LIMIT 20 ')
return render_template("last_backups.html", backups=exports) return render_template("last_backups.html", backups=exports)
@app.route('/export_backup') @app.route("/export_backup")
def export_backup(): def export_backup():
raise_error("", "") raise_error("", "")
backup_dict = read_config() backup_dict = read_config()
sections = [] sections = []
@@ -418,12 +435,11 @@ def export_backup():
if len(section) > 0: if len(section) > 0:
sections.append(section[1]) sections.append(section[1])
noJobs = (not runnings_backups()) noJobs = not runnings_backups()
if "start" in list(request.args.keys()) or not noJobs: if "start" in list(request.args.keys()) or not noJobs:
start = True start = True
if "sections" in list(request.args.keys()): if "sections" in list(request.args.keys()):
backup_sections = request.args.getlist('sections') backup_sections = request.args.getlist("sections")
else: else:
start = False start = False
@@ -440,10 +456,14 @@ def export_backup():
mindate = datetime2isodate(datetime.datetime.now()) mindate = datetime2isodate(datetime.datetime.now())
if not error and start: if not error and start:
print(tisbackup_config_file) print(tisbackup_config_file)
task = run_export_backup(base=backup_base_dir, config_file=CONFIG[config_number], mount_point=mount_point, backup_sections=",".join([str(x) for x in backup_sections])) task = run_export_backup(
base=backup_base_dir,
config_file=CONFIG[config_number],
mount_point=mount_point,
backup_sections=",".join([str(x) for x in backup_sections]),
)
set_task(task) set_task(task)
return render_template("export_backup.html", error=error, start=start, info=info, email=ADMIN_EMAIL, sections=sections) return render_template("export_backup.html", error=error, start=start, info=info, email=ADMIN_EMAIL, sections=sections)
@@ -456,6 +476,7 @@ def raise_error(strError, strInfo):
if __name__ == "__main__": if __name__ == "__main__":
read_config() read_config()
from os import environ from os import environ
if 'WINGDB_ACTIVE' in environ:
if "WINGDB_ACTIVE" in environ:
app.debug = False app.debug = False
app.run(host= '0.0.0.0',port=8080) app.run(host="0.0.0.0", port=8080)
Generated
+440
View File
@@ -0,0 +1,440 @@
version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
name = "bcrypt"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719 },
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001 },
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451 },
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792 },
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752 },
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762 },
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384 },
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329 },
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241 },
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617 },
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751 },
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965 },
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316 },
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752 },
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 },
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 },
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 },
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 },
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 },
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 },
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 },
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 },
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 },
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 },
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 },
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 },
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 },
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 },
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 },
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 },
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 },
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 },
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 },
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 },
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 },
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 },
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 },
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 },
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 },
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 },
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 },
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "cryptography"
version = "44.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
{ url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
{ url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
{ url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
{ url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
{ url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
{ url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
{ url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
{ url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
{ url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
{ url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
{ url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
{ url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
{ url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
{ url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
{ url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
{ url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
{ url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
{ url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
{ url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
{ url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
{ url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
{ url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
{ url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
]
[[package]]
name = "flask"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
]
[[package]]
name = "huey"
version = "2.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/8c/2dfecf3705f5e522097b4e9fb6fb38e627a0340f8362cc05bd7a845e4279/huey-2.5.3.tar.gz", hash = "sha256:089fc72b97fd26a513f15b09925c56fad6abe4a699a1f0e902170b37e85163c7", size = 836918 }
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "iniparse"
version = "0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4c/9a/02beaf11fc9ea7829d3a9041536934cd03990e09c359724f99ee6bd2b41b/iniparse-0.5.tar.gz", hash = "sha256:932e5239d526e7acb504017bb707be67019ac428a6932368e6851691093aa842", size = 32233 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/b0/4d357324948188e76154b332e119fb28e374c1ebe4d4f6bca729aaa44309/iniparse-0.5-py3-none-any.whl", hash = "sha256:db6ef1d8a02395448e0e7b17ac0aa28b8d338b632bbd1ffca08c02ddae32cf97", size = 24445 },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
[[package]]
name = "paramiko"
version = "3.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "cryptography" },
{ name = "pynacl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298 },
]
[[package]]
name = "peewee"
version = "3.17.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/09/4393bd378e70b7fc3163ee83353cc27bb520010a5c2b3c924121e7e7e068/peewee-3.17.9.tar.gz", hash = "sha256:fe15cd001758e324c8e3ca8c8ed900e7397c2907291789e1efc383e66b9bc7a8", size = 3026085 }
[[package]]
name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 },
]
[[package]]
name = "pycparser"
version = "2.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
]
[[package]]
name = "pynacl"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 },
{ url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 },
{ url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 },
{ url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 },
{ url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 },
{ url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 },
{ url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 },
{ url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 },
{ url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 },
]
[[package]]
name = "redis"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "simplejson"
version = "3.20.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/92/51b417685abd96b31308b61b9acce7ec50d8e1de8fbc39a7fd4962c60689/simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d", size = 85591 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/03/0f453a27877cb5a5fff16a975925f4119102cc8552f52536b9a98ef0431e/simplejson-3.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71e849e7ceb2178344998cbe5ade101f1b329460243c79c27fbfc51c0447a7c3", size = 93109 },
{ url = "https://files.pythonhosted.org/packages/74/1f/a729f4026850cabeaff23e134646c3f455e86925d2533463420635ae54de/simplejson-3.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b63fdbab29dc3868d6f009a59797cefaba315fd43cd32ddd998ee1da28e50e29", size = 75475 },
{ url = "https://files.pythonhosted.org/packages/e2/14/50a2713fee8ff1f8d655b1a14f4a0f1c0c7246768a1b3b3d12964a4ed5aa/simplejson-3.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1190f9a3ce644fd50ec277ac4a98c0517f532cfebdcc4bd975c0979a9f05e1fb", size = 75112 },
{ url = "https://files.pythonhosted.org/packages/45/86/ea9835abb646755140e2d482edc9bc1e91997ed19a59fd77ae4c6a0facea/simplejson-3.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1336ba7bcb722ad487cd265701ff0583c0bb6de638364ca947bb84ecc0015d1", size = 150245 },
{ url = "https://files.pythonhosted.org/packages/12/b4/53084809faede45da829fe571c65fbda8479d2a5b9c633f46b74124d56f5/simplejson-3.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e975aac6a5acd8b510eba58d5591e10a03e3d16c1cf8a8624ca177491f7230f0", size = 158465 },
{ url = "https://files.pythonhosted.org/packages/a9/7d/d56579468d1660b3841e1f21c14490d103e33cf911886b22652d6e9683ec/simplejson-3.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a6dd11ee282937ad749da6f3b8d87952ad585b26e5edfa10da3ae2536c73078", size = 148514 },
{ url = "https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab980fcc446ab87ea0879edad41a5c28f2d86020014eb035cf5161e8de4474c6", size = 152262 },
{ url = "https://files.pythonhosted.org/packages/32/84/f0fdb3625292d945c2bd13a814584603aebdb38cfbe5fe9be6b46fe598c4/simplejson-3.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f5aee2a4cb6b146bd17333ac623610f069f34e8f31d2f4f0c1a2186e50c594f0", size = 150164 },
{ url = "https://files.pythonhosted.org/packages/95/51/6d625247224f01eaaeabace9aec75ac5603a42f8ebcce02c486fbda8b428/simplejson-3.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:652d8eecbb9a3b6461b21ec7cf11fd0acbab144e45e600c817ecf18e4580b99e", size = 151795 },
{ url = "https://files.pythonhosted.org/packages/7f/d9/bb921df6b35be8412f519e58e86d1060fddf3ad401b783e4862e0a74c4c1/simplejson-3.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c09948f1a486a89251ee3a67c9f8c969b379f6ffff1a6064b41fea3bce0a112", size = 159027 },
{ url = "https://files.pythonhosted.org/packages/03/c5/5950605e4ad023a6621cf4c931b29fd3d2a9c1f36be937230bfc83d7271d/simplejson-3.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cbbd7b215ad4fc6f058b5dd4c26ee5c59f72e031dfda3ac183d7968a99e4ca3a", size = 154380 },
{ url = "https://files.pythonhosted.org/packages/66/ad/b74149557c5ec1e4e4d55758bda426f5d2ec0123cd01a53ae63b8de51fa3/simplejson-3.20.1-cp313-cp313-win32.whl", hash = "sha256:ae81e482476eaa088ef9d0120ae5345de924f23962c0c1e20abbdff597631f87", size = 74102 },
{ url = "https://files.pythonhosted.org/packages/db/a9/25282fdd24493e1022f30b7f5cdf804255c007218b2bfaa655bd7ad34b2d/simplejson-3.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:1b9fd15853b90aec3b1739f4471efbf1ac05066a2c7041bf8db821bb73cd2ddc", size = 75736 },
{ url = "https://files.pythonhosted.org/packages/4b/30/00f02a0a921556dd5a6db1ef2926a1bc7a8bbbfb1c49cfed68a275b8ab2b/simplejson-3.20.1-py3-none-any.whl", hash = "sha256:8a6c1bbac39fa4a79f83cbf1df6ccd8ff7069582a9fd8db1e52cea073bc2c697", size = 57121 },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "tisbackup"
version = "1.8.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "huey" },
{ name = "iniparse" },
{ name = "paramiko" },
{ name = "peewee" },
{ name = "pexpect" },
{ name = "redis" },
{ name = "requests" },
{ name = "simplejson" },
{ name = "six" },
]
[package.metadata]
requires-dist = [
{ name = "flask", specifier = "==3.1.0" },
{ name = "huey", specifier = "==2.5.3" },
{ name = "iniparse", specifier = "==0.5" },
{ name = "paramiko", specifier = "==3.5.1" },
{ name = "peewee", specifier = "==3.17.9" },
{ name = "pexpect", specifier = "==4.9.0" },
{ name = "redis", specifier = "==5.2.1" },
{ name = "requests", specifier = "==2.32.3" },
{ name = "simplejson", specifier = "==3.20.1" },
{ name = "six", specifier = "==1.17.0" },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]
[[package]]
name = "werkzeug"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
]