| 1 | '''This file stores persistent metadata for the Bcfg2 Configuration Repository''' |
|---|
| 2 | __revision__ = '$Revision$' |
|---|
| 3 | |
|---|
| 4 | import copy |
|---|
| 5 | import fcntl |
|---|
| 6 | import lxml.etree |
|---|
| 7 | import socket |
|---|
| 8 | import time |
|---|
| 9 | import Bcfg2.Server.Plugin |
|---|
| 10 | |
|---|
| 11 | class MetadataConsistencyError(Exception): |
|---|
| 12 | '''This error gets raised when metadata is internally inconsistent''' |
|---|
| 13 | pass |
|---|
| 14 | |
|---|
| 15 | class MetadataRuntimeError(Exception): |
|---|
| 16 | '''This error is raised when the metadata engine is called prior to reading enough data''' |
|---|
| 17 | pass |
|---|
| 18 | |
|---|
| 19 | class ClientMetadata(object): |
|---|
| 20 | '''This object contains client metadata''' |
|---|
| 21 | def __init__(self, client, profile, groups, bundles, |
|---|
| 22 | aliases, addresses, categories, uuid, password, query): |
|---|
| 23 | self.hostname = client |
|---|
| 24 | self.profile = profile |
|---|
| 25 | self.bundles = bundles |
|---|
| 26 | self.aliases = aliases |
|---|
| 27 | self.addresses = addresses |
|---|
| 28 | self.groups = groups |
|---|
| 29 | self.categories = categories |
|---|
| 30 | self.uuid = uuid |
|---|
| 31 | self.password = password |
|---|
| 32 | self.connectors = [] |
|---|
| 33 | self.query = query |
|---|
| 34 | |
|---|
| 35 | def inGroup(self, group): |
|---|
| 36 | '''Test to see if client is a member of group''' |
|---|
| 37 | return group in self.groups |
|---|
| 38 | |
|---|
| 39 | class MetadataQuery(object): |
|---|
| 40 | def __init__(self, by_name, get_clients, by_groups, by_profiles, all_groups): |
|---|
| 41 | # resolver is set later |
|---|
| 42 | self.by_name = by_name |
|---|
| 43 | self.names_by_groups = by_groups |
|---|
| 44 | self.names_by_profiles = by_profiles |
|---|
| 45 | self.all_clients = get_clients |
|---|
| 46 | self.all_groups = all_groups |
|---|
| 47 | |
|---|
| 48 | def by_groups(self, groups): |
|---|
| 49 | return [self.by_name(name) for name in self.names_by_groups(groups)] |
|---|
| 50 | |
|---|
| 51 | def by_profiles(self, profiles): |
|---|
| 52 | return [self.by_name(name) for name in self.names_by_profiles(profiles)] |
|---|
| 53 | |
|---|
| 54 | def all(self): |
|---|
| 55 | return [self.by_name(name) for name in self.all_clients()] |
|---|
| 56 | |
|---|
| 57 | class Metadata(Bcfg2.Server.Plugin.Plugin, |
|---|
| 58 | Bcfg2.Server.Plugin.Metadata, |
|---|
| 59 | Bcfg2.Server.Plugin.Statistics): |
|---|
| 60 | '''This class contains data for bcfg2 server metadata''' |
|---|
| 61 | __version__ = '$Id$' |
|---|
| 62 | __author__ = 'bcfg-dev@mcs.anl.gov' |
|---|
| 63 | name = "Metadata" |
|---|
| 64 | |
|---|
| 65 | def __init__(self, core, datastore, watch_clients=True): |
|---|
| 66 | Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) |
|---|
| 67 | Bcfg2.Server.Plugin.Metadata.__init__(self) |
|---|
| 68 | Bcfg2.Server.Plugin.Statistics.__init__(self) |
|---|
| 69 | if watch_clients: |
|---|
| 70 | try: |
|---|
| 71 | core.fam.AddMonitor("%s/%s" % (self.data, "groups.xml"), self) |
|---|
| 72 | core.fam.AddMonitor("%s/%s" % (self.data, "clients.xml"), self) |
|---|
| 73 | except: |
|---|
| 74 | raise Bcfg2.Server.Plugin.PluginInitError |
|---|
| 75 | self.states = {} |
|---|
| 76 | if watch_clients: |
|---|
| 77 | self.states = {"groups.xml":False, "clients.xml":False} |
|---|
| 78 | self.addresses = {} |
|---|
| 79 | self.auth = dict() |
|---|
| 80 | self.clients = {} |
|---|
| 81 | self.aliases = {} |
|---|
| 82 | self.groups = {} |
|---|
| 83 | self.cgroups = {} |
|---|
| 84 | self.public = [] |
|---|
| 85 | self.private = [] |
|---|
| 86 | self.profiles = [] |
|---|
| 87 | self.categories = {} |
|---|
| 88 | self.bad_clients = {} |
|---|
| 89 | self.uuid = {} |
|---|
| 90 | self.secure = [] |
|---|
| 91 | self.floating = [] |
|---|
| 92 | self.passwords = {} |
|---|
| 93 | self.session_cache = {} |
|---|
| 94 | self.clientdata = None |
|---|
| 95 | self.clientdata_original = None |
|---|
| 96 | self.default = None |
|---|
| 97 | self.pdirty = False |
|---|
| 98 | self.extra = {'groups.xml':[], 'clients.xml':[]} |
|---|
| 99 | self.password = core.password |
|---|
| 100 | self.query = MetadataQuery(core.build_metadata, |
|---|
| 101 | lambda:self.clients.keys(), |
|---|
| 102 | self.get_client_names_by_groups, |
|---|
| 103 | self.get_client_names_by_profiles, |
|---|
| 104 | self.get_all_group_names) |
|---|
| 105 | |
|---|
| 106 | @classmethod |
|---|
| 107 | def init_repo(cls, repo, groups, os_selection, clients): |
|---|
| 108 | path = '%s/%s' % (repo, cls.name) |
|---|
| 109 | cls.make_path(path) |
|---|
| 110 | open("%s/Metadata/groups.xml" % |
|---|
| 111 | repo, "w").write(groups % os_selection) |
|---|
| 112 | open("%s/Metadata/clients.xml" % |
|---|
| 113 | repo, "w").write(clients % socket.getfqdn()) |
|---|
| 114 | |
|---|
| 115 | def get_groups(self): |
|---|
| 116 | '''return groups xml tree''' |
|---|
| 117 | groups_tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 118 | root = groups_tree.getroot() |
|---|
| 119 | return root |
|---|
| 120 | |
|---|
| 121 | def search_group(self, group_name, tree): |
|---|
| 122 | '''find a group''' |
|---|
| 123 | for node in tree.findall("//Group"): |
|---|
| 124 | if node.get("name") == group_name: |
|---|
| 125 | return node |
|---|
| 126 | for child in node: |
|---|
| 127 | if child.tag == "Alias" and child.attrib["name"] == group_name: |
|---|
| 128 | return node |
|---|
| 129 | return None |
|---|
| 130 | |
|---|
| 131 | def add_group(self, group_name, attribs): |
|---|
| 132 | '''add group to groups.xml''' |
|---|
| 133 | tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 134 | root = tree.getroot() |
|---|
| 135 | element = lxml.etree.Element("Group", name=group_name) |
|---|
| 136 | for key, val in attribs.iteritems(): |
|---|
| 137 | element.set(key, val) |
|---|
| 138 | node = self.search_group(group_name, tree) |
|---|
| 139 | if node != None: |
|---|
| 140 | self.logger.error("Group \"%s\" already exists" % (group_name)) |
|---|
| 141 | raise MetadataConsistencyError |
|---|
| 142 | root.append(element) |
|---|
| 143 | group_tree = open(self.data + "/groups.xml","w") |
|---|
| 144 | fd = group_tree.fileno() |
|---|
| 145 | while True: |
|---|
| 146 | try: |
|---|
| 147 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 148 | except IOError: |
|---|
| 149 | continue |
|---|
| 150 | else: |
|---|
| 151 | break |
|---|
| 152 | tree.write(group_tree) |
|---|
| 153 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 154 | group_tree.close() |
|---|
| 155 | |
|---|
| 156 | def update_group(self, group_name, attribs): |
|---|
| 157 | '''Update a groups attributes''' |
|---|
| 158 | tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 159 | root = tree.getroot() |
|---|
| 160 | node = self.search_group(group_name, tree) |
|---|
| 161 | if node == None: |
|---|
| 162 | self.logger.error("Group \"%s\" not found" % (group_name)) |
|---|
| 163 | raise MetadataConsistencyError |
|---|
| 164 | node.attrib.update(attribs) |
|---|
| 165 | group_tree = open(self.data + "/groups.xml","w") |
|---|
| 166 | fd = group_tree.fileno() |
|---|
| 167 | while True: |
|---|
| 168 | try: |
|---|
| 169 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 170 | except IOError: |
|---|
| 171 | continue |
|---|
| 172 | else: |
|---|
| 173 | break |
|---|
| 174 | tree.write(group_tree) |
|---|
| 175 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 176 | group_tree.close() |
|---|
| 177 | |
|---|
| 178 | def remove_group(self, group_name): |
|---|
| 179 | '''Remove a group''' |
|---|
| 180 | tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 181 | root = tree.getroot() |
|---|
| 182 | node = self.search_group(group_name, tree) |
|---|
| 183 | if node == None: |
|---|
| 184 | self.logger.error("Client \"%s\" not found" % (group_name)) |
|---|
| 185 | raise MetadataConsistencyError |
|---|
| 186 | root.remove(node) |
|---|
| 187 | group_tree = open(self.data + "/groups.xml","w") |
|---|
| 188 | fd = group_tree.fileno() |
|---|
| 189 | while True: |
|---|
| 190 | try: |
|---|
| 191 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 192 | except IOError: |
|---|
| 193 | continue |
|---|
| 194 | else: |
|---|
| 195 | break |
|---|
| 196 | tree.write(group_tree) |
|---|
| 197 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 198 | group_tree.close() |
|---|
| 199 | |
|---|
| 200 | def add_bundle(self, bundle_name): |
|---|
| 201 | '''add bundle to groups.xml''' |
|---|
| 202 | tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 203 | root = tree.getroot() |
|---|
| 204 | element = lxml.etree.Element("Bundle", name=bundle_name) |
|---|
| 205 | node = self.search_group(bundle_name, tree) |
|---|
| 206 | if node != None: |
|---|
| 207 | self.logger.error("Bundle \"%s\" already exists" % (bundle_name)) |
|---|
| 208 | raise MetadataConsistencyError |
|---|
| 209 | root.append(element) |
|---|
| 210 | group_tree = open(self.data + "/groups.xml","w") |
|---|
| 211 | fd = group_tree.fileno() |
|---|
| 212 | while True: |
|---|
| 213 | try: |
|---|
| 214 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 215 | except IOError: |
|---|
| 216 | continue |
|---|
| 217 | else: |
|---|
| 218 | break |
|---|
| 219 | tree.write(group_tree) |
|---|
| 220 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 221 | group_tree.close() |
|---|
| 222 | |
|---|
| 223 | def remove_bundle(self, bundle_name): |
|---|
| 224 | '''Remove a bundle''' |
|---|
| 225 | tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 226 | root = tree.getroot() |
|---|
| 227 | node = self.search_group(bundle_name, tree) |
|---|
| 228 | if node == None: |
|---|
| 229 | self.logger.error("Bundle \"%s\" not found" % (bundle_name)) |
|---|
| 230 | raise MetadataConsistencyError |
|---|
| 231 | root.remove(node) |
|---|
| 232 | group_tree = open(self.data + "/groups.xml","w") |
|---|
| 233 | fd = group_tree.fileno() |
|---|
| 234 | while True: |
|---|
| 235 | try: |
|---|
| 236 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 237 | except IOError: |
|---|
| 238 | continue |
|---|
| 239 | else: |
|---|
| 240 | break |
|---|
| 241 | tree.write(group_tree) |
|---|
| 242 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 243 | group_tree.close() |
|---|
| 244 | |
|---|
| 245 | def search_client(self, client_name, tree): |
|---|
| 246 | '''find a client''' |
|---|
| 247 | for node in tree.findall("//Client"): |
|---|
| 248 | if node.get("name") == client_name: |
|---|
| 249 | return node |
|---|
| 250 | for child in node: |
|---|
| 251 | if child.tag == "Alias" and child.attrib["name"] == client_name: |
|---|
| 252 | return node |
|---|
| 253 | return None |
|---|
| 254 | |
|---|
| 255 | def add_client(self, client_name, attribs): |
|---|
| 256 | '''add client to clients.xml''' |
|---|
| 257 | tree = lxml.etree.parse(self.data + "/clients.xml") |
|---|
| 258 | root = tree.getroot() |
|---|
| 259 | element = lxml.etree.Element("Client", name=client_name) |
|---|
| 260 | for key, val in attribs.iteritems(): |
|---|
| 261 | element.set(key, val) |
|---|
| 262 | node = self.search_client(client_name, tree) |
|---|
| 263 | if node != None: |
|---|
| 264 | self.logger.error("Client \"%s\" already exists" % (client_name)) |
|---|
| 265 | raise MetadataConsistencyError |
|---|
| 266 | root.append(element) |
|---|
| 267 | client_tree = open(self.data + "/clients.xml","w") |
|---|
| 268 | fd = client_tree.fileno() |
|---|
| 269 | while True: |
|---|
| 270 | try: |
|---|
| 271 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 272 | except IOError: |
|---|
| 273 | continue |
|---|
| 274 | else: |
|---|
| 275 | break |
|---|
| 276 | tree.write(client_tree) |
|---|
| 277 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 278 | client_tree.close() |
|---|
| 279 | |
|---|
| 280 | def update_client(self, client_name, attribs): |
|---|
| 281 | '''Update a clients attributes''' |
|---|
| 282 | tree = lxml.etree.parse(self.data + "/clients.xml") |
|---|
| 283 | root = tree.getroot() |
|---|
| 284 | node = self.search_client(client_name, tree) |
|---|
| 285 | if node == None: |
|---|
| 286 | self.logger.error("Client \"%s\" not found" % (client_name)) |
|---|
| 287 | raise MetadataConsistencyError |
|---|
| 288 | node.attrib.update(attribs) |
|---|
| 289 | client_tree = open(self.data + "/clients.xml","w") |
|---|
| 290 | fd = client_tree.fileno() |
|---|
| 291 | while True: |
|---|
| 292 | try: |
|---|
| 293 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 294 | except IOError: |
|---|
| 295 | continue |
|---|
| 296 | else: |
|---|
| 297 | break |
|---|
| 298 | tree.write(client_tree) |
|---|
| 299 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 300 | client_tree.close() |
|---|
| 301 | |
|---|
| 302 | def HandleEvent(self, event): |
|---|
| 303 | '''Handle update events for data files''' |
|---|
| 304 | filename = event.filename.split('/')[-1] |
|---|
| 305 | if filename in ['groups.xml', 'clients.xml']: |
|---|
| 306 | dest = filename |
|---|
| 307 | elif filename in reduce(lambda x, y:x+y, self.extra.values()): |
|---|
| 308 | if event.code2str() == 'exists': |
|---|
| 309 | return |
|---|
| 310 | dest = [key for key, value in self.extra.iteritems() if filename in value][0] |
|---|
| 311 | else: |
|---|
| 312 | return |
|---|
| 313 | if event.code2str() == 'endExist': |
|---|
| 314 | return |
|---|
| 315 | try: |
|---|
| 316 | xdata = lxml.etree.parse("%s/%s" % (self.data, dest)) |
|---|
| 317 | except lxml.etree.XMLSyntaxError: |
|---|
| 318 | self.logger.error('Failed to parse %s' % (dest)) |
|---|
| 319 | return |
|---|
| 320 | included = [ent.get('href') for ent in \ |
|---|
| 321 | xdata.findall('./{http://www.w3.org/2001/XInclude}include')] |
|---|
| 322 | xdata_original = copy.deepcopy(xdata) |
|---|
| 323 | if included: |
|---|
| 324 | for name in included: |
|---|
| 325 | if name not in self.extra[dest]: |
|---|
| 326 | self.core.fam.AddMonitor("%s/%s" % (self.data, name), self) |
|---|
| 327 | self.extra[dest].append(name) |
|---|
| 328 | try: |
|---|
| 329 | xdata.xinclude() |
|---|
| 330 | except lxml.etree.XIncludeError: |
|---|
| 331 | self.logger.error("Failed to process XInclude for file %s" % dest) |
|---|
| 332 | |
|---|
| 333 | if dest == 'clients.xml': |
|---|
| 334 | self.clients = {} |
|---|
| 335 | self.aliases = {} |
|---|
| 336 | self.raliases = {} |
|---|
| 337 | self.bad_clients = {} |
|---|
| 338 | self.secure = [] |
|---|
| 339 | self.floating = [] |
|---|
| 340 | self.addresses = {} |
|---|
| 341 | self.raddresses = {} |
|---|
| 342 | self.clientdata_original = xdata_original |
|---|
| 343 | self.clientdata = xdata |
|---|
| 344 | for client in xdata.findall('.//Client'): |
|---|
| 345 | clname = client.get('name').lower() |
|---|
| 346 | if 'address' in client.attrib: |
|---|
| 347 | caddr = client.get('address') |
|---|
| 348 | if caddr in self.addresses: |
|---|
| 349 | self.addresses[caddr].append(clname) |
|---|
| 350 | else: |
|---|
| 351 | self.addresses[caddr] = [clname] |
|---|
| 352 | if clname not in self.raddresses: |
|---|
| 353 | self.raddresses[clname] = set() |
|---|
| 354 | self.raddresses[clname].add(caddr) |
|---|
| 355 | if 'auth' in client.attrib: |
|---|
| 356 | self.auth[client.get('name')] = client.get('auth', |
|---|
| 357 | 'cert+password') |
|---|
| 358 | if 'uuid' in client.attrib: |
|---|
| 359 | self.uuid[client.get('uuid')] = clname |
|---|
| 360 | if client.get('secure', 'false') == 'true': |
|---|
| 361 | self.secure.append(clname) |
|---|
| 362 | if client.get('location', 'fixed') == 'floating': |
|---|
| 363 | self.floating.append(clname) |
|---|
| 364 | if 'password' in client.attrib: |
|---|
| 365 | self.passwords[clname] = client.get('password') |
|---|
| 366 | for alias in [alias for alias in client.findall('Alias')\ |
|---|
| 367 | if 'address' in alias.attrib]: |
|---|
| 368 | if alias.get('address') in self.addresses: |
|---|
| 369 | self.addresses[alias.get('address')].append(clname) |
|---|
| 370 | else: |
|---|
| 371 | self.addresses[alias.get('address')] = [clname] |
|---|
| 372 | if clname not in self.raddresses: |
|---|
| 373 | self.raddresses[clname] = set() |
|---|
| 374 | self.raddresses[clname].add(alias.get('address')) |
|---|
| 375 | self.clients.update({clname: client.get('profile')}) |
|---|
| 376 | [self.aliases.update({alias.get('name'): clname}) \ |
|---|
| 377 | for alias in client.findall('Alias')] |
|---|
| 378 | self.raliases[clname] = set() |
|---|
| 379 | [self.raliases[clname].add(alias.get('name')) for alias \ |
|---|
| 380 | in client.findall('Alias')] |
|---|
| 381 | elif dest == 'groups.xml': |
|---|
| 382 | self.public = [] |
|---|
| 383 | self.private = [] |
|---|
| 384 | self.profiles = [] |
|---|
| 385 | self.groups = {} |
|---|
| 386 | grouptmp = {} |
|---|
| 387 | self.categories = {} |
|---|
| 388 | for group in xdata.xpath('//Groups/Group') \ |
|---|
| 389 | + xdata.xpath('Group'): |
|---|
| 390 | grouptmp[group.get('name')] = tuple([[item.get('name') for item in group.findall(spec)] |
|---|
| 391 | for spec in ['./Bundle', './Group']]) |
|---|
| 392 | grouptmp[group.get('name')][1].append(group.get('name')) |
|---|
| 393 | if group.get('default', 'false') == 'true': |
|---|
| 394 | self.default = group.get('name') |
|---|
| 395 | if group.get('profile', 'false') == 'true': |
|---|
| 396 | self.profiles.append(group.get('name')) |
|---|
| 397 | if group.get('public', 'false') == 'true': |
|---|
| 398 | self.public.append(group.get('name')) |
|---|
| 399 | elif group.get('public', 'true') == 'false': |
|---|
| 400 | self.private.append(group.get('name')) |
|---|
| 401 | if 'category' in group.attrib: |
|---|
| 402 | self.categories[group.get('name')] = group.get('category') |
|---|
| 403 | for group in grouptmp: |
|---|
| 404 | # self.groups[group] => (bundles, groups, categories) |
|---|
| 405 | self.groups[group] = (set(), set(), {}) |
|---|
| 406 | tocheck = [group] |
|---|
| 407 | group_cat = self.groups[group][2] |
|---|
| 408 | while tocheck: |
|---|
| 409 | now = tocheck.pop() |
|---|
| 410 | self.groups[group][1].add(now) |
|---|
| 411 | if now in grouptmp: |
|---|
| 412 | (bundles, groups) = grouptmp[now] |
|---|
| 413 | for ggg in [ggg for ggg in groups if ggg not in self.groups[group][1]]: |
|---|
| 414 | if ggg not in self.categories or \ |
|---|
| 415 | self.categories[ggg] not in self.groups[group][2]: |
|---|
| 416 | self.groups[group][1].add(ggg) |
|---|
| 417 | tocheck.append(ggg) |
|---|
| 418 | if ggg in self.categories: |
|---|
| 419 | group_cat[self.categories[ggg]] = ggg |
|---|
| 420 | elif ggg in self.categories: |
|---|
| 421 | self.logger.info("Group %s: %s cat-suppressed %s" % \ |
|---|
| 422 | (group, |
|---|
| 423 | group_cat[self.categories[ggg]], |
|---|
| 424 | ggg)) |
|---|
| 425 | [self.groups[group][0].add(bund) for bund in bundles] |
|---|
| 426 | self.states[dest] = True |
|---|
| 427 | if False not in self.states.values(): |
|---|
| 428 | # check that all client groups are real and complete |
|---|
| 429 | real = self.groups.keys() |
|---|
| 430 | for client in self.clients.keys(): |
|---|
| 431 | if self.clients[client] not in self.profiles: |
|---|
| 432 | self.logger.error("Client %s set as nonexistent or incomplete group %s" \ |
|---|
| 433 | % (client, self.clients[client])) |
|---|
| 434 | self.logger.error("Removing client mapping for %s" % (client)) |
|---|
| 435 | self.bad_clients[client] = self.clients[client] |
|---|
| 436 | del self.clients[client] |
|---|
| 437 | for bclient in self.bad_clients.keys(): |
|---|
| 438 | if self.bad_clients[bclient] in self.profiles: |
|---|
| 439 | self.logger.info("Restored profile mapping for client %s" % bclient) |
|---|
| 440 | self.clients[bclient] = self.bad_clients[bclient] |
|---|
| 441 | del self.bad_clients[bclient] |
|---|
| 442 | |
|---|
| 443 | def set_profile(self, client, profile, addresspair): |
|---|
| 444 | '''Set group parameter for provided client''' |
|---|
| 445 | self.logger.info("Asserting client %s profile to %s" % (client, profile)) |
|---|
| 446 | if False in self.states.values(): |
|---|
| 447 | raise MetadataRuntimeError |
|---|
| 448 | if profile not in self.public: |
|---|
| 449 | self.logger.error("Failed to set client %s to private group %s" % (client, profile)) |
|---|
| 450 | raise MetadataConsistencyError |
|---|
| 451 | if client in self.clients: |
|---|
| 452 | self.logger.info("Changing %s group from %s to %s" % (client, self.clients[client], profile)) |
|---|
| 453 | cli = self.clientdata_original.xpath('.//Client[@name="%s"]' % (client)) |
|---|
| 454 | cli[0].set('profile', profile) |
|---|
| 455 | else: |
|---|
| 456 | self.logger.info("Creating new client: %s, profile %s" % \ |
|---|
| 457 | (client, profile)) |
|---|
| 458 | if addresspair in self.session_cache: |
|---|
| 459 | # we are working with a uuid'd client |
|---|
| 460 | lxml.etree.SubElement(self.clientdata_original.getroot(), |
|---|
| 461 | 'Client', name=client, |
|---|
| 462 | uuid=client, profile=profile, |
|---|
| 463 | address=addresspair[0]) |
|---|
| 464 | else: |
|---|
| 465 | lxml.etree.SubElement(self.clientdata_original.getroot(), |
|---|
| 466 | 'Client', name=client, |
|---|
| 467 | profile=profile) |
|---|
| 468 | self.clients[client] = profile |
|---|
| 469 | self.write_back_clients() |
|---|
| 470 | |
|---|
| 471 | def write_back_clients(self): |
|---|
| 472 | '''Write changes to client.xml back to disk''' |
|---|
| 473 | try: |
|---|
| 474 | datafile = open("%s/%s" % (self.data, 'clients.xml'), 'w') |
|---|
| 475 | except IOError: |
|---|
| 476 | self.logger.error("Failed to write clients.xml") |
|---|
| 477 | raise MetadataRuntimeError |
|---|
| 478 | fd = datafile.fileno() |
|---|
| 479 | while self.locked(fd) == True: |
|---|
| 480 | pass |
|---|
| 481 | dataroot = self.clientdata_original.getroot() |
|---|
| 482 | if hasattr(dataroot, 'iter'): |
|---|
| 483 | items = dataroot.iter() |
|---|
| 484 | else: |
|---|
| 485 | items = dataroot.getchildren() |
|---|
| 486 | for item in items: |
|---|
| 487 | # no items have text data of any sort |
|---|
| 488 | item.tail = None |
|---|
| 489 | item.text = None |
|---|
| 490 | datafile.write(lxml.etree.tostring(dataroot, pretty_print=True)) |
|---|
| 491 | fcntl.lockf(fd, fcntl.LOCK_UN) |
|---|
| 492 | datafile.close() |
|---|
| 493 | |
|---|
| 494 | def locked(self, fd): |
|---|
| 495 | try: |
|---|
| 496 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|---|
| 497 | except IOError: |
|---|
| 498 | return True |
|---|
| 499 | return False |
|---|
| 500 | |
|---|
| 501 | def resolve_client(self, addresspair): |
|---|
| 502 | '''Lookup address locally or in DNS to get a hostname''' |
|---|
| 503 | #print self.session_cache |
|---|
| 504 | if addresspair in self.session_cache: |
|---|
| 505 | (stamp, uuid) = self.session_cache[addresspair] |
|---|
| 506 | if time.time() - stamp < 60: |
|---|
| 507 | return self.uuid[uuid] |
|---|
| 508 | address = addresspair[0] |
|---|
| 509 | if address in self.addresses: |
|---|
| 510 | if len(self.addresses[address]) != 1: |
|---|
| 511 | self.logger.error("Address %s has multiple reverse assignments; a uuid must be used" % (address)) |
|---|
| 512 | raise MetadataConsistencyError |
|---|
| 513 | return self.addresses[address][0] |
|---|
| 514 | try: |
|---|
| 515 | cname = socket.gethostbyaddr(address)[0].lower() |
|---|
| 516 | if cname in self.aliases: |
|---|
| 517 | return self.aliases[cname] |
|---|
| 518 | return cname |
|---|
| 519 | except socket.herror: |
|---|
| 520 | warning = "address resolution error for %s" % (address) |
|---|
| 521 | self.logger.warning(warning) |
|---|
| 522 | raise MetadataConsistencyError |
|---|
| 523 | |
|---|
| 524 | def get_initial_metadata(self, client): |
|---|
| 525 | '''Return the metadata for a given client''' |
|---|
| 526 | client = client.lower() |
|---|
| 527 | if client in self.aliases: |
|---|
| 528 | client = self.aliases[client] |
|---|
| 529 | if client in self.clients: |
|---|
| 530 | profile = self.clients[client] |
|---|
| 531 | (bundles, groups, categories) = self.groups[profile] |
|---|
| 532 | else: |
|---|
| 533 | if self.default == None: |
|---|
| 534 | self.logger.error("Cannot set group for client %s; no default group set" % (client)) |
|---|
| 535 | raise MetadataConsistencyError |
|---|
| 536 | self.set_profile(client, self.default, (None, None)) |
|---|
| 537 | profile = self.default |
|---|
| 538 | [bundles, groups, categories] = self.groups[self.default] |
|---|
| 539 | aliases = self.raliases.get(client, set()) |
|---|
| 540 | addresses = self.raddresses.get(client, set()) |
|---|
| 541 | newgroups = set(groups) |
|---|
| 542 | newbundles = set(bundles) |
|---|
| 543 | newcategories = {} |
|---|
| 544 | newcategories.update(categories) |
|---|
| 545 | if client in self.passwords: |
|---|
| 546 | password = self.passwords[client] |
|---|
| 547 | else: |
|---|
| 548 | password = None |
|---|
| 549 | uuids = [item for item, value in self.uuid.iteritems() if value == client] |
|---|
| 550 | if uuids: |
|---|
| 551 | uuid = uuids[0] |
|---|
| 552 | else: |
|---|
| 553 | uuid = None |
|---|
| 554 | for group in self.cgroups.get(client, []): |
|---|
| 555 | if group in self.groups: |
|---|
| 556 | nbundles, ngroups, ncategories = self.groups[group] |
|---|
| 557 | else: |
|---|
| 558 | nbundles, ngroups, ncategories = ([], [group], {}) |
|---|
| 559 | [newbundles.add(b) for b in nbundles if b not in newbundles] |
|---|
| 560 | [newgroups.add(g) for g in ngroups if g not in newgroups] |
|---|
| 561 | newcategories.update(ncategories) |
|---|
| 562 | return ClientMetadata(client, profile, newgroups, newbundles, aliases, |
|---|
| 563 | addresses, newcategories, uuid, password, self.query) |
|---|
| 564 | |
|---|
| 565 | def get_all_group_names(self): |
|---|
| 566 | all_groups = set() |
|---|
| 567 | [all_groups.update(g[1]) for g in self.groups.values()] |
|---|
| 568 | return all_groups |
|---|
| 569 | |
|---|
| 570 | def get_client_names_by_profiles(self, profiles): |
|---|
| 571 | return [client for client, profile in self.clients.iteritems() \ |
|---|
| 572 | if profile in profiles] |
|---|
| 573 | |
|---|
| 574 | def get_client_names_by_groups(self, groups): |
|---|
| 575 | gprofiles = [profile for profile in self.profiles if \ |
|---|
| 576 | self.groups[profile][1].issuperset(groups)] |
|---|
| 577 | return self.get_client_names_by_profiles(gprofiles) |
|---|
| 578 | |
|---|
| 579 | def merge_additional_groups(self, imd, groups): |
|---|
| 580 | for group in groups: |
|---|
| 581 | if group in self.categories and \ |
|---|
| 582 | self.categories[group] in imd.categories: |
|---|
| 583 | continue |
|---|
| 584 | nb, ng, _ = self.groups.get(group, (list(), [group], dict())) |
|---|
| 585 | for b in nb: |
|---|
| 586 | if b not in imd.bundles: |
|---|
| 587 | imd.bundles.add(b) |
|---|
| 588 | for g in ng: |
|---|
| 589 | if g not in imd.groups: |
|---|
| 590 | if g in self.categories and \ |
|---|
| 591 | self.categories[g] in imd.categories: |
|---|
| 592 | continue |
|---|
| 593 | if g in self.private: |
|---|
| 594 | self.logger.error("Refusing to add dynamic membership in private group %s for client %s" % (g, imd.hostname)) |
|---|
| 595 | continue |
|---|
| 596 | imd.groups.add(g) |
|---|
| 597 | |
|---|
| 598 | def merge_additional_data(self, imd, source, data): |
|---|
| 599 | if not hasattr(imd, source): |
|---|
| 600 | setattr(imd, source, data) |
|---|
| 601 | imd.connectors.append(source) |
|---|
| 602 | |
|---|
| 603 | def validate_client_address(self, client, addresspair): |
|---|
| 604 | '''Check address against client''' |
|---|
| 605 | address = addresspair[0] |
|---|
| 606 | if client in self.floating: |
|---|
| 607 | self.debug_log("Client %s is floating" % client) |
|---|
| 608 | return True |
|---|
| 609 | if address in self.addresses: |
|---|
| 610 | if client in self.addresses[address]: |
|---|
| 611 | self.debug_log("Client %s matches address %s" % (client, address)) |
|---|
| 612 | return True |
|---|
| 613 | else: |
|---|
| 614 | self.logger.error("Got request for non-float client %s from %s" \ |
|---|
| 615 | % (client, address)) |
|---|
| 616 | return False |
|---|
| 617 | resolved = self.resolve_client(addresspair) |
|---|
| 618 | if resolved == client: |
|---|
| 619 | return True |
|---|
| 620 | else: |
|---|
| 621 | self.logger.error("Got request for %s from incorrect address %s" \ |
|---|
| 622 | % (client, address)) |
|---|
| 623 | self.logger.error("Resolved to %s" % resolved) |
|---|
| 624 | return False |
|---|
| 625 | |
|---|
| 626 | def AuthenticateConnection(self, cert, user, password, address): |
|---|
| 627 | '''This function checks auth creds''' |
|---|
| 628 | if cert: |
|---|
| 629 | id_method = 'cert' |
|---|
| 630 | certinfo = dict([x[0] for x in cert['subject']]) |
|---|
| 631 | # look at cert.cN |
|---|
| 632 | client = certinfo['commonName'] |
|---|
| 633 | self.debug_log("Got cN %s; using as client name" % client) |
|---|
| 634 | auth_type = self.auth.get(client, 'cert+password') |
|---|
| 635 | elif user == 'root': |
|---|
| 636 | id_method = 'address' |
|---|
| 637 | try: |
|---|
| 638 | client = self.resolve_client(address) |
|---|
| 639 | except MetadataConsistencyError: |
|---|
| 640 | self.logger.error("Client %s failed to resolve; metadata problem" % (address[0])) |
|---|
| 641 | return False |
|---|
| 642 | else: |
|---|
| 643 | id_method = 'uuid' |
|---|
| 644 | # user maps to client |
|---|
| 645 | if user not in self.uuid: |
|---|
| 646 | client = user |
|---|
| 647 | self.uuid[user] = user |
|---|
| 648 | else: |
|---|
| 649 | client = self.uuid[user] |
|---|
| 650 | |
|---|
| 651 | # we have the client name |
|---|
| 652 | self.debug_log("Authenticating client %s" % client) |
|---|
| 653 | |
|---|
| 654 | # next we validate the address |
|---|
| 655 | if id_method == 'uuid': |
|---|
| 656 | addr_is_valid = True |
|---|
| 657 | else: |
|---|
| 658 | addr_is_valid = self.validate_client_address(client, address) |
|---|
| 659 | |
|---|
| 660 | if not addr_is_valid: |
|---|
| 661 | return False |
|---|
| 662 | |
|---|
| 663 | if id_method == 'cert' and auth_type != 'cert+password': |
|---|
| 664 | # we are done if cert+password not required |
|---|
| 665 | return True |
|---|
| 666 | |
|---|
| 667 | if client not in self.passwords: |
|---|
| 668 | if client in self.secure: |
|---|
| 669 | self.logger.error("Client %s in secure mode but has no password" % (address[0])) |
|---|
| 670 | return False |
|---|
| 671 | if password != self.password: |
|---|
| 672 | self.logger.error("Client %s used incorrect global password" % (address[0])) |
|---|
| 673 | return False |
|---|
| 674 | if client not in self.secure: |
|---|
| 675 | if client in self.passwords: |
|---|
| 676 | plist = [self.password, self.passwords[client]] |
|---|
| 677 | else: |
|---|
| 678 | plist = [self.password] |
|---|
| 679 | if password not in plist: |
|---|
| 680 | self.logger.error("Client %s failed to use either allowed password" % \ |
|---|
| 681 | (address[0])) |
|---|
| 682 | return False |
|---|
| 683 | else: |
|---|
| 684 | # client in secure mode and has a client password |
|---|
| 685 | if password != self.passwords[client]: |
|---|
| 686 | self.logger.error("Client %s failed to use client password in secure mode" % \ |
|---|
| 687 | (address[0])) |
|---|
| 688 | return False |
|---|
| 689 | |
|---|
| 690 | if id_method != 'address': |
|---|
| 691 | # populate the session cache |
|---|
| 692 | self.session_cache[address] = (time.time(), user) |
|---|
| 693 | return True |
|---|
| 694 | |
|---|
| 695 | def process_statistics(self, meta, _): |
|---|
| 696 | '''Hook into statistics interface to toggle clients in bootstrap mode''' |
|---|
| 697 | client = meta.hostname |
|---|
| 698 | if client in self.auth and self.auth[client] == 'bootstrap': |
|---|
| 699 | self.logger.info("Asserting client %s auth mode to cert" % client) |
|---|
| 700 | cli = self.clientdata_original.xpath('.//Client[@name="%s"]' \ |
|---|
| 701 | % (client)) |
|---|
| 702 | cli[0].set('auth', 'cert') |
|---|
| 703 | self.write_back_clients() |
|---|
| 704 | |
|---|
| 705 | def viz(self, hosts, bundles, key, colors): |
|---|
| 706 | '''admin mode viz support''' |
|---|
| 707 | groups_tree = lxml.etree.parse(self.data + "/groups.xml") |
|---|
| 708 | groups = groups_tree.getroot() |
|---|
| 709 | categories = {'default':'grey83'} |
|---|
| 710 | instances = {} |
|---|
| 711 | viz_str = "" |
|---|
| 712 | egroups = groups.findall("Group") + groups.findall('.//Groups/Group') |
|---|
| 713 | for group in egroups: |
|---|
| 714 | if not group.get('category') in categories: |
|---|
| 715 | categories[group.get('category')] = colors.pop() |
|---|
| 716 | group.set('color', categories[group.get('category')]) |
|---|
| 717 | if None in categories: |
|---|
| 718 | del categories[None] |
|---|
| 719 | if hosts: |
|---|
| 720 | clients = self.clients |
|---|
| 721 | for client, profile in clients.iteritems(): |
|---|
| 722 | if profile in instances: |
|---|
| 723 | instances[profile].append(client) |
|---|
| 724 | else: |
|---|
| 725 | instances[profile] = [client] |
|---|
| 726 | for profile, clist in instances.iteritems(): |
|---|
| 727 | clist.sort() |
|---|
| 728 | viz_str += '''\t"%s-instances" [ label="%s", shape="record" ];\n''' \ |
|---|
| 729 | % (profile, '|'.join(clist)) |
|---|
| 730 | viz_str += '''\t"%s-instances" -> "group-%s";\n''' \ |
|---|
| 731 | % (profile, profile) |
|---|
| 732 | if bundles: |
|---|
| 733 | bundles = [] |
|---|
| 734 | [bundles.append(bund.get('name')) \ |
|---|
| 735 | for bund in groups.findall('.//Bundle') \ |
|---|
| 736 | if bund.get('name') not in bundles] |
|---|
| 737 | bundles.sort() |
|---|
| 738 | for bundle in bundles: |
|---|
| 739 | viz_str += '''\t"bundle-%s" [ label="%s", shape="septagon"];\n''' \ |
|---|
| 740 | % (bundle, bundle) |
|---|
| 741 | gseen = [] |
|---|
| 742 | for group in egroups: |
|---|
| 743 | if group.get('profile', 'false') == 'true': |
|---|
| 744 | style = "filled, bold" |
|---|
| 745 | else: |
|---|
| 746 | style = "filled" |
|---|
| 747 | gseen.append(group.get('name')) |
|---|
| 748 | viz_str += '\t"group-%s" [label="%s", style="%s", fillcolor=%s];\n' % \ |
|---|
| 749 | (group.get('name'), group.get('name'), style, group.get('color')) |
|---|
| 750 | if bundles: |
|---|
| 751 | for bundle in group.findall('Bundle'): |
|---|
| 752 | viz_str += '\t"group-%s" -> "bundle-%s";\n' % \ |
|---|
| 753 | (group.get('name'), bundle.get('name')) |
|---|
| 754 | gfmt = '\t"group-%s" [label="%s", style="filled", fillcolor="grey83"];\n' |
|---|
| 755 | for group in egroups: |
|---|
| 756 | for parent in group.findall('Group'): |
|---|
| 757 | if parent.get('name') not in gseen: |
|---|
| 758 | viz_str += gfmt % (parent.get('name'), parent.get('name')) |
|---|
| 759 | gseen.append(parent.get("name")) |
|---|
| 760 | viz_str += '\t"group-%s" -> "group-%s" ;\n' % \ |
|---|
| 761 | (group.get('name'), parent.get('name')) |
|---|
| 762 | if key: |
|---|
| 763 | for category in categories: |
|---|
| 764 | viz_str += '''\t"''' + category + '''" [label="''' + category + \ |
|---|
| 765 | '''", shape="record", style="filled", fillcolor=''' + \ |
|---|
| 766 | categories[category] + '''];\n''' |
|---|
| 767 | return viz_str |
|---|