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 
152 
154  offline = 'Offline'
155  hlt = 'HLT'
156  prompt = 'Prompt'
157 
158 
159 class TimeType(Enum):
160  run = 'Run'
161  time = 'Time'
162  lumi = 'Lumi'
163  hash = 'Hash'
164  user = 'User'
165 
166 
167 # Schema definition
168 _Base = sqlalchemy.ext.declarative.declarative_base()
169 
170 
171 class Tag(_Base):
172  __tablename__ = 'TAG'
173 
174  name = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
175  time_type = sqlalchemy.Column(sqlalchemy.Enum(*tuple(TimeType)), nullable=False)
176  object_type = sqlalchemy.Column(sqlalchemy.String(name_length), nullable=False)
177  synchronization = sqlalchemy.Column(sqlalchemy.Enum(*tuple(Synchronization)), nullable=False)
178  description = sqlalchemy.Column(sqlalchemy.String(description_length), nullable=False)
179  last_validated_time = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
180  end_of_validity = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
181  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
182  modification_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
183 
184  iovs = sqlalchemy.orm.relationship('IOV')
185 
186 
187 class IOV(_Base):
188  __tablename__ = 'IOV'
189 
190  tag_name = sqlalchemy.Column(sqlalchemy.ForeignKey('TAG.name'), primary_key=True)
191  since = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
192  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, primary_key=True)
193  payload_hash = sqlalchemy.Column(sqlalchemy.ForeignKey('PAYLOAD.hash'), nullable=False)
194 
195  tag = sqlalchemy.orm.relationship('Tag')
196  payload = sqlalchemy.orm.relationship('Payload')
197 
198 
199 class Payload(_Base):
200  __tablename__ = 'PAYLOAD'
201 
202  hash = sqlalchemy.Column(sqlalchemy.CHAR(hash_length), primary_key=True)
203  object_type = sqlalchemy.Column(sqlalchemy.String(name_length), nullable=False)
204  data = sqlalchemy.Column(sqlalchemy.BLOB, nullable=False)
205  streamer_info = sqlalchemy.Column(sqlalchemy.BLOB, nullable=False)
206  version = sqlalchemy.Column(sqlalchemy.String(20), nullable=False)
207  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
208 
209 
211  __tablename__ = 'GLOBAL_TAG'
212 
213  name = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
214  validity = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
215  description = sqlalchemy.Column(sqlalchemy.String(description_length), nullable=False)
216  release = sqlalchemy.Column(sqlalchemy.String(name_length), nullable=False)
217  insertion_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
218  snapshot_time = sqlalchemy.Column(sqlalchemy.TIMESTAMP, nullable=False)
219 
220 
222  __tablename__ = 'GLOBAL_TAG_MAP'
223 
224  global_tag_name = sqlalchemy.Column(sqlalchemy.ForeignKey('GLOBAL_TAG.name'), primary_key=True)
225  record = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
226  label = sqlalchemy.Column(sqlalchemy.String(name_length), primary_key=True)
227  tag_name = sqlalchemy.Column(sqlalchemy.ForeignKey('TAG.name'), nullable=False)
228 
229  global_tag = sqlalchemy.orm.relationship('GlobalTag')
230  tag = sqlalchemy.orm.relationship('Tag')
231 
232 
233 # CondDB object
235 
236  def __init__(self, url, init=False):
237  # Workaround to avoid creating files if not present.
238  # Python's sqlite3 module does not use sqlite3_open_v2(),
239  # and therefore we cannot disable SQLITE_OPEN_CREATE.
240  # Only in the case of creating a new database we skip the check.
241  if url.drivername == 'sqlite':
242 
243  if not init and url.database is not None and not os.path.isfile(url.database):
244  # url.database is None if opening a in-memory DB, e.g. 'sqlite://'
245  raise Exception('SQLite database %s not found.' % url.database)
246 
247  self.engine = sqlalchemy.create_engine(url)
248 
249  enabled_foreign_keys = self.engine.execute('pragma foreign_keys').scalar()
250  supports_foreign_keys = enabled_foreign_keys is not None
251  if not supports_foreign_keys:
252  logger.warning('Your SQLite database does not support foreign keys, so constraints will not be checked. Please upgrade.')
253  elif not enabled_foreign_keys:
254  self.engine.execute('pragma foreign_keys = on')
255 
256  else:
257  self.engine = sqlalchemy.create_engine(url)
258 
259  self._session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker(bind=self.engine))
260 
261  self._is_frontier = url.drivername == 'oracle+frontier'
262  self._is_oracle = url.drivername == 'oracle'
263  self._is_sqlite = url.drivername == 'sqlite'
264 
265  self._is_read_only = self._is_frontier or url.host in {
266  'cms_orcon_adg',
267  'cmsarc_lb',
268  }
269 
270  self._is_official = self._is_frontier or url.host in {
271  'cms_orcon_adg',
272  'cmsarc_lb',
273  'cms_orcoff_int',
274  'cms_orcoff_prep',
275  'cms_orcon_prod',
276  'cmsintr_lb',
277  }
278 
279  def session(self):
280  return self._session()
281 
282  @property
283  def metadata(self):
284  return _Base.metadata
285 
286  @property
287  def is_frontier(self):
288  return self._is_frontier
289 
290  @property
291  def is_oracle(self):
292  return self._is_oracle
293 
294  @property
295  def is_sqlite(self):
296  return self._is_sqlite
297 
298  @property
299  def is_read_only(self):
300  return self._is_read_only
301 
302  @property
303  def is_official(self):
304  return self._is_official
305 
306  def is_valid(self):
307  '''Tests whether the current DB looks like a valid CMS Conditions one.
308  '''
309 
310  engine_connection = self.engine.connect()
311  ret = all([self.engine.dialect.has_table(engine_connection, table.__tablename__) for table in [Tag, IOV, Payload, GlobalTag, GlobalTagMap]])
312  engine_connection.close()
313  return ret
314 
315  def init(self, drop=False):
316  '''Initializes a database.
317  '''
318 
319  if drop:
320  logger.debug('Dropping tables...')
321  self.metadata.drop_all(self.engine)
322  else:
323  if self.is_valid():
324  raise Exception('Looks like the database is already a valid CMS Conditions one. Please use drop=True if you really want to scratch it.')
325 
326  logger.debug('Creating tables...')
327  self.metadata.create_all(self.engine)
328 
329  # TODO: Create indexes
330  #logger.debug('Creating indexes...')
331 
332 
333 # Connection helpers
335  import subprocess
336  return subprocess.Popen(['cmsGetFnConnect', 'frontier://%s' % database], stdout = subprocess.PIPE).communicate()[0].strip()
337 
338 
339 def _getCMSFrontierSQLAlchemyConnectionString(database, schema = 'cms_conditions'):
340  import urllib
341  return 'oracle+frontier://@%s/%s' % (urllib.quote_plus(_getCMSFrontierConnectionString(database)), schema)
342 
343 
344 def _getCMSOracleSQLAlchemyConnectionString(database, schema = 'cms_conditions'):
345  return 'oracle://%s@%s' % (schema, database)
346 
347 
348 # Entry point
349 def connect(database='pro', init=False, verbose=0):
350  '''Returns a Connection instance to the CMS Condition DB.
351 
352  See database_help for the description of the database parameter.
353 
354  The verbosity level is as follows:
355 
356  0 = No output (default).
357  1 = SQL statements issued, including their parameters.
358  2 = In addition, results of the queries (all rows and the column headers).
359  '''
360 
361  # Lazy in order to avoid calls to cmsGetFnConnect
362  mapping = {
363  'pro': lambda: _getCMSFrontierSQLAlchemyConnectionString('PromptProd'),
364  'arc': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierArc'),
365  'int': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierInt'),
366  'dev': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierPrep', 'cms_conditions_002'),
367  'boost': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierProd', 'cms_conditions'),
368  'boostprep': lambda: _getCMSFrontierSQLAlchemyConnectionString('FrontierPrep', 'cms_conditions_002'),
369 
370  'orapro': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcon_adg'),
371  'oraarc': lambda: _getCMSOracleSQLAlchemyConnectionString('cmsarc_lb'),
372  'oraint': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcoff_int'),
373  'oradev': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcoff_prep', 'cms_conditions_002'),
374  'oraboost': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcon_adg' , 'cms_conditions'),
375  'oraboostprep': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcoff_prep', 'cms_conditions_002'),
376 
377  'onlineorapro': lambda: _getCMSOracleSQLAlchemyConnectionString('cms_orcon_prod'),
378  'onlineoraint': lambda: _getCMSOracleSQLAlchemyConnectionString('cmsintr_lb'),
379  }
380 
381  if database in mapping:
382  database = mapping[database]()
383 
384  try:
385  url = sqlalchemy.engine.url.make_url(database)
386  if url.drivername == 'oracle' and url.password is None:
387  import getpass
388  url.password = getpass.getpass('Password for %s: ' % str(url))
389  except sqlalchemy.exc.ArgumentError:
390  url = sqlalchemy.engine.url.make_url('sqlite:///%s' % database)
391 
392  if verbose >= 1:
393  logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
394 
395  if verbose >= 2:
396  logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
397 
398  return Connection(url, init=init)
399 
400 
401 def _exists(session, primary_key, value):
402  ret = None
403  try:
404  ret = session.query(primary_key).\
405  filter(primary_key == value).\
406  count() != 0
407  except sqlalchemy.exc.OperationalError:
408  pass
409 
410  return ret
411 
412 def _inserted_before(timestamp):
413  '''To be used inside filter().
414  '''
415 
416  if timestamp is None:
417  # XXX: Returning None does not get optimized (skipped) by SQLAlchemy,
418  # and returning True does not work in Oracle (generates "and 1"
419  # which breaks Oracle but not SQLite). For the moment just use
420  # this dummy condition.
421  return sqlalchemy.literal(True) == sqlalchemy.literal(True)
422 
423  return conddb.IOV.insertion_time <= _parse_timestamp(timestamp)
424 
425 def listObject(session, name, snapshot=None):
426 
427  is_tag = _exists(session, Tag.name, name)
428  result = {}
429  if is_tag:
430  result['type'] = 'Tag'
431  result['name'] = session.query(Tag).get(name).name
432  result['timeType'] = session.query(Tag.time_type).\
433  filter(Tag.name == name).\
434  scalar()
435 
436  result['iovs'] = session.query(IOV.since, IOV.insertion_time, IOV.payload_hash, Payload.object_type).\
437  join(IOV.payload).\
438  filter(
439  IOV.tag_name == name,
440  _inserted_before(snapshot),
441  ).\
442  order_by(IOV.since.desc(), IOV.insertion_time.desc()).\
443  from_self().\
444  order_by(IOV.since, IOV.insertion_time).\
445  all()
446 
447  try:
448  is_global_tag = _exists(session, GlobalTag.name, name)
449  if is_global_tag:
450  result['type'] = 'GlobalTag'
451  result['name'] = session.query(GlobalTag).get(name)
452  result['tags'] = session.query(GlobalTagMap.record, GlobalTagMap.label, GlobalTagMap.tag_name).\
453  filter(GlobalTagMap.global_tag_name == name).\
454  order_by(GlobalTagMap.record, GlobalTagMap.label).\
455  all()
456  except sqlalchemy.exc.OperationalError:
457  sys.stderr.write("No table for GlobalTags found in DB.\n\n")
458 
459  if not is_tag and not is_global_tag:
460  raise Exception('There is no tag or global tag named %s in the database.' % name)
461 
462  return result
463 
464 def getPayload(session, hash):
465  # get payload from DB:
466  data, payloadType = session.query(Payload.data, Payload.object_type).filter(Payload.hash == hash).one()
467  return data
def _exists
Definition: conddblib.py:401
tuple _Base
Definition: conddblib.py:168
static void * communicate(void *obj)
Definition: DQMNet.cc:1246
def _getCMSFrontierConnectionString
Definition: conddblib.py:334
def _inserted_before
Definition: conddblib.py:412
def listObject
Definition: conddblib.py:425
static std::string join(char **cmd)
Definition: RemoteFile.cc:18
list object
Definition: dbtoconf.py:77
def _getCMSOracleSQLAlchemyConnectionString
Definition: conddblib.py:344
def connect
Definition: conddblib.py:349
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:339
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:464