From b2903c149076449a2d729313219d5870da1410b6 Mon Sep 17 00:00:00 2001 From: Shawn Davis Date: Tue, 12 Apr 2022 21:47:30 -0500 Subject: [PATCH] Started yet another new scheme for managing commands. --- scripttease/exceptions.py | 6 + scripttease/lib/commands/__init__.py | 0 scripttease/lib/commands/base.py | 389 ++++++++++++++++++++ scripttease/lib/commands/centos.py | 279 ++++++++++++++ scripttease/lib/commands/django.py | 92 +++++ scripttease/lib/commands/messages.py | 51 +++ scripttease/lib/commands/mysql.py | 173 +++++++++ scripttease/lib/commands/pgsql.py | 155 ++++++++ scripttease/lib/commands/posix.py | 532 +++++++++++++++++++++++++++ scripttease/lib/commands/python.py | 39 ++ scripttease/lib/commands/ubuntu.py | 337 +++++++++++++++++ scripttease/lib/loaders/base.py | 62 ++-- scripttease/lib/loaders/ini.py | 2 +- 13 files changed, 2089 insertions(+), 28 deletions(-) create mode 100644 scripttease/exceptions.py create mode 100644 scripttease/lib/commands/__init__.py create mode 100644 scripttease/lib/commands/base.py create mode 100644 scripttease/lib/commands/centos.py create mode 100644 scripttease/lib/commands/django.py create mode 100644 scripttease/lib/commands/messages.py create mode 100644 scripttease/lib/commands/mysql.py create mode 100644 scripttease/lib/commands/pgsql.py create mode 100644 scripttease/lib/commands/posix.py create mode 100644 scripttease/lib/commands/python.py create mode 100644 scripttease/lib/commands/ubuntu.py diff --git a/scripttease/exceptions.py b/scripttease/exceptions.py new file mode 100644 index 0000000..6c7cdc7 --- /dev/null +++ b/scripttease/exceptions.py @@ -0,0 +1,6 @@ +class InvalidInput(Exception): + pass + + +class UnknownCommand(Exception): + pass diff --git a/scripttease/lib/commands/__init__.py b/scripttease/lib/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripttease/lib/commands/base.py b/scripttease/lib/commands/base.py new file mode 100644 index 0000000..ca61802 --- /dev/null +++ b/scripttease/lib/commands/base.py @@ -0,0 +1,389 @@ +# Imports + +from commonkit import parse_jinja_template, read_file +from jinja2 import TemplateNotFound, TemplateError +import logging +import os + +log = logging.getLogger(__name__) + +# Exports + +__all__ = ( + "EXCLUDED_KWARGS", + "run", + "Command", + "ItemizedCommand", + "Sudo", + "Template", +) + +# Constants + +EXCLUDED_KWARGS = [ + "cd", + "comment", + "condition", + "prefix", + "register", + "stop", + "sudo", + "tags", +] + +# Functions + + +def run(statement, **kwargs): + """Run any statement. + + - statement (str): The statement to be executed. + + """ + kwargs.setdefault("comment", "run statement") + return Command(statement, **kwargs) + +# Classes + + +class Command(object): + + def __init__(self, statement, cd=None, comment=None, condition=None, prefix=None, register=None, stop=False, sudo=None, tags=None, **kwargs): + self.cd = cd + self.comment = comment + self.condition = condition + self.prefix = prefix + self.register = register + self.statement = statement + self.stop = stop + self.tags = tags or list() + + if isinstance(sudo, Sudo): + self.sudo = sudo + elif type(sudo) is str: + self.sudo = Sudo(enabled=True, user=sudo) + elif sudo is True: + self.sudo = Sudo(enabled=True) + else: + self.sudo = Sudo() + + self.options = kwargs + + def __getattr__(self, item): + return self.options.get(item) + + def __repr__(self): + if self.comment: + return "<%s %s>" % (self.__class__.__name__, self.comment) + + return "<%s>" % self.__class__.__name__ + + def get_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): + """Get the full statement. + + :param cd: Include the directory change, if given. + :type cd: bool + + :param suppress_comment: Don't include the comment. + :type suppress_comment: bool + + :rtype: str + + """ + a = list() + + if cd and self.cd is not None: + a.append("( cd %s &&" % self.cd) + + if self.prefix is not None: + a.append("%s &&" % self.prefix) + + if self.sudo: + statement = "%s %s" % (self.sudo, self._get_statement()) + else: + statement = self._get_statement() + + a.append("%s" % statement) + + if cd and self.cd is not None: + a.append(")") + + b = list() + if self.comment is not None and include_comment: + b.append("# %s" % self.comment) + + if self.condition is not None: + b.append("if [[ %s ]]; then %s; fi;" % (self.condition, " ".join(a))) + else: + b.append(" ".join(a)) + + if self.register is not None and include_register: + b.append("%s=$?;" % self.register) + + if self.stop and include_stop: + b.append("if [[ $%s -gt 0 ]]; exit 1; fi;" % self.register) + elif self.stop and include_stop: + b.append("if [[ $? -gt 0 ]]; exit 1; fi;") + else: + pass + + return "\n".join(b) + + @property + def is_itemized(self): + """Always returns ``False``.""" + return False + + def _get_statement(self): + """By default, get the statement passed upon command initialization. + + :rtype: str + + """ + return self.statement + + +class Sudo(object): + """Helper class for defining sudo options.""" + + def __init__(self, enabled=False, user="root"): + """Initialize the helper. + + :param enabled: Indicates sudo is enabled. + :type enabled: bool + + :param user: The user to be invoked. + :type user: str + + """ + self.enabled = enabled + self.user = user + + def __bool__(self): + return self.enabled + + def __str__(self): + if self.enabled: + return "sudo -u %s" % self.user + + return "" + + +class ItemizedCommand(object): + """An itemized command represents multiple commands of with the same statement but different parameters.""" + + def __init__(self, callback, items, *args, name=None, **kwargs): + """Initialize the command. + + :param callback: The function to be used to generate the command. + + :param items: The command arguments. + :type items: list[str] + + :param name: The name of the command from the mapping. Not used and not required for programmatic use, but + automatically assigned during factory instantiation. + :type name: str + + :param args: The itemized arguments. ``$item`` should be included. + + Keyword arguments are passed to the command class upon instantiation. + + """ + self.args = args + self.callback = callback + self.items = items + self.kwargs = kwargs + self.name = name + + # Set defaults for when ItemizedCommand is referenced directly before individual commands are instantiated. For + # example, when command filtering occurs. + self.kwargs.setdefault("tags", list()) + + def __getattr__(self, item): + return self.kwargs.get(item) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.callback.__name__) + + def get_commands(self): + """Get the commands to be executed. + + :rtype: list[BaseType(Command)] + + """ + kwargs = self.kwargs.copy() + + a = list() + for item in self.items: + args = list() + for arg in self.args: + args.append(arg.replace("$item", item)) + + command = self.callback(*args, **kwargs) + a.append(command) + + return a + + def get_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): + """Override to get multiple commands.""" + kwargs = self.kwargs.copy() + comment = kwargs.pop("comment", "execute multiple commands") + + a = list() + # a.append("# %s" % comment) + + commands = self.get_commands() + for c in commands: + a.append(c.get_statement(cd=cd, include_comment=False, include_register=include_register, include_stop=include_stop)) + a.append("") + + return "\n".join(a) + + @property + def is_itemized(self): + """Always returns ``True``.""" + return True + + +class Template(object): + + PARSER_JINJA = "jinja2" + PARSER_PYTHON = "python" + PARSER_SIMPLE = "simple" + + def __init__(self, source, target, backup=True, parser=PARSER_JINJA, **kwargs): + self.backup_enabled = backup + self.context = kwargs.pop("context", dict()) + self.name = "template" + self.parser = parser + self.language = kwargs.pop("lang", None) + self.locations = kwargs.pop("locations", list()) + self.source = os.path.expanduser(source) + self.target = target + + sudo = kwargs.pop("sudo", None) + if isinstance(sudo, Sudo): + self.sudo = sudo + elif type(sudo) is str: + self.sudo = Sudo(enabled=True, user=sudo) + elif sudo is True: + self.sudo = Sudo(enabled=True) + else: + self.sudo = Sudo() + + self.kwargs = kwargs + + def __getattr__(self, item): + return self.kwargs.get(item) + + def __str__(self): + return "template" + + def get_content(self): + """Parse the template. + + :rtype: str | None + + """ + template = self.get_template() + + if self.parser == self.PARSER_SIMPLE: + content = read_file(template) + for key, value in self.context.items(): + replace = "$%s$" % key + content = content.replace(replace, str(value)) + + return content + + if self.parser == self.PARSER_PYTHON: + content = read_file(template) + return content % self.context + + try: + return parse_jinja_template(template, self.context) + except TemplateNotFound: + log.error("Template not found: %s" % template) + return None + except TemplateError as e: + 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): + 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: + command = "%s mv %s %s.b" % (self.sudo, self.target, self.target) + lines.append('if [[ -f "%s" ]]; then %s; fi;' % (self.target, command.lstrip())) + + # 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) + command = '%s echo "%s" > %s' % (self.sudo, first_line, self.target) + lines.append(command.lstrip()) + command = "%s cat > %s << EOF" % (self.sudo, self.target) + lines.append(command.lstrip()) + lines.append("\n".join(_content)) + lines.append("EOF") + else: + command = "%s cat > %s << EOF" % (self.sudo, self.target) + lines.append(command.lstrip()) + 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_target_language(self): + if self.language is not None: + return self.language + + if self.target.endswith(".conf"): + return "conf" + elif self.target.endswith(".ini"): + return "ini" + elif self.target.endswith(".php"): + return "php" + elif self.target.endswith(".py"): + return "python" + elif self.target.endswith(".sh"): + return "bash" + elif self.target.endswith(".yml"): + return "yaml" + else: + return "text" + + def get_template(self): + """Get the template path. + + :rtype: str + + """ + source = self.source + for location in self.locations: + _source = os.path.join(location, self.source) + if os.path.exists(_source): + return _source + + return source + + @property + def is_itemized(self): + # return "$item" in self.target + return False diff --git a/scripttease/lib/commands/centos.py b/scripttease/lib/commands/centos.py new file mode 100644 index 0000000..c871013 --- /dev/null +++ b/scripttease/lib/commands/centos.py @@ -0,0 +1,279 @@ +# Imports + +from commonkit import split_csv +from .base import Command, Template +from .django import DJANGO_MAPPINGS +from .mysql import MYSQL_MAPPINGS +from .pgsql import PGSQL_MAPPINGS +from .posix import POSIX_MAPPINGS + +# Exports + +__all__ = ( + "MAPPINGS", + "apache", + "apache_reload", + "apache_restart", + "apache_start", + "apache_stop", + "apache_test", + "command_exists", + "service_reload", + "service_restart", + "service_start", + "service_stop", + "system", + "system_install", + "system_reboot", + "system_update", + "system_upgrade", + "system_uninstall", + "template", + "user", +) + + +def command_exists(name): + """Indicates whether a given command exists in this overlay. + + :param name: The name of the command. + :type name: str + + :rtype: bool + + """ + return name in MAPPINGS + + +def apache(op, **kwargs): + """Execute an Apache-related command. + + - op (str): The operation to perform; reload, restart, start, stop, test. + + """ + if op == "reload": + return apache_reload(**kwargs) + elif op == "restart": + return apache_restart(**kwargs) + elif op == "start": + return apache_start(**kwargs) + elif op == "stop": + return apache_stop(**kwargs) + elif op == "test": + return apache_test(**kwargs) + else: + raise NameError("Unrecognized or unsupported apache operation: %s" % op) + + +def apache_reload(**kwargs): + kwargs.setdefault("comment", "reload apache") + kwargs.setdefault("register", "apache_reloaded") + + return Command("apachectl –k reload", **kwargs) + + +def apache_restart(**kwargs): + kwargs.setdefault("comment", "restart apache") + kwargs.setdefault("register", "apache_restarted") + + return Command("apachectl –k restart", **kwargs) + + +def apache_start(**kwargs): + kwargs.setdefault("comment", "start apache") + kwargs.setdefault("register", "apache_started") + + return Command("apachectl –k start", **kwargs) + + +def apache_stop(**kwargs): + kwargs.setdefault("comment", "stop apache") + + return Command("apachectl –k stop", **kwargs) + + +def apache_test(**kwargs): + kwargs.setdefault("comment", "check apache configuration") + kwargs.setdefault("register", "apache_checks_out") + + return Command("apachectl configtest", **kwargs) + + +def service_reload(name, **kwargs): + """Reload a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "reload %s service" % name) + kwargs.setdefault("register", "%s_reloaded" % name) + + return Command("systemctl reload %s" % name, **kwargs) + + +def service_restart(name, **kwargs): + """Restart a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "restart %s service" % name) + kwargs.setdefault("register", "%s_restarted" % name) + + return Command("ssystemctl restart %s" % name, **kwargs) + + +def service_start(name, **kwargs): + """Start a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "start %s service" % name) + kwargs.setdefault("register", "%s_started" % name) + + return Command("systemctl start %s" % name, **kwargs) + + +def service_stop(name, **kwargs): + """Stop a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "stop %s service" % name) + kwargs.setdefault("register", "%s_stopped" % name) + + return Command("systemctl stop %s" % name, **kwargs) + + +def system(op, **kwargs): + """Perform a system operation. + + - op (str): The operation to perform; reboot, update, upgrade. + + """ + if op == "reboot": + return system_reboot(**kwargs) + elif op == "update": + return system_update(**kwargs) + elif op == "upgrade": + return system_upgrade(**kwargs) + else: + raise NameError("Unrecognized or unsupported system operation: %s" % op) + + +def system_install(name, **kwargs): + """Install a system-level package. + + - name (str): The name of the package to install. + + """ + kwargs.setdefault("comment", "install system package %s" % name) + + return Command("yum install -y %s" % name, **kwargs) + + +def system_reboot(**kwargs): + kwargs.setdefault("comment", "reboot the system") + + return Command("reboot", **kwargs) + + +def system_uninstall(name, **kwargs): + """Uninstall a system-level package. + + - name (str): The name of the package to uninstall. + + """ + kwargs.setdefault("comment", "remove system package %s" % name) + + return Command("yum remove -y %s" % name, **kwargs) + + +def system_update(**kwargs): + kwargs.setdefault("comment", "update system package info") + + return Command("yum check-update", **kwargs) + + +def system_upgrade(**kwargs): + kwargs.setdefault("comment", "upgrade the system") + + return Command("yum update -y", **kwargs) + + +def template(source, target, backup=True, parser=None, **kwargs): + """Create a file from a template. + + - source (str): The path to the template file. + - target (str): The path to where the new file should be created. + - backup (bool): Indicates whether a backup should be made if the target file already exists. + - parser (str): The parser to use ``jinja`` (the default) or ``simple``. + + """ + return Template(source, target, backup=backup, parser=parser, **kwargs) + + +def user(name, groups=None, home=None, op="add", password=None, **kwargs): + """Create or remove a user. + + - name (str): The user name. + - groups (str | list): A list of groups to which the user should belong. + - home (str): The path to the user's home directory. + - op (str); The operation to perform; ``add`` or ``remove``. + - password (str): The user's password. (NOT IMPLEMENTED) + + """ + if op == "add": + kwargs.setdefault("comment", "create a user named %s" % name) + + commands = list() + + a = list() + a.append('adduser %s' % name) + if home is not None: + a.append("--home %s" % home) + + commands.append(Command(" ".join(a), **kwargs)) + + if type(groups) is str: + groups = split_csv(groups, smart=False) + + if type(groups) in [list, tuple]: + for group in groups: + commands.append(Command("gpasswd -a %s %s" % (name, group), **kwargs)) + + a = list() + for c in commands: + a.append(c.get_statement(include_comment=True)) + + return Command("\n".join(a), **kwargs) + elif op == "remove": + kwargs.setdefault("comment", "remove a user named %s" % name) + return Command("userdel -r %s" % name, **kwargs) + else: + raise NameError("Unsupported or unrecognized operation: %s" % op) + + +MAPPINGS = { + 'apache': apache, + 'install': system_install, + 'reboot': system_reboot, + 'reload': service_reload, + 'restart': service_restart, + 'start': service_start, + 'stop': service_stop, + 'system': system, + 'template': template, + 'update': system_update, + 'uninstall': system_uninstall, + 'upgrade': system_upgrade, + 'user': user, +} + +MAPPINGS.update(COMMON_MAPPINGS) +MAPPINGS.update(DJANGO_MAPPINGS) +MAPPINGS.update(MYSQL_MAPPINGS) +MAPPINGS.update(PGSQL_MAPPINGS) +MAPPINGS.update(POSIX_MAPPINGS) diff --git a/scripttease/lib/commands/django.py b/scripttease/lib/commands/django.py new file mode 100644 index 0000000..8a5be2b --- /dev/null +++ b/scripttease/lib/commands/django.py @@ -0,0 +1,92 @@ +""" +[run django checks] +django: check + +[export fixtures] +django: dump lookups.Category + +[import fixtures] +django: load lookups.Category + +[migrate the database] +django: migrate + +[collect static files] +django: static + +[create super user (ad hoc command)] +django: createsuperuser root + +""" +from .base import EXCLUDED_KWARGS, Command + + +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 + + # 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: + _kwargs[key] = kwargs.pop(key) + + if 'comment' not in _kwargs: + _kwargs['comment'] = "run %s django management command" % management_command + + a = list() + a.append("./manage.py %s" % management_command) + for key, value in kwargs.items(): + key = key.replace("_", "-") + if type(value) is bool and value is True: + a.append("--%s" % key) + elif type(value) is str: + a.append('--%s="%s"' % (key, value)) + else: + a.append('--%s=%s' % (key, value)) + + _args = list(args) + if len(_args) > 0: + a.append(" ".join(_args)) + + statement = " ".join(a) + + return Command(statement, **_kwargs) + + +def django_check(**kwargs): + kwargs.setdefault("comment", "run django checks") + kwargs.setdefault("register", "django_checks_out") + return django("check", **kwargs) + + +def django_dump(target, path=None, **kwargs): + kwargs.setdefault("comment", "dump app/model data") + kwargs.setdefault("format", "json") + kwargs.setdefault("indent", 4) + + if path is None: + path = "../deploy/fixtures/%s.%s" % (target, kwargs['format']) + + return django("dumpdata", target, "> %s" % path, **kwargs) + + +def django_load(target, path=None, **kwargs): + kwargs.setdefault("comment", "load app/model data") + input_format = kwargs.pop("format", "json") + if path is None: + path = "../deploy/fixtures/%s.%s" % (target, input_format) + + return django("loaddata", path, **kwargs) + + +def django_migrate(**kwargs): + kwargs.setdefault("comment", "apply database migrations") + return django("migrate", **kwargs) + + +def django_static(**kwargs): + kwargs.setdefault("comment", "collect static files") + kwargs.setdefault("noinput", True) + return django("collectstatic", **kwargs) diff --git a/scripttease/lib/commands/messages.py b/scripttease/lib/commands/messages.py new file mode 100644 index 0000000..2e6f18c --- /dev/null +++ b/scripttease/lib/commands/messages.py @@ -0,0 +1,51 @@ +from .base import Command +from ...exceptions import InvalidInput + + +def dialog(message, height=15, title="Message", width=100, **kwargs): + statement = list() + statement.append("dialog --clear") + statement.append('--backtitle "%s"' % title) + statement.append('--msgbox "%s" %s %s;' % (message, height, width)) + statement.append("clear;") + + return Command(" ".join(statement), **kwargs) + + +def echo(message, **kwargs): + return Command('echo "%s"' % message, **kwargs) + + +def explain(message, heading=None, **kwargs): + kwargs['heading'] = heading + return Command(message, **kwargs) + + +def screenshot(image, caption=None, **kwargs): + kwargs['caption'] = caption + + return Command(image, **kwargs) + + +def slack(message, url=None, **kwargs): + if url is None: + raise InvalidInput("Slack command requires a url parameter.") + + statement = list() + statement.append("curl -X POST -H 'Content-type: application/json' --data") + statement.append('{"text": "%s"}' % message) + statement.append(url) + + return Command(statement, **kwargs) + + +def twist(message, title="Notice", url=None, **kwargs): + if url is None: + raise InvalidInput("Twist command requires a url parameter.") + + statement = list() + statement.append("curl -X POST -H 'Content-type: application/json' --data") + statement.append('{"content": "%s", "title": "%s"' % (message, title)) + statement.append(url) + + return Command(" ".join(statement), **kwargs) diff --git a/scripttease/lib/commands/mysql.py b/scripttease/lib/commands/mysql.py new file mode 100644 index 0000000..e59f944 --- /dev/null +++ b/scripttease/lib/commands/mysql.py @@ -0,0 +1,173 @@ +from ...exceptions import InvalidInput +from .base import EXCLUDED_KWARGS, Command + + +__all__ = ( + "mysql_create", + "mysql_dump", + "mysql_exists", + "mysql_load", + "mysql_user", +) + + +def mysql(command, *args, host="localhost", excluded_kwargs=None, password=None, port=3306, user="root", **kwargs): + # The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default. + excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS + + # if 'comment' not in kwargs: + # kwargs['comment'] = "run %s mysql 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: + _kwargs[key] = kwargs.pop(key) + + # MySQL commands always run without sudo because the --user may be provided. + _kwargs['sudo'] = False + + a = list() + + a.append(command) + a.append("--user %s --host=%s --port=%s" % (user, host, port)) + + if password: + a.append('--password="%s"' % password) + + for key, value in kwargs.items(): + key = key.replace("_", "-") + if type(value) is bool and value is True: + a.append("--%s" % key) + elif type(value) is str: + a.append('--%s="%s"' % (key, value)) + else: + a.append('--%s=%s' % (key, value)) + + _args = list(args) + if len(_args) > 0: + a.append(" ".join(_args)) + + statement = " ".join(a) + + return Command(statement, **_kwargs) + + +def mysql_create(database, owner=None, **kwargs): + kwargs.setdefault("comment", "create mysql database") + + command = mysql("mysqladmin create", database, **kwargs) + + if owner is not None: + grant = mysql_grant(owner, database=database, **kwargs) + command.statement += " && " + grant.statement + + return command + + +def mysql_drop(database, **kwargs): + kwargs.setdefault("comment", "drop %s mysql database" % database) + + return mysql("mysqladmin drop", database, **kwargs) + + +def mysql_dump(database, path=None, **kwargs): + kwargs.setdefault("comment", "dump mysql database") + kwargs.setdefault("complete_inserts", True) + + if path is None: + path = "%s.sql" % database + + return mysql("mysqldump", database, "> %s" % path, **kwargs) + + +def mysql_exists(database, **kwargs): + kwargs.setdefault("comment", "determine if %s mysql database exists" % database) + kwargs.setdefault("register", "%s_exists" % database) + + command = mysql("mysql", **kwargs) + + sql = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '%s'" % database + + command.statement += '--execute="%s"' % sql + + return command + + +def mysql_grant(to, database=None, privileges="ALL", **kwargs): + """Grant privileges to a user. + + - to (str): The user name to which privileges are granted. + - database (str): The database name. + - host (str): The database host name or IP address. + - password (str): The password for the user with sufficient access privileges to execute the command. + - port (int): The TCP port number of the MySQL service running on the host. + - privileges (str): The privileges to be granted. + - user (str): The name of the user with sufficient access privileges to execute the command. + + """ + kwargs.setdefault("comment", "grant mysql privileges to %s" % to) + + host = kwargs.get("host", "localhost") + + command = mysql("mysql", **kwargs) + + # See https://dev.mysql.com/doc/refman/5.7/en/grant.html + _database = database or "*" + sql = "GRANT %(privileges)s ON %(database)s.* TO '%(user)s'@'%(host)s'" % { + 'database': _database, + 'host': host, + 'privileges': privileges, + 'user': to, + } + command.statement += ' --execute="%s"' % sql + + return command + + +def mysql_load(database, path, **kwargs): + kwargs.setdefault("comment", "load data into a mysql database") + + return mysql("psql", database, "< %s" % path, **kwargs) + + +def mysql_user(name, admin_pass=None, admin_user="root", op="create", password=None, **kwargs): + host = kwargs.get("host", "localhost") + + if op == "create": + kwargs.setdefault("comment", "create %s mysql user" % name) + + command = mysql("mysql", password=admin_pass, user=admin_user, **kwargs) + + sql = "CREATE USER IF NOT EXISTS '%s'@'%s'" % (name, host) + if password is not None: + sql += " IDENTIFIED BY PASSWORD('%s')" % password + + command.statement += ' --execute="%s"' % sql + + return command + elif op == "drop": + kwargs.setdefault("comment", "remove %s mysql user" % name) + + command = mysql("mysql", password=admin_pass, user=admin_user, **kwargs) + + sql = "DROP USER IF EXISTS '%s'@'%s'" % (name, host) + + command.statement += ' --execute="%s"' % sql + + return command + elif op == "exists": + kwargs.setdefault("comment", "determine if %s mysql user exists" % name) + kwargs.setdefault("register", "mysql_use_exists") + + command = mysql("mysql", password=admin_pass, user=admin_user, **kwargs) + + sql = "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '%s')" % name + + command.statement += ' --execute "%s"' % sql + + return command + else: + raise InvalidInput("Unrecognized or unsupported MySQL user operation: %s" % op) diff --git a/scripttease/lib/commands/pgsql.py b/scripttease/lib/commands/pgsql.py new file mode 100644 index 0000000..e863050 --- /dev/null +++ b/scripttease/lib/commands/pgsql.py @@ -0,0 +1,155 @@ +""" +[run django checks] +django: check + +[export fixtures] +django: dump lookups.Category + +[import fixtures] +django: load lookups.Category + +[migrate the database] +django: migrate + +[collect static files] +django: static + +[create super user (ad hoc command)] +django: createsuperuser root + +""" +from ...exceptions import InvalidInput +from .base import EXCLUDED_KWARGS, Command + + +__all__ = ( + "pgsql_create", + "pgsql_drop", + "pgsql_dump", + "pgsql_exists", + "pgsql_load", + "pgsql_user", +) + +def pgsql(command, *args, host="localhost", excluded_kwargs=None, password=None, port=5432, user="postgres", **kwargs): + # The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default. + excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS + + # if 'comment' not in kwargs: + # 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: + _kwargs[key] = kwargs.pop(key) + + # Postgres commands always run without sudo because the -U may be provided. + _kwargs['sudo'] = False + + a = list() + + if password is not None: + a.append('export PGPASSWORD="%s" &&' % password) + + a.append(command) + a.append("-U %s --host=%s --port=%s" % (user, host, port)) + for key, value in kwargs.items(): + key = key.replace("_", "-") + if type(value) is bool and value is True: + a.append("--%s" % key) + elif type(value) is str: + a.append('--%s="%s"' % (key, value)) + else: + a.append('--%s=%s' % (key, value)) + + _args = list(args) + if len(_args) > 0: + a.append(" ".join(_args)) + + statement = " ".join(a) + + return Command(statement, **_kwargs) + + +def pgsql_create(database, owner=None, template=None, **kwargs): + kwargs.setdefault("comment", "create %s postgres database" % database) + + if owner is not None: + kwargs['owner'] = owner + + if template is not None: + kwargs['template'] = template + + return pgsql("createdb", database, **kwargs) + + +def pgsql_drop(database, **kwargs): + kwargs.setdefault("comment", "drop %s postgres database" % database) + + return pgsql("dropdb", database, **kwargs) + + +def pgsql_dump(database, path=None, **kwargs): + kwargs.setdefault("comment", "dump postgres database") + kwargs.setdefault("column_inserts", True) + + if path is None: + path = "%s.sql" % database + + kwargs['dbname'] = database + kwargs['file'] = path + + return pgsql("pg_dump", **kwargs) + + +def pgsql_exists(database, **kwargs): + kwargs.setdefault("comment", "determine if %s postgres database exists" % database) + kwargs.setdefault("register", "%s_exists" % database) + + command = pgsql("psql", **kwargs) + command.statement += r" -lqt | cut -d \| -f 1 | grep -qw %s" % database + + return command + + +def pgsql_load(database, path, **kwargs): + kwargs.setdefault("comment", "load data into a postgres database") + + kwargs['dbname'] = database + kwargs['file'] = path + + return pgsql("psql", **kwargs) + + +def pgsql_user(name, admin_pass=None, admin_user="postgres", op="create", password=None, **kwargs): + if op == "create": + kwargs.setdefault("comment", "create %s postgres user" % name) + + command = pgsql("createuser", "-DRS %s" % name, password=admin_pass, user=admin_user, **kwargs) + + if password is not None: + extra = pgsql("psql", password=admin_pass, user=admin_user, **kwargs) + command.statement += " && " + extra.statement + command.statement += " -c \"ALTER USER %s WITH ENCRYPTED PASSWORD '%s';\"" % (name, password) + + return command + elif op == "drop": + kwargs.setdefault("comment", "remove %s postgres user" % name) + + return pgsql("dropuser", name, password=admin_pass, user=admin_user, **kwargs) + elif op == "exists": + kwargs.setdefault("comment", "determine if %s postgres user exists" % name) + kwargs.setdefault("register", "pgsql_use_exists") + + command = pgsql("psql", password=admin_pass, user=admin_user, **kwargs) + + sql = "SELECT 1 FROM pgsql_roles WHERE rolname='%s'" % name + + command.statement += ' -c "%s"' % sql + + return command + else: + raise InvalidInput("Unrecognized or unsupported Postgres user operation: %s" % op) diff --git a/scripttease/lib/commands/posix.py b/scripttease/lib/commands/posix.py new file mode 100644 index 0000000..2c66603 --- /dev/null +++ b/scripttease/lib/commands/posix.py @@ -0,0 +1,532 @@ +import os +from .base import Command + + +def append(path, content=None, **kwargs): + """Append content to a file. + + - path (str): The path to the file. + - content (str): The content to be appended. + + """ + kwargs.setdefault("comment", "append to %s" % path) + + statement = 'echo "%s" >> %s' % (content or "", path) + + return Command(statement, **kwargs) + + +def archive(from_path, absolute=False, exclude=None, file_name="archive.tgz", strip=None, to_path=".", view=False, + **kwargs): + """Create a file archive. + + - from_path (str): The path that should be archived. + - absolute (bool): Set to ``True`` to preserve the leading slash. + - exclude (str): A pattern to be excluded from the archive. + - strip (int): Remove the specified number of leading elements from the path. + - to_path (str): Where the archive should be created. This should *not* include the file name. + - view (bool): View the output of the command as it happens. + + """ + tokens = ["tar"] + switches = ["-cz"] + + if absolute: + switches.append("P") + + if view: + switches.append("v") + + tokens.append("".join(switches)) + + if exclude: + tokens.append("--exclude %s" % exclude) + + if strip: + tokens.append("--strip-components %s" % strip) + + to_path = "%s/%s" % (to_path, file_name) + tokens.append('-f %s %s' % (to_path, from_path)) + + name = " ".join(tokens) + + return Command(name, **kwargs) + + +def certbot(domain_name, email=None, webroot=None, **kwargs): + """Get new SSL certificate from Let's Encrypt. + + - domain_name (str): The domain name for which the SSL certificate is requested. + - email (str): The email address of the requester sent to the certificate authority. Required. + - webroot (str): The directory where the challenge file will be created. + + """ + _email = email or os.environ.get("SCRIPTTEASE_CERTBOT_EMAIL", None) + _webroot = webroot or os.path.join("/var", "www", "domains", domain_name.replace(".", "_"), "www") + + if not _email: + raise ValueError("Email is required for certbot command.") + + template = "certbot certonly --agree-tos --email %(email)s -n --webroot -w %(webroot)s -d %(domain_name)s" + statement = template % { + 'domain_name': domain_name, + 'email': _email, + 'webroot': _webroot, + } + + return Command(statement, **kwargs) + + +def copy(from_path, to_path, overwrite=False, recursive=False, **kwargs): + """Copy a file or directory. + + - from_path (str): The file or directory to be copied. + - to_path (str): The location to which the file or directory should be copied. + - overwrite (bool): Indicates files and directories should be overwritten if they exist. + - recursive (bool): Copy sub-directories. + + """ + kwargs.setdefault("comment", "copy %s to %s" % (from_path, to_path)) + + a = list() + a.append("cp") + + if not overwrite: + a.append("-n") + + if recursive: + a.append("-R") + + a.append(from_path) + a.append(to_path) + + return Command(" ".join(a), **kwargs) + + +def dir(path, group=None, mode=None, owner=None, recursive=True, **kwargs): + """Create a directory. + + - path (str): The path to be created. + - mode (int | str): The access permissions of the new directory. + - recursive (bool): Create all directories along the path. + + """ + kwargs.setdefault("comment", "create directory %s" % path) + + statement = ["mkdir"] + if mode is not None: + statement.append("-m %s" % mode) + + if recursive: + statement.append("-p") + + if group: + if recursive: + statement.append("&& chgrp -R %s" % group) + else: + statement.append("&& chgrp %s" % group) + + if owner: + if recursive: + statement.append("&& chown -R %s" % owner) + else: + statement.append("&& chown %s" % owner) + + statement.append(path) + + return Command(" ".join(statement), **kwargs) + + +def extract(from_path, absolute=False, exclude=None, strip=None, to_path=None, view=False, **kwargs): + """Extract a file archive. + + - from_path (str): The path that should be archived. + - absolute (bool): Set to ``True`` to preserve the leading slash. + - exclude (str): A pattern to be excluded from the archive. + - strip (int): Remove the specified number of leading elements from the path. + - to_path (str): Where the archive should be extracted. This should *not* include the file name. + - view (bool): View the output of the command as it happens. + + """ + _to_path = to_path or "./" + + tokens = ["tar"] + switches = ["-xz"] + + if absolute: + switches.append("P") + + if view: + switches.append("v") + + tokens.append("".join(switches)) + + if exclude: + tokens.append("--exclude %s" % exclude) + + if strip: + tokens.append("--strip-components %s" % strip) + + tokens.append('-f %s %s' % (from_path, _to_path)) + + statement = " ".join(tokens) + + return Command(statement, **kwargs) + + +def link(source, force=False, target=None, **kwargs): + """Create a symlink. + + - source (str): The source of the link. + - force (bool): Force the creation of the link. + - target (str): The name or path of the target. Defaults to the base name of the source path. + + """ + _target = target or os.path.basename(source) + + kwargs.setdefault("comment", "link to %s" % source) + + statement = ["ln -s"] + + if force: + statement.append("-f") + + statement.append(source) + statement.append(_target) + + return Command(" ".join(statement), **kwargs) + + +def move(from_path, to_path, **kwargs): + """Move a file or directory. + + - from_path (str): The current path. + - to_path (str): The new path. + + """ + kwargs.setdefault("comment", "move %s to %s" % (from_path, to_path)) + statement = "mv %s %s" % (from_path, to_path) + + return Command(statement, **kwargs) + + +def perms(path, group=None, mode=None, owner=None, recursive=False, **kwargs): + """Set permissions on a file or directory. + + - path (str): The path to be changed. + - group (str): The name of the group to be applied. + - mode (int | str): The access permissions of the file or directory. + - owner (str): The name of the user to be applied. + - recursive: Create all directories along the path. + + """ + commands = list() + + kwargs['comment'] = "set permissions on %s" % path + + if group is not None: + statement = ["chgrp"] + + if recursive: + statement.append("-R") + + statement.append(group) + statement.append(path) + + commands.append(Command(" ".join(statement), **kwargs)) + + if owner is not None: + statement = ["chown"] + + if recursive: + statement.append("-R") + + statement.append(owner) + statement.append(path) + + commands.append(Command(" ".join(statement), **kwargs)) + + if mode is not None: + statement = ["chmod"] + + if recursive: + statement.append("-R") + + statement.append(str(mode)) + statement.append(path) + + commands.append(Command(" ".join(statement), **kwargs)) + + kwargs.setdefault("comment", "set permissions on %s" % path) + + a = list() + for c in commands: + a.append(c.get_statement(include_comment=True)) + + return Command("\n".join(a), **kwargs) + + +def remove(path, force=False, recursive=False, **kwargs): + """Remove a file or directory. + + - path (str): The path to be removed. + - force (bool): Force the removal. + - recursive (bool): Remove all directories along the path. + + """ + kwargs.setdefault("comment", "remove %s" % path) + + statement = ["rm"] + + if force: + statement.append("-f") + + if recursive: + statement.append("-r") + + statement.append(path) + + return Command(" ".join(statement), **kwargs) + + +def replace(path, backup=".b", delimiter="/", find=None, replace=None, **kwargs): + """Find and replace text in a file. + + - path (str): The path to the file to be edited. + - backup (str): The backup file extension to use. + - delimiter (str): The pattern delimiter. + - find (str): The old text. Required. + - replace (str): The new text. Required. + + """ + + kwargs.setdefault("comment", "find and replace in %s" % path) + + context = { + 'backup': backup, + 'delimiter': delimiter, + 'path': path, + 'pattern': find, + 'replace': replace, + } + + template = "sed -i %(backup)s 's%(delimiter)s%(pattern)s%(delimiter)s%(replace)s%(delimiter)sg' %(path)s" + + statement = template % context + + return Command(statement, **kwargs) + + +def rsync(source, target, delete=False, exclude=None, host=None, key_file=None, links=True, port=22, + recursive=True, user=None, **kwargs): + """Synchronize a directory structure. + + - source (str): The source directory. + - target (str): The target directory. + - delete (bool): Indicates target files that exist in source but not in target should be removed. + - exclude (str): The path to an exclude file. + - host (str): The host name or IP address. This causes the command to run over SSH. + - key_file (str): The privacy SSH key (path) for remote connections. User expansion is automatically applied. + - links (bool): Include symlinks in the sync. + - port (int): The SSH port to use for remote connections. + - recursive (bool): Indicates source contents should be recursively synchronized. + - user (str): The user name to use for remote connections. + + """ + # - guess: When ``True``, the ``host``, ``key_file``, and ``user`` will be guessed based on the base name of + # the source path. + # :type guess: bool + # if guess: + # host = host or os.path.basename(source).replace("_", ".") + # key_file = key_file or os.path.expanduser(os.path.join("~/.ssh", os.path.basename(source))) + # user = user or os.path.basename(source) + # else: + # host = host + # key_file = key_file + # user = user + + kwargs.setdefault("comment", "sync %s with %s" % (source, target)) + + # rsync -e "ssh -i $(SSH_KEY) -p $(SSH_PORT)" -P -rvzc --delete + # $(OUTPUTH_PATH) $(SSH_USER)@$(SSH_HOST):$(UPLOAD_PATH) --cvs-exclude; + + tokens = list() + tokens.append("rsync") + tokens.append("--cvs-exclude") + tokens.append("--checksum") + tokens.append("--compress") + + if links: + tokens.append("--copy-links") + + if delete: + tokens.append("--delete") + + if exclude is not None: + tokens.append("--exclude-from=%s" % exclude) + + # --partial and --progress + tokens.append("-P") + + if recursive: + tokens.append("--recursive") + + tokens.append(source) + + conditions = [ + host is not None, + key_file is not None, + user is not None, + ] + if all(conditions): + tokens.append('-e "ssh -i %s -p %s"' % (key_file, port)) + tokens.append("%s@%s:%s" % (user, host, target)) + else: + tokens.append(target) + + statement = " ".join(tokens) + + return Command(statement, **kwargs) + + +def scopy(from_path, to_path, host=None, key_file=None, port=22, user=None, **kwargs): + """Copy a file or directory to a remote server. + + - from_path (str): The source directory. + - to_path (str): The target directory. + - host (str): The host name or IP address. Required. + - key_file (str): The privacy SSH key (path) for remote connections. User expansion is automatically applied. + - port (int): The SSH port to use for remote connections. + - user (str): The user name to use for remote connections. + + """ + kwargs.setdefault("comment", "copy %s to remote %s" % (from_path, to_path)) + + # TODO: What to do to force local versus remote commands? + # kwargs['local'] = True + + kwargs['sudo'] = False + + statement = ["scp"] + + if key_file is not None: + statement.append("-i %s" % key_file) + + statement.append("-P %s" % port) + statement.append(from_path) + + if host is not None and user is not None: + statement.append("%s@%s:%s" % (user, host, to_path)) + elif host is not None: + statement.append("%s:%s" % (host, to_path)) + else: + raise ValueError("Host is a required keyword argument.") + + return Command(" ".join(statement), **kwargs) + + +def sync(source, target, delete=False, exclude=None, links=True, recursive=True, **kwargs): + """Synchronize a local directory structure. + + - source (str): The source directory. + - target (str): The target directory. + - delete (bool): Indicates target files that exist in source but not in target should be removed. + - exclude (str): The path to an exclude file. + - host (str): The host name or IP address. This causes the command to run over SSH. + - key_file (str): The privacy SSH key (path) for remote connections. User expansion is automatically applied. + - links (bool): Include symlinks in the sync. + - port (int): The SSH port to use for remote connections. + - recursive (bool): Indicates source contents should be recursively synchronized. + - user (str): The user name to use for remote connections. + + """ + # - guess: When ``True``, the ``host``, ``key_file``, and ``user`` will be guessed based on the base name of + # the source path. + # :type guess: bool + # if guess: + # host = host or os.path.basename(source).replace("_", ".") + # key_file = key_file or os.path.expanduser(os.path.join("~/.ssh", os.path.basename(source))) + # user = user or os.path.basename(source) + # else: + # host = host + # key_file = key_file + # user = user + + kwargs.setdefault("comment", "sync %s with %s" % (source, target)) + + # rsync -e "ssh -i $(SSH_KEY) -p $(SSH_PORT)" -P -rvzc --delete + # $(OUTPUTH_PATH) $(SSH_USER)@$(SSH_HOST):$(UPLOAD_PATH) --cvs-exclude; + + tokens = list() + tokens.append("rsync") + tokens.append("--cvs-exclude") + tokens.append("--checksum") + tokens.append("--compress") + + if links: + tokens.append("--copy-links") + + if delete: + tokens.append("--delete") + + if exclude is not None: + tokens.append("--exclude-from=%s" % exclude) + + # --partial and --progress + tokens.append("-P") + + if recursive: + tokens.append("--recursive") + + tokens.append(source) + tokens.append(target) + + statement = " ".join(tokens) + + return Command(statement, **kwargs) + + +def touch(path, **kwargs): + """Touch a file or directory. + + - path (str): The file or directory to touch. + + """ + kwargs.setdefault("comment", "touch %s" % path) + + return Command("touch %s" % path, **kwargs) + + +def wait(seconds, **kwargs): + """Pause execution for a number of seconds. + + - seconds (int): The number of seconds to wait. + + """ + kwargs.setdefault("comment", "pause for %s seconds" % seconds) + + return Command("sleep %s" % seconds, **kwargs) + + +def write(path, content=None, **kwargs): + """Write to a file. + + - path (str): The file to be written. + - content (str): The content to be written. Note: If omitted, this command is equivalent to ``touch``. + + """ + _content = content or "" + + kwargs.setdefault("comment", "write to %s" % path) + + a = list() + + if len(_content.split("\n")) > 1: + a.append("cat > %s << EOF" % path) + a.append(_content) + a.append("EOF") + else: + a.append('echo "%s" > %s' % (_content, path)) + + return Command(" ".join(a), **kwargs) + diff --git a/scripttease/lib/commands/python.py b/scripttease/lib/commands/python.py new file mode 100644 index 0000000..f4bb698 --- /dev/null +++ b/scripttease/lib/commands/python.py @@ -0,0 +1,39 @@ +from .base import Command + + +def python_pip(name, op="install", upgrade=False, venv=None, version=3, **kwargs): + """Use pip to install or uninstall a Python package. + + - name (str): The name of the package. + - op (str): The operation to perform; install, uninstall + - upgrade (bool): Upgrade an installed package. + - venv (str): The name of the virtual environment to load. + - version (int): The Python version to use, e.g. ``2`` or ``3``. + + """ + manager = "pip" + if version == 3: + manager = "pip3" + + if upgrade: + statement = "%s install --upgrade %s" % (manager, name) + else: + statement = "%s %s %s" % (manager, op, name) + + if venv is not None: + kwargs['prefix'] = "source %s/bin/activate" % venv + + kwargs.setdefault("comment", "%s %s" % (op, name)) + + return Command(statement, **kwargs) + + +def python_virtualenv(name, **kwargs): + """Create a Python virtual environment. + + - name (str): The name of the environment to create. + + """ + kwargs.setdefault("comment", "create %s virtual environment" % name) + + return Command("virtualenv %s" % name, **kwargs) diff --git a/scripttease/lib/commands/ubuntu.py b/scripttease/lib/commands/ubuntu.py new file mode 100644 index 0000000..767b88f --- /dev/null +++ b/scripttease/lib/commands/ubuntu.py @@ -0,0 +1,337 @@ +# Imports + +from commonkit import split_csv +from .base import Command, Template +from .common import COMMON_MAPPINGS +from .django import DJANGO_MAPPINGS +from .mysql import MYSQL_MAPPINGS +from .pgsql import PGSQL_MAPPINGS +from .posix import POSIX_MAPPINGS + +# Exports + +__all__ = ( + "MAPPINGS", + "apache", + "apache_disable_module", + "apache_disable_site", + "apache_enable_module", + "apache_enable_site", + "apache_reload", + "apache_restart", + "apache_start", + "apache_stop", + "apache_test", + "command_exists", + "service_reload", + "service_restart", + "service_start", + "service_stop", + "system", + "system_install", + "system_reboot", + "system_update", + "system_upgrade", + "system_uninstall", + "template", + "user", +) + + +def command_exists(name): + """Indicates whether a given command exists in this overlay. + + :param name: The name of the command. + :type name: str + + :rtype: bool + + """ + return name in MAPPINGS + + +def apache(op, **kwargs): + """Execute an Apache-related command. + + - op (str): The operation to perform; reload, restart, start, stop, test. + + """ + if op == "reload": + return apache_reload(**kwargs) + elif op == "restart": + return apache_restart(**kwargs) + elif op == "start": + return apache_start(**kwargs) + elif op == "stop": + return apache_stop(**kwargs) + elif op == "test": + return apache_test(**kwargs) + else: + raise NameError("Unrecognized or unsupported apache operation: %s" % op) + + +def apache_disable_module(name, **kwargs): + """Disable an Apache module. + + - name (str): The module name. + + """ + kwargs.setdefault("comment", "disable %s apache module" % name) + + return Command("a2dismod %s" % name, **kwargs) + + +def apache_disable_site(name, **kwargs): + """Disable an Apache site. + + - name (str): The domain name. + + """ + kwargs.setdefault("comment", "disable %s apache site" % name) + + return Command("a2dissite %s" % name, **kwargs) + + +def apache_enable_module(name, **kwargs): + """Enable an Apache module. + + - name (str): The module name. + + """ + kwargs.setdefault("comment", "enable %s apache module" % name) + + return Command("a2enmod %s" % name, **kwargs) + + +def apache_enable_site(name, **kwargs): + """Enable an Apache site. + + + """ + kwargs.setdefault("comment", "enable %s apache module" % name) + + return Command("a2ensite %s" % name, **kwargs) + + +def apache_reload(**kwargs): + kwargs.setdefault("comment", "reload apache") + kwargs.setdefault("register", "apache_reloaded") + + return Command("service apache2 reload", **kwargs) + + +def apache_restart(**kwargs): + kwargs.setdefault("comment", "restart apache") + kwargs.setdefault("register", "apache_restarted") + + return Command("service apache2 restart", **kwargs) + + +def apache_start(**kwargs): + kwargs.setdefault("comment", "start apache") + kwargs.setdefault("register", "apache_started") + + return Command("service apache2 start", **kwargs) + + +def apache_stop(**kwargs): + kwargs.setdefault("comment", "stop apache") + + return Command("service apache2 stop", **kwargs) + + +def apache_test(**kwargs): + kwargs.setdefault("comment", "check apache configuration") + kwargs.setdefault("register", "apache_checks_out") + + return Command("apachectl configtest", **kwargs) + + +def service_reload(name, **kwargs): + """Reload a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "reload %s service" % name) + kwargs.setdefault("register", "%s_reloaded" % name) + + return Command("service %s reload" % name, **kwargs) + + +def service_restart(name, **kwargs): + """Restart a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "restart %s service" % name) + kwargs.setdefault("register", "%s_restarted" % name) + + return Command("service %s restart" % name, **kwargs) + + +def service_start(name, **kwargs): + """Start a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "start %s service" % name) + kwargs.setdefault("register", "%s_started" % name) + + return Command("service %s start" % name, **kwargs) + + +def service_stop(name, **kwargs): + """Stop a service. + + - name (str): The service name. + + """ + kwargs.setdefault("comment", "stop %s service" % name) + kwargs.setdefault("register", "%s_stopped" % name) + + return Command("service %s stop" % name, **kwargs) + + +def system(op, **kwargs): + """Perform a system operation. + + - op (str): The operation to perform; reboot, update, upgrade. + + """ + if op == "reboot": + return system_reboot(**kwargs) + elif op == "update": + return system_update(**kwargs) + elif op == "upgrade": + return system_upgrade(**kwargs) + else: + raise NameError("Unrecognized or unsupported system operation: %s" % op) + + +def system_install(name, **kwargs): + """Install a system-level package. + + - name (str): The name of the package to install. + + """ + kwargs.setdefault("comment", "install system package %s" % name) + + return Command("apt-get install -y %s" % name, **kwargs) + + +def system_reboot(**kwargs): + kwargs.setdefault("comment", "reboot the system") + + return Command("reboot", **kwargs) + + +def system_uninstall(name, **kwargs): + """Uninstall a system-level package. + + - name (str): The name of the package to uninstall. + + """ + kwargs.setdefault("comment", "remove system package %s" % name) + + return Command("apt-get uninstall -y %s" % name, **kwargs) + + +def system_update(**kwargs): + kwargs.setdefault("comment", "update system package info") + + return Command("apt-get update -y", **kwargs) + + +def system_upgrade(**kwargs): + kwargs.setdefault("comment", "upgrade the system") + + return Command("apt-get upgrade -y", **kwargs) + + +def template(source, target, backup=True, parser=None, **kwargs): + """Create a file from a template. + + - source (str): The path to the template file. + - target (str): The path to where the new file should be created. + - backup (bool): Indicates whether a backup should be made if the target file already exists. + - parser (str): The parser to use ``jinja`` (the default) or ``simple``. + + """ + return Template(source, target, backup=backup, parser=parser, **kwargs) + + +def user(name, groups=None, home=None, op="add", password=None, **kwargs): + """Create or remove a user. + + - name (str): The user name. + - groups (str | list): A list of groups to which the user should belong. + - home (str): The path to the user's home directory. + - op (str); The operation to perform; ``add`` or ``remove``. + - password (str): The user's password. (NOT IMPLEMENTED) + + """ + if op == "add": + kwargs.setdefault("comment", "create a user named %s" % name) + + commands = list() + + # The gecos switch eliminates the prompts. + a = list() + a.append('adduser %s --disabled-password --gecos ""' % name) + if home is not None: + a.append("--home %s" % home) + + commands.append(Command(" ".join(a), **kwargs)) + + if type(groups) is str: + groups = split_csv(groups, smart=False) + + if type(groups) in [list, tuple]: + for group in groups: + commands.append(Command("adduser %s %s" % (name, group), **kwargs)) + + a = list() + for c in commands: + a.append(c.get_statement(include_comment=True)) + + return Command("\n".join(a), **kwargs) + elif op == "remove": + kwargs.setdefault("comment", "remove a user named %s" % name) + return Command("deluser %s" % name, **kwargs) + else: + raise NameError("Unsupported or unrecognized operation: %s" % op) + + +MAPPINGS = { + 'apache': apache, + 'apache.disable_module': apache_disable_module, + 'apache.disable_site': apache_disable_site, + 'apache.enable_module': apache_enable_module, + 'apache.enable_site': apache_enable_site, + 'apache.reload': apache_reload, + 'apache.restart': apache_restart, + 'apache.start': apache_start, + 'apache.stop': apache_stop, + 'apache.test': apache_test, + 'install': system_install, + 'reboot': system_reboot, + 'reload': service_reload, + 'restart': service_restart, + 'start': service_start, + 'stop': service_stop, + 'system': system, + 'template': template, + 'update': system_update, + 'uninstall': system_uninstall, + 'upgrade': system_upgrade, + 'user': user, +} + +MAPPINGS.update(COMMON_MAPPINGS) +MAPPINGS.update(DJANGO_MAPPINGS) +MAPPINGS.update(MYSQL_MAPPINGS) +MAPPINGS.update(PGSQL_MAPPINGS) +MAPPINGS.update(POSIX_MAPPINGS) diff --git a/scripttease/lib/loaders/base.py b/scripttease/lib/loaders/base.py index 50e14ac..954af7d 100644 --- a/scripttease/lib/loaders/base.py +++ b/scripttease/lib/loaders/base.py @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) __all__ = ( - "filter_snippets", + # "filter_snippets", "load_variables", "BaseLoader", "Snippet", @@ -26,32 +26,40 @@ __all__ = ( # Functions -def filter_snippets(snippets, environments=None, tags=None): - """Filter snippets based on the given criteria. - - :param snippets: The snippets to be filtered. - :type snippets: list[scripttease.lib.loaders.base.Snippet] - - :param environments: Environment names to be matched. - :type environments: list[str] - - :param tags: Tag names to be matched. - :type tags: list[str] - - """ - filtered = list() - for snippet in snippets: - if environments is not None and len(snippet.environments) > 0: - if not any_list_item(environments, snippet.environments): - continue - - if tags is not None: - if not any_list_item(tags, snippet.tags): - continue - - filtered.append(snippet) - - return filtered +# def filter_commands(commands, key, value): +# """Filter commands based on the given criteria. +# +# :param commands: The commands to be filtered. +# :type commands: list[scripttease.lib.commands.base.Command] +# +# :param key: The attribute name to be matched. +# :type key: str +# +# :param value: The value of the attribute. +# +# """ +# filtered = list() +# for command in commands: +# try: +# values = getattr(command, key) +# except AttributeError: +# continue +# +# if not any_list_item(values, key): +# continue +# +# if not any_list_item() +# if environments is not None and len(snippet.environments) > 0: +# if not any_list_item(environments, snippet.environments): +# continue +# +# if tags is not None: +# if not any_list_item(tags, snippet.tags): +# continue +# +# filtered.append(snippet) +# +# return filtered def load_variables(path, env=None): diff --git a/scripttease/lib/loaders/ini.py b/scripttease/lib/loaders/ini.py index 0260f59..3206296 100644 --- a/scripttease/lib/loaders/ini.py +++ b/scripttease/lib/loaders/ini.py @@ -66,7 +66,7 @@ class INILoader(BaseLoader): # continue # Arguments surrounded by quotes are considered to be one argument. All others are split into a - # list to be passed to the callback. It is also possible that this is a call where no arguments are + # list to be passed to the parser. It is also possible that this is a call where no arguments are # present, so the whole thing is wrapped to protect against an index error. A TypeError is raised in # cases where a command is provided with no positional arguments; we interpret this as True. try: