diff --git a/.gitignore b/.gitignore index 90559d6..8d622da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ logs bin .build .dist +dist/ # python **/__pycache__/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..f24f188 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,29 @@ +# DEVELOPMENT + +This guide helps through the development process + +## Contribution + +Read the [CONTRIBUTING.md guide](CONTRIBUTING.md). + +## Development environment + +We recommend you to create a dedicated environment for your developments with Pyenv. + +```sh +pyenv install 3.9 +pyenv virtualenv 3.9 freyja +pyenv activate freyja +pip install --upgrade pip +``` + +## Running + +While you develop, stick to the Poetry usage to leverage your development environment : + +```sh +poetry update +poetry install +# use poetry to run freyja development version +poetry run freyja --help +``` diff --git a/dist/freyja-0.1.0-py3-none-any.whl b/dist/freyja-0.1.0-py3-none-any.whl index 72e74e6..c2e951a 100644 Binary files a/dist/freyja-0.1.0-py3-none-any.whl and b/dist/freyja-0.1.0-py3-none-any.whl differ diff --git a/docs/usage.md b/docs/usage.md index 8b1b41f..80bcb1b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -59,12 +59,33 @@ freyja machine usage vm1 vm2 --watch freyja machine usage vm1 ``` +Opens a console in a specific machine : + +```sh +freyja machine console vm1 +``` + List mac addresses already in use : ```sh freyja machine info | grep mac ``` +Create a snapshot of a machine : +```sh +freyja machine snapshot vm1 snaphost_name +``` + +Restore a snapshot of a machine : +```sh +freyja machine restore vm1 snaphost_name +``` + +List the snapshots of a machine : +```sh +freyja machine list-snapshots vm1 +``` + List networks: ```sh @@ -77,4 +98,4 @@ Describe networks : freyja network info # filter by name freyja network info net1 net2 -``` \ No newline at end of file +``` diff --git a/freyja/cli/cli.py b/freyja/cli/cli.py index e6be0e2..f1f34fc 100644 --- a/freyja/cli/cli.py +++ b/freyja/cli/cli.py @@ -2,7 +2,7 @@ import typer -from freyja.cli import machine, network +from freyja.cli import machine, network, snapshot from freyja.environment import FreyjaEnvironment from freyja.logger import FreyjaLogger @@ -10,6 +10,7 @@ no_args_is_help=True) app.add_typer(machine.app, name="machine") app.add_typer(network.app, name="network") +app.add_typer(snapshot.app, name="snapshot") logger = logging.getLogger(FreyjaLogger.name) diff --git a/freyja/cli/machine.py b/freyja/cli/machine.py index 2a8e638..2edfb10 100644 --- a/freyja/cli/machine.py +++ b/freyja/cli/machine.py @@ -8,7 +8,7 @@ from freyja.core.services.machine_service import create_machines, delete_machines, info_machines, \ list_machines, \ restart_machines, start_machines, \ - stop_machines, usage_machine + stop_machines, usage_machine, open_console_machine from freyja.lib.exceptions.configuration_exceptions import ConfigurationContentError, \ ConfigurationFileNotFoundException, \ ConfigurationFormatError @@ -126,3 +126,11 @@ def usage(names: Optional[List[str]] = typer.Argument(None, help="VM names list Display the virtual machines cpu and memory usage. """ usage_machine(names, watch) + + +@app.command() +def console(name: str = typer.Argument(..., help="VM name in which a console should be opened")): + """ + Opens a console in the specified machine + """ + open_console_machine(name) diff --git a/freyja/cli/snapshot.py b/freyja/cli/snapshot.py new file mode 100644 index 0000000..fcae1be --- /dev/null +++ b/freyja/cli/snapshot.py @@ -0,0 +1,57 @@ +import logging + +import typer + +from freyja.core.services.snapshot_service import restore_snapshot, create_snapshot, list_snapshot, delete_snapshot +from freyja.lib.utils.subprocess_utils import yes_no_question +from freyja.logger import FreyjaLogger + +app = typer.Typer(help="Manage snapshots of virtual machines") + +logger: logging.Logger = logging.getLogger(FreyjaLogger.name) + + +@app.command() +def restore(name: str = typer.Argument(..., help="VM name to restore"), + snapshot: str = typer.Argument(..., help="Name of the snapshot")): + """ + Restore a VM from a snapshot + """ + restore_snapshot(name, snapshot) + logger.warning(f"The machine {name} will be restore to snapshot {snapshot}") + if yes_no_question("Are you sure ? (Y/n)[default: n]", False): + restore_snapshot(name, snapshot) + logger.info("OK") + else: + logger.info("Aborted") + + +@app.command() +def create(name: str = typer.Argument(..., help="VM name to snapshot"), + snapshot: str = typer.Argument(..., help="Name of the snapshot")): + """ + Create a snapshot of a VM + """ + create_snapshot(name, snapshot) + logger.info(f"Created snapshot {snapshot}") + + +@app.command() +def delete(name: str = typer.Argument(..., help="VM name concerned by the snapshot deletion"), + snapshot: str = typer.Argument(..., help="Name of the snapshot to delete")): + """ + Delete a snapshot of a VM + """ + if yes_no_question(f"Are you sure to delete snapshot {snapshot} ? (Y/n)[default: n]", False): + delete_snapshot(name, snapshot) + logger.info(f"Deleted snapshot {snapshot}") + else: + logger.info("Aborted") + + +@app.command(name="list") +def list_(name: str = typer.Argument(..., help="The name of the VM concerned by the snapshots")): + """ + List snapshots of a VM + """ + list_snapshot(name) diff --git a/freyja/core/services/machine_service.py b/freyja/core/services/machine_service.py index 8b2648e..cc83976 100644 --- a/freyja/core/services/machine_service.py +++ b/freyja/core/services/machine_service.py @@ -10,7 +10,7 @@ from freyja.lib.exceptions.machine_exceptions import MachineAlreadyExists from freyja.lib.utils.bytes_utils import convert_size from freyja.lib.utils.error_utils import check_message -from freyja.lib.utils.subprocess_utils import execute +from freyja.lib.utils.subprocess_utils import execute, execute_interactive from freyja.lib.utils.virsh_utils import parse_info, parse_list, parse_stats from freyja.logger import FreyjaLogger from freyja.models import machine_info @@ -342,3 +342,14 @@ def usage_machine(names: List[str], watch: bool = False): curses.nocbreak() curses.echo() curses.endwin() + + +def open_console_machine(domain: str): + """ + Opens a console for the provided machine + :param name: name of the machine in which the console will be opened + """ + try: + execute_interactive(["virsh", "console", domain]) + except ChildProcessError as e: + logger.warning(f"Skip {domain}: Machine not found") diff --git a/freyja/core/services/snapshot_service.py b/freyja/core/services/snapshot_service.py new file mode 100644 index 0000000..f9ee092 --- /dev/null +++ b/freyja/core/services/snapshot_service.py @@ -0,0 +1,39 @@ +import logging + +from freyja.lib.utils.error_utils import check_message +from freyja.lib.utils.subprocess_utils import execute +from freyja.logger import FreyjaLogger + +logger: logging.Logger = logging.getLogger(FreyjaLogger.name) + + +def create_snapshot(domain: str, name: str): + try: + execute(["virsh", "snapshot-create-as", domain, "--name", name]) + except ChildProcessError as e: + logger.warning(f"Skip {domain}: Machine not found") + + +def delete_snapshot(domain: str, name: str): + try: + execute(["virsh", "snapshot-delete", domain, "--snapshotname", name]) + except ChildProcessError as e: + logger.warning(f"Skip {domain}: Machine not found") + + +def restore_snapshot(domain: str, name: str): + try: + execute(["virsh", "snapshot-revert", domain, "--snapshotname", name]) + except ChildProcessError as e: + if check_message(e, "snapshot"): + logger.warning(f"Skip {domain}: snapshot {name} not found") + else: + logger.warning(f"Skip {domain}: Machine not found") + + +def list_snapshot(domain: str): + try: + execute(["virsh", "snapshot-list", domain], stream_stdout=True) + + except ChildProcessError as e: + logger.warning(f"Skip {domain}: Machine not found") diff --git a/freyja/lib/utils/subprocess_utils.py b/freyja/lib/utils/subprocess_utils.py index 53d85e9..9e1886a 100644 --- a/freyja/lib/utils/subprocess_utils.py +++ b/freyja/lib/utils/subprocess_utils.py @@ -51,3 +51,10 @@ def yes_no_question(question: str, default: bool): return valid[choice] else: pass + + +def execute_interactive(command: List[str]): + try: + subprocess.check_call(command) + except subprocess.CalledProcessError as e: + raise ChildProcessError(str(e)) \ No newline at end of file