diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2014-05-08 11:41:25 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2014-05-08 11:41:25 (GMT) |
commit | 70c1b1a26a610c082a49bd5fb29e6dca2bf47943 (patch) | |
tree | 935412a227a5bcd63580010a65a15e5dcc5d3050 | |
parent | 5ebeca1a965aa4028fde7a73c7baffbdba705ef5 (diff) |
Start local node router for admin needs
-rw-r--r-- | TODO | 1 | ||||
-rwxr-xr-x | misc/aslo-sync | 13 | ||||
-rwxr-xr-x | sugar-network-node | 93 | ||||
-rw-r--r-- | sugar_network/node/auth.py | 23 | ||||
-rw-r--r-- | sugar_network/node/routes.py | 9 | ||||
-rw-r--r-- | sugar_network/node/slave.py | 4 | ||||
-rw-r--r-- | sugar_network/toolkit/__init__.py | 8 | ||||
-rw-r--r-- | sugar_network/toolkit/application.py | 18 | ||||
-rw-r--r-- | sugar_network/toolkit/coroutine.py | 26 | ||||
-rw-r--r-- | sugar_network/toolkit/http.py | 3 | ||||
-rw-r--r-- | sugar_network/toolkit/router.py | 3 | ||||
-rw-r--r-- | tests/__init__.py | 12 | ||||
-rwxr-xr-x | tests/units/node/node_routes.py | 25 | ||||
-rwxr-xr-x | tests/units/node/slave.py | 32 |
14 files changed, 195 insertions, 75 deletions
@@ -2,7 +2,6 @@ - do not return Context.releases while non-slave sync - deliver spawn events only to local subscribers - switch auth from WWW-AUTHENTICATE to mutual authentication over the HTTPS -- restrict ACL.LOCAL routes only to localhost clients - pull node changes periodically for checked-in contexts - refount blobs on node and client sides to delete orphaned ones - secure node-to-node sync diff --git a/misc/aslo-sync b/misc/aslo-sync index 141883e..53aa7a3 100755 --- a/misc/aslo-sync +++ b/misc/aslo-sync @@ -39,7 +39,7 @@ from sugar_network.toolkit import licenses, application, Option DOWNLOAD_URL = 'http://download.sugarlabs.org/activities' ASLO_AUTHOR = {'d26cef70447160f31a7497cc0320f23a4e383cc3': { - 'order': 0, 'role': 1, 'name': 'Activity Library', + 'role': 1, 'name': 'Activity Library', }} ACTIVITIES_PATH = '/upload/activities' SUGAR_GUID = 'sugar' @@ -366,7 +366,7 @@ class Application(application.Application): 'message': self.get_i18n_field(content), 'vote': vote, 'author': {nickname: { - 'order': 0, 'role': 3, 'name': fullname, + 'role': 3, 'name': fullname, }}, }) @@ -402,7 +402,7 @@ class Application(application.Application): fullname = nickname updates[guid] = { 'author': {nickname: { - 'order': 0, 'role': 3, 'name': fullname, + 'role': 3, 'name': fullname, }}, 'value': self.get_i18n_field(content), 'ctime': int(time.mktime(modified.timetuple())), @@ -539,7 +539,7 @@ class Application(application.Application): updates[version_id] = { 'author': { nickname: { - 'order': 0, 'role': 3, 'name': fullname, + 'role': 3, 'name': fullname, }, }, 'value': release, @@ -612,7 +612,7 @@ class Application(application.Application): tags.add(row[0]) authors = {} - for order, (role, email, nickname, fullname) in enumerate(self.sqlexec( + for role, email, nickname, fullname in self.sqlexec( """ SELECT addons_users.role, @@ -626,14 +626,13 @@ class Application(application.Application): addons_users.addon_id=%s ORDER BY position - """ % addon_id)): + """ % addon_id): if not nickname: nickname = email.split('@')[0] fullname = fullname.strip() if not fullname: fullname = nickname authors[nickname] = { - 'order': order, 'role': 3 if role == 5 else 1, 'name': fullname, } diff --git a/sugar-network-node b/sugar-network-node index 0b997a0..fea0d01 100755 --- a/sugar-network-node +++ b/sugar-network-node @@ -19,35 +19,61 @@ import os import locale import gettext import logging -from os.path import exists, join +from os.path import exists, join, isabs from sugar_network.toolkit import coroutine coroutine.inject() from sugar_network import db, node, toolkit -from sugar_network.node.auth import SugarAuth +from sugar_network.node.auth import SugarAuth, RootAuth from sugar_network.node import obs, master, slave, model from sugar_network.toolkit.http import Connection from sugar_network.toolkit.router import Router from sugar_network.toolkit import application, Option, enforce +backdoor = Option( + 'path to a UNIX socket to serve administrative API requests; ' + 'the entry point is not authenticated and assumes root privileges, ' + 'thus, make sure that --backdoor path is accessible only by admins; ' + 'not absolute path will be prefixed by the --rundir', + default='backdoor', name='backdoor') + +http_logdir = Option( + 'path to a directory to log HTTP requests; ' + 'not absolute path will be prefixed by the --logdir', + name='http-logdir') + gettext.textdomain('sugar-network') class Application(application.Daemon): jobs = coroutine.Pool() - node = None + servers = [] - def ensure_run(self): - if toolkit.cachedir.value and not exists(toolkit.cachedir.value): - os.makedirs(toolkit.cachedir.value) + def prolog(self): if not exists(node.data_root.value): os.makedirs(node.data_root.value) enforce(os.access(node.data_root.value, os.W_OK), 'No write access to %r directory', node.data_root.value) + for opt, dirname in [ + (toolkit.cachedir, 'cache'), + (application.logdir, 'log'), + (application.rundir, 'run'), + ]: + if not opt.value: + opt.value = join(node.data_root.value, dirname) + if not exists(opt.value): + os.makedirs(opt.value) + + if http_logdir.value and not isabs(http_logdir.value): + http_logdir.value = \ + join(application.logdir.value, http_logdir.value) + if not isabs(backdoor.value): + backdoor.value = join(application.rundir.value, backdoor.value) + def run(self): enforce(node.master_api.value, 'Option --master-api missed') @@ -66,40 +92,65 @@ class Application(application.Daemon): resources = slave.RESOURCES logging.info('Start slave node') volume = model.Volume(node.data_root.value, resources) - node_routes = node_routes_class(node.master_api.value, + routes = node_routes_class(node.master_api.value, volume=volume, auth=SugarAuth(node.data_root.value), find_limit=node.find_limit.value) self.jobs.spawn(volume.populate) + self.jobs.spawn(model.presolve, join(node.data_root.value, 'files')) - logging.info('Listening for requests on %s:%s', + logging.info('Listen requests on %s:%s', node.host.value, node.port.value) - server = coroutine.WSGIServer((node.host.value, node.port.value), - Router(node_routes), **ssl_args) + server = coroutine.WSGIServer( + (node.host.value, node.port.value), Router(routes), + http_log=_open_logfile('access'), **ssl_args) self.jobs.spawn(server.serve_forever) - self.accept() + self.servers.append(server) + + logging.info('Listen admin requests on %s', backdoor.value) + sock = coroutine.listen_unix_socket(backdoor.value, + reuse_address=True, mode=0660) + routes = node_routes_class(node.master_api.value, + volume=volume, auth=RootAuth()) + server = coroutine.WSGIServer(sock, Router(routes), + http_log=_open_logfile('backdoor')) + self.jobs.spawn(server.serve_forever) + self.servers.append(server) + self.accept() try: self.jobs.join() finally: volume.close() + os.unlink(backdoor.value) def shutdown(self): self.jobs.kill() + def reopen_logs(self): + application.Daemon.reopen_logs(self) + if http_logdir.value: + for server in self.servers: + if server.http_log is None: + continue + server.http_log.close() + server.http_log = file(server.http_log.name, 'a+') + @application.command( 'direct synchronization with master node', name='online-sync') def online_sync(self): - self._ensure_instance().post(cmd='online-sync') + enforce(node.mode.value == 'slave', 'Node is not slave') + self._ensure_instance().post(cmd='online_sync') @application.command( 'sneakernet synchronization with other nodes using files ' 'placed to the specified directory', args='PATH', name='offline-sync') def offline_sync(self): + enforce(node.mode.value == 'slave', 'Node is not slave') enforce(self.args, 'PATH was not specified') path = self.args.pop(0) - self._ensure_instance().post(cmd='offline-sync', path=path) + self._ensure_instance().post(cmd='offline_sync', path=path) @application.command( 'resolve/presolve packages for all package-type Contexts', @@ -123,15 +174,27 @@ class Application(application.Daemon): def _ensure_instance(self): enforce(self.check_for_instance(), 'Node is not started') - return Connection('http://localhost:%s' % - node.port.value, trust_env=False) + return Connection('file://' + backdoor.value) + + +def _open_logfile(name): + if not http_logdir.value: + return None + if not exists(http_logdir.value): + os.makedirs(http_logdir.value) + return file(join(http_logdir.value, name + '.log'), 'a+') locale.setlocale(locale.LC_ALL, '') +# New defaults +application.logdir.value = None +application.rundir.value = None + Option.seek('main', application) Option.seek('main', [toolkit.cachedir]) Option.seek('node', node) +Option.seek('node', [http_logdir]) Option.seek('obs', obs) Option.seek('db', db) diff --git a/sugar_network/node/auth.py b/sugar_network/node/auth.py index d14bde6..13a9608 100644 --- a/sugar_network/node/auth.py +++ b/sugar_network/node/auth.py @@ -62,14 +62,25 @@ class Principal(str): @property def cap_create_with_guid(self): - return self._caps & 1 + return self._caps & 2 @cap_create_with_guid.setter def cap_create_with_guid(self, value): if value: - self._caps |= 1 + self._caps |= 2 else: - self._caps ^= 1 + self._caps ^= 2 + + @property + def cap_admin(self): + return self._caps & 4 + + @cap_admin.setter + def cap_admin(self, value): + if value: + self._caps |= 4 + else: + self._caps ^= 4 def __enter__(self): self._backup = self._caps @@ -141,3 +152,9 @@ class SugarAuth(object): # TODO return principal + + +class RootAuth(object): + + def logon(self, request): + return Principal('root', 0xFFFF) diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py index 68beb65..79c4056 100644 --- a/sugar_network/node/routes.py +++ b/sugar_network/node/routes.py @@ -82,6 +82,9 @@ class NodeRoutes(db.Routes, FrontRoutes): allowed = True enforce(allowed, http.Forbidden, 'Authors only') + if op.acl & ACL.ADMIN: + enforce(this.principal.cap_admin, http.Forbidden, 'Admins only') + @postroute def postroute(self, result, exception): request = this.request @@ -172,9 +175,9 @@ class NodeRoutes(db.Routes, FrontRoutes): return solution @route('GET', ['context', None], cmd='resolve', - arguments={'requires': list, 'stability': list}) - def resolve(self): - solution = self.solve() + arguments={'requires': list, 'stability': list, 'assume': list}) + def resolve(self, assume=None): + solution = self.solve(assume) return self.volume.blobs.get(solution[this.request.guid]['blob']) @route('GET', [None, None], cmd='diff') diff --git a/sugar_network/node/slave.py b/sugar_network/node/slave.py index a1195ab..7bf29e2 100644 --- a/sugar_network/node/slave.py +++ b/sugar_network/node/slave.py @@ -57,7 +57,7 @@ class SlaveRoutes(NodeRoutes): self._master_guid = urlsplit(master_api).netloc self._master_api = master_api - @route('POST', cmd='online_sync', acl=ACL.LOCAL, + @route('POST', cmd='online_sync', acl=ACL.AUTH | ACL.ADMIN, arguments={'no_pull': bool}) def online_sync(self, no_pull=False): conn = http.Connection(self._master_api) @@ -70,7 +70,7 @@ class SlaveRoutes(NodeRoutes): headers={'Transfer-Encoding': 'chunked'}) self._import(packets.decode(response.raw)) - @route('POST', cmd='offline_sync', acl=ACL.LOCAL) + @route('POST', cmd='offline_sync', acl=ACL.AUTH | ACL.ADMIN) def offline_sync(self, path): enforce(isabs(path), "Argument 'path' is not an absolute path") diff --git a/sugar_network/toolkit/__init__.py b/sugar_network/toolkit/__init__.py index bf80271..9cad5e0 100644 --- a/sugar_network/toolkit/__init__.py +++ b/sugar_network/toolkit/__init__.py @@ -34,7 +34,7 @@ BUFFER_SIZE = 1024 * 10 cachedir = Option( 'path to a directory to keep cached files; such files ' 'might take considerable number of bytes', - default='/var/cache/sugar-network', name='cachedir') + name='cachedir') _logger = logging.getLogger('toolkit') @@ -390,7 +390,7 @@ def unique_filename(root, filename): class mkdtemp(str): def __new__(cls, *args, **kwargs): - if 'dir' not in kwargs: + if cachedir.value and 'dir' not in kwargs: kwargs['dir'] = cachedir.value if not exists(kwargs['dir']): os.makedirs(kwargs['dir']) @@ -431,7 +431,7 @@ def svg_to_png(data, w, h=None): def TemporaryFile(*args, **kwargs): - if 'dir' not in kwargs: + if cachedir.value and 'dir' not in kwargs: kwargs['dir'] = cachedir.value if not exists(kwargs['dir']): os.makedirs(kwargs['dir']) @@ -441,7 +441,7 @@ def TemporaryFile(*args, **kwargs): class NamedTemporaryFile(object): def __init__(self, *args, **kwargs): - if 'dir' not in kwargs: + if cachedir.value and 'dir' not in kwargs: kwargs['dir'] = cachedir.value if not exists(kwargs['dir']): os.makedirs(kwargs['dir']) diff --git a/sugar_network/toolkit/application.py b/sugar_network/toolkit/application.py index bc6b99c..670a2d3 100644 --- a/sugar_network/toolkit/application.py +++ b/sugar_network/toolkit/application.py @@ -175,8 +175,6 @@ class Application(object): pass def start(self): - self._rundir = abspath(rundir.value or '/var/run/' + self.name) - cmd_name = self.args.pop(0) try: cmd = self._commands.get(cmd_name) @@ -186,10 +184,11 @@ class Application(object): logging.info('Load configuration from %s file(s)', ', '.join(Option.config_files)) - if cmd.options.get('keep_stdout') and not foreground.value: - self._keep_stdout() - self.prolog() + self._rundir = abspath(rundir.value or '/var/run/' + self.name) + + if cmd.options.get('keep_stdout') and not foreground.value: + self.reopen_logs() exit(cmd() or 0) except Exception: printf.exception('%s %s', _('Aborted'), self.name) @@ -214,9 +213,6 @@ class Application(object): pid = None return pid - def ensure_run(self): - pass - def ensure_pidfile(self): if not exists(self._rundir): os.makedirs(self._rundir) @@ -237,7 +233,7 @@ class Application(object): else: print Option.help() - def _keep_stdout(self): + def reopen_logs(self): log_dir = abspath(logdir.value) if not exists(log_dir): os.makedirs(log_dir) @@ -280,7 +276,6 @@ class Daemon(Application): pass time.sleep(.5) - self.ensure_run() if foreground.value: self._launch() else: @@ -333,7 +328,7 @@ class Daemon(Application): def sighup_cb(): logging.info('Reload %s on SIGHUP signal', self.name) - self._keep_stdout() + self.reopen_logs() coroutine.signal(signal.SIGINT, sigterm_cb, signal.SIGINT) coroutine.signal(signal.SIGTERM, sigterm_cb, signal.SIGTERM) @@ -343,7 +338,6 @@ class Daemon(Application): try: self.run() finally: - self.epilog() os.unlink(pid_path) def _daemonize(self): diff --git a/sugar_network/toolkit/coroutine.py b/sugar_network/toolkit/coroutine.py index 1f3d842..c3d4a47 100644 --- a/sugar_network/toolkit/coroutine.py +++ b/sugar_network/toolkit/coroutine.py @@ -80,13 +80,15 @@ def socket(*args, **kwargs): return gevent.socket.socket(*args, **kwargs) -def listen_unix_socket(path, backlog=5): +def listen_unix_socket(path, backlog=5, reuse_address=False, mode=None): # pylint: disable-msg=E1101 from tempfile import NamedTemporaryFile import _socket if exists(path): - raise RuntimeError('The socket address is in use') + if not reuse_address: + raise RuntimeError('The socket address is in use') + os.unlink(path) sock = socket(_socket.AF_UNIX, _socket.SOCK_STREAM) sock.setblocking(0) @@ -94,6 +96,8 @@ def listen_unix_socket(path, backlog=5): with NamedTemporaryFile(dir=dirname(path)) as tmp_path: pass sock.bind(tmp_path.name) + if mode is not None: + os.chmod(tmp_path.name, mode) try: os.rename(tmp_path.name, path) except Exception, error: @@ -141,7 +145,11 @@ def Server(*args, **kwargs): def WSGIServer(*args, **kwargs): import gevent.wsgi - class WSGIHandler(gevent.wsgi.WSGIHandler): + class Server(gevent.wsgi.WSGIServer): + + http_log = kwargs.pop('http_log') if 'http_log' in kwargs else None + + class Handler(gevent.wsgi.WSGIHandler): def log_error(self, msg, *args): if args: @@ -149,14 +157,16 @@ def WSGIServer(*args, **kwargs): _wsgi_logger.error('%s %s', self.format_request(), msg) def log_request(self): - _wsgi_logger.debug('%s', self.format_request()) + logfile = server.http_log + if logfile is not None: + logfile.write(self.format_request()) + logfile.write('\n') kwargs['spawn'] = Pool() if 'handler_class' not in kwargs: - if logging.getLogger().level >= logging.DEBUG: - WSGIHandler.log_request = lambda * args: None - kwargs['handler_class'] = WSGIHandler - return gevent.wsgi.WSGIServer(*args, **kwargs) + kwargs['handler_class'] = Handler + server = Server(*args, **kwargs) + return server def Event(): diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index f3c2ef8..51b34ee 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -119,6 +119,9 @@ class Connection(object): self._session = None self._auth_request = auth_request + if self.url and self.url.startswith('file://'): + self._session_args['trust_env'] = False + def __repr__(self): return '<Connection url=%s>' % self.url diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index ba498d6..7b2186d 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -88,6 +88,7 @@ class ACL(object): AGG_AUTHOR = 1 << 12 LOCAL = 1 << 20 + ADMIN = 1 << 21 NAMES = { CREATE: 'Create', @@ -854,6 +855,8 @@ class _Route(object): 'ACL.AUTHOR requires longer path') enforce(acl ^ ACL.AGG_AUTHOR or len(path) >= 3, 'ACL.AGG_AUTHOR requires longer path') + enforce(acl ^ ACL.ADMIN or acl & ACL.AUTH, + 'ACL.ADMIN without ACL.AUTH') self.op = (method, cmd) self.callback = callback diff --git a/tests/__init__.py b/tests/__init__.py index b890003..c096121 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -279,12 +279,14 @@ class Test(unittest.TestCase): def create_mountset(self, classes=None): self.start_server(classes, root=False) - def start_master(self, classes=None, routes=MasterRoutes): + def start_master(self, classes=None, routes=MasterRoutes, auth=None): if classes is None: classes = master.RESOURCES + if auth is None: + auth = SugarAuth('master') #self.touch(('master/etc/private/node', file(join(root, 'data', NODE_UID)).read())) self.node_volume = NodeVolume('master', classes) - self.node_routes = routes(node.master_api.value, volume=self.node_volume, auth=SugarAuth('master')) + self.node_routes = routes(node.master_api.value, volume=self.node_volume, auth=auth) self.node_router = Router(self.node_routes) self.node = coroutine.WSGIServer(('127.0.0.1', 7777), self.node_router) coroutine.spawn(self.node.serve_forever) @@ -293,15 +295,17 @@ class Test(unittest.TestCase): this.call = self.node_router.call return self.node_volume - def fork_master(self, classes=None, routes=MasterRoutes, cb=None): + def fork_master(self, classes=None, routes=MasterRoutes, cb=None, auth=None): if classes is None: classes = master.RESOURCES + if auth is None: + auth = SugarAuth('master') def _node(): volume = NodeVolume('master', classes) if cb is not None: cb(volume) - anode = coroutine.WSGIServer(('127.0.0.1', 7777), Router(routes(node.master_api.value, volume=volume, auth=SugarAuth('master')))) + anode = coroutine.WSGIServer(('127.0.0.1', 7777), Router(routes(node.master_api.value, volume=volume, auth=auth))) anode.serve_forever() pid = self.fork(_node) diff --git a/tests/units/node/node_routes.py b/tests/units/node/node_routes.py index 9ebd117..bd29a57 100755 --- a/tests/units/node/node_routes.py +++ b/tests/units/node/node_routes.py @@ -99,6 +99,31 @@ class NodeRoutesTest(tests.Test): this.call(method='GET', cmd='probe2', path=['document', guid], environ=auth_env(tests.UID2)) this.call(method='GET', cmd='probe2', path=['document', guid]) + def test_CapAdmin(self): + + class Routes(NodeRoutes): + + def __init__(self, master_api, **kwargs): + NodeRoutes.__init__(self, 'node', **kwargs) + + @route('PROBE', acl=ACL.AUTH | ACL.ADMIN) + def probe(self): + pass + + class Auth(object): + + caps = 0 + + def logon(self, request): + return Principal('user', Auth.caps) + + volume = self.start_master([], Routes, auth=Auth()) + + Auth.caps = 0 + self.assertRaises(http.Forbidden, this.call, method='PROBE') + Auth.caps = 0xFF + this.call(method='PROBE') + def test_ForbiddenCommandsForUserResource(self): volume = self.start_master() diff --git a/tests/units/node/slave.py b/tests/units/node/slave.py index f7c149d..f00f0e1 100755 --- a/tests/units/node/slave.py +++ b/tests/units/node/slave.py @@ -14,7 +14,7 @@ from sugar_network.client import Connection from sugar_network.node import master_api from sugar_network.node.master import MasterRoutes from sugar_network.node.slave import SlaveRoutes -from sugar_network.node.auth import SugarAuth +from sugar_network.node.auth import RootAuth from sugar_network.node.model import User from sugar_network.db.volume import Volume from sugar_network.toolkit.router import Router, File @@ -50,13 +50,13 @@ class SlaveTest(tests.Test): self.Document = Document self.slave_volume = Volume('slave', [User, Document]) - self.slave_routes = SlaveRoutes(master_api.value, volume=self.slave_volume, auth=SugarAuth('slave')) + self.slave_routes = SlaveRoutes(master_api.value, volume=self.slave_volume, auth=RootAuth()) self.slave_server = coroutine.WSGIServer(('127.0.0.1', 8888), Router(self.slave_routes)) coroutine.spawn(self.slave_server.serve_forever) coroutine.dispatch() def test_online_sync_Push(self): - self.fork_master([User, self.Document]) + self.fork_master([User, self.Document], auth=RootAuth()) master = Connection('http://127.0.0.1:7777') slave = Connection('http://127.0.0.1:8888') @@ -74,7 +74,7 @@ class SlaveTest(tests.Test): ], master.get(['document'], reply=['guid', 'message'])['result']) self.assertEqual([[2, None]], json.load(file('slave/var/pull'))) - self.assertEqual([[4, None]], json.load(file('slave/var/push'))) + self.assertEqual([[3, None]], json.load(file('slave/var/push'))) guid3 = slave.post(['document'], {'message': '3', 'title': ''}) slave.post(cmd='online_sync') @@ -85,14 +85,14 @@ class SlaveTest(tests.Test): ], master.get(['document'], reply=['guid', 'message'])['result']) self.assertEqual([[3, None]], json.load(file('slave/var/pull'))) - self.assertEqual([[5, None]], json.load(file('slave/var/push'))) + self.assertEqual([[4, None]], json.load(file('slave/var/push'))) coroutine.sleep(1) slave.put(['document', guid2], {'message': '22'}) slave.post(cmd='online_sync') self.assertEqual('22', master.get(['document', guid2, 'message'])) self.assertEqual([[4, None]], json.load(file('slave/var/pull'))) - self.assertEqual([[6, None]], json.load(file('slave/var/push'))) + self.assertEqual([[5, None]], json.load(file('slave/var/push'))) coroutine.sleep(1) slave.delete(['document', guid1]) @@ -103,7 +103,7 @@ class SlaveTest(tests.Test): ], master.get(['document'], reply=['guid', 'message'])['result']) self.assertEqual([[5, None]], json.load(file('slave/var/pull'))) - self.assertEqual([[7, None]], json.load(file('slave/var/push'))) + self.assertEqual([[6, None]], json.load(file('slave/var/push'))) coroutine.sleep(1) slave.put(['document', guid2], {'message': 'b'}) @@ -117,10 +117,10 @@ class SlaveTest(tests.Test): ]), sorted(master.get(['document'], reply=['guid', 'message'])['result'])) self.assertEqual([[6, None]], json.load(file('slave/var/pull'))) - self.assertEqual([[11, None]], json.load(file('slave/var/push'))) + self.assertEqual([[10, None]], json.load(file('slave/var/push'))) def test_online_sync_Pull(self): - self.fork_master([User, self.Document]) + self.fork_master([User, self.Document], auth=RootAuth()) master = Connection('http://127.0.0.1:7777') slave = Connection('http://127.0.0.1:8888') @@ -138,7 +138,7 @@ class SlaveTest(tests.Test): {'guid': guid2, 'message': '2'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[4, None]], json.load(file('slave/var/pull'))) + self.assertEqual([[3, None]], json.load(file('slave/var/pull'))) self.assertEqual([[2, None]], json.load(file('slave/var/push'))) guid3 = master.post(['document'], {'message': '3', 'title': ''}) @@ -149,14 +149,14 @@ class SlaveTest(tests.Test): {'guid': guid3, 'message': '3'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[5, None]], json.load(file('slave/var/pull'))) + self.assertEqual([[4, None]], json.load(file('slave/var/pull'))) self.assertEqual([[3, None]], json.load(file('slave/var/push'))) coroutine.sleep(1) master.put(['document', guid2], {'message': '22'}) slave.post(cmd='online_sync') self.assertEqual('22', slave.get(['document', guid2, 'message'])) - self.assertEqual([[6, None]], json.load(file('slave/var/pull'))) + self.assertEqual([[5, None]], json.load(file('slave/var/pull'))) self.assertEqual([[4, None]], json.load(file('slave/var/push'))) coroutine.sleep(1) @@ -167,7 +167,7 @@ class SlaveTest(tests.Test): {'guid': guid3, 'message': '3'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[7, None]], json.load(file('slave/var/pull'))) + self.assertEqual([[6, None]], json.load(file('slave/var/pull'))) self.assertEqual([[5, None]], json.load(file('slave/var/push'))) coroutine.sleep(1) @@ -181,11 +181,11 @@ class SlaveTest(tests.Test): {'guid': guid4, 'message': 'd'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[11, None]], json.load(file('slave/var/pull'))) + self.assertEqual([[10, None]], json.load(file('slave/var/pull'))) self.assertEqual([[6, None]], json.load(file('slave/var/push'))) def test_online_sync_PullBlobs(self): - self.fork_master([User, self.Document]) + self.fork_master([User, self.Document], auth=RootAuth()) master = Connection('http://127.0.0.1:7777') slave = Connection('http://127.0.0.1:8888') @@ -203,7 +203,7 @@ class SlaveTest(tests.Test): self.assertEqual('file', file('slave/files/foo/bar').read()) def test_online_sync_PullFromPreviouslyMergedRecord(self): - self.fork_master([User, self.Document]) + self.fork_master([User, self.Document], auth=RootAuth()) master = Connection('http://127.0.0.1:7777') slave = Connection('http://127.0.0.1:8888') |