Ejemplo n.º 1
0
    def update_txt(self, show=None):
        if show:
            self.txt['status'] = self.replace_show(show)

        try:
            pybonjour.DNSServiceUpdateRecord(self.service_sdRef, None, 0,
                                             self.txt)
        except pybonjour.BonjourError, e:
            return False
Ejemplo n.º 2
0
 def update(self, name, regtype, txtRecord=None):
     recRef = self._find_record(name, regtype)
     if not recRef:
         return False
     if txtRecord is None:
         txtRecord = {}
     if not isinstance(txtRecord, pybonjour.TXTRecord):
         txtRecord = pybonjour.TXTRecord(txtRecord)
     pybonjour.DNSServiceUpdateRecord(recRef[0], None, 0, txtRecord, 0)
     return True
Ejemplo n.º 3
0
    def update_txt(self, show=None):
        if show:
            self.txt['status'] = self.replace_show(show)

        txt = pybonjour.TXTRecord(self.txt, strict=True)
        try:
            pybonjour.DNSServiceUpdateRecord(self.service_sdRef, None, 0, txt)
        except pybonjour.BonjourError as e:
            log.error('Error when updating TXT Record: %s', e)
            return False
        return True
Ejemplo n.º 4
0
def beacon(config):
    """
    Broadcast values via zeroconf

    If the announced values are static, it is advised to set run_once: True
    (do not poll) on the beacon configuration.

    The following are required configuration settings:

    - ``servicetype`` - The service type to announce
    - ``port`` - The port of the service to announce
    - ``txt`` - The TXT record of the service being announced as a dict. Grains
      can be used to define TXT values using one of following two formats:

      - ``grains.<grain_name>``
      - ``grains.<grain_name>[i]`` where i is an integer representing the
        index of the grain to use. If the grain is not a list, the index is
        ignored.

    The following are optional configuration settings:

    - ``servicename`` - Set the name of the service. Will use the hostname from
      the minion's ``host`` grain if this value is not set.
    - ``reset_on_change`` - If ``True`` and there is a change in TXT records
      detected, it will stop announcing the service and then restart announcing
      the service. This interruption in service announcement may be desirable
      if the client relies on changes in the browse records to update its cache
      of TXT records. Defaults to ``False``.
    - ``reset_wait`` - The number of seconds to wait after announcement stops
      announcing and before it restarts announcing in the case where there is a
      change in TXT records detected and ``reset_on_change`` is ``True``.
      Defaults to ``0``.
    - ``copy_grains`` - If ``True``, Salt will copy the grains passed into the
      beacon when it backs them up to check for changes on the next iteration.
      Normally, instead of copy, it would use straight value assignment. This
      will allow detection of changes to grains where the grains are modified
      in-place instead of completely replaced.  In-place grains changes are not
      currently done in the main Salt code but may be done due to a custom
      plug-in. Defaults to ``False``.

    Example Config

    .. code-block:: yaml

       beacons:
         bonjour_announce:
           - run_once: True
           - servicetype: _demo._tcp
           - port: 1234
           - txt:
               ProdName: grains.productname
               SerialNo: grains.serialnumber
               Comments: 'this is a test'
    """
    ret = []
    changes = {}
    txt = {}

    global LAST_GRAINS
    global SD_REF

    _config = {}
    list(map(_config.update, config))

    if "servicename" in _config:
        servicename = _config["servicename"]
    else:
        servicename = __grains__["host"]
        # Check for hostname change
        if LAST_GRAINS and LAST_GRAINS["host"] != servicename:
            changes["servicename"] = servicename

    if LAST_GRAINS and _config.get("reset_on_change", False):
        # Check for IP address change in the case when we reset on change
        if LAST_GRAINS.get("ipv4", []) != __grains__.get("ipv4", []):
            changes["ipv4"] = __grains__.get("ipv4", [])
        if LAST_GRAINS.get("ipv6", []) != __grains__.get("ipv6", []):
            changes["ipv6"] = __grains__.get("ipv6", [])

    for item in _config["txt"]:
        changes_key = "txt." + salt.utils.stringutils.to_unicode(item)
        if _config["txt"][item].startswith("grains."):
            grain = _config["txt"][item][7:]
            grain_index = None
            square_bracket = grain.find("[")
            if square_bracket != -1 and grain[-1] == "]":
                grain_index = int(grain[square_bracket + 1:-1])
                grain = grain[:square_bracket]

            grain_value = __grains__.get(grain, "")
            if isinstance(grain_value, list):
                if grain_index is not None:
                    grain_value = grain_value[grain_index]
                else:
                    grain_value = ",".join(grain_value)
            txt[item] = _enforce_txt_record_maxlen(item, grain_value)
            if LAST_GRAINS and (LAST_GRAINS.get(grain, "") != __grains__.get(
                    grain, "")):
                changes[changes_key] = txt[item]
        else:
            txt[item] = _enforce_txt_record_maxlen(item, _config["txt"][item])

        if not LAST_GRAINS:
            changes[changes_key] = txt[item]

    if changes:
        txt_record = pybonjour.TXTRecord(items=txt)
        if not LAST_GRAINS:
            changes["servicename"] = servicename
            changes["servicetype"] = _config["servicetype"]
            changes["port"] = _config["port"]
            changes["ipv4"] = __grains__.get("ipv4", [])
            changes["ipv6"] = __grains__.get("ipv6", [])
            SD_REF = pybonjour.DNSServiceRegister(
                name=servicename,
                regtype=_config["servicetype"],
                port=_config["port"],
                txtRecord=txt_record,
                callBack=_register_callback,
            )
            atexit.register(_close_sd_ref)
            ready = select.select([SD_REF], [], [])
            if SD_REF in ready[0]:
                pybonjour.DNSServiceProcessResult(SD_REF)
        elif _config.get("reset_on_change", False) or "servicename" in changes:
            # A change in 'servicename' requires a reset because we can only
            # directly update TXT records
            SD_REF.close()
            SD_REF = None
            reset_wait = _config.get("reset_wait", 0)
            if reset_wait > 0:
                time.sleep(reset_wait)
            SD_REF = pybonjour.DNSServiceRegister(
                name=servicename,
                regtype=_config["servicetype"],
                port=_config["port"],
                txtRecord=txt_record,
                callBack=_register_callback,
            )
            ready = select.select([SD_REF], [], [])
            if SD_REF in ready[0]:
                pybonjour.DNSServiceProcessResult(SD_REF)
        else:
            txt_record_raw = str(txt_record).encode("utf-8")
            pybonjour.DNSServiceUpdateRecord(SD_REF,
                                             RecordRef=None,
                                             flags=0,
                                             rdata=txt_record_raw)

        ret.append({"tag": "result", "changes": changes})

    if _config.get("copy_grains", False):
        LAST_GRAINS = __grains__.copy()
    else:
        LAST_GRAINS = __grains__

    return ret
