django-sync-env 0.2.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.
- django_sync_env-0.2.0/PKG-INFO +65 -0
- django_sync_env-0.2.0/README.md +49 -0
- django_sync_env-0.2.0/pyproject.toml +16 -0
- django_sync_env-0.2.0/src/django_sync_env/__init__.py +0 -0
- django_sync_env-0.2.0/src/django_sync_env/apps.py +20 -0
- django_sync_env-0.2.0/src/django_sync_env/checks.py +39 -0
- django_sync_env-0.2.0/src/django_sync_env/constants.py +3 -0
- django_sync_env-0.2.0/src/django_sync_env/db/__init__.py +0 -0
- django_sync_env-0.2.0/src/django_sync_env/db/base.py +186 -0
- django_sync_env-0.2.0/src/django_sync_env/db/exceptions.py +17 -0
- django_sync_env-0.2.0/src/django_sync_env/db/mongodb.py +54 -0
- django_sync_env-0.2.0/src/django_sync_env/db/mysql.py +45 -0
- django_sync_env-0.2.0/src/django_sync_env/db/postgresql.py +127 -0
- django_sync_env-0.2.0/src/django_sync_env/db/sqlite.py +98 -0
- django_sync_env-0.2.0/src/django_sync_env/log.py +11 -0
- django_sync_env-0.2.0/src/django_sync_env/management/__init__.py +0 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/__init__.py +0 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/_base.py +160 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_backup_db.py +88 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_backup_media.py +128 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_list_db_backups.py +52 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_list_media_backups.py +51 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_restore_db.py +166 -0
- django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_restore_media.py +170 -0
- django_sync_env-0.2.0/src/django_sync_env/settings.py +53 -0
- django_sync_env-0.2.0/src/django_sync_env/storage.py +290 -0
- django_sync_env-0.2.0/src/django_sync_env/tasks.py +27 -0
- django_sync_env-0.2.0/src/django_sync_env/utils.py +462 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django-sync-env
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Backup, Sync and Restore Databases and Media
|
|
5
|
+
Author: Dan Brosnan
|
|
6
|
+
Author-email: dan.brosnan@octave.nz
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: django-storages (>=1.14.2,<2.0.0)
|
|
13
|
+
Requires-Dist: inquirer (>=3.1.3,<4.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
## Django sync env
|
|
17
|
+
|
|
18
|
+
django-sync-env is a Django app to manage backing up and restoring django databases and media assets easy.
|
|
19
|
+
|
|
20
|
+
Detailed documentation is in the "docs" directory. (needs some work)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Requirements:
|
|
24
|
+
- configurable backups and restore which can be ran on demand, celery cron or in dev.
|
|
25
|
+
- support s3 bucket and file storage options first, add azure blob storage later
|
|
26
|
+
- interactive prompts for local dev
|
|
27
|
+
- command line options with --no-input for CICD and automations
|
|
28
|
+
- restore latest option for media and backups for X env
|
|
29
|
+
- backup before restore option
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## TODO:
|
|
33
|
+
- update docs around how to development this package
|
|
34
|
+
- remove command options which we wont use
|
|
35
|
+
- make sure all management commands work will good error logging and handling
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Quick start
|
|
39
|
+
-----------
|
|
40
|
+
|
|
41
|
+
1. Add "django_sync_env" to your INSTALLED_APPS setting like this:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
INSTALLED_APPS = [
|
|
45
|
+
...,
|
|
46
|
+
"django_sync_env",
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. Configure the app via a settings file .typically `./settings/partial/sync_env.py` for base configuration,
|
|
51
|
+
don't forget to import this file via your base.py settings file
|
|
52
|
+
And override any required settings per environment via `./settings/partials/[env].py`
|
|
53
|
+
see [example_partials](docs/example_partials.md)
|
|
54
|
+
|
|
55
|
+
There is a [s3 terraform example](docs/example_terraform_aws_s3_bucket.md) for provisioning
|
|
56
|
+
an aws s3 bucket, iam user, roles and policy to allow for remote backup/restore to/from a secure s3 bucket.
|
|
57
|
+
|
|
58
|
+
See [management commands](docs/management_commands.md) for more details for each of the commands available.
|
|
59
|
+
|
|
60
|
+
- `./manage.py sync_env_backup_db`
|
|
61
|
+
- `./manage.py sync_env_backup_media`
|
|
62
|
+
- `./manage.py sync_env_restore_db`
|
|
63
|
+
- `./manage.py sync_env_restore_media`
|
|
64
|
+
- `./manage.py sync_env_list_backups`
|
|
65
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## Django sync env
|
|
2
|
+
|
|
3
|
+
django-sync-env is a Django app to manage backing up and restoring django databases and media assets easy.
|
|
4
|
+
|
|
5
|
+
Detailed documentation is in the "docs" directory. (needs some work)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Requirements:
|
|
9
|
+
- configurable backups and restore which can be ran on demand, celery cron or in dev.
|
|
10
|
+
- support s3 bucket and file storage options first, add azure blob storage later
|
|
11
|
+
- interactive prompts for local dev
|
|
12
|
+
- command line options with --no-input for CICD and automations
|
|
13
|
+
- restore latest option for media and backups for X env
|
|
14
|
+
- backup before restore option
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## TODO:
|
|
18
|
+
- update docs around how to development this package
|
|
19
|
+
- remove command options which we wont use
|
|
20
|
+
- make sure all management commands work will good error logging and handling
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Quick start
|
|
24
|
+
-----------
|
|
25
|
+
|
|
26
|
+
1. Add "django_sync_env" to your INSTALLED_APPS setting like this:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
INSTALLED_APPS = [
|
|
30
|
+
...,
|
|
31
|
+
"django_sync_env",
|
|
32
|
+
]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. Configure the app via a settings file .typically `./settings/partial/sync_env.py` for base configuration,
|
|
36
|
+
don't forget to import this file via your base.py settings file
|
|
37
|
+
And override any required settings per environment via `./settings/partials/[env].py`
|
|
38
|
+
see [example_partials](docs/example_partials.md)
|
|
39
|
+
|
|
40
|
+
There is a [s3 terraform example](docs/example_terraform_aws_s3_bucket.md) for provisioning
|
|
41
|
+
an aws s3 bucket, iam user, roles and policy to allow for remote backup/restore to/from a secure s3 bucket.
|
|
42
|
+
|
|
43
|
+
See [management commands](docs/management_commands.md) for more details for each of the commands available.
|
|
44
|
+
|
|
45
|
+
- `./manage.py sync_env_backup_db`
|
|
46
|
+
- `./manage.py sync_env_backup_media`
|
|
47
|
+
- `./manage.py sync_env_restore_db`
|
|
48
|
+
- `./manage.py sync_env_restore_media`
|
|
49
|
+
- `./manage.py sync_env_list_backups`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "django-sync-env"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Backup, Sync and Restore Databases and Media"
|
|
5
|
+
authors = ["Dan Brosnan <dan.brosnan@octave.nz>", "Will Cook <will@octave.nz>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{ include = "django_sync_env", from = "src" }]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.10"
|
|
11
|
+
inquirer = "^3.1.3"
|
|
12
|
+
django-storages = "^1.14.2"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["poetry-core"]
|
|
16
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Apps for SyncEnv"""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
from django.utils.translation import gettext_lazy
|
|
5
|
+
|
|
6
|
+
from django_sync_env import log
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SyncEnvConfig(AppConfig):
|
|
10
|
+
"""
|
|
11
|
+
Config for SyncEnv
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name = "django_sync_env"
|
|
15
|
+
label = "django_sync_env"
|
|
16
|
+
verbose_name = gettext_lazy("sync env")
|
|
17
|
+
default_auto_field = "django.db.models.AutoField"
|
|
18
|
+
|
|
19
|
+
def ready(self):
|
|
20
|
+
log.load_logger()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from django.core.checks import Tags, Warning, register
|
|
4
|
+
|
|
5
|
+
from syncenv import settings
|
|
6
|
+
|
|
7
|
+
W001 = Warning(
|
|
8
|
+
"SYNC_ENV_BACKUP_CONFIG setting has not be configured",
|
|
9
|
+
hint="Set up settings.SYNC_ENV_BACKUP_CONFIG see syncenv readme",
|
|
10
|
+
id="syncenv.W001",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
W002 = Warning(
|
|
14
|
+
"SYNC_ENV_RESTORE_CONFIG setting has not be configured",
|
|
15
|
+
hint="Set up settings.SYNC_ENV_RESTORE_CONFIG see syncenv readme",
|
|
16
|
+
id="syncenv.W002",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
W005 = Warning(
|
|
20
|
+
"Invalid DATE_FORMAT parameter",
|
|
21
|
+
hint="settings.SYNC_ENV_DATE_FORMAT can contain only [A-Za-z0-9%_-]",
|
|
22
|
+
id="syncenv.W005",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register(Tags.compatibility)
|
|
27
|
+
def check_settings(app_configs, **kwargs):
|
|
28
|
+
errors = []
|
|
29
|
+
|
|
30
|
+
if not settings.SYNC_ENV_BACKUP_CONFIG:
|
|
31
|
+
errors.append(W001)
|
|
32
|
+
|
|
33
|
+
if not settings.SYNC_ENV_RESTORE_CONFIG:
|
|
34
|
+
errors.append(W001)
|
|
35
|
+
|
|
36
|
+
if re.search(r"[^A-Za-z0-9%_-]", settings.DATE_FORMAT):
|
|
37
|
+
errors.append(W005)
|
|
38
|
+
|
|
39
|
+
return errors
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base database connectors
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from subprocess import Popen
|
|
9
|
+
from tempfile import SpooledTemporaryFile
|
|
10
|
+
|
|
11
|
+
from django.core.files.base import File
|
|
12
|
+
|
|
13
|
+
from django_sync_env import settings, utils
|
|
14
|
+
|
|
15
|
+
from django_sync_env.db import exceptions
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("sync_env")
|
|
18
|
+
logger.setLevel(logging.DEBUG)
|
|
19
|
+
|
|
20
|
+
CONNECTOR_MAPPING = {
|
|
21
|
+
# "django.db.backends.sqlite3": "syncenv.db.sqlite.SqliteConnector",
|
|
22
|
+
# "django.db.backends.mysql": "syncenv.db.mysql.MysqlDumpConnector",
|
|
23
|
+
"django.db.backends.postgresql": "django_sync_env.db.postgresql.PgDumpBinaryConnector",
|
|
24
|
+
"django.db.backends.postgresql_psycopg2": "django_sync_env.db.postgresql.PgDumpBinaryConnector",
|
|
25
|
+
# "django.db.backends.oracle": None,
|
|
26
|
+
# "django_mongodb_engine": "syncenv.db.mongodb.MongoDumpConnector",
|
|
27
|
+
# "djongo": "syncenv.db.mongodb.MongoDumpConnector",
|
|
28
|
+
# "django.contrib.gis.db.backends.postgis": "syncenv.db.postgresql.PgDumpGisConnector",
|
|
29
|
+
# "django.contrib.gis.db.backends.mysql": "syncenv.db.mysql.MysqlDumpConnector",
|
|
30
|
+
# "django.contrib.gis.db.backends.oracle": None,
|
|
31
|
+
# "django.contrib.gis.db.backends.spatialite": "syncenv.db.sqlite.SqliteConnector",
|
|
32
|
+
# "django_prometheus.db.backends.postgresql": "syncenv.db.postgresql.PgDumpBinaryConnector",
|
|
33
|
+
# "django_prometheus.db.backends.sqlite3": "syncenv.db.sqlite.SqliteConnector",
|
|
34
|
+
# "django_prometheus.db.backends.mysql": "syncenv.db.mysql.MysqlDumpConnector",
|
|
35
|
+
# "django_prometheus.db.backends.postgis": "syncenv.db.postgresql.PgDumpGisConnector",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if settings.CUSTOM_CONNECTOR_MAPPING:
|
|
39
|
+
CONNECTOR_MAPPING.update(settings.CUSTOM_CONNECTOR_MAPPING)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_connector(database_name=None):
|
|
43
|
+
"""
|
|
44
|
+
Get a connector from its database key in settings.
|
|
45
|
+
"""
|
|
46
|
+
from django.db import DEFAULT_DB_ALIAS, connections
|
|
47
|
+
|
|
48
|
+
# Get DB
|
|
49
|
+
database_name = database_name or DEFAULT_DB_ALIAS
|
|
50
|
+
connection = connections[database_name]
|
|
51
|
+
engine = connection.settings_dict["ENGINE"]
|
|
52
|
+
connector_settings = settings.CONNECTORS.get(database_name, {})
|
|
53
|
+
connector_path = connector_settings.get("CONNECTOR", CONNECTOR_MAPPING[engine])
|
|
54
|
+
connector_module_path = ".".join(connector_path.split(".")[:-1])
|
|
55
|
+
print("DEBUG connector_module_path: ", connector_module_path)
|
|
56
|
+
module = import_module(connector_module_path)
|
|
57
|
+
connector_name = connector_path.split(".")[-1]
|
|
58
|
+
connector = getattr(module, connector_name)
|
|
59
|
+
return connector(database_name, **connector_settings)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BaseDBConnector:
|
|
63
|
+
"""
|
|
64
|
+
Base class for create database connector. This kind of object creates
|
|
65
|
+
interaction with database and allow backup and restore operations.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
extension = "dump"
|
|
69
|
+
exclude = []
|
|
70
|
+
|
|
71
|
+
def __init__(self, database_name=None, **kwargs):
|
|
72
|
+
from django.db import DEFAULT_DB_ALIAS, connections
|
|
73
|
+
|
|
74
|
+
self.database_name = database_name or DEFAULT_DB_ALIAS
|
|
75
|
+
self.connection = connections[self.database_name]
|
|
76
|
+
for attr, value in kwargs.items():
|
|
77
|
+
setattr(self, attr.lower(), value)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def settings(self):
|
|
81
|
+
"""Mix of database and connector settings."""
|
|
82
|
+
if not hasattr(self, "_settings"):
|
|
83
|
+
sett = self.connection.settings_dict.copy()
|
|
84
|
+
sett.update(settings.CONNECTORS.get(self.database_name, {}))
|
|
85
|
+
self._settings = sett
|
|
86
|
+
return self._settings
|
|
87
|
+
|
|
88
|
+
def generate_filename(self, environment, server_name=None, database_name=None):
|
|
89
|
+
return utils.filename_generate(
|
|
90
|
+
self.extension,
|
|
91
|
+
database_name=database_name,
|
|
92
|
+
environment=environment
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def create_dump(self):
|
|
96
|
+
return self._create_dump()
|
|
97
|
+
|
|
98
|
+
def _create_dump(self):
|
|
99
|
+
"""
|
|
100
|
+
Override this method to define dump creation.
|
|
101
|
+
"""
|
|
102
|
+
raise NotImplementedError("_create_dump not implemented")
|
|
103
|
+
|
|
104
|
+
def restore_dump(self, dump):
|
|
105
|
+
"""
|
|
106
|
+
:param dump: Dump file
|
|
107
|
+
:type dump: file
|
|
108
|
+
"""
|
|
109
|
+
return self._restore_dump(dump)
|
|
110
|
+
|
|
111
|
+
def _restore_dump(self, dump):
|
|
112
|
+
"""
|
|
113
|
+
Override this method to define dump creation.
|
|
114
|
+
:param dump: Dump file
|
|
115
|
+
:type dump: file
|
|
116
|
+
"""
|
|
117
|
+
raise NotImplementedError("_restore_dump not implemented")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BaseCommandDBConnector(BaseDBConnector):
|
|
121
|
+
"""
|
|
122
|
+
Base class for create database connector based on command line tools.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
dump_prefix = ""
|
|
126
|
+
dump_suffix = ""
|
|
127
|
+
restore_prefix = ""
|
|
128
|
+
restore_suffix = ""
|
|
129
|
+
|
|
130
|
+
use_parent_env = True
|
|
131
|
+
env = {}
|
|
132
|
+
dump_env = {}
|
|
133
|
+
restore_env = {}
|
|
134
|
+
|
|
135
|
+
def run_command(self, command, stdin=None, env=None):
|
|
136
|
+
"""
|
|
137
|
+
Launch a shell command line.
|
|
138
|
+
|
|
139
|
+
:param command: Command line to launch
|
|
140
|
+
:type command: str
|
|
141
|
+
:param stdin: Standard input of command
|
|
142
|
+
:type stdin: file
|
|
143
|
+
:param env: Environment variable used in command
|
|
144
|
+
:type env: dict
|
|
145
|
+
:return: Standard output of command
|
|
146
|
+
:rtype: file
|
|
147
|
+
"""
|
|
148
|
+
logger.debug(command)
|
|
149
|
+
cmd = shlex.split(command)
|
|
150
|
+
stdout = SpooledTemporaryFile(
|
|
151
|
+
max_size=settings.TMP_FILE_MAX_SIZE, dir=settings.TMP_DIR
|
|
152
|
+
)
|
|
153
|
+
stderr = SpooledTemporaryFile(
|
|
154
|
+
max_size=settings.TMP_FILE_MAX_SIZE, dir=settings.TMP_DIR
|
|
155
|
+
)
|
|
156
|
+
full_env = os.environ.copy() if self.use_parent_env else {}
|
|
157
|
+
full_env.update(self.env)
|
|
158
|
+
full_env.update(env or {})
|
|
159
|
+
try:
|
|
160
|
+
if isinstance(stdin, File):
|
|
161
|
+
process = Popen(
|
|
162
|
+
cmd,
|
|
163
|
+
stdin=stdin.open("rb"),
|
|
164
|
+
stdout=stdout,
|
|
165
|
+
stderr=stderr,
|
|
166
|
+
env=full_env,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
process = Popen(
|
|
170
|
+
cmd, stdin=stdin, stdout=stdout, stderr=stderr, env=full_env
|
|
171
|
+
)
|
|
172
|
+
process.wait()
|
|
173
|
+
if process.poll():
|
|
174
|
+
stderr.seek(0)
|
|
175
|
+
raise exceptions.CommandConnectorError(
|
|
176
|
+
"Error running: {}\n{}".format(
|
|
177
|
+
command, stderr.read().decode("utf-8")
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
stdout.seek(0)
|
|
181
|
+
stderr.seek(0)
|
|
182
|
+
return stdout, stderr
|
|
183
|
+
except OSError as err:
|
|
184
|
+
raise exceptions.CommandConnectorError(
|
|
185
|
+
f"Error running: {command}\n{str(err)}"
|
|
186
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Exceptions for database connectors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConnectorError(Exception):
|
|
5
|
+
"""Base connector error"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DumpError(ConnectorError):
|
|
9
|
+
"""Error on dump"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RestoreError(ConnectorError):
|
|
13
|
+
"""Error on restore"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandConnectorError(ConnectorError):
|
|
17
|
+
"""Failing command"""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from syncenv import utils
|
|
2
|
+
|
|
3
|
+
from .base import BaseCommandDBConnector
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MongoDumpConnector(BaseCommandDBConnector):
|
|
7
|
+
"""
|
|
8
|
+
MongoDB connector, creates dump with ``mongodump`` and restore with
|
|
9
|
+
``mongorestore``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
dump_cmd = "mongodump"
|
|
13
|
+
restore_cmd = "mongorestore"
|
|
14
|
+
object_check = True
|
|
15
|
+
drop = True
|
|
16
|
+
|
|
17
|
+
def _create_dump(self):
|
|
18
|
+
cmd = f"{self.dump_cmd} --db {self.settings['NAME']}"
|
|
19
|
+
host = self.settings.get("HOST") or "localhost"
|
|
20
|
+
port = self.settings.get("PORT") or 27017
|
|
21
|
+
cmd += f" --host {host}:{port}"
|
|
22
|
+
if self.settings.get("USER"):
|
|
23
|
+
cmd += f" --username {self.settings['USER']}"
|
|
24
|
+
if self.settings.get("PASSWORD"):
|
|
25
|
+
cmd += f" --password {utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
|
|
26
|
+
|
|
27
|
+
if self.settings.get("AUTH_SOURCE"):
|
|
28
|
+
cmd += f" --authenticationDatabase {self.settings['AUTH_SOURCE']}"
|
|
29
|
+
for collection in self.exclude:
|
|
30
|
+
cmd += f" --excludeCollection {collection}"
|
|
31
|
+
cmd += " --archive"
|
|
32
|
+
cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
|
|
33
|
+
stdout, stderr = self.run_command(cmd, env=self.dump_env)
|
|
34
|
+
return stdout
|
|
35
|
+
|
|
36
|
+
def _restore_dump(self, dump):
|
|
37
|
+
cmd = self.restore_cmd
|
|
38
|
+
host = self.settings.get("HOST") or "localhost"
|
|
39
|
+
port = self.settings.get("PORT") or 27017
|
|
40
|
+
cmd += f" --host {host}:{port}"
|
|
41
|
+
if self.settings.get("USER"):
|
|
42
|
+
cmd += f" --username {self.settings['USER']}"
|
|
43
|
+
if self.settings.get("PASSWORD"):
|
|
44
|
+
cmd += f" --password {utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
|
|
45
|
+
|
|
46
|
+
if self.settings.get("AUTH_SOURCE"):
|
|
47
|
+
cmd += f" --authenticationDatabase {self.settings['AUTH_SOURCE']}"
|
|
48
|
+
if self.object_check:
|
|
49
|
+
cmd += " --objcheck"
|
|
50
|
+
if self.drop:
|
|
51
|
+
cmd += " --drop"
|
|
52
|
+
cmd += " --archive"
|
|
53
|
+
cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
|
|
54
|
+
return self.run_command(cmd, stdin=dump, env=self.restore_env)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from syncenv import utils
|
|
2
|
+
|
|
3
|
+
from .base import BaseCommandDBConnector
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MysqlDumpConnector(BaseCommandDBConnector):
|
|
7
|
+
"""
|
|
8
|
+
MySQL connector, creates dump with ``mysqldump`` and restore with
|
|
9
|
+
``mysql``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
dump_cmd = "mysqldump"
|
|
13
|
+
restore_cmd = "mysql"
|
|
14
|
+
|
|
15
|
+
def _create_dump(self):
|
|
16
|
+
cmd = f"{self.dump_cmd} {self.settings['NAME']} --quick"
|
|
17
|
+
if self.settings.get("HOST"):
|
|
18
|
+
cmd += f" --host={self.settings['HOST']}"
|
|
19
|
+
if self.settings.get("PORT"):
|
|
20
|
+
cmd += f" --port={self.settings['PORT']}"
|
|
21
|
+
if self.settings.get("USER"):
|
|
22
|
+
cmd += f" --user={self.settings['USER']}"
|
|
23
|
+
if self.settings.get("PASSWORD"):
|
|
24
|
+
cmd += f" --password={utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
|
|
25
|
+
|
|
26
|
+
for table in self.exclude:
|
|
27
|
+
cmd += f" --ignore-table={self.settings['NAME']}.{table}"
|
|
28
|
+
cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
|
|
29
|
+
stdout, stderr = self.run_command(cmd, env=self.dump_env)
|
|
30
|
+
return stdout
|
|
31
|
+
|
|
32
|
+
def _restore_dump(self, dump):
|
|
33
|
+
cmd = f"{self.restore_cmd} {self.settings['NAME']}"
|
|
34
|
+
if self.settings.get("HOST"):
|
|
35
|
+
cmd += f" --host={self.settings['HOST']}"
|
|
36
|
+
if self.settings.get("PORT"):
|
|
37
|
+
cmd += f" --port={self.settings['PORT']}"
|
|
38
|
+
if self.settings.get("USER"):
|
|
39
|
+
cmd += f" --user={self.settings['USER']}"
|
|
40
|
+
if self.settings.get("PASSWORD"):
|
|
41
|
+
cmd += f" --password={utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
|
|
42
|
+
|
|
43
|
+
cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
|
|
44
|
+
stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
|
|
45
|
+
return stdout, stderr
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from urllib.parse import quote
|
|
3
|
+
|
|
4
|
+
from .base import BaseCommandDBConnector
|
|
5
|
+
from .exceptions import DumpError
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("sync_env")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_postgres_uri(self):
|
|
11
|
+
host = self.settings.get("HOST")
|
|
12
|
+
if not host:
|
|
13
|
+
raise DumpError("A host name is required")
|
|
14
|
+
|
|
15
|
+
dbname = self.settings.get("NAME") or ""
|
|
16
|
+
user = quote(self.settings.get("USER") or "")
|
|
17
|
+
password = self.settings.get("PASSWORD") or ""
|
|
18
|
+
password = f":{quote(password)}" if password else ""
|
|
19
|
+
if not user:
|
|
20
|
+
password = ""
|
|
21
|
+
else:
|
|
22
|
+
host = "@" + host
|
|
23
|
+
|
|
24
|
+
port = ":{}".format(self.settings.get("PORT")) if self.settings.get("PORT") else ""
|
|
25
|
+
dbname = f"--dbname=postgresql://{user}{password}{host}{port}/{dbname}"
|
|
26
|
+
return dbname
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PgDumpConnector(BaseCommandDBConnector):
|
|
30
|
+
"""
|
|
31
|
+
PostgreSQL connector, it uses pg_dump`` to create an SQL text file
|
|
32
|
+
and ``psql`` for restore it.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
extension = "psql"
|
|
36
|
+
dump_cmd = "pg_dump"
|
|
37
|
+
restore_cmd = "psql"
|
|
38
|
+
single_transaction = True
|
|
39
|
+
drop = True
|
|
40
|
+
|
|
41
|
+
def _create_dump(self):
|
|
42
|
+
cmd = f"{self.dump_cmd} "
|
|
43
|
+
cmd = cmd + create_postgres_uri(self)
|
|
44
|
+
|
|
45
|
+
for table in self.exclude:
|
|
46
|
+
cmd += f" --exclude-table-data={table}"
|
|
47
|
+
if self.drop:
|
|
48
|
+
cmd += " --clean"
|
|
49
|
+
|
|
50
|
+
cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
|
|
51
|
+
stdout, stderr = self.run_command(cmd, env=self.dump_env)
|
|
52
|
+
return stdout
|
|
53
|
+
|
|
54
|
+
def _restore_dump(self, dump):
|
|
55
|
+
cmd = f"{self.restore_cmd} "
|
|
56
|
+
cmd = cmd + create_postgres_uri(self)
|
|
57
|
+
|
|
58
|
+
# without this, psql terminates with an exit value of 0 regardless of errors
|
|
59
|
+
cmd += " --set ON_ERROR_STOP=on"
|
|
60
|
+
if self.single_transaction:
|
|
61
|
+
cmd += " --single-transaction"
|
|
62
|
+
cmd += " {}".format(self.settings["NAME"])
|
|
63
|
+
cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
|
|
64
|
+
stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
|
|
65
|
+
return stdout, stderr
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PgDumpGisConnector(PgDumpConnector):
|
|
69
|
+
"""
|
|
70
|
+
PostgreGIS connector, same than :class:`PgDumpGisConnector` but enable
|
|
71
|
+
postgis if not made.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
psql_cmd = "psql"
|
|
75
|
+
|
|
76
|
+
def _enable_postgis(self):
|
|
77
|
+
cmd = f'{self.psql_cmd} -c "CREATE EXTENSION IF NOT EXISTS postgis;"'
|
|
78
|
+
cmd += " --username={}".format(self.settings["ADMIN_USER"])
|
|
79
|
+
cmd += " --no-password"
|
|
80
|
+
if self.settings.get("HOST"):
|
|
81
|
+
cmd += " --host={}".format(self.settings["HOST"])
|
|
82
|
+
if self.settings.get("PORT"):
|
|
83
|
+
cmd += " --port={}".format(self.settings["PORT"])
|
|
84
|
+
return self.run_command(cmd)
|
|
85
|
+
|
|
86
|
+
def _restore_dump(self, dump):
|
|
87
|
+
if self.settings.get("ADMIN_USER"):
|
|
88
|
+
self._enable_postgis()
|
|
89
|
+
return super()._restore_dump(dump)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PgDumpBinaryConnector(PgDumpConnector):
|
|
93
|
+
"""
|
|
94
|
+
PostgreSQL connector, it uses pg_dump`` to create an SQL text file
|
|
95
|
+
and ``pg_restore`` for restore it.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
extension = "psql.bin"
|
|
99
|
+
dump_cmd = "pg_dump"
|
|
100
|
+
restore_cmd = "pg_restore"
|
|
101
|
+
single_transaction = True
|
|
102
|
+
drop = True
|
|
103
|
+
|
|
104
|
+
def _create_dump(self):
|
|
105
|
+
cmd = f"{self.dump_cmd} "
|
|
106
|
+
cmd = cmd + create_postgres_uri(self)
|
|
107
|
+
|
|
108
|
+
cmd += " --format=custom"
|
|
109
|
+
for table in self.exclude:
|
|
110
|
+
cmd += f" --exclude-table-data={table}"
|
|
111
|
+
cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
|
|
112
|
+
logger.info(cmd)
|
|
113
|
+
stdout, stderr = self.run_command(cmd, env=self.dump_env)
|
|
114
|
+
return stdout
|
|
115
|
+
|
|
116
|
+
def _restore_dump(self, dump):
|
|
117
|
+
dbname = create_postgres_uri(self)
|
|
118
|
+
cmd = f"{self.restore_cmd} {dbname}"
|
|
119
|
+
|
|
120
|
+
if self.single_transaction:
|
|
121
|
+
cmd += " --single-transaction"
|
|
122
|
+
if self.drop:
|
|
123
|
+
cmd += " --clean"
|
|
124
|
+
cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
|
|
125
|
+
logger.info(cmd)
|
|
126
|
+
stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
|
|
127
|
+
return stdout, stderr
|