From 6562f2f3dfc0ca6f521d8c83a8bbd8c1e1480c36 Mon Sep 17 00:00:00 2001 From: Shawn Davis Date: Mon, 17 Jan 2022 16:57:15 -0600 Subject: [PATCH] Tweaks to loaders and snippets. --- scripttease/library/snippets/centos.py | 2 + scripttease/library/snippets/django.py | 30 +-- scripttease/library/snippets/mappings.py | 8 +- scripttease/library/snippets/ubuntu.py | 10 +- scripttease/loaders/base.py | 229 +++++++++++------------ scripttease/loaders/ini.py | 3 + 6 files changed, 145 insertions(+), 137 deletions(-) diff --git a/scripttease/library/snippets/centos.py b/scripttease/library/snippets/centos.py index 1c74c73..d08f896 100644 --- a/scripttease/library/snippets/centos.py +++ b/scripttease/library/snippets/centos.py @@ -9,6 +9,7 @@ centos = { 'install': "yum install -y {{ args[0] }}", 'reload': "systemctl reload {{ args[0] }}", 'restart': "systemctl restart {{ args[0] }}", + 'run': "{{ args[0] }}", 'start': "systemctl start {{ args[0] }}", 'stop': "systemctl stop {{ args[0] }}", 'system': { @@ -17,6 +18,7 @@ centos = { 'upgrade': "yum update -y", }, 'uninstall': "yum remove -y {{ args[0] }}", + 'upgrade': "yum upgrade -y {{ args[0] }}", 'user': { 'create': [ "adduser {{ args[0] }}", diff --git a/scripttease/library/snippets/django.py b/scripttease/library/snippets/django.py index 54eeae6..21bf612 100644 --- a/scripttease/library/snippets/django.py +++ b/scripttease/library/snippets/django.py @@ -1,20 +1,26 @@ from commonkit import parse_jinja_string - -def django_command_parser(snippet, args=None): +DJANGO_EXCLUDED_KWARGS = [ + "cd", + "comment", + "environments", + "prefix", + "register", + "shell", + "stop", + "tags", + # "venv", # ? +] + +def django_command_parser(snippet, args=None, excluded_kwargs=None): + + _excluded_kwargs = excluded_kwargs or DJANGO_EXCLUDED_KWARGS # We need to remove the common options so any remaining keyword arguments are converted to switches for the # management command. _kwargs = snippet.kwargs.copy() - _kwargs.pop("comment") - _kwargs.pop("environments", None) - _kwargs.pop("prefix", None) - _kwargs.pop("cd", None) - _kwargs.pop("register", None) - _kwargs.pop("shell", None) - _kwargs.pop("stop", None) - _kwargs.pop("tags", None) - _kwargs.pop("venv", None) + for name in _excluded_kwargs: + _kwargs.pop(name, None) # We need to remove some parameters for dumpdata and loaddata. Otherwise they end up as switches. if snippet.name in ("django.dumpdata", "django.loaddata"): @@ -94,7 +100,7 @@ django = { 'command': "./manage.py {{ command_name }} {% if args %}{{ ' '.join(args) }}{% endif %} {{ switches }}", 'dumpdata': [ "./manage.py dumpdata {{ app }}{% if model %}.{{ model }}{% endif %}", - "--indent=4", + # "--indent=4", "{{ switches }}", '> {{ path }}', ], diff --git a/scripttease/library/snippets/mappings.py b/scripttease/library/snippets/mappings.py index 544ec9a..9d0f922 100644 --- a/scripttease/library/snippets/mappings.py +++ b/scripttease/library/snippets/mappings.py @@ -11,6 +11,12 @@ from .ubuntu import ubuntu # Exports +__all__ = ( + "MAPPINGS", + "merge", + "merge_dictionaries", +) + # Functions @@ -50,6 +56,6 @@ def merge_dictionaries(first: dict, second: dict) -> dict: MAPPINGS = { - 'centos': merge(centos, django, messages, mysql, pgsql, posix, py), + 'centos': merge(centos, django, messages, mysql, pgsql, posix, python), 'ubuntu': merge(ubuntu, django, messages, mysql, pgsql, posix, python), } diff --git a/scripttease/library/snippets/ubuntu.py b/scripttease/library/snippets/ubuntu.py index 969ac8f..7338fe1 100644 --- a/scripttease/library/snippets/ubuntu.py +++ b/scripttease/library/snippets/ubuntu.py @@ -13,13 +13,11 @@ ubuntu = { 'test': "apachectl configtest", }, 'install': "apt-get install -y {{ args[0] }}", + 'reload': "service {{ args[0] }} reload", + 'restart': "service {{ args[0] }} restart", 'run': "{{ args[0] }}", - 'service': { - 'reload': "service {{ args[0] }} reload", - 'restart': "service {{ args[0] }} restart", - 'start': "service {{ args[0] }} start", - 'stop': "service {{ args[0] }} stop", - }, + 'start': "service {{ args[0] }} start", + 'stop': "service {{ args[0] }} stop", 'system': { 'reboot': "reboot", 'update': "apt-get update -y", diff --git a/scripttease/loaders/base.py b/scripttease/loaders/base.py index 105a7df..667489e 100644 --- a/scripttease/loaders/base.py +++ b/scripttease/loaders/base.py @@ -20,6 +20,28 @@ __all__ = ( class BaseLoader(File): def __init__(self, path, context=None, locations=None, mappings=None, profile="ubuntu", **kwargs): + """Initialize the loader. + + :param path: The path to the command file. + :type path: str + + :param context: Global context that may be used when to parse the command file, snippets, and templates. + :type context: dict + + :param locations: A list of paths where templates and other external files may be found. The directory in which + the command file exists is added automatically. + :type locations: list[str] + + :param mappings: A mapping of canonical command names and their snippets, organized by ``profile``. + :type mappings: dict + + :param profile: The profile (operating system or platform) to be used. + :type profile: str + + kwargs are stored as ``options`` and may include any of the common options for command configuration. These may + be supplied as defaults for snippet processing. + + """ self.context = context or dict() self.is_loaded = False self.locations = locations or list() @@ -28,10 +50,11 @@ class BaseLoader(File): self.profile = profile self.snippets = list() + super().__init__(path) + # Always include the path to the current file in locations. self.locations.insert(0, self.directory) - - super().__init__(path) + # command.locations.append(os.path.join(self.directory, "templates")) def get_snippets(self): a = list() @@ -131,37 +154,6 @@ class BaseLoader(File): return read_file(self.path) - # def _get_command(self, name, *args, **kwargs): - # args = list(args) - # - # if name not in self.mappings: - # return None - # - # if type(self.mappings[name]) is dict: - # sub = args.pop(0) - # subs = self.mappings[name] - # if sub not in subs: - # return None - # - # _command = subs[sub] - # else: - # _command = self.mappings[name] - # - # context = self.context.copy() - # context['args'] = args - # context.update(kwargs) - # - # if type(_command) in (list, tuple): - # # print(" ".join(_command)) - # a = list() - # for i in _command: - # i = parse_jinja_string(i, context) - # a.append(i) - # - # return " ".join(a) - # - # return parse_jinja_string(_command, context) - # noinspection PyMethodMayBeStatic def _get_key_value(self, key, value): """Process a key/value pair from an INI section. @@ -204,6 +196,13 @@ class BaseLoader(File): class Snippet(object): + """A snippet is a pseudo-command which collects the content of the snippet as well as the parameters that may be + used to create an executable statement. + + The purpose of a snippet is *not* to provide command execution, but to capture the results of a command requested in + a command configuration file. + + """ def __init__(self, name, args=None, content=None, context=None, kwargs=None, parser=None): """Initialize a snippet. @@ -252,11 +251,37 @@ class Snippet(object): return str(self.name) def get_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): + """Get the command statement represented by the snippet. + + :param cd: Indicates whether the change directory option should be included in the output. The ``cd`` option + must also be provided for the command in the configuration file. + :type cd: bool + + :param include_comment: Indicates whether the command comment should be included in the output. + :type include_comment: bool + + :param include_register: Indicates whether an additional statement to capture the result of the command should + be included in the output. The register option must also be defined for the command in + the configuration file. + :type include_register: bool + + :param include_stop: Indicates whether an additional statement to exit on failure of the command should be + included in the output. The stop option must also be defined for the command in the + configuration file. + :type include_stop: bool + + :rtype: str + + .. note:: + The boolean options allow implementers to exercise control over the output of the statement, so that the + snippet may be used in ways appropriate to the implementation. + + """ lines = list() if self.comment and include_comment: lines.append("# %s" % self.comment) - # Handle command itemization. Note that register and stop options are ignored. + # Handle snippet itemization. Note that register and stop options are ignored. if self.is_itemized: for item in self.items: args = list() @@ -287,7 +312,7 @@ class Snippet(object): return "\n".join(lines) - # Handle normal (not itemized) comands. + # Handle normal (not itemized) snippets. a = list() if cd and self.cd is not None: a.append("( cd %s &&" % self.cd) @@ -327,53 +352,26 @@ class Snippet(object): @property def is_itemized(self): + """Indicates whether the snippet includes multiple occurrences of the same command. + + :rtype: bool + + """ s = " ".join(self.args) return "$item" in s @property def is_valid(self): - return self.content is not None + """Indicates whether the snippet is valid. + + :rtype: bool - # def _get_statement(self): - # if self.is_itemized: - # a = list() - # for item in self.items: - # args = list() - # for arg in self.args: - # args.append(arg.replace("$item", item)) - # - # context = self.context.copy() - # context['args'] = args - # context.update(self.kwargs) - # - # if type(self.content) is list: - # b = list() - # for i in self.content: - # i = parse_jinja_string(i, context) - # b.append(i) - # - # a.append(" ".join(b)) - # else: - # a.append(parse_jinja_string(self.content, context)) - # - # return "\n".join(a) - # - # context = self.context.copy() - # context['args'] = self.args - # context.update(self.kwargs) - # - # a = list() - # if type(self.content) is list: - # b = list() - # for i in self.content: - # i = parse_jinja_string(i, context) - # b.append(i) - # - # a.append(" ".join(b)) - # else: - # a.append(parse_jinja_string(self.content, context)) - # - # return " ".join(a) + .. note:: + This is done by determining if snippet content is not ``None``. The content is found during the loading + process when the Snippet instance is created. + + """ + return self.content is not None def _parse(self, args=None, kwargs=None): """Build the command statement from snippet content. @@ -477,13 +475,43 @@ class Template(object): log.error("Could not parse %s template: %s" % (template, e)) return None + # noinspection PyUnusedLocal def get_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): - if self.parser == self.PARSER_SIMPLE: - return self._get_simple_statement(cd=cd, include_comment=include_comment, include_register=include_register, - include_stop=include_stop) + lines = list() + if include_comment and self.comment is not None: + lines.append("# %s" % self.comment) + + # TODO: Backing up a template's target is currently specific to bash. + if self.backup_enabled: + lines.append('if [[ -f "%s" ]]; then mv %s %s.b; fi;' % (self.target, self.target, self.target)) + + # Get the content; e.g. parse the template. + content = self.get_content() + + # Templates that are bash scripts will fail to write because of the shebang. + if content.startswith("#!"): + _content = content.split("\n") + first_line = _content.pop(0) + lines.append('echo "%s" > %s' % (first_line, self.target)) + lines.append("cat >> %s << EOF" % self.target) + lines.append("\n".join(_content)) + lines.append("EOF") else: - return self._get_jinja2_statement(cd=cd, include_comment=include_comment, include_register=include_register, - include_stop=include_stop) + lines.append("cat > %s << EOF" % self.target) + lines.append(content) + lines.append("EOF") + + if include_register and self.register is not None: + lines.append("%s=$?;" % self.register) + + if include_stop and self.stop: + lines.append("if [[ $%s -gt 0 ]]; exit 1; fi;" % self.register) + elif include_stop and self.stop: + lines.append("if [[ $? -gt 0 ]]; exit 1; fi;") + else: + pass + + return "\n".join(lines) def get_template(self): """Get the template path. @@ -501,44 +529,9 @@ class Template(object): @property def is_itemized(self): - return "$item" in self.target + # return "$item" in self.target + return False @property def is_valid(self): return True - - def _get_command(self, content): - """Get the cat command.""" - output = list() - - # TODO: Template backup is not system safe, but is specific to bash. - if self.backup_enabled: - output.append('if [[ -f "%s" ]]; then mv %s %s.b; fi;' % (self.target, self.target, self.target)) - - if content.startswith("#!"): - _content = content.split("\n") - first_line = _content.pop(0) - output.append('echo "%s" > %s' % (first_line, self.target)) - output.append("cat >> %s << EOF" % self.target) - output.append("\n".join(_content)) - output.append("EOF") - else: - output.append("cat > %s << EOF" % self.target) - output.append(content) - output.append("EOF") - - statement = "\n".join(output) - - # noinspection PyUnusedLocal - def _get_jinja2_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): - """Parse a Jinja2 template.""" - content = self.get_content() - return self._get_command(content) - - # noinspection PyUnusedLocal - def _get_simple_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): - """Parse a "simple" template.""" - content = self.get_content() - - return self._get_command(content) - diff --git a/scripttease/loaders/ini.py b/scripttease/loaders/ini.py index e01bed8..f164c45 100644 --- a/scripttease/loaders/ini.py +++ b/scripttease/loaders/ini.py @@ -46,6 +46,9 @@ class INILoader(BaseLoader): kwargs['comment'] = comment for key, value in ini.items(comment): + if key.startswith("_"): + continue + # The first key/value pair is the command name and arguments. if count == 0: command_name = key