import configparser
import os
import pwd
from importlib import resources as importlib_resources
from lxml import etree
from mache.discover import discover_machine
[docs]
class MachineInfo:
"""
An object containing information about an E3SM supported machine
Attributes
----------
machine : str
The name of an E3SM supported machine
config : configparser.ConfigParser
Config options for this machine
e3sm_supported : bool
Whether this machine supports running E3SM itself, and therefore has
a list of compilers, MPI libraries, and the modules needed to load them
compilers : list
A list of compilers for this machine if ``e3sm_supported == True``
mpilibs : list
A list of MPI libraries for this machine if ``e3sm_supported == True``
os : str
The machine's operating system if ``e3sm_supported == True``
e3sm_unified_mpi : {'nompi', 'system', None}
Which MPI type is included in the E3SM-Unified environment (if one is
loaded)
e3sm_unified_base : str
The base path where E3SM-Unified and its activation scripts are
installed if ``e3sm_unified`` is not ``None``
e3sm_unified_activation : str
The activation script used to activate E3SM-Unified if ``e3sm_unified``
is not ``None``
diagnostics_base : str
The base directory for diagnostics data
web_portal_base : str
The base directory for the web portal
web_portal_url : str
The base URL for the web portal
username : str
The name of the current user, for use in web-portal directories. This
value is also added to the ``web_portal`` and ``username`` option of
the ``config`` attribute.
"""
[docs]
def __init__(self, machine=None, quiet=False):
"""
Create an object with information about the E3SM supported machine
Parameters
----------
machine : str, optional
The name of an E3SM supported machine. By default, the machine
will be inferred from the host name
quiet : bool, optional
Whether to print warnings if the machine name is ambiguous
"""
if machine is None:
machine = discover_machine(quiet=quiet)
if machine is None:
raise ValueError('Unable to discover machine from host name')
self.machine = machine
self.config = self._get_config()
self.e3sm_supported = False
self.compilers = None
self.mpilibs = None
self.os = None
self._parse_compilers_and_mpi()
self.e3sm_unified_mpi = None
self.e3sm_unified_base = None
self.e3sm_unified_activation = None
self._detect_e3sm_unified()
self.diagnostics_base = None
self._get_diagnostics_info()
self.web_portal_base = None
self.web_portal_url = None
self.username = pwd.getpwuid(os.getuid()).pw_name
if not self.config.has_section('web_portal'):
self.config.add_section('web_portal')
self.config.set('web_portal', 'username', self.username)
def __str__(self):
"""
Convert the info to a format that is good for printing
Returns
-------
info : str
The contents as a string for printing to the terminal
"""
info = (
f'Machine: {self.machine}\n'
f' E3SM Supported Machine: {self.e3sm_supported}'
)
if (
self.e3sm_supported
and self.compilers is not None
and self.mpilibs is not None
and self.os is not None
):
info = (
f'{info}\n'
f' Compilers: {", ".join(self.compilers)}\n'
f' MPI libraries: {", ".join(self.mpilibs)}\n'
f' OS: {self.os}'
)
info = f'{info}\n'
print_unified = (
self.e3sm_unified_activation is not None
or self.e3sm_unified_base is not None
or self.e3sm_unified_mpi is not None
)
if print_unified:
info = f'{info}\nE3SM-Unified: '
if self.e3sm_unified_activation is None:
info = f'{info}\n E3SM-Unified is not currently loaded'
else:
info = f'{info}\n Activation: {self.e3sm_unified_activation}'
if self.e3sm_unified_base is not None:
info = f'{info}\n Base path: {self.e3sm_unified_base}'
if self.e3sm_unified_mpi is not None:
info = f'{info}\n MPI type: {self.e3sm_unified_mpi}'
info = f'{info}\n'
print_diags = self.diagnostics_base is not None
if print_diags:
info = f'{info}\nDiagnostics: '
if self.diagnostics_base is not None:
info = f'{info}\n Base path: {self.diagnostics_base}'
info = f'{info}\n'
info = f'{info}\nConfig options: '
for section in self.config.sections():
info = f'{info}\n [{section}]'
for key, value in self.config.items(section):
info = f'{info}\n {key} = {value}'
info = f'{info}\n'
return info
[docs]
def get_account_defaults(self):
"""
Get default account, partition and quality of service (QOS) for
this machine.
Returns
-------
account : str
The E3SM account on the machine
partition : str
The default partition on the machine, or ``None`` if no partition
should be specified
constraint : str
The default constraint on the machine, or ``None`` if no
constraint should be specified
qos : str
The default quality of service on the machine, or ``None`` if no
QOS should be specified
"""
config = self.config
if config.has_option('parallel', 'account'):
account = config.get('parallel', 'account')
else:
account = None
if config.has_option('parallel', 'partitions'):
partition = config.get('parallel', 'partitions')
# take the first entry
partition = partition.split(',')[0].strip()
else:
partition = None
if config.has_option('parallel', 'constraints'):
constraint = config.get('parallel', 'constraints')
# take the first entry
constraint = constraint.split(',')[0].strip()
else:
constraint = None
if config.has_option('parallel', 'qos'):
qos = config.get('parallel', 'qos')
# take the first entry
qos = qos.split(',')[0].strip()
else:
qos = None
return account, partition, constraint, qos
def _get_config(self):
"""get a parser for config options"""
config = configparser.ConfigParser(
interpolation=configparser.ExtendedInterpolation()
)
machine = self.machine
try:
cfg_path = (
importlib_resources.files('mache.machines') / f'{machine}.cfg'
)
config.read(str(cfg_path))
except FileNotFoundError:
# this isn't a known machine so use the default
cfg_path = (
importlib_resources.files('mache.machines') / 'default.cfg'
)
config.read(str(cfg_path))
return config
def _parse_compilers_and_mpi(self):
"""Parse the compilers and mpi modules from XML config files"""
machine = self.machine
xml_path = (
importlib_resources.files('mache.cime_machine_config')
/ 'config_machines.xml'
)
root = etree.parse(str(xml_path))
machines = next(root.iter('config_machines'))
mach = None
found = False
for mach in machines:
if mach.tag == 'machine' and mach.attrib['MACH'] == machine:
found = True
break
if not found:
# this is not an E3SM supported machine, so we're done
self.e3sm_supported = False
return
self.e3sm_supported = True
compilers = None
for child in mach:
if child.tag == 'COMPILERS':
compilers = child.text.split(',')
break
self.compilers = compilers
mpilibs = None
for child in mach:
if child.tag == 'MPILIBS':
mpilibs = child.text.split(',')
break
self.mpilibs = mpilibs
machine_os = None
for child in mach:
if child.tag == 'OS':
machine_os = child.text
break
self.os = machine_os
def _detect_e3sm_unified(self):
"""Read E3SM-Unified base path and detect whether it is running"""
config = self.config
if config is not None and config.has_option(
'e3sm_unified', 'base_path'
):
self.e3sm_unified_base = config.get('e3sm_unified', 'base_path')
if 'E3SMU_SCRIPT' in os.environ:
self.e3sm_unified_activation = os.environ['E3SMU_SCRIPT']
if 'E3SMU_MPI' in os.environ:
self.e3sm_unified_mpi = os.environ['E3SMU_MPI'].lower()
def _get_diagnostics_info(self):
"""Get config options related to diagnostics data"""
config = self.config
if config is not None and config.has_option(
'diagnostics', 'base_path'
):
self.diagnostics_base = config.get('diagnostics', 'base_path')