# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0
import os
import os.path
import shutil
from glideinwms.lib import pubCrypto, symCrypto
############################################################
#
# Configuration
#
############################################################
[docs]
class FactoryConfig:
def __init__(self):
"""Set the default values, user should modify if needed
These values should be consistent w/ creation/lib/c?WConst.py files content
"""
self.glidein_descript_file = "glidein.descript"
self.job_descript_file = "job.descript"
self.job_attrs_file = "attributes.cfg"
self.job_params_file = "params.cfg"
self.job_submit_attrs_file = "submit_attrs.cfg"
self.frontend_descript_file = "frontend.descript"
self.signatures_file = "signatures.sha1"
self.aggregated_stats_file = "aggregated_stats_dict.data"
# global configuration of the module
factoryConfig = FactoryConfig()
############################################################
#
# Generic Class
# You most probably don't want to use these
#
############################################################
[docs]
class ConfigFile:
"""In memory dictionary-like representation of key-value config files
Loads a file composed of
NAME VAL
and creates
self.data[NAME]=convert_function(VAL) # repr is the default conversion
It also defines:
self.config_file="name of file"
This is used only to load into memory and access the dictionary, not to update the
on-disk persistent values
"""
def __init__(self, config_file, convert_function=repr):
self.config_file = config_file
self.load(config_file, convert_function)
[docs]
def load(self, fname, convert_function):
self.data = {}
with open(fname) as fd:
lines = fd.readlines()
for line in lines:
if line[0] == "#":
continue # comment
if len(line.strip()) == 0:
continue # empty line
larr = line.split(None, 1)
lname = larr[0]
if len(larr) == 1:
lval = ""
else:
lval = larr[1][:-1] # strip newline
exec(f"self.data['{lname}']={convert_function(lval)}")
[docs]
def has_key(self, key_name):
return key_name in self.data
def __contains__(self, key_name):
return key_name in self.data
def __str__(self):
output = "\n"
for key in list(self.data.keys()):
output += f"{key} = {str(self.data[key])}, ({type(self.data[key])})\n"
return output
[docs]
class EntryConfigFile(ConfigFile):
"""Load from the entry subdir
It also defines:
self.config_file="name of file with entry directory" (from parent ConfigFile)
self.entry_name="Entry name"
self.config_file_short="name of file" (just the file name since the other had the directory)
"""
def __init__(self, entry_name, config_file, convert_function=repr):
ConfigFile.__init__(self, os.path.join("entry_" + entry_name, config_file), convert_function)
self.entry_name = entry_name
self.config_file_short = config_file
[docs]
class JoinConfigFile(ConfigFile):
"""Load both the main and entry subdir config file and join the results
Data is only read, not saved
`self.data` will contain the joint items (initially the common one,
then is updated using the content of `entry_obj.data`)
It also defines:
self.config_file="name of both files, with and without entry directory"
It is not an actual file
self.entry_name="Entry name"
self.config_file_short="name of file" (just the file name without the directory)
"""
def __init__(self, entry_name, config_file, convert_function=repr):
ConfigFile.__init__(self, config_file, convert_function)
self.entry_name = entry_name
entry_obj = EntryConfigFile(entry_name, config_file, convert_function)
# merge by overriding whatever is found in the subdir (Entry)
for k in list(entry_obj.data.keys()):
self.data[k] = entry_obj.data[k]
self.config_file = f"{config_file} AND {entry_obj.config_file}"
self.config_file_short = config_file
############################################################
#
# Configuration
#
############################################################
[docs]
class GlideinKey:
def __init__(self, pub_key_type, key_fname=None, recreate=False):
self.pub_key_type = pub_key_type
self.load(key_fname, recreate)
[docs]
def load(self, key_fname=None, recreate=False):
"""Create the key if required and initialize it
Args:
key_fname (str): Filename of the key
recreate (bool): Create a new key if True else load existing key. Defaults to False.
Raises:
RuntimeError: if asking for a key type different from RSA
"""
if self.pub_key_type == "RSA":
# hashlib methods are called dynamically
from hashlib import md5
if key_fname is None:
key_fname = "rsa.key"
self.rsa_key = pubCrypto.RSAKey(key_fname=key_fname)
if recreate:
# recreate it
self.rsa_key.new()
self.rsa_key.save(key_fname)
self.pub_rsa_key = self.rsa_key.PubRSAKey()
self.pub_key_id = md5(b" ".join((self.pub_key_type.encode("utf-8"), self.pub_rsa_key.get()))).hexdigest()
self.sym_class = symCrypto.AutoSymKey
else:
raise RuntimeError("Invalid pub key type value(%s), only RSA supported" % self.pub_key_type)
[docs]
def get_pub_key_type(self):
return self.pub_key_type[0:]
[docs]
def get_pub_key_value(self):
if self.pub_key_type == "RSA":
return self.pub_rsa_key.get()
else:
raise RuntimeError("Invalid pub key type value(%s), only RSA supported" % self.pub_key_type)
[docs]
def get_pub_key_id(self):
return self.pub_key_id[0:]
[docs]
class GlideinDescript(ConfigFile):
def __init__(self):
global factoryConfig
ConfigFile.__init__(self, factoryConfig.glidein_descript_file, repr) # convert everything in strings
if ("FactoryCollector" not in self.data) or (self.data["FactoryCollector"] == "None"):
self.data["FactoryCollector"] = None
if self.data["PubKeyType"] == "None":
self.data["PubKeyType"] = None
self.default_rsakey_fname = "rsa.key"
self.backup_rsakey_fname = "rsa.key.bak"
[docs]
def backup_and_load_old_key(self):
"""
Backup existing key and load the key object
"""
if self.data["PubKeyType"] is not None:
self.backup_rsa_key()
self.load_old_rsa_key()
[docs]
def backup_rsa_key(self):
"""Backup existing rsa key."""
if self.data["PubKeyType"] == "RSA":
try:
shutil.copy(self.default_rsakey_fname, self.backup_rsakey_fname)
self.data["OldPubKeyType"] = self.data["PubKeyType"]
return
except Exception:
# In case of failure, the requests from frontend get
# delayed. So it is not critical enough to fail.
pass
self.data["OldPubKeyType"] = None
self.data["OldPubKeyObj"] = None
return
[docs]
def load_old_rsa_key(self):
"""Load the old key object."""
# Assume that old key if exists is of same type
self.data["OldPubKeyType"] = self.data["PubKeyType"]
self.data["OldPubKeyObj"] = None
if self.data["OldPubKeyType"] is not None:
try:
self.data["OldPubKeyObj"] = GlideinKey(self.data["OldPubKeyType"], key_fname=self.backup_rsakey_fname)
except Exception:
self.data["OldPubKeyType"] = None
self.data["OldPubKeyObj"] = None
return
[docs]
def remove_old_key(self):
try:
os.remove(self.backup_rsakey_fname)
except:
self.data["OldPubKeyType"] = None
self.data["OldPubKeyObj"] = None
raise
self.data["OldPubKeyType"] = None
self.data["OldPubKeyObj"] = None
return
[docs]
def load_pub_key(self, recreate=False):
"""Load the key object. Create the key if required
Args:
recreate (bool): Create a new key overwriting the old one. Defaults to False
"""
if self.data["PubKeyType"] is not None:
self.data["PubKeyObj"] = GlideinKey(
self.data["PubKeyType"], key_fname=self.default_rsakey_fname, recreate=recreate
)
else:
self.data["PubKeyObj"] = None
return
[docs]
class JobDescript(EntryConfigFile):
def __init__(self, entry_name):
global factoryConfig
EntryConfigFile.__init__(
self, entry_name, factoryConfig.job_descript_file, repr
) # convert everything in strings
[docs]
class JobAttributes(JoinConfigFile):
def __init__(self, entry_name):
global factoryConfig
JoinConfigFile.__init__(
self, entry_name, factoryConfig.job_attrs_file, lambda s: s
) # values are in python format
[docs]
class JobParams(JoinConfigFile):
def __init__(self, entry_name):
global factoryConfig
JoinConfigFile.__init__(
self, entry_name, factoryConfig.job_params_file, lambda s: s
) # values are in python format
[docs]
class JobSubmitAttrs(JoinConfigFile):
def __init__(self, entry_name):
global factoryConfig
JoinConfigFile.__init__(
# Using repr instead of identity (convert into strings) would keep the quotes in the values
self,
entry_name,
factoryConfig.job_submit_attrs_file,
lambda s: s,
) # values are in python format
[docs]
class FrontendDescript(ConfigFile):
"""Contains the security identity and username mappings for the Frontends
that are authorized to use this factory.
Contains dictionary of dictionaries:
obj.data[frontend]['ident']=identity
obj.data[frontend]['usermap'][sec_class]=username
"""
def __init__(self):
global factoryConfig
ConfigFile.__init__(self, factoryConfig.frontend_descript_file, lambda s: s) # values are in python format
[docs]
def get_identity(self, frontend):
"""Get the identity for the given frontend.
If the Frontend is unknown, returns None.
Args:
frontend (str): frontend name
Returns:
str|None: identity
"""
if frontend in self.data:
fe = self.data[frontend]
return fe["ident"]
else:
return None
[docs]
def get_username(self, frontend, sec_class):
"""Get the security name mapping for the given frontend and security class.
If not found or not authorized, returns None.
Args:
frontend (str): frontend name
sec_class (str): security class name
Returns:
str|None: security name
"""
if frontend in self.data:
fe = self.data[frontend]["usermap"]
if sec_class in fe:
return fe[sec_class]
return None
[docs]
def get_all_usernames(self):
"""Get all the usernames assigned to all the frontends.
Returns:
list: list of usernames
"""
usernames = {}
for frontend in list(self.data.keys()):
fe = self.data[frontend]["usermap"]
for sec_class in list(fe.keys()):
username = fe[sec_class]
usernames[username] = True
return list(usernames.keys())
[docs]
def get_all_frontend_sec_classes(self):
"""Get a list of all frontend:sec_class
Returns:
list: Frontend security classes
"""
frontend_sec_classes = []
for fe_name in list(self.data.keys()):
fe = self.data[fe_name]["usermap"]
for sec_class in list(fe.keys()):
frontend_sec_classes.append(f"{fe_name}:{sec_class}")
return frontend_sec_classes
[docs]
def get_frontend_name(self, identity):
"""Get the frontend:sec_class mapping for the given identity
Args:
identity (str): identity
Returns:
str: Frontend name
"""
for fe_name in list(self.data.keys()):
if self.data[fe_name]["ident"] == identity:
return fe_name
# Signatures File format:
## File: signatures.sha1
##
# 6e3565a9a0f39e0641d7e3e777b8f22d7ebc8b0f description.a92arS.cfg entry_AmazonEC2
# 51b01a3c38589a41fb7a44936e12b31fe506ec7b description.a92aqM.cfg main
[docs]
class SignatureFile(ConfigFile):
"""Signatures File dictionary"""
def __init__(self):
global factoryConfig
ConfigFile.__init__(self, factoryConfig.signatures_file, lambda s: s) # values are in python format
[docs]
def load(self, fname, convert_function):
"""Load the signatures.sha1 file into the class as a dictionary. The
convert_function is completely ignored here. The line format is different
from all the other class in that there are three values with the key being
the last value. The internal dictionary has the following structure:
where:
line[0] is the signature for the line
line[1] is the descript file for the line
line[2] is the key for the line
for each line:
line[2]_sign = line[0]
line[2]_descript = line[1]
"""
self.data = {}
with open(fname) as fd:
lines = fd.readlines()
for line in lines:
if line[0] == "#":
continue # comment
if len(line.strip()) == 0:
continue # empty line
larr = line.split(None)
lsign = larr[0]
ldescript = larr[1]
lname = larr[2]
self.data["%s_sign" % str(lname)] = str(lsign)
self.data["%s_descript" % str(lname)] = str(ldescript)