Belle II Software  release-08-01-10
cli_management.py
1 
8 import functools
9 import time
10 from fnmatch import fnmatch
11 from concurrent.futures import ThreadPoolExecutor
12 from collections import defaultdict
13 from basf2 import B2INFO, B2ERROR, B2WARNING, LogPythonInterface # noqa
14 from basf2.utils import pretty_print_table
15 from terminal_utils import Pager
16 from conditions_db import set_cdb_authentication_token
17 from conditions_db.iov import IoVSet, IntervalOfValidity
18 from conditions_db.runningupdate import RunningTagUpdater, RunningTagUpdaterError, RunningTagUpdateMode
19 from conditions_db.cli_utils import ItemFilter
20 
21 
22 def get_all_iovsets(existing_payloads, run_range=None):
23  """Given a list of PayloadInformation objects, return a reduced list PayloadInformation
24  objects with the single iovs replaced with IoVSets. Payloads with the same
25  name and revision will be merged.
26 
27  Overlaps will raise an B2ERROR
28  """
29  all_payloads = defaultdict(lambda: IoVSet(allow_overlaps=True))
30  by_name = defaultdict(lambda: IoVSet(allow_overlaps=False))
31  infos = {}
32  for payload in existing_payloads:
33  # we want to make set of iovs for each payload
34  iov = IntervalOfValidity(payload.iov)
35  # possibly we have a run range we want to limit to
36  if run_range is not None:
37  iov &= run_range
38  if not iov:
39  continue
40 
41  # merge the iovs for the same revision
42  all_payloads[payload.name, payload.revision].add(iov)
43  # also keep the PayloadInformation
44  infos[payload.name, payload.revision] = payload
45 
46  # and also check if there are any overlaps with the same payload name
47  try:
48  by_name[payload.name].add(iov)
49  except ValueError as e:
50  B2ERROR(f"Overlap for payload {payload.name} r{payload.revision}: {e}")
51 
52  # so now flatten the thing again and return PayloadInformation objects we slightly modify
53  result = []
54  for (name, revision), iov in all_payloads.items():
55  info = infos[name, revision]
56  info.iov = iov
57  info.iov_id = None
58  result.append(info)
59 
60  return result
61 
62 
63 def create_iov_wrapper(db, globaltag_id, payload):
64  """
65  Wrapper function for adding payloads into a given globaltag.
66  """
67  for iov in payload.iov:
68  if db.create_iov(globaltag_id, payload.payload_id, *iov.tuple) is None:
69  raise RuntimeError(f"Cannot create iov for {payload.name} r{payload.revision}")
70 
71 
72 def command_tag_merge(args, db=None):
73  """
74  Merge a list of globaltags in the order they are given.
75 
76  This command allows to merge a number of globaltags into a single globaltag.
77  Payloads from later globaltags in the list of arguments are used to fill gaps
78  present in earlier globaltags.
79 
80  The result is equivalent to having multiple globaltags setup in the conditions
81  access for basf2 (highest priority goes first).
82 
83  Warning:
84  The order of the globaltags is highest priority first, so payloads from
85  globaltags earlier on the command line will be taken with before globaltags
86  from later tags.
87 
88  This command requires that all globaltags are overlap free.
89 
90  For each globaltag in the list we copy all payloads to the output globaltag
91  if there is no payload of that name valid for the given interval of validity
92  in any previous globaltags in the list.
93 
94  If the payload overlaps partially with a payload from a previous globaltag
95  in the list the interval of validity is shortened (and possibly split) to
96  not overlap but to just fill the gaps.
97 
98  For example:
99 
100  globaltag A contains ::
101 
102  payload1, rev 2, valid from 1,0 to 1,10
103  payload1, rev 3, valid from 1,20 to 1,22
104  payload2, rev 1, valid from 1,0 to 1,-1
105 
106  globaltag B contains ::
107 
108  payload1, rev 1, valid from 1,1 to 1,30
109  payload2, rev 2, valid from 0,1 to 1,20
110 
111  Then running ``b2conditionsdb tag merge -o output A B``, the output globaltag
112  after the merge will contain::
113 
114  payload1, rev 2, valid from 1,0 to 1,10
115  payload1, rev 1, valid from 1,11 to 1,19
116  payload1, rev 3, valid from 1,20 to 1,22
117  payload1, rev 1, valid from 1,23 to 1,30
118  payload2, rev 2, valid from 0,1 to 0,-1
119  payload2, rev 1, valid from 1,0 to 1,-1
120 
121  When finished, this command will print a table of payloads and their
122  validity and from which globaltag they were taken. If ``--dry-run`` is given
123  it will only print the list of payloads.
124 
125  Optionally one can specify ``--run-range`` to limit the run range for which
126  the merging should take place. In the example above, running with
127  ``--run-range 1 0 1 21`` the result would be ::
128 
129  payload1, rev 2, valid from 1,0 to 1,10
130  payload1, rev 1, valid from 1,11 to 1,19
131  payload1, rev 3, valid from 1,20 to 1,21
132  payload2, rev 1, valid from 1,0 to 1,21
133 
134  .. versionadded:: release-05-01-00
135  """
136 
137  if db is None:
138  args.add_argument("globaltag", nargs="+", help="name of the globaltag")
139  group = args.add_argument_group("required named arguments")
140  group.add_argument("-o", "--output", required=True, help="Name of the output globaltag")
141  args.add_argument("--dry-run", help="Don't do anything, just print a table with the results",
142  action="store_true", default=False)
143  args.add_argument("--run-range", nargs=4, default=None, type=int,
144  metavar=("FIRST_EXP", "FIRST_RUN", "FINAL_EXP", "FINAL_RUN"),
145  help="Can be for numbers to limit the run range to put"
146  "in the output globaltag: All iovs will be limited to "
147  "be in this range.")
148  args.add_argument("-j", type=int, default=10, dest="nprocess",
149  help="Number of concurrent threads to use for "
150  "creating payloads into the output globaltag.")
151  return
152 
153  if not args.dry_run:
154  set_cdb_authentication_token(db, args.auth_token)
155 
156  # prepare some colors for easy distinction of source tag
157  support_color = LogPythonInterface.terminal_supports_colors()
158  if support_color:
159  colors = "\x1b[32m \x1b[34m \x1b[35m \x1b[31m".split()
160  colors = {tag: color for tag, color in zip(args.globaltag, colors)}
161 
162  def color_row(row, _, line):
163  """Color the lines depending on which globaltag the payload comes from"""
164  if not support_color:
165  return line
166  begin = colors.get(row[-1], "")
167  end = '\x1b[0m'
168  return begin + line + end
169 
170  with Pager("Result of merging globaltags", True):
171  # make sure output tag exists
172  output_id = db.get_globalTagInfo(args.output)
173  if output_id is None:
174  B2ERROR("Output globaltag doesn't exist. Please create it first with a proper description")
175  return False
176 
177  output_id = output_id["globalTagId"]
178 
179  # check all globaltags exist
180  if any(db.get_globalTagInfo(tag) is None for tag in args.globaltag):
181  return False
182 
183  final = []
184  table = []
185  existing = defaultdict(lambda: IoVSet(allow_overlaps=True))
186  if args.run_range is not None:
187  args.run_range = IntervalOfValidity(args.run_range)
188 
189  # For each globaltag
190  for tag in args.globaltag:
191  # get all the payloads and iovs from the globaltag
192  all_payloads = db.get_all_iovs(tag)
193  # sort all the payloads by revision number (reversed sort: highest revisions first)
194  all_payloads.sort(key=lambda p: p.revision, reverse=True)
195  # and sort again but this time by name: not really necessary,
196  # but it helps printing the log messages ordered by payloads name
197  all_payloads.sort(key=lambda p: p.name, reverse=False)
198  # get all payload information objects with their iovs already merged to iovset instances
199  payloads = get_all_iovsets(all_payloads, args.run_range)
200  for payload in payloads:
201  # make sure it doesn't overlap with any of the previous
202  payload.iov.remove(existing[payload.name])
203  # but if there's something left
204  if payload.iov:
205  # extend the known coverage of this payload
206  existing[payload.name] |= payload.iov
207  # extend this payload to the list to create later
208  final.append(payload)
209  # and add all separate iovs to the table to show the user
210  for iov in payload.iov:
211  table.append([payload.name, payload.revision] + list(iov.tuple) + [tag])
212 
213  # sort the table by payload name and start run ... we want to display it
214  table.sort(key=lambda r: (r[0], r[3:5]))
215 
216  # and fancy print it ...
217  table.insert(0, ["Name", "Rev", "First Exp", "First Run", "Final Exp", "Final Run", "Source"])
218  columns = ["+", -8, 6, 6, 6, 6, max(len(_) for _ in args.globaltag)]
219 
220  B2INFO(f"Result of merging the globaltags {', '.join(args.globaltag)}")
221 
222  pretty_print_table(table, columns, transform=color_row)
223 
224  # Ok, we're still alive, create all the payloads using multiple processes.
225  if not args.dry_run:
226  B2INFO(f'Now copying the {len(final)} payloads into {args.output} to create {len(table)-1} iovs ...')
227  create_iov = functools.partial(create_iov_wrapper, db, output_id)
228  try:
229  with ThreadPoolExecutor(max_workers=args.nprocess) as pool:
230  start = time.monotonic()
231  for payload, _ in enumerate(pool.map(create_iov, final), 1):
232  eta = (time.monotonic() - start) / payload * (len(final) - payload)
233  B2INFO(f"{payload}/{len(final)} payloads copied, ETA: {eta:.1f} seconds")
234  except RuntimeError:
235  B2ERROR("Not all iovs could be created. This could be a server/network problem "
236  "or the destination globaltag was not empty or not writeable. Please make "
237  "sure the target tag is empty and try again")
238  return 1
239 
240  return 0
241 
242 
243 def command_tag_runningupdate(args, db=None):
244  """
245  Update a running globaltag with payloads from a staging tag
246 
247  This command will calculate and apply the necessary updates to a running
248  globaltag with a given staging globaltag
249 
250  Running tags are defined as "immutable for existing data but conditions for
251  newer runs may be added" and the only modification allowed is to add new
252  payloads for new runs or close existing payloads to no longer be valid for
253  new runs.
254 
255  This command takes previously prepared and validated payloads in a staging
256  globaltag and will then calculate which payloads to close and what to add to
257  the running globaltag.
258 
259  For this to work we require
260 
261  1. A running globaltag in the state "RUNNING"
262 
263  2. A (experiment, run) number from which run on the update should be valid.
264  This run number needs to be
265 
266  a) bigger than the start of validity for all iovs in the running tag
267  b) bigger than the end of validity for all closed iovs (i.e. not valid
268  to infinity) in the running tag
269 
270  3. A staging globaltag with the new payloads in state "VALIDATED"
271 
272  a) payloads in the staging tag starting at (0,0) will be interpreted as
273  starting at the first valid run for the update
274  b) all other payloads need to start at or after the first valid run for
275  the update.
276  c) The globaltag needs to be gap and overlap free
277  d) All payloads in the staging tag should have as last iov an open iov
278  (i.e. valid to infinity) but this can be disabled.
279 
280  The script will check all the above requirements and will then calculate the
281  necessary operations to
282 
283  1. Add all payloads from the staging tag where a start validity of (0, 0) is
284  replaced by the starting run for which this update should be valid.
285  2. close all iovs for payloads in the running tags just before the
286  corresponding iov of the same payload in the staging tag, so either at the
287  first run for the update to be valid or later
288  3. Optionally, make sure all payloads in the staging tag end in an open iov.
289 
290  Example:
291 
292  running tag contains ::
293 
294  payload1, rev 1, valid from 0,1 to 1,0
295  payload1, rev 2, valid from 1,1 to -1,-1
296  payload2, rev 1, valid from 0,1 to -1,-1
297  payload3, rev 1, valid from 0,1 to 1,0
298  payload4, rev 1, valid from 0,1 to -1,-1
299  payload5, rev 1, valid from 0,1 to -1,-1
300 
301  staging tag contains ::
302 
303  payload1, rev 3, valid from 0,0 to 1,8
304  payload1, rev 4, valid from 1,9 to 1,20
305  payload2, rev 2, valid from 1,5 to 1,20
306  payload3, rev 2, valid from 0,0 to -1,-1
307  payload4, rev 1, valid from 0,0 to 1,20
308 
309  Then running ``b2conditionsdb tag runningupdate running staging --run 1 2 --allow-closed``,
310  the running globaltag after the update will contain ::
311 
312  payload1, rev 1, valid from 0,1 to 1,0
313  payload1, rev 2, valid from 1,1 to 1,1
314  payload1, rev 3, valid from 1,2 to 1,8
315  payload1, rev 4, valid from 1,9 to 1,20
316  payload2, rev 1, valid from 0,1 to 1,4
317  payload2, rev 2, valid from 1,5 to 1,20
318  payload3, rev 1, valid from 0,1 to 1,0
319  payload3, rev 2, valid from 1,2 to -1,-1
320  payload4, rev 1, valid from 0,1 to 1,20
321  payload5, rev 1, valid from 0,1 to -1,-1
322 
323  Note that
324 
325  - the start of payload1 and payload3 in staging has been adjusted
326  - payload2 in the running tag as been closed at 1,4, just before the
327  validity from the staging tag
328  - payload3 was already closed in the running tag so no change is
329  performed. This might result in gaps but is intentional
330  - payload4 was not closed at rim 1,2 because the staging tag had the same
331  revision of the payload so the these were merged to one long validity.
332  - payload5 was not closed as there was no update to it in the staging tag.
333  If we would have run with ``--full-replacement`` it would have been closed.
334  - if we would have chosen ``--run 1 1`` the update would have failed because
335  payload1, rev2 in running starts at 1,1 so we would have a conflict
336  - if we would have chosen ``--run 1 6`` the update would have failed because
337  payload2 in the staging tag starts before this run
338  - if we would have chosen to open the final iovs in staging by using
339  ``--fix-closed``, payload1, rev 4; payload2, rev 2 and payload4 rev 1
340  would be valid until -1,-1 after the running tag. In fact, payload 4
341  would not be changed at all.
342  """
343  if db is None:
344  args.add_argument("running", help="name of the running globaltag")
345  args.add_argument("staging", help="name of the staging globaltag")
346  group = args.add_argument_group("required named arguments")
347  group.add_argument("-r", "--run", required=True, nargs=2, type=int, metavar=("EXP", "RUN"),
348  help="First experiment + run number for which the update should be "
349  "valid. Two numbers separated by space")
350  choice = args.add_mutually_exclusive_group()
351  choice.add_argument("--allow-closed", dest="mode", action="store_const",
352  const=RunningTagUpdateMode.ALLOW_CLOSED,
353  default=RunningTagUpdateMode.STRICT,
354  help="if given allow payloads in the staging tag to not "
355  "be open, i.e. they don't have to be open ended in the "
356  "update. Useful to retire a payload by adding one last update")
357  choice.add_argument("--fix-closed", dest="mode", action="store_const",
358  const=RunningTagUpdateMode.FIX_CLOSED,
359  help="if given automatically open the last iov for each "
360  "payload in staging if it is closed.")
361  choice.add_argument("--simple-mode", dest="mode", action="store_const",
362  const=RunningTagUpdateMode.SIMPLE,
363  help="if given require the staging tag to solely consist "
364  "of fully infinite validities: Only one iov per payload "
365  "with a validity of (0,0,-1,-1)")
366  choice.add_argument("--full-replacement", dest="mode", action="store_const",
367  const=RunningTagUpdateMode.FULL_REPLACEMENT,
368  help="if given perform a full replacement and close all "
369  "open iovs in the running tag not present in the staging tag. "
370  "After such an update exactly the payloads in the staging tag "
371  "will be valid after the given run. This allows for closed iovs "
372  "in the staging tag as with ``--allow-closed``")
373  args.add_argument("--dry-run", default=False, action="store_true",
374  help="Only show the changes, don't try to apply them")
375  return
376 
377  if not args.dry_run:
378  set_cdb_authentication_token(db, args.auth_token)
379 
380  try:
381  updater = RunningTagUpdater(db, args.running, args.staging, args.run, args.mode, args.dry_run)
382  operations = updater.calculate_update()
383  except RunningTagUpdaterError as e:
384  B2ERROR(e, **e.extra_vars)
385  return 1
386 
387  # make sure we exit if we have nothing to do
388  if not operations:
389  B2INFO("Nothing to do, please check the globaltags given are correct")
390  return 1
391 
392  # show operations in a table and some summary
393  table = []
394  last_valid = tuple(args.run)
395  summary = {
396  "first valid run": last_valid,
397  "payloads closed": 0,
398  "payloads updated": 0,
399  "payload iovs added": 0,
400  "next possible update": last_valid,
401  }
402 
403  updated_payloads = set()
404  for op, payload in operations:
405  # calculate how many payloads/iovs will be closed/added
406  if op == "CLOSE":
407  summary['payloads closed'] += 1
408  else:
409  updated_payloads.add(payload.name)
410  summary['payload iovs added'] += 1
411  # remember the highest run number of any iov, so first run
412  last_valid = max(payload.iov[:2], last_valid)
413  # and final run if not open
414  if payload.iov[2:] != (-1, -1):
415  last_valid = max(payload.iov[2:], last_valid)
416  # and add to the table of operations to be shown
417  table.append([op, payload.name, payload.revision] + list(payload.iov))
418 
419  # calculate the next fee run
420  summary['next possible update'] = (last_valid[0] + (1 if last_valid[1] < 0 else 0), last_valid[1] + 1)
421  # and the number of distinct payloads
422  summary['payloads updated'] = len(updated_payloads)
423 
424  # prepare some colors for easy distinction of closed payloads
425  support_color = LogPythonInterface.terminal_supports_colors()
426 
427  def color_row(row, _, line):
428  """Color the lines depending on which globaltag the payload comes from"""
429  if not support_color:
430  return line
431  begin = "" if row[0] != "CLOSE" else "\x1b[31m"
432  end = '\x1b[0m'
433  return begin + line + end
434 
435  # and then show the table
436  table.sort(key=lambda x: (x[1], x[3:], x[2], x[0]))
437  table.insert(0, ["Action", "Payload", "Rev", "First Exp", "First Run", "Final Exp", "Final Run"])
438  columns = [6, '*', -6, 6, 6, 6, 6]
439 
440  with Pager(f"Changes to running tag {args.running}:", True):
441  B2INFO(f"Changes to be applied to the running tag {args.running}")
442  pretty_print_table(table, columns, transform=color_row)
443 
444  if args.dry_run:
445  B2INFO("Running in dry mode, not applying any changes.", **summary)
446  return 0
447 
448  B2WARNING("Applying these changes cannot be undone and further updates to "
449  "this run range will **NOT** be possible", **summary)
450  # ask if the user really knows what they're doing
451  answer = input("Are you sure you want to continue? [yes/No]: ")
452  while answer.lower().strip() not in ['yes', 'no', 'n', '']:
453  answer = input("Please enter 'yes' or 'no': ")
454 
455  if answer.lower().strip() != 'yes':
456  B2INFO("Aborted by user ...")
457  return 1
458 
459  # Ok, all set ... apply the update
460  try:
461  updater.apply_update()
462  B2INFO("done")
463  return 0
464  except RunningTagUpdaterError as e:
465  B2ERROR(e, **e.extra_vars)
466  return 1
467 
468 
469 def command_iovs(args, db=None):
470  """
471  Modify, delete or copy iovs from a globaltags.
472 
473  This command allows to modify, delete or copy iovs of a globaltag. If no other command is given do nothing.
474  """
475 
476  # no arguments to define, just a group command
477  pass
478 
479 # Helper class to unify common parts of commands_iovs_*
480 
481 
482 class CommandIoVsHelper:
483  """
484  Class to unify common parts of b2conditionsdb iovs commands
485 
486  This class defines common argparse arguments,
487  common filter of iovs and common multithreading
488  """
489 
490  def __init__(self, whichcommand, args, db):
491  """initialization, just remember the arguments or parser and the database instance
492 
493  Args:
494  whichcommand (str): from whichcommand it is called (copy, delete or modify)
495  args (argparse.ArgumentParser): where to append new arguments
496  db (conditions_db.ConditionsDB): database instance to be used
497  """
498 
499 
500  self.whichcommandwhichcommand = whichcommand
501 
502  self._args_args = args
503 
504  self.dbdb = db
505 
506  self.iovfilteriovfilter = ItemFilter(args)
507 
508  self.num_all_iovsnum_all_iovs = None
509 
510  self.past_dictpast_dict = {"delete": "deleted", "modify": "modified", "copy": "copied", "create": "created"}
511 
512  def add_arguments(self):
513  """Add arguments to the parser"""
514 
515  self._args_args.add_argument("tag", metavar="INPUT_TAGNAME",
516  help=f"globaltag for which the the IoVs should be {self.past_dict[self.whichcommand]}")
517  if self.whichcommandwhichcommand == "copy":
518  self._args_args.add_argument("output", metavar="OUTPUT_TAGNAME", help="globaltag to which the the IoVs should be copied")
519  if self.whichcommandwhichcommand == "modify":
520  self._args_args.add_argument("new_iov", metavar="NEW_IOV", help="New iov to be set to all considered iovs."
521  " It should be a string with 4 numbers separated by spaces."
522  " Use * to mark the fields that should not be modified. For example"
523  " if 7 0 * * is given the iov (7, 1, 9, 42) will become (7 0 9 42).")
524  self._args_args.add_argument("--iov-id", default=None, type=int,
525  help="IoVid of the iov to be considered")
526  self._args_args.add_argument(
527  "--iov-pattern",
528  default=None,
529  help="whitespace-separated string with pattern of the iov to be considered. "
530  " Use * to mark the fields that shold be ignored. Valid patterns are 0 0 -1 -1"
531  " (a very specific IoV), 0 * -1 -1 (any iov that starts in any run of exp 0 and ends exactly in exp -1, run -1)"
532  ", * * 3 45 (any Iov ending in exp 3, run 45, regardless from where it starts).")
533  self._args_args.add_argument("--run-range", nargs=4, default=None, type=int,
534  metavar=("FIRST_EXP", "FIRST_RUN", "FINAL_EXP", "FINAL_RUN"),
535  help="Can be four numbers to limit the run range to be considered"
536  " Only iovs overlapping, even partially, with this range will be considered.")
537  self._args_args.add_argument("--fully-contained", action="store_true",
538  help="If given together with --run_range limit the list of payloads "
539  "to the ones fully contained in the given run range")
540  if self.whichcommandwhichcommand == "copy":
541  self._args_args.add_argument("--set-run-range", action="store_true",
542  help="If given together with --run_range modify the interval of validity"
543  " of partially overlapping iovs to be fully contained in the given run range")
544  self.iovfilteriovfilter.add_arguments("payloads")
545  self._args_args.add_argument("--revision", metavar='revision', type=int,
546  help="Specify the revision of the payload to be removed")
547  self._args_args.add_argument("--dry-run", help="Don't do anything, just print what would be done",
548  action="store_true", default=False)
549  if self.whichcommandwhichcommand == "copy":
550  self._args_args.add_argument("--replace", help="Modify the iovs in the output tag to avoid overlaps",
551  action="store_true", default=False)
552  self._args_args.add_argument("-j", type=int, default=10, dest="nprocess",
553  help="Number of concurrent threads to use.")
554 
555  def get_iovs(self):
556  """Get the iovs already filtered"""
557  if not self.iovfilteriovfilter.check_arguments():
558  B2ERROR("Issue with arguments")
559 
560  all_iovs = self.dbdb.get_all_iovs(
561  self._args_args.tag,
562  run_range=self._args_args.run_range,
563  fully_contained=self._args_args.fully_contained,
564  message=str(self.iovfilteriovfilter))
565  self.num_all_iovsnum_all_iovs = len(all_iovs)
566  iovs_to_return = []
567  for iov in all_iovs:
568  if self._args_args.iov_id and iov.iov_id != self._args_args.iov_id:
569  continue
570  if not self.iovfilteriovfilter.check(iov.name):
571  continue
572  if self._args_args.iov_pattern and not fnmatch("{} {} {} {}".format(*iov.iov),
573  "{} {} {} {}".format(*self._args_args.iov_pattern.split())):
574  continue
575  if self._args_args.revision and iov.revision != self._args_args.revision:
576  continue
577  iovs_to_return.append(iov)
578  return iovs_to_return
579 
580  def modify_db(self, func, func_args, whichcommand=None):
581  """Modify the database using multithreading"""
582  if whichcommand is None:
583  whichcommand = self.whichcommandwhichcommand
584  try:
585  with ThreadPoolExecutor(max_workers=self._args_args.nprocess) as pool:
586  start = time.monotonic()
587  for iov_num, _ in enumerate(pool.map(func, func_args), 1):
588  eta = (time.monotonic() - start) / iov_num * (len(func_args) - iov_num)
589  B2INFO(f"{iov_num}/{len(func_args)} iovs {self.past_dict[whichcommand]}, ETA: {eta:.1f} seconds")
590  except RuntimeError:
591  B2ERROR(f"Not all iovs could be {self.past_dict[whichcommand]}. This could be a server/network problem "
592  "or the destination globaltag was not writeable.")
593  raise
594 
595 
596 def command_iovs_delete(args, db=None):
597  """
598  Delete iovs from a globaltag
599 
600  This command allows to delete the iovs from a globaltags, optionally limiting the iovs to be deleted to those
601  of a specific payload, revision, IoVid or run range.
602  """
603  command_iovs_helper = CommandIoVsHelper("delete", args, db)
604  if db is None:
605  command_iovs_helper.add_arguments()
606  return
607 
608  iovs_to_delete = command_iovs_helper.get_iovs()
609 
610  table = [[i.iov_id, i.name, i.revision] + list(i.iov) for i in iovs_to_delete]
611  table.insert(0, ["IovId", "Name", "Rev", "First Exp", "First Run", "Final Exp", "Final Run"])
612  columns = [9, "+", 6, 6, 6, 6, 6]
613 
614  if command_iovs_helper.num_all_iovs == len(iovs_to_delete) and args.run_range is None:
615  B2WARNING(f"All the iovs in the globaltag {args.tag} will be deleted!")
616  gt_check = input('Please enter the global tag name again to confirm this action: ')
617  if gt_check != args.tag:
618  B2ERROR('global tag names do not match.')
619  return 1
620 
621  B2INFO("Deleting the following iovs")
622  pretty_print_table(table, columns)
623 
624  if not args.dry_run:
625  set_cdb_authentication_token(db, args.auth_token)
626  try:
627  command_iovs_helper.modify_db(db.delete_iov, [i.iov_id for i in iovs_to_delete])
628  except RuntimeError:
629  return 1
630 
631  return 0
632 
633 
634 def command_iovs_copy(args, db=None):
635  """
636  Copy iovs from a globaltag to another one
637 
638  This command allows to copy the iovs from a globaltags to another one, optionally limiting
639  the iovs to be copied to those of a specific payload, revision, IoV id or run range.
640  """
641  command_iovs_helper = CommandIoVsHelper("copy", args, db)
642  if db is None:
643  command_iovs_helper.add_arguments()
644  return
645 
646  iovs_to_copy = command_iovs_helper.get_iovs()
647 
648  # make sure that there are no overlaps in the iovs to copy and set run range if needed
649  by_name = defaultdict(lambda: IoVSet(allow_overlaps=False))
650  for iov in iovs_to_copy:
651  if args.run_range and args.set_run_range:
652  iov.iov = (IntervalOfValidity(*iov.iov) & IntervalOfValidity(*args.run_range)).tuple
653  try:
654  by_name[iov.name].add(iov.iov)
655  except ValueError as e:
656  B2ERROR(f"Overlap for payload {iov.name} r{iov.revision}: {e}")
657  return 1
658  del by_name
659 
660  # make sure output tag exists
661  output_id = db.get_globalTagInfo(args.output)
662  if output_id is None:
663  B2ERROR("Output globaltag doesn't exist. Please create it first with a proper description")
664  return False
665  output_id = output_id["globalTagId"]
666 
667  # get iovs already present in output global tag
668  iovs_output = db.get_all_iovs(args.output)
669  iovs_output.sort(key=lambda i: i.iov[:2])
670  payloads_output = defaultdict(list)
671  for iov in iovs_output:
672  iov.newiov = IoVSet([IntervalOfValidity(*iov.iov)])
673  payloads_output[iov.name].append(iov)
674 
675  # remove from iovs to copy those that are already present in the output global tag
676  iovs_in_output = [(i.checksum, i.iov) for i in iovs_output]
677  iovs_to_copy = [i for i in iovs_to_copy if (i.checksum, i.iov) not in iovs_in_output]
678  del iovs_in_output
679 
680  iovs_not_to_copy = []
681 
682  # readjust the iovs already present if they overlap
683  # with the ones to be copied
684  for iov_to_copy in iovs_to_copy:
685  iov_ = IntervalOfValidity(*iov_to_copy.iov)
686  still_to_copy = True
687  for iov_present in payloads_output[iov_to_copy.name]:
688  if iov_present.newiov & iov_:
689  if still_to_copy and iov_to_copy.checksum == iov_present.checksum:
690  iov_present.newiov.add(iov_, allow_overlaps=True)
691  iovs_not_to_copy.append(iov_to_copy)
692  still_to_copy = False
693  else:
694  iov_present.newiov -= iov_
695 
696  iovs_to_delete = []
697  iovs_to_modify = []
698  iovs_to_create = []
699 
700  # Fill lists of iovs to be deleted, modified or added
701  for iov_present in sum(payloads_output.values(), []):
702  # iov_present was replaced by copied iovs so it should be deleted
703  if len(iov_present.newiov) == 0:
704  iovs_to_delete.append(iov_present)
705  # iov_present was partially overlapping by copied iovs so it should be modified
706  elif len(iov_present.newiov) == 1:
707  if not list(iov_present.newiov)[0].tuple == iov_present.iov:
708  iovs_to_modify.append((iov_present, list(iov_present.newiov)[0].tuple))
709  # iov_present was split by copied iovs so its first iov should be modified and new iovs should be created
710  else:
711  newiovs = list(iov_present.newiov)
712  iovs_to_modify.append((iov_present, newiovs[0].tuple))
713  for newiov in newiovs[1:]:
714  iovs_to_create.append((iov_present, newiov.tuple))
715 
716  # If I fix the overlaps do not copy iovs that can be obtained extending iovs already present
717  if args.replace:
718  iovs_to_copy = [i for i in iovs_to_copy if i not in iovs_not_to_copy]
719 
720  # Print nice table
721  table = [[i.iov_id, i.name, i.revision] + list(i.iov) for i in iovs_to_copy]
722  table.insert(0, ["IovId", "Name", "Rev", "First Exp", "First Run", "Final Exp", "Final Run"])
723  columns = [9, "+", 6, 6, 6, 6, 6]
724  B2INFO(f"Copying the following iovs to {args.output}")
725  pretty_print_table(table, columns)
726 
727  if (iovs_to_delete or iovs_to_modify or iovs_to_create) and not args.replace:
728  B2WARNING("Inserting the iovs will create overlaps,"
729  f" to avoid them the changes below should be implemented on {args.tag}")
730 
731  if iovs_to_delete:
732  B2WARNING("iovs to be deleted")
733  # Sort by payload name and iov for nicer logs
734  iovs_to_delete.sort(key=lambda x: x.iov[:2])
735  iovs_to_delete.sort(key=lambda x: x.name)
736  # Print nice table
737  table = [[i.iov_id, i.name, i.revision] + list(i.iov) for i in iovs_to_delete]
738  table.insert(0, ["IovId", "Name", "Rev", "First Exp", "First Run", "Final Exp", "Final Run"])
739  columns = [9, "+", 6, 6, 6, 6, 6]
740  pretty_print_table(table, columns)
741  if iovs_to_modify:
742  B2WARNING("iovs to be modified")
743  # Sort by payload name and iov for nicer logs
744  iovs_to_modify.sort(key=lambda x: x[0].iov[:2])
745  iovs_to_modify.sort(key=lambda x: x[0].name)
746  # Print nice table
747  table = [[i[0].iov_id, i[0].name, i[0].revision] + list(i[0].iov) + list(i[1]) for i in iovs_to_modify]
748  table.insert(0, ["IovId", "Name", "Rev", "Old First Exp", "Old First Run", "Old Final Exp", "Old Final Run",
749  "New First Exp", "New First Run", "New Final Exp", "New Final Run"])
750  columns = [9, "+", 6, 6, 6, 6, 6, 6, 6, 6, 6]
751  pretty_print_table(table, columns)
752  if iovs_to_create:
753  B2WARNING("iovs to be created")
754  # Sort by payload name and iov for nicer logs
755  iovs_to_create.sort(key=lambda x: x[1][:2])
756  iovs_to_create.sort(key=lambda x: x[0].name)
757  # Print nice table
758  table = [[i[0].name, i[0].revision] + list(i[1]) for i in iovs_to_create]
759  table.insert(0, ["Name", "Rev", "First Exp", "First Run", "Final Exp", "Final Run"])
760  columns = ["+", 6, 6, 6, 6, 6]
761  pretty_print_table(table, columns)
762 
763  if (iovs_to_delete or iovs_to_modify or iovs_to_create) and not args.replace:
764  B2WARNING("To apply them use the option --replace")
765  B2WARNING("If instead you want these overlaps to be present enter"
766  " the output global tag name again to confirm this action: ")
767  gt_check = input()
768  if gt_check != args.output:
769  B2ERROR("global tag names do not match.")
770  return 1
771 
772  if not args.dry_run:
773  set_cdb_authentication_token(db, args.auth_token)
774  try:
775  command_iovs_helper.modify_db(lambda args: db.create_iov(output_id, *args),
776  [(i.payload_id, *i.iov) for i in iovs_to_copy])
777  if args.replace:
778  command_iovs_helper.modify_db(db.delete_iov, [i.iov_id for i in iovs_to_delete], "delete")
779  command_iovs_helper.modify_db(lambda args: db.modify_iov(*args),
780  [(i[0].iov_id, *i[1]) for i in iovs_to_modify], "modify")
781  command_iovs_helper.modify_db(lambda args: db.create_iov(output_id, *args),
782  [(i[0].payload_id, *i[1]) for i in iovs_to_create], "create")
783  except RuntimeError:
784  return 1
785 
786  return 0
787 
788 
789 def command_iovs_modify(args, db=None):
790  """
791  Modify iovs from a globaltag
792 
793  This command allows to modify the iovs from a globaltags, optionally limiting the iovs to be modified to those
794  of a specific payload, revision, IoV id or run range.
795  """
796  command_iovs_helper = CommandIoVsHelper("modify", args, db)
797  if db is None:
798  command_iovs_helper.add_arguments()
799  return
800 
801  new_iov = args.new_iov.split()
802  # Transform integer values of iov in int and keep '*' as strings. Raise error if something different is given.
803  for i in range(len(new_iov)):
804  try:
805  new_iov[i] = int(new_iov[i])
806  except ValueError:
807  if new_iov[i] != '*':
808  raise ValueError(f"Invalid IOV value: {new_iov[i]} should be an integer or '*'")
809 
810  iovs_to_modify = []
811  for iov in command_iovs_helper.get_iovs():
812  new_iow_ = new_iov.copy()
813  for i in range(len(new_iow_)):
814  if new_iow_[i] == '*':
815  new_iow_[i] = iov.iov[i]
816  iovs_to_modify.append((iov, new_iow_))
817 
818  table = [[i[0].iov_id, i[0].name, i[0].revision] + list(i[0].iov) + list(i[1]) for i in iovs_to_modify]
819  table.insert(0, ["IovId", "Name", "Rev", "Old First Exp", "Old First Run", "Old Final Exp", "Old Final Run",
820  "New First Exp", "New First Run", "New Final Exp", "New Final Run"])
821  columns = [9, "+", 6, 6, 6, 6, 6, 6, 6, 6, 6]
822 
823  B2INFO("Changing the following iovs")
824  pretty_print_table(table, columns)
825 
826  if not args.dry_run:
827  set_cdb_authentication_token(db, args.auth_token)
828  try:
829  command_iovs_helper.modify_db(lambda args: db.modify_iov(*args),
830  [(i[0].iov_id, *i[1]) for i in iovs_to_modify])
831  except RuntimeError:
832  return 1
833 
834  return 0
_args
argparse.ArgumentParser instance
past_dict
Dictionary with past participles.
def modify_db(self, func, func_args, whichcommand=None)
db
conditions_db.ConditionsDB instance
num_all_iovs
number of iovs before payload and revision selection
def __init__(self, whichcommand, args, db)
whichcommand
from whichcommand it is called (copy, delete or modify)