diff --git a/CIME/XML/files.py b/CIME/XML/files.py
index 26843409ed0..c0149f9601a 100644
--- a/CIME/XML/files.py
+++ b/CIME/XML/files.py
@@ -136,7 +136,9 @@ def set_value(self, vid, value, subgroup=None, ignore_type=False):
def get_schema(self, nodename, attributes=None):
node = self.get_optional_child("entry", {"id": nodename})
+
schemanode = self.get_optional_child("schema", root=node, attributes=attributes)
+
if schemanode is not None:
logger.debug("Found schema for {}".format(nodename))
return self.get_resolved_value(self.text(schemanode))
diff --git a/CIME/XML/generic_xml.py b/CIME/XML/generic_xml.py
index a45ca766ee7..3b303047ad3 100644
--- a/CIME/XML/generic_xml.py
+++ b/CIME/XML/generic_xml.py
@@ -8,7 +8,7 @@
import xml.etree.ElementTree as ET
# pylint: disable=import-error
-from distutils.spawn import find_executable
+from shutil import which
import getpass
from copy import deepcopy
from collections import namedtuple
@@ -105,7 +105,8 @@ def __init__(
def read(self, infile, schema=None):
"""
- Read and parse an xml file into the object
+ Read and parse an xml file into the object. The schema variable can either be a path to an xsd schema file or
+ a dictionary of paths to files by version.
"""
cached_read = False
if not self.DISABLE_CACHING and infile in self._FILEMAP:
@@ -126,8 +127,10 @@ def read(self, infile, schema=None):
logger.debug("read: {}".format(infile))
with open(infile, "r", encoding="utf-8") as fd:
self.read_fd(fd)
-
- if schema is not None and self.get_version() > 1.0:
+ version = str(self.get_version())
+ if type(schema) is dict:
+ self.validate_xml_file(infile, schema[version])
+ elif schema is not None and self.get_version() > 1.0:
self.validate_xml_file(infile, schema)
logger.debug("File version is {}".format(str(self.get_version())))
@@ -472,7 +475,7 @@ def write(self, outfile=None, force_write=False):
xmlstr = self.get_raw_record()
# xmllint provides a better format option for the output file
- xmllint = find_executable("xmllint")
+ xmllint = which("xmllint")
if xmllint:
if isinstance(outfile, str):
@@ -688,9 +691,14 @@ def validate_xml_file(self, filename, schema):
"""
validate an XML file against a provided schema file using pylint
"""
- expect(os.path.isfile(filename), "xml file not found {}".format(filename))
- expect(os.path.isfile(schema), "schema file not found {}".format(schema))
- xmllint = find_executable("xmllint")
+ expect(
+ filename and os.path.isfile(filename),
+ "xml file not found {}".format(filename),
+ )
+ expect(
+ schema and os.path.isfile(schema), "schema file not found {}".format(schema)
+ )
+ xmllint = which("xmllint")
expect(
xmllint and os.path.isfile(xmllint),
diff --git a/CIME/XML/machines.py b/CIME/XML/machines.py
index 1ef33bd5d35..a9536d74572 100644
--- a/CIME/XML/machines.py
+++ b/CIME/XML/machines.py
@@ -30,6 +30,9 @@ def __init__(
additional directory that will be searched for a config_machines.xml file; if
found, the contents of this file will be appended to the standard
config_machines.xml. An empty string is treated the same as None.
+
+ The schema variable can be passed as a path to an xsd schema file or a dictionary of paths
+ with version number as keys.
"""
self.machine_node = None
@@ -44,8 +47,6 @@ def __init__(
files = Files()
if infile is None:
infile = files.get_value("MACHINES_SPEC_FILE")
- schema = files.get_schema("MACHINES_SPEC_FILE")
- logger.debug("Verifying using schema {}".format(schema))
self.machines_dir = os.path.dirname(infile)
if os.path.exists(infile):
@@ -53,6 +54,20 @@ def __init__(
else:
expect(False, f"file not found {infile}")
+ schema = {
+ "3.0": files.get_schema(
+ "MACHINES_SPEC_FILE", attributes={"version": "3.0"}
+ ),
+ "2.0": files.get_schema(
+ "MACHINES_SPEC_FILE", attributes={"version": "2.0"}
+ ),
+ }
+ # Before v3 there was but one choice
+ if not schema["3.0"]:
+ schema = files.get_schema("MACHINES_SPEC_FILE")
+
+ logger.debug("Verifying using schema {}".format(schema))
+
GenericXML.__init__(self, infile, schema, read_only=read_only)
# Append the contents of $HOME/.cime/config_machines.xml if it exists.
@@ -91,7 +106,7 @@ def __init__(
machine is not None,
f"Could not initialize machine object from {', '.join(checked_files)}. This machine is not available for the target CIME_MODEL.",
)
- self.set_machine(machine)
+ self.set_machine(machine, schema=schema)
def get_child(self, name=None, attributes=None, root=None, err_msg=None):
if root is None:
@@ -135,10 +150,19 @@ def list_available_machines(self):
Return a list of machines defined for a given CIME_MODEL
"""
machines = []
- nodes = self.get_children("machine")
- for node in nodes:
- mach = self.get(node, "MACH")
- machines.append(mach)
+ if self.get_version() < 3:
+ nodes = self.get_children("machine")
+ for node in nodes:
+ mach = self.get(node, "MACH")
+ machines.append(mach)
+ else:
+ machines = [
+ os.path.basename(f.path)
+ for f in os.scandir(self.machines_dir)
+ if f.is_dir()
+ ]
+ machines.remove("cmake_macros")
+ machines.sort()
return machines
def probe_machine_name(self, warn=True):
@@ -150,6 +174,7 @@ def probe_machine_name(self, warn=True):
names_not_found = []
nametomatch = socket.getfqdn()
+
machine = self._probe_machine_name_one_guess(nametomatch)
if machine is None:
@@ -177,10 +202,15 @@ def _probe_machine_name_one_guess(self, nametomatch):
Find a matching regular expression for nametomatch in the NODENAME_REGEX
field in the file. First match wins. Returns None if no match is found.
"""
+ if self.get_version() < 3:
+ return self._probe_machine_name_one_guess_v2(nametomatch)
+ else:
+ return self._probe_machine_name_one_guess_v3(nametomatch)
- machine = None
- nodes = self.get_children("machine")
+ def _probe_machine_name_one_guess_v2(self, nametomatch):
+ nodes = self.get_children("machine")
+ machine = None
for node in nodes:
machtocheck = self.get(node, "MACH")
logger.debug("machine is " + machtocheck)
@@ -222,7 +252,53 @@ def _probe_machine_name_one_guess(self, nametomatch):
return machine
- def set_machine(self, machine):
+ def _probe_machine_name_one_guess_v3(self, nametomatch):
+
+ node = self.get_child("NODENAME_REGEX", root=self.root)
+
+ for child in self.get_children(root=node):
+ machtocheck = self.get(child, "MACH")
+ regex_str = self.text(child)
+ logger.debug(
+ "machine is {} regex {}, nametomatch {}".format(
+ machtocheck, regex_str, nametomatch
+ )
+ )
+
+ if regex_str is not None:
+ # an environment variable can be used
+ if regex_str.startswith("$ENV"):
+ machine_value = self.get_resolved_value(
+ regex_str, allow_unresolved_envvars=True
+ )
+ logger.debug("machine_value is {}".format(machine_value))
+ if not machine_value.startswith("$ENV"):
+ try:
+ match, this_machine = machine_value.split(":")
+ except ValueError:
+ expect(
+ False,
+ "Bad formation of NODENAME_REGEX. Expected envvar:value, found {}".format(
+ regex_str
+ ),
+ )
+ if match == this_machine:
+ machine = machtocheck
+ break
+ else:
+ regex = re.compile(regex_str)
+ if regex.match(nametomatch):
+ logger.debug(
+ "Found machine: {} matches {}".format(
+ machtocheck, nametomatch
+ )
+ )
+ machine = machtocheck
+ break
+
+ return machine
+
+ def set_machine(self, machine, schema=None):
"""
Sets the machine block in the Machines object
@@ -235,15 +311,24 @@ def set_machine(self, machine):
CIMEError: ERROR: No machine trump found
"""
if machine == "Query":
- self.machine = machine
- elif self.machine != machine or self.machine_node is None:
- self.machine_node = super(Machines, self).get_child(
- "machine",
- {"MACH": machine},
- err_msg="No machine {} found".format(machine),
+ return machine
+ elif self.get_version() == 3:
+ machines_file = os.path.join(
+ self.machines_dir, machine, "config_machines.xml"
)
- self.machine = machine
+ if os.path.isfile(machines_file):
+ GenericXML.read(
+ self,
+ machines_file,
+ schema=schema,
+ )
+ self.machine_node = super(Machines, self).get_child(
+ "machine",
+ {"MACH": machine},
+ err_msg="No machine {} found".format(machine),
+ )
+ self.machine = machine
return machine
# pylint: disable=arguments-differ
@@ -292,6 +377,11 @@ def get_field_from_list(self, listname, reqval=None, attributes=None):
"""
expect(self.machine_node is not None, "Machine object has no machine defined")
supported_values = self.get_value(listname, attributes=attributes)
+ logger.debug(
+ "supported values for {} on {} is {}".format(
+ listname, self.machine, supported_values
+ )
+ )
# if no match with attributes, try without
if supported_values is None:
supported_values = self.get_value(listname, attributes=None)
diff --git a/CIME/data/config/cesm/config_files.xml b/CIME/data/config/cesm/config_files.xml
index b3346815bd5..75a9520dc72 100644
--- a/CIME/data/config/cesm/config_files.xml
+++ b/CIME/data/config/cesm/config_files.xml
@@ -43,7 +43,8 @@
case_last
env_case.xml
file containing machine specifications for target model primary component (for documentation only - DO NOT EDIT)
- $CIMEROOT/CIME/data/config/xml_schemas/config_machines.xsd
+ $CIMEROOT/CIME/data/config/xml_schemas/config_machines.xsd
+ $CIMEROOT/CIME/data/config/xml_schemas/config_machines_version3.xsd
diff --git a/CIME/data/config/xml_schemas/config_machines_version3.xsd b/CIME/data/config/xml_schemas/config_machines_version3.xsd
new file mode 100644
index 00000000000..92b55839fb2
--- /dev/null
+++ b/CIME/data/config/xml_schemas/config_machines_version3.xsd
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CIME/tests/test_sys_test_scheduler.py b/CIME/tests/test_sys_test_scheduler.py
index b53809ba306..3dfd4b62124 100755
--- a/CIME/tests/test_sys_test_scheduler.py
+++ b/CIME/tests/test_sys_test_scheduler.py
@@ -20,22 +20,22 @@ def test_chksum(self, strftime): # pylint: disable=unused-argument
self.skipTest("Skipping chksum test. Depends on CESM settings")
ts = test_scheduler.TestScheduler(
- ["SEQ_Ln9.f19_g16_rx1.A.cori-haswell_gnu"],
- machine_name="cori-haswell",
+ ["SEQ_Ln9.f19_g16_rx1.A.perlmutter_gnu"],
+ machine_name="perlmutter",
chksum=True,
test_root="/tests",
)
with mock.patch.object(ts, "_shell_cmd_for_phase") as _shell_cmd_for_phase:
ts._run_phase(
- "SEQ_Ln9.f19_g16_rx1.A.cori-haswell_gnu"
+ "SEQ_Ln9.f19_g16_rx1.A.perlmutter_gnu"
) # pylint: disable=protected-access
_shell_cmd_for_phase.assert_called_with(
- "SEQ_Ln9.f19_g16_rx1.A.cori-haswell_gnu",
+ "SEQ_Ln9.f19_g16_rx1.A.perlmutter_gnu",
"./case.submit --skip-preview-namelist --chksum",
"RUN",
- from_dir="/tests/SEQ_Ln9.f19_g16_rx1.A.cori-haswell_gnu.00:00:00",
+ from_dir="/tests/SEQ_Ln9.f19_g16_rx1.A.perlmutter_gnu.00:00:00",
)
def test_a_phases(self):
diff --git a/CIME/tests/test_unit_case.py b/CIME/tests/test_unit_case.py
index 8a1190456c2..820bd9ac91c 100755
--- a/CIME/tests/test_unit_case.py
+++ b/CIME/tests/test_unit_case.py
@@ -232,14 +232,14 @@ def test_copy(
self.srcroot,
"A",
"f19_g16_rx1",
- machine_name="cori-haswell",
+ machine_name="perlmutter",
)
# Check that they're all called
configure.assert_called_with(
"A",
"f19_g16_rx1",
- machine_name="cori-haswell",
+ machine_name="perlmutter",
project=None,
pecount=None,
compiler=None,
@@ -309,14 +309,14 @@ def test_create(
self.srcroot,
"A",
"f19_g16_rx1",
- machine_name="cori-haswell",
+ machine_name="perlmutter",
)
# Check that they're all called
configure.assert_called_with(
"A",
"f19_g16_rx1",
- machine_name="cori-haswell",
+ machine_name="perlmutter",
project=None,
pecount=None,
compiler=None,