diff --git a/apps/cli/executables/null/README.md b/apps/cli/executables/null/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2840736025b0f9676f061d687d4c28451517db8c --- /dev/null +++ b/apps/cli/executables/null/README.md @@ -0,0 +1,29 @@ +This is the null executable, a baseline test of the functionality of the Workspaces system. + +It can: +- Print a message to stderr +- Print a message to stdout +- Exit with status code -1 +- Exit with random status code in [-50, 50] +- Sleep for 5 seconds +- Abort and dump the core + +``` +usage: null [-h] [-v] [-pe | -g | -ef | -er | -n | -d] + +Workspaces null executable, a status capture test of the system. Version 4.0.0a1.dev1 + +optional arguments: + -h, --help show this help message and exit + -pe, --print-error print out aggressive message to stderr + -g, --greeting print out a friendly greeting to stdout + -ef, --exit-fail print error message and exit with status code -1 + -er, --exit-random print error message and exit with random status code within [-50, 50] + -n, --nap take a short nap + -d, --dump abort program and dump core + +options: + settings for altering program behavior + + -v, --verbose allow the program the gift of speech +``` diff --git a/apps/cli/executables/null/__init__.py b/apps/cli/executables/null/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/cli/executables/null/setup.py b/apps/cli/executables/null/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..91b6b575c50c3c08f10dbc16446d91301d71d40c --- /dev/null +++ b/apps/cli/executables/null/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from pathlib import Path +from setuptools import setup + +VERSION = open('src/null/_version.py').readlines()[-1].split()[-1].strip("\"'") +README = Path('README.md').read_text() + +# requires = [ +# ] + +tests_require = [ + 'pytest>=5.4,<6.0' +] +setup( + name=Path().absolute().name, + version=VERSION, + description='Workspaces null executable.', + long_description=README, + author='NRAO SSA Team', + author_email='dms-ssa@nrao.edu', + url='TBD', + license="GPL", + # install_requires=requires, + tests_require=tests_require, + keywords=[], + packages=['null'], + package_dir={'':'src'}, + classifiers=[ + 'Programming Language :: Python :: 3.8' + ], + entry_points={ + 'console_scripts': ['null = null.null:main'] + }, +) diff --git a/apps/cli/executables/null/src/null/__init__.py b/apps/cli/executables/null/src/null/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/cli/executables/null/src/null/_version.py b/apps/cli/executables/null/src/null/_version.py new file mode 100644 index 0000000000000000000000000000000000000000..f27d146a3f39885ce269bacf9ab4510254147c8d --- /dev/null +++ b/apps/cli/executables/null/src/null/_version.py @@ -0,0 +1,2 @@ +""" Version information for this package, don't put anything else here. """ +___version___ = '4.0.0a1.dev1' diff --git a/apps/cli/executables/null/src/null/null.py b/apps/cli/executables/null/src/null/null.py new file mode 100644 index 0000000000000000000000000000000000000000..6f806ae60910206b36c8a97d3fb9eca700595529 --- /dev/null +++ b/apps/cli/executables/null/src/null/null.py @@ -0,0 +1,111 @@ +""" Module for the null executable. Performs some very basic actions + and utilizes pymygdala's LogHandler for logging. """ + +import os +import sys +import time +import random +import logging +import argparse + +from ._version import ___version___ as version + +_DESCRIPTION = """Workspaces null executable, a status capture test of the system. Version {}""" + +logger = logging.getLogger("null") +logger.setLevel(logging.INFO) +handler = logging.StreamHandler(stream=sys.stdout) + +class Null: + def __init__(self, args, verbose): + self.args = args + if verbose: + logger.setLevel(logging.DEBUG) + self.args_to_funcs = { + 'print-error': self.print_error, + 'greeting': self.print_greeting, + 'exit-fail': self.exit_with_failure, + 'exit-random': self.exit_randomly, + 'nap': self.take_nap, + 'dump': self.dump_core + } + + def print_error(self): + logger.removeHandler(handler) + err_handler = logging.StreamHandler(stream=sys.stderr) + logger.addHandler(err_handler) + logger.error("ERROR: This is an error.") + + def print_greeting(self): + logger.info("Hello, world!") + logger.debug("And goodbye, world...") + + def exit_with_failure(self): + logger.error("Error purposefully induced. Exiting with status code -1...") + sys.exit(-1) + + def exit_randomly(self): + status_code = random.randint(-50, 50) + logger.debug("Exiting with status code {}".format(status_code)) + sys.exit(status_code) + + def take_nap(self): + logger.debug("Going to sleep...") + time.sleep(5) + logger.debug("Waking up.") + + def dump_core(self): + logger.debug("Aborting and dumping core...", stack_info=True) + os.abort() + + def execute(self): + """ + Executes command specified by CL arguments. + """ + for arg, val in vars(self.args).items(): + if val and arg in self.args_to_funcs: + self.args_to_funcs[arg]() + +def make_arg_parser(): + """ + Creates an argparse arguments parser with appropriate options + :return: Said argument parser + """ + parser = argparse.ArgumentParser(description=_DESCRIPTION.format(version), + formatter_class=argparse.RawTextHelpFormatter) + options = parser.add_argument_group('options', 'settings for altering program behavior') + options.add_argument('-v', '--verbose', action='store_true', + required=False, dest='verbose', default=False, + help='allow the program the gift of speech') + + functions = parser.add_mutually_exclusive_group(required=False) + functions.add_argument('-pe', '--print-error', action='store_true', + required=False, dest='print-error', default=False, + help='print out aggressive message to stderr') + functions.add_argument('-g', '--greeting', action='store_true', + required=False, dest='greeting', default=False, + help='print out a friendly greeting to stdout') + functions.add_argument('-ef', '--exit-fail', action='store_true', + required=False, dest='exit-fail', default=False, + help='print error message and exit with status code -1') + functions.add_argument('-er', '--exit-random', action='store_true', + required=False, dest='exit-random', default=False, + help='print error message and exit with random status code within [-50, 50]') + functions.add_argument('-n', '--nap', action='store_true', + required=False, dest='nap', default=False, + help='take a short nap') + functions.add_argument('-d', '--dump', action='store_true', + required=False, dest='dump', default=False, + help='abort program and dump core') + return parser + +def main(): + arg_parser = make_arg_parser() + args = arg_parser.parse_args() + logger.addHandler(handler) + + executable = Null(args, args.verbose) + executable.execute() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/apps/cli/executables/null/test/__init__.py b/apps/cli/executables/null/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/cli/executables/null/test/test_null.py b/apps/cli/executables/null/test/test_null.py new file mode 100644 index 0000000000000000000000000000000000000000..7dd49717c246daa13db90f959016aa4ce59af8dc --- /dev/null +++ b/apps/cli/executables/null/test/test_null.py @@ -0,0 +1,36 @@ +import pytest +import argparse + +# from null.null import Null +from ..src.null.null import Null + +@pytest.fixture() +def null(): + null = Null(argparse.Namespace(), True) + return null + +def test_print_error(null, caplog): + null.print_error() + assert 'ERROR: This is an error.' in caplog.text + +def test_print_greeting(null, caplog): + null.print_greeting() + assert 'Hello, world!' in caplog.text + assert 'And goodbye, world...' in caplog.text + +def test_exit_with_failure(null, caplog): + with pytest.raises(SystemExit) as e: + null.exit_with_failure() + assert 'Error purposefully induced. Exiting with status code -1...' in caplog.text + assert e.value.code == -1 + +def test_exit_randomly(null, caplog): + with pytest.raises(SystemExit) as e: + null.exit_randomly() + assert 'Exiting with status code' in caplog.text + assert -50 <= e.value.code <= 50 + +def test_take_nap(null, caplog): + null.take_nap() + assert 'Going to sleep...' in caplog.text + assert 'Waking up.' in caplog.text \ No newline at end of file diff --git a/build/recipes/build_pkgs/build_pkgs.py b/build/recipes/build_pkgs/build_pkgs.py index ad50462f74a3f8ed847c85e21b000e999551bdde..f542b7b3b63d112b31c109be6e3f02f4552439f3 100644 --- a/build/recipes/build_pkgs/build_pkgs.py +++ b/build/recipes/build_pkgs/build_pkgs.py @@ -1,16 +1,38 @@ import subprocess -def get_pkg_list(): +def get_dirs(): """ - Run a couple shell commands to parse the metadata directory for its packages. - :return: List of packages in metadata directory + Finds all subdirectories containing setup.py files. + :return: List of directories as strings. """ - find_proc = subprocess.run(["find", "build/metadata", - "-name", "meta.yaml"], - stdout=subprocess.PIPE) - paths = find_proc.stdout.decode('utf-8') - fmt_paths = paths.replace("build/metadata/", "").replace("/meta.yaml", "") - return fmt_paths.split('\n') + find = subprocess.run([ + 'find', '.', '-name', 'setup.py', '-not', '-path', './build/recipes/*' + ], stdout=subprocess.PIPE) + dirs = find.stdout.decode('utf-8').split('\n') + dirs_cpy = dirs + + for i, d in enumerate(dirs_cpy): + dirs[i] = d.replace('/setup.py', '') + + return dirs + +def get_names(dirs): + """ + Generate list of subproject names based on the rule that the name of the + subproject directory will be the name of the subproject. + :return: List of names as strings. + """ + names = [] + for d in dirs: + if d != '': + name = d.split('/')[-1] + + if name == "archive": + # Case with ./services/archive having special dir structure + name = "services" + names.append(name) + + return names class Recipe: def __init__(self, buildout, name, options): @@ -23,7 +45,7 @@ class Recipe: """ self.name = name self.options = options - self.pkg_list = get_pkg_list() + self.pkg_list = get_names(get_dirs()) def install(self): """ diff --git a/build/recipes/setup_to_meta/setup_to_meta.py b/build/recipes/setup_to_meta/setup_to_meta.py index e26e4777b24cf0c638dc4cdba01eeff83f3d9278..e65c1ba2ae9e104a35dfb543cc9985c0224ab107 100644 --- a/build/recipes/setup_to_meta/setup_to_meta.py +++ b/build/recipes/setup_to_meta/setup_to_meta.py @@ -46,18 +46,20 @@ class MetadataGenerator: """ reqs_string = '' reqs_list = '' + reqs_string += 'requirements:\n' + build_reqs = ' build:\n' + run_reqs = ' run:\n' + host_reqs = ' host:\n' + reqs_list += ' - python={}\n'.format(PYTHON_VERSION) + if 'install_requires' in self.setup.keys(): - reqs_string += 'requirements:\n' - build_reqs = ' build:\n' - run_reqs = ' run:\n' - host_reqs = ' host:\n' - reqs_list += ' - python={}\n'.format(PYTHON_VERSION) for req in self.setup['install_requires']: reqs_list += ' - {}\n'.format(req) - reqs_string += build_reqs + reqs_list + \ - run_reqs + reqs_list + \ - host_reqs + reqs_list + \ - '\n' + + reqs_string += build_reqs + reqs_list + \ + run_reqs + reqs_list + \ + host_reqs + reqs_list + \ + '\n' return reqs_string def fmt_test(self): @@ -88,8 +90,11 @@ class MetadataGenerator: def generate(self): # Filter numpy etc. out of the requirements - self.setup['install_requires'] = [req for req in self.setup['install_requires'] if req != 'numpy'] - + try: + self.setup['install_requires'] = [req for req in self.setup['install_requires'] if req != 'numpy'] + except KeyError: + pass + name = self.setup['name'] version = self.setup['version'] entry_points = self.fmt_ep() diff --git a/build/tools/transfer_to_builder.py b/build/tools/transfer_to_builder.py index 6ae2edf2a24991e2aad023b7b65237f956d132eb..64d7e4fc7ff61c4fdc8b32d4f8c7fd627f7981d8 100644 --- a/build/tools/transfer_to_builder.py +++ b/build/tools/transfer_to_builder.py @@ -1,4 +1,9 @@ -import subprocess, paramiko, fnmatch, os +import subprocess +import paramiko +import fnmatch +import os +import getpass + from scp import SCPClient def get_build_pkg_names(): @@ -23,7 +28,11 @@ def create_ssh_client(server): client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - client.connect(server) + + username = input("Enter NRAO username: ") + password = getpass.getpass(prompt="Enter NRAO password: ") + + client.connect(server, username=username, password=password) return client def transfer_packages(pkg_names): @@ -34,9 +43,9 @@ def transfer_packages(pkg_names): if len(pkg_names): builder_addr = "builder.aoc.nrao.edu" builder_path = "/home/builder.aoc.nrao.edu/content/conda/noarch" - ssh = create_ssh_client(builder_addr) - with SCPClient(ssh.get_transport()) as scp: - [scp.put(pkg, builder_path) for pkg in pkg_names] + with create_ssh_client(builder_addr) as ssh: + with SCPClient(ssh.get_transport()) as scp: + [scp.put(pkg, builder_path) for pkg in pkg_names] cmd_cd = "cd {}".format(builder_path) cmd_index = "conda index .." cmd_chmod = "chmod -f 664 *" diff --git a/environment.yml b/environment.yml index e42e0d028dc8b175016cda74043e2cd13651f303..f64d06a7133dbcea3db8ec4b3610aaeffb882891 100644 --- a/environment.yml +++ b/environment.yml @@ -13,6 +13,7 @@ dependencies: - jxmlease=1.0 - lxml=4.5 - mysqlclient=1.4 + - paramiko - pandas=1.0 - pendulum=2.1 - pid=2.2