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 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
+ ![GitHub](https://img.shields.io/github/license/geozeke/vbart)
36
+ ![PyPI](https://img.shields.io/pypi/v/vbart)
37
+ ![PyPI - Status](https://img.shields.io/pypi/status/vbart)
38
+ ![GitHub last commit](https://img.shields.io/github/last-commit/geozeke/vbart)
39
+ ![GitHub issues](https://img.shields.io/github/issues/geozeke/vbart)
40
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/parser201)
41
+ ![GitHub repo size](https://img.shields.io/github/repo-size/geozeke/vbart)
42
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/vbart)
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
+ ![GitHub](https://img.shields.io/github/license/geozeke/vbart)
4
+ ![PyPI](https://img.shields.io/pypi/v/vbart)
5
+ ![PyPI - Status](https://img.shields.io/pypi/status/vbart)
6
+ ![GitHub last commit](https://img.shields.io/github/last-commit/geozeke/vbart)
7
+ ![GitHub issues](https://img.shields.io/github/issues/geozeke/vbart)
8
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/parser201)
9
+ ![GitHub repo size](https://img.shields.io/github/repo-size/geozeke/vbart)
10
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/vbart)
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"
@@ -0,0 +1,4 @@
1
+ FROM alpine:latest
2
+ RUN apk -U upgrade
3
+ # Add support for xz compression
4
+ RUN apk add --no-cache xz
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