vbart 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vbart-0.1.0/LICENSE +22 -0
- vbart-0.1.0/PKG-INFO +164 -0
- vbart-0.1.0/README.md +131 -0
- vbart-0.1.0/pyproject.toml +61 -0
- vbart-0.1.0/src/vbart/Dockerfile +4 -0
- vbart-0.1.0/src/vbart/__init__.py +0 -0
- vbart-0.1.0/src/vbart/__main__.py +95 -0
- vbart-0.1.0/src/vbart/backup.py +42 -0
- vbart-0.1.0/src/vbart/backups.py +60 -0
- vbart-0.1.0/src/vbart/classes.py +122 -0
- vbart-0.1.0/src/vbart/constants.py +17 -0
- vbart-0.1.0/src/vbart/null.py +13 -0
- vbart-0.1.0/src/vbart/py.typed +0 -0
- vbart-0.1.0/src/vbart/refresh.py +55 -0
- vbart-0.1.0/src/vbart/restore.py +79 -0
- vbart-0.1.0/src/vbart/utilities.py +145 -0
vbart-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2024 Peter Nardi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
6
|
+
copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included
|
|
14
|
+
in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
17
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
vbart-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: vbart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Volume Backup And Restoration Tool for Docker
|
|
5
|
+
Home-page: https://github.com/geozeke/vbart
|
|
6
|
+
Keywords: archive,backup,compose,compress,compression,docker,restore,vbart,volume,volumes
|
|
7
|
+
Author: Peter Nardi
|
|
8
|
+
Author-email: pete@nardi.com
|
|
9
|
+
Maintainer: Peter Nardi
|
|
10
|
+
Maintainer-email: pete@nardi.com
|
|
11
|
+
Requires-Python: >=3.8.1,<4.0.0
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
25
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
26
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
27
|
+
Classifier: Topic :: Utilities
|
|
28
|
+
Requires-Dist: docker (>=7.0.0,<8.0.0)
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/geozeke/vbart/issues
|
|
30
|
+
Project-URL: Source Code, https://github.com/geozeke/vbart
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# vbart
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
<br>
|
|
45
|
+
|
|
46
|
+
<img
|
|
47
|
+
src="https://drive.google.com/uc?export=view&id=1H04KVAA3ohH_dLXIrC0bXuJXDn3VutKc"
|
|
48
|
+
alt = "Dinobox logo" width="120"/>
|
|
49
|
+
|
|
50
|
+
## Volume Backup And Restoration Tool for Docker
|
|
51
|
+
|
|
52
|
+
Why is backing up named docker volumes so hard? There's an
|
|
53
|
+
[extension][def] for Docker Desktop, but I just want a simple, easy to
|
|
54
|
+
use, command-line tool that allows me to backup and restore my named
|
|
55
|
+
docker volumes. That's what vbart does.
|
|
56
|
+
|
|
57
|
+
With vbart you can:
|
|
58
|
+
|
|
59
|
+
* Backup a single named volume.
|
|
60
|
+
* Backup all active named volumes on your host.
|
|
61
|
+
* Backup just the volumes you list in a separate file.
|
|
62
|
+
* Restore a single backup to a named volume.
|
|
63
|
+
|
|
64
|
+
All backups are stored in compressed (xz) tar archives. Once you create
|
|
65
|
+
a backup, you can copy it off-host, install it on another machine, share
|
|
66
|
+
with friends, etc.
|
|
67
|
+
|
|
68
|
+
### Installation
|
|
69
|
+
|
|
70
|
+
The preferred way to install vbart is with [pipx][def2]:
|
|
71
|
+
|
|
72
|
+
```shell
|
|
73
|
+
pipx install vbart
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Alternatively, you can create a separate virtual environment and install
|
|
77
|
+
it the traditional way:
|
|
78
|
+
|
|
79
|
+
```shell
|
|
80
|
+
pip3 install vbart
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Usage
|
|
84
|
+
|
|
85
|
+
For an overview, run:
|
|
86
|
+
|
|
87
|
+
```shell
|
|
88
|
+
vbart -h
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Backup a Single Volume
|
|
92
|
+
|
|
93
|
+
```shell
|
|
94
|
+
vbart backup volume_name
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For example, to backup a volume named `mysql_db`, use:
|
|
98
|
+
|
|
99
|
+
```shell
|
|
100
|
+
vbart backup mysql_db
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
vbart will then create a backup file in your current working directory
|
|
104
|
+
named:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
YYYYMMDD-mysql_db-backup.xz
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Backup Multiple Volumes
|
|
111
|
+
|
|
112
|
+
```shell
|
|
113
|
+
vbart backups [-v VOLUMES]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Note the plural command name (`backups` as opposed to `backup`).
|
|
117
|
+
`VOLUMES` is the optional name of a textfile that contains case
|
|
118
|
+
sensitive volume names (one per line) that you want to backup. Within
|
|
119
|
+
`VOLUMES` blank lines and lines beginning with `#` are ignored, so you
|
|
120
|
+
can comment the file if you wish.
|
|
121
|
+
|
|
122
|
+
If `VOLUMES` is not specified, all active docker volumes on the current
|
|
123
|
+
host are backed up. All volume backups are saved in the current working
|
|
124
|
+
directory and named as:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
YYYYMMDD-{volume_name}-backup.xz
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Restore a Single Volume
|
|
131
|
+
|
|
132
|
+
```shell
|
|
133
|
+
vbart restore backup_file volume_name
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The first argument (`backup_file`) is the compressed tar archive you
|
|
137
|
+
created when you made a backup. The file must have a `.xz` extension.
|
|
138
|
+
|
|
139
|
+
The second argument (`volume_name`) is the named volume to create from
|
|
140
|
+
the backup. If the named volume already exists, vbart will terminate
|
|
141
|
+
with no action. Otherwise, a new empty volume will be created with the
|
|
142
|
+
given name and the backup will be restored to that volume.
|
|
143
|
+
|
|
144
|
+
### Refresh vbart
|
|
145
|
+
|
|
146
|
+
If vbart is interrupted during execution (e.g. hitting `Control+C`),
|
|
147
|
+
then there may be dangling docker containers that hang on to existing
|
|
148
|
+
volumes. Running the refresh command will clear those dangling
|
|
149
|
+
containers.
|
|
150
|
+
|
|
151
|
+
Also, when you run vbart for the first time, it creates a small
|
|
152
|
+
(alpine-based) docker image to perform the actual backups. This image is
|
|
153
|
+
called `vbart_utility`. The refresh command also deletes the utility
|
|
154
|
+
image, causing it to be recreated the next time you run vbart.
|
|
155
|
+
|
|
156
|
+
To refresh vbart, use:
|
|
157
|
+
|
|
158
|
+
```shell
|
|
159
|
+
vbart refresh
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
[def]: https://hub.docker.com/extensions/docker/volumes-backup-extension
|
|
163
|
+
[def2]: https://pipx.pypa.io/stable/
|
|
164
|
+
|
vbart-0.1.0/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# vbart
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
<img
|
|
15
|
+
src="https://drive.google.com/uc?export=view&id=1H04KVAA3ohH_dLXIrC0bXuJXDn3VutKc"
|
|
16
|
+
alt = "Dinobox logo" width="120"/>
|
|
17
|
+
|
|
18
|
+
## Volume Backup And Restoration Tool for Docker
|
|
19
|
+
|
|
20
|
+
Why is backing up named docker volumes so hard? There's an
|
|
21
|
+
[extension][def] for Docker Desktop, but I just want a simple, easy to
|
|
22
|
+
use, command-line tool that allows me to backup and restore my named
|
|
23
|
+
docker volumes. That's what vbart does.
|
|
24
|
+
|
|
25
|
+
With vbart you can:
|
|
26
|
+
|
|
27
|
+
* Backup a single named volume.
|
|
28
|
+
* Backup all active named volumes on your host.
|
|
29
|
+
* Backup just the volumes you list in a separate file.
|
|
30
|
+
* Restore a single backup to a named volume.
|
|
31
|
+
|
|
32
|
+
All backups are stored in compressed (xz) tar archives. Once you create
|
|
33
|
+
a backup, you can copy it off-host, install it on another machine, share
|
|
34
|
+
with friends, etc.
|
|
35
|
+
|
|
36
|
+
### Installation
|
|
37
|
+
|
|
38
|
+
The preferred way to install vbart is with [pipx][def2]:
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
pipx install vbart
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Alternatively, you can create a separate virtual environment and install
|
|
45
|
+
it the traditional way:
|
|
46
|
+
|
|
47
|
+
```shell
|
|
48
|
+
pip3 install vbart
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Usage
|
|
52
|
+
|
|
53
|
+
For an overview, run:
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
vbart -h
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Backup a Single Volume
|
|
60
|
+
|
|
61
|
+
```shell
|
|
62
|
+
vbart backup volume_name
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For example, to backup a volume named `mysql_db`, use:
|
|
66
|
+
|
|
67
|
+
```shell
|
|
68
|
+
vbart backup mysql_db
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
vbart will then create a backup file in your current working directory
|
|
72
|
+
named:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
YYYYMMDD-mysql_db-backup.xz
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Backup Multiple Volumes
|
|
79
|
+
|
|
80
|
+
```shell
|
|
81
|
+
vbart backups [-v VOLUMES]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Note the plural command name (`backups` as opposed to `backup`).
|
|
85
|
+
`VOLUMES` is the optional name of a textfile that contains case
|
|
86
|
+
sensitive volume names (one per line) that you want to backup. Within
|
|
87
|
+
`VOLUMES` blank lines and lines beginning with `#` are ignored, so you
|
|
88
|
+
can comment the file if you wish.
|
|
89
|
+
|
|
90
|
+
If `VOLUMES` is not specified, all active docker volumes on the current
|
|
91
|
+
host are backed up. All volume backups are saved in the current working
|
|
92
|
+
directory and named as:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
YYYYMMDD-{volume_name}-backup.xz
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Restore a Single Volume
|
|
99
|
+
|
|
100
|
+
```shell
|
|
101
|
+
vbart restore backup_file volume_name
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The first argument (`backup_file`) is the compressed tar archive you
|
|
105
|
+
created when you made a backup. The file must have a `.xz` extension.
|
|
106
|
+
|
|
107
|
+
The second argument (`volume_name`) is the named volume to create from
|
|
108
|
+
the backup. If the named volume already exists, vbart will terminate
|
|
109
|
+
with no action. Otherwise, a new empty volume will be created with the
|
|
110
|
+
given name and the backup will be restored to that volume.
|
|
111
|
+
|
|
112
|
+
### Refresh vbart
|
|
113
|
+
|
|
114
|
+
If vbart is interrupted during execution (e.g. hitting `Control+C`),
|
|
115
|
+
then there may be dangling docker containers that hang on to existing
|
|
116
|
+
volumes. Running the refresh command will clear those dangling
|
|
117
|
+
containers.
|
|
118
|
+
|
|
119
|
+
Also, when you run vbart for the first time, it creates a small
|
|
120
|
+
(alpine-based) docker image to perform the actual backups. This image is
|
|
121
|
+
called `vbart_utility`. The refresh command also deletes the utility
|
|
122
|
+
image, causing it to be recreated the next time you run vbart.
|
|
123
|
+
|
|
124
|
+
To refresh vbart, use:
|
|
125
|
+
|
|
126
|
+
```shell
|
|
127
|
+
vbart refresh
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
[def]: https://hub.docker.com/extensions/docker/volumes-backup-extension
|
|
131
|
+
[def2]: https://pipx.pypa.io/stable/
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "vbart"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Volume Backup And Restoration Tool for Docker"
|
|
5
|
+
authors = ["Peter Nardi <pete@nardi.com>"]
|
|
6
|
+
maintainers = ["Peter Nardi <pete@nardi.com>"]
|
|
7
|
+
homepage = "https://github.com/geozeke/vbart"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
packages = [{ include = "vbart", from = "src" }]
|
|
10
|
+
include = ["LICENSE"]
|
|
11
|
+
keywords = [
|
|
12
|
+
"archive",
|
|
13
|
+
"backup",
|
|
14
|
+
"compose",
|
|
15
|
+
"compress",
|
|
16
|
+
"compression",
|
|
17
|
+
"docker",
|
|
18
|
+
"restore",
|
|
19
|
+
"vbart",
|
|
20
|
+
"volume",
|
|
21
|
+
"volumes",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Intended Audience :: Information Technology",
|
|
27
|
+
"Intended Audience :: System Administrators",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Natural Language :: English",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3.8",
|
|
32
|
+
"Programming Language :: Python :: 3.9",
|
|
33
|
+
"Programming Language :: Python :: 3.10",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Topic :: System :: Archiving :: Backup",
|
|
37
|
+
"Topic :: System :: Archiving :: Compression",
|
|
38
|
+
"Topic :: Utilities",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.poetry.urls]
|
|
42
|
+
"Source Code" = "https://github.com/geozeke/vbart"
|
|
43
|
+
"Bug Tracker" = "https://github.com/geozeke/vbart/issues"
|
|
44
|
+
|
|
45
|
+
[tool.poetry.scripts]
|
|
46
|
+
vbart = "vbart.__main__:main"
|
|
47
|
+
|
|
48
|
+
[tool.poetry.dependencies]
|
|
49
|
+
python = "^3.8.1"
|
|
50
|
+
docker = "^7.0.0"
|
|
51
|
+
|
|
52
|
+
[tool.poetry.group.dev.dependencies]
|
|
53
|
+
flake8 = "^7.0.0"
|
|
54
|
+
flake8-docstrings = "^1.7.0"
|
|
55
|
+
mypy = "^1.9.0"
|
|
56
|
+
black = "^24.3.0"
|
|
57
|
+
isort = "^5.13.2"
|
|
58
|
+
|
|
59
|
+
[build-system]
|
|
60
|
+
requires = ["poetry-core"]
|
|
61
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Entry point for vbart."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
from typing import List
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from vbart.constants import ARG_PARSERS_BASE
|
|
15
|
+
|
|
16
|
+
# ======================================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def collect_modules(start: Path) -> List[str]:
|
|
20
|
+
"""Collect the names of all modules to import.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
start : Path
|
|
25
|
+
This the starting point (directory) for collection.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
list[str]
|
|
30
|
+
A list of module names.
|
|
31
|
+
"""
|
|
32
|
+
mod_names: List[str] = []
|
|
33
|
+
for p in start.iterdir():
|
|
34
|
+
if p.is_file() and p.name != "__init__.py":
|
|
35
|
+
if "plugins" in str(p):
|
|
36
|
+
prefix = "plugins.parsers"
|
|
37
|
+
else:
|
|
38
|
+
prefix = "parsers"
|
|
39
|
+
mod_names.append(f"{prefix}.{p.stem}")
|
|
40
|
+
return mod_names
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ======================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> None:
|
|
47
|
+
"""Get user input and perform backup and restore operations."""
|
|
48
|
+
# Make sure docker is installed before going any further
|
|
49
|
+
if not (shutil.which("docker")):
|
|
50
|
+
print("\nYou must have docker installed to use vbart.\n")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
msg = """Volume Backup And Restoration Tool (for docker). A tool to
|
|
54
|
+
easily backup and restore named docker volumes. For help on any
|
|
55
|
+
command below, use: vbart {command} -h"""
|
|
56
|
+
epi = "Version: 0.1.0"
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
description=msg,
|
|
59
|
+
epilog=epi,
|
|
60
|
+
)
|
|
61
|
+
subparsers = parser.add_subparsers(title="commands", dest="cmd")
|
|
62
|
+
|
|
63
|
+
# Dynamically load argument subparsers.
|
|
64
|
+
|
|
65
|
+
mod_names: List[str] = []
|
|
66
|
+
mod: Union[ModuleType, None] = None
|
|
67
|
+
mod_names = collect_modules(ARG_PARSERS_BASE)
|
|
68
|
+
mod_names.sort()
|
|
69
|
+
|
|
70
|
+
# Argument parsers are saved in alphabetical order. This is a little
|
|
71
|
+
# slight-of-hand to get the desired order presented on screen.
|
|
72
|
+
mod_names[-1], mod_names[-2] = mod_names[-2], mod_names[-1]
|
|
73
|
+
|
|
74
|
+
for mod_name in mod_names:
|
|
75
|
+
mod = importlib.import_module(mod_name)
|
|
76
|
+
mod.load_command_args(subparsers)
|
|
77
|
+
|
|
78
|
+
args = parser.parse_args()
|
|
79
|
+
if args.cmd == "backup":
|
|
80
|
+
mod = importlib.import_module("vbart.backup")
|
|
81
|
+
elif args.cmd == "backups":
|
|
82
|
+
mod = importlib.import_module("vbart.backups")
|
|
83
|
+
elif args.cmd == "restore":
|
|
84
|
+
mod = importlib.import_module("vbart.restore")
|
|
85
|
+
elif args.cmd == "refresh":
|
|
86
|
+
mod = importlib.import_module("vbart.refresh")
|
|
87
|
+
else:
|
|
88
|
+
mod = importlib.import_module("vbart.null")
|
|
89
|
+
mod.task_runner(args)
|
|
90
|
+
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Perform single volume backup."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import docker # type:ignore
|
|
7
|
+
from docker import errors
|
|
8
|
+
|
|
9
|
+
from vbart.classes import Labels
|
|
10
|
+
from vbart.utilities import backup_one_volume
|
|
11
|
+
from vbart.utilities import clear
|
|
12
|
+
from vbart.utilities import verify_utility_image
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def task_runner(args: argparse.Namespace) -> None:
|
|
16
|
+
"""Backup a single named docker volume.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
args : Namespace
|
|
21
|
+
Command line arguments.
|
|
22
|
+
"""
|
|
23
|
+
verify_utility_image()
|
|
24
|
+
client = docker.from_env()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
client.volumes.get(args.volume_name)
|
|
28
|
+
msg = f'Backing up volume "{args.volume_name}"'
|
|
29
|
+
labels = Labels(msg)
|
|
30
|
+
except errors.NotFound:
|
|
31
|
+
print(f'Volume "{args.volume_name}" not found.')
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
clear()
|
|
35
|
+
labels.next()
|
|
36
|
+
print(backup_one_volume(args.volume_name))
|
|
37
|
+
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
pass
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Perform backup of multiple volumes."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
import docker # type:ignore
|
|
6
|
+
|
|
7
|
+
from vbart.classes import Labels
|
|
8
|
+
from vbart.utilities import backup_one_volume
|
|
9
|
+
from vbart.utilities import clear
|
|
10
|
+
from vbart.utilities import verify_utility_image
|
|
11
|
+
from vbart.utilities import wrap_tight
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def task_runner(args: argparse.Namespace) -> None:
|
|
15
|
+
"""Backup multiple docker volumes.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
args : Namespace
|
|
20
|
+
Command line arguments.
|
|
21
|
+
"""
|
|
22
|
+
verify_utility_image()
|
|
23
|
+
clear()
|
|
24
|
+
client = docker.from_env()
|
|
25
|
+
active_names = [v.name for v in client.volumes.list()] # type:ignore
|
|
26
|
+
|
|
27
|
+
if args.volumes:
|
|
28
|
+
print(f"Performing backups using {args.volumes.name}\n")
|
|
29
|
+
custom_names = [
|
|
30
|
+
t for
|
|
31
|
+
token in args.volumes
|
|
32
|
+
if (t := token.strip()) and (t[0] != "#")
|
|
33
|
+
] # fmt: skip
|
|
34
|
+
args.volumes.close()
|
|
35
|
+
target_names = list(set(active_names).intersection(set(custom_names)))
|
|
36
|
+
else:
|
|
37
|
+
print("Backing up all active docker volumes\n")
|
|
38
|
+
target_names = active_names.copy()
|
|
39
|
+
target_names.sort()
|
|
40
|
+
|
|
41
|
+
if target_names:
|
|
42
|
+
label_list = [f'Backing up volume "{name}"' for name in target_names]
|
|
43
|
+
labels = Labels("\n".join(label_list))
|
|
44
|
+
for name in target_names:
|
|
45
|
+
labels.next()
|
|
46
|
+
print(backup_one_volume(name))
|
|
47
|
+
else:
|
|
48
|
+
if args.volumes:
|
|
49
|
+
msg = f"""None of the volume names listed in
|
|
50
|
+
{args.volumes.name} are currently showing up as active
|
|
51
|
+
docker volumes."""
|
|
52
|
+
print(wrap_tight(msg=msg, columns=60))
|
|
53
|
+
else:
|
|
54
|
+
print("No active docker volumes found.")
|
|
55
|
+
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
pass
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Support classes."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExhaustedListError(Exception):
|
|
8
|
+
"""Exception when attempting to pop elements from an empty list.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
Exception : Python Exception type
|
|
13
|
+
ExhaustedListError is a sub-class of Python's Exception class.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize exception."""
|
|
18
|
+
self.message = "Cannot remove label, list of labels is empty."
|
|
19
|
+
super().__init__(self.message)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Labels:
|
|
23
|
+
"""Class to manage status labels."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, s: str) -> None:
|
|
26
|
+
"""Create a new Labels object.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
s : str
|
|
31
|
+
This is a docstring that has one label per line, the
|
|
32
|
+
initializer will repackage it into a list, along with an int
|
|
33
|
+
variable (pad) that represents the length of the longest
|
|
34
|
+
label. This is used for justifying the output when printing.
|
|
35
|
+
"""
|
|
36
|
+
# The (t := token.strip()) part of the list comprehension below is
|
|
37
|
+
# python's assignment expression and takes care of any blank lines or
|
|
38
|
+
# leading/trailing whitespace in the docstring. It assigns
|
|
39
|
+
# token.strip() to t then evaluates t. If t is an empty string, it
|
|
40
|
+
# evaluates to False otherwise it's True.
|
|
41
|
+
self.labels = [t for token in s.split("\n") if (t := token.strip())]
|
|
42
|
+
self.pad = len(max(self.labels, key=len)) + 3
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def next(self) -> None:
|
|
46
|
+
"""Print the next label (the one at position 0).
|
|
47
|
+
|
|
48
|
+
Raises
|
|
49
|
+
------
|
|
50
|
+
ExhaustedListError
|
|
51
|
+
If attempting to pop from an empty list.
|
|
52
|
+
"""
|
|
53
|
+
if len(self.labels) == 0:
|
|
54
|
+
raise ExhaustedListError()
|
|
55
|
+
print(f"{self.labels.pop(0):.<{self.pad}}", end="", flush=True)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
def pop_first(self) -> str:
|
|
59
|
+
"""Pop and return the first label (position 0).
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
str
|
|
64
|
+
A label.
|
|
65
|
+
|
|
66
|
+
Raises
|
|
67
|
+
------
|
|
68
|
+
ExhaustedListError
|
|
69
|
+
If attempting to pop from an empty list.
|
|
70
|
+
"""
|
|
71
|
+
if len(self.labels) == 0:
|
|
72
|
+
raise ExhaustedListError()
|
|
73
|
+
return self.labels.pop(0)
|
|
74
|
+
|
|
75
|
+
def pop_last(self) -> str:
|
|
76
|
+
"""Pop and return the last label (position -1).
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
str
|
|
81
|
+
A label.
|
|
82
|
+
|
|
83
|
+
Raises
|
|
84
|
+
------
|
|
85
|
+
ExhaustedListError
|
|
86
|
+
If attempting to pop from an empty list.
|
|
87
|
+
"""
|
|
88
|
+
if len(self.labels) == 0:
|
|
89
|
+
raise ExhaustedListError()
|
|
90
|
+
return self.labels.pop(-1)
|
|
91
|
+
|
|
92
|
+
def pop_item(self, index: int) -> str:
|
|
93
|
+
"""Pop a label from a given index.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
index : int
|
|
98
|
+
The index to pop from.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
str
|
|
103
|
+
The popped label.
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
ExhaustedListError
|
|
108
|
+
If attempting to pop from an empty list.
|
|
109
|
+
"""
|
|
110
|
+
if len(self.labels) == 0:
|
|
111
|
+
raise ExhaustedListError()
|
|
112
|
+
try:
|
|
113
|
+
label = self.labels.pop(index)
|
|
114
|
+
return label
|
|
115
|
+
except IndexError as e:
|
|
116
|
+
print(f"{e}. Attempting to pop index {index}.")
|
|
117
|
+
print("Terminating program.")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
pass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Constants."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
GREEN = "\033[0;32;49m"
|
|
6
|
+
RED = "\033[0;31;49m"
|
|
7
|
+
COLOR_END = "\x1b[0m"
|
|
8
|
+
|
|
9
|
+
PASS = f"{GREEN}\u2714{COLOR_END}"
|
|
10
|
+
FAIL = f"{RED}\u2718{COLOR_END}"
|
|
11
|
+
|
|
12
|
+
HOME = Path(__file__).parents[2]
|
|
13
|
+
|
|
14
|
+
ARG_PARSERS_BASE = HOME / "src/parsers"
|
|
15
|
+
BASE_IMAGE = "alpine:latest"
|
|
16
|
+
DOCKERFILE_PATH = HOME / "src/vbart"
|
|
17
|
+
UTILITY_IMAGE = "vbart_utility"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Taskrunner for no command.
|
|
2
|
+
|
|
3
|
+
This will be the default command, which reminds the user how to use the
|
|
4
|
+
program, then exits.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def task_runner(args: argparse.Namespace) -> None:
|
|
11
|
+
"""Print reminder message and exit."""
|
|
12
|
+
print("run 'vbart -h' for help.")
|
|
13
|
+
return
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Remove the vbart_utility image."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
import docker # type:ignore
|
|
6
|
+
from docker import errors
|
|
7
|
+
|
|
8
|
+
from vbart.constants import UTILITY_IMAGE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def task_runner(args: argparse.Namespace) -> None:
|
|
12
|
+
"""Purge any dangling containers and remove the vbart_utility image.
|
|
13
|
+
|
|
14
|
+
If a backup is interrupted (e.g. cntl-C), then there may be dangling
|
|
15
|
+
containers that are still hanging on to existing volumes. The
|
|
16
|
+
refresh option purges those dangling containers.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
args : Namespace
|
|
21
|
+
Command line arguments.
|
|
22
|
+
"""
|
|
23
|
+
client = docker.from_env()
|
|
24
|
+
|
|
25
|
+
# Prune any dangling containers.
|
|
26
|
+
|
|
27
|
+
filter = {"ancestor": f"{UTILITY_IMAGE}:latest"}
|
|
28
|
+
dangling = client.containers.list(
|
|
29
|
+
all=True,
|
|
30
|
+
filters=filter,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
for container in dangling:
|
|
34
|
+
container.remove(force=True) # type:ignore
|
|
35
|
+
|
|
36
|
+
# Delete the utility image and appropriate dependency.
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
client.images.get(UTILITY_IMAGE)
|
|
40
|
+
client.images.remove(UTILITY_IMAGE)
|
|
41
|
+
except errors.NotFound:
|
|
42
|
+
print("No refresh needed. All good.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
if dangling:
|
|
46
|
+
noun = "container" if len(dangling) == 1 else "containers"
|
|
47
|
+
print(f"{len(dangling)} dangling {noun} removed.")
|
|
48
|
+
print(f"The {UTILITY_IMAGE} image was deleted.")
|
|
49
|
+
print(f"{UTILITY_IMAGE} will be recreated the next time you run vbart.")
|
|
50
|
+
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Perform volume restoration."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import docker # type:ignore
|
|
8
|
+
from docker import errors
|
|
9
|
+
|
|
10
|
+
from vbart.classes import Labels
|
|
11
|
+
from vbart.constants import FAIL
|
|
12
|
+
from vbart.constants import PASS
|
|
13
|
+
from vbart.constants import UTILITY_IMAGE
|
|
14
|
+
from vbart.utilities import clear
|
|
15
|
+
from vbart.utilities import verify_utility_image
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def task_runner(args: argparse.Namespace) -> None:
|
|
19
|
+
"""Restore a backup to a docker volume.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
args : Namespace
|
|
24
|
+
Command line arguments.
|
|
25
|
+
"""
|
|
26
|
+
verify_utility_image()
|
|
27
|
+
client = docker.from_env()
|
|
28
|
+
|
|
29
|
+
# Check to see if the volume already exists. If so, report that and
|
|
30
|
+
# exit. If it doesn't exist, create it. Also, close the backup file
|
|
31
|
+
# - when it comes time to run the container, we only need the name.
|
|
32
|
+
|
|
33
|
+
args.backup_file.close()
|
|
34
|
+
try:
|
|
35
|
+
client.volumes.get(args.volume_name)
|
|
36
|
+
print(f'Volume "{args.volume_name}" already exists.')
|
|
37
|
+
print("No restoration performed.")
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
except errors.NotFound:
|
|
40
|
+
msg = f'Restoring backup to volume "{args.volume_name}"'
|
|
41
|
+
volume = client.volumes.create(args.volume_name)
|
|
42
|
+
labels = Labels(msg)
|
|
43
|
+
|
|
44
|
+
# Build volume map.
|
|
45
|
+
|
|
46
|
+
p = Path(args.backup_file.name)
|
|
47
|
+
volume_map = {
|
|
48
|
+
args.volume_name: {"bind": "/recover", "mode": "rw"},
|
|
49
|
+
p.parent.absolute(): {"bind": "/backup", "mode": "rw"},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Build the shell command to be run in the container
|
|
53
|
+
|
|
54
|
+
shell_arg = f'"cd /recover && tar xvf /backup/{p.name} --strip 1"'
|
|
55
|
+
shell_cmd = f"sh -c {shell_arg}"
|
|
56
|
+
|
|
57
|
+
# Run the container and extract the backup.
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
clear()
|
|
61
|
+
labels.next()
|
|
62
|
+
client.containers.run(
|
|
63
|
+
image=UTILITY_IMAGE,
|
|
64
|
+
command=shell_cmd,
|
|
65
|
+
remove=True,
|
|
66
|
+
volumes=volume_map,
|
|
67
|
+
)
|
|
68
|
+
print(PASS)
|
|
69
|
+
except errors.ContainerError:
|
|
70
|
+
print(FAIL)
|
|
71
|
+
print("\nInvalid backup file provided. Unable to restore.")
|
|
72
|
+
volume.remove() # type:ignore
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
pass
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Utilities to support docker volume backup/restore."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile as tf
|
|
5
|
+
import textwrap
|
|
6
|
+
from datetime import datetime as dt
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import docker # type:ignore
|
|
10
|
+
from docker import errors
|
|
11
|
+
|
|
12
|
+
from vbart.constants import BASE_IMAGE
|
|
13
|
+
from vbart.constants import DOCKERFILE_PATH
|
|
14
|
+
from vbart.constants import FAIL
|
|
15
|
+
from vbart.constants import PASS
|
|
16
|
+
from vbart.constants import UTILITY_IMAGE
|
|
17
|
+
|
|
18
|
+
# ======================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def wrap_tight(msg: str, columns=70) -> str:
|
|
22
|
+
"""Clean up a multi-line docstring.
|
|
23
|
+
|
|
24
|
+
Take a multi-line docstring and wrap it cleanly as a paragraph to a
|
|
25
|
+
specified column width.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
msg : str
|
|
30
|
+
The docstring to be wrapped.
|
|
31
|
+
columns : int, optional
|
|
32
|
+
Column width for wrapping, by default 70.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
str
|
|
37
|
+
A wrapped paragraph.
|
|
38
|
+
"""
|
|
39
|
+
clean = " ".join([t for token in msg.split("\n") if (t := token.strip())])
|
|
40
|
+
return textwrap.fill(clean, width=columns)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ======================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def clear() -> None:
|
|
47
|
+
"""Clear the screen.
|
|
48
|
+
|
|
49
|
+
OS-agnostic version, which will work with both Windows and Linux.
|
|
50
|
+
"""
|
|
51
|
+
os.system("clear" if os.name == "posix" else "cls")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ======================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def verify_utility_image() -> None:
|
|
58
|
+
"""Verify the backup utility image is in place.
|
|
59
|
+
|
|
60
|
+
If the utility image is not present, then build it. Also delete any
|
|
61
|
+
interim build products (images) that were created.
|
|
62
|
+
"""
|
|
63
|
+
# NOTE: The python docker package is not typed, so you'll see lots
|
|
64
|
+
# of "type: ignore" hashtags sprinkled throughout.
|
|
65
|
+
|
|
66
|
+
client = docker.from_env()
|
|
67
|
+
try:
|
|
68
|
+
client.images.get(UTILITY_IMAGE)
|
|
69
|
+
return
|
|
70
|
+
except errors.ImageNotFound:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# If the alpine image is already installed, don't delete it after
|
|
74
|
+
# creating the utility image.
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
client.images.get(BASE_IMAGE)
|
|
78
|
+
alpine_present = True
|
|
79
|
+
except errors.ImageNotFound:
|
|
80
|
+
alpine_present = False
|
|
81
|
+
|
|
82
|
+
msg = "Building utility image (this is a one-time operation)..."
|
|
83
|
+
print(f"{msg}", end="", flush=True)
|
|
84
|
+
client.images.build(
|
|
85
|
+
path=str(DOCKERFILE_PATH),
|
|
86
|
+
tag=f"{UTILITY_IMAGE}:latest",
|
|
87
|
+
nocache=True,
|
|
88
|
+
rm=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Saving and reloading the image flattens it so it will operate
|
|
92
|
+
# standalone, meaning it won't need any parent image dependencies.
|
|
93
|
+
|
|
94
|
+
utility_image = client.images.get(UTILITY_IMAGE)
|
|
95
|
+
with tf.TemporaryFile() as f:
|
|
96
|
+
for chunk in utility_image.save(named=True): # type: ignore
|
|
97
|
+
f.write(chunk)
|
|
98
|
+
f.seek(0)
|
|
99
|
+
client.images.remove(UTILITY_IMAGE)
|
|
100
|
+
if not alpine_present:
|
|
101
|
+
client.images.remove(BASE_IMAGE)
|
|
102
|
+
client.images.load(f)
|
|
103
|
+
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ======================================================================
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def backup_one_volume(volume: str) -> str:
|
|
111
|
+
"""Perform a backup on a single named volume.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
volume : str
|
|
116
|
+
The name of the volume to backup
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
str
|
|
121
|
+
Return either PASS or FAIL, depending on the result of the
|
|
122
|
+
backup.
|
|
123
|
+
"""
|
|
124
|
+
client = docker.from_env()
|
|
125
|
+
now = dt.now()
|
|
126
|
+
prefix = f"{now.year}{now.month:02d}{now.day:02d}"
|
|
127
|
+
p = Path(f"{prefix}-{volume}-backup.xz")
|
|
128
|
+
volume_map = {
|
|
129
|
+
volume: {"bind": "/recover", "mode": "rw"},
|
|
130
|
+
p.parent.absolute(): {"bind": "/backup", "mode": "rw"},
|
|
131
|
+
}
|
|
132
|
+
cmd = f"tar cavf /backup/{p.name} /recover"
|
|
133
|
+
|
|
134
|
+
# Run the container and perform the backup.
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
client.containers.run(
|
|
138
|
+
image=UTILITY_IMAGE,
|
|
139
|
+
command=cmd,
|
|
140
|
+
remove=True,
|
|
141
|
+
volumes=volume_map,
|
|
142
|
+
)
|
|
143
|
+
return PASS
|
|
144
|
+
except errors.ContainerError:
|
|
145
|
+
return FAIL
|