Ticket #916: ds-backup.py

File ds-backup.py, 5.4 KB (added by hamiltonchua, 15 years ago)
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# Copyright (C) 2007 Ivan Krstić
4# Copyright (C) 2007 Tomeu Vizoso
5# Copyright (C) 2007 One Laptop per Child
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of version 2 of the GNU General Public License (and
9# no other version) as published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
20import os
21import sha
22import urllib2
23from urllib2 import URLError, HTTPError
24import os.path
25import tempfile
26import time
27import glob
28import popen2
29import re
30import gconf
31
32from sugar import env
33from sugar import profile
34
35class BackupError(Exception): pass
36class ProtocolVersionError(BackupError): pass
37class RefusedByServerError(BackupError): pass
38class ServerTooBusyError(BackupError): pass
39class TransferError(BackupError): pass
40class NoPriorBackups(BackupError): pass
41class BulkRestoreUnavailable(BackupError): pass
42
43def check_server_available(server, xo_serial):
44
45    try:
46        ret = urllib2.urlopen(server + '/available/%s' % xo_serial).read()
47        return 200
48    except HTTPError, e:
49        # server is there, did not fullfull req
50        #  expect 404, 403, 503 as e[1]
51        return e.code
52    except URLError, e:
53        # log it?
54        # print e.reason
55        return -1
56
57def rsync_to_xs(from_path, to_path, keyfile, user):
58
59    # add a trailing slash to ensure
60    # that we don't generate a subdir
61    # at the remote end. rsync oddities...
62    if not re.compile('/$').search(from_path):
63        from_path = from_path + '/'
64
65    ssh = '/usr/bin/ssh -F /dev/null -o "PasswordAuthentication no" -o "StrictHostKeyChecking no" -i "%s" -l "%s"' \
66        % (keyfile, user)
67    rsync = "/usr/bin/rsync -z -rlt --partial --delete --timeout=160 -e '%s' '%s' '%s' " % \
68            (ssh, from_path, to_path)
69    print rsync
70    rsync_p = popen2.Popen3(rsync, True)
71
72    # here we could track progress with a
73    # for line in pipe:
74    # (an earlier version had it)
75
76    # wait() returns a DWORD, we want the lower
77    # byte of that.
78    rsync_exit = os.WEXITSTATUS(rsync_p.wait())
79    if rsync_exit != 0:
80        # TODO: retry a couple of times
81        # if rsync_exit is 30 (Timeout in data send/receive)
82        raise TransferError('rsync error code %s, message:'
83                            % rsync_exit, rsync_p.childerr.read())
84
85    # Transfer an empty file marking completion
86    # so the XS can see we are done.
87    # Note: the dest dir on the XS is watched via
88    # inotify - so we avoid creating tempfiles there.
89    tmpfile = tempfile.mkstemp()
90    rsync = ("/usr/bin/rsync -z -rlt --timeout 10 -T /tmp -e '%s' '%s' '%s' "
91             % (ssh, tmpfile[1], 'schoolserver:/var/lib/ds-backup/completion/'+user))
92    rsync_p = popen2.Popen3(rsync, True)
93    rsync_exit = os.WEXITSTATUS(rsync_p.wait())
94    if rsync_exit != 0:
95        # TODO: retry a couple ofd times
96        # if rsync_exit is 30 (Timeout in data send/receive)
97        raise TransferError('rsync error code %s, message:'
98                            % rsync_exit, rsync_p.childerr.read())
99
100def have_ofw_tree():
101    return os.path.exists('/ofw')
102
103def read_ofw(path):
104    path = os.path.join('/ofw', path)
105    if not os.path.exists(path):
106        return None
107    fh = open(path, 'r')
108    data = fh.read().rstrip('\0\n')
109    fh.close()
110    return data
111
112def get_soas_sn():
113  soas_dir = '/home/liveuser/.sugar/soas/'
114  fh = open(os.path.join(soas_dir, 'sn'),'r')
115  data = fh.read().rstrip('\0\n')
116  fh.close()
117  return data
118
119def get_soas_uuid():
120  soas_dir = '/home/liveuser/.sugar/soas/'
121  fh = open(os.path.join(soas_dir, 'uuid'),'r')
122  data = fh.read().rstrip('\0\n')
123  fh.close()
124  return data
125
126# if run directly as script
127if __name__ == "__main__":
128
129    backup_url = 'http://schoolserver/backup/1'
130
131    if have_ofw_tree():
132        sn = read_ofw('mfg-data/SN')
133    else:
134      # HAM : SoaS
135      # sn = 'SHF00000000'
136      # client = gconf.client_get_default()
137      # sn = client.get_string('/desktop/sugar/soas_serial')
138      sn = get_soas_sn()
139
140    ds_path = env.get_profile_path('datastore')
141    pk_path = os.path.join(env.get_profile_path(), 'owner.key')
142
143    # Check backup server availability.
144    # On 503 ("too busy") apply exponential back-off
145    # over 10 attempts. Combined with the staggered sleep
146    # in ds-backup.sh, this should keep thundering herds
147    # under control. We are also holding a flock to prevent
148    # local races.
149    # With range(1,7) we sleep up to 64 minutes.
150    for n in range(1,7):
151        sstatus = check_server_available(backup_url, sn)
152        if (sstatus == 200):
153            # cleared to run
154            rsync_to_xs(ds_path, 'schoolserver:datastore-current', pk_path, sn)
155            # this marks success to the controlling script...
156            os.system('touch ~/.sugar/default/ds-backup-done')
157            exit(0)
158        elif (sstatus == 503):
159            # exponenxtial backoff
160            time.sleep(60 * 2**n)
161        elif (sstatus == -1):
162            # could not connect - XS is not there
163            exit(1)
164        else:
165            # 500, 404, 403, or other unexpected value
166            exit(sstatus)
167