changeset 1:9b139702a8f9

use a real backend architecture with an inteface and vastly simplify unify.py
author k0s <k0scist@gmail.com>
date Sat, 26 Sep 2009 23:36:42 -0400
parents abb358e2434c
children 0d9094bb98b0
files setup.py silvermirror-whitepaper.txt silvermirror/hg.py silvermirror/interface.py silvermirror/unify.py silvermirror/unison.py
diffstat 6 files changed, 173 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/setup.py
+++ b/setup.py
@@ -22,11 +22,14 @@ setup(name='silvermirror',
         'netifaces'
       ],
       dependency_links=[
         'https://svn.openplans.org/svn/standalone/martINI#egg=martINI',
         ],
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
-      silvermirror = silvermirror.unify:unify
+      silvermirror = silvermirror.unify:main
+
+      [silvermirror.reflectors]
+      unison = silvermirror.unison:unison
       """,
       )
--- a/silvermirror-whitepaper.txt
+++ b/silvermirror-whitepaper.txt
@@ -37,16 +37,18 @@ The main section, denoted [::SilverMirro
 
   * directory: base directory for SilverMirror. The SilverMirror  
 configuration is stored in ${directory}/.silvermirror . If omitted,  
 the user's home directory is used.
 
   * ignore: global patterns of files and directories to ignore. Paths  
 matching these patterns will not be versioned.
 
+  * reflector: which back-end to use (unison, hg, etc)
+
 Each section has the following configuration options:
 
   * directory: path of the resource. If a relative path is used, it is  
 joined with the directory setting from the main section. If this  
 setting is not specified, the section name is used as a relative path.
 
   * ignore: paths not to version on a per resource basis. This is in  
 addition to the patterns specified by the ignore setting in the main  
new file mode 100644
--- /dev/null
+++ b/silvermirror/hg.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+"""
+stub for the hg backend of silvermirror
+"""
+
+import os
+from mercurial import commands, hg, ui
+
+def update(host, path):
+    """
+    get changes from host on path
+    """
+    ui = ui.ui()
+    try: 
+        repo = hg.repository(ui, path)
+        command = commands.pull
+    except mercurial.repo.RepoError:
+        repo = hg.repository(ui, 'ssh://%s/%s' % (host, path))
+        command = commands.clone
new file mode 100644
--- /dev/null
+++ b/silvermirror/interface.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+"""
+interface for Reflector backends
+"""
+
+def notimplemented(func):
+    """
+    mark a function as not implemented
+    TODO: calling should raise a NotImplementedError 
+    """
+    func.notimplemented = True
+    return func
+
+class Reflector(object):
+
+    def __init__(self):
+        pass
+    
+    ### API
+
+    @notimplemented
+    def sync(self):
+        """
+        synchronize
+        """
+
+### for testing notimplemented decorator
+
+class Foo(object):
+
+    @notimplemented
+    def bar(self):
+        "stufff"
+
+    @notimplemented
+    def fleem(self):
+        pass
+
+    @notimplemented
+    def foox(self):
+        pass
+
+class Bar(Foo):
+
+    def bar(self):
+        return 1
+
+    @notimplemented
+    def fleem(self):
+        pass
+
+if __name__ == '__main__':
+    foo = Foo()
+    bar = Bar()
+    import pdb; pdb.set_trace()
--- a/silvermirror/unify.py
+++ b/silvermirror/unify.py
@@ -4,16 +4,17 @@ import getpass
 import os
 import pexpect
 import socket
 import subprocess
 import sys
 
 from martini.config import ConfigMunger
 from optparse import OptionParser
+from pkg_resources import iter_entry_points
 from pprint import pprint
 from utils import home
 from utils import ip_addresses
 
 def make_config(filename):
     # XXX needed?
     raise NotImplementedError('Need to specify a config file, like\n~/silvermirror/silvermirror.ini')
 
@@ -47,109 +48,111 @@ def read_config(filename):
         if config[resource].has_key('ignore'):
             ignore += config[resource]['ignore'].split()
         config[resource]['ignore'] = ignore
 
     ###
     config = { 'main': main, 'resources': config }
     return config
 
-def unify(args=sys.argv[1:]):
+def unify(conf, _resources, test=False):
 
     # passwords
     pw = {}
 
+    # XXX needed for now
+    assert conf['main']['basedir'] == home()
+
+    ### determine hosts to sync with
+    hosts = conf['hosts']
+    addresses = ip_addresses().values()
+    hosts = hosts.difference(addresses) # don't sync with self
+    _hosts = []
+    for host in hosts:
+        s = socket.socket()
+        s.settimeout(conf['main']['timeout'])
+        if test:
+            print 'Resolving %s' % host
+        try: 
+            s.connect((host, 22))
+            s.close()
+        except (socket.gaierror, socket.timeout, socket.error):
+            continue
+        _hosts.append(host)
+    hosts = _hosts
+    if test:
+        print 'Hosts:'
+        for host in hosts:
+            print host
+    assert hosts
+
+    if conf['main']['password']:
+        for host in hosts:
+            pw[host] = getpass.getpass('Enter password for %s: ' % host)
+    # TODO: ensure that the hosts are resolvable
+    # XXX: hosts should actually be manageable on a per-resource basis
+
+    ### determine resources to sync
+    cwd = os.path.realpath(os.getcwd())
+    resources = conf['resources']
+    if 'all' not in _resources:
+        if _resources:
+            resources = dict([(key, value) for key, value in resources.items()
+                              if key in _resources])
+        else:
+            for key, value in resources.items():
+                directory = os.path.realpath(value['directory']) + os.sep
+                if (cwd + os.sep).startswith(directory):
+                    resources = { key: value }
+                    break
+    if test:
+        print 'Resources:'
+        pprint(resources)
+
+    ### choose reflector backend
+    reflectors = dict([(i.name, i.load()) for i in iter_entry_points('silvermirror.reflectors')])
+    reflector = reflectors['unison']() # only one right now
+
+    ### sync with hosts
+    os.chdir(conf['main']['basedir'])
+    for resource in resources:
+        for host in hosts:
+            reflector.sync(host, resource, resources[resource]['ignore'], pw, test)
+    os.chdir(cwd)
+
+def main(args=sys.argv[1:]):
+
     ### command line options
     parser = OptionParser()
     parser.add_option('-c', '--config')
     parser.add_option('-H', '--host', dest='hosts',
                       action='append', default=None)
     parser.add_option('--no-password', dest='password',
                       action='store_false', default=True)
     parser.add_option('--test', dest='test', 
                       action='store_true', default=False)
     (options, args) = parser.parse_args()
-    
+
+
     ### configuration
     user_conf = os.path.join(home(), '.silvermirror')
     if options.config:
         assert os.path.exists(options.config)
         conf = read_config(options.config)
     else:
         for i in user_conf, '/etc/silvermirror':
             if os.path.exists(i):
                 conf = read_config(i)
                 break
         else:
             conf = make_config(user_conf)
 
     # XXX needed for now
     assert conf['main']['basedir'] == home()
 
-    ### determine hosts to sync with
-    hosts = set(options.hosts or conf['main']['hosts'])
-    addresses = ip_addresses().values()
-    hosts = hosts.difference(addresses) # don't sync with self
-    _hosts = []
-    for host in hosts:
-        s = socket.socket()
-        s.settimeout(conf['main']['timeout'])
-        if options.test:
-            print 'Resolving %s' % host
-        try: 
-            s.connect((host, 22))
-            s.close()
-        except (socket.gaierror, socket.timeout, socket.error):
-            continue
-        _hosts.append(host)
-    hosts = _hosts
-    if options.test:
-        print 'Hosts:'
-        for host in hosts:
-            print host
-    assert hosts
+    # fix up configuration from command line options
+    conf['hosts'] = set(options.hosts or conf['main']['hosts'])
+    conf['main']['password'] = options.password and conf['main']['password']
 
-    if options.password and conf['main']['password']:
-        for host in hosts:
-            pw[host] = getpass.getpass('Enter password for %s: ' % host)
-    # TODO: ensure that the hosts are resolvable
-    # XXX: hosts should actually be manageable on a per-resource basis
-
-    ### determine resources to sync
-    cwd = os.path.realpath(os.getcwd())
-    resources = conf['resources']
-    _resources = args
-    if 'all' not in _resources:
-        if _resources:
-            resources = dict([(key, value) for key, value in resources.items()
-                              if key in _resources])
-        else:
-            for key, value in resources.items():
-                directory = os.path.realpath(value['directory']) + os.sep
-                if (cwd + os.sep).startswith(directory):
-                    resources = { key: value }
-                    break
-    if options.test:
-        print 'Resources:'
-        pprint(resources)
-
-    ### sync with hosts
-    os.chdir(conf['main']['basedir'])
-    for resource in resources:
-        for host in hosts:
-            command = ['unison', '-auto', '-batch', resource, 'ssh://%s/%s' % (host, resource)]
-
-            # XXX - to refactor?
-            for i in resources[resource]['ignore']:
-                command.extend(('-ignore', "'Name %s'" % i))
-
-            command = ' '.join(command) 
-            print command # XXX debug 
-            if not options.test:
-                child = pexpect.spawn(command, timeout=36000, maxread=1)
-                child.expect('password: ')
-                child.sendline(pw[host])
-                print child.read()
-                #                subprocess.call(command)
-    os.chdir(cwd)
+    unify(conf, args, options.test)
 
 if __name__ == '__main__':
     unify()
new file mode 100644
--- /dev/null
+++ b/silvermirror/unison.py
@@ -0,0 +1,23 @@
+"""
+unison backend for silvermirror
+"""
+
+import pexpect
+
+from interface import Reflector
+
+class unison(Reflector):
+
+    def sync(self, host, resource, ignore=(), password=None, test=False):
+        command = ['unison', '-auto', '-batch', resource, 'ssh://%s/%s' % (host, resource)]
+        for i in ignore:
+            command.extend(('-ignore', "'Name %s'" % i))
+
+        command = ' '.join(command)
+        print command # XXX debug -- should go to logging
+        if not test:
+            child = pexpect.spawn(command, timeout=36000, maxread=1)
+            child.expect('password: ')
+            child.sendline(password[host])
+            print child.read() # XXX -> logging
+