Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/scripts/send_zfs_filesystem.py
Views: 275
1
#!/usr/bin/env python
2
###############################################################################
3
#
4
# CoCalc: Collaborative Calculation
5
#
6
# Copyright (C) 2016, Sagemath Inc.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#
21
###############################################################################
22
23
# This is a very conservative approach to sending a ZFS filesystem from one machine to another,
24
# with exactly the same name on each side, and not including subfilesystems recursively.
25
# It determines snapshots locally and remotely.
26
# It sends first snapshot, if nothing exists remotely. Then...
27
# Then it does this from local for each successive snapshot a0 a1:
28
#
29
# time zfs send -v -i a0 a1 | ssh remote "zfs recv filesystem"
30
#
31
# If this fails (e.g., due to network/buffer issues), it will retry the
32
# number of times giving by the RETRIES parameter:
33
34
RETRIES = 2
35
36
# Why not just use "zfs send -I"? Because it often doesn't work. Simple as that.
37
# The main reason for existence of this script is due to zfsonlinux being still
38
# somewhat broken.
39
40
import argparse, os, sys, time
41
from subprocess import Popen, PIPE
42
43
44
def cmd(v):
45
t = time.time()
46
print v if isinstance(v, str) else ' '.join(v),
47
sys.stdout.flush()
48
out = Popen(
49
v, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=isinstance(v, str))
50
x = out.stdout.read()
51
y = out.stderr.read()
52
e = out.wait()
53
if e:
54
raise RuntimeError(y)
55
print " (%.2f seconds)" % (time.time() - t)
56
return x
57
58
59
def system(c):
60
for n in range(RETRIES):
61
print c
62
if not os.system(c):
63
return
64
raise RuntimeError('error running "%s" %s times' % (c, RETRIES))
65
66
67
def send(filesystem, remote):
68
if ':' in filesystem:
69
local_filesystem, remote_filesystem = filesystem.split(':')
70
else:
71
local_filesystem = remote_filesystem = filesystem
72
print "sending %s to %s on %s" % (local_filesystem, remote_filesystem,
73
remote)
74
75
# get list of snapshots locally, sorted by time.
76
s = [
77
'zfs', 'list', '-r', '-H', '-t', 'snapshot', '-o', 'name', '-s',
78
'creation'
79
]
80
local_snapshots = cmd(s + [local_filesystem]).splitlines()
81
82
if len(local_snapshots) == 0:
83
#TODO
84
raise RuntimeError("you must have at least one local snapshot of %s" %
85
local_filesystem)
86
87
# get list of snapshots remotely, sorted by time
88
if remote == 'localhost':
89
remote_snapshots = cmd(' '.join(s + [remote_filesystem])).splitlines()
90
else:
91
remote_snapshots = cmd(
92
"ssh %s '%s'" % (remote,
93
' '.join(s + [remote_filesystem]))).splitlines()
94
95
local_snapshot_names = set([x.split('@')[1] for x in local_snapshots])
96
97
if len(remote_snapshots) == 0:
98
# transfer up to first snapshot to remote
99
first = local_snapshots[0]
100
if remote == 'localhost':
101
system('time zfs send -v %s | zfs recv -F %s' %
102
(first, remote_filesystem))
103
else:
104
system('time zfs send -v %s | ssh %s "zfs recv -F %s"' %
105
(first, remote, remote_filesystem))
106
start = first
107
else:
108
# transfer starting with newest snapshot this available locally (destructively killing all older snapshots).
109
i = len(remote_snapshots) - 1
110
while i >= 0 and remote_snapshots[i].split(
111
'@')[1] not in local_snapshot_names:
112
i -= 1
113
if i == -1:
114
start = local_snapshots[0]
115
else:
116
start = remote_snapshots[i]
117
118
i = local_snapshots.index(
119
"%s@%s" % (local_filesystem, start.split('@')[1]))
120
v = range(i + 1, len(local_snapshots))
121
n = 1
122
for j in v:
123
print "(%s/%s) sending %s" % (n, len(v), local_snapshots[j])
124
if remote == 'localhost':
125
system('time zfs send -v -i %s %s | zfs recv -F %s' % (
126
local_snapshots[j - 1], local_snapshots[j], remote_filesystem))
127
else:
128
system('time zfs send -v -i %s %s | ssh %s "zfs recv -F %s"' %
129
(local_snapshots[j - 1], local_snapshots[j], remote,
130
remote_filesystem))
131
n += 1
132
133
134
if __name__ == "__main__":
135
136
parser = argparse.ArgumentParser(description="Send ZFS filesystems")
137
parser.add_argument(
138
"remote",
139
help="remote machine's ip address/hostname (or localhost)",
140
type=str)
141
parser.add_argument(
142
"filesystem",
143
help=
144
"name of filesystem to send or name_local:name_remote to send to a different remote name",
145
type=str,
146
nargs="+")
147
args = parser.parse_args()
148
149
for filesystem in args.filesystem:
150
send(filesystem, args.remote)
151
152