Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import deployment plan from a file ; rebase done #511

Merged
merged 2 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion tdp/cli/commands/plan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
81 changes: 4 additions & 77 deletions tdp/cli/commands/plan/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@

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:
from tdp.core.collections import Collections

logger = logging.getLogger("tdp").getChild("edit")
logger = logging.getLogger(__name__)


def _get_header_message(deployment_id: int, temp_file_name: str) -> str:
Expand All @@ -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.
Expand Down Expand Up @@ -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.")
Expand Down
48 changes: 48 additions & 0 deletions tdp/cli/commands/plan/import_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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(__name__)


@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)
with open(file_name) as file:
# 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 a planned deployment is present, update it instead of creating it
if planned_deployment:
deployment.id = planned_deployment.id
session.merge(deployment)
session.commit()
click.echo("Deployment plan successfully imported.")
2 changes: 1 addition & 1 deletion tdp/cli/commands/plan/test_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from tdp.cli.commands.plan.edit import _parse_line
from tdp.cli.utils import _parse_line


def test_mandatory_operation_name():
Expand Down
73 changes: 73 additions & 0 deletions tdp/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""

Expand Down
Loading