Skip to content

Commit

Permalink
feat: import deployment plan from a file ; rebase done
Browse files Browse the repository at this point in the history
  • Loading branch information
mdrutel committed Nov 15, 2023
1 parent 64b0024 commit ca963a6
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 77 deletions.
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)
79 changes: 3 additions & 76 deletions tdp/cli/commands/plan/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
56 changes: 56 additions & 0 deletions tdp/cli/commands/plan/import_file.py
Original file line number Diff line number Diff line change
@@ -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))
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

0 comments on commit ca963a6

Please sign in to comment.