Ejemplo n.º 5
0
 def updateRecord(self, **values):
     for k in values:
         self.txt[k] = values[k]
     pybonjour.DNSServiceUpdateRecord(self.sdRef, None, 0,
                                      pybonjour.TXTRecord(self.txt))
Ejemplo n.º 6
0
def beacon(config):
    '''
    Broadcast values via zeroconf

    If the announced values are static, it is advised to set run_once: True
    (do not poll) on the beacon configuration.

    The following are required configuration settings:
        'servicetype': The service type to announce.
        'port': The port of the service to announce.
        'txt': The TXT record of the service being announced as a dict.
               Grains can be used to define TXT values using the syntax:
                   grains.<grain_name>
               or:
                   grains.<grain_name>[i]
               where i is an integer representing the index of the grain to
               use. If the grain is not a list, the index is ignored.

    The following are optional configuration settings:
        'servicename': Set the name of the service. Will use the hostname from
                       __grains__['host'] if not set.
        'reset_on_change': If true and there is a change in TXT records
                           detected, it will stop announcing the service and
                           then restart announcing the service. This
                           interruption in service announcement may be
                           desirable if the client relies on changes in the
                           browse records to update its cache of the TXT
                           records.
                           Defaults to False.
        'reset_wait': The number of seconds to wait after announcement stops
                      announcing and before it restarts announcing in the
                      case where there is a change in TXT records detected
                      and 'reset_on_change' is True.
                      Defaults to 0.
        'copy_grains': If set to True, it will copy the grains passed into
                       the beacon when it backs them up to check for changes
                       on the next iteration. Normally, instead of copy, it
                       would use straight value assignment. This will allow
                       detection of changes to grains where the grains are
                       modified in-place instead of completely replaced.
                       In-place grains changes are not currently done in the
                       main Salt code but may be done due to a custom
                       plug-in.
                       Defaults to False.

    Example Config

    .. code-block:: yaml

       beacons:
         bonjour_announce:
           run_once: True
           servicetype: _demo._tcp
           port: 1234
           txt:
             ProdName: grains.productname
             SerialNo: grains.serialnumber
             Comments: 'this is a test'
    '''
    ret = []
    changes = {}
    txt = {}

    global LAST_GRAINS
    global SD_REF

    _validate = __validate__(config)
    if not _validate[0]:
        log.warning('Beacon {0} configuration invalid, '
                    'not adding. {1}'.format(__virtualname__, _validate[1]))
        return ret

    if 'servicename' in config:
        servicename = config['servicename']
    else:
        servicename = __grains__['host']
        # Check for hostname change
        if LAST_GRAINS and LAST_GRAINS['host'] != servicename:
            changes['servicename'] = servicename

    if LAST_GRAINS and config.get('reset_on_change', False):
        # Check for IP address change in the case when we reset on change
        if LAST_GRAINS.get('ipv4', []) != __grains__.get('ipv4', []):
            changes['ipv4'] = __grains__.get('ipv4', [])
        if LAST_GRAINS.get('ipv6', []) != __grains__.get('ipv6', []):
            changes['ipv6'] = __grains__.get('ipv6', [])

    for item in config['txt']:
        if config['txt'][item].startswith('grains.'):
            grain = config['txt'][item][7:]
            grain_index = None
            square_bracket = grain.find('[')
            if square_bracket != -1 and grain[-1] == ']':
                grain_index = int(grain[square_bracket + 1:-1])
                grain = grain[:square_bracket]

            grain_value = __grains__.get(grain, '')
            if isinstance(grain_value, list):
                if grain_index is not None:
                    grain_value = grain_value[grain_index]
                else:
                    grain_value = ','.join(grain_value)
            txt[item] = _enforce_txt_record_maxlen(item, grain_value)
            if LAST_GRAINS and (LAST_GRAINS.get(grain, '') != __grains__.get(
                    grain, '')):
                changes[str('txt.' + item)] = txt[item]
        else:
            txt[item] = _enforce_txt_record_maxlen(item, config['txt'][item])

        if not LAST_GRAINS:
            changes[str('txt.' + item)] = txt[item]

    if changes:
        txt_record = pybonjour.TXTRecord(items=txt)
        if not LAST_GRAINS:
            changes['servicename'] = servicename
            changes['servicetype'] = config['servicetype']
            changes['port'] = config['port']
            changes['ipv4'] = __grains__.get('ipv4', [])
            changes['ipv6'] = __grains__.get('ipv6', [])
            SD_REF = pybonjour.DNSServiceRegister(
                name=servicename,
                regtype=config['servicetype'],
                port=config['port'],
                txtRecord=txt_record,
                callBack=_register_callback)
            atexit.register(_close_sd_ref)
            ready = select.select([SD_REF], [], [])
            if SD_REF in ready[0]:
                pybonjour.DNSServiceProcessResult(SD_REF)
        elif config.get('reset_on_change', False) or 'servicename' in changes:
            # A change in 'servicename' requires a reset because we can only
            # directly update TXT records
            SD_REF.close()
            SD_REF = None
            reset_wait = config.get('reset_wait', 0)
            if reset_wait > 0:
                time.sleep(reset_wait)
            SD_REF = pybonjour.DNSServiceRegister(
                name=servicename,
                regtype=config['servicetype'],
                port=config['port'],
                txtRecord=txt_record,
                callBack=_register_callback)
            ready = select.select([SD_REF], [], [])
            if SD_REF in ready[0]:
                pybonjour.DNSServiceProcessResult(SD_REF)
        else:
            txt_record_raw = str(txt_record).encode('utf-8')
            pybonjour.DNSServiceUpdateRecord(SD_REF,
                                             RecordRef=None,
                                             flags=0,
                                             rdata=txt_record_raw)

        ret.append({'tag': 'result', 'changes': changes})

    if config.get('copy_grains', False):
        LAST_GRAINS = __grains__.copy()
    else:
        LAST_GRAINS = __grains__

    return ret
