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
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
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
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
def updateRecord(self, **values): for k in values: self.txt[k] = values[k] pybonjour.DNSServiceUpdateRecord(self.sdRef, None, 0, pybonjour.TXTRecord(self.txt))
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
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)