diff --git a/CHANGELOG.md b/CHANGELOG.md index d4447b50fc9d566e1a024a1a69be0b793b8ba950..34a28d9dadedd2c344714f5b3f3162d7f85dd9c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ This file logs the changes that are actually interesting to users (new features, changed functionality, fixed bugs). +## Version 2.0.7 (in development) + +- Use UPnP/SSDP to automatically find Manager when manager_url is empty. + This is now also the new default, since we can't provide a sane default URL anyway. + + ## Version 2.0.6 (released 2017-06-23) - Fixed incompatibility with attrs version 17.1+. diff --git a/README.md b/README.md index 28353875e05cc3d1799f76cf6dbc4b0735544549..83451c7def02e67cf0cead6da63ca147d5aaf153 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ The configuration files should be in INI format, as specified by the All configuration keys should be placed in the `[flamenco-worker]` section of the config files. At least take a look at: -- `manager_url`: Flamenco Manager URL. +- `manager_url`: Flamenco Manager URL. Leave blank to auto-discover Flamenco Manager + on your network using UPnP/SSDP. - `task_types`: Space-separated list of task types this worker may execute. - `task_update_queue_db`: filename of the SQLite3 database holding the queue of task updates to be sent to the Master. diff --git a/flamenco-worker.cfg b/flamenco-worker.cfg index 0857b8dd6b4475af251fa32e409ce1958e052d32..ab4f60ff9255bbb46f66094dbe76f61be79df554 100644 --- a/flamenco-worker.cfg +++ b/flamenco-worker.cfg @@ -1,5 +1,8 @@ [flamenco-worker] -manager_url = http://localhost:8083/ + +# The URL of the Flamenco Manager. Leave empty for auto-discovery via UPnP/SSDP. +manager_url = + task_types = sleep blender-render file-management task_update_queue_db = flamenco-worker.db diff --git a/flamenco_worker/cli.py b/flamenco_worker/cli.py index c8805d4fe7f8c2791d67baf3cfa375c325808dd8..5ab4e4871e0a2bbbed4192c8e16335189013df29 100644 --- a/flamenco_worker/cli.py +++ b/flamenco_worker/cli.py @@ -35,6 +35,19 @@ def main(): confparser.erase('worker_id') confparser.erase('worker_secret') + # Find the Manager using UPnP/SSDP if we have no manager_url. + if not confparser.value('manager_url'): + from . import ssdp_discover + + try: + manager_url = ssdp_discover.find_flamenco_manager() + except ssdp_discover.DiscoveryFailed: + log.fatal('Unable to find Flamenco Manager via UPnP/SSDP.') + raise SystemExit(1) + + log.info('Found Flamenco Manager at %s', manager_url) + confparser.setvalue('manager_url', manager_url) + # Patch AsyncIO from . import patch_asyncio patch_asyncio.patch_asyncio() diff --git a/flamenco_worker/config.py b/flamenco_worker/config.py index 411db829461e5d7ec6ca671fc35791be29108cfb..7ea44328875355056fc6893b23230bc6ccef6a52 100644 --- a/flamenco_worker/config.py +++ b/flamenco_worker/config.py @@ -14,7 +14,7 @@ CONFIG_SECTION = 'flamenco-worker' DEFAULT_CONFIG = { 'flamenco-worker': collections.OrderedDict([ - ('manager_url', 'http://flamenco-manager/'), + ('manager_url', ''), ('task_types', 'unknown sleep blender-render'), ('task_update_queue_db', 'flamenco-worker.db'), ('may_i_run_interval_seconds', '5'), @@ -39,6 +39,9 @@ class ConfigParser(configparser.ConfigParser): def value(self, key, valtype: type=str): return valtype(self.get(CONFIG_SECTION, key)) + def setvalue(self, key, value): + self.set(CONFIG_SECTION, key, value) + def interval_secs(self, key) -> datetime.timedelta: """Returns the configuration value as timedelta.""" diff --git a/flamenco_worker/ssdp_discover.py b/flamenco_worker/ssdp_discover.py new file mode 100644 index 0000000000000000000000000000000000000000..865003eaed20a961c23f8bc94a4a963bb6732d95 --- /dev/null +++ b/flamenco_worker/ssdp_discover.py @@ -0,0 +1,93 @@ +import logging +import socket +from http.client import HTTPResponse + +DISCOVERY_MSG = (b'M-SEARCH * HTTP/1.1\r\n' + + b'ST: urn:flamenco:manager:0\r\n' + + b'MX: 3\r\n' + + b'MAN: "ssdp:discover"\r\n' + + b'HOST: 239.255.255.250:1900\r\n\r\n') + +# We use site-local multicast, both in IPv6 and IPv4. +DESTINATIONS = { + socket.AF_INET6: 'FF05::C', + socket.AF_INET: '239.255.255.250', +} + +log = logging.getLogger(__name__) + + +class DiscoveryFailed(Exception): + """Raised when we cannot find a Manager through SSDP.""" + + +class Response(HTTPResponse): + # noinspection PyMissingConstructor + def __init__(self, payload: bytes): + from io import BytesIO + + self.fp = BytesIO(payload) + self.debuglevel = 0 + self.strict = 0 + self.headers = self.msg = None + self._method = None + self.begin() + + +def interface_addresses(): + for dest in ('0.0.0.0', '::'): + for family, _, _, _, sockaddr in socket.getaddrinfo(dest, None): + yield family, sockaddr[0] + + +def unique(addresses): + seen = set() + for family_addr in addresses: + if family_addr in seen: + continue + + seen.add(family_addr) + yield family_addr + + +def find_flamenco_manager(timeout=1, retries=5): + log.info('Finding Flamenco Manager through UPnP/SSDP discovery.') + + socket.setdefaulttimeout(timeout) + families_and_addresses = list(unique(interface_addresses())) + + for _ in range(retries): + for family, addr in families_and_addresses: + try: + dest = DESTINATIONS[family] + except KeyError: + log.warning('Unknown address family %s, skipping', family) + continue + + log.debug('Sending to %s %s' % (family, addr)) + + sock = socket.socket(family, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + sock.bind((addr, 0)) + + for _ in range(2): + # sending it more than once will + # decrease the probability of a timeout + sock.sendto(DISCOVERY_MSG, (dest, 1900)) + + try: + data = sock.recv(1024) + except socket.timeout: + pass + else: + response = Response(data) + return response.getheader('Location') + + raise DiscoveryFailed('Unable to find Flamenco Manager after %i tries' % retries) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + location = find_flamenco_manager() + print('Found the service at %s' % location)