Package sabayon :: Module systemdb
[hide private]
[frames] | no frames]

Source Code for Module sabayon.systemdb

  1  # 
  2  # Copyright (C) 2005 Red Hat, Inc. 
  3  # 
  4  # This program is free software; you can redistribute it and/or modify 
  5  # it under the terms of the GNU General Public License as published by 
  6  # the Free Software Foundation; either version 2 of the License, or 
  7  # (at your option) any later version. 
  8  # 
  9  # This program is distributed in the hope that it will be useful, 
 10  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 12  # GNU General Public License for more details. 
 13  # 
 14  # You should have received a copy of the GNU General Public License 
 15  # along with this program; if not, write to the Free Software 
 16  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 17  # 
 18   
 19  import sys 
 20  import string 
 21  import pwd 
 22  import grp 
 23  import os 
 24  import libxml2 
 25  import config 
 26  import util 
 27  import cache 
 28  import random 
 29  import socket 
 30  import debuglog 
 31   
 32  # 
 33  # LDAP should be a soft dependency. 
 34  # 
 35   
 36  try: 
 37      import ldap 
 38      has_ldap = True; 
 39  except ImportError: 
 40      has_ldap = False; 
 41   
 42  # 
 43  # selinux should be a soft dependency. 
 44  # 
 45   
 46  try: 
 47      import selinux 
 48      has_selinux = True; 
 49  except ImportError: 
 50      has_selinux = False; 
 51   
 52  # 
 53  # Default empty config. 
 54  # 
 55  defaultConf="""<profiles> 
 56    <default profile=""/> 
 57  </profiles>""" 
 58   
 59  # make sure to initialize the cache first 
 60  # this will make sure we can handle disconnection 
 61  # and initialize libxml2 environment 
 62  cache.initialize() 
 63   
