forked from postgrespro/testgres
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.py
More file actions
208 lines (161 loc) · 5.99 KB
/
backup.py
File metadata and controls
208 lines (161 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# coding: utf-8
from six import raise_from
from .enums import XLogMethod
from .consts import \
DATA_DIR, \
TMP_NODE, \
TMP_BACKUP, \
PG_CONF_FILE, \
BACKUP_LOG_FILE
from .exceptions import BackupException
from testgres.operations.os_ops import OsOperations
from .utils import \
get_bin_path2, \
execute_utility2, \
clean_on_error
class NodeBackup(object):
"""
Smart object responsible for backups
"""
@property
def log_file(self):
assert self.os_ops is not None
assert isinstance(self.os_ops, OsOperations)
return self.os_ops.build_path(self.base_dir, BACKUP_LOG_FILE)
def __init__(self,
node,
base_dir=None,
username=None,
xlog_method=XLogMethod.fetch,
options=None):
"""
Create a new backup.
Args:
node: :class:`.PostgresNode` we're going to backup.
base_dir: where should we store it?
username: database user name.
xlog_method: none | fetch | stream (see docs)
"""
assert node.os_ops is not None
assert isinstance(node.os_ops, OsOperations)
if not options:
options = []
self.os_ops = node.os_ops
if not node.status():
raise BackupException('Node must be running')
# Check arguments
if not isinstance(xlog_method, XLogMethod):
try:
xlog_method = XLogMethod(xlog_method)
except ValueError:
msg = 'Invalid xlog_method "{}"'.format(xlog_method)
raise BackupException(msg)
# Set default arguments
username = username or self.os_ops.get_user()
base_dir = base_dir or self.os_ops.mkdtemp(prefix=TMP_BACKUP)
# public
self.original_node = node
self.base_dir = base_dir
self.username = username
# private
self._available = True
data_dir = self.os_ops.build_path(self.base_dir, DATA_DIR)
_params = [
get_bin_path2(self.os_ops, "pg_basebackup"),
"-p", str(node.port),
"-h", node.host,
"-U", username,
"-D", data_dir,
"-X", xlog_method.value
] # yapf: disable
_params += options
execute_utility2(self.os_ops, _params, self.log_file)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.cleanup()
def _prepare_dir(self, destroy):
"""
Provide a data directory for a copy of node.
Args:
destroy: should we convert this backup into a node?
Returns:
Path to data directory.
"""
if not self._available:
raise BackupException('Backup is exhausted')
# Do we want to use this backup several times?
available = not destroy
if available:
assert self.os_ops is not None
assert isinstance(self.os_ops, OsOperations)
dest_base_dir = self.os_ops.mkdtemp(prefix=TMP_NODE)
data1 = self.os_ops.build_path(self.base_dir, DATA_DIR)
data2 = self.os_ops.build_path(dest_base_dir, DATA_DIR)
try:
# Copy backup to new data dir
self.os_ops.copytree(data1, data2)
except Exception as e:
raise_from(BackupException('Failed to copy files'), e)
else:
dest_base_dir = self.base_dir
# Is this backup exhausted?
self._available = available
# Return path to new node
return dest_base_dir
def spawn_primary(self, name=None, destroy=True):
"""
Create a primary node from a backup.
Args:
name: primary's application name.
destroy: should we convert this backup into a node?
Returns:
New instance of :class:`.PostgresNode`.
"""
# Prepare a data directory for this node
base_dir = self._prepare_dir(destroy)
# Build a new PostgresNode
assert self.original_node is not None
if (hasattr(self.original_node, "clone_with_new_name_and_base_dir")):
node = self.original_node.clone_with_new_name_and_base_dir(name=name, base_dir=base_dir)
else:
# For backward compatibility
NodeClass = self.original_node.__class__
node = NodeClass(name=name, base_dir=base_dir, conn_params=self.original_node.os_ops.conn_params)
assert node is not None
assert type(node) is self.original_node.__class__
with clean_on_error(node) as node:
# Set a new port
node.append_conf(filename=PG_CONF_FILE, line='\n')
node.append_conf(filename=PG_CONF_FILE, port=node.port)
return node
def spawn_replica(self, name=None, destroy=True, slot=None):
"""
Create a replica of the original node from a backup.
Args:
name: replica's application name.
slot: create a replication slot with the specified name.
destroy: should we convert this backup into a node?
Returns:
New instance of :class:`.PostgresNode`.
"""
# Build a new PostgresNode
node = self.spawn_primary(name=name, destroy=destroy)
assert node is not None
try:
# Assign it a master and a recovery file (private magic)
node._assign_master(self.original_node)
node._create_recovery_conf(username=self.username, slot=slot)
except: # noqa: E722
# TODO: Pass 'final=True' ?
node.cleanup(release_resources=True)
raise
return node
def cleanup(self):
"""
Remove all files that belong to this backup.
No-op if it's been converted to a PostgresNode (destroy=True).
"""
if self._available:
self._available = False
self.os_ops.rmdirs(self.base_dir, ignore_errors=True)