| 1 | diff --git a/generate-cert.sh b/generate-cert.sh |
|---|
| 2 | index 4daf76a..ce28e03 100755 |
|---|
| 3 | --- a/generate-cert.sh |
|---|
| 4 | +++ b/generate-cert.sh |
|---|
| 5 | @@ -5,4 +5,5 @@ if [ ! -x /usr/bin/certtool ] ; then |
|---|
| 6 | fi |
|---|
| 7 | |
|---|
| 8 | certtool --generate-privkey --outfile ca-key.pem |
|---|
| 9 | -certtool --generate-self-signed --load-privkey ca-key.pem --outfile ca-cert.pem |
|---|
| 10 | \ No newline at end of file |
|---|
| 11 | +certtool --generate-self-signed --load-privkey ca-key.pem --outfile ca-cert.pem |
|---|
| 12 | + |
|---|
| 13 | diff --git a/manage.py b/manage.py |
|---|
| 14 | old mode 100644 |
|---|
| 15 | new mode 100755 |
|---|
| 16 | diff --git a/pydra_server/cluster/master.py b/pydra_server/cluster/master.py |
|---|
| 17 | index 47bb2a6..fe5d612 100755 |
|---|
| 18 | --- a/pydra_server/cluster/master.py |
|---|
| 19 | +++ b/pydra_server/cluster/master.py |
|---|
| 20 | @@ -46,6 +46,12 @@ import datetime |
|---|
| 21 | |
|---|
| 22 | from threading import Lock |
|---|
| 23 | |
|---|
| 24 | +# should be executed before any other reactor stuff to prevent from using non |
|---|
| 25 | +# glib2 event loop which we need for dbus |
|---|
| 26 | + |
|---|
| 27 | +from twisted.internet import glib2reactor |
|---|
| 28 | +glib2reactor.install() |
|---|
| 29 | + |
|---|
| 30 | from zope.interface import implements |
|---|
| 31 | from twisted.cred import portal, checkers |
|---|
| 32 | from twisted.spread import pb |
|---|
| 33 | @@ -56,6 +62,8 @@ from twisted.web import server, resource |
|---|
| 34 | from twisted.cred import credentials |
|---|
| 35 | from django.utils import simplejson |
|---|
| 36 | import settings |
|---|
| 37 | +import dbus, avahi |
|---|
| 38 | +from dbus.mainloop.glib import DBusGMainLoop |
|---|
| 39 | |
|---|
| 40 | from pydra_server.models import Node, TaskInstance |
|---|
| 41 | from pydra_server.cluster.constants import * |
|---|
| 42 | @@ -143,6 +151,66 @@ class Master(object): |
|---|
| 43 | |
|---|
| 44 | self.host = 'localhost' |
|---|
| 45 | self.port = 18800 |
|---|
| 46 | + self.autodiscovery() |
|---|
| 47 | + |
|---|
| 48 | + def autodiscovery(self, callback=None): |
|---|
| 49 | + """ |
|---|
| 50 | + set up the dbus loop, and add the callbacks for adding nodes on the fly |
|---|
| 51 | + |
|---|
| 52 | + based on http://avahi.org/wiki/PythonBrowseExample |
|---|
| 53 | + """ |
|---|
| 54 | + def service_resolved(*args): |
|---|
| 55 | + # at this point we have all the info about the node we need |
|---|
| 56 | + |
|---|
| 57 | + #decode TXT record, dbus's array of array's of dbus.Byte |
|---|
| 58 | + txt = "" |
|---|
| 59 | + for c in args[9][0]: |
|---|
| 60 | + if c >= 32 and c < 127: |
|---|
| 61 | + txt += "%c" % c |
|---|
| 62 | + else: |
|---|
| 63 | + txt += "." |
|---|
| 64 | + |
|---|
| 65 | + # make sure we only accept nodes we haven't seen before this is NOT |
|---|
| 66 | + # a security measure (the key verification still takes place), only |
|---|
| 67 | + # intended to prevent multiple hosts from being discovered via |
|---|
| 68 | + # different routes (for example over both ipv6 and ipv4) |
|---|
| 69 | + from md5 import md5 |
|---|
| 70 | + for node in self.nodes.values(): |
|---|
| 71 | + if md5(str(node.pub_key)).hexdigest() == txt: |
|---|
| 72 | + return |
|---|
| 73 | + |
|---|
| 74 | + # add the node (without the restart) |
|---|
| 75 | + node = Node.objects.create(host=args[7], port=args[8]) |
|---|
| 76 | + self.nodes[node.id] = node |
|---|
| 77 | + self.reconnect_nodes() |
|---|
| 78 | + |
|---|
| 79 | + def print_error(*args): |
|---|
| 80 | + logger.info("Couldn't resolve avahi name: %s" % str(args)) |
|---|
| 81 | + |
|---|
| 82 | + def node_found(interface, protocol, name, stype, domain, flags): |
|---|
| 83 | + if flags & avahi.LOOKUP_RESULT_LOCAL: |
|---|
| 84 | + # local service, skip |
|---|
| 85 | + pass |
|---|
| 86 | + |
|---|
| 87 | + server.ResolveService(interface, protocol, name, stype, |
|---|
| 88 | + domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), |
|---|
| 89 | + reply_handler=service_resolved, error_handler=print_error) |
|---|
| 90 | + |
|---|
| 91 | + # initialize dbus stuff needed for discovery |
|---|
| 92 | + loop = DBusGMainLoop() |
|---|
| 93 | + |
|---|
| 94 | + bus = dbus.SystemBus(mainloop=loop) |
|---|
| 95 | + |
|---|
| 96 | + server = dbus.Interface( bus.get_object(avahi.DBUS_NAME, '/'), |
|---|
| 97 | + 'org.freedesktop.Avahi.Server') |
|---|
| 98 | + |
|---|
| 99 | + sbrowser = dbus.Interface(bus.get_object(avahi.DBUS_NAME, |
|---|
| 100 | + server.ServiceBrowserNew(avahi.IF_UNSPEC, |
|---|
| 101 | + avahi.PROTO_UNSPEC, '_pydra._tcp', 'local', dbus.UInt32(0))), |
|---|
| 102 | + avahi.DBUS_INTERFACE_SERVICE_BROWSER) |
|---|
| 103 | + |
|---|
| 104 | + sbrowser.connect_to_signal("ItemNew", node_found) |
|---|
| 105 | + |
|---|
| 106 | |
|---|
| 107 | def get_services(self): |
|---|
| 108 | """ |
|---|
| 109 | diff --git a/pydra_server/cluster/node.py b/pydra_server/cluster/node.py |
|---|
| 110 | index d4a727f..4eb9ce0 100755 |
|---|
| 111 | --- a/pydra_server/cluster/node.py |
|---|
| 112 | +++ b/pydra_server/cluster/node.py |
|---|
| 113 | @@ -46,6 +46,8 @@ from twisted.application import service, internet |
|---|
| 114 | |
|---|
| 115 | import os |
|---|
| 116 | from subprocess import Popen |
|---|
| 117 | +import platform, dbus, avahi |
|---|
| 118 | +from django.utils import simplejson |
|---|
| 119 | from pydra_server.cluster.auth.rsa_auth import load_crypto |
|---|
| 120 | from pydra_server.cluster.auth.master_avatar import MasterAvatar |
|---|
| 121 | |
|---|
| 122 | @@ -55,6 +57,46 @@ import settings |
|---|
| 123 | from pydra_server.logging.logger import init_logging |
|---|
| 124 | logger = init_logging(settings.LOG_FILENAME_NODE) |
|---|
| 125 | |
|---|
| 126 | +class ZeroconfService: |
|---|
| 127 | + """A simple class to publish a network service with zeroconf using |
|---|
| 128 | + avahi. |
|---|
| 129 | + |
|---|
| 130 | + Shamelessly stolen from http://stackp.online.fr/?p=35 |
|---|
| 131 | + """ |
|---|
| 132 | + |
|---|
| 133 | + def __init__(self, name, port, stype="_http._tcp", |
|---|
| 134 | + domain="", host="", text=""): |
|---|
| 135 | + self.name = name |
|---|
| 136 | + self.stype = stype |
|---|
| 137 | + self.domain = domain |
|---|
| 138 | + self.host = host |
|---|
| 139 | + self.port = port |
|---|
| 140 | + self.text = text |
|---|
| 141 | + |
|---|
| 142 | + def publish(self): |
|---|
| 143 | + bus = dbus.SystemBus() |
|---|
| 144 | + server = dbus.Interface( |
|---|
| 145 | + bus.get_object( |
|---|
| 146 | + avahi.DBUS_NAME, |
|---|
| 147 | + avahi.DBUS_PATH_SERVER), |
|---|
| 148 | + avahi.DBUS_INTERFACE_SERVER) |
|---|
| 149 | + |
|---|
| 150 | + g = dbus.Interface( |
|---|
| 151 | + bus.get_object(avahi.DBUS_NAME, |
|---|
| 152 | + server.EntryGroupNew()), |
|---|
| 153 | + avahi.DBUS_INTERFACE_ENTRY_GROUP) |
|---|
| 154 | + |
|---|
| 155 | + g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC,dbus.UInt32(0), |
|---|
| 156 | + self.name, self.stype, self.domain, self.host, |
|---|
| 157 | + dbus.UInt16(self.port), self.text) |
|---|
| 158 | + |
|---|
| 159 | + g.Commit() |
|---|
| 160 | + #import ipdb; ipdb.set_trace() |
|---|
| 161 | + self.group = g |
|---|
| 162 | + |
|---|
| 163 | + def unpublish(self): |
|---|
| 164 | + self.group.Reset() |
|---|
| 165 | + |
|---|
| 166 | |
|---|
| 167 | class NodeServer: |
|---|
| 168 | """ |
|---|
| 169 | @@ -81,6 +123,18 @@ class NodeServer: |
|---|
| 170 | # get information about the server |
|---|
| 171 | self.determine_info() |
|---|
| 172 | |
|---|
| 173 | + |
|---|
| 174 | + # TXT record must be array of array of dbus.Byte, let's encode it |
|---|
| 175 | + dbus_array = [] |
|---|
| 176 | + from md5 import md5 |
|---|
| 177 | + |
|---|
| 178 | + for c in md5(simplejson.dumps(self.pub_key)).hexdigest(): |
|---|
| 179 | + dbus_array.append(dbus.Byte(ord(c))) |
|---|
| 180 | + service = ZeroconfService(name=platform.node(), port=self.port, |
|---|
| 181 | + stype="_pydra._tcp", text=[dbus_array]) |
|---|
| 182 | + service.publish() |
|---|
| 183 | + |
|---|
| 184 | + |
|---|
| 185 | logger.info('Node - starting server on port %s' % self.port) |
|---|
| 186 | |
|---|
| 187 | |
|---|
| 188 | @@ -142,7 +196,6 @@ class NodeServer: |
|---|
| 189 | Exchange public keys with the master. This allows the Master |
|---|
| 190 | to authenticate in the future using the keypair handshake |
|---|
| 191 | """ |
|---|
| 192 | - from django.utils import simplejson |
|---|
| 193 | from Crypto.PublicKey import RSA |
|---|
| 194 | from twisted.conch.ssh.keys import Key |
|---|
| 195 | import math |
|---|