64 -def dprint (fmt, *args):
65 debuglog.debug_log (False, debuglog.DEBUG_LOG_DOMAIN_USER_DB, fmt % args)
66
67 -def get_setting (node, setting, default = None, convert_to = str):
68 a = node.hasProp(setting) 69 if a: 70 try: 71 return convert_to (a.content) 72 except: 73 np = node.nodePath() 74 # Translators: You may move the "%(setting)s" and "%(np)s" items as you wish, but 75 # do not change the way they are written. The intended string is 76 # something like "invalid type for setting blah in /ldap/path/to/blah" 77 raise SystemDatabaseException(_("invalid type for setting %(setting)s in %(np)s") % { "setting": setting, 78 "np": np }) 79 return default
80
81 -def expand_string (string, attrs):
82 res = "" 83 i = 0 84 while i < len(string): 85 c = string[i] 86 i = i + 1 87 if c == "%": 88 if i < len(string): 89 c = string[i] 90 if c != "%": 91 if c in attrs: 92 c = attrs[c] 93 i = i + 1 94 res = res + c 95 return res
96 97
98 -class SystemDatabaseException (Exception):
99 pass
100
101 -class SystemDatabase(object):
102 """An encapsulation of the database which maintains an 103 association between users and profiles. 104 105 This database is stored by default in 106 $(sysconfdir)/sabayon/profiles/users.xml and contains a 107 list of users and the profile associated with each of 108 those users. 109 110 A profile can be reference by either its name (in which case 111 the profile is stored at /etc/sabayon/profiles/$(name).zip), 112 an absolute path or a http/file URL. 113 """
114 - def __init__ (self, db_file = None):
115 """Create a SystemDatabase object. 116 117 @db_file: a mandatory path which specifes the location 118 of the database file. If only a file is specified, the 119 directory /etc/sabayon is used. 120 """ 121 if db_file is None: 122 raise SystemDatabaseException(_("No database file provided")) 123 elif db_file[0] != '/': 124 file = os.path.join (config.CONFIGDIR, db_file) 125 else: 126 file = db_file 127 self.file = file 128 self.xmlquery = None 129 self.nodes = None # nodes from the XML file for LDAP usage. 130 self.modified = 0 131 dprint("New UserDatabase(%s) object\n" % self.file) 132 133 try: 134 self.doc = libxml2.readFile(file, None, libxml2.XML_PARSE_NOBLANKS) 135 # Process XInclude statements 136 self.doc.xincludeProcess() 137 except: 138 # TODO add fallback to last good database 139 dprint("failed to parse %s falling back to default conf\n" % 140 self.file) 141 self.doc = None 142 if self.doc == None: 143 self.doc = libxml2.readMemory(defaultConf, len(defaultConf), 144 None, None, 145 libxml2.XML_PARSE_NOBLANKS)
146
147 - def __del__ (self):
148 if self.doc != None: 149 self.doc.freeDoc()
150
151 - def __profile_name_to_location (self, profile, node):
152 if not profile: 153 return None 154 155 uri = self.__ldap_query ("locationmap", {"p":profile, "h":socket.getfqdn()}) 156 if uri: 157 return uri 158 159 # do the necessary URI escaping of the profile name if needed 160 orig_profile = profile 161 try: 162 tmp = parseURI(profile) 163 except: 164 profile = libxml2.URIEscapeStr(profile, "/:") 165 166 # if there is a base on the node, then use 167 if node != None: 168 try: 169 base = node.getBase(None) 170 if base != None and base != "" and \ 171 base != self.file: 172 # URI composition from the base 173 ret = libxml2.buildURI(profile, base) 174 if ret[0] == '/': 175 ret = libxml2.URIUnescapeString(ret, len(ret), None) 176 dprint("Converted profile name '%s' to location '%s'\n", 177 orig_profile, ret) 178 return ret 179 except: 180 pass 181 try: 182 uri = libxml2.parseURI(profile); 183 if uri.scheme() is None: 184 # it is a file path 185 if profile[0] != '/': 186 profile = os.path.join (config.PROFILESDIR, profile) 187 if profile[-4:] != ".zip": 188 profile = profile + ".zip" 189 else: 190 # TODO need to make a local copy or use the local copy 191 profile = profile 192 except: 193 # we really expect an URI there 194 profile = None 195 196 if profile[0] == '/': 197 profile = libxml2.URIUnescapeString(profile, len(profile), None) 198 dprint("Converted profile name '%s' to location '%s'\n", 199 orig_profile, profile) 200 return profile
201
202 - def __open_ldap (self):
203 ldap_node = self.nodes[0] 204 205 # 206 # Get a list of uri's from the "uri" setting on the ldap node. 207 # 208 uris = get_setting (ldap_node, "uri", "ldap://localhost") 209 210 # 211 # Could be a comma separated list. 212 # 213 urilist = uris.split(',') 214 215 for uri in urilist: 216 try: 217 l = ldap.initialize (uri) 218 219 l.protocol_version = get_setting (ldap_node, "version", ldap.VERSION3, int) 220 l.timeout = get_setting (ldap_node, "timeout", 10, int) 221 222 bind_dn = get_setting (ldap_node, "bind_dn", "") 223 bind_pw = get_setting (ldap_node, "bind_pw", "") 224 # Bind no matter what, so we know the server's there. 225 l.simple_bind (bind_dn, bind_pw) 226 227 return l 228 except ldap.LDAPError, error_message: 229 dprint("Couldn't bind to %s: %s\n", uri, error_message)
230
231 - def __ldap_query (self, map, replace):
232 global has_ldap 233 if not has_ldap: 234 return None 235 if not self.nodes: 236 self.nodes = self.doc.xpathEval ("/profiles/ldap/" + map) 237 if len (self.nodes) == 0: 238 has_ldap = False # No LDAP nodes in the xml file. 239 return None 240 map_node = self.nodes[0] 241 242 l = self.__open_ldap () 243 if not l: 244 return None 245 246 search_base = get_setting (map_node, "search_base") 247 query_filter = get_setting (map_node, "query_filter") 248 result_attribute = get_setting (map_node, "result_attribute") 249 scope = get_setting (map_node, "scope", "sub") 250 multiple_result = get_setting (map_node, "multiple_result", "first") 251 252 if search_base == None: 253 raise SystemDatabaseException(_("No LDAP search base specified for %s" % map)) 254 255 if query_filter == None: 256 raise SystemDatabaseException(_("No LDAP query filter specified for %s" % map)) 257 258 if result_attribute == None: 259 raise SystemDatabaseException(_("No LDAP result attribute specified for %s" % map)) 260 261 if scope == "sub": 262 scope = ldap.SCOPE_SUBTREE 263 elif scope == "base": 264 scope = ldap.SCOPE_BASE 265 elif scope == "one": 266 scope = ldap.SCOPE_ONELEVEL 267 else: 268 raise SystemDatabaseException(_("LDAP Scope must be one of: ") + "sub, base, one") 269 270 query_filter = expand_string (query_filter, replace) 271 search_base = expand_string (search_base, replace) 272 273 results = l.search_s (search_base, scope, query_filter, [result_attribute]) 274 275 if len (results) == 0: 276 return None 277 278 (dn, attrs) = results[0] 279 if not result_attribute in attrs: 280 return None 281 vals = attrs[result_attribute] 282 283 if multiple_result == "first": 284 val = vals[0] 285 elif multiple_result == "random": 286 val = vals[random.randint(0, len(vals)-1)] 287 else: 288 raise SystemDatabaseException(_("multiple_result must be one of: ") + "first, random") 289 290 l.unbind () 291 292 return val
293 294
295 - def get_default_profile (self, profile_location = True):
296 """Look up the default profile. 297 298 @profile_location: whether the profile location should 299 be returned 300 301 Return value: the location of the default profile, which 302 should be in a suitable form for constructing a ProfileStorage 303 object, or the default profile name if @profile_location is 304 False. 305 """ 306 default = None 307 try: 308 default = self.doc.xpathEval("/profiles/default")[0] 309 profile = default.prop("profile") 310 except: 311 profile = None 312 313 if not profile_location: 314 return profile 315 316 return self.__profile_name_to_location (profile, default)
317
318 - def gen_get_profile (self, searchterm, replace, profile_location = True, ignore_default = False):
319 """Look up the profile for a given searchterm. 320 321 @username: the user whose profile location should be 322 returned. 323 @profile_location: whether the profile location should 324 be returned 325 @ignore_default: don't use the default profile if 326 no profile is explicitly set. 327 328 Return value: the location of the profile, which 329 should be in a suitable form for constructing a 330 ProfileStorage object, or the profile name if 331 @profile_location is False. 332 """ 333 user = None 334 profile = self.__ldap_query ("profilemap", replace) 335 if not profile: 336 try: 337 query = self.xmlquery % searchterm 338 user = self.doc.xpathEval(query)[0] 339 profile = user.prop("profile") 340 except: 341 profile = None 342 if not profile and not ignore_default: 343 try: 344 query = "/profiles/default[1][@profile]" 345 user = self.doc.xpathEval(query)[0] 346 profile = user.prop("profile") 347 except: 348 profile = None 349 350 if not profile_location: 351 return profile 352 353 # TODO Check the resulting file path exists 354 return self.__profile_name_to_location (profile, user)
355
356 - def __save_as(self, filename = None):
357 global has_selinux 358 """Save the current version to the given filename""" 359 if filename == None: 360 filename = self.file 361 362 dprint("Saving UserDatabase to %s\n", filename) 363 try: 364 os.rename(filename, filename + ".bak") 365 backup = 1 366 except: 367 backup = 0 368 pass 369 370 try: 371 f = open(filename, 'w') 372 except: 373 if backup == 1: 374 try: 375 os.rename(filename + ".bak", filename) 376 dprint("Restore from %s.bak\n", filename) 377 except: 378 dprint("Failed to restore from %s.bak\n", filename) 379 380 raise SystemDatabaseException( 381 _("Could not open %s for writing") % filename) 382 try: 383 f.write(self.doc.serialize("UTF-8", format=1)) 384 f.close() 385 except: 386 if backup == 1: 387 try: 388 os.rename(filename + ".bak", filename) 389 dprint("Restore from %s.bak\n", filename) 390 except: 391 dprint("Failed to restore from %s.bak\n", filename) 392 393 raise SystemDatabaseException( 394 _("Failed to save UserDatabase to %s") % filename) 395 396 if has_selinux: 397 if selinux.is_selinux_enabled() > 0: 398 rc, con = selinux.matchpathcon(filename, 0) 399 if rc == 0: 400 selinux.setfilecon(filename, con) 401 402 self.modified = 0
403
404 - def set_default_profile (self, profile):
405 """Set the default profile to be used in this database. 406 407 @profile: the location of the profile. 408 """ 409 if profile is None: 410 profile = "" 411 self.modified = 0 412 try: 413 default = self.doc.xpathEval("/profiles/default")[0] 414 oldprofile = default.prop("profile") 415 if oldprofile != profile: 416 default.setProp("profile", profile) 417 self.modified = 1 418 except: 419 try: 420 profiles = self.doc.xpathEval("/profiles")[0] 421 except: 422 raise SystemDatabaseException( 423 _("File %s is not a profile configuration") % 424 (self.file)) 425 try: 426 default = profiles.newChild(None, "default", None) 427 default.setProp("profile", profile) 428 except: 429 raise SystemDatabaseException( 430 _("Failed to add default profile %s to configuration") % 431 (profile)) 432 self.modified = 1 433 if self.modified == 1: 434 self.__save_as()
435
436 - def gen_set_profile (self, searchterm, child, profile):
437 """Set the profile for a given searchterm. 438 439 @searchterm: the term whose profile location should be set. 440 @profile: the location of the profile. 441 """ 442 if profile is None: 443 profile = "" 444 self.modified = 0 445 try: 446 query = self.xmlquery % searchterm 447 user = self.doc.xpathEval(query)[0] 448 oldprofile = user.prop("profile") 449 if oldprofile != profile: 450 user.setProp("profile", profile) 451 self.modified = 1 452 except: 453 try: 454 profiles = self.doc.xpathEval("/profiles")[0] 455 except: 456 raise SystemDatabaseException( 457 _("File %s is not a profile configuration") % (self.file)) 458 try: 459 user = profiles.newChild(None, child, None) 460 user.setProp("name", searchterm) 461 user.setProp("profile", profile) 462 except: 463 raise SystemDatabaseException( 464 _("Failed to add user %s to profile configuration") % 465 (username)) 466 self.modified = 1 467 if self.modified == 1: 468 self.__save_as()
469
470 - def gen_is_sabayon_controlled (self, searchterm, replace):
471 """Return True if user's configuration was ever under Sabayon's 472 control. 473 """ 474 profile = self.__ldap_query ("profilemap", replace) 475 476 if profile: 477 return True 478 479 try: 480 query = self.xmlquery % searchterm 481 user = self.doc.xpathEval(query)[0] 482 except: 483 return False 484 485 if user: 486 return True 487 488 return False
489
490 - def get_profiles (self):
491 """Return the list of currently available profiles. 492 This is basically just list of zip files in 493 /etc/sabayon/profiles, each without the .zip extension. 494 """ 495 list = [] 496 try: 497 for file in os.listdir(config.PROFILESDIR): 498 if file[-4:] != ".zip": 499 continue 500 list.append(file[0:-4]) 501 except: 502 dprint("Failed to read directory(%s)\n" % (config.PROFILESDIR)) 503 # TODO: also list remote profiles as found in self.doc 504 return list
505
506 -class UserDatabase(SystemDatabase):
507 """Encapsulate a user mapping 508 """
509 - def __init__ (self, db_file = None):
510 if db_file is None: 511 SystemDatabase.__init__(self, "users.xml") 512 else: 513 SystemDatabase.__init__(self, db_file) 514 515 self.xmlquery = "/profiles/user[@name='%s']"
516
517 - def get_profile (self, username, profile_location = True, ignore_default = False):
518 return self.gen_get_profile(username, {"u":username, 519 "h":socket.getfqdn()}, profile_location, ignore_default)
520
521 - def is_sabayon_controlled (self, username):
522 return self.gen_is_sabayon_controlled(username, {"u":username, "h":socket.getfqdn()})
523
524 - def set_profile (self, username, profile):
525 return self.gen_set_profile (username, "user", profile)
526
527 - def get_users (self):
528 """Return the list of users on the system. These should 529 be real users - i.e. should not include system users 530 like nobody, gdm, nfsnobody etc. 531 """ 532 list = [] 533 try: 534 users = pwd.getpwall() 535 except: 536 raise SystemDatabaseException(_("Failed to get the user list")) 537 538 for user in pwd.getpwall(): 539 try: 540 # remove non-users 541 if user[2] < 500: 542 continue 543 if user[0] == "nobody": 544 continue 545 if user[0] in list: 546 continue 547 if user[6] == "" or string.find(user[6], "nologin") != -1: 548 continue 549 if user[0][len (user[0]) - 1] == "$": # Active Directory hosts end in "$"; we don't want to show those as users 550 continue 551 list.append(user[0]) 552 except: 553 pass 554 return list
555
556 -class GroupDatabase(SystemDatabase):
557 """Encapsulate a group mapping 558 """
559 - def __init__ (self, db_file = None):
560 if db_file is None: 561 SystemDatabase.__init__(self, "groups.xml") 562 else: 563 SystemDatabase.__init__(self, db_file) 564 565 self.xmlquery = "/profiles/group[@name='%s']"
566
567 - def get_profile (self, groupname, profile_location = True, ignore_default = False):
568 return self.gen_get_profile(groupname, {"g":groupname, 569 "h":socket.getfqdn()}, profile_location, ignore_default)
570
571 - def is_sabayon_controlled (self, groupname):
572 return self.gen_is_sabayon_controlled(groupname, {"g":groupname, "h":socket.getfqdn()})
573
574 - def set_profile (self, groupname, profile):
575 return self.gen_set_profile (groupname, "group", profile)
576
577 - def get_groups (self):
578 """Return the list of groups on the system. These should 579 be real groups - i.e. should not include system groups 580 like lp, udev, etc. 581 """ 582 list = [] 583 try: 584 groups = grp.getgrall() 585 except: 586 raise GroupDatabaseException(_("Failed to get the group list")) 587 588 for group in groups: 589 # remove non-groups 590 if group[2] < 500: 591 continue 592 if group[0] == "nogroup": 593 continue 594 if group[0] in list: 595 continue 596 # We don't want to include "user" primary groups 597 try: 598 user = pwd.getpwnam(group[0]) 599 except: 600 user = None 601 if user is not None and user[2] == group[2]: 602 continue 603 list.append(group[0]) 604 return list
605 606 user_database = None 607 group_database = None 608
609 -def get_user_database ():
610 """Return a UserDatabase singleton""" 611 global user_database 612 if user_database is None: 613 user_database = UserDatabase () 614 return user_database
615
616 -def get_group_database ():
617 """Return a UserDatabase singleton""" 618 global group_database 619 if group_database is None: 620 group_database = GroupDatabase () 621 return group_database
622 # 623 # Unit tests 624 # 625
626 -def run_unit_tests ():
627 testuserfile = "/tmp/test_users.xml" 628 testgroupfile = "/tmp/test_groups.xml" 629 try: 630 os.unlink(testuserfile) 631 os.unlink(testgroupfile) 632 except: 633 pass 634 db = UserDatabase(testuserfile) 635 db.set_default_profile("default") 636 res = db.get_profile("localuser", False) 637 assert not res is None 638 assert res == "default" 639 db.set_profile("localuser", "groupA") 640 res = db.get_profile("localuser") 641 assert not res is None 642 assert res[-20:] == "/profiles/groupA.zip" 643 db.set_profile("localuser", "groupB") 644 res = db.get_profile("localuser") 645 assert not res is None 646 assert res[-20:] == "/profiles/groupB.zip" 647 res = db.get_users() 648 print res 649 db = GroupDatabase(testgroupfile) 650 db.set_default_profile("default") 651 res = db.get_profile("localuser", False) 652 assert not res is None 653 assert res == "default" 654 db.set_profile("localgroup", "groupA") 655 res = db.get_profile("localgroup") 656 assert not res is None 657 assert res[-20:] == "/profiles/groupA.zip" 658 db.set_profile("localgroup", "groupB") 659 res = db.get_profile("localgroup") 660 assert not res is None 661 assert res[-20:] == "/profiles/groupB.zip" 662 res = db.get_groups() 663 print res
664 665 if __name__ == "__main__": 666 util.init_gettext () 667 run_unit_tests() 668