Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e823f65c3c | |||
| 5c627f3a64 | |||
| 7b6ce02a93 | |||
| e7d3e1140c | |||
| 6fe3eebf36 | |||
| 79d15628bd | |||
| 3a4f3267eb | |||
| 8761a04c40 | |||
| 586991bcf1 | |||
| ddb5f3716d | |||
| b805f8387e | |||
| da50051a3f | |||
| 8ef9bbde06 | |||
| 737f9bea38 |
+101
@@ -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/
|
||||||
@@ -14,7 +14,7 @@ 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: |
|
||||||
|
|||||||
+4
-2
@@ -2,13 +2,15 @@
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/*
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
.ruff_cache/
|
||||||
|
.mypy_cache/
|
||||||
/tasks.sqlite
|
/tasks.sqlite
|
||||||
/tasks.sqlite-wal
|
/tasks.sqlite-wal
|
||||||
/srvinstallation
|
/srvinstallation
|
||||||
/tasks.sqlite-shm
|
/tasks.sqlite-shm
|
||||||
.idea
|
.idea
|
||||||
.ruff_cache/*
|
|
||||||
/deb/builddir
|
/deb/builddir
|
||||||
/deb/*.deb
|
/deb/*.deb
|
||||||
/lib
|
/lib
|
||||||
|
|||||||
@@ -5,3 +5,12 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- 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
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"conventionalCommits.scopes": [
|
||||||
|
"tisbackup"
|
||||||
|
]
|
||||||
|
}
|
||||||
+11
-4
@@ -1,14 +1,21 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.13-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
WORKDIR /opt/tisbackup
|
WORKDIR /opt/tisbackup
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
COPY . /opt/tisbackup
|
COPY . /opt/tisbackup
|
||||||
|
|
||||||
RUN apt-get update \
|
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 \
|
&& apt-get install --no-install-recommends -y rsync ssh cron \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& /usr/local/bin/python3.12 -m pip install --no-cache-dir -r requirements.txt \
|
#&& /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 \
|
&& mkdir -p /var/spool/cron/crontabs \
|
||||||
&& echo '59 03 * * * root /bin/bash /opt/tisbackup/backup.sh' > /etc/crontab \
|
&& echo '59 03 * * * root /bin/bash /opt/tisbackup/backup.sh' > /etc/crontab \
|
||||||
&& echo '' >> /etc/crontab \
|
&& echo '' >> /etc/crontab \
|
||||||
@@ -17,4 +24,4 @@ RUN apt-get update \
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
CMD ["/usr/local/bin/python3.12","/opt/tisbackup/tisbackup_gui.py"]
|
CMD ["/usr/local/bin/python3.13","/opt/tisbackup/tisbackup_gui.py"]
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
#
|
#
|
||||||
|
|||||||
+44
-45
@@ -61,10 +61,11 @@ import sys
|
|||||||
import six.moves.http_client as httplib
|
import six.moves.http_client as httplib
|
||||||
import six.moves.xmlrpc_client as xmlrpclib
|
import six.moves.xmlrpc_client as xmlrpclib
|
||||||
|
|
||||||
translation = gettext.translation('xen-xm', fallback = True)
|
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):
|
||||||
@@ -79,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.
|
||||||
@@ -126,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:
|
||||||
@@ -162,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
|
||||||
|
|
||||||
@@ -208,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.
|
||||||
@@ -251,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:
|
||||||
|
|||||||
@@ -19,11 +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))
|
||||||
@@ -36,29 +35,29 @@ 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:
|
||||||
@@ -67,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)
|
||||||
@@ -109,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)
|
||||||
@@ -119,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)
|
||||||
@@ -130,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)
|
||||||
|
|||||||
@@ -27,25 +27,32 @@ 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
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
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))
|
||||||
@@ -38,17 +38,19 @@ 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:
|
||||||
@@ -60,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):
|
||||||
@@ -71,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)
|
||||||
@@ -89,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)
|
||||||
@@ -106,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)
|
||||||
@@ -117,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)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
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))
|
||||||
@@ -33,13 +33,14 @@ 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)
|
||||||
@@ -50,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)
|
||||||
@@ -58,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)
|
||||||
@@ -107,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()
|
||||||
@@ -116,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)
|
||||||
|
|||||||
+145
-110
@@ -30,31 +30,37 @@ 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,54 +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()))
|
||||||
|
|||||||
+151
-116
@@ -30,31 +30,37 @@ 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():
|
||||||
@@ -63,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:
|
||||||
@@ -79,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])
|
||||||
@@ -122,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)
|
||||||
@@ -177,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)
|
||||||
|
|
||||||
@@ -211,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()
|
||||||
|
|
||||||
@@ -242,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:
|
||||||
@@ -254,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)
|
||||||
@@ -295,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
|
||||||
@@ -307,54 +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()))
|
||||||
|
|||||||
@@ -19,11 +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)
|
||||||
@@ -36,24 +35,25 @@ 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:
|
||||||
@@ -62,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)
|
||||||
@@ -96,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)
|
||||||
@@ -106,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)
|
||||||
@@ -117,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)
|
||||||
|
|||||||
@@ -19,11 +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)
|
||||||
@@ -40,41 +39,40 @@ 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'
|
||||||
@@ -83,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)
|
||||||
@@ -93,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)
|
||||||
@@ -103,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)
|
||||||
@@ -114,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)
|
||||||
|
|||||||
@@ -41,16 +41,16 @@ 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))
|
||||||
@@ -58,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:
|
||||||
@@ -80,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")
|
||||||
|
|
||||||
@@ -92,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:
|
||||||
@@ -112,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:
|
||||||
@@ -132,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(".*#")
|
||||||
@@ -166,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):
|
||||||
@@ -184,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:
|
||||||
@@ -198,67 +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)
|
||||||
|
|||||||
+48
-59
@@ -26,6 +26,7 @@ import pyVmomi
|
|||||||
import requests
|
import requests
|
||||||
from pyVim.connect import Disconnect, SmartConnect
|
from pyVim.connect import Disconnect, SmartConnect
|
||||||
from pyVmomi import vim, vmodl
|
from pyVmomi import vim, vmodl
|
||||||
|
|
||||||
# Disable HTTPS verification warnings.
|
# Disable HTTPS verification warnings.
|
||||||
from requests.packages import urllib3
|
from requests.packages import urllib3
|
||||||
|
|
||||||
@@ -41,10 +42,10 @@ 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,32 +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)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
|
|
||||||
from .common import *
|
from .common import *
|
||||||
@@ -27,67 +26,76 @@ from .common import *
|
|||||||
|
|
||||||
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)
|
||||||
|
|||||||
+88
-68
@@ -36,16 +36,24 @@ import requests
|
|||||||
from . import XenAPI
|
from . import XenAPI
|
||||||
from .common import *
|
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"
|
||||||
@@ -55,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):
|
||||||
@@ -91,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))
|
||||||
@@ -114,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)
|
||||||
@@ -130,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")))
|
||||||
@@ -152,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)
|
||||||
@@ -170,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)
|
||||||
@@ -196,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)
|
||||||
|
|||||||
+342
-232
File diff suppressed because it is too large
Load Diff
+59
-69
@@ -36,16 +36,17 @@ from stat import *
|
|||||||
from . import XenAPI
|
from . import XenAPI
|
||||||
from .common import *
|
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 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
|
||||||
@@ -53,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()
|
||||||
|
|
||||||
@@ -87,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]
|
||||||
@@ -121,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():
|
||||||
@@ -141,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)
|
||||||
|
|
||||||
@@ -152,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)
|
||||||
@@ -171,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)
|
||||||
@@ -179,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)
|
||||||
|
|
||||||
@@ -190,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)
|
||||||
@@ -219,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:
|
||||||
@@ -266,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):
|
||||||
@@ -294,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)
|
||||||
|
|||||||
@@ -3,21 +3,37 @@
|
|||||||
# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk>
|
# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk>
|
||||||
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
|
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
|
||||||
|
|
||||||
from .compat import ConfigParser, RawConfigParser, SafeConfigParser
|
|
||||||
from .config import BasicConfig, ConfigNamespace
|
|
||||||
from .configparser import (DEFAULTSECT, MAX_INTERPOLATION_DEPTH,
|
|
||||||
DuplicateSectionError, InterpolationDepthError,
|
|
||||||
InterpolationMissingOptionError,
|
|
||||||
InterpolationSyntaxError, NoOptionError,
|
|
||||||
NoSectionError)
|
|
||||||
from .ini import INIConfig, change_comment_syntax
|
from .ini import INIConfig, change_comment_syntax
|
||||||
|
from .config import BasicConfig, ConfigNamespace
|
||||||
|
from .compat import RawConfigParser, ConfigParser, SafeConfigParser
|
||||||
from .utils import tidy
|
from .utils import tidy
|
||||||
|
|
||||||
|
from .configparser import (
|
||||||
|
DuplicateSectionError,
|
||||||
|
NoSectionError,
|
||||||
|
NoOptionError,
|
||||||
|
InterpolationMissingOptionError,
|
||||||
|
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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,41 +12,49 @@ The underlying INIConfig object can be accessed as cfg.data
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from typing import Dict, List, TextIO, Optional, Type, Union, Tuple
|
||||||
|
|
||||||
import six
|
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.
|
||||||
|
from .configparser import Error, InterpolationError, MissingSectionHeaderError, ParsingError
|
||||||
|
|
||||||
from . import ini
|
from . import ini
|
||||||
# These are imported only for compatiability.
|
|
||||||
# The code below does not reference them directly.
|
|
||||||
from .configparser import (DEFAULTSECT, MAX_INTERPOLATION_DEPTH,
|
|
||||||
DuplicateSectionError, Error,
|
|
||||||
InterpolationDepthError, InterpolationError,
|
|
||||||
InterpolationMissingOptionError,
|
|
||||||
InterpolationSyntaxError, MissingSectionHeaderError,
|
|
||||||
NoOptionError, NoSectionError, ParsingError)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@@ -56,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
|
||||||
@@ -86,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:
|
||||||
@@ -100,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
|
||||||
@@ -110,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]
|
||||||
@@ -175,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
|
||||||
@@ -183,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):
|
||||||
@@ -199,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
|
||||||
@@ -223,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
|
||||||
@@ -233,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.
|
||||||
|
|
||||||
@@ -269,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:
|
||||||
@@ -327,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))
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
try:
|
|
||||||
# not all objects get imported with __all__
|
|
||||||
from ConfigParser import *
|
|
||||||
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
@@ -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
|
||||||
|
|
||||||
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
|
||||||
from .configparser import DEFAULTSECT, MissingSectionHeaderError, ParsingError
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from compat import RawConfigParser
|
||||||
|
|
||||||
|
|
||||||
class LineType(object):
|
class LineType:
|
||||||
line = None
|
line: Optional[str] = None
|
||||||
|
|
||||||
def __init__(self, line=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:
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from . import compat
|
from . import compat
|
||||||
from .ini import EmptyLine, LineContainer
|
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]
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-1
@@ -1,10 +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]
|
[tool.black]
|
||||||
line-length = 140
|
line-length = 140
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
# Allow lines to be as long as 120.
|
# Allow lines to be as long as 120.
|
||||||
line-length = 140
|
line-length = 140
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
ignore = ["F401","F403","F405","E402"]
|
ignore = ["F401", "F403", "F405", "E402", "E701", "E722", "E741"]
|
||||||
|
|||||||
+10
-10
@@ -1,10 +1,10 @@
|
|||||||
six
|
flask==3.1.0
|
||||||
requests
|
huey==2.5.3
|
||||||
paramiko
|
iniparse==0.5
|
||||||
pexpect
|
paramiko==3.5.1
|
||||||
flask
|
peewee==3.17.9
|
||||||
simplejson
|
pexpect==4.9.0
|
||||||
huey
|
redis==5.2.1
|
||||||
iniparse
|
requests==2.32.3
|
||||||
redis
|
simplejson==3.20.1
|
||||||
peewee
|
six==1.17.0
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ 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)
|
||||||
@@ -29,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
|
||||||
|
|||||||
@@ -375,10 +375,8 @@ def run_command(cmd, info=""):
|
|||||||
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -425,7 +423,6 @@ def last_backup():
|
|||||||
|
|
||||||
@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 = []
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user