diff --git a/tdp/cli/commands/plan/__init__.py b/tdp/cli/commands/plan/__init__.py index 0cbdf737..efa5da77 100644 --- a/tdp/cli/commands/plan/__init__.py +++ b/tdp/cli/commands/plan/__init__.py @@ -5,6 +5,7 @@ from tdp.cli.commands.plan.dag import dag from tdp.cli.commands.plan.edit import edit +from tdp.cli.commands.plan.import_file import import_file from tdp.cli.commands.plan.ops import ops from tdp.cli.commands.plan.reconfigure import reconfigure from tdp.cli.commands.plan.resume import resume @@ -17,7 +18,8 @@ def plan(): plan.add_command(dag) -plan.add_command(ops) plan.add_command(edit) +plan.add_command(import_file) +plan.add_command(ops) plan.add_command(reconfigure) plan.add_command(resume) diff --git a/tdp/cli/commands/plan/edit.py b/tdp/cli/commands/plan/edit.py index 117441d5..c1a1cde9 100644 --- a/tdp/cli/commands/plan/edit.py +++ b/tdp/cli/commands/plan/edit.py @@ -5,16 +5,15 @@ import logging import os -import re import tempfile from contextlib import contextmanager -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import click from tdp.cli.queries import get_planned_deployment from tdp.cli.session import get_session -from tdp.cli.utils import collections, database_dsn +from tdp.cli.utils import collections, database_dsn, parse_file from tdp.core.models.deployment_model import DeploymentModel if TYPE_CHECKING: @@ -47,78 +46,6 @@ def _get_header_message(deployment_id: int, temp_file_name: str) -> str: """ -def _parse_line(line: str) -> tuple[str, Optional[str], Optional[list[str]]]: - """Parses a line which contains an operation, and eventually a host and extra vars. - - Args: - line: Line to be parsed. - - Returns: - Operation, host and extra vars. - """ - parsed_line = re.match( - r"^(.*?)( on .*?){0,1}( ?with .*?){0,1}( ?on .*?){0,1}$", line - ) - - if parsed_line is None: - raise ValueError( - "Error on line '" - + line - + "': it must be 'OPERATION [on HOST] [with EXTRA_VARS[,EXTRA_VARS]].'" - ) - - if parsed_line.group(1).split(" ")[0] == "": - raise ValueError("Error on line '" + line + "': it must contain an operation.") - - if len(parsed_line.group(1).strip().split(" ")) > 1: - raise ValueError("Error on line '" + line + "': only 1 operation is allowed.") - - if parsed_line.group(2) is not None and parsed_line.group(4) is not None: - raise ValueError( - "Error on line '" + line + "': only 1 host is allowed in a line." - ) - - operation = parsed_line.group(1).strip() - - # Get the host and test if it is declared - if parsed_line.group(2) is not None: - host = parsed_line.group(2).split(" on ")[1] - if host == "": - raise ValueError( - "Error on line '" + line + "': host is required after 'on' keyword." - ) - elif parsed_line.group(4) is not None: - host = parsed_line.group(4).split(" on ")[1] - if host == "": - raise ValueError( - "Error on line '" + line + "': host is required after 'on' keyword." - ) - else: - host = None - - # Get the extra vars and test if they are declared - if parsed_line.group(3) is not None: - extra_vars = parsed_line.group(3).split(" with ")[1] - if extra_vars == "": - raise ValueError("Extra vars are required after 'with' keyword.") - extra_vars = extra_vars.split(",") - extra_vars = [item.strip() for item in extra_vars] - else: - extra_vars = None - - return (operation, host, extra_vars) - - -def _parse_file(file_name) -> list[tuple[str, Optional[str], Optional[list[str]]]]: - """Parses a file which contains operations, hosts and extra vars.""" - file_content = file_name.read() - return [ - _parse_line(line) - for line in file_content.split("\n") - if line and not line.startswith("#") - ] - - @contextmanager def _managed_temp_file(**kwargs): """Creates a temporary file and deletes it when the context is exited. @@ -195,7 +122,7 @@ def edit( try: # Remove empty elements and comments # and get the operations, hosts and extra vars in a list - new_operations_hosts_vars = _parse_file(file) + new_operations_hosts_vars = parse_file(file) if new_operations_hosts_vars == operation_list: raise click.ClickException("Plan was not modified.") diff --git a/tdp/cli/commands/plan/import_file.py b/tdp/cli/commands/plan/import_file.py new file mode 100644 index 00000000..43317841 --- /dev/null +++ b/tdp/cli/commands/plan/import_file.py @@ -0,0 +1,56 @@ +# Copyright 2022 TOSIT.IO +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import click + +from tdp.cli.queries import get_planned_deployment +from tdp.cli.session import get_session +from tdp.cli.utils import collections, database_dsn, parse_file +from tdp.core.models.deployment_model import DeploymentModel + +if TYPE_CHECKING: + from tdp.core.collections import Collections + +logger = logging.getLogger("tdp").getChild("edit") + + +@click.command("import") +@click.argument("file_name", nargs=1, required=True) +@collections +@database_dsn +def import_file( + collections: Collections, + database_dsn: str, + file_name: str, +): + """Import a deployment from a file.""" + with get_session(database_dsn, commit_on_exit=True) as session: + planned_deployment = get_planned_deployment(session) + try: + with open(file_name) as file: + try: + # Remove empty elements and comments + # and get the operations, hosts and extra vars in a list + new_operations_hosts_vars = parse_file(file) + + if not new_operations_hosts_vars: + raise click.ClickException("Plan must not be empty.") + + deployment = DeploymentModel.from_operations_hosts_vars( + collections, new_operations_hosts_vars + ) + + if planned_deployment: + deployment.id = planned_deployment.id + session.merge(deployment) + session.commit() + click.echo("Deployment plan successfully imported.") + except Exception as e: + logger.error(str(e)) + except Exception as e: + logger.error(str(e)) diff --git a/tdp/cli/utils.py b/tdp/cli/utils.py index 1c442ccd..a7441375 100644 --- a/tdp/cli/utils.py +++ b/tdp/cli/utils.py @@ -3,6 +3,7 @@ from __future__ import annotations +import re from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Optional @@ -202,6 +203,78 @@ def decorator(fn: FC) -> FC: return decorator(func) +def _parse_line(line: str) -> tuple[str, Optional[str], Optional[list[str]]]: + """Parses a line which contains an operation, and eventually a host and extra vars. + + Args: + line: Line to be parsed. + + Returns: + Operation, host and extra vars. + """ + parsed_line = re.match( + r"^(.*?)( on .*?){0,1}( ?with .*?){0,1}( ?on .*?){0,1}$", line + ) + + if parsed_line is None: + raise ValueError( + "Error on line '" + + line + + "': it must be 'OPERATION [on HOST] [with EXTRA_VARS[,EXTRA_VARS]].'" + ) + + if parsed_line.group(1).split(" ")[0] == "": + raise ValueError("Error on line '" + line + "': it must contain an operation.") + + if len(parsed_line.group(1).strip().split(" ")) > 1: + raise ValueError("Error on line '" + line + "': only 1 operation is allowed.") + + if parsed_line.group(2) is not None and parsed_line.group(4) is not None: + raise ValueError( + "Error on line '" + line + "': only 1 host is allowed in a line." + ) + + operation = parsed_line.group(1).strip() + + # Get the host and test if it is declared + if parsed_line.group(2) is not None: + host = parsed_line.group(2).split(" on ")[1] + if host == "": + raise ValueError( + "Error on line '" + line + "': host is required after 'on' keyword." + ) + elif parsed_line.group(4) is not None: + host = parsed_line.group(4).split(" on ")[1] + if host == "": + raise ValueError( + "Error on line '" + line + "': host is required after 'on' keyword." + ) + else: + host = None + + # Get the extra vars and test if they are declared + if parsed_line.group(3) is not None: + extra_vars = parsed_line.group(3).split(" with ")[1] + if extra_vars == "": + raise ValueError("Extra vars are required after 'with' keyword.") + extra_vars = extra_vars.split(",") + extra_vars = [item.strip() for item in extra_vars] + else: + extra_vars = None + + return (operation, host, extra_vars) + + +def parse_file(file_name) -> list[tuple[str, Optional[str], Optional[list[str]]]]: + """Parses a file which contains operations, hosts and extra vars.""" + file_content = file_name.read() + return [ + _parse_line(line) + for line in file_content.split("\n") + if line and not line.startswith("#") + ] + + class CatchGroup(click.Group): """Catch exceptions and print them to stderr."""