[Yum-devel] Service discovery plugin

Mihai Ibanescu misa+yum at redhat.com
Thu May 18 16:40:01 UTC 2006


Hello,

Attached are the 2 files that are part of the service discovery plugin.
Some notes:

- you will need the python-adns package from Extras 5 (more specifically,
  version 1.1.0-3.fc5 or newer). This is because previous versions of
  python-adns did not have support for SRV record retrieval. Release 3 has the
  patch that I sent upstream to Andy Dustman, but I have seen zero reply from
  him so far).
- servicedisc.py (the plugin) assumes that zeroconf.py lives in yum's code
  directory. This is probably not right, but can be fixed afterwards. Problem
  is, I would not put zeroconf.py in /usr/lib/yum-plugins because it's not a
  plugin, it's a support library. It may make sense to package zeroconf.py as
  a separate rpm - but for prototyping purposes we can deploy in
  /usr/lib/python2.4/site-packages/yum
- if python-adns is not installed, the plugin should not raise exceptions (but
  it won't do anything useful either).
- read the notes at the beginning of servicedisc.py for how to set up DNS in
  order for this to work.

I can bore you with details about some of the comments in the code if you
care.

Please send flames my way :-)

Misa

References:
DNS-SD ietf draft: http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
Setting up unicast DNS to support DNS-SD:
    http://www.dns-sd.org/ServerStaticSetup.html
DNS-SD service types: http://www.dns-sd.org/ServiceTypes.html
DNS SRV records RFC: http://www.ietf.org/rfc/rfc2782.txt
-------------- next part --------------
#!/usr/bin/env python

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# (C) Copyright 2006 Red Hat, Inc.
# Author: Mihai Ibanescu <misa at redhat.com>

"""
Version: 0.1

A plugin for the Yellowdog Updater Modified which tries to discover mirror 
servers that are advertised over DNS

For each configured repository, the plugin will make a DNS query for a SRV
record with the name <repo-id>._yum._tcp 

If such a resource exists, it will be preferred over the 'baseurl' and
'mirrorlist' settings in the repository's configuration section.

If the 'strict' option is set in the plugin's configuration file, then
_only_ the discovered URLs will be used (baseurl and mirrorlist are ignored).

To install this plugin, just drop it into /usr/lib/yum-plugins, and
make sure you have 'plugins=1' in your /etc/yum.conf.

# Configuration Options
# /etc/yum/pluginconf.d/servicedisc.conf:
   [main]
   enabled=1

   # strict=1 # use _only_ the urls defined
   # debuglevel=1


To configure DNS, add the following to your DNS zone file. This assumes you do
not have anything else configured to use DNS-SD.

; This domain is browsable
b._dns-sd._udp		IN	PTR	@
lb._dns-sd._udp		IN	PTR	@

; Available services through this domain
_services._dns-sd._udp	IN	PTR	_yum._tcp

; Here we ennumerate service instances for each service
core._yum._tcp                  IN      SRV     1 0 80 download.fedora.mirrors.example.com.
                                IN      TXT     "txtvers=1.0" "path=/pub/fedora/linux/core/$releasever/$basearch/os"

updates._yum._tcp               IN      SRV     1 0 80 download.fedora.mirrors.example.com.
                                IN      TXT     "txtvers=1.0" "path=/pub/fedora/linux/core/updates/$releasever/$basearch"

extras._yum._tcp                IN      SRV     1 0 80 download.fedora.mirrors.example.com.
                                IN      TXT     "txtvers=1.0" "path=/pub/fedora/linux/extras/$releasever/$basearch"

; Advertise instances for this service
_yum._tcp                       IN      PTR     core._yum._tcp
                                IN      PTR     updates._yum._tcp
                                IN      PTR     extras._yum._tcp

; No other services supported
*._tcp				IN      SRV     0 0 0 .
*._udp				IN      SRV     0 0 0 .

(C) Copyright 2006 Red Hat, Inc.
Author: Mihai Ibanescu <misa at redhat.com>
"""

from yum.plugins import TYPE_INTERFACE, TYPE_CORE
from yum.plugins import PluginYumExit

requires_api_version = '2.1'
plugin_type = (TYPE_INTERFACE, TYPE_CORE)

try:
    from yum import zeroconf
except ImportError:
    zeroconf = None

def init_hook(conduit):
    if zeroconf is None:
        return
    # Debug level - inherit it from the yum's main config file
    dl = conduit.getConf().debuglevel
    debuglevel = conduit.confInt('main', 'debuglevel', default=dl)
    # Strict?
    strict = conduit.confBool('main', 'strict', default=False)
    # Instantiate (or reset) the service discovery singleton
    ServiceDiscovery(debuglevel=debuglevel, strict=strict)

def prereposetup_hook(conduit):
    if zeroconf is None:
        return
    s =  ServiceDiscovery()
    ret = s.prereposetup_hook(conduit)
    s.cleanup()
    return ret