Ejemplo n.º 7
0
class AImDNS(object):
    ''' Class: AImDNS - base class for registering, browsing and looking up
                        AI and ad hoc mDNS records.
    '''
    # a _handle_event() loop control variable, used to restart the loop
    # after modification to the self.sdrefs variable, private
    _restart_loop = False

    # find/browse mode variables, private
    _do_lookup = False
    _found = False

    # mDNS record resolved variable, used as a stack to indicate that the
    # service has been found, private
    _resolved = list()

    def __init__(self, servicename=None, domain='local', comment=None):
        '''Method: __init__, class private
           Parameters:
                        servicename - the AI servicename
                        domain      - the domain for the registered service
                        comment     - the text comment for the service
           Raises:
               AImDNSError - when unable to retrieve setup information from
                             the host about the available interfaces or the
                             AI SMF service.
        '''
        gettext.install("ai", "/usr/lib/locale")

        # find sdref handle
        self._find = None
        self._lookup = False
        self.services = dict()
        self.servicename = servicename
        self.domain = domain
        self.txt = comment
        self.inter = None
        self.port = 0
        self.verbose = False
        self.timeout = 5
        self.done = False
        self.count = 0

        self.sdrefs = dict()

        self.interfaces = libaimdns.getifaddrs()

        self.register_initialized = False
        self.exclude = False
        self.networks = ['0.0.0.0/0']

        self.instance = None
        self.instance_services = None

    def __del__(self):
        '''Method: __del__
           Parameters:
                None

           Raises:
                None
        '''
        self.done = True
        self.clear_sdrefs()

    def _resolve_callback(self, sdref, flags, interfaceindex, errorcode,
                          fullname, hosttarget, port, txtrecord):
        '''Method: _resolve_callback, class private
        Description:
            DNS Callback for the resolve process, stories the service
            information within the self.services variable.

        Args
            sdref          - service reference,
                             standard argument for callback, not used
            flags          - flag to determine what action is taking place
                             standard argument for callback, not used
            interfaceindex - the index for the interface that the service was
                             found on
            errorcode      - flag to determine if a registration error occurred
            fullname       - name of the service, should be
                             <service>._OSInstall._tcp.local.
            hosttarget     - name of the host, should be <nodename>.local.
            port           - the service port being used
            txtrecord      - the text record associated with the service,
                             standard argument for callback

        Returns
            None

        Raises
            None
        '''
        # handle errors from within the _browse_callback
        # after the select() call
        if errorcode == pyb.kDNSServiceErr_NoError:
            self._found = True
            # get the interface name for the index
            interface = netif.if_indextoname(interfaceindex)
            # interested in the service name and the domain only
            parts = fullname.split('.')
            service = dict()
            service['flags'] = not (flags & pyb.kDNSServiceFlagsAdd)
            service['hosttarget'] = hosttarget
            service['servicename'] = parts[0]
            service['domain'] = parts[-2]
            service['port'] = port
            service['comments'] = str(pyb.TXTRecord.parse(txtrecord))[1:]
            self.services.setdefault(interface, list()).append(service)

            # update the resolve stack flag
            self._resolved.append(True)

    def _browse_callback(self, sdref, flags, interfaceindex, errorcode,
                         servicename, regtype, replydomain):
        '''Method: _browse_callback, class private
        Description:
            DNS Callback for the browse process

        Args
            sdref          - service reference,
                             standard argument for callback, not used
            flags          - flag to determine what action is taking place
                             standard argument for callback, not used
            interfaceindex - the index for the interface that the service was
                             found on
            errorcode      - flag to determine if a registration error occurred
            servicename    - name of the service
            hosttarget     - name of the host, should be <nodename>.local.
            regtype        - registration type, should be _OSInstall._tcp.
            replydomain    - DNS domain, either local or remote

        Returns
            None

        Raises
            None
        '''
        if errorcode != pyb.kDNSServiceErr_NoError:
            return  # error handled in the _handle_event() method

        if self._lookup and servicename != self.servicename:
            return

        resolve_sdref = pyb.DNSServiceResolve(0, interfaceindex, servicename,
                                              regtype, replydomain,
                                              self._resolve_callback)

        # wait for and process resolve the current request
        try:
            while not self._resolved:
                try:
                    ready = select.select([resolve_sdref], list(), list(),
                                          self.timeout)
                except select.error:
                    # purposely ignore errors.
                    continue

                if resolve_sdref not in ready[0]:
                    # not a catastrophic error for the class, therefore,
                    # simply warn that the mDNS service record needed
                    # additional time to process and do not issue an
                    # exception.
                    sys.stderr.write(
                        cw(
                            _('warning: unable to resolve "%s", '
                              'try using a longer timeout\n') % servicename))
                    break
                # process the service
                pyb.DNSServiceProcessResult(resolve_sdref)
            else:
                self._resolved.pop()
        # allow exceptions to fall through
        finally:
            # clean up when there is no exception
            resolve_sdref.close()

    def _handle_events(self):
        ''' Method: __handle_events, class private
            Description:
                Handle the event processing for the registered service
                requests.

            Args
                None

            Returns
                None

            Raises
                None
        '''
        self.done = False
        while not self.done:
            # The self.sdrefs is a dictionary of the form:
            #
            #   for the find mode:
            #       { 'find':[list of sdrefs] }
            #
            #   OR for the browse mode:
            #       { 'browse':[list of sdrefs] }
            #
            #   OR for the register mode:
            #       { <service-name>:[list of sdrefs] }
            #
            #   OR for the register all mode:
            #       { <service-name1>:[list of sdrefs],
            #         <service-name2>:[list of sdrefs],
            #         ... }
            #
            # This must be converted to a simple list of sdrefs for the
            # select() call.
            therefs = list()
            # iterate through the dictionary
            for srv in self.sdrefs:
                for refs in self.sdrefs.get(srv, list()):
                    if refs is not None:
                        therefs.append(refs)

            # loop until done or we need to redo the service reference
            # list mentioned above.  The service reference list will be
            # updated when the SMF service is refreshed which sends a
            # SIGHUP to the application in daemon mode.  This processing
            # of the SIGHUP is done in the signal_hup() method below.
            self._restart_loop = False
            count = 0
            while not self._restart_loop and not self.done:
                try:
                    # process the appropriate service reference
                    try:
                        ready = select.select(therefs, list(), list(),
                                              self.timeout)
                    except select.error:
                        continue

                    # check to ensure that the __del__ method was not called
                    # between the select and the DNS processing.
                    if self.done:
                        continue

                    for sdref in therefs:
                        if sdref in ready[0]:
                            pyb.DNSServiceProcessResult(sdref)

                    # if browse or find loop then loop only long enough to
                    # ensure that all the registered mDNS records are
                    # retrieved per interface configured
                    if self._do_lookup is True:
                        count += 1
                        if count >= self.count:
                            self.done = True

                # <CTL>-C will exit the loop, application
                # needed for command line invocation
                except KeyboardInterrupt:
                    self.done = True

    def _register_callback(self, sdref, flags, errorcode, name, regtype,
                           domain):
        '''Method: _register_callback, private to class
           Description:
                DNS Callback for the registration process

            Args
                sdref       - service reference
                              standard argument for callback, not used
                flags       - flag to determine what action is taking place
                              standard argument for callback, not used
                errorcode   - flag to determine if a registration error
                              occurred
                name        - name of the service
                regtype     - registration type, should be _OSInstall._tcp.
                domain      - DNS domain, either local or remote

            Returns
                None

            Raises
                None
        '''
        # note: DNSService Errors are ignored here and handled elsewhere.
        if errorcode == pyb.kDNSServiceErr_NoError and \
           self.verbose:
            print _('Registered service:')
            print _('\tname    = %s') % name
            print _('\tregtype = %s') % regtype
            print _('\tdomain  = %s') % domain

    def _register_a_service(self,
                            name,
                            interfaces=None,
                            port=0,
                            comments=None):
        '''Method: _register_a_service, private to class

        Description:
            Register a single service on the interfaces

        Args
            interfaces - the interfaces to register the service on
            instance   - the SMF service instance handle
            name       - the service name to be registered
            port       - the port that the service is listening on, if
                         port is 0 then registering a service listed in
                         the AI SMF service instance.
            comments   - comments for the ad hoc registered service

        Returns
            list_sdrefs - list of service references

        Raises
            AImDNSError - if SMF status property does not exist, OR
                          if SMF txt_record property does not exist, OR
                          if SMF port property does not exist.
        '''
        if not self.register_initialized:
            self.exclude = libaimdns.getboolean_property(
                common.SRVINST, common.EXCLPROP)
            self.networks = libaimdns.getstrings_property(
                common.SRVINST, common.NETSPROP)
            self.register_initialized = True

        smf_port = None
        # if port is 0 then processing an AI service
        if port == 0:
            serv = config.get_service_props(name)
            if not serv:
                raise AIMDNSError(
                    cw(
                        _('error: aiMDNSError: no such '
                          'installation service "%s"') % name))

            # ensure the service is enabled
            if config.PROP_STATUS not in serv:
                raise AIMDNSError(
                    cw(
                        _('error: aiMDNSError: installation '
                          'service key "status" property does '
                          'not exist')))

            if serv[config.PROP_STATUS] != config.STATUS_ON:
                print(
                    cw(
                        _('warning: Installation service "%s" is not enabled '
                          % name)))
                return None

            smf_port = config.get_service_port(name)
            if not smf_port:
                try:
                    smf_port = libaimdns.getinteger_property(
                        common.SRVINST, common.PORTPROP)
                    smf_port = str(smf_port)
                except libaimdns.aiMDNSError, err:
                    raise AIMDNSError(
                        cw(
                            _('error: aiMDNSError: port property '
                              'failure (%s)') % err))

        # iterate over the interfaces saving the service references
        list_sdrefs = list()
        valid_networks = common.get_valid_networks()
        for inf in interfaces:
            include_it = False
            for ip in valid_networks:
                if interfaces[inf].startswith(ip):
                    include_it = True
                    break

            if not include_it:
                continue

            if self.verbose:
                print cw(_('Registering %s on %s (%s)') % \
                           (name, inf, interfaces[inf]))

            if smf_port is not None:
                # comments are part of the service record
                commentkey = serv[config.PROP_TXT_RECORD].split('=')[0]
                commenttxt = interfaces[inf].split('/')[0] + ':' + smf_port
                text = pyb.TXTRecord({commentkey: commenttxt})
                try:
                    port = int(smf_port)
                except ValueError:
                    # not a catastrophic error, just
                    # assume the default port of 5555.
                    port = common.DEFAULT_PORT
            # processing an ad hoc registration
            elif comments is None:
                adhoc_dict = {'service': 'ad hoc registration'}
                text = pyb.TXTRecord(adhoc_dict)
            else:
                text = pyb.TXTRecord({'service': comments})

            # register the service on the appropriate interface index
            try:
                interfaceindex = netif.if_nametoindex(inf)
            except netif.NetIFError, err:
                raise AIMDNSError(err)

            sdref = pyb.DNSServiceRegister(name=name,
                                           interfaceIndex=interfaceindex,
                                           regtype=common.REGTYPE,
                                           port=port,
                                           callBack=self._register_callback,
                                           txtRecord=text)

            # DNSServiceUpdateRecord will update the default record if
            # RecordRef is None. Time-to-live (ttl) for the record is being
            # set to 10 seconds.  This value allows enough time for the
            # record to be looked up and it is short enough that when the
            # service is deleted then the mdns daemon will remove it from
            # the cache after this value expires but prior to another service
            # with the same name being created.
            pyb.DNSServiceUpdateRecord(sdRef=sdref,
                                       RecordRef=None,
                                       rdata=text,
                                       ttl=10)

            # save the registered service reference
            list_sdrefs.append(sdref)