removing feature bloat: monitor and analyzers (#31130)

Signed-off-by: vsoch <vsoch@users.noreply.github.com>

Co-authored-by: vsoch <vsoch@users.noreply.github.com>
This commit is contained in:
Vanessasaurus 2022-07-07 00:49:40 -06:00 committed by GitHub
parent 3338d536f6
commit 6b1e86aecc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 5 additions and 2192 deletions

View file

@ -107,7 +107,6 @@ with a high level view of Spack's directory structure:
llnl/ <- some general-use libraries
spack/ <- spack module; contains Python code
analyzers/ <- modules to run analysis on installed packages
build_systems/ <- modules for different build systems
cmd/ <- each file in here is a spack subcommand
compilers/ <- compiler description files
@ -242,22 +241,6 @@ Unit tests
Implements Spack's test suite. Add a module and put its name in
the test suite in ``__init__.py`` to add more unit tests.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Research and Monitoring Modules
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:mod:`spack.monitor`
Contains :class:`~spack.monitor.SpackMonitorClient`. This is accessed from
the ``spack install`` and ``spack analyze`` commands to send build and
package metadata up to a `Spack Monitor
<https://github.com/spack/spack-monitor>`_ server.
:mod:`spack.analyzers`
A module folder with a :class:`~spack.analyzers.analyzer_base.AnalyzerBase`
that provides base functions to run, save, and (optionally) upload analysis
results to a `Spack Monitor <https://github.com/spack/spack-monitor>`_ server.
^^^^^^^^^^^^^
Other Modules
@ -301,240 +284,6 @@ Most spack commands look something like this:
The information in Package files is used at all stages in this
process.
Conceptually, packages are overloaded. They contain:
-------------
Stage objects
-------------
.. _writing-analyzers:
-----------------
Writing analyzers
-----------------
To write an analyzer, you should add a new python file to the
analyzers module directory at ``lib/spack/spack/analyzers`` .
Your analyzer should be a subclass of the :class:`AnalyzerBase <spack.analyzers.analyzer_base.AnalyzerBase>`. For example, if you want
to add an analyzer class ``Myanalyzer`` you would write to
``spack/analyzers/myanalyzer.py`` and import and
use the base as follows:
.. code-block:: python
from .analyzer_base import AnalyzerBase
class Myanalyzer(AnalyzerBase):
Note that the class name is your module file name, all lowercase
except for the first capital letter. You can look at other analyzers in
that analyzer directory for examples. The guide here will tell you about the basic functions needed.
^^^^^^^^^^^^^^^^^^^^^^^^^
Analyzer Output Directory
^^^^^^^^^^^^^^^^^^^^^^^^^
By default, when you run ``spack analyze run`` an analyzer output directory will
be created in your spack user directory in your ``$HOME``. The reason we output here
is because the install directory might not always be writable.
.. code-block:: console
~/.spack/
analyzers
Result files will be written here, organized in subfolders in the same structure
as the package, with each analyzer owning it's own subfolder. for example:
.. code-block:: console
$ tree ~/.spack/analyzers/
/home/spackuser/.spack/analyzers/
└── linux-ubuntu20.04-skylake
└── gcc-9.3.0
└── zlib-1.2.11-sl7m27mzkbejtkrajigj3a3m37ygv4u2
├── environment_variables
│   └── spack-analyzer-environment-variables.json
├── install_files
│   └── spack-analyzer-install-files.json
└── libabigail
└── lib
└── spack-analyzer-libabigail-libz.so.1.2.11.xml
Notice that for the libabigail analyzer, since results are generated per object,
we honor the object's folder in case there are equivalently named files in
different folders. The result files are typically written as json so they can be easily read and uploaded in a future interaction with a monitor.
^^^^^^^^^^^^^^^^^
Analyzer Metadata
^^^^^^^^^^^^^^^^^
Your analyzer is required to have the class attributes ``name``, ``outfile``,
and ``description``. These are printed to the user with they use the subcommand
``spack analyze list-analyzers``. Here is an example.
As we mentioned above, note that this analyzer would live in a module named
``libabigail.py`` in the analyzers folder so that the class can be discovered.
.. code-block:: python
class Libabigail(AnalyzerBase):
name = "libabigail"
outfile = "spack-analyzer-libabigail.json"
description = "Application Binary Interface (ABI) features for objects"
This means that the name and output file should be unique for your analyzer.
Note that "all" cannot be the name of an analyzer, as this key is used to indicate
that the user wants to run all analyzers.
.. _analyzer_run_function:
^^^^^^^^^^^^^^^^^^^^^^^^
An analyzer run Function
^^^^^^^^^^^^^^^^^^^^^^^^
The core of an analyzer is its ``run()`` function, which should accept no
arguments. You can assume your analyzer has the package spec of interest at ``self.spec``
and it's up to the run function to generate whatever analysis data you need,
and then return the object with a key as the analyzer name. The result data
should be a list of objects, each with a name, ``analyzer_name``, ``install_file``,
and one of ``value`` or ``binary_value``. The install file should be for a relative
path, and not the absolute path. For example, let's say we extract a metric called
``metric`` for ``bin/wget`` using our analyzer ``thebest-analyzer``.
We might have data that looks like this:
.. code-block:: python
result = {"name": "metric", "analyzer_name": "thebest-analyzer", "value": "1", "install_file": "bin/wget"}
We'd then return it as follows - note that they key is the analyzer name at ``self.name``.
.. code-block:: python
return {self.name: result}
This will save the complete result to the analyzer metadata folder, as described
previously. If you want support for adding a different kind of metadata (e.g.,
not associated with an install file) then the monitor server would need to be updated
to support this first.
^^^^^^^^^^^^^^^^^^^^^^^^^
An analyzer init Function
^^^^^^^^^^^^^^^^^^^^^^^^^
If you don't need any extra dependencies or checks, you can skip defining an analyzer
init function, as the base class will handle it. Typically, it will accept
a spec, and an optional output directory (if the user does not want the default
metadata folder for analyzer results). The analyzer init function should call
it's parent init, and then do any extra checks or validation that are required to
work. For example:
.. code-block:: python
def __init__(self, spec, dirname=None):
super(Myanalyzer, self).__init__(spec, dirname)
# install extra dependencies, do extra preparation and checks here
At the end of the init, you will have available to you:
- **self.spec**: the spec object
- **self.dirname**: an optional directory name the user as provided at init to save
- **self.output_dir**: the analyzer metadata directory, where we save by default
- **self.meta_dir**: the path to the package metadata directory (.spack) if you need it
And can proceed to write your analyzer.
^^^^^^^^^^^^^^^^^^^^^^^
Saving Analyzer Results
^^^^^^^^^^^^^^^^^^^^^^^
The analyzer will have ``save_result`` called, with the result object generated
to save it to the filesystem, and if the user has added the ``--monitor`` flag
to upload it to a monitor server. If your result follows an accepted result
format and you don't need to parse it further, you don't need to add this
function to your class. However, if your result data is large or otherwise
needs additional parsing, you can define it. If you define the function, it
is useful to know about the ``output_dir`` property, which you can join
with your output file relative path of choice:
.. code-block:: python
outfile = os.path.join(self.output_dir, "my-output-file.txt")
The directory will be provided by the ``output_dir`` property but it won't exist,
so you should create it:
.. code::block:: python
# Create the output directory
if not os.path.exists(self._output_dir):
os.makedirs(self._output_dir)
If you are generating results that match to specific files in the package
install directory, you should try to maintain those paths in the case that
there are equivalently named files in different directories that would
overwrite one another. As an example of an analyzer with a custom save,
the Libabigail analyzer saves ``*.xml`` files to the analyzer metadata
folder in ``run()``, as they are either binaries, or as xml (text) would
usually be too big to pass in one request. For this reason, the files
are saved during ``run()`` and the filenames added to the result object,
and then when the result object is passed back into ``save_result()``,
we skip saving to the filesystem, and instead read the file and send
each one (separately) to the monitor:
.. code-block:: python
def save_result(self, result, monitor=None, overwrite=False):
"""ABI results are saved to individual files, so each one needs to be
read and uploaded. Result here should be the lookup generated in run(),
the key is the analyzer name, and each value is the result file.
We currently upload the entire xml as text because libabigail can't
easily read gzipped xml, but this will be updated when it can.
"""
if not monitor:
return
name = self.spec.package.name
for obj, filename in result.get(self.name, {}).items():
# Don't include the prefix
rel_path = obj.replace(self.spec.prefix + os.path.sep, "")
# We've already saved the results to file during run
content = spack.monitor.read_file(filename)
# A result needs an analyzer, value or binary_value, and name
data = {"value": content, "install_file": rel_path, "name": "abidw-xml"}
tty.info("Sending result for %s %s to monitor." % (name, rel_path))
monitor.send_analyze_metadata(self.spec.package, {"libabigail": [data]})
Notice that this function, if you define it, requires a result object (generated by
``run()``, a monitor (if you want to send), and a boolean ``overwrite`` to be used
to check if a result exists first, and not write to it if the result exists and
overwrite is False. Also notice that since we already saved these files to the analyzer metadata folder, we return early if a monitor isn't defined, because this function serves to send results to the monitor. If you haven't saved anything to the analyzer metadata folder
yet, you might want to do that here. You should also use ``tty.info`` to give
the user a message of "Writing result to $DIRNAME."
.. _writing-commands:
@ -699,23 +448,6 @@ with a hook, and this is the purpose of this particular hook. Akin to
``on_phase_success`` we require the same variables - the package that failed,
the name of the phase, and the log file where we might find errors.
"""""""""""""""""""""""""""""""""
``on_analyzer_save(pkg, result)``
"""""""""""""""""""""""""""""""""
After an analyzer has saved some result for a package, this hook is called,
and it provides the package that we just ran the analysis for, along with
the loaded result. Typically, a result is structured to have the name
of the analyzer as key, and the result object that is defined in detail in
:ref:`analyzer_run_function`.
.. code-block:: python
def on_analyzer_save(pkg, result):
"""given a package and a result...
"""
print('Do something extra with a package analysis result here')
^^^^^^^^^^^^^^^^^^^^^^
Adding a New Hook Type

View file

@ -1,42 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""This package contains code for creating analyzers to extract Application
Binary Interface (ABI) information, along with simple analyses that just load
existing metadata.
"""
from __future__ import absolute_import
import llnl.util.tty as tty
import spack.paths
import spack.util.classes
mod_path = spack.paths.analyzers_path
analyzers = spack.util.classes.list_classes("spack.analyzers", mod_path)
# The base analyzer does not have a name, and cannot do dict comprehension
analyzer_types = {}
for a in analyzers:
if not hasattr(a, "name"):
continue
analyzer_types[a.name] = a
def list_all():
"""A helper function to list all analyzers and their descriptions
"""
for name, analyzer in analyzer_types.items():
print("%-25s: %-35s" % (name, analyzer.description))
def get_analyzer(name):
"""Courtesy function to retrieve an analyzer, and exit on error if it
does not exist.
"""
if name in analyzer_types:
return analyzer_types[name]
tty.die("Analyzer %s does not exist" % name)

View file

@ -1,116 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""An analyzer base provides basic functions to run the analysis, save results,
and (optionally) interact with a Spack Monitor
"""
import os
import llnl.util.tty as tty
import spack.config
import spack.hooks
import spack.monitor
import spack.util.path
def get_analyzer_dir(spec, analyzer_dir=None):
"""
Given a spec, return the directory to save analyzer results.
We create the directory if it does not exist. We also check that the
spec has an associated package. An analyzer cannot be run if the spec isn't
associated with a package. If the user provides a custom analyzer_dir,
we use it over checking the config and the default at ~/.spack/analyzers
"""
# An analyzer cannot be run if the spec isn't associated with a package
if not hasattr(spec, "package") or not spec.package:
tty.die("A spec can only be analyzed with an associated package.")
# The top level directory is in the user home, or a custom location
if not analyzer_dir:
analyzer_dir = spack.util.path.canonicalize_path(
spack.config.get('config:analyzers_dir', '~/.spack/analyzers'))
# We follow the same convention as the spec install (this could be better)
package_prefix = os.sep.join(spec.package.prefix.split('/')[-3:])
meta_dir = os.path.join(analyzer_dir, package_prefix)
return meta_dir
class AnalyzerBase(object):
def __init__(self, spec, dirname=None):
"""
Verify that the analyzer has correct metadata.
An Analyzer is intended to run on one spec install, so the spec
with its associated package is required on init. The child analyzer
class should define an init function that super's the init here, and
also check that the analyzer has all dependencies that it
needs. If an analyzer subclass does not have dependencies, it does not
need to define an init. An Analyzer should not be allowed to proceed
if one or more dependencies are missing. The dirname, if defined,
is an optional directory name to save to (instead of the default meta
spack directory).
"""
self.spec = spec
self.dirname = dirname
self.meta_dir = os.path.dirname(spec.package.install_log_path)
for required in ["name", "outfile", "description"]:
if not hasattr(self, required):
tty.die("Please add a %s attribute on the analyzer." % required)
def run(self):
"""
Given a spec with an installed package, run the analyzer on it.
"""
raise NotImplementedError
@property
def output_dir(self):
"""
The full path to the output directory.
This includes the nested analyzer directory structure. This function
does not create anything.
"""
if not hasattr(self, "_output_dir"):
output_dir = get_analyzer_dir(self.spec, self.dirname)
self._output_dir = os.path.join(output_dir, self.name)
return self._output_dir
def save_result(self, result, overwrite=False):
"""
Save a result to the associated spack monitor, if defined.
This function is on the level of the analyzer because it might be
the case that the result is large (appropriate for a single request)
or that the data is organized differently (e.g., more than one
request per result). If an analyzer subclass needs to over-write
this function with a custom save, that is appropriate to do (see abi).
"""
# We maintain the structure in json with the analyzer as key so
# that in the future, we could upload to a monitor server
if result[self.name]:
outfile = os.path.join(self.output_dir, self.outfile)
# Only try to create the results directory if we have a result
if not os.path.exists(self._output_dir):
os.makedirs(self._output_dir)
# Don't overwrite an existing result if overwrite is False
if os.path.exists(outfile) and not overwrite:
tty.info("%s exists and overwrite is False, skipping." % outfile)
else:
tty.info("Writing result to %s" % outfile)
spack.monitor.write_json(result[self.name], outfile)
# This hook runs after a save result
spack.hooks.on_analyzer_save(self.spec.package, result)

View file

@ -1,33 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""A configargs analyzer is a class of analyzer that typically just uploads
already existing metadata about config args from a package spec install
directory."""
import os
import spack.monitor
from .analyzer_base import AnalyzerBase
class ConfigArgs(AnalyzerBase):
name = "config_args"
outfile = "spack-analyzer-config-args.json"
description = "config args loaded from spack-configure-args.txt"
def run(self):
"""
Load the configure-args.txt and save in json.
The run function will find the spack-config-args.txt file in the
package install directory, and read it into a json structure that has
the name of the analyzer as the key.
"""
config_file = os.path.join(self.meta_dir, "spack-configure-args.txt")
return {self.name: spack.monitor.read_file(config_file)}

View file

@ -1,54 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""An environment analyzer will read and parse the environment variables
file in the installed package directory, generating a json file that has
an index of key, value pairs for environment variables."""
import os
import llnl.util.tty as tty
from spack.util.environment import EnvironmentModifications
from .analyzer_base import AnalyzerBase
class EnvironmentVariables(AnalyzerBase):
name = "environment_variables"
outfile = "spack-analyzer-environment-variables.json"
description = "environment variables parsed from spack-build-env.txt"
def run(self):
"""
Load, parse, and save spack-build-env.txt to analyzers.
Read in the spack-build-env.txt file from the package install
directory and parse the environment variables into key value pairs.
The result should have the key for the analyzer, the name.
"""
env_file = os.path.join(self.meta_dir, "spack-build-env.txt")
return {self.name: self._read_environment_file(env_file)}
def _read_environment_file(self, filename):
"""
Read and parse the environment file.
Given an environment file, we want to read it, split by semicolons
and new lines, and then parse down to the subset of SPACK_* variables.
We assume that all spack prefix variables are not secrets, and unlike
the install_manifest.json, we don't (at least to start) parse the values
to remove path prefixes specific to user systems.
"""
if not os.path.exists(filename):
tty.warn("No environment file available")
return
mods = EnvironmentModifications.from_sourcing_file(filename)
env = {}
mods.apply_modifications(env)
return env

View file

@ -1,31 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""The install files json file (install_manifest.json) already exists in
the package install folder, so this analyzer simply moves it to the user
analyzer folder for further processing."""
import os
import spack.monitor
from .analyzer_base import AnalyzerBase
class InstallFiles(AnalyzerBase):
name = "install_files"
outfile = "spack-analyzer-install-files.json"
description = "install file listing read from install_manifest.json"
def run(self):
"""
Load in the install_manifest.json and save to analyzers.
We write it out to the analyzers folder, with key as the analyzer name.
"""
manifest_file = os.path.join(self.meta_dir, "install_manifest.json")
return {self.name: spack.monitor.read_json(manifest_file)}

View file

@ -1,114 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import llnl.util.tty as tty
import spack
import spack.binary_distribution
import spack.bootstrap
import spack.error
import spack.hooks
import spack.monitor
import spack.package_base
import spack.repo
import spack.util.executable
from .analyzer_base import AnalyzerBase
class Libabigail(AnalyzerBase):
name = "libabigail"
outfile = "spack-analyzer-libabigail.json"
description = "Application Binary Interface (ABI) features for objects"
def __init__(self, spec, dirname=None):
"""
init for an analyzer ensures we have all needed dependencies.
For the libabigail analyzer, this means Libabigail.
Since the output for libabigail is one file per object, we communicate
with the monitor multiple times.
"""
super(Libabigail, self).__init__(spec, dirname)
# This doesn't seem to work to import on the module level
tty.debug("Preparing to use Libabigail, will install if missing.")
with spack.bootstrap.ensure_bootstrap_configuration():
# libabigail won't install lib/bin/share without docs
spec = spack.spec.Spec("libabigail+docs")
spack.bootstrap.ensure_executables_in_path_or_raise(
["abidw"], abstract_spec=spec
)
self.abidw = spack.util.executable.which('abidw')
def run(self):
"""
Run libabigail, and save results to filename.
This run function differs in that we write as we generate and then
return a dict with the analyzer name as the key, and the value of a
dict of results, where the key is the object name, and the value is
the output file written to.
"""
manifest = spack.binary_distribution.get_buildfile_manifest(self.spec)
# This result will store a path to each file
result = {}
# Generate an output file for each binary or object
for obj in manifest.get("binary_to_relocate_fullpath", []):
# We want to preserve the path in the install directory in case
# a library has an equivalenly named lib or executable, for example
outdir = os.path.dirname(obj.replace(self.spec.package.prefix,
'').strip(os.path.sep))
outfile = "spack-analyzer-libabigail-%s.xml" % os.path.basename(obj)
outfile = os.path.join(self.output_dir, outdir, outfile)
outdir = os.path.dirname(outfile)
# Create the output directory
if not os.path.exists(outdir):
os.makedirs(outdir)
# Sometimes libabigail segfaults and dumps
try:
self.abidw(obj, "--out-file", outfile)
result[obj] = outfile
tty.info("Writing result to %s" % outfile)
except spack.error.SpackError:
tty.warn("Issue running abidw for %s" % obj)
return {self.name: result}
def save_result(self, result, overwrite=False):
"""
Read saved ABI results and upload to monitor server.
ABI results are saved to individual files, so each one needs to be
read and uploaded. Result here should be the lookup generated in run(),
the key is the analyzer name, and each value is the result file.
We currently upload the entire xml as text because libabigail can't
easily read gzipped xml, but this will be updated when it can.
"""
if not spack.monitor.cli:
return
name = self.spec.package.name
for obj, filename in result.get(self.name, {}).items():
# Don't include the prefix
rel_path = obj.replace(self.spec.prefix + os.path.sep, "")
# We've already saved the results to file during run
content = spack.monitor.read_file(filename)
# A result needs an analyzer, value or binary_value, and name
data = {"value": content, "install_file": rel_path, "name": "abidw-xml"}
tty.info("Sending result for %s %s to monitor." % (name, rel_path))
spack.hooks.on_analyzer_save(self.spec.package, {"libabigail": [data]})

View file

@ -1,116 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import sys
import llnl.util.tty as tty
import spack.analyzers
import spack.build_environment
import spack.cmd
import spack.cmd.common.arguments as arguments
import spack.environment as ev
import spack.fetch_strategy
import spack.monitor
import spack.paths
import spack.report
description = "run analyzers on installed packages"
section = "analysis"
level = "long"
def setup_parser(subparser):
sp = subparser.add_subparsers(metavar='SUBCOMMAND', dest='analyze_command')
sp.add_parser('list-analyzers',
description="list available analyzers",
help="show list of analyzers that are available to run.")
# This adds the monitor group to the subparser
spack.monitor.get_monitor_group(subparser)
# Run Parser
run_parser = sp.add_parser('run', description="run an analyzer",
help="provide the name of the analyzer to run.")
run_parser.add_argument(
'--overwrite', action='store_true',
help="re-analyze even if the output file already exists.")
run_parser.add_argument(
'-p', '--path', default=None,
dest='path',
help="write output to a different directory than ~/.spack/analyzers")
run_parser.add_argument(
'-a', '--analyzers', default=None,
dest="analyzers", action="append",
help="add an analyzer (defaults to all available)")
arguments.add_common_arguments(run_parser, ['spec'])
def analyze_spec(spec, analyzers=None, outdir=None, monitor=None, overwrite=False):
"""
Do an analysis for a spec, optionally adding monitoring.
We also allow the user to specify a custom output directory.
analyze_spec(spec, args.analyzers, args.outdir, monitor)
Args:
spec (spack.spec.Spec): spec object of installed package
analyzers (list): list of analyzer (keys) to run
monitor (spack.monitor.SpackMonitorClient): a monitor client
overwrite (bool): overwrite result if already exists
"""
analyzers = analyzers or list(spack.analyzers.analyzer_types.keys())
# Load the build environment from the spec install directory, and send
# the spec to the monitor if it's not known
if monitor:
monitor.load_build_environment(spec)
monitor.new_configuration([spec])
for name in analyzers:
# Instantiate the analyzer with the spec and outdir
analyzer = spack.analyzers.get_analyzer(name)(spec, outdir)
# Run the analyzer to get a json result - results are returned as
# a dictionary with a key corresponding to the analyzer type, so
# we can just update the data
result = analyzer.run()
# Send the result. We do them separately because:
# 1. each analyzer might have differently organized output
# 2. the size of a result can be large
analyzer.save_result(result, overwrite)
def analyze(parser, args, **kwargs):
# If the user wants to list analyzers, do so and exit
if args.analyze_command == "list-analyzers":
spack.analyzers.list_all()
sys.exit(0)
# handle active environment, if any
env = ev.active_environment()
# Get an disambiguate spec (we should only have one)
specs = spack.cmd.parse_specs(args.spec)
if not specs:
tty.die("You must provide one or more specs to analyze.")
spec = spack.cmd.disambiguate_spec(specs[0], env)
# The user wants to monitor builds using github.com/spack/spack-monitor
# It is instantianted once here, and then available at spack.monitor.cli
monitor = None
if args.use_monitor:
monitor = spack.monitor.get_client(
host=args.monitor_host,
prefix=args.monitor_prefix,
)
# Run the analysis
analyze_spec(spec, args.analyzers, args.path, monitor, args.overwrite)

View file

@ -9,7 +9,6 @@
import spack.container
import spack.container.images
import spack.monitor
description = ("creates recipes to build images for different"
" container runtimes")
@ -18,7 +17,6 @@
def setup_parser(subparser):
monitor_group = spack.monitor.get_monitor_group(subparser) # noqa
subparser.add_argument(
'--list-os', action='store_true', default=False,
help='list all the OS that can be used in the bootstrap phase and exit'
@ -46,14 +44,5 @@ def containerize(parser, args):
raise ValueError(msg.format(config_file))
config = spack.container.validate(config_file)
# If we have a monitor request, add monitor metadata to config
if args.use_monitor:
config['spack']['monitor'] = {
"host": args.monitor_host,
"keep_going": args.monitor_keep_going,
"prefix": args.monitor_prefix,
"tags": args.monitor_tags
}
recipe = spack.container.recipe(config, last_phase=args.last_stage)
print(recipe)

View file

@ -17,7 +17,6 @@
import spack.cmd.common.arguments as arguments
import spack.environment as ev
import spack.fetch_strategy
import spack.monitor
import spack.paths
import spack.report
from spack.error import SpackError
@ -105,8 +104,6 @@ def setup_parser(subparser):
'--cache-only', action='store_true', dest='cache_only', default=False,
help="only install package from binary mirrors")
monitor_group = spack.monitor.get_monitor_group(subparser) # noqa
subparser.add_argument(
'--include-build-deps', action='store_true', dest='include_build_deps',
default=False, help="""include build deps when installing from cache,
@ -292,15 +289,6 @@ def install(parser, args, **kwargs):
parser.print_help()
return
# The user wants to monitor builds using github.com/spack/spack-monitor
if args.use_monitor:
monitor = spack.monitor.get_client(
host=args.monitor_host,
prefix=args.monitor_prefix,
tags=args.monitor_tags,
save_local=args.monitor_save_local,
)
reporter = spack.report.collect_info(
spack.package_base.PackageInstaller, '_install_task', args.log_format, args)
if args.log_file:
@ -341,10 +329,6 @@ def get_tests(specs):
reporter.filename = default_log_file(specs[0])
reporter.specs = specs
# Tell the monitor about the specs
if args.use_monitor and specs:
monitor.new_configuration(specs)
tty.msg("Installing environment {0}".format(env.name))
with reporter('build'):
env.install_all(**kwargs)
@ -390,10 +374,6 @@ def get_tests(specs):
except SpackError as e:
tty.debug(e)
reporter.concretization_report(e.message)
# Tell spack monitor about it
if args.use_monitor and abstract_specs:
monitor.failed_concretization(abstract_specs)
raise
# 2. Concrete specs from yaml files
@ -454,17 +434,4 @@ def get_tests(specs):
# overwrite all concrete explicit specs from this build
kwargs['overwrite'] = [spec.dag_hash() for spec in specs]
# Update install_args with the monitor args, needed for build task
kwargs.update({
"monitor_keep_going": args.monitor_keep_going,
"monitor_host": args.monitor_host,
"use_monitor": args.use_monitor,
"monitor_prefix": args.monitor_prefix,
})
# If we are using the monitor, we send configs. and create build
# The dag_hash is the main package id
if args.use_monitor and specs:
monitor.new_configuration(specs)
install_specs(args, kwargs, zip(abstract_specs, specs))

View file

@ -1,33 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import spack.monitor
description = "interact with a monitor server"
section = "analysis"
level = "long"
def setup_parser(subparser):
sp = subparser.add_subparsers(metavar='SUBCOMMAND', dest='monitor_command')
# This adds the monitor group to the subparser
spack.monitor.get_monitor_group(subparser)
# Spack Monitor Uploads
monitor_parser = sp.add_parser('upload', description="upload to spack monitor")
monitor_parser.add_argument("upload_dir", help="directory root to upload")
def monitor(parser, args, **kwargs):
if args.monitor_command == "upload":
monitor = spack.monitor.get_client(
host=args.monitor_host,
prefix=args.monitor_prefix,
)
# Upload the directory
monitor.upload_local_save(args.upload_dir)

View file

@ -180,26 +180,6 @@ def paths(self):
view='/opt/view'
)
@tengine.context_property
def monitor(self):
"""Enable using spack monitor during build."""
Monitor = collections.namedtuple('Monitor', [
'enabled', 'host', 'prefix', 'keep_going', 'tags'
])
monitor = self.config.get("monitor")
# If we don't have a monitor group, cut out early.
if not monitor:
return Monitor(False, None, None, None, None)
return Monitor(
enabled=True,
host=monitor.get('host'),
prefix=monitor.get('prefix'),
keep_going=monitor.get("keep_going"),
tags=monitor.get('tags')
)
@tengine.context_property
def manifest(self):
"""The spack.yaml file that should be used in the image"""
@ -208,8 +188,6 @@ def manifest(self):
# Copy in the part of spack.yaml prescribed in the configuration file
manifest = copy.deepcopy(self.config)
manifest.pop('container')
if "monitor" in manifest:
manifest.pop("monitor")
# Ensure that a few paths are where they need to be
manifest.setdefault('config', syaml.syaml_dict())

View file

@ -21,7 +21,6 @@
* on_phase_success(pkg, phase_name, log_file)
* on_phase_error(pkg, phase_name, log_file)
* on_phase_error(pkg, phase_name, log_file)
* on_analyzer_save(pkg, result)
* post_env_write(env)
This can be used to implement support for things like module
@ -92,8 +91,5 @@ def __call__(self, *args, **kwargs):
on_install_failure = _HookRunner('on_install_failure')
on_install_cancel = _HookRunner('on_install_cancel')
# Analyzer hooks
on_analyzer_save = _HookRunner('on_analyzer_save')
# Environment hooks
post_env_write = _HookRunner('post_env_write')

View file

@ -1,85 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import llnl.util.tty as tty
import spack.monitor
def on_install_start(spec):
"""On start of an install, we want to ping the server if it exists
"""
if not spack.monitor.cli:
return
tty.debug("Running on_install_start for %s" % spec)
build_id = spack.monitor.cli.new_build(spec)
tty.verbose("Build created with id %s" % build_id)
def on_install_success(spec):
"""On the success of an install (after everything is complete)
"""
if not spack.monitor.cli:
return
tty.debug("Running on_install_success for %s" % spec)
result = spack.monitor.cli.update_build(spec, status="SUCCESS")
tty.verbose(result.get('message'))
def on_install_failure(spec):
"""Triggered on failure of an install
"""
if not spack.monitor.cli:
return
tty.debug("Running on_install_failure for %s" % spec)
result = spack.monitor.cli.fail_task(spec)
tty.verbose(result.get('message'))
def on_install_cancel(spec):
"""Triggered on cancel of an install
"""
if not spack.monitor.cli:
return
tty.debug("Running on_install_cancel for %s" % spec)
result = spack.monitor.cli.cancel_task(spec)
tty.verbose(result.get('message'))
def on_phase_success(pkg, phase_name, log_file):
"""Triggered on a phase success
"""
if not spack.monitor.cli:
return
tty.debug("Running on_phase_success %s, phase %s" % (pkg.name, phase_name))
result = spack.monitor.cli.send_phase(pkg, phase_name, log_file, "SUCCESS")
tty.verbose(result.get('message'))
def on_phase_error(pkg, phase_name, log_file):
"""Triggered on a phase error
"""
if not spack.monitor.cli:
return
tty.debug("Running on_phase_error %s, phase %s" % (pkg.name, phase_name))
result = spack.monitor.cli.send_phase(pkg, phase_name, log_file, "ERROR")
tty.verbose(result.get('message'))
def on_analyzer_save(pkg, result):
"""given a package and a result, if we have a spack monitor, upload
the result to it.
"""
if not spack.monitor.cli:
return
# This hook runs after a save result
spack.monitor.cli.send_analyze_metadata(pkg, result)

View file

@ -49,7 +49,6 @@
import spack.compilers
import spack.error
import spack.hooks
import spack.monitor
import spack.package_base
import spack.package_prefs as prefs
import spack.repo

View file

@ -1,738 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Interact with a Spack Monitor Service. Derived from
https://github.com/spack/spack-monitor/blob/main/script/spackmoncli.py
"""
import base64
import hashlib
import os
import re
from datetime import datetime
try:
from urllib.error import URLError
from urllib.request import Request, urlopen
except ImportError:
from urllib2 import urlopen, Request, URLError # type: ignore # novm
from copy import deepcopy
from glob import glob
import llnl.util.tty as tty
import spack
import spack.config
import spack.hash_types as ht
import spack.main
import spack.paths
import spack.store
import spack.util.path
import spack.util.spack_json as sjson
import spack.util.spack_yaml as syaml
# A global client to instantiate once
cli = None
def get_client(host, prefix="ms1", allow_fail=False, tags=None, save_local=False):
"""
Get a monitor client for a particular host and prefix.
If the client is not running, we exit early, unless allow_fail is set
to true, indicating that we should continue the build even if the
server is not present. Note that this client is defined globally as "cli"
so we can istantiate it once (checking for credentials, etc.) and then
always have access to it via spack.monitor.cli. Also note that
typically, we call the monitor by way of hooks in spack.hooks.monitor.
So if you want the monitor to have a new interaction with some part of
the codebase, it's recommended to write a hook first, and then have
the monitor use it.
"""
global cli
cli = SpackMonitorClient(host=host, prefix=prefix, allow_fail=allow_fail,
tags=tags, save_local=save_local)
# Auth is always required unless we are saving locally
if not save_local:
cli.require_auth()
# We will exit early if the monitoring service is not running, but
# only if we aren't doing a local save
if not save_local:
info = cli.service_info()
# If we allow failure, the response will be done
if info:
tty.debug("%s v.%s has status %s" % (
info['id'],
info['version'],
info['status'])
)
return cli
def get_monitor_group(subparser):
"""
Retrieve the monitor group for the argument parser.
Since the monitor group is shared between commands, we provide a common
function to generate the group for it. The user can pass the subparser, and
the group is added, and returned.
"""
# Monitoring via https://github.com/spack/spack-monitor
monitor_group = subparser.add_argument_group()
monitor_group.add_argument(
'--monitor', action='store_true', dest='use_monitor', default=False,
help="interact with a monitor server during builds.")
monitor_group.add_argument(
'--monitor-save-local', action='store_true', dest='monitor_save_local',
default=False, help="save monitor results to .spack instead of server.")
monitor_group.add_argument(
'--monitor-tags', dest='monitor_tags', default=None,
help="One or more (comma separated) tags for a build.")
monitor_group.add_argument(
'--monitor-keep-going', action='store_true', dest='monitor_keep_going',
default=False, help="continue the build if a request to monitor fails.")
monitor_group.add_argument(
'--monitor-host', dest='monitor_host', default="http://127.0.0.1",
help="If using a monitor, customize the host.")
monitor_group.add_argument(
'--monitor-prefix', dest='monitor_prefix', default="ms1",
help="The API prefix for the monitor service.")
return monitor_group
class SpackMonitorClient:
"""Client to interact with a spack monitor server.
We require the host url, along with the prefix to discover the
service_info endpoint. If allow_fail is set to True, we will not exit
on error with tty.die given that a request is not successful. The spack
version is one of the fields to uniquely identify a spec, so we add it
to the client on init.
"""
def __init__(self, host=None, prefix="ms1", allow_fail=False, tags=None,
save_local=False):
# We can control setting an arbitrary version if needed
sv = spack.main.get_version()
self.spack_version = os.environ.get("SPACKMON_SPACK_VERSION") or sv
self.host = host or "http://127.0.0.1"
self.baseurl = "%s/%s" % (self.host, prefix.strip("/"))
self.token = os.environ.get("SPACKMON_TOKEN")
self.username = os.environ.get("SPACKMON_USER")
self.headers = {}
self.allow_fail = allow_fail
self.capture_build_environment()
self.tags = tags
self.save_local = save_local
# We key lookup of build_id by dag_hash
self.build_ids = {}
self.setup_save()
def setup_save(self):
"""Given a local save "save_local" ensure the output directory exists.
"""
if not self.save_local:
return
save_dir = spack.util.path.canonicalize_path(
spack.config.get('config:monitor_dir', spack.paths.default_monitor_path)
)
# Name based on timestamp
now = datetime.now().strftime('%Y-%m-%d-%H-%M-%S-%s')
self.save_dir = os.path.join(save_dir, now)
if not os.path.exists(self.save_dir):
os.makedirs(self.save_dir)
def save(self, obj, filename):
"""
Save a monitor json result to the save directory.
"""
filename = os.path.join(self.save_dir, filename)
write_json(obj, filename)
return {"message": "Build saved locally to %s" % filename}
def load_build_environment(self, spec):
"""
Load a build environment from install_environment.json.
If we are running an analyze command, we will need to load previously
used build environment metadata from install_environment.json to capture
what was done during the build.
"""
if not hasattr(spec, "package") or not spec.package:
tty.die("A spec must have a package to load the environment.")
pkg_dir = os.path.dirname(spec.package.install_log_path)
env_file = os.path.join(pkg_dir, "install_environment.json")
build_environment = read_json(env_file)
if not build_environment:
tty.warn(
"install_environment.json not found in package folder. "
" This means that the current environment metadata will be used."
)
else:
self.build_environment = build_environment
def capture_build_environment(self):
"""
Capture the environment for the build.
This uses spack.util.environment.get_host_environment_metadata to do so.
This is important because it's a unique identifier, along with the spec,
for a Build. It should look something like this:
{'host_os': 'ubuntu20.04',
'platform': 'linux',
'host_target': 'skylake',
'hostname': 'vanessa-ThinkPad-T490s',
'spack_version': '0.16.1-1455-52d5b55b65',
'kernel_version': '#73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021'}
This is saved to a package install's metadata folder as
install_environment.json, and can be loaded by the monitor for uploading
data relevant to a later analysis.
"""
from spack.util.environment import get_host_environment_metadata
self.build_environment = get_host_environment_metadata()
keys = list(self.build_environment.keys())
# Allow to customize any of these values via the environment
for key in keys:
envar_name = "SPACKMON_%s" % key.upper()
envar = os.environ.get(envar_name)
if envar:
self.build_environment[key] = envar
def require_auth(self):
"""
Require authentication.
The token and username must not be unset
"""
if not self.save_local and (not self.token or not self.username):
tty.die("You are required to export SPACKMON_TOKEN and SPACKMON_USER")
def set_header(self, name, value):
self.headers.update({name: value})
def set_basic_auth(self, username, password):
"""
A wrapper to adding basic authentication to the Request
"""
auth_str = "%s:%s" % (username, password)
auth_header = base64.b64encode(auth_str.encode("utf-8"))
self.set_header("Authorization", "Basic %s" % auth_header.decode("utf-8"))
def reset(self):
"""
Reset and prepare for a new request.
"""
if "Authorization" in self.headers:
self.headers = {"Authorization": self.headers['Authorization']}
else:
self.headers = {}
def prepare_request(self, endpoint, data, headers):
"""
Prepare a request given an endpoint, data, and headers.
If data is provided, urllib makes the request a POST
"""
# Always reset headers for new request.
self.reset()
# Preserve previously used auth token
headers = headers or self.headers
# The calling function can provide a full or partial url
if not endpoint.startswith("http"):
endpoint = "%s/%s" % (self.baseurl, endpoint)
# If we have data, the request will be POST
if data:
if not isinstance(data, str):
data = sjson.dump(data)
data = data.encode('ascii')
return Request(endpoint, data=data, headers=headers)
def issue_request(self, request, retry=True):
"""
Given a prepared request, issue it.
If we get an error, die. If
there are times when we don't want to exit on error (but instead
disable using the monitoring service) we could add that here.
"""
try:
response = urlopen(request)
except URLError as e:
# If we have an authorization request, retry once with auth
if hasattr(e, "code") and e.code == 401 and retry:
if self.authenticate_request(e):
request = self.prepare_request(
e.url,
sjson.load(request.data.decode('utf-8')),
self.headers
)
return self.issue_request(request, False)
# Handle permanent re-directs!
elif hasattr(e, "code") and e.code == 308:
location = e.headers.get('Location')
request_data = None
if request.data:
request_data = sjson.load(request.data.decode('utf-8'))[0]
if location:
request = self.prepare_request(
location,
request_data,
self.headers
)
return self.issue_request(request, True)
# Otherwise, relay the message and exit on error
msg = ""
if hasattr(e, 'reason'):
msg = e.reason
elif hasattr(e, 'code'):
msg = e.code
# If we can parse the message, try it
try:
msg += "\n%s" % e.read().decode("utf8", 'ignore')
except Exception:
pass
if self.allow_fail:
tty.warning("Request to %s was not successful, but continuing." % e.url)
return
tty.die(msg)
return response
def do_request(self, endpoint, data=None, headers=None, url=None):
"""
Do the actual request.
If data is provided, it is POST, otherwise GET.
If an entire URL is provided, don't use the endpoint
"""
request = self.prepare_request(endpoint, data, headers)
# If we have an authorization error, we retry with
response = self.issue_request(request)
# A 200/201 response incidates success
if response.code in [200, 201]:
return sjson.load(response.read().decode('utf-8'))
return response
def authenticate_request(self, originalResponse):
"""
Authenticate the request.
Given a response (an HTTPError 401), look for a Www-Authenticate
header to parse. We return True/False to indicate if the request
should be retried.
"""
authHeaderRaw = originalResponse.headers.get("Www-Authenticate")
if not authHeaderRaw:
return False
# If we have a username and password, set basic auth automatically
if self.token and self.username:
self.set_basic_auth(self.username, self.token)
headers = deepcopy(self.headers)
if "Authorization" not in headers:
tty.error(
"This endpoint requires a token. Please set "
"client.set_basic_auth(username, password) first "
"or export them to the environment."
)
return False
# Prepare request to retry
h = parse_auth_header(authHeaderRaw)
headers.update({
"service": h.Service,
"Accept": "application/json",
"User-Agent": "spackmoncli"}
)
# Currently we don't set a scope (it defaults to build)
authResponse = self.do_request(h.Realm, headers=headers)
# Request the token
token = authResponse.get("token")
if not token:
return False
# Set the token to the original request and retry
self.headers.update({"Authorization": "Bearer %s" % token})
return True
# Functions correspond to endpoints
def service_info(self):
"""
Get the service information endpoint
"""
# Base endpoint provides service info
return self.do_request("")
def new_configuration(self, specs):
"""
Given a list of specs, generate a new configuration for each.
We return a lookup of specs with their package names. This assumes
that we are only installing one version of each package. We aren't
starting or creating any builds, so we don't need a build environment.
"""
configs = {}
# There should only be one spec generally (what cases would have >1?)
for spec in specs:
# Not sure if this is needed here, but I see it elsewhere
if spec.name in spack.repo.path or spec.virtual:
spec.concretize()
# Remove extra level of nesting
# This is the only place in Spack we still use full_hash, as `spack monitor`
# requires specs with full_hash-keyed dependencies.
as_dict = {"spec": spec.to_dict(hash=ht.full_hash)['spec'],
"spack_version": self.spack_version}
if self.save_local:
filename = "spec-%s-%s-config.json" % (spec.name, spec.version)
self.save(as_dict, filename)
else:
response = self.do_request("specs/new/", data=sjson.dump(as_dict))
configs[spec.package.name] = response.get('data', {})
return configs
def failed_concretization(self, specs):
"""
Given a list of abstract specs, tell spack monitor concretization failed.
"""
configs = {}
# There should only be one spec generally (what cases would have >1?)
for spec in specs:
# update the spec to have build hash indicating that cannot be built
meta = spec.to_dict()['spec']
nodes = []
for node in meta.get("nodes", []):
node["full_hash"] = "FAILED_CONCRETIZATION"
nodes.append(node)
meta['nodes'] = nodes
# We can't concretize / hash
as_dict = {"spec": meta,
"spack_version": self.spack_version}
if self.save_local:
filename = "spec-%s-%s-config.json" % (spec.name, spec.version)
self.save(as_dict, filename)
else:
response = self.do_request("specs/new/", data=sjson.dump(as_dict))
configs[spec.package.name] = response.get('data', {})
return configs
def new_build(self, spec):
"""
Create a new build.
This means sending the hash of the spec to be built,
along with the build environment. These two sets of data uniquely can
identify the build, and we will add objects (the binaries produced) to
it. We return the build id to the calling client.
"""
return self.get_build_id(spec, return_response=True)
def get_build_id(self, spec, return_response=False, spec_exists=True):
"""
Retrieve a build id, either in the local cache, or query the server.
"""
dag_hash = spec.dag_hash()
if dag_hash in self.build_ids:
return self.build_ids[dag_hash]
# Prepare build environment data (including spack version)
data = self.build_environment.copy()
data['full_hash'] = dag_hash
# If the build should be tagged, add it
if self.tags:
data['tags'] = self.tags
# If we allow the spec to not exist (meaning we create it) we need to
# include the full specfile here
if not spec_exists:
meta_dir = os.path.dirname(spec.package.install_log_path)
spec_file = os.path.join(meta_dir, "spec.json")
if os.path.exists(spec_file):
data['spec'] = sjson.load(read_file(spec_file))
else:
spec_file = os.path.join(meta_dir, "spec.yaml")
data['spec'] = syaml.load(read_file(spec_file))
if self.save_local:
return self.get_local_build_id(data, dag_hash, return_response)
return self.get_server_build_id(data, dag_hash, return_response)
def get_local_build_id(self, data, dag_hash, return_response):
"""
Generate a local build id based on hashing the expected data
"""
hasher = hashlib.md5()
hasher.update(str(data).encode('utf-8'))
bid = hasher.hexdigest()
filename = "build-metadata-%s.json" % bid
response = self.save(data, filename)
if return_response:
return response
return bid
def get_server_build_id(self, data, dag_hash, return_response=False):
"""
Retrieve a build id from the spack monitor server
"""
response = self.do_request("builds/new/", data=sjson.dump(data))
# Add the build id to the lookup
bid = self.build_ids[dag_hash] = response['data']['build']['build_id']
self.build_ids[dag_hash] = bid
# If the function is called directly, the user might want output
if return_response:
return response
return bid
def update_build(self, spec, status="SUCCESS"):
"""
Update a build with a new status.
This typically updates the relevant package to indicate a
successful install. This endpoint can take a general status to update.
"""
data = {"build_id": self.get_build_id(spec), "status": status}
if self.save_local:
filename = "build-%s-status.json" % data['build_id']
return self.save(data, filename)
return self.do_request("builds/update/", data=sjson.dump(data))
def fail_task(self, spec):
"""Given a spec, mark it as failed. This means that Spack Monitor
marks all dependencies as cancelled, unless they are already successful
"""
return self.update_build(spec, status="FAILED")
def cancel_task(self, spec):
"""Given a spec, mark it as cancelled.
"""
return self.update_build(spec, status="CANCELLED")
def send_analyze_metadata(self, pkg, metadata):
"""
Send spack analyzer metadata to the spack monitor server.
Given a dictionary of analyzers (with key as analyzer type, and
value as the data) upload the analyzer output to Spack Monitor.
Spack Monitor should either have a known understanding of the analyzer,
or if not (the key is not recognized), it's assumed to be a dictionary
of objects/files, each with attributes to be updated. E.g.,
{"analyzer-name": {"object-file-path": {"feature1": "value1"}}}
"""
# Prepare build environment data (including spack version)
# Since the build might not have been generated, we include the spec
data = {"build_id": self.get_build_id(pkg.spec, spec_exists=False),
"metadata": metadata}
return self.do_request("analyze/builds/", data=sjson.dump(data))
def send_phase(self, pkg, phase_name, phase_output_file, status):
"""
Send the result of a phase during install.
Given a package, phase name, and status, update the monitor endpoint
to alert of the status of the stage. This includes parsing the package
metadata folder for phase output and error files
"""
data = {"build_id": self.get_build_id(pkg.spec)}
# Send output specific to the phase (does this include error?)
data.update({"status": status,
"output": read_file(phase_output_file),
"phase_name": phase_name})
if self.save_local:
filename = "build-%s-phase-%s.json" % (data['build_id'], phase_name)
return self.save(data, filename)
return self.do_request("builds/phases/update/", data=sjson.dump(data))
def upload_specfile(self, filename):
"""
Upload a spec file to the spack monitor server.
Given a spec file (must be json) upload to the UploadSpec endpoint.
This function is not used in the spack to server workflow, but could
be useful is Spack Monitor is intended to send an already generated
file in some kind of separate analysis. For the environment file, we
parse out SPACK_* variables to include.
"""
# We load as json just to validate it
spec = read_json(filename)
data = {"spec": spec, "spack_verison": self.spack_version}
if self.save_local:
filename = "spec-%s-%s.json" % (spec.name, spec.version)
return self.save(data, filename)
return self.do_request("specs/new/", data=sjson.dump(data))
def iter_read(self, pattern):
"""
A helper to read json from a directory glob and return it loaded.
"""
for filename in glob(pattern):
basename = os.path.basename(filename)
tty.info("Reading %s" % basename)
yield read_json(filename)
def upload_local_save(self, dirname):
"""
Upload results from a locally saved directory to spack monitor.
The general workflow will first include an install with save local:
spack install --monitor --monitor-save-local
And then a request to upload the root or specific directory.
spack upload monitor ~/.spack/reports/monitor/<date>/
"""
dirname = os.path.abspath(dirname)
if not os.path.exists(dirname):
tty.die("%s does not exist." % dirname)
# We can't be sure the level of nesting the user has provided
# So we walk recursively through and look for build metadata
for subdir, dirs, files in os.walk(dirname):
root = os.path.join(dirname, subdir)
# A metadata file signals a monitor export
metadata = glob("%s%sbuild-metadata*" % (root, os.sep))
if not metadata or not files or not root or not subdir:
continue
self._upload_local_save(root)
tty.info("Upload complete")
def _upload_local_save(self, dirname):
"""
Given a found metadata file, upload results to spack monitor.
"""
# First find all the specs
for spec in self.iter_read("%s%sspec*" % (dirname, os.sep)):
self.do_request("specs/new/", data=sjson.dump(spec))
# Load build metadata to generate an id
metadata = glob("%s%sbuild-metadata*" % (dirname, os.sep))
if not metadata:
tty.die("Build metadata file(s) missing in %s" % dirname)
# Create a build_id lookup based on hash
hashes = {}
for metafile in metadata:
data = read_json(metafile)
build = self.do_request("builds/new/", data=sjson.dump(data))
localhash = os.path.basename(metafile).replace(".json", "")
hashes[localhash.replace('build-metadata-', "")] = build
# Next upload build phases
for phase in self.iter_read("%s%sbuild*phase*" % (dirname, os.sep)):
build_id = hashes[phase['build_id']]['data']['build']['build_id']
phase['build_id'] = build_id
self.do_request("builds/phases/update/", data=sjson.dump(phase))
# Next find the status objects
for status in self.iter_read("%s%sbuild*status*" % (dirname, os.sep)):
build_id = hashes[status['build_id']]['data']['build']['build_id']
status['build_id'] = build_id
self.do_request("builds/update/", data=sjson.dump(status))
# Helper functions
def parse_auth_header(authHeaderRaw):
"""
Parse an authentication header into relevant pieces
"""
regex = re.compile('([a-zA-z]+)="(.+?)"')
matches = regex.findall(authHeaderRaw)
lookup = dict()
for match in matches:
lookup[match[0]] = match[1]
return authHeader(lookup)
class authHeader:
def __init__(self, lookup):
"""Given a dictionary of values, match them to class attributes"""
for key in lookup:
if key in ["realm", "service", "scope"]:
setattr(self, key.capitalize(), lookup[key])
def read_file(filename):
"""
Read a file, if it exists. Otherwise return None
"""
if not os.path.exists(filename):
return
with open(filename, 'r') as fd:
content = fd.read()
return content
def write_file(content, filename):
"""
Write content to file
"""
with open(filename, 'w') as fd:
fd.writelines(content)
return content
def write_json(obj, filename):
"""
Write a json file, if the output directory exists.
"""
if not os.path.exists(os.path.dirname(filename)):
return
return write_file(sjson.dump(obj), filename)
def read_json(filename):
"""
Read a file and load into json, if it exists. Otherwise return None.
"""
if not os.path.exists(filename):
return
return sjson.load(read_file(filename))

View file

@ -1,180 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import sys
import pytest
import spack.cmd.install
import spack.config
import spack.package_base
import spack.util.spack_json as sjson
from spack.main import SpackCommand
from spack.spec import Spec
install = SpackCommand('install')
analyze = SpackCommand('analyze')
pytestmark = pytest.mark.skipif(sys.platform == 'win32',
reason="Test is unsupported on Windows")
def test_test_package_not_installed(mock_fetch, install_mockery_mutable_config):
# We cannot run an analysis for a package not installed
out = analyze('run', 'libdwarf', fail_on_error=False)
assert "==> Error: Spec 'libdwarf' matches no installed packages.\n" in out
def test_analyzer_get_install_dir(mock_fetch, install_mockery_mutable_config):
"""
Test that we cannot get an analyzer directory without a spec package.
"""
spec = Spec('libdwarf').concretized()
assert 'libdwarf' in spack.analyzers.analyzer_base.get_analyzer_dir(spec)
# Case 1: spec is missing attribute for package
with pytest.raises(SystemExit):
spack.analyzers.analyzer_base.get_analyzer_dir(None)
class Packageless(object):
package = None
# Case 2: spec has package attribute, but it's None
with pytest.raises(SystemExit):
spack.analyzers.analyzer_base.get_analyzer_dir(Packageless())
def test_malformed_analyzer(mock_fetch, install_mockery_mutable_config):
"""
Test that an analyzer missing needed attributes is invalid.
"""
from spack.analyzers.analyzer_base import AnalyzerBase
# Missing attribute description
class MyAnalyzer(AnalyzerBase):
name = "my_analyzer"
outfile = "my_analyzer_output.txt"
spec = Spec('libdwarf').concretized()
with pytest.raises(SystemExit):
MyAnalyzer(spec)
def test_analyze_output(tmpdir, mock_fetch, install_mockery_mutable_config):
"""
Test that an analyzer errors if requested name does not exist.
"""
install('libdwarf')
install('python@3.8')
analyzer_dir = tmpdir.join('analyzers')
# An analyzer that doesn't exist should not work
out = analyze('run', '-a', 'pusheen', 'libdwarf', fail_on_error=False)
assert '==> Error: Analyzer pusheen does not exist\n' in out
# We will output to this analyzer directory
analyzer_dir = tmpdir.join('analyzers')
out = analyze('run', '-a', 'install_files', '-p', str(analyzer_dir), 'libdwarf')
# Ensure that if we run again without over write, we don't run
out = analyze('run', '-a', 'install_files', '-p', str(analyzer_dir), 'libdwarf')
assert "skipping" in out
# With overwrite it should run
out = analyze('run', '-a', 'install_files', '-p', str(analyzer_dir),
'--overwrite', 'libdwarf')
assert "==> Writing result to" in out
def _run_analyzer(name, package, tmpdir):
"""
A shared function to test that an analyzer runs.
We return the output file for further inspection.
"""
analyzer = spack.analyzers.get_analyzer(name)
analyzer_dir = tmpdir.join('analyzers')
out = analyze('run', '-a', analyzer.name, '-p', str(analyzer_dir), package)
assert "==> Writing result to" in out
assert "/%s/%s\n" % (analyzer.name, analyzer.outfile) in out
# The output file should exist
output_file = out.strip('\n').split(' ')[-1].strip()
assert os.path.exists(output_file)
return output_file
def test_installfiles_analyzer(tmpdir, mock_fetch, install_mockery_mutable_config):
"""
test the install files analyzer
"""
install('libdwarf')
output_file = _run_analyzer("install_files", "libdwarf", tmpdir)
# Ensure it's the correct content
with open(output_file, 'r') as fd:
content = sjson.load(fd.read())
basenames = set()
for key, attrs in content.items():
basenames.add(os.path.basename(key))
# Check for a few expected files
for key in ['.spack', 'libdwarf', 'packages', 'repo.yaml', 'repos']:
assert key in basenames
def test_environment_analyzer(tmpdir, mock_fetch, install_mockery_mutable_config):
"""
test the environment variables analyzer.
"""
install('libdwarf')
output_file = _run_analyzer("environment_variables", "libdwarf", tmpdir)
with open(output_file, 'r') as fd:
content = sjson.load(fd.read())
# Check a few expected keys
for key in ['SPACK_CC', 'SPACK_COMPILER_SPEC', 'SPACK_ENV_PATH']:
assert key in content
# The analyzer should return no result if the output file does not exist.
spec = Spec('libdwarf').concretized()
env_file = os.path.join(spec.package.prefix, '.spack', 'spack-build-env.txt')
assert os.path.exists(env_file)
os.remove(env_file)
analyzer = spack.analyzers.get_analyzer("environment_variables")
analyzer_dir = tmpdir.join('analyzers')
result = analyzer(spec, analyzer_dir).run()
assert "environment_variables" in result
assert not result['environment_variables']
def test_list_analyzers():
"""
test that listing analyzers shows all the possible analyzers.
"""
from spack.analyzers import analyzer_types
# all cannot be an analyzer
assert "all" not in analyzer_types
# All types should be present!
out = analyze('list-analyzers')
for analyzer_type in analyzer_types:
assert analyzer_type in out
def test_configargs_analyzer(tmpdir, mock_fetch, install_mockery_mutable_config):
"""
test the config args analyzer.
Since we don't have any, this should return an empty result.
"""
install('libdwarf')
analyzer_dir = tmpdir.join('analyzers')
out = analyze('run', '-a', 'config_args', '-p', str(analyzer_dir), 'libdwarf')
assert out == ''

View file

@ -1,278 +0,0 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import sys
import pytest
import llnl.util.tty as tty
import spack.config
import spack.monitor
import spack.spec
from spack.main import SpackCommand
from spack.monitor import SpackMonitorClient
install = SpackCommand('install')
def get_client(host, prefix="ms1", allow_fail=False, tags=None, save_local=False):
"""
We replicate this function to not generate a global client.
"""
cli = SpackMonitorClient(host=host, prefix=prefix, allow_fail=allow_fail,
tags=tags, save_local=save_local)
# We will exit early if the monitoring service is not running, but
# only if we aren't doing a local save
if not save_local:
info = cli.service_info()
# If we allow failure, the response will be done
if info:
tty.debug("%s v.%s has status %s" % (
info['id'],
info['version'],
info['status'])
)
return cli
@pytest.fixture
def mock_monitor_request(monkeypatch):
"""
Monitor requests that are shared across tests go here
"""
def mock_do_request(self, endpoint, *args, **kwargs):
# monitor was originally keyed by full_hash, but now dag_hash is the full hash.
# the name of the field in monitor is still spec_full_hash, for now.
build = {"build_id": 1,
"spec_full_hash": "bpfvysmqndtmods4rmy6d6cfquwblngp",
"spec_name": "dttop"}
# Service Info
if endpoint == "":
organization = {"name": "spack", "url": "https://github.com/spack"}
return {"id": "spackmon", "status": "running",
"name": "Spack Monitor (Spackmon)",
"description": "The best spack monitor",
"organization": organization,
"contactUrl": "https://github.com/spack/spack-monitor/issues",
"documentationUrl": "https://spack-monitor.readthedocs.io",
"createdAt": "2021-04-09T21:54:51Z",
"updatedAt": "2021-05-24T15:06:46Z",
"environment": "test",
"version": "0.0.1",
"auth_instructions_url": "url"}
# New Build
elif endpoint == "builds/new/":
return {"message": "Build get or create was successful.",
"data": {
"build_created": True,
"build_environment_created": True,
"build": build
},
"code": 201}
# Update Build
elif endpoint == "builds/update/":
return {"message": "Status updated",
"data": {"build": build},
"code": 200}
# Send Analyze Metadata
elif endpoint == "analyze/builds/":
return {"message": "Metadata updated",
"data": {"build": build},
"code": 200}
# Update Build Phase
elif endpoint == "builds/phases/update/":
return {"message": "Phase autoconf was successfully updated.",
"code": 200,
"data": {
"build_phase": {
"id": 1,
"status": "SUCCESS",
"name": "autoconf"
}
}}
# Update Phase Status
elif endpoint == "phases/update/":
return {"message": "Status updated",
"data": {"build": build},
"code": 200}
# New Spec
elif endpoint == "specs/new/":
return {"message": "success",
"data": {
"full_hash": "bpfvysmqndtmods4rmy6d6cfquwblngp",
"name": "dttop",
"version": "1.0",
"spack_version": "0.16.0-1379-7a5351d495",
"specs": {
"dtbuild1": "btcmljubs4njhdjqt2ebd6nrtn6vsrks",
"dtlink1": "x4z6zv6lqi7cf6l4twz4bg7hj3rkqfmk",
"dtrun1": "i6inyro74p5yqigllqk5ivvwfjfsw6qz"
}
}}
else:
pytest.fail("bad endpoint: %s" % endpoint)
monkeypatch.setattr(spack.monitor.SpackMonitorClient, "do_request", mock_do_request)
def test_spack_monitor_auth(mock_monitor_request):
os.environ["SPACKMON_TOKEN"] = "xxxxxxxxxxxxxxxxx"
os.environ["SPACKMON_USER"] = "spackuser"
get_client(host="http://127.0.0.1")
def test_spack_monitor_without_auth(mock_monitor_request):
get_client(host="hostname")
@pytest.mark.skipif(sys.platform == 'win32',
reason="Not supported on Windows (yet)")
def test_spack_monitor_build_env(mock_monitor_request, install_mockery_mutable_config):
monitor = get_client(host="hostname")
assert hasattr(monitor, "build_environment")
for key in ["host_os", "platform", "host_target", "hostname", "spack_version",
"kernel_version"]:
assert key in monitor.build_environment
spec = spack.spec.Spec("dttop")
spec.concretize()
# Loads the build environment from the spec install folder
monitor.load_build_environment(spec)
def test_spack_monitor_basic_auth(mock_monitor_request):
monitor = get_client(host="hostname")
# Headers should be empty
assert not monitor.headers
monitor.set_basic_auth("spackuser", "password")
assert "Authorization" in monitor.headers
assert monitor.headers['Authorization'].startswith("Basic")
def test_spack_monitor_new_configuration(mock_monitor_request, install_mockery):
monitor = get_client(host="hostname")
spec = spack.spec.Spec("dttop")
spec.concretize()
response = monitor.new_configuration([spec])
# The response is a lookup of specs
assert "dttop" in response
def test_spack_monitor_new_build(mock_monitor_request, install_mockery_mutable_config,
install_mockery):
monitor = get_client(host="hostname")
spec = spack.spec.Spec("dttop")
spec.concretize()
response = monitor.new_build(spec)
assert "message" in response and "data" in response and "code" in response
assert response['code'] == 201
# We should be able to get a build id
monitor.get_build_id(spec)
def test_spack_monitor_update_build(mock_monitor_request, install_mockery,
install_mockery_mutable_config):
monitor = get_client(host="hostname")
spec = spack.spec.Spec("dttop")
spec.concretize()
response = monitor.update_build(spec, status="SUCCESS")
assert "message" in response and "data" in response and "code" in response
assert response['code'] == 200
def test_spack_monitor_fail_task(mock_monitor_request, install_mockery,
install_mockery_mutable_config):
monitor = get_client(host="hostname")
spec = spack.spec.Spec("dttop")
spec.concretize()
response = monitor.fail_task(spec)
assert "message" in response and "data" in response and "code" in response
assert response['code'] == 200
def test_spack_monitor_send_analyze_metadata(monkeypatch, mock_monitor_request,
install_mockery,
install_mockery_mutable_config):
def buildid(*args, **kwargs):
return 1
monkeypatch.setattr(spack.monitor.SpackMonitorClient, "get_build_id", buildid)
monitor = get_client(host="hostname")
spec = spack.spec.Spec("dttop")
spec.concretize()
response = monitor.send_analyze_metadata(spec.package, metadata={"boop": "beep"})
assert "message" in response and "data" in response and "code" in response
assert response['code'] == 200
def test_spack_monitor_send_phase(mock_monitor_request, install_mockery,
install_mockery_mutable_config):
monitor = get_client(host="hostname")
def get_build_id(*args, **kwargs):
return 1
spec = spack.spec.Spec("dttop")
spec.concretize()
response = monitor.send_phase(spec.package, "autoconf",
spec.package.install_log_path,
"SUCCESS")
assert "message" in response and "data" in response and "code" in response
assert response['code'] == 200
def test_spack_monitor_info(mock_monitor_request):
os.environ["SPACKMON_TOKEN"] = "xxxxxxxxxxxxxxxxx"
os.environ["SPACKMON_USER"] = "spackuser"
monitor = get_client(host="http://127.0.0.1")
info = monitor.service_info()
for key in ['id', 'status', 'name', 'description', 'organization',
'contactUrl', 'documentationUrl', 'createdAt', 'updatedAt',
'environment', 'version', 'auth_instructions_url']:
assert key in info
@pytest.fixture(scope='session')
def test_install_monitor_save_local(install_mockery_mutable_config,
mock_fetch, tmpdir_factory):
"""
Mock installing and saving monitor results to file.
"""
reports_dir = tmpdir_factory.mktemp('reports')
spack.config.set('config:monitor_dir', str(reports_dir))
out = install('--monitor', '--monitor-save-local', 'dttop')
assert "Successfully installed dttop" in out
# The reports directory should not be empty (timestamped folders)
assert os.listdir(str(reports_dir))
# Get the spec name
spec = spack.spec.Spec("dttop")
spec.concretize()
# Ensure we have monitor results saved
for dirname in os.listdir(str(reports_dir)):
dated_dir = os.path.join(str(reports_dir), dirname)
build_metadata = "build-metadata-%s.json" % spec.dag_hash()
assert build_metadata in os.listdir(dated_dir)
spec_file = "spec-dttop-%s-config.json" % spec.version
assert spec_file in os.listdir(dated_dir)
spack.config.set('config:monitor_dir', "~/.spack/reports/monitor")

View file

@ -337,7 +337,7 @@ _spack() {
then
SPACK_COMPREPLY="-h --help -H --all-help --color -c --config -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -b --bootstrap -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
else
SPACK_COMPREPLY="activate add analyze arch audit blame bootstrap build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop diff docs edit env extensions external fetch find gc gpg graph help info install license list load location log-parse maintainers make-installer mark mirror module monitor patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style tags test test-env tutorial undevelop uninstall unit-test unload url verify versions view"
SPACK_COMPREPLY="activate add arch audit blame bootstrap build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop diff docs edit env extensions external fetch find gc gpg graph help info install license list load location log-parse maintainers make-installer mark mirror module patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style tags test test-env tutorial undevelop uninstall unit-test unload url verify versions view"
fi
}
@ -359,28 +359,6 @@ _spack_add() {
fi
}
_spack_analyze() {
if $list_options
then
SPACK_COMPREPLY="-h --help --monitor --monitor-save-local --monitor-tags --monitor-keep-going --monitor-host --monitor-prefix"
else
SPACK_COMPREPLY="list-analyzers run"
fi
}
_spack_analyze_list_analyzers() {
SPACK_COMPREPLY="-h --help"
}
_spack_analyze_run() {
if $list_options
then
SPACK_COMPREPLY="-h --help --overwrite -p --path -a --analyzers"
else
_all_packages
fi
}
_spack_arch() {
SPACK_COMPREPLY="-h --help -g --generic-target --known-targets -p --platform -o --operating-system -t --target -f --frontend -b --backend"
}
@ -829,7 +807,7 @@ _spack_config_revert() {
}
_spack_containerize() {
SPACK_COMPREPLY="-h --help --monitor --monitor-save-local --monitor-tags --monitor-keep-going --monitor-host --monitor-prefix --list-os --last-stage"
SPACK_COMPREPLY="-h --help --list-os --last-stage"
}
_spack_create() {
@ -1201,7 +1179,7 @@ _spack_info() {
_spack_install() {
if $list_options
then
SPACK_COMPREPLY="-h --help --only -u --until -j --jobs --overwrite --fail-fast --keep-prefix --keep-stage --dont-restage --use-cache --no-cache --cache-only --monitor --monitor-save-local --monitor-tags --monitor-keep-going --monitor-host --monitor-prefix --include-build-deps --no-check-signature --show-log-on-error --source -n --no-checksum --deprecated -v --verbose --fake --only-concrete --no-add -f --file --clean --dirty --test --log-format --log-file --help-cdash --cdash-upload-url --cdash-build --cdash-site --cdash-track --cdash-buildstamp -y --yes-to-all -U --fresh --reuse"
SPACK_COMPREPLY="-h --help --only -u --until -j --jobs --overwrite --fail-fast --keep-prefix --keep-stage --dont-restage --use-cache --no-cache --cache-only --include-build-deps --no-check-signature --show-log-on-error --source -n --no-checksum --deprecated -v --verbose --fake --only-concrete --no-add -f --file --clean --dirty --test --log-format --log-file --help-cdash --cdash-upload-url --cdash-build --cdash-site --cdash-track --cdash-buildstamp -y --yes-to-all -U --fresh --reuse"
else
_all_packages
fi
@ -1470,10 +1448,6 @@ _spack_module_tcl_setdefault() {
fi
}
_spack_monitor() {
SPACK_COMPREPLY="-h --help --monitor --monitor-save-local --monitor-tags --monitor-keep-going --monitor-host --monitor-prefix"
}
_spack_patch() {
if $list_options
then

View file

@ -19,9 +19,7 @@ RUN mkdir {{ paths.environment }} \
{{ manifest }} > {{ paths.environment }}/spack.yaml
# Install the software, remove unnecessary deps
RUN {% if monitor.enabled %}--mount=type=secret,id=su --mount=type=secret,id=st {% endif %}cd {{ paths.environment }} && \
spack env activate . {% if monitor.enabled %}{% if not monitor.disable_auth %}&& export SPACKMON_USER=$(cat /run/secrets/su) && export SPACKMON_TOKEN=$(cat /run/secrets/st) {% endif %}{% endif %}&& \
spack install {% if monitor.enabled %}--monitor {% if monitor.prefix %}--monitor-prefix {{ monitor.prefix }} {% endif %}{% if monitor.tags %}--monitor-tags {{ monitor.tags }} {% endif %}{% if monitor.keep_going %}--monitor-keep-going {% endif %}{% if monitor.host %}--monitor-host {{ monitor.host }} {% endif %}{% if monitor.disable_auth %}--monitor-disable-auth {% endif %}{% endif %}--fail-fast && \
RUN spack install --fail-fast && \
spack gc -y
{% if strip %}

View file

@ -21,7 +21,7 @@ EOF
# Install all the required software
. /opt/spack/share/spack/setup-env.sh
spack env activate .
spack install {% if monitor.enabled %}--monitor {% if monitor.prefix %}--monitor-prefix {{ monitor.prefix }} {% endif %}{% if monitor.tags %}--monitor-tags {{ monitor.tags }} {% endif %}{% if monitor.keep_going %}--monitor-keep-going {% endif %}{% if monitor.host %}--monitor-host {{ monitor.host }} {% endif %}{% if monitor.disable_auth %}--monitor-disable-auth {% endif %}{% endif %}--fail-fast
spack install --fail-fast
spack gc -y
spack env deactivate
spack env activate --sh -d . >> {{ paths.environment }}/environment_modifications.sh