Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding system info logging for Linux #1689

Merged
merged 9 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 152 additions & 17 deletions armi/bookkeeping/report/reportingUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def _writeCaseInformation(o, cs):
(Operator_ArmiCodebase, context.ROOT),
(Operator_WorkingDirectory, os.getcwd()),
(Operator_PythonInterperter, sys.version),
(Operator_MasterMachine, os.environ.get("COMPUTERNAME", "?")),
(Operator_MasterMachine, getNodeName()),
(Operator_NumProcessors, context.MPI_SIZE),
(Operator_Date, context.START_TIME),
]
Expand Down Expand Up @@ -191,16 +191,9 @@ def _writeMachineInformation():
nodeMappingData.append(
(uniqueName, numProcessors, ", ".join(matchingProcs))
)
# If this is on Windows: run sys info on each unique node too
if "win" in sys.platform:
sysInfoCmd = (
'systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version" /B '
'/C:"Processor" && systeminfo | findstr /E /C:"Mhz"'
)
out = subprocess.run(
sysInfoCmd, capture_output=True, text=True, shell=True
)
sysInfo += out.stdout

sysInfo += getSystemInfo()

runLog.header("=========== Machine Information ===========")
runLog.info(
tabulate.tabulate(
Expand All @@ -209,6 +202,7 @@ def _writeMachineInformation():
tablefmt="armi",
)
)

if sysInfo:
runLog.header("=========== System Information ===========")
runLog.info(sysInfo)
Expand Down Expand Up @@ -241,6 +235,148 @@ def _writeReactorCycleInformation(o, cs):
_writeReactorCycleInformation(o, cs)


def getNodeName():
"""Get the name of this comput node.
john-science marked this conversation as resolved.
Show resolved Hide resolved

First, look in context.py. Then try various Linux tools. Then try Windows commands.

Returns
-------
str
Compute node name.
"""
hostNames = [
context.MPI_NODENAME,
context.MPI_NODENAMES[0],
subprocess.run("hostname", capture_output=True, text=True, shell=True).stdout,
subprocess.run("uname -n", capture_output=True, text=True, shell=True).stdout,
os.environ.get("COMPUTERNAME", context.LOCAL),
]
for nodeName in hostNames:
if nodeName and nodeName != context.LOCAL:
return nodeName

return context.LOCAL


def _getSystemInfoWindows():
"""Get system information, assuming the system is Windows.

Returns
-------
str
Basic system information: OS name, OS version, basic processor information

Examples
--------
Example results:

OS Name: Microsoft Windows 10 Enterprise
OS Version: 10.0.19041 N/A Build 19041
Processor(s): 1 Processor(s) Installed.
[01]: Intel64 Family 6 Model 142 Stepping 12 GenuineIntel ~801 Mhz
"""
cmd = (
'systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version" /B '
'/C:"Processor" && systeminfo | findstr /E /C:"Mhz"'
)
return subprocess.run(cmd, capture_output=True, text=True, shell=True).stdout


def _getSystemInfoLinux():
"""Get system information, assuming the system is Linux.

This method uses multiple, redundant variations on common Linux command utilities to get the
information necessary. While it is not possible to guarantee what programs or files will be
available on "all Linux operating system", this collection of tools is widely supported and
should provide a reasonably broad-distribution coverage.

Returns
-------
str
Basic system information: OS name, OS version, basic processor information

Examples
--------
Example results:

OS Info: Ubuntu 22.04.3 LTS
Processor(s):
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 126
model name : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
...
"""
# get OS name / version
linuxOsCommands = [
'cat /etc/os-release | grep "^PRETTY_NAME=" | cut -d = -f 2',
"uname -a",
"lsb_release -d | cut -d : -f 2",
'hostnamectl | grep "Operating System" | cut -d : -f 2',
]
osInfo = ""
for cmd in linuxOsCommands:
osInfo = subprocess.run(
cmd, capture_output=True, text=True, shell=True
).stdout.strip()
if osInfo:
break

if not osInfo:
runLog.warning("Linux OS information not found.")
return ""

# get processor information
linuxProcCommands = ["cat /proc/cpuinfo", "lscpu", "lshw -class CPU"]
procInfo = ""
for cmd in linuxProcCommands:
procInfo = subprocess.run(
cmd, capture_output=True, text=True, shell=True
).stdout
if procInfo:
break

if not procInfo:
runLog.warning("Linux processor information not found.")
return ""

# build output string
out = "OS Info: "
out += osInfo.strip()
out += "\nProcessor(s):\n "
out += procInfo.strip().replace("\n", "\n ")
out += "\n"

return out


def getSystemInfo():
"""Get system information, assuming the system is Windows or Linux.

Notes
-----
The format of the system information will be different on Windows vs Linux.

Returns
-------
str
Basic system information: OS name, OS version, basic processor information
"""
# Get basic system information (on Windows and Linux)
if "win" in sys.platform:
return _getSystemInfoWindows()
elif "linux" in sys.platform:
return _getSystemInfoLinux()
else:
runLog.warning(
f"Cannot get system information for {sys.platform} because ARMI only "
+ "supports Linux and Windows."
)
return ""


def getInterfaceStackSummary(o):
data = []
for ii, i in enumerate(o.interfaces, start=1):
Expand Down Expand Up @@ -390,8 +526,7 @@ def _makeBOLAssemblyMassSummary(massSum):
line += "{0:<25.3f}".format(s[val])
str_.append("{0:12s}{1}".format(val, line))

