From b35d708ea1e56a8d79b2f4e5651c57ebc99e7f8d Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 11 Jul 2012 10:48:44 -0400 Subject: added feature to allow clients to declare their version to server --- src/lib/Bcfg2/Component.py | 3 +- src/lib/Bcfg2/Server/Core.py | 89 +++++++++++++----------- src/lib/Bcfg2/Server/Plugins/Metadata.py | 35 ++++++++-- src/lib/Bcfg2/version.py | 115 +++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 47 deletions(-) create mode 100644 src/lib/Bcfg2/version.py (limited to 'src/lib/Bcfg2') diff --git a/src/lib/Bcfg2/Component.py b/src/lib/Bcfg2/Component.py index b6cea3e22..3ee3a14c8 100644 --- a/src/lib/Bcfg2/Component.py +++ b/src/lib/Bcfg2/Component.py @@ -216,7 +216,8 @@ class Component (object): method_func = self._resolve_exposed_method(method) except NoExposedMethod: self.logger.error("Unknown method %s" % (method)) - raise xmlrpclib.Fault(7, "Unknown method %s" % method) + raise xmlrpclib.Fault(xmlrpclib.METHOD_NOT_FOUND, + "Unknown method %s" % method) except Exception: e = sys.exc_info()[1] if getattr(e, "log", True): diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index e8ebd511d..6dbab64bd 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -30,7 +30,7 @@ logger = logging.getLogger('Bcfg2.Server.Core') def critical_error(operation): """Log and err, traceback and return an xmlrpc fault to client.""" logger.error(operation, exc_info=1) - raise xmlrpclib.Fault(7, "Critical unexpected failure: %s" % (operation)) + raise xmlrpclib.Fault(xmlrpclib.APPLICATION_ERROR, "Critical unexpected failure: %s" % (operation)) try: import psyco @@ -413,98 +413,108 @@ class Core(Component): state.get('state'))) self.client_run_hook("end_statistics", meta) + def resolve_client(self, address, cleanup_cache=False, metadata=True): + try: + client = self.metadata.resolve_client(address, + cleanup_cache=cleanup_cache) + if metadata: + meta = self.build_metadata(client) + else: + meta = None + except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: + critical_error("Client metadata resolution error for %s; " + "check server log" % address[0]) + except Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError: + critical_error('Metadata system runtime failure') + return (client, meta) + # XMLRPC handlers start here + @exposed + def DeclareVersion(self, address, version): + """ declare the client version """ + client, metadata = self.resolve_client(address) + try: + self.metadata.set_version(client, version) + except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError, + Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError): + err = sys.exc_info()[1] + critical_error("Unable to set version for %s: %s" % + (client, err)) + return True + @exposed def GetProbes(self, address): """Fetch probes for a particular client.""" resp = lxml.etree.Element('probes') + client, metadata = self.resolve_client(address, cleanup_cache=True) try: - name = self.metadata.resolve_client(address, cleanup_cache=True) - meta = self.build_metadata(name) - for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Probing): - for probe in plugin.GetProbes(meta): + for probe in plugin.GetProbes(metadata): resp.append(probe) return lxml.etree.tostring(resp, encoding='UTF-8', xml_declaration=True) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - warning = 'Client metadata resolution error for %s' % address[0] - self.logger.warning(warning) - raise xmlrpclib.Fault(6, warning + "; check server log") - except Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError: - err_msg = 'Metadata system runtime failure' - self.logger.error(err_msg) - raise xmlrpclib.Fault(6, err_msg) except: - critical_error("Error determining client probes") + critical_error("Error determining probes for %s" % client) @exposed def RecvProbeData(self, address, probedata): """Receive probe data from clients.""" - try: - name = self.metadata.resolve_client(address) - meta = self.build_metadata(name) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - warning = 'Metadata consistency error' - self.logger.warning(warning) - raise xmlrpclib.Fault(6, warning) + client, metadata = self.resolve_client(address) # clear dynamic groups - self.metadata.cgroups[meta.hostname] = [] + self.metadata.cgroups[metadata.hostname] = [] try: xpdata = lxml.etree.XML(probedata.encode('utf-8'), parser=Bcfg2.Server.XMLParser) except: - self.logger.error("Failed to parse probe data from client %s" % \ - (address[0])) - return False + critical_error("Failed to parse probe data from client %s" % + client) sources = [] [sources.append(data.get('source')) for data in xpdata if data.get('source') not in sources] for source in sources: if source not in self.plugins: - self.logger.warning("Failed to locate plugin %s" % (source)) + self.logger.warning("Failed to locate plugin %s" % source) continue dl = [data for data in xpdata if data.get('source') == source] try: - self.plugins[source].ReceiveData(meta, dl) + self.plugins[source].ReceiveData(metadata, dl) except: - logger.error("Failed to process probe data from client %s" % \ - (address[0]), exc_info=1) + critical_error("Failed to process probe data from client %s" % + client) return True @exposed def AssertProfile(self, address, profile): """Set profile for a client.""" + client = self.resolve_client(address, metadata=False)[0] try: - client = self.metadata.resolve_client(address) self.metadata.set_profile(client, profile, address) except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError, Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError): - warning = 'Metadata consistency error' - self.logger.warning(warning) - raise xmlrpclib.Fault(6, warning) + err = sys.exc_info()[1] + critical_error("Unable to assert profile for %s: %s" % + (client, err)) return True @exposed def GetConfig(self, address, checksum=False): """Build config for a client.""" + client = self.resolve_client(address)[0] try: - client = self.metadata.resolve_client(address) config = self.BuildConfiguration(client) return lxml.etree.tostring(config, encoding='UTF-8', xml_declaration=True) except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - self.logger.warning("Metadata consistency failure for %s" % (address)) - raise xmlrpclib.Fault(6, "Metadata consistency failure") + critical_error("Metadata consistency failure for %s" % client) @exposed def RecvStats(self, address, stats): """Act on statistics upload.""" + client = self.resolve_client(address)[0] sdata = lxml.etree.XML(stats.encode('utf-8'), parser=Bcfg2.Server.XMLParser) - client = self.metadata.resolve_client(address) self.process_statistics(client, sdata) return "" @@ -519,6 +529,5 @@ class Core(Component): @exposed def GetDecisionList(self, address, mode): """Get the data of the decision list.""" - client = self.metadata.resolve_client(address) - meta = self.build_metadata(client) - return self.GetDecisions(meta, mode) + client, metadata = self.resolve_client(address) + return self.GetDecisions(metadata, mode) diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index 9786466bc..77e433ab1 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -12,7 +12,7 @@ import time import Bcfg2.Server import Bcfg2.Server.FileMonitor import Bcfg2.Server.Plugin - +from Bcfg2.version import Bcfg2VersionInfo def locked(fd): """Aquire a lock on a file""" @@ -171,8 +171,8 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): class ClientMetadata(object): """This object contains client metadata.""" - def __init__(self, client, profile, groups, bundles, - aliases, addresses, categories, uuid, password, query): + def __init__(self, client, profile, groups, bundles, aliases, addresses, + categories, uuid, password, version, query): self.hostname = client self.profile = profile self.bundles = bundles @@ -183,6 +183,11 @@ class ClientMetadata(object): self.uuid = uuid self.password = password self.connectors = [] + self.version = version + try: + self.version_info = Bcfg2VersionInfo(version) + except: + self.version_info = None self.query = query def inGroup(self, group): @@ -248,6 +253,7 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, self.aliases = {} self.groups = {} self.cgroups = {} + self.versions = {} self.public = [] self.private = [] self.profiles = [] @@ -412,6 +418,8 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, self.floating.append(clname) if 'password' in client.attrib: self.passwords[clname] = client.get('password') + if 'version' in client.attrib: + self.versions[clname] = client.get('version') self.raliases[clname] = set() for alias in client.findall('Alias'): @@ -537,6 +545,20 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, self.clients[client] = profile self.clients_xml.write() + def set_version(self, client, version): + """Set group parameter for provided client.""" + self.logger.info("Asserting client %s version to %s" % (client, version)) + if client in self.clients: + self.logger.info("Setting version on client %s to %s" % + (client, version)) + self.update_client(client, dict(version=version)) + else: + msg = "Cannot set version on non-existent client %s" % client + self.logger.error(msg) + raise MetadataConsistencyError(msg) + self.versions[client] = version + self.clients_xml.write() + def resolve_client(self, addresspair, cleanup_cache=False): """Lookup address locally or in DNS to get a hostname.""" if addresspair in self.session_cache: @@ -575,9 +597,9 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, return self.aliases[cname] return cname except socket.herror: - warning = "address resolution error for %s" % (address) + warning = "address resolution error for %s" % address self.logger.warning(warning) - raise MetadataConsistencyError + raise MetadataConsistencyError(warning) def get_initial_metadata(self, client): """Return the metadata for a given client.""" @@ -599,6 +621,7 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, [bundles, groups, categories] = self.groups[self.default] aliases = self.raliases.get(client, set()) addresses = self.raddresses.get(client, set()) + version = self.versions.get(client, None) newgroups = set(groups) newbundles = set(bundles) newcategories = {} @@ -622,7 +645,7 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, [newgroups.add(g) for g in ngroups if g not in newgroups] newcategories.update(ncategories) return ClientMetadata(client, profile, newgroups, newbundles, aliases, - addresses, newcategories, uuid, password, + addresses, newcategories, uuid, password, version, self.query) def get_all_group_names(self): diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py new file mode 100644 index 000000000..ac10dac94 --- /dev/null +++ b/src/lib/Bcfg2/version.py @@ -0,0 +1,115 @@ +import re + +__version__ = "1.3.0" + +class Bcfg2VersionInfo(tuple): + v_re = re.compile(r'(\d+)(\w+)(\d+)') + + def __new__(cls, vstr): + (major, minor, rest) = vstr.split(".") + match = cls.v_re.match(rest) + if match: + micro, releaselevel, serial = match.groups() + else: + micro = rest + releaselevel = 'final' + serial = 0 + return tuple.__new__(cls, [int(major), int(minor), int(micro), + releaselevel, int(serial)]) + + def __init__(self, vstr): + tuple.__init__(self) + self.major, self.minor, self.micro, self.releaselevel, self.serial = \ + tuple(self) + + def __repr__(self): + return "(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" % \ + tuple(self) + + def _release_cmp(self, r1, r2): + if r1 == r2: + return 0 + elif r1 == "final": + return -1 + elif r2 == "final": + return 1 + elif r1 == "rc": + return -1 + elif r2 == "rc": + return 1 + # should never get to anything past this point + elif r1 == "pre": + return -1 + elif r2 == "pre": + return 1 + else: + # wtf? + return 0 + + def __gt__(self, version): + if version is None: + # older bcfg2 clients didn't report their version, so we + # handle this case specially and assume that any reported + # version is newer than any indeterminate version + return True + try: + for i in range(3): + if self[i] > version[i]: + return True + elif self[i] < version[i]: + return False + rel = self._release_cmp(self[3], version[3]) + if rel < 0: + return True + elif rel > 0: + return False + if self[4] > version[4]: + return True + else: + return False + except TypeError: + return self > Bcfg2VersionInfo(version) + + def __lt__(self, version): + if version is None: + # older bcfg2 clients didn't report their version, so we + # handle this case specially and assume that any reported + # version is newer than any indeterminate version + return False + try: + for i in range(3): + if self[i] < version[i]: + return True + elif self[i] > version[i]: + return False + rel = self._release_cmp(self[3], version[3]) + if rel > 0: + return True + elif rel < 0: + return False + if self[4] < version[4]: + return True + else: + return False + except TypeError: + return self < Bcfg2VersionInfo(version) + + def __eq__(self, version): + if version is None: + # older bcfg2 clients didn't report their version, so we + # handle this case specially and assume that any reported + # version is newer than any indeterminate version + return False + try: + rv = True + for i in range(len(self)): + rv &= self[i] == version[i] + return rv + except TypeError: + return self == Bcfg2VersionInfo(version) + + def __ge__(self, version): + return not self < version + + def __le__(self, version): + return not self > version -- cgit v1.2.3-1-g7c22