15 Python interface to the ConditionsDB
20 from basf2
import B2FATAL, B2ERROR, B2INFO, B2WARNING, conditions
22 from requests.packages.urllib3.fields
import RequestField
23 from requests.packages.urllib3.filepost
import encode_multipart_formdata
26 from versioning
import upload_global_tag, jira_global_tag_v2
27 from collections
import defaultdict
28 from concurrent.futures
import ThreadPoolExecutor, wait
as futures_wait
31 from typing
import Union
37 def encode_name(name):
38 """Escape name to be used in an url"""
39 return urllib.parse.quote(name, safe=
"")
42 def file_checksum(filename):
43 """Calculate md5 hash of file"""
44 md5hash = hashlib.md5()
45 with open(filename,
"rb")
as data:
46 md5hash.update(data.read())
47 return md5hash.hexdigest()
50 def chunks(container, chunk_size):
51 """Cut a container in chunks of max. chunk_size"""
54 chunk = tuple(itertools.islice(it, chunk_size))
60 def get_cdb_authentication_token(path=None):
62 Helper function for correctly retrieving the CDB authentication token (either via file or via issuing server).
64 :param path: Path to a file containing a CDB authentication token; if None, the function will use
65 a default path (``${HOME}/b2cdb_${BELLE2_USER}.token`` or ``/tmp/b2cdb_${BELLE2_USER}.token``) to look for a token.
71 path_to_token = os.path.join(os.getenv(
'HOME', tempfile.gettempdir()), f
'b2cdb_{os.getenv("BELLE2_USER", None)}.token')
74 if os.path.isfile(path_to_token):
75 with open(path_to_token)
as token_file:
76 response = requests.get(
'https://token.belle2.org/check', verify=
False, params={
'token': token_file.read().strip()})
77 if response.status_code == 400:
78 B2INFO(f
'The file {path_to_token} contains an invalid token, getting a new token...')
79 os.unlink(path_to_token)
80 elif response.status_code > 400:
81 B2WARNING(
"The validity of the existing token could not be checked. Trying to connect to CDB anyway.")
84 if not os.path.isfile(path_to_token):
85 username = os.environ[
'BELLE2_USER']
86 othername = input(f
'\nConfirm your DESY user name by pressing enter or type the correct one [{username}]: ')
89 password = getpass.getpass(
"Please type your DESY password to request a CDB token: ")
90 response = requests.get(
'https://token.belle2.org', verify=
False, auth=(username, password))
91 print(response.content)
92 if response.status_code == requests.codes.ok:
93 with open(path_to_token,
'wb')
as token_file:
94 os.chmod(path_to_token, stat.S_IRUSR | stat.S_IWUSR)
95 token_file.write(response.content)
97 B2ERROR(
'Failed to get token')
101 with open(path_to_token)
as token_file:
102 return token_file.read().strip()
105 def set_cdb_authentication_token(cdb_instance, auth_token=None):
107 Helper function for setting the CDB authentication token.
109 :param cdb_instance: An instance of the ``ConditionsDB`` class.
110 :param auth_token: A CDB authentication token: if ``None``, it is automatically retrieved from the issuing server
113 if auth_token
is not None:
114 cdb_instance.set_authentication_token(auth_token)
117 token = get_cdb_authentication_token(os.getenv(
'BELLE2_CDB_AUTH_TOKEN', default=
None))
118 cdb_instance.set_authentication_token(token)
122 """Simple class to present bearer token instead of username/password"""
125 """Construct from a token"""
130 """Update headers to include token"""
131 r.headers[
"X-Authorization"] = self.
_authtoken_authtoken
136 """Small container class to help compare payload information for efficient
137 comparison between globaltags"""
141 """Set all internal members from the json information of the payload and the iov.
144 payload (dict): json information of the payload as returned by REST api
145 iov (dict): json information of the iov as returned by REST api
148 iov = {
"payloadIovId":
None,
"expStart":
None,
"runStart":
None,
"expEnd":
None,
"runEnd":
None}
151 payload[
'payloadId'],
152 payload[
'basf2Module'][
'name'],
155 payload[
'payloadUrl'],
158 (iov[
"expStart"], iov[
"runStart"], iov[
"expEnd"], iov[
"runEnd"]),
161 def __init__(self, payload_id, name, revision, checksum, payload_url, base_url, iov_id=None, iov=None):
163 Create a new object from the given information
184 """Return the full url to the payload on the server"""
188 """Make object hashable"""
192 """Check if two payloads are equal"""
193 return (self.
namename, self.
checksumchecksum, self.
ioviov) == (other.name, other.checksum, other.iov)
196 """Sort payloads by name, iov, revision"""
197 return (self.
namename.lower(), self.
ioviov, self.
revisionrevision) < (other.name.lower(), other.iov, other.revision)
200 """return a human readable name for the IoV"""
201 if self.
ioviov
is None:
204 if self.
ioviov == (0, 0, -1, -1):
207 e1, r1, e2, r2 = self.
ioviov
209 if r1 == 0
and r2 == -1:
212 return f
"exp {e1}, runs {r1}+"
214 return f
"exp {e1}, run {r1}"
216 return f
"exp {e1}, runs {r1} - {r2}"
218 if e2 == -1
and r1 == 0:
219 return f
"exp {e1} - forever"
221 return f
"exp {e1}, run {r1} - forever"
222 elif r1 == 0
and r2 == -1:
223 return f
"exp {e1}-{e2}, all runs"
225 return f
"exp {e1}, run {r1} - exp {e2}, all runs"
227 return f
"exp {e1}, run {r1} - exp {e2}, run {r2}"
231 """Class to interface conditions db REST interface"""
234 BASE_URLS = [conditions.default_metadata_provider_url]
237 """Class to be thrown by request() if there is any error"""
242 """Resolve the list of server urls. If a url is given just return it.
243 Otherwise return servers listed in BELLE2_CONDB_SERVERLIST or the
247 given_url (str): Explicit base_url. If this is not None it will be
248 returned as is in a list of length 1
251 a list of urls to try for database connectivity
254 base_url_list = ConditionsDB.BASE_URLS[:]
255 base_url_env = os.environ.get(
"BELLE2_CONDB_SERVERLIST",
None)
256 if given_url
is not None:
257 base_url_list = [given_url]
258 elif base_url_env
is not None:
259 base_url_list = base_url_env.split()
260 B2INFO(
"Getting Conditions Database servers from Environment:")
261 for i, url
in enumerate(base_url_list, 1):
262 B2INFO(f
" {i}. {url}")
265 for url
in base_url_list:
266 if url.startswith(
"http://"):
267 full_list.append(
"https" + url[4:])
269 full_list.append(url)
272 def __init__(self, base_url=None, max_connections=10, retries=3):
274 Create a new instance of the interface
277 base_url (string): base url of the rest interface
278 max_connections (int): number of connections to keep open, mostly useful for threaded applications
279 retries (int): number of retries in case of connection problems
285 adapter = requests.adapters.HTTPAdapter(
286 pool_connections=max_connections, pool_maxsize=max_connections,
287 max_retries=retries, pool_block=
True
289 self.
_session_session.mount(
"http://", adapter)
290 self.
_session_session.mount(
"https://", adapter)
292 if "BELLE2_CONDB_PROXY" in os.environ:
294 "http": os.environ.get(
"BELLE2_CONDB_PROXY"),
295 "https": os.environ.get(
"BELLE2_CONDB_PROXY"),
298 base_url_list = ConditionsDB.get_base_urls(base_url)
300 for url
in base_url_list:
305 req.raise_for_status()
306 except requests.RequestException
as e:
307 B2WARNING(f
"Problem connecting to {url}:\n {e}\n Trying next server ...")
311 B2FATAL(
"No working database servers configured, giving up")
317 self.
_session_session.headers.update({
"Accept":
"application/json",
"Cache-Control":
"no-cache"})
321 Set authentication token when talking to the database
324 token (str): JWT to hand to the database. Will not be checked
329 def request(self, method, url, message=None, *args, **argk):
331 Request function, similar to requests.request but adding the base_url
334 method (str): GET, POST, etc.
335 url (str): url for the request, base_url will be prepended
336 message (str): message to show when starting the request and if it fails
338 All other arguments will be forwarded to requests.request.
340 if message
is not None:
345 except requests.exceptions.ConnectionError
as e:
346 B2FATAL(
"Could not access '" + self.
_base_url_base_url + url.lstrip(
"/") +
"': " + str(e))
348 if req.status_code >= 300:
352 response = req.json()
353 message = response.get(
"message",
"")
354 colon =
": " if message.strip()
else ""
355 error =
"Request {method} {url} returned {code} {reason}{colon}{message}".format(
356 method=method, url=url,
357 code=response[
"code"],
358 reason=response[
"reason"],
362 except json.JSONDecodeError:
364 error =
"Request {method} {url} returned non JSON response {code}: {content}".format(
365 method=method, url=url,
366 code=req.status_code,
370 if message
is not None:
375 if method !=
"HEAD" and req.status_code != requests.codes.no_content:
378 except json.JSONDecodeError
as e:
379 B2INFO(f
"Invalid response: {req.content}")
381 .format(e, method=method, url=url))
385 """Get a list of all globaltags. Returns a dictionary with the globaltag
386 names and the corresponding ids in the database"""
389 req = self.
requestrequest(
"GET",
"/globalTags")
391 B2ERROR(f
"Could not get the list of globaltags: {e}")
395 for tag
in req.json():
396 result[tag[
"name"]] = tag
401 """Check whether the globaltag with the given name exists."""
404 self.
requestrequest(
"GET",
"/globalTag/{globalTagName}".format(globalTagName=encode_name(name)))
411 """Get the id of the globaltag with the given name. Returns either the
412 id or None if the tag was not found"""
415 req = self.
requestrequest(
"GET",
"/globalTag/{globalTagName}".format(globalTagName=encode_name(name)))
417 B2ERROR(f
"Cannot find globaltag '{name}': {e}")
424 Get the dictionary describing the given globaltag type (currently
425 one of DEV or RELEASE). Returns None if tag type was not found.
428 req = self.
requestrequest(
"GET",
"/globalTagType")
430 B2ERROR(f
"Could not get list of valid globaltag types: {e}")
433 types = {e[
"name"]: e
for e
in req.json()}
438 B2ERROR(
"Unknown globaltag type: '{}', please use one of {}".format(name,
", ".join(types)))
443 Create a new globaltag
445 info = {
"name": name,
"description": description,
"modifiedBy": user,
"isDefault":
False}
447 req = self.
requestrequest(
"POST",
"/globalTag/DEV", f
"Creating globaltag {name}", json=info)
449 B2ERROR(f
"Could not create globaltag {name}: {e}")
454 def get_all_iovs(self, globalTag, exp=None, run=None, message=None, run_range=None, fully_contained=False):
456 Return list of all payloads in the given globaltag where each element is
457 a `PayloadInformation` instance
460 gobalTag (str): name of the globaltag
461 exp (int): if given limit the list of payloads to the ones valid for
462 the given exp,run combination
463 run (int): if given limit the list of payloads to the ones valid for
464 the given exp,run combination
465 message (str): additional message to show when downloading the
466 payload information. Will be directly appended to
467 "Obtaining lists of iovs for globaltag {globalTag}"
468 run_range (tuple): if given limit the list of payloads to the ones
469 overlapping with the given run range, if
470 fully_contained (bool): if True and the run_range is not None it limits
471 the list of payloads to the ones fully contained in the given run range
474 Both, exp and run, need to be given at the same time. Just supplying
475 an experiment or a run number will not work
477 globalTag = encode_name(globalTag)
480 if run_range
is not None:
482 message += f
" [fully contained in {tuple(run_range)}]"
484 message += f
" [valid in {tuple(run_range)}]"
488 msg = f
"Obtaining list of iovs for globaltag {globalTag}, exp={exp}, run={run}{message}"
489 req = self.
requestrequest(
"GET",
"/iovPayloads", msg, params={
'gtName': globalTag,
'expNumber': exp,
'runNumber': run})
491 msg = f
"Obtaining list of iovs for globaltag {globalTag}{message}"
492 req = self.
requestrequest(
"GET", f
"/globalTag/{globalTag}/globalTagPayloads", msg)
494 for item
in req.json():
495 payload = item[
"payload" if 'payload' in item
else "payloadId"]
496 if "payloadIov" in item:
497 iovs = [item[
'payloadIov']]
499 iovs = item[
'payloadIovs']
502 if run_range
is not None:
503 iov_ =
IntervalOfValidity(iov[
'expStart'], iov[
'runStart'], iov[
'expEnd'], iov[
'runEnd'])
505 if not iov_ & run_range == iov_:
508 if iov_ & run_range
is None:
510 all_iovs.append(PayloadInformation.from_json(payload, iov))
517 Get a list of all defined payloads (for the given global_tag or by default for all).
518 Returns a dictionary which maps (module, checksum) to the payload id.
523 req = self.
requestrequest(
"GET",
"/globalTag/{global_tag}/payloads"
524 .format(global_tag=encode_name(global_tag)))
526 req = self.
requestrequest(
"GET",
"/payloads")
528 B2ERROR(f
"Cannot get list of payloads: {e}")
532 for payload
in req.json():
533 module = payload[
"basf2Module"][
"name"]
534 checksum = payload[
"checksum"]
535 result[(module, checksum)] = payload[
"payloadId"]
541 Check for the existence of payloads in the database.
544 payloads (list((str,str))): A list of payloads to check for. Each
545 payload needs to be a tuple of the name of the payload and the
546 md5 checksum of the payload file.
547 information (str): The information to be extracted from the
551 A dictionary with the payload identifiers (name, checksum) as keys
552 and the requested information as values for all payloads which are already
553 present in the database.
556 search_query = [{
"name": e[0],
"checksum": e[1]}
for e
in payloads]
558 req = self.
requestrequest(
"POST",
"/checkPayloads", json=search_query)
560 B2ERROR(f
"Cannot check for existing payloads: {e}")
564 for payload
in req.json():
565 module = payload[
"basf2Module"][
"name"]
566 checksum = payload[
"checksum"]
567 result[(module, checksum)] = payload[information]
573 Get the revision numbers of payloads in the database.
576 entries (list): A list of payload entries.
577 Each entry must have the attributes module and checksum.
583 result = self.
check_payloadscheck_payloads([(entry.module, entry.checksum)
for entry
in entries],
"revision")
587 for entry
in entries:
588 entry.revision = result.get((entry.module, entry.checksum), 0)
597 module (str): name of the module
598 filename (str): name of the file
599 checksum (str): md5 hexdigest of the file. Will be calculated automatically if not given
602 checksum = file_checksum(filename)
608 (filename, open(filename,
"rb").read(),
"application/x-root"),
609 (
"json", json.dumps({
"checksum": checksum,
"isDefault":
False}),
"application/json"),
614 for name, contents, mimetype
in files:
615 rf = RequestField(name=name, data=contents)
616 rf.make_multipart(content_type=mimetype)
619 post_body, content_type = encode_multipart_formdata(fields)
620 content_type =
''.join((
'multipart/mixed',) + content_type.partition(
';')[1:])
621 headers = {
'Content-Type': content_type}
626 req = self.
requestrequest(
"POST",
"/package/dbstore/module/{moduleName}/payload"
627 .format(moduleName=encode_name(module)),
628 data=post_body, headers=headers)
630 B2ERROR(f
"Could not create Payload: {e}")
633 return req.json()[
"payloadId"]
635 def create_iov(self, globalTagId, payloadId, firstExp, firstRun, finalExp, finalRun):
640 globalTagId (int): id of the globaltag, obtain with get_globalTagId()
641 payloadId (int): id of the payload, obtain from create_payload() or get_payloads()
642 firstExp (int): first experiment for which this iov is valid
643 firstRun (int): first run for which this iov is valid
644 finalExp (int): final experiment for which this iov is valid
645 finalRun (int): final run for which this iov is valid
648 payloadIovId of the created iov, None if creation was not successful
653 local_variables = locals()
654 variables = {e: int(local_variables[e])
for e
in
655 [
"globalTagId",
"payloadId",
"firstExp",
"firstRun",
"finalExp",
"finalRun"]}
657 B2ERROR(
"create_iov: All parameters need to be integers")
662 req = self.
requestrequest(
"POST",
"/globalTagPayload/{globalTagId},{payloadId}"
663 "/payloadIov/{firstExp},{firstRun},{finalExp},{finalRun}".format(**variables))
665 B2ERROR(f
"Could not create IOV: {e}")
668 return req.json()[
"payloadIovId"]
674 iovId (int): id of the iov to be deleted
677 self.
requestrequest(
"DELETE", f
"/payloadIov/{iovId}")
679 B2ERROR(f
"Could not delete IOV: {e}")
681 def modify_iov(self, iovId, firstExp, firstRun, finalExp, finalRun):
682 """Modify the validity range of a given iov
685 iovId (int): id of the iov to be modified
686 firstExp (int): first experiment for which this iov is valid
687 firstRun (int): first run for which this iov is valid
688 finalExp (int): final experiment for which this iov is valid
689 finalRun (int): final run for which this iov is valid
692 querystring = {
"expStart": str(firstExp),
"runStart": str(firstRun),
"expEnd": str(finalExp),
"runEnd": str(finalRun)}
693 self.
requestrequest(
"PUT", f
"/payloadIov/{iovId}", params=querystring)
695 B2ERROR(f
"Could not modify IOV: {e}")
697 def get_iovs(self, globalTagName, payloadName=None):
698 """Return existing iovs for a given tag name. It returns a dictionary
699 which maps (payloadId, first runId, final runId) to iovId
702 globalTagName(str): Global tag name.
703 payloadName(str): Payload name (if None, selection by name is
708 req = self.
requestrequest(
"GET",
"/globalTag/{globalTagName}/globalTagPayloads"
709 .format(globalTagName=encode_name(globalTagName)))
715 for payload
in req.json():
716 payloadId = payload[
"payloadId"][
"payloadId"]
717 if payloadName
is not None:
718 if payload[
"payloadId"][
"basf2Module"][
"name"] != payloadName:
720 for iov
in payload[
"payloadIovs"]:
721 iovId = iov[
"payloadIovId"]
722 firstExp, firstRun = iov[
"expStart"], iov[
"runStart"]
723 finalExp, finalRun = iov[
"expEnd"], iov[
"runEnd"]
724 result[(payloadId, firstExp, firstRun, finalExp, finalRun)] = iovId
728 def upload(self, filename, global_tag, normalize=False, ignore_existing=False, nprocess=1, uploaded_entries=None):
730 Upload a testing payload storage to the conditions database.
733 filename (str): filename of the testing payload storage file that should be uploaded
734 global_tage (str): name of the globaltag to which the data should be uploaded
735 normalize (Union[bool, str]): if True the payload root files will be normalized to have the same checksum for the
736 same content, if normalize is a string in addition the file name in the root file metadata will be set to it
737 ignore_existing (bool): if True do not upload payloads that already exist
738 nprocess (int): maximal number of parallel uploads
739 uploaded_entries (list): the list of successfully uploaded entries
742 True if the upload was successful
747 B2INFO(f
"Reading payload list from {filename}")
748 entries = parse_testing_payloads_file(filename)
750 B2ERROR(f
"Problems with testing payload storage file {filename}, exiting")
754 B2INFO(f
"No payloads found in {filename}, exiting")
757 B2INFO(f
"Found {len(entries)} iovs to upload")
763 tagId = tagId[
"globalTagId"]
767 entries = sorted(set(reversed(entries)))
770 name = normalize
if normalize
is not True else None
772 e.normalize(name=name)
776 payloads = defaultdict(list)
778 payloads[(e.module, e.checksum)].append(e)
780 existing_payloads = {}
783 def upload_payload(item):
784 """Upload a payload file if necessary but first check list of existing payloads"""
786 if key
in existing_payloads:
787 B2INFO(f
"{key[0]} (md5:{key[1]}) already existing in database, skipping.")
788 payload_id = existing_payloads[key]
791 payload_id = self.
create_payloadcreate_payload(entry.module, entry.filename, entry.checksum)
792 if payload_id
is None:
795 B2INFO(f
"Created new payload {payload_id} for {entry.module} (md5:{entry.checksum})")
797 for entry
in entries:
798 entry.payload = payload_id
803 """Create an iov if necessary but first check the list of existing iovs"""
804 if entry.payload
is None:
807 iov_key = (entry.payload,) + entry.iov_tuple
808 if iov_key
in existing_iovs:
809 entry.iov = existing_iovs[iov_key]
810 B2INFO(f
"IoV {entry.iov_tuple} for {entry.module} (md5:{entry.checksum}) already existing in database, skipping.")
812 entry.payloadIovId = self.
create_iovcreate_iov(tagId, entry.payload, *entry.iov_tuple)
813 if entry.payloadIovId
is None:
816 B2INFO(f
"Created IoV {entry.iov_tuple} for {entry.module} (md5:{entry.checksum})")
821 with ThreadPoolExecutor(max_workers=nprocess)
as pool:
824 if not ignore_existing:
825 B2INFO(
"Downloading information about existing payloads and iovs...")
828 existing_payloads = {}
830 def create_future(iter, func, callback=None):
831 fn = pool.submit(iter, func)
832 if callback
is not None:
833 fn.add_done_callback(callback)
836 def update_iovs(iovs):
837 existing_iovs.update(iovs.result())
838 B2INFO(f
"Found {len(existing_iovs)} existing iovs in {global_tag}")
840 def update_payloads(payloads):
841 existing_payloads.update(payloads.result())
842 B2INFO(f
"Found {len(existing_payloads)} existing payloads")
844 create_future(self.
get_iovsget_iovs, global_tag, update_iovs)
846 for chunk
in chunks(payloads.keys(), 1000):
847 create_future(self.
check_payloadscheck_payloads, chunk, update_payloads)
849 futures_wait(futures)
852 failed_payloads = sum(0
if result
else 1
for result
in pool.map(upload_payload, payloads.items()))
853 if failed_payloads > 0:
854 B2ERROR(f
"{failed_payloads} payloads could not be uploaded")
858 for entry
in pool.map(create_iov, entries):
860 if uploaded_entries
is not None:
861 uploaded_entries.append(entry)
865 B2ERROR(f
"{failed_iovs} IoVs could not be created")
868 if uploaded_entries
is not None:
871 return failed_payloads + failed_iovs == 0
875 Upload a testing payload storage to a staging globaltag and create or update a jira issue
878 filename (str): filename of the testing payload storage file that should be uploaded
879 normalize (Union[bool, str]): if True the payload root files will be
880 normalized to have the same checksum for the same content, if
881 normalize is a string in addition the file name in the root file
882 metadata will be set to it
883 data (dict): a dictionary with the information provided by the user:
885 * task: category of globaltag, either main, online, prompt, data, mc, or analysis
886 * tag: the globaltag name
887 * request: type of request, either Update, New, or Modification. The latter two imply task == main because
888 if new payload classes are introduced or payload classes are modified then they will first be included in
889 the main globaltag. Here a synchronization of code and payload changes has to be managed.
890 If new or modified payload classes should be included in other globaltags they must already be in a release.
891 * pull-request: number of the pull request containing new or modified payload classes,
892 only for request == New or Modified
893 * backward-compatibility: description of what happens if the old payload is encountered by the updated code,
894 only for request == Modified
895 * forward-compatibility: description of what happens if a new payload is encountered by the existing code,
896 only for request == Modified
897 * release: the required release version
898 * reason: the reason for the request
899 * description: a detailed description for the globaltag manager
900 * issue: identifier of an existing jira issue (optional)
901 * user: name of the user
902 * time: time stamp of the request
904 password: the password for access to jira or the access token and secret for oauth access
907 True if the upload and jira issue creation/upload was successful
911 data[
'tag'] = upload_global_tag(data[
'task'])
912 if data[
'tag']
is None:
913 data[
'tag'] = f
"temp_{data['task']}_{data['user']}_{data['time']}"
917 if not self.
create_globalTagcreate_globalTag(data[
'tag'], data[
'reason'], data[
'user']):
921 B2INFO(f
"Uploading testing database {filename} to globaltag {data['tag']}")
923 if not self.
uploadupload(filename, data[
'tag'], normalize, uploaded_entries=entries):
928 issue = data[
'issue']
930 issue = jira_global_tag_v2(data[
'task'])
932 issue = {
"components": [{
"name":
"globaltag"}]}
935 if type(issue)
is tuple:
936 description = issue[1].format(**data)
940 |*Upload globaltag* | {data['tag']} |
941 |*Request reason* | {data['reason']} |
942 |*Required release* | {data['release']} |
943 |*Type of request* | {data['request']} |
945 if 'pull-request' in data.keys():
946 description += f
"|*Pull request* | \\#{data['pull-request']} |\n"
947 if 'backward-compatibility' in data.keys():
948 description += f
"|*Backward compatibility* | \\#{data['backward-compatibility']} |\n"
949 if 'forward-compatibility' in data.keys():
950 description += f
"|*Forward compatibility* | \\#{data['forward-compatibility']} |\n"
951 description +=
'|*Details* |' +
''.join(data[
'details']) +
' |\n'
952 if data[
'task'] ==
'online':
953 description +=
'|*Impact on data taking*|' +
''.join(data[
'data_taking']) +
' |\n'
956 description +=
'\nPayloads\n||Name||Revision||IoV||\n'
957 for entry
in entries:
958 description += f
"|{entry.module} | {entry.revision} | ({entry.iov_str()}) |\n"
961 if type(issue)
is dict:
962 issue[
"description"] = description
963 if "summary" in issue.keys():
964 issue[
"summary"] = issue[
"summary"].format(**data)
966 issue[
"summary"] = f
"Globaltag request for {data['task']} by {data['user']} at {data['time']}"
967 if "project" not in issue.keys():
968 issue[
"project"] = {
"key":
"BII"}
969 if "issuetype" not in issue.keys():
970 issue[
"issuetype"] = {
"name":
"Task"}
971 if data[
"task"] ==
"main":
972 issue[
"labels"] = [
"TUPPR"]
974 B2INFO(f
"Creating jira issue for {data['task']} globaltag request")
975 if isinstance(password, str):
976 response = requests.post(
'https://agira.desy.de/rest/api/latest/issue', auth=(data[
'user'], password),
977 json={
'fields': issue})
979 fields = {
'issue': json.dumps(issue)}
980 if 'user' in data.keys():
981 fields[
'user'] = data[
'user']
983 fields[
'token'] = password[0]
984 fields[
'secret'] = password[1]
985 response = requests.post(
'https://b2-master.belle2.org/cgi-bin/jira_issue.py', data=fields)
986 if response.status_code
in range(200, 210):
987 B2INFO(f
"Issue successfully created: https://agira.desy.de/browse/{response.json()['key']}")
989 B2ERROR(
'The creation of the issue failed: ' + requests.status_codes._codes[response.status_code][0])
996 new_issue_config = jira_global_tag_v2(data[
'task'])
997 if isinstance(new_issue_config, dict)
and "assignee" in new_issue_config:
998 user = new_issue_config[
'assignee'].get(
'name',
None)
999 if user
is not None and isinstance(password, str):
1000 response = requests.post(f
'https://agira.desy.de/rest/api/latest/issue/{issue}/watchers',
1001 auth=(data[
'user'], password), json=user)
1002 if response.status_code
in range(200, 210):
1003 B2INFO(f
"Added {user} as watcher to {issue}")
1005 B2WARNING(f
"Could not add {user} as watcher to {issue}: {response.status_code}")
1007 B2INFO(f
"Commenting on jira issue {issue} for {data['task']} globaltag request")
1008 if isinstance(password, str):
1009 response = requests.post(
'https://agira.desy.de/rest/api/latest/issue/%s/comment' % issue,
1010 auth=(data[
'user'], password), json={
'body': description})
1012 fields = {
'id': issue,
'user': user,
'comment': description}
1014 fields[
'token'] = password[0]
1015 fields[
'secret'] = password[1]
1016 response = requests.post(
'https://b2-master.belle2.org/cgi-bin/jira_issue.py', data=fields)
1017 if response.status_code
in range(200, 210):
1018 B2INFO(f
"Issue successfully updated: https://agira.desy.de/browse/{issue}")
1020 B2ERROR(
'The commenting of the issue failed: ' + requests.status_codes._codes[response.status_code][0])
1026 def require_database_for_test(timeout=60, base_url=None):
1027 """Make sure that the database is available and skip the test if not.
1029 This function should be called in test scripts if they are expected to fail
1030 if the database is down. It either returns when the database is ok or it
1031 will signal test_basf2 that the test should be skipped and exit
1034 if os.environ.get(
"BELLE2_CONDB_GLOBALTAG",
None) ==
"":
1035 raise Exception(
"Access to the Database is disabled")
1036 base_url_list = ConditionsDB.get_base_urls(base_url)
1037 for url
in base_url_list:
1039 req = requests.request(
"HEAD", url.rstrip(
'/') +
"/v2/globalTags")
1040 req.raise_for_status()
1041 except requests.RequestException
as e:
1042 B2WARNING(f
"Problem connecting to {url}:\n {e}\n Trying next server ...")
1046 print(
"TEST SKIPPED: No working database servers configured, giving up", file=sys.stderr)
1050 def enable_debugging():
1051 """Enable verbose output of python-requests to be able to debug http connections"""
1055 import http.client
as http_client
1057 http_client.HTTPConnection.debuglevel = 1
1059 logging.basicConfig()
1060 logging.getLogger().setLevel(logging.DEBUG)
1061 requests_log = logging.getLogger(
"requests.packages.urllib3")
1062 requests_log.setLevel(logging.DEBUG)
1063 requests_log.propagate =
True
_authtoken
Authorization header to send with each request.
def __init__(self, token)
def get_payloads(self, global_tag=None)
def get_iovs(self, globalTagName, payloadName=None)
def upload(self, filename, global_tag, normalize=False, ignore_existing=False, nprocess=1, uploaded_entries=None)
def set_authentication_token(self, token)
def delete_iov(self, iovId)
def get_all_iovs(self, globalTag, exp=None, run=None, message=None, run_range=None, fully_contained=False)
def get_globalTagType(self, name)
def check_payloads(self, payloads, information="payloadId")
def has_globalTag(self, name)
def create_iov(self, globalTagId, payloadId, firstExp, firstRun, finalExp, finalRun)
def request(self, method, url, message=None, *args, **argk)
def __init__(self, base_url=None, max_connections=10, retries=3)
def get_revisions(self, entries)
def create_globalTag(self, name, description, user)
def get_globalTagInfo(self, name)
_session
session object to get keep-alive support and connection pooling
def get_base_urls(given_url)
def modify_iov(self, iovId, firstExp, firstRun, finalExp, finalRun)
def staging_request(self, filename, normalize, data, password)
def create_payload(self, module, filename, checksum=None)
_base_url
base url to be prepended to all requests