# print blocks in this assembly
# up to 10
# print blocks in this assembly up to 10
for i in range(10):
line = " " * 12
for s in massSum:
Expand All @@ -401,6 +536,7 @@ def _makeBOLAssemblyMassSummary(massSum):
line += " " * 25
if re.search(r"\S", line): # \S matches any non-whitespace character.
str_.append(line)

return "\n".join(str_)


Expand All @@ -425,10 +561,10 @@ def writeCycleSummary(core):

Parameters
----------
core: armi.reactor.reactors.Core
core: armi.reactor.reactors.Core
cs: armi.settings.caseSettings.Settings
"""
# would io be worth considering for this?
# Would io be worth considering for this?
cycle = core.r.p.cycle
str_ = []
runLog.important("Cycle {0} Summary:".format(cycle))
Expand All @@ -446,7 +582,6 @@ def setNeutronBalancesReport(core):
Parameters
----------
core : armi.reactor.reactors.Core

"""
if not core.getFirstBlock().p.rateCap:
runLog.warning(
Expand Down Expand Up @@ -642,7 +777,7 @@ def makeCoreDesignReport(core, cs):

Parameters
----------
core: armi.reactor.reactors.Core
core: armi.reactor.reactors.Core
cs: armi.settings.caseSettings.Settings
"""
coreDesignTable = report.data.Table(
Expand Down
79 changes: 79 additions & 0 deletions armi/bookkeeping/report/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@
"""Really basic tests of the report Utils."""
import logging
import os
import subprocess
import unittest
from unittest.mock import patch

from armi import runLog, settings
from armi.bookkeeping import report
from armi.bookkeeping.report import data, reportInterface
from armi.bookkeeping.report.reportingUtils import (
_getSystemInfoLinux,
_getSystemInfoWindows,
getNodeName,
getSystemInfo,
makeBlockDesignReport,
setNeutronBalancesReport,
summarizePinDesign,
Expand All @@ -35,6 +41,79 @@
from armi.utils.directoryChangers import TemporaryDirectoryChanger


class _MockReturnResult:
"""Mocking the subprocess.run() return object."""

def __init__(self, stdout):
self.stdout = stdout


class TestReportingUtils(unittest.TestCase):
def test_getSystemInfoLinux(self):
"""Test _getSystemInfoLinux() on any operating system, by mocking the system calls."""
osInfo = '"Ubuntu 22.04.3 LTS"'
procInfo = """processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 126
model name : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
...
"""
correctResult = """OS Info: "Ubuntu 22.04.3 LTS"
Processor(s):
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 126
model name : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
..."""

def __mockSubprocessRun(*args, **kwargs):
if "os-release" in args[0]:
return _MockReturnResult(osInfo)
else:
return _MockReturnResult(procInfo)

with patch.object(subprocess, "run", side_effect=__mockSubprocessRun):
out = _getSystemInfoLinux()
self.assertEqual(out.strip(), correctResult)

@patch("subprocess.run")
def test_getSystemInfoWindows(self, mockSubprocess):
"""Test _getSystemInfoWindows() on any operating system, by mocking the system call."""
windowsResult = """OS Name: Microsoft Windows 10 Enterprise
OS Version: 10.0.19041 N/A Build 19041
Processor(s): 1 Processor(s) Installed.
[01]: Intel64 Family 6 Model 142 Stepping 12 GenuineIntel ~801 Mhz"""

mockSubprocess.return_value = _MockReturnResult(windowsResult)

out = _getSystemInfoWindows()
self.assertEqual(out, windowsResult)

def test_getSystemInfo(self):
"""Basic sanity check of getSystemInfo() running in the wild.

This test should pass if it is run on Window or mainstream Linux distros. But we expect this
to fail if the test is run on some other OS.
"""
out = getSystemInfo()
substrings = ["OS ", "Processor(s):"]
for sstr in substrings:
self.assertIn(sstr, out)

self.assertGreater(len(out), sum(len(sstr) + 5 for sstr in substrings))

def test_getNodeName(self):
"""Test that the getNodeName() method returns a non-empty string.

It is hard to know what string SHOULD be return here, and it would depend on how the OS is
set up on your machine or cluster. But this simple test needs to pass as-is on Windows
and Linux.
"""
self.assertGreater(len(getNodeName()), 0)


class TestReport(unittest.TestCase):
def setUp(self):
self.test_group = data.Table(settings.Settings(), "banana")
Expand Down
5 changes: 3 additions & 2 deletions armi/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ def setMode(cls, mode):
# MPI_SIZE is the total number of CPUs
MPI_RANK = 0
MPI_SIZE = 1
MPI_NODENAME = "local"
MPI_NODENAMES = ["local"]
LOCAL = "local"
MPI_NODENAME = LOCAL
MPI_NODENAMES = [LOCAL]


try:
Expand Down
1 change: 1 addition & 0 deletions doc/release/0.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Release Date: TBD
New Features
------------
#. Conserve mass by component in assembly.setBlockMesh(). (`PR#1665 <https://github.com/terrapower/armi/pull/1665>`_)
#. System information is now also logged on Linux. (`PR#1689 <https://github.com/terrapower/armi/pull/1689>`_)
#. TBD

API Changes
Expand Down
Loading