class ServiceDiscovery(object):
    """Singleton object to avoid keeping globals around"""
    __instance = None
    known_protos = [ ('http', 80), ('https', 443), ('ftp', 21), ]
    def __new__(typ, *args, **kwargs):
        if ServiceDiscovery.__instance is None:
            ServiceDiscovery.__instance = object.__new__(typ, *args, **kwargs)
        return ServiceDiscovery.__instance

    def __init__(self, **kwargs):
        """Constructor - will not change state if called with no arguments"""
        if not kwargs:
            # No arguments passed, do not change the internal state
            return

        self.debuglevel = int(kwargs.get('debuglevel', 1))
        self.strict = bool(kwargs.get('strict', True))
        self.log = None

    def prereposetup_hook(self, conduit):
        # Expose the logger to other functions
        self.log = conduit.info
        repos = conduit.getRepos()
        for repo in repos.listEnabled():
            self.log(4, "prereposetup_hook: Repo id: %s; baseurl: %s" % 
                (repo.id, repo.baseurl))
            # Discover new URL
            discovery_urls = self.discover_repo(repo.id)
            if not discovery_urls:
                # Unable to discover a better repo
                continue
            self.log(4, "Discovered URLs: %s" % discovery_urls)
            if self.strict:
                # We only want this URL to serve files
                repo.mirrorlist = None
                repo.baseurl = discovery_urls
            else:
                # Prepend the discovered URL to the list
                repo.baseurl[0:0] = discovery_urls

            # Make sure all variables are expanded
            repo.baseurlSetup()

    def cleanup(self):
        self.log = None

    def discover_repo(self, repo_id):
        """Use dns-sd to discover a local repository
        Return a list of URLs to use"""
        service = "%s._yum._tcp" % repo_id
        z = zeroconf.Zeroconf()
        try:
            services, txts = z.resolve_service(service, search=1)
        except zeroconf.Error, e:
            self.log(5, "Error discovering repo: %s" % e)
            return []
        # Loop through the text entries - need to fetch proto and path
        # XXX fixme: this part is a bit shaky right now
        # if you have more than one text entry and more than one server, it
        # will build a cartesian product
        extras = []
        for txt in txts:
            h = self._txt2hash(txt)
            proto = h.get('proto', 'http')
            path = h.get('path', '')
            extras.append((proto, path))
            
        urls = []
        for priority, weight, port, target in services:
            for proto, path in extras:
                if (proto, port) in self.known_protos:
                    url = "%s://%s%s" % (proto, target, path)
                else:
                    url = "%s://%s:%s%s" % (proto, target, port, path)
            urls.append(url)
        return urls

    def _txt2hash(self, txt):
        """Converts a (set of) text entries in the form name=value to a hash
        keyed on the name"""
        ret = {}
        for t in txt:
            arr = t.split('=', 1)
            if len(arr) != 2:
                self.log(1, "Invalid entry", txt)
                continue
            ret[arr[0]] = arr[1]
        return ret
        
def main():
    pass

if __name__ == '__main__':
    main()
-------------- next part --------------
#!/usr/bin/env python -tt
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# 
# (C) Copyright 2006 Red Hat, Inc.
# Author: Mihai Ibanescu <misa at redhat.com>



import sys
import adns

class Error(Exception):
    pass

class Zeroconf:
    def __init__(self):
        self._client = adns.init()

    def browse_browsable_domains(self):
        """Returns all domains that are browsable based on the search list"""
        return self._ptr_lookup('b._dns-sd._udp', search=1)

    def browse_domain(self, domain):
        qname = "_services._dns-sd._udp." + domain
        return self._ptr_lookup(qname, search=0)

    def browse_services(self, service, search=1):
        return self._ptr_lookup(service, search=search)

    def resolve_service(self, service, search=0):
        srvces = self._srv_lookup(service, search=search)
        txts = self._txt_lookup(service, search=search)
        return (srvces, txts)

    def _ptr_lookup(self, qname, search=0):
        """PTR lookup"""
        
        qflags = self._build_qflags(search=search)
        qtype = adns.rr.PTRraw

        return self._query(qname, qtype, qflags)

    def _srv_lookup(self, qname, search=0):
        """SRV lookup"""

        qflags = self._build_qflags(search=search)
        qtype = adns.rr.SRVraw

        return self._query(qname, qtype, qflags)

    def _txt_lookup(self, qname, search=0):
        """TXT lookup"""

        qflags = self._build_qflags(search=search)
        qtype = adns.rr.TXT

        return self._query(qname, qtype, qflags)

    def _query(self, qname, qtype, qflags):
        ret = self._client.synchronous(qname, qtype, qflags)
        if ret[0] != 0:
            raise Error(ret)

        return ret[3]

    def _build_qflags(self, search=0):
        qflags = adns.qflags.quoteok_anshost | adns.qflags.quoteok_query
        if search:
            qflags |= adns.qflags.search

        return qflags

def test():
    z = Zeroconf()
    doms = z.browse_browsable_domains()
    for d in doms:
        dd = z.browse_domain(d)
        print "Domain: %s" % d
        for s in dd:
            print "  Service: %s" % s
            srv = z.browse_services(s)
            for i in srv:
                rez, txts = z.resolve_service(i)
                print "    Instance: %s; text: %s" % (i, txts)
                for r in rez:
                    print "      ", r
    #print z.browse_domain()

def test_direct():
    z = Zeroconf()
    print z.resolve_service('core._yum._tcp', search=1)

def main():
    test_direct()
    return test()

if __name__ == '__main__':
    sys.exit(main() or 0)


More information about the Yum-devel mailing list