diff --git a/VERSION.txt b/VERSION.txt index e69fb78..9fe9e05 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -6.8.13 \ No newline at end of file +6.8.17 \ No newline at end of file diff --git a/sandbox/tease.py b/sandbox/tease.py index d106be4..ea53983 100755 --- a/sandbox/tease.py +++ b/sandbox/tease.py @@ -5,8 +5,10 @@ import sys sys.path.insert(0, "../") -from scripttease.cli import main_command +from scripttease.cli import execute if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main_command()) + sys.exit(execute()) + # old: + # sys.exit(main_command()) diff --git a/scripttease/cli/__init__.py b/scripttease/cli/__init__.py index 8a596cb..fdc9f83 100644 --- a/scripttease/cli/__init__.py +++ b/scripttease/cli/__init__.py @@ -1,12 +1,22 @@ # Imports from argparse import ArgumentParser, RawDescriptionHelpFormatter + from commonkit.logging import LoggingHelper -from ..variables import LOGGER_NAME +from ..variables import LOGGER_NAME, PATH_TO_SCRIPT_TEASE from ..version import DATE as VERSION_DATE, VERSION from . import initialize from . import subcommands +# New: +from commonkit import highlight_code, indent, smart_cast, write_file +from commonkit.shell import EXIT +from markdown import markdown +import os +from scripttease.lib.contexts import Context +from scripttease.lib.factories import command_factory +from scripttease.lib.loaders import load_variables, INILoader, YMLLoader + DEBUG = 10 logging = LoggingHelper(colorize=True, name=LOGGER_NAME) @@ -15,6 +25,350 @@ log = logging.setup() # Commands +def execute(): + """Process script configurations.""" + + __author__ = "Shawn Davis " + __date__ = VERSION_DATE + __help__ = """NOTES + + This command is used to parse configuration files and output the commands. + + """ + __version__ = VERSION + + # Main argument parser from which sub-commands are created. + parser = ArgumentParser(description=__doc__, epilog=__help__, formatter_class=RawDescriptionHelpFormatter) + + parser.add_argument( + "path", + default="steps.ini", + nargs="?", + help="The path to the configuration file." + ) + + parser.add_argument( + "-c", + "--color", + action="store_true", + dest="color_enabled", + help="Enable code highlighting for terminal output." + ) + + parser.add_argument( + "-C=", + "--context=", + action="append", + dest="variables", + help="Context variables for use in pre-parsing the config and templates. In the form of: name:value" + ) + + parser.add_argument( + "-d=", + "--docs=", + choices=["html", "markdown", "plain", "rst"], + # default="markdown", + dest="docs", + help="Output documentation instead of code." + ) + + parser.add_argument( + "-D", + "--debug", + action="store_true", + dest="debug_enabled", + help="Enable debug output." + ) + + parser.add_argument( + "-f=", + "--filter=", + action="append", + dest="filters", + help="Filter the commands in the form of: attribute:value" + ) + + parser.add_argument( + "-i=", + "--inventory=", + dest="inventory", + help="Copy an inventory item to a local directory." + ) + + parser.add_argument( + "-o=", + "--option=", + action="append", + dest="options", + help="Common command options in the form of: name:value" + ) + + parser.add_argument( + "-P=", + "--profile=", + choices=["centos", "ubuntu"], + default="ubuntu", + dest="profile", + help="The OS profile to use." + ) + + parser.add_argument( + "-T=", + "--template-path=", + action="append", + dest="template_locations", + help="The location of template files that may be used with the template command." + ) + + parser.add_argument( + "-w=", + "--write=", + dest="output_file", + help="Write the output to disk." + ) + + parser.add_argument( + "-V=", + "--variables-file=", + dest="variables_file", + help="Load variables from a file." + ) + + # Access to the version number requires special consideration, especially + # when using sub parsers. The Python 3.3 behavior is different. See this + # answer: http://stackoverflow.com/questions/8521612/argparse-optional-subparser-for-version + parser.add_argument( + "-v", + action="version", + help="Show version number and exit.", + version=__version__ + ) + + parser.add_argument( + "--version", + action="version", + help="Show verbose version information and exit.", + version="%(prog)s" + " %s %s by %s" % (__version__, __date__, __author__) + ) + + # Parse arguments. + args = parser.parse_args() + + if args.debug_enabled: + log.setLevel(DEBUG) + + log.debug("Namespace: %s" % args) + + # Create the global context. + context = Context() + + if args.variables_file: + variables = load_variables(args.variables_file) + for v in variables: + context.variables[v.name] = v + + if args.variables: + for token in args.variables: + try: + key, value = token.split(":") + context.add(key, smart_cast(value)) + except ValueError: + context.add(token, True) + + # Capture filters. + if args.filters: + filters = dict() + for token in args.filters: + key, value = token.split(":") + if key not in filters: + filters[key] = list() + + filters[key].append(value) + + # Handle global command options. + options = dict() + if args.options: + for token in args.options: + try: + key, value = token.split(":") + options[key] = smart_cast(value) + except ValueError: + options[token] = True + + # The path may have been given as a file name (steps.ini), path, or an inventory name. + input_locations = [ + args.path, + os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", args.path, "steps.ini"), + os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", args.path, "steps.yml"), + ] + path = None + for location in input_locations: + if os.path.exists(location): + path = location + break + + if path is None: + log.warning("Path does not exist: %s" % args.path) + exit(EXIT.INPUT) + + # Load the commands. + if path.endswith(".ini"): + loader = INILoader( + path, + context=context, + locations=args.template_locations, + profile=args.profile, + **options + ) + elif path.endswith(".yml"): + loader = YMLLoader( + path, + context=context, + locations=args.template_locations, + profile=args.profile, + **options + ) + else: + log.error("Unsupported file format: %s" % path) + exit(EXIT.ERROR) + + # noinspection PyUnboundLocalVariable + if not loader.load(): + log.error("Failed to load the input file: %s" % path) + exit(EXIT.ERROR) + + # Generate output. + if args.docs: + output = list() + for command in loader.commands: + + # Will this every happen? + # if command is None: + # continue + + if command.name == "explain": + if command.header: + if args.docs == "plain": + output.append("***** %s *****" % command.name.title()) + elif args.docs == "rst": + output.append(command.name.title()) + output.append("=" * len(command.name)) + else: + output.append("## %s" % command.name.title()) + + output.append("") + + output.append(command.content) + output.append("") + elif command.name == "screenshot": + if args.docs == "html": + b = list() + b.append('") + output.append("") + elif args.docs == "plain": + output.append(command.args[0]) + output.append("") + elif args.docs == "rst": + output.append(".. figure:: %s" % command.args[0]) + + if command.caption: + output.append(indent(":alt: %s" % command.caption or command.comment)) + + if command.height: + output.append(indent(":height: %s" % command.height)) + + if command.width: + output.append(indent(":width: %s" % command.width)) + + output.append("") + else: + output.append("![%s](%s)" % (command.caption or command.comment, command.args[0])) + output.append("") + elif command.name == "template": + if args.docs == "plain": + output.append("+++") + output.append(command.get_content()) + output.append("+++") + elif args.docs == "rst": + output.append(".. code-block:: %s" % command.get_target_language()) + output.append("") + output.append(indent(command.get_content())) + output.append("") + else: + output.append("```%s" % command.get_target_language()) + output.append(command.get_content()) + output.append("```") + output.append("") + else: + statement = command.get_statement(include_comment=False, include_register=False, include_stop=False) + if statement is not None: + line = command.comment.replace("#", "") + output.append("%s:" % line.capitalize()) + output.append("") + if args.docs == "plain": + output.append("---") + output.append(statement) + output.append("---") + output.append("") + elif args.docs == "rst": + output.append(".. code-block:: bash") + output.append("") + output.append(indent(statement)) + output.append("") + else: + output.append("```bash") + output.append(statement) + output.append("```") + output.append("") + + if args.docs == "html": + _output = markdown("\n".join(output), extensions=['fenced_code']) + else: + _output = "\n".join(output) + + print(_output) + + if args.output_file: + write_file(args.output_file, _output) + else: + commands = command_factory(loader) + output = list() + for command in commands: + # print("COMMAND", command) + # Explanations and screenshots don't produce usable statements but may be added as comments. + if command.name in ("explain", "screenshot"): + # commands.append("# %s" % command.content) + # commands.append("") + continue + + statement = command.get_statement(include_comment=False) + if statement is not None: + output.append(statement) + output.append("") + + if args.color_enabled: + print(highlight_code("\n".join(output), language="bash")) + else: + print("\n".join(output)) + + if args.output_file: + write_file(args.output_file, "\n".join(output)) + + exit(EXIT.OK) + + def main_command(): """Process script configurations.""" diff --git a/scripttease/lib/commands/base.py b/scripttease/lib/commands/base.py index a9816d4..3e83f17 100644 --- a/scripttease/lib/commands/base.py +++ b/scripttease/lib/commands/base.py @@ -510,8 +510,11 @@ class Template(object): def __getattr__(self, item): return self.kwargs.get(item) - def __str__(self): - return "template" + # def __str__(self): + # return "template" + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.source) def get_content(self): """Parse the template. diff --git a/scripttease/lib/commands/django.py b/scripttease/lib/commands/django.py index 4d240ce..80106a3 100644 --- a/scripttease/lib/commands/django.py +++ b/scripttease/lib/commands/django.py @@ -26,6 +26,10 @@ def django(management_command, *args, excluded_kwargs=None, **kwargs): # The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default. excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS + venv = kwargs.pop("venv", None) + if venv is not None: + kwargs['prefix'] = "source %s/bin/activate" % venv + # Django's management commands can have a number of options. We need to filter out internal parameters so that these # are not used as options for the management command. _kwargs = dict() diff --git a/scripttease/lib/commands/pgsql.py b/scripttease/lib/commands/pgsql.py index e5c41a5..1b68411 100644 --- a/scripttease/lib/commands/pgsql.py +++ b/scripttease/lib/commands/pgsql.py @@ -25,8 +25,6 @@ def pgsql(command, *args, host="localhost", excluded_kwargs=None, password=None, # kwargs['comment'] = "run %s postgres command" % command # Allow additional command line switches to pass through? - # Django's management commands can have a number of options. We need to filter out internal parameters so that these - # are not used as options for the management command. _kwargs = dict() for key in excluded_kwargs: if key in kwargs: @@ -69,6 +67,22 @@ def pgsql_create(database, owner=None, template=None, **kwargs): if template is not None: kwargs['template'] = template + # psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = ''" | grep -q 1 | psql -U postgres -c "CREATE DATABASE " + # first = pgsql("psql", **kwargs) + # + # first_query = "SELECT 1 FROM pg_database WHERE datname = '%s'" % database + # first_statement = '%s -tc "%s" | grep -q 1' % (first.statement, first_query) + # + # kwargs_without_password = kwargs.copy() + # if 'password' in kwargs_without_password: + # kwargs_without_password.pop("password") + # + # second = pgsql("psql", **kwargs_without_password) + # second_statement = '%s -c "CREATE DATABASE %s"' % (second.statement, database) + # + # final_statement = "%s | %s" % (first_statement, second_statement) + # return Command(final_statement, **kwargs) + return pgsql("createdb", database, **kwargs) diff --git a/scripttease/lib/commands/posix.py b/scripttease/lib/commands/posix.py index d801f66..2028991 100644 --- a/scripttease/lib/commands/posix.py +++ b/scripttease/lib/commands/posix.py @@ -120,19 +120,23 @@ def directory(path, group=None, mode=None, owner=None, recursive=True, **kwargs) if recursive: statement.append("-p") + statement.append(path) + if group: if recursive: statement.append("&& chgrp -R %s" % group) else: statement.append("&& chgrp %s" % group) + statement.append(path) + if owner: if recursive: statement.append("&& chown -R %s" % owner) else: statement.append("&& chown %s" % owner) - statement.append(path) + statement.append(path) return Command(" ".join(statement), **kwargs) @@ -381,6 +385,13 @@ def rsync(source, target, delete=False, exclude=None, host=None, key_file=None, # rsync -e "ssh -i $(SSH_KEY) -p $(SSH_PORT)" -P -rvzc --delete # $(OUTPUTH_PATH) $(SSH_USER)@$(SSH_HOST):$(UPLOAD_PATH) --cvs-exclude; + # ansible: + # /usr/bin/rsync --delay-updates -F --compress --delete-after --copy-links --archive --rsh='/usr/bin/ssh -S none - + # i /home/shawn/.ssh/sharedservices_group -o Port=4894 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + # --rsync-path='sudo -u root rsync' + # --exclude-from=/home/shawn/Work/app_sharedservices_group/deploy/roles/project/rsync.txt + # --out-format='<>%i %n%L' + tokens = list() tokens.append("rsync") tokens.append("--cvs-exclude") @@ -576,7 +587,7 @@ POSIX_MAPPINGS = { 'remove': remove, 'replace': replace, 'run': run, - 'rysnc': rsync, + 'rsync': rsync, 'scopy': scopy, 'sync': sync, 'touch': touch, diff --git a/scripttease/lib/commands/python.py b/scripttease/lib/commands/python.py index 5262e9e..e30fae4 100644 --- a/scripttease/lib/commands/python.py +++ b/scripttease/lib/commands/python.py @@ -28,6 +28,32 @@ def python_pip(name, op="install", upgrade=False, venv=None, version=3, **kwargs return Command(statement, **kwargs) +def python_pip_file(path, venv=None, version=3, **kwargs): + """Install Python packages from a pip file. + + :param path: The path to the file. + :type path: str + + :param venv: The name (and/or path) of the virtual environment. + :type venv: str + + :param version: The pip version to use. + + """ + manager = "pip" + if version == 3: + manager = "pip3" + + if venv is not None: + kwargs['prefix'] = "source %s/bin/activate" % venv + + kwargs.setdefault("comment", "install packages from pip file %s" % path) + + statement = "%s -r %s" % (manager, path) + + return Command(statement, **kwargs) + + def python_virtualenv(name, **kwargs): """Create a Python virtual environment. @@ -41,5 +67,7 @@ def python_virtualenv(name, **kwargs): PYTHON_MAPPINGS = { 'pip': python_pip, + 'pipf': python_pip_file, + 'pip_file': python_pip_file, 'virtualenv': python_virtualenv, } diff --git a/scripttease/lib/commands/ubuntu.py b/scripttease/lib/commands/ubuntu.py index 9a79c98..b8ba99b 100644 --- a/scripttease/lib/commands/ubuntu.py +++ b/scripttease/lib/commands/ubuntu.py @@ -276,10 +276,10 @@ def user(name, groups=None, home=None, op="add", password=None, **kwargs): for c in commands: a.append(c.get_statement(include_comment=True)) - return Command("\n".join(a), **kwargs) + return Command("\n".join(a), name="user_add", **kwargs) elif op == "remove": kwargs.setdefault("comment", "remove a user named %s" % name) - return Command("deluser %s" % name, **kwargs) + return Command("deluser %s" % name, name="user_remove", **kwargs) else: raise NameError("Unsupported or unrecognized operation: %s" % op) diff --git a/scripttease/lib/factories.py b/scripttease/lib/factories.py index 3d87c64..66db04c 100644 --- a/scripttease/lib/factories.py +++ b/scripttease/lib/factories.py @@ -58,6 +58,10 @@ def command_factory(loader, excluded_kwargs=None, mappings=None, profile=PROFILE command = get_command(_mappings, command_name, *args, locations=loader.locations, **kwargs) if command is not None: command.number = number + + if command.name == "template": + command.context = loader.get_context() + commands.append(command) number += 1 diff --git a/scripttease/lib/loaders/base.py b/scripttease/lib/loaders/base.py index 6711e05..63f639a 100644 --- a/scripttease/lib/loaders/base.py +++ b/scripttease/lib/loaders/base.py @@ -8,7 +8,6 @@ import logging import os from ...constants import EXCLUDED_KWARGS from ..contexts import Variable -from ..snippets.mappings import MAPPINGS log = logging.getLogger(__name__) @@ -149,6 +148,8 @@ class BaseLoader(File): self.context = context self.is_loaded = False self.locations = locations or list() + self.profile = kwargs.pop("profile", "ubuntu") + self.options = kwargs super().__init__(path) diff --git a/scripttease/version.py b/scripttease/version.py index fef3ff8..feafa52 100644 --- a/scripttease/version.py +++ b/scripttease/version.py @@ -1,5 +1,5 @@ -DATE = "2021-01-26" -VERSION = "6.8.2" +DATE = "2022-06-14" +VERSION = "6.8.15" MAJOR = 6 MINOR = 8 -PATCH = 3 +PATCH = 15 diff --git a/setup.py b/setup.py index 91d5774..89d4032 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( test_suite='runtests.runtests', entry_points={ 'console_scripts': [ - 'tease = script_tease.cli:main_command', + 'tease = scripttease.cli:main_command', ], }, )