CMS 3D CMS Logo

 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
conddblib.py
Go to the documentation of this file.
1 '''CMS Conditions DB Python library.
2 '''
3 
4 __author__ = 'Miguel Ojeda'
5 __copyright__ = 'Copyright 2013, CERN'
6 __credits__ = ['Giacomo Govi', 'Miguel Ojeda', 'Andreas Pfeiffer']
7 __license__ = 'Unknown'
8 __maintainer__ = 'Miguel Ojeda'
9 __email__ = 'mojedasa@cern.ch'
10 
11 
12 import os
13 import hashlib
14 import logging
15 
16 import sqlalchemy
17 import sqlalchemy.ext.declarative
18 
19 
20 logger = logging.getLogger(__name__)
21 
22 # Set initial level to WARN. This so that log statements don't occur in
23 # the absense of explicit logging being enabled.
24 if logger.level == logging.NOTSET:
25  logger.setLevel(logging.WARN)
26 
27 
28 class EnumMetaclass(type):
29  def __init__(cls, name, bases, dct):
30  cls._members = sorted([member for member in dir(cls) if not member.startswith('_')])
31  cls._map = dict([(member, getattr(cls, member)) for member in cls._members])
32  cls._reversemap = dict([(value, key) for (key, value) in cls._map.items()])
33  super(EnumMetaclass, cls).__init__(name, bases, dct)
34 
35  def __len__(cls):
36  return len(cls._members)
37 
38  def __getitem__(cls, key):
39  '''Returns the value for this key (if the key is an integer,
40  the value is the nth member from the sorted members list).
41  '''
42 
43  if isinstance(key, int):
44  # for tuple() and list()
45  key = cls._members[key]
46  return cls._map[key]
47 
48  def __call__(cls, value):
49  '''Returns the key for this value.
50  '''
51 
52  return cls._reversemap[value]
53 
54 
55 class Enum(object):
56  '''A la PEP 435, simplified.
57  '''
58 
59  __metaclass__ = EnumMetaclass
60 
61 
62 # Utility functions
63 def hash(data):
64  return hashlib.sha1(data).hexdigest()
65 
66 
67 # Constants
68 empty_label = '-'
69 
70 name_length = 100
71 description_length = 4000
72 hash_length = len(hash(''))
73 
74 web_experts_email = 'cms-cond-dev@cern.ch'
75 offline_db_experts_email = 'cms-offlinedb-exp@cern.ch'
76 offline_db_experts_phone = '+41 22 76 70817, or 70817 from CERN; check https://twiki.cern.ch/twiki/bin/viewauth/CMS/DBShifterHelpPage if it does not work; availability depends on the state of the LHC'
77 
78 contact_help = 'If you need assistance, please write an email to %s and %s. If you need immediate/urgent assistance, you can call the Offline DB expert on call (%s).' % (offline_db_experts_email, web_experts_email, offline_db_experts_phone)
79 database_help = '''
80  The database parameter (--db) refers to the database where the tool
81  will connect to read all the data. By default, the production account
82  (through Frontier) will be used.
83 
84  In subcommands which take a source and a destination, --db always refers to
85  the source, and --destdb to the destination. For both of them the following
86  rules apply.
87 
88  The database parameter can be an official alias, a filename or any
89  valid SQLAlchemy URL.
90 
91  The official aliases are the following strings (first column):
92 
93  Alias Level Database RO/RW Notes
94  ------------ ----------- ------------- ---------- -------------------------------
95 
96  pro Production Frontier (ADG) read-only Default.
97  arc Archive Frontier read-only
98  int Integration Frontier read-only
99  dev Development Frontier read-only
100  boost Production Frontier read-only
101  boostprep Development Frontier read-only
102 
103  orapro Production Oracle (ADG) read-only Password required.
104  oraarc Archive Oracle read-only Password required.
105  oraint Integration Oracle read-write Password required.
106  oradev Development Oracle read-write Password required.
107  oraboost Production Oracle (ADG) read-write Password required.
108  oraboostprep Development Oracle read-write Password required.
109 
110  onlineorapro Production Oracle read-write Password required. Online only.
111  onlineoraint Online Int Oracle read-write Password required. Online only.
112 
113  Most of the time, if you are a regular user, you will want to read/copy
114  conditions from the Frontier production account. Therefore, you can omit
115  the --db parameter, unless you want to read from somewhere else,
116  e.g. from your local SQLite file.
117 
118  In addition, the parameter may be a filename (path) pointing to a local
119  SQLite file, e.g.
120 
121  file.db
122  relative/path/to/file.db
123  /absolute/path/to/file.db
124 
125  Finally, any valid SQLAlchemy URL can be used. This allows full
126  flexibility in cases where it may be needed, e.g.
127 
128  sqlite:// In-memory, volatile SQLite DB.
129  oracle://user@devdb11 Your private Oracle DB in devdb11 [*]
130 
131  [*] See https://account.cern.ch/ -> Services for more information
132  on personal Oracle accounts.
133 
134  For the official aliases, the password will be asked automatically
135  interactively. The same applies for Oracle URLs where the password
136  was not provided inside it, e.g.:
137 
138  oracle://user@devdb11 The tool will prompt you for the password.
139  oracle://user:pass@devdb11 Password inlined. [+]
140 
141  [+] Caution: Never write passwords in command-line parameters in
142  multi-user machines (e.g. lxplus), since other users can see them
143  in the process table (e.g. ps).
144 
145  This means that both the official aliases and the filenames are shortcuts
146  to the full SQLAlchemy URL equivalents, e.g. the following are the same:
147 
148  relative/path/to/file.db === sqlite:///relative/path/to/file.db
149  /absolute/path/to/file.db === sqlite:////absolute/path/to/file.db
150 '''
151 
153  any = 'any'
154  validation = 'validation'
155  mc = 'mc'
156  runmc = 'runmc'
157  hlt = 'hlt'
158  express = 'express'
159  prompt = 'prompt'
160  pcl = 'pcl'
161 
162 class TimeType(Enum):
163  run = 'Run'
164  time = 'Time'
165  lumi = 'Lumi'
166  hash = 'Hash'
167  user = 'User'
168 
169 
170 # Schema definition
171 _Base = sqlalchemy.ext.declarative.declarative_base()
172 
173 
174 class Tag(_Base):
175  __tablename__ = 'TAG'
176 
177  name = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
178  time_type = sqlalchemy.Column(sqlalchemy.Enum(*tuple(TimeType)), nullable=False)
179  object_type = sqlalchemy.Column(sqlalchemy.String(name_length), nullable=False)
180  synchronization = sqlalchemy.Column(sqlalchemy.Enum(*tuple(Synchronization)), nullable=False)
181  description = sqlalchemy.Column(sqlalchemy.String(description_length), nullable=False)
182  last_validated_time = sqlalchemy.Column(sqlalchemy.BIGINT, nullable=False)
183  end_of_validity = sqlalchemy.Column(sqlalchemy.BIGINT, nullable=False)
184  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
185  modification_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
186 
187  iovs = sqlalchemy.orm.relationship('IOV')
188 
189 
190 class IOV(_Base):
191  __tablename__ = 'IOV'
192 
193  tag_name = sqlalchemy.Column(sqlalchemy.ForeignKey('TAG.name'), primary_key=True)
194  since = sqlalchemy.Column(sqlalchemy.BIGINT, primary_key=True)
195  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, primary_key=True)
196  payload_hash = sqlalchemy.Column(sqlalchemy.ForeignKey('PAYLOAD.hash'), nullable=False)
197 
198  tag = sqlalchemy.orm.relationship('Tag')
199  payload = sqlalchemy.orm.relationship('Payload')
200 
201 
202 class Payload(_Base):
203  __tablename__ = 'PAYLOAD'
204 
205  hash = sqlalchemy.Column(sqlalchemy.CHAR(hash_length), primary_key=True)
206  object_type = sqlalchemy.Column(sqlalchemy.String(name_length), nullable=False)
207  data = sqlalchemy.Column(sqlalchemy.BLOB, nullable=False)
208  streamer_info = sqlalchemy.Column(sqlalchemy.BLOB, nullable=False)
209  version = sqlalchemy.Column(sqlalchemy.String(20), nullable=False)
210  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
211 
212 
214  __tablename__ = 'GLOBAL_TAG'
215 
216  name = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
217  validity = sqlalchemy.Column(sqlalchemy.BIGINT, nullable=False)
218  description = sqlalchemy.Column(sqlalchemy.String(description_length), nullable=False)
219  release = sqlalchemy.Column(sqlalchemy.String(name_length), nullable=False)
220  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
221  snapshot_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
222 
223 
225  __tablename__ = 'GLOBAL_TAG_MAP'
226 
227  global_tag_name = sqlalchemy.Column(sqlalchemy.ForeignKey('GLOBAL_TAG.name'), primary_key=True)
228  record = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
229  label = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
230  tag_name = sqlalchemy.Column(sqlalchemy.ForeignKey('TAG.name'), nullable=False)
231 
232  global_tag = sqlalchemy.orm.relationship('GlobalTag')
233  tag = sqlalchemy.orm.relationship('Tag')
234 
235 
236 # CondDB object
238 
239  def __init__(self, url, init=False):
240  # Workaround to avoid creating files if not present.
241  # Python's sqlite3 module does not use sqlite3_open_v2(),
242  # and therefore we cannot disable SQLITE_OPEN_CREATE.
243  # Only in the case of creating a new database we skip the check.
244  if url.drivername == 'sqlite':
245 
246  if not init and url.database is not None and not os.path.isfile(url.database):
247  # url.database is None if opening a in-memory DB, e.g. 'sqlite://'
248  raise Exception('SQLite database %s not found.' % url.database)
249 
250  self.engine = sqlalchemy.create_engine(url)
251 
252  enabled_foreign_keys = self.engine.execute('pragma foreign_keys').scalar()
253  supports_foreign_keys = enabled_foreign_keys is not None
254  if not supports_foreign_keys:
255  logger.warning('Your SQLite database does not support foreign keys, so constraints will not be checked. Please upgrade.')
256  elif not enabled_foreign_keys:
257  self.engine.execute('pragma foreign_keys = on')
258 
259  else:
260  self.engine = sqlalchemy.create_engine(url)
261 
262  self._session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker(bind=self.engine))
263 
264  self._is_frontier = url.drivername == 'oracle+frontier'
265  self._is_oracle = url.drivername == 'oracle'
266  self._is_sqlite = url.drivername == 'sqlite'
267 
268  self._is_read_only = self._is_frontier or url.host in {
269  'cms_orcon_adg',
270  'cmsarc_lb',
271  }
272 
273  self._is_official = self._is_frontier or url.host in {
274  'cms_orcon_adg',
275  'cmsarc_lb',
276  'cms_orcoff_int',
277  'cms_orcoff_prep',
278  'cms_orcon_prod',
279  'cmsintr_lb',
280  }
281 
282  def session(self):
283  return self._session()
284 
285  @property
286  def metadata(self):
287  return _Base.metadata
288 
289  @property
290  def is_frontier(self):
291  return self._is_frontier
292 
293  @property
294  def is_oracle(self):
295  return self._is_oracle
296 
297  @property
298  def is_sqlite(self):
299  return self._is_sqlite
300 
301  @property
302  def is_read_only(self):
303  return self._is_read_only
304 
305  @property
306  def is_official(self):
307  return self._is_official
308 
309  def is_valid(self):
310  '''Tests whether the current DB looks like a valid CMS Conditions one.
311  '''
312  engine_connection = self.engine.connect()
313  ret = all([self.engine.dialect.has_table(engine_connection, table.__tablename__) for table in [Tag, IOV, Payload, GlobalTag, GlobalTagMap]])
314  engine_connection.close()
315  return ret
316 
317  def init(self, drop=False):
318  '''Initializes a database.
319  '''
320 
321  if drop:
322  logger.debug('Dropping tables...')
323  self.metadata.drop_all(self.engine)
324  else:
325  if self.is_valid():
326  raise Exception('Looks like the database is already a valid CMS Conditions one. Please use drop=True if you really want to scratch it.')
327 
328  logger.debug('Creating tables...')
329  self.metadata.create_all(self.engine)
330 
331  # TODO: Create indexes
332  #logger.debug('Creating indexes...')
333 
334 
335 # Connection helpers
337  import subprocess
338  return subprocess.Popen(['cmsGetFnConnect', 'frontier://%s' % database], stdout = subprocess.PIPE).communicate()[0].strip()
339 
340 
341 def _getCMSFrontierSQLAlchemyConnectionString(database, schema = 'cms_conditions'):
342  import urllib
343  return 'oracle+frontier://@%s/%s' % (urllib.quote_plus(_getCMSFrontierConnectionString(database)), schema)
344 
345 
346 def _getCMSOracleSQLAlchemyConnectionString(database, schema = 'cms_conditions'):
347  return 'oracle://%s@%s' % (schema, database)
348 
349 
350 # Entry point
351 
352 def make_url(database='pro'):
353 
354  schema = 'cms_conditions' # set the default
355  if ':' in database and '://' not in database: # check if we really got a shortcut like "pro:<schema>" (and not a url like proto://...), if so, disentangle
356  database, schema = database.split(':')
357  logging.debug(' ... using db "%s", schema "%s"' % (database, schema) )
358 
359  # Lazy in order to avoid calls to cmsGetFnConnect
360  mapping = {
361  'pro': lambda: _getCMSFrontierSQLAlchemyConnectionString('PromptProd', schema),
362  'arc': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierArc', schema),
363  'int': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierInt', schema),
364  'dev': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierPrep', schema),
365 
366  'orapro': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcon_adg', schema),
367  'oraarc': lambda: _getCMSOracleSQLAlchemyConnectionString('cmsarc_lb', schema),
368  'oraint': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcoff_int', schema),
369  'oradev': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcoff_prep', schema),
370 
371  'onlineorapro': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcon_prod', schema),
372  'onlineoraint': lambda: _getCMSOracleSQLAlchemyConnectionString('cmsintr_lb', schema),
373  }
374 
375  if database in mapping:
376  database = mapping[database]()
377 
378  logging.debug('connection string set to "%s"' % database)
379 
380  try:
381  url = sqlalchemy.engine.url.make_url(database)
382  except sqlalchemy.exc.ArgumentError:
383  url = sqlalchemy.engine.url.make_url('sqlite:///%s' % database)
384  return url
385 
386 def connect(url, init=False, verbose=0):
387  '''Returns a Connection instance to the CMS Condition DB.
388 
389  See database_help for the description of the database parameter.
390 
391  The verbosity level is as follows:
392 
393  0 = No output (default).
394  1 = SQL statements issued, including their parameters.
395  2 = In addition, results of the queries (all rows and the column headers).
396  '''
397 
398  if url.drivername == 'oracle' and url.password is None:
399  import getpass
400  url.password = getpass.getpass('Password for %s: ' % str(url))
401 
402  if verbose >= 1:
403  logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
404 
405  if verbose >= 2:
406  logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
407 
408  return Connection(url, init=init)
409 
410 
411 def _exists(session, primary_key, value):
412  ret = None
413  try:
414  ret = session.query(primary_key).\
415  filter(primary_key == value).\
416  count() != 0
417  except sqlalchemy.exc.OperationalError:
418  pass
419 
420  return ret
421 
422 def _inserted_before(timestamp):
423  '''To be used inside filter().
424  '''
425 
426  if timestamp is None:
427  # XXX: Returning None does not get optimized (skipped) by SQLAlchemy,
428  # and returning True does not work in Oracle (generates "and 1"
429  # which breaks Oracle but not SQLite). For the moment just use
430  # this dummy condition.
431  return sqlalchemy.literal(True) == sqlalchemy.literal(True)
432 
433  return conddb.IOV.insertion_time <= _parse_timestamp(timestamp)
434 
435 def listObject(session, name, snapshot=None):
436 
437  is_tag = _exists(session, Tag.name, name)
438  result = {}
439  if is_tag:
440  result['type'] = 'Tag'
441  result['name'] = session.query(Tag).get(name).name
442  result['timeType'] = session.query(Tag.time_type).\
443  filter(Tag.name == name).\
444  scalar()
445 
446  result['iovs'] = session.query(IOV.since, IOV.insertion_time, IOV.payload_hash, Payload.object_type).\
447  join(IOV.payload).\
448  filter(
449  IOV.tag_name == name,
450  _inserted_before(snapshot),
451  ).\
452  order_by(IOV.since.desc(), IOV.insertion_time.desc()).\
453  from_self().\
454  order_by(IOV.since, IOV.insertion_time).\
455  all()
456 
457  try:
458  is_global_tag = _exists(session, GlobalTag.name, name)
459  if is_global_tag:
460  result['type'] = 'GlobalTag'
461  result['name'] = session.query(GlobalTag).get(name)
462  result['tags'] = session.query(GlobalTagMap.record, GlobalTagMap.label, GlobalTagMap.tag_name).\
463  filter(GlobalTagMap.global_tag_name == name).\
464  order_by(GlobalTagMap.record, GlobalTagMap.label).\
465  all()
466  except sqlalchemy.exc.OperationalError:
467  sys.stderr.write("No table for GlobalTags found in DB.\n\n")
468 
469  if not is_tag and not is_global_tag:
470  raise Exception('There is no tag or global tag named %s in the database.' % name)
471 
472  return result
473 
474 def getPayload(session, hash):
475  # get payload from DB:
476  data, payloadType = session.query(Payload.data, Payload.object_type).filter(Payload.hash == hash).one()
477  return data
def _exists
Definition: conddblib.py:411
tuple _Base
Definition: conddblib.py:171
static void * communicate(void *obj)
Definition: DQMNet.cc:1246
def _getCMSFrontierConnectionString
Definition: conddblib.py:336
def _inserted_before
Definition: conddblib.py:422
def listObject
Definition: conddblib.py:435
static std::string join(char **cmd)
Definition: RemoteFile.cc:18
def make_url
Definition: conddblib.py:352
list object
Definition: dbtoconf.py:77
def _getCMSOracleSQLAlchemyConnectionString
Definition: conddblib.py:346
def connect
Definition: conddblib.py:386
dbl *** dir
Definition: mlp_gen.cc:35
def hash
Definition: conddblib.py:63
T get(const Candidate &c)
Definition: component.h:55
def _getCMSFrontierSQLAlchemyConnectionString
Definition: conddblib.py:341
double scalar(const CLHEP::HepGenMatrix &m)
Return the matrix as a scalar. Raise an assertion if the matris is not .
Definition: matutil.cc:183
def getPayload
Definition: conddblib.py:474