# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0
"""This module describes base classes for classads and advertisers
"""
import os
import time
from . import condorManager, logSupport
###############################################################################
# Generic Classad Structure
###############################################################################
[docs]
class Classad:
"""Base class describing a classad."""
def __init__(self, adType, advertiseCmd, invalidateCmd):
"""Constructor
Args:
adType (str): Type of the classad
advertiseCmd (str): Condor update-command to advertise this classad
invalidateCmd (str): Condor update-command to invalidate this classad
"""
self.adType = adType
self.adAdvertiseCmd = advertiseCmd
self.adInvalidateCmd = invalidateCmd
self.adParams = {}
"""
try:
self.adParams
except Exception:
self.adParams = {}
"""
self.adParams["MyType"] = self.adType
self.adParams["GlideinMyType"] = self.adType
self.adParams["GlideinWMSVersion"] = "UNKNOWN"
[docs]
def update(self, params_dict, prefix=""):
"""Update or Add ClassAd attributes
Args:
params_dict: new attributes
prefix: prefix used for the attribute names (Default: "")
"""
for k, v in list(params_dict.items()):
if isinstance(v, int):
# don't quote ints
self.adParams[f"{prefix}{k}"] = v
else:
escaped_v = str(v).replace("\n", "\\n")
self.adParams[f"{prefix}{k}"] = "%s" % escaped_v
[docs]
def writeToFile(self, filename, append=True):
"""Write a ClassAd to file, adding a blank line if in append mode to separate the ClassAd
There can be no empty line at the beginning of the file:
https://htcondor-wiki.cs.wisc.edu/index.cgi/tktview?tn=5147
Args:
filename: file to write to
append: write mode if False, append if True (Default)
"""
o_flag = "a"
if not append:
o_flag = "w"
try:
f = open(filename, o_flag)
except Exception:
raise
with f:
if append and f.tell() > 0:
# Write empty line when in append mode to be considered a separate classad
# Skip at the beginning of the file (HTCondor bug #5147)
# (one or more empty lines separate multiple classads on the same file)
f.write("\n")
f.write("%s" % self)
def __str__(self):
"""String representation of the classad."""
ad = ""
for key, value in self.adParams.items():
if isinstance(value, str) or isinstance(value, str):
# Format according to Condor String Literal definition
# http://research.cs.wisc.edu/htcondor/manual/v7.8/4_1HTCondor_s_ClassAd.html#SECTION005121
classad_value = value.replace('"', r"\"")
ad += f'{key} = "{classad_value}"\n'
else:
ad += f"{key} = {value}\n"
return ad
###############################################################################
# Generic Classad Advertiser
###############################################################################
[docs]
class ClassadAdvertiser:
"""
Base Class to handle the advertisement of classads to condor pools.
It contains a dictionary of classads keyed by the classad name and
functions to do advertisement and invalidation of classads
"""
def __init__(self, pool=None, multi_support=False, tcp_support=False):
"""
Constructor
@type pool: string
@param pool: Collector address
@type multi_support: bool
@param multi_support: True if the installation support advertising multiple classads with one condor_advertise command. Defaults to False.
"""
# Dictionary of classad objects
self.classads = {}
self.pool = pool
self.multiAdvertiseSupport = multi_support
self.multiClassadDelimiter = "\n"
self.tcpAdvertiseSupport = tcp_support
# Following data members should be overridden.
# Use generic defaults here.
self.adType = "glideclassad"
self.adAdvertiseCmd = "UPDATE_AD_GENERIC"
self.adInvalidateCmd = "INVALIDATE_ADS_GENERIC"
# gcs_ac = glide-classad-support_advertise-classad
self.advertiseFilePrefix = "gcs_ac"
[docs]
def addClassad(self, name, ad_obj):
"""
Adds the classad to the classad dictionary
@type name: string
@param name: Name of the classad
@type ad_obj: ClassAd
@param ad_obj: Actual classad object
"""
self.classads[name] = ad_obj
[docs]
def classadToFile(self, ad):
"""
Write classad to the file and return the filename
@type ad: string
@param ad: Name of the classad
@rtype: string
@return: Name of the file
"""
fname = self.getUniqClassadFilename()
try:
with open(fname, "w") as fd:
fd.write("%s" % self.classads[ad])
except Exception:
logSupport.log.error("Error creating a classad file %s" % fname)
return ""
return fname
[docs]
def classadsToFile(self, ads):
"""
Write multiple classads to a file and return the filename.
Use only when multi advertise is supported by condor.
@type ads: list
@param ads: Classad names
@rtype: string
@return: Filename containing all the classads to advertise
"""
fname = self.getUniqClassadFilename()
try:
with open(fname, "w") as fd:
for ad in ads:
fd.write("%s" % self.classads[ad])
# Condor uses an empty line as classad delimiter
# Append an empty line for advertising multiple classads
fd.write(self.multiClassadDelimiter)
except Exception:
logSupport.log.error("Error creating a classad file %s" % fname)
return ""
return fname
[docs]
def doAdvertise(self, fname):
"""
Do the actual advertisement of classad(s) in the file
@type fname: string
@param fname: File name containing classad(s)
"""
if (fname) and (fname != ""):
try:
exe_condor_advertise(
fname,
self.adAdvertiseCmd,
self.pool,
is_multi=self.multiAdvertiseSupport,
use_tcp=self.tcpAdvertiseSupport,
)
finally:
os.remove(fname)
else:
raise RuntimeError("Failed advertising %s classads" % self.adType)
[docs]
def advertiseClassads(self, ads=None):
"""
Advertise multiple classads to the pool
@type ads: list
@param ads: classad names to advertise
"""
if (ads is None) or (len(ads) == 0):
logSupport.log.info("There are 0 classads to advertise")
return
logSupport.log.info("There are %i classads to advertise" % len(ads))
if self.multiAdvertiseSupport:
fname = self.classadsToFile(ads)
self.doAdvertise(fname)
else:
# There is no multi advertise support.
# Advertise one classad at a time.
for ad in ads:
self.advertiseClassad(ad)
[docs]
def advertiseClassad(self, ad):
"""
Advertise the classad to the pool
@type ad: string
@param ad: Name of the classad
"""
fname = self.classadToFile(ad)
self.doAdvertise(fname)
[docs]
def advertiseAllClassads(self):
"""
Advertise all the known classads to the pool
"""
self.advertiseClassads(list(self.classads.keys()))
[docs]
def invalidateClassad(self, ad):
"""
Invalidate the classad from the pool
@type type: string
@param type: Name of the classad
"""
self.invalidateConstrainedClassads('Name == "%s"' % ad)
[docs]
def invalidateAllClassads(self):
"""
Invalidate all the known classads
"""
for ad in list(self.classads.keys()):
self.invalidateClassad(ad)
[docs]
def invalidateConstrainedClassads(self, constraint):
"""
Invalidate classads from the pool matching the given constraints
@type type: string
@param type: Condor constraints for filtering the classads
"""
try:
fname = self.getUniqClassadFilename()
with open(fname, "w") as fd:
fd.write('MyType = "Query"\n')
fd.write('TargetType = "%s"\n' % self.adType)
fd.write("Requirements = %s" % constraint)
exe_condor_advertise(
fname,
self.adInvalidateCmd,
self.pool,
is_multi=self.multiAdvertiseSupport,
use_tcp=self.tcpAdvertiseSupport,
)
finally:
if fd:
os.remove(fname)
else:
logSupport.log.error("Error creating a classad file %s" % fname)
[docs]
def getAllClassads(self):
"""
Return all the known classads
@rtype: string
@return: All the known classads delimited by empty line
"""
ads = ""
for ad in list(self.classads.keys()):
ads = f"{ads}{self.classads[ad]}\n"
return ads
[docs]
def getUniqClassadFilename(self):
"""
Return a uniq file name for advertising/invalidating classads
@rtype: string
@return: Filename
"""
return generate_classad_filename(prefix=self.advertiseFilePrefix)
###############################################################################
# Generic Utility Functions used with classads
###############################################################################
[docs]
def generate_classad_filename(prefix="gwms_classad"):
"""
Return a uniq file name for advertising/invalidating classads
@type prefix: string
@param prefix: Prefix to be used for the filename
@rtype: string
@return: Filename
"""
# Get a 9 digit number that will stay 9 digit for next 25 years
short_time = time.time() - 1.05e9
fname = "/tmp/%s_%li_%li" % (prefix, short_time, os.getpid())
return fname
############################################################
#
# I N T E R N A L - Do not use
#
############################################################
[docs]
def exe_condor_advertise(fname, command, pool, is_multi=False, use_tcp=False):
"""
Wrapper to execute condorAdvertise from the condorManager
"""
logSupport.log.debug(f"CONDOR ADVERTISE {fname} {command} {pool} {is_multi} {use_tcp}")
return condorManager.condorAdvertise(fname, command, use_tcp, is_multi, pool)