Skip to content
Open
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
24 changes: 15 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,29 @@ jobs:
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
run: sudo apt update && sudo apt install --yes tox
- name: Black
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install tox-uv
run: uv tool install tox --with tox-uv
- name: ruff
run: |
tox -e black-check
tox -e ruff-check
Tests:
runs-on: ubuntu-24.04
strategy:
fail-fast: true
matrix:
python-version: ["3.8", "3.10", "3.12", "3.13"]
python-version: ["3.8", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
run: sudo apt update && sudo apt install --yes tox
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install tox-uv
run: uv tool install tox --with tox-uv
- name: Unit tests
run: |
tox -e ${{ matrix.python-version }}
Expand All @@ -51,8 +55,10 @@ jobs:
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
run: sudo apt update && sudo apt install --yes tox
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install tox-uv
run: uv tool install tox --with tox-uv
- name: Coverage
run: |
tox -e begin,${{ matrix.python-version }},end
25 changes: 10 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Install packages
```sh
sudo apt update
sudo apt install qemu-kvm qemu-utils libvirt-daemon-system libvirt-dev mkisofs
curl -LsSf https://astral.sh/uv/install.sh | sh
source "$HOME"/.local/bin/env
uv tool install tox --with tox-uv
```

Add user to group
Expand All @@ -45,15 +48,11 @@ To install the `genesis-devtools` package, follow these steps:
cd genesis_devtools
```

3. Install the required dependencies:
```sh
pip install -r requirements.txt
```

4. Install the package using pip:
```sh
pip install .
```
3. Initialize virtual environment:
```bash
tox -e develop
source .tox/develop/bin/activate
```

# Quickstart

Expand Down Expand Up @@ -109,9 +108,7 @@ For every genesis project the directory `genesis` should exist in the project ro
├── my_project
│ └── main.py
├── project_settings.json
├── requirements.txt
├── setup.cfg
├── setup.py
├── pyproject.toml
└── README.md
```

Expand All @@ -125,9 +122,7 @@ The project should be extended as follows:
│ └── genesis.yaml
├── README.md
├── project_settings.json
├── requirements.txt
├── setup.cfg
├── setup.py
├── pyproject.toml
└── README.md
```

Expand Down
9 changes: 2 additions & 7 deletions genesis_devtools/backup/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ def validate_env(cls):
key = os.environ["GEN_DEV_BACKUP_KEY"]
iv = os.environ["GEN_DEV_BACKUP_IV"]

if (
cls.MIN_LEN <= len(key) <= cls.LEN
and cls.MIN_LEN <= len(iv) <= cls.LEN
):
if cls.MIN_LEN <= len(key) <= cls.LEN and cls.MIN_LEN <= len(iv) <= cls.LEN:
return

raise ValueError(
Expand Down Expand Up @@ -143,9 +140,7 @@ def _do_backup(

click.echo(f"Encrypting {compressed_backup_path}")
try:
utils.encrypt_file(
compressed_backup_path, encryption.key, encryption.iv
)
utils.encrypt_file(compressed_backup_path, encryption.key, encryption.iv)
except Exception:
click.secho(f"Encryption of {compressed_backup_path} failed", fg="red")
return
Expand Down
5 changes: 1 addition & 4 deletions genesis_devtools/backup/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ def validate_env(cls):
key = os.environ["GEN_DEV_BACKUP_KEY"]
iv = os.environ["GEN_DEV_BACKUP_IV"]

if (
cls.MIN_LEN <= len(key) <= cls.LEN
and cls.MIN_LEN <= len(iv) <= cls.LEN
):
if cls.MIN_LEN <= len(key) <= cls.LEN and cls.MIN_LEN <= len(iv) <= cls.LEN:
return

raise ValueError(
Expand Down
15 changes: 4 additions & 11 deletions genesis_devtools/backup/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@


class LocalQcowBackuper(qcow.AbstractQcowBackuper):

def __init__(
self,
backup_dir: str,
Expand Down Expand Up @@ -99,9 +98,7 @@ def backup_domain_snapshot(
snapshot_backup_path = os.path.join(
domain_backup_path, os.path.basename(snapshot_path)
)
self._save_file_to_backup(
snapshot_path, snapshot_backup_path, encryption
)
self._save_file_to_backup(snapshot_path, snapshot_backup_path, encryption)

def _do_backup(
self,
Expand Down Expand Up @@ -166,15 +163,12 @@ def _cleanup_after_failure(
if os.path.exists(snapshot_path):
# Find the original disk
for disk_format in ("raw", "qcow2"):
disk_path = (
".".join(disks[0].split(".")[:-1]) + disk_format
)
disk_path = ".".join(disks[0].split(".")[:-1]) + disk_format
if os.path.exists(disk_path):
break
else:
self._logger.error(
"The original disk hasn't been found for domain "
f"{domain}"
f"The original disk hasn't been found for domain {domain}"
)
continue

Expand All @@ -184,8 +178,7 @@ def _cleanup_after_failure(
libvirt.delete_snapshot(domain, self._snapshot_name)
except Exception:
self._logger.error(
f"Failed to merge snapshot {snapshot_path} "
f"for domain {domain}"
f"Failed to merge snapshot {snapshot_path} for domain {domain}"
)

def backup(
Expand Down
5 changes: 1 addition & 4 deletions genesis_devtools/backup/qcow.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@


class AbstractQcowBackuper(base.AbstractBackuper):

COMPRESS_SUFFIX = ".tar.gz"
ENCRYPTED_SUFFIX = ".encrypted"

Expand Down Expand Up @@ -97,9 +96,7 @@ def _backup_domain(
for i, disk in enumerate(disks):
device = "vd" + chr(ord("a") + i)
snapshot_path = self._snapshot_path(disk)
libvirt.merge_disk_snapshot(
domain, device, disk, snapshot_path
)
libvirt.merge_disk_snapshot(domain, device, disk, snapshot_path)

# Copy snapshot
self.backup_domain_snapshot(
Expand Down
5 changes: 1 addition & 4 deletions genesis_devtools/backup/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@


class S3QcowBackuper(qcow.AbstractQcowBackuper):

def __init__(
self,
endpoint_url: str,
Expand Down Expand Up @@ -62,9 +61,7 @@ def _upload_stream(
)

if encryption:
stream = utils.ReaderEncryptorIO(
stream, encryption.key, encryption.iv
)
stream = utils.ReaderEncryptorIO(stream, encryption.key, encryption.iv)
s3_path += self.ENCRYPTED_SUFFIX
s3_client.upload_fileobj(stream, self._bucket_name, s3_path)

Expand Down
2 changes: 1 addition & 1 deletion genesis_devtools/builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.

from genesis_devtools.builder import dependency
from genesis_devtools.builder import dependency as dependency
8 changes: 2 additions & 6 deletions genesis_devtools/builder/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class Image:
override: dict[str, tp.Any] | None = None

@classmethod
def from_config(
cls, image_config: tp.Dict[str, tp.Any], work_dir: str
) -> "Image":
def from_config(cls, image_config: tp.Dict[str, tp.Any], work_dir: str) -> "Image":
"""Create an image from configuration."""
script = image_config.pop("script")
if not os.path.isabs(script):
Expand Down Expand Up @@ -116,9 +114,7 @@ def load(cls, path: pathlib.Path) -> "ElementInventory":
"version": inventory["version"],
}
for category in cls.categories():
kwargs[category] = [
pathlib.Path(p) for p in inventory.get(category, [])
]
kwargs[category] = [pathlib.Path(p) for p in inventory.get(category, [])]

return cls(**kwargs)

Expand Down
15 changes: 4 additions & 11 deletions genesis_devtools/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ def _build_image(

# Determine images output directory
if inventory_mode:
images_output_dir = os.path.join(
self._elements_output_dir, "images"
)
images_output_dir = os.path.join(self._elements_output_dir, "images")
else:
images_output_dir = self._elements_output_dir

Expand Down Expand Up @@ -161,9 +159,7 @@ def build_element(
if not element.manifest:
raise ValueError("Element must have a manifest")

with open(
os.path.join(self._work_dir, element.manifest), "r"
) as f:
with open(os.path.join(self._work_dir, element.manifest), "r") as f:
manifest = yaml.safe_load(f)

version = build_suffix
Expand Down Expand Up @@ -263,16 +259,13 @@ def from_config(
for dep in dep_configs:
dep_item = base.AbstractDependency.find_dependency(dep, work_dir)
if dep_item is None:
raise ValueError(
f"Unable to handle dependency: {dep}. Unknown type."
)
raise ValueError(f"Unable to handle dependency: {dep}. Unknown type.")
deps.append(dep_item)

# Prepare elements
element_configs = build_config.get(cls.ELEMENT_KEY, [])
elements = [
base.Element.from_config(elem, work_dir)
for elem in element_configs
base.Element.from_config(elem, work_dir) for elem in element_configs
]

if not elements:
Expand Down
19 changes: 5 additions & 14 deletions genesis_devtools/builder/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ def _ignore_func(self, dirpath: str, names: list[str]) -> list[str]:
for pattern in self._exclude:
pattern = pattern.lstrip("/")
for name in names:
rel_path = os.path.relpath(
os.path.join(dirpath, name), self._path
)
rel_path = os.path.relpath(os.path.join(dirpath, name), self._path)
if fnmatch.fnmatch(rel_path, pattern):
ignored.add(name)
return list(ignored)
Expand All @@ -72,9 +70,7 @@ def fetch(self, output_dir: str) -> None:
path = path[:-1]
name = os.path.basename(path)
ignore_func = self._ignore_func if self._exclude else None
shutil.copytree(
path, os.path.join(output_dir, name), ignore=ignore_func
)
shutil.copytree(path, os.path.join(output_dir, name), ignore=ignore_func)
self._local_path = os.path.join(output_dir, name)
else:
shutil.copy(path, output_dir)
Expand Down Expand Up @@ -134,9 +130,7 @@ def fetch(self, output_dir: str) -> None:
path = os.environ.get(self._env_path)
if not path or not os.path.exists(path):
if not self._optional:
raise ValueError(
f"Environment variable {self._env_path} not found"
)
raise ValueError(f"Environment variable {self._env_path} not found")
return

self._path = path
Expand Down Expand Up @@ -235,9 +229,7 @@ def from_config(
class GitDependency(base.AbstractDependency):
"""Git dependency item."""

def __init__(
self, repo_url: str, img_dest: str, branch: str | None = None
) -> None:
def __init__(self, repo_url: str, img_dest: str, branch: str | None = None) -> None:
super().__init__()
self._repo_url = repo_url
self._branch = branch
Expand Down Expand Up @@ -280,8 +272,7 @@ def from_config(
"""Create a dependency item from configuration."""
if "git" not in dep_config or "src" not in dep_config["git"]:
raise ValueError(
"Git source not found in dependency "
f"configuration: {dep_config}"
f"Git source not found in dependency configuration: {dep_config}"
)

repo_url = dep_config["git"]["src"]
Expand Down
Loading