00001
00002 """
00003 When invoked as a script determines if the
00004 configuration named in the single argument exists.
00005
00006 Usage example::
00007
00008 python path/to/dbconf.py configname && echo configname exists || echo no configname
00009
00010 """
00011
00012 import os
00013 class DBConf(dict):
00014 """
00015 Reads a section of the Database configuration file,
00016 storing key/value pairs into this dict. The default file *path* is ``~/.my.cnf``
00017 which is formatted like::
00018
00019 [testdb]
00020 host = dybdb1.ihep.ac.cn
00021 database = testdb
00022 user = dayabay
00023 password = youknowoit
00024
00025 The standard python :py:mod:`ConfigParser` is used,
00026 which supports ``%(name)s`` style replacements in other values.
00027
00028 Usage example::
00029
00030 from DybPython import DBConf
00031 dbc = DBConf(sect="client", path="~/.my.cnf" )
00032 print dbc['host']
00033
00034 dbo = DBConf("offline_db")
00035 assert dbo['host'] == "dybdb1.ihep.ac.cn"
00036
00037 .. warning::
00038 As passwords are contained **DO NOT COMMIT** into any repository, and protect the file.
00039
00040 """
00041 defaults = {
00042 'path':"$SITEROOT/../.my.cnf:~/.my.cnf",
00043 'sect':"offline_db",
00044 'host':"%(host)s",
00045 'user':"%(user)s",
00046 'db':"%(database)s",
00047 'pswd':"%(password)s",
00048 'url':"mysql://%(host)s/%(database)s",
00049 'fix':None,
00050 'fixpass':None,
00051 'restrict':None,
00052 }
00053
00054 def Export(cls, sect=None , **extras ):
00055 """
00056 Exports the environment settings into environment of python process
00057 this is invoked by the C++ *DbiCascader* ctor
00058 """
00059 cnf = DBConf( sect=sect )
00060 if cnf.fix == None:
00061 cnf.export_to_env(**extras)
00062 else:
00063 from dbcas import DBCas
00064 cas = DBCas(cnf)
00065 tas = cas.spawn()
00066 cnf.export_to_env( supplier=tas )
00067 return cnf
00068
00069 Export = classmethod( Export )
00070
00071 def __init__(self, sect=None , path=None , user=None, pswd=None, url=None , host=None, db=None , fix=None, fixpass=None, restrict=None, verbose=False, secure=False, from_env=False , nodb=False ):
00072 """
00073
00074 See also :ref:`dbi:running` section of the Offline User Manual
00075
00076 Interpolates the DB connection parameter patterns gleaned
00077 from arguments, envvars or defaults (in that precedence order)
00078 into usable values using the context supplied by the
00079 *sect* section of the ini format config file at *path*
00080
00081 Optional keyword arguments:
00082
00083 ================ =======================================================
00084 Keyword Description
00085 ================ =======================================================
00086 *sect* section in config file
00087 *path* colon delimited list of paths to config file
00088
00089 *user* username
00090 *pswd* password
00091 *url* connection url
00092 *host* db host
00093 *db* db name
00094
00095 *fix* triggers fixture loading into temporary
00096 spawned cascade and specifies paths to fixture files
00097 for each member of the cascade (semi-colon delimited)
00098 *fixpass* skip the DB cascade dropping/creation that is
00099 normally done
00100 as part of cascade spawning (used in DBWriter/tests)
00101 *restrict* constrain the names of DB that can connect to
00102 starting with a string, eg ``tmp_`` as a safeguard
00103 *nodb* used to connect without specifying the database
00104 this requires greater access privileges and is used
00105 to perform database dropping/creation
00106 ================ =======================================================
00107
00108
00109 Correspondingly named envvars can also be used:
00110
00111 .. envvar:: DBCONF
00112 .. envvar:: DBCONF_PATH
00113
00114 .. envvar:: DBCONF_USER
00115 .. envvar:: DBCONF_PWSD
00116 .. envvar:: DBCONF_URL
00117 .. envvar:: DBCONF_HOST
00118 .. envvar:: DBCONF_DB
00119
00120 .. envvar:: DBCONF_FIX
00121 .. envvar:: DBCONF_FIXPASS
00122 .. envvar:: DBCONF_RESTRICT
00123
00124 The :envvar:`DBCONF` existance also triggers the
00125 :meth:`DybPython.dbconf.DBConf.Export` in :dybgaudi:`Database/DatabaseInterface/src/DbiCascader.cxx`
00126
00127 The :envvar:`DBCONF_PATH` is a colon delimited list of paths that are
00128 user (~) and $envvar OR ${envvar} expanded, some of the paths
00129 may not exist. When there are repeated settings in more than one
00130 file the last one wins.
00131
00132 In secure mode a single protected config file is required, the security
00133 comes with a high price in convenience
00134
00135 """
00136
00137 self.secure = secure
00138 self.verbose = verbose
00139
00140 esect = os.environ.get('DBCONF', None )
00141 if esect == "" or esect == None:
00142 esect = DBConf.defaults['sect']
00143
00144 sect = sect or esect
00145 path = path or os.environ.get('DBCONF_PATH', DBConf.defaults['path'] )
00146
00147 user = user or os.environ.get('DBCONF_USER', DBConf.defaults['user'] )
00148 pswd = pswd or os.environ.get('DBCONF_PSWD', DBConf.defaults['pswd'] )
00149 url = url or os.environ.get('DBCONF_URL', DBConf.defaults['url'] )
00150 host = host or os.environ.get('DBCONF_HOST', DBConf.defaults['host'] )
00151 db = db or os.environ.get('DBCONF_DB' , DBConf.defaults['db'] )
00152
00153 fix = fix or os.environ.get('DBCONF_FIX' , DBConf.defaults['fix'] )
00154 fixpass = fixpass or os.environ.get('DBCONF_FIXPASS' , DBConf.defaults['fixpass'] )
00155 restrict = restrict or os.environ.get('DBCONF_RESTRICT' , DBConf.defaults['restrict'] )
00156
00157 if self.secure:
00158 self._check_path( path )
00159 if not from_env:
00160 user,pswd,host,db,url = self.configure_cascade( sect, path )
00161
00162 if restrict:
00163 dbns = db.split(";")
00164 dbok = filter( lambda _:_.startswith(restrict) , dbns )
00165 assert len(dbns) == len(dbok), "DBCONF_RESTRICTion on DB names violated : all DB names must be prefixed with \"%s\" DB names :\"%s\" DB ok names: \"%s\" " % ( restrict , dbns , dbok )
00166
00167
00168 fsect = sect.split(":")[0]
00169
00170 self.sect = sect
00171 self.path = path
00172 self.user = user
00173 self.pswd = pswd
00174 self.url = url
00175 self.host = host
00176 self.db = db
00177 self.fix = fix
00178 self.fixpass = fixpass
00179 self.nodb = nodb
00180
00181 def mysqldb_parameters(self, nodb=False):
00182 """
00183 Using the `nodb=True` option skips database name parameter, this is useful
00184 when creating or dropping a database
00185 """
00186
00187 d = dict(host=self.host % self, user=self.user % self, passwd=self.pswd % self )
00188 if not nodb:
00189 d.update( db=self.db % self )
00190 if self.verbose:
00191 print "dbconf : connecting to %s " % dict(d, passwd="***" )
00192 return d
00193
00194
00195 def _check_path(self, path ):
00196 """
00197 Check existance and permissions of path
00198 """
00199 assert os.path.exists( path ), "config path %s does not exist " % path
00200 from stat import S_IMODE, S_IRUSR, S_IWUSR
00201 s = os.stat(path)
00202 assert S_IMODE( s.st_mode ) == S_IRUSR | S_IWUSR , "incorrect permissions, config file must be protected with : chmod go-rw \"%s\" " % path
00203
00204
00205 def configure_cascade(self, sect , path):
00206 """
00207 Interpret the `sect` argument comprised of a either a single section name eg `offline_db`
00208 or a colon delimited list of section names eg `tmp_offline_db:offline_db`
00209 to provide easy cascade configuration. A single section is of course a special case of a
00210 cascade. The first(or only) section in zeroth slot is treated specially with its config
00211 parameters being propagated into `self`.
00212
00213 Caution any settings of `url`, `user`, `pswd`, `host`, `db` are overridden when
00214 the `sect` argument contains a colon.
00215 """
00216 cfp, paths = DBConf.read_cfg( path )
00217 secs = cfp.sections()
00218 csect = sect.split(":")
00219 zsect = csect[0]
00220
00221 if self.verbose:
00222 print "configure_cascade sect %s secs %r csect %r " % ( sect, secs, csect )
00223 cascade = {}
00224 for sect in csect:
00225 assert sect in secs , "section %s is not one of these : %s configured in %s " % ( sect, secs, paths )
00226 cascade[sect] = {}
00227 cascade[sect].update( cfp.items(sect) )
00228
00229 user = ";".join([ cascade[sect]['user'] for sect in csect ])
00230 pswd = ";".join([ cascade[sect]['password'] for sect in csect ])
00231 host = ";".join([ cascade[sect]['host'] for sect in csect ])
00232 db = ";".join([ cascade[sect]['database'] for sect in csect ])
00233 url = ";".join([ DBConf.defaults['url'] % cascade[sect] for sect in csect ])
00234
00235 self.update( cascade[zsect] )
00236 self.cascade = cascade
00237 return user, pswd, host, db, url
00238
00239 def dump_env(self, epfx='env_'):
00240 e = {}
00241 for k,v in os.environ.items():
00242 if k.startswith(epfx.upper()):e.update({k:v} )
00243 return e
00244
00245
00246 urls = property( lambda self:(self.url % self).split(";") )
00247 users = property( lambda self:(self.user % self).split(";") )
00248 pswds = property( lambda self:(self.pswd % self).split(";") )
00249 fixs = property( lambda self:(self.fix % self).split(";") )
00250
00251 def from_env(cls):
00252 """
00253 Construct :class:`DBConf` objects from environment :
00254
00255 :envvar:`ENV_TSQL_URL`
00256 :envvar:`ENV_TSQL_USER`
00257 :envvar:`ENV_TSQL_PSWD`
00258
00259 """
00260 url = os.environ.get( 'ENV_TSQL_URL', None )
00261 user = os.environ.get( 'ENV_TSQL_USER', None )
00262 pswd = os.environ.get( 'ENV_TSQL_PSWD', None )
00263 assert url and user and pswd , "DBConf.from_env reconstruction requites the ENV_TSQL_* "
00264 cnf = DBConf(url=url, user=user, pswd=pswd,from_env=True)
00265 return cnf
00266 from_env = classmethod(from_env)
00267
00268
00269 def export_(self, **extras):
00270 """
00271 Exports the interpolated configuration into corresponding *DBI* envvars :
00272
00273 :envvar:`ENV_TSQL_USER`
00274 :envvar:`ENV_TSQL_PSWD`
00275 :envvar:`ENV_TSQL_URL`
00276
00277 And *DatabaseSvc* envvars for access to non-DBI tables via DatabaseSvc :
00278
00279 :envvar:`DYB_DB_USER`
00280 :envvar:`DYB_DB_PWSD`
00281 :envvar:`DYB_DB_URL`
00282
00283 """
00284 supplier = extras.pop('supplier', None )
00285 if supplier:
00286 print "export_ supplier is %s " % supplier
00287 else:
00288 supplier = self
00289
00290 self.export={}
00291 self.export['ENV_TSQL_URL'] = supplier.url % self
00292 self.export['ENV_TSQL_USER'] = supplier.user % self
00293 self.export['ENV_TSQL_PSWD'] = supplier.pswd % self
00294
00295 self.export['DYB_DB_HOST'] = supplier.host % self
00296 self.export['DYB_DB_NAME'] = supplier.db % self
00297 self.export['DYB_DB_USER'] = supplier.user % self
00298 self.export['DYB_DB_PSWD'] = supplier.pswd % self
00299
00300 for k,v in extras.items():
00301 self.export[k] = v % self
00302
00303
00304 def read_cfg( cls , path=None ):
00305 """
00306 Classmethod to read config file(s) as specified by `path` argument or :envvar:`DBCONF_PATH` using
00307 :py:mod:`ConfigParser`
00308 """
00309 path = path or os.environ.get('DBCONF_PATH', DBConf.defaults['path'] )
00310 from ConfigParser import ConfigParser
00311 cfp = ConfigParser(DBConf.prime_parser())
00312 cfp.optionxform = str
00313 paths = cfp.read( [os.path.expandvars(os.path.expanduser(p)) for p in path.split(":")] )
00314 return cfp, paths
00315 read_cfg = classmethod( read_cfg )
00316
00317
00318 def has_config( cls , name_=None ):
00319 """
00320 Returns if the named config is available in any of the available DBCONF files
00321
00322 For cascade configs (which comprise a colon delimited list of section names) all
00323 the config sections must be present.
00324
00325 As this module exposes this in its main, config sections can be tested on command line with::
00326
00327 ./dbconf.py offline_db && echo y || echo n
00328 ./dbconf.py offline_dbx && echo y || echo n
00329 ./dbconf.py tmp_offline_db:offline_db && echo y || echo n
00330 ./dbconf.py tmp_offline_dbx:offline_db && echo y || echo n
00331
00332 """
00333 if not name_:
00334 name_ = os.environ.get('DBCONF',None)
00335 assert name_, "has_config requires an argument if DBCONF envvar is not defined "
00336 cfp, paths = DBConf.read_cfg()
00337 sects = cfp.sections()
00338 if ":" in name_:
00339 names = name_.split(":")
00340 ok_names = filter(lambda _:_ in sects, names )
00341 return len(names) == len(ok_names)
00342 else:
00343 return name_ in sects
00344 has_config = classmethod( has_config )
00345
00346 def prime_parser( cls ):
00347 """
00348 Prime parser with "today" to allow expansion of ``%(today)s`` in ``~/.my.cnf``
00349 allowing connection to a daily recovered database named after todays date
00350 """
00351 from datetime import datetime
00352 return dict(today=datetime.now().strftime("%Y%m%d"))
00353 prime_parser = classmethod( prime_parser )
00354
00355
00356 def export_to_env(self, **extras):
00357 self.export_(**extras)
00358 print "dbconf:export_to_env from %s section %s " % ( self.path, self.sect )
00359 os.environ.update(self.export)
00360 if self.verbose:
00361 print " ==> %s " % dict(self.export, ENV_TSQL_PSWD='***', DYB_DB_PSWD='***' )
00362
00363 __str__ = lambda self:"\n".join( ["[%s]" % self.sect] + [ "%s = %s" % (k,v ) for k,v in self.items() ] + [""] )
00364
00365
00366 if __name__=='__main__':
00367 import sys
00368 assert len(sys.argv) == 2 , __doc__ % { 'path':sys.argv[0] }
00369 sys.exit( not(DBConf.has_config(sys.argv[1])) )
00370