hysn-firecracker-python 1.0.5__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hysn_firecracker_python-1.0.5/hysn_firecracker_python.egg-info → hysn_firecracker_python-1.0.7}/PKG-INFO +2 -2
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/debian/control +1 -1
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/debian/copyright +2 -2
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/Dockerfile +1 -1
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/_version.py +3 -3
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/microvm.py +34 -2
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/network.py +87 -39
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/utils.py +14 -1
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7/hysn_firecracker_python.egg-info}/PKG-INFO +2 -2
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/pyproject.toml +1 -1
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/setup.py +1 -1
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_port_forward.py +67 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_utils.py +48 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/.github/workflows/pypi-release.yml +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/.gitignore +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/AGENTS.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/Dockerfile.test +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/FIRECRACKER_SETUP.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/LICENSE +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/Makefile +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/QUICKSTART.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/README.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/TODO.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/assets/img/firecracker.png +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/assets/rootfs/setup-firecracker-official.sh +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/cleanup_firecracker.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/debian/compat +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/debian/rules +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docker-compose.test.yml +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/README.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/api-reference.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/cleanup.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/configuration.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/examples.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/getting-started.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/index.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/network.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/docs/testing-in-docker.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/configure_vm_network.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/create_vm.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/create_vm_with_filesystem_format.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/id_rsa +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/id_rsa.pub +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/init +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/load_snapshot.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/sample.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/setup.sh +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/__init__.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/api.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/config.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/exceptions.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/logger.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/process.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/scripts.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/firecracker/vmm.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/hysn_firecracker_python.egg-info/SOURCES.txt +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/hysn_firecracker_python.egg-info/dependency_links.txt +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/hysn_firecracker_python.egg-info/entry_points.txt +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/hysn_firecracker_python.egg-info/requires.txt +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/hysn_firecracker_python.egg-info/top_level.txt +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/requirements.txt +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/scripts/cleanup_resources.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/scripts/run-tests-docker.sh +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/setup.cfg +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/README.md +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/conftest.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_api_client.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_cleanup.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_docker_integration.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_error_handling.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_logger.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_microvm.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_microvm_error_paths.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_microvm_initialization.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_network_error_paths.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_networking.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_process_manager.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_rootfs_format.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_scripts.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_snapshots.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_vm_configuration.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_vmm_manager.py +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/uv.lock +0 -0
- {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/verify-setup.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hysn-firecracker-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A Python client library to interact with Firecracker microVMs
|
|
5
|
-
Author:
|
|
5
|
+
Author: PDSO Platform
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
8
8
|
Copyright (c) 2024 Practical DevSecOps
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Source: firecracker-python
|
|
2
2
|
Section: python
|
|
3
3
|
Priority: optional
|
|
4
|
-
Maintainer:
|
|
4
|
+
Maintainer: PDSO Platform
|
|
5
5
|
Standards-Version: 4.6.2
|
|
6
6
|
Build-Depends: debhelper (>= 11), dh-python, python3-all, python3-setuptools, curl, jq
|
|
7
7
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
2
2
|
Upstream-Name: firecracker-python
|
|
3
|
-
Upstream-Contact:
|
|
3
|
+
Upstream-Contact: PDSO Platform
|
|
4
4
|
Source: https://github.com/myugan/firecracker-python
|
|
5
5
|
|
|
6
6
|
Files: *
|
|
7
|
-
Copyright: 2025
|
|
7
|
+
Copyright: 2025 PDSO Platform
|
|
8
8
|
License: MIT
|
|
9
9
|
|
|
10
10
|
License: MIT
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 7)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g3fc03cf51'
|
|
@@ -1541,6 +1541,8 @@ class MicroVM:
|
|
|
1541
1541
|
container_name = image.split("/")[-1].replace(":", "-")
|
|
1542
1542
|
tar_file = f"{self._config.data_path}/rootfs_{container_name}.tar"
|
|
1543
1543
|
|
|
1544
|
+
container = None
|
|
1545
|
+
merged_dir = None
|
|
1544
1546
|
try:
|
|
1545
1547
|
if not image:
|
|
1546
1548
|
raise VMMError(f"Failed to download Docker image {image}")
|
|
@@ -1549,6 +1551,21 @@ class MicroVM:
|
|
|
1549
1551
|
self._logger.debug(f"Creating container: {container_name}")
|
|
1550
1552
|
|
|
1551
1553
|
container = self._docker.containers.create(image, name=container_name)
|
|
1554
|
+
|
|
1555
|
+
# container.export() makes the daemon mount the container's overlay
|
|
1556
|
+
# rootfs (overlay2/<id>/merged), and container.remove() does not
|
|
1557
|
+
# unmount it. The orphaned mount pins the image layers, so a later
|
|
1558
|
+
# `docker image prune` reclaims 0B and disk fills up over repeated
|
|
1559
|
+
# builds. Capture the merged path now (populated at create time) so
|
|
1560
|
+
# we can unmount it explicitly in the finally block below.
|
|
1561
|
+
try:
|
|
1562
|
+
graph = self._docker.api.inspect_container(
|
|
1563
|
+
container.id
|
|
1564
|
+
).get("GraphDriver", {})
|
|
1565
|
+
merged_dir = graph.get("Data", {}).get("MergedDir")
|
|
1566
|
+
except Exception:
|
|
1567
|
+
merged_dir = None
|
|
1568
|
+
|
|
1552
1569
|
export_data = container.export()
|
|
1553
1570
|
|
|
1554
1571
|
if self._config.verbose:
|
|
@@ -1558,8 +1575,6 @@ class MicroVM:
|
|
|
1558
1575
|
for chunk in export_data:
|
|
1559
1576
|
f.write(chunk)
|
|
1560
1577
|
|
|
1561
|
-
container.remove(force=True)
|
|
1562
|
-
|
|
1563
1578
|
if self._config.verbose:
|
|
1564
1579
|
self._logger.debug(f"Successfully exported container to {tar_file}")
|
|
1565
1580
|
|
|
@@ -1569,6 +1584,23 @@ class MicroVM:
|
|
|
1569
1584
|
raise VMMError(f"Docker error: {e}")
|
|
1570
1585
|
except Exception as e:
|
|
1571
1586
|
raise VMMError(f"Unexpected error: {e}")
|
|
1587
|
+
finally:
|
|
1588
|
+
# Remove the container, then ensure its overlay rootfs is unmounted
|
|
1589
|
+
# so the image layers can actually be pruned/reclaimed afterwards.
|
|
1590
|
+
if container is not None:
|
|
1591
|
+
try:
|
|
1592
|
+
container.remove(force=True)
|
|
1593
|
+
except Exception as e:
|
|
1594
|
+
if self._config.verbose:
|
|
1595
|
+
self._logger.debug(
|
|
1596
|
+
f"Failed to remove container {container_name}: {e}"
|
|
1597
|
+
)
|
|
1598
|
+
if merged_dir and os.path.ismount(merged_dir):
|
|
1599
|
+
# Plain unmount first; lazy unmount if briefly busy after teardown.
|
|
1600
|
+
if run(f"umount {merged_dir}").returncode != 0:
|
|
1601
|
+
run(f"umount -l {merged_dir}")
|
|
1602
|
+
if self._config.verbose:
|
|
1603
|
+
self._logger.debug(f"Unmounted leaked overlay: {merged_dir}")
|
|
1572
1604
|
|
|
1573
1605
|
def _build_rootfs(self, image: str, file: str, size: str, fs_format: str = "ext4"):
|
|
1574
1606
|
"""Create a filesystem image from a tar file.
|
|
@@ -497,6 +497,18 @@ class NetworkManager:
|
|
|
497
497
|
has_correct_dnat = False
|
|
498
498
|
|
|
499
499
|
for e in expr:
|
|
500
|
+
# Current form: `fib daddr type local` (no host-IP match).
|
|
501
|
+
if (
|
|
502
|
+
"match" in e
|
|
503
|
+
and e["match"]["op"] == "=="
|
|
504
|
+
and "fib" in e["match"].get("left", {})
|
|
505
|
+
and "daddr"
|
|
506
|
+
in e["match"]["left"]["fib"].get("flags", [])
|
|
507
|
+
and e["match"]["right"] == "local"
|
|
508
|
+
):
|
|
509
|
+
has_daddr_match = True
|
|
510
|
+
|
|
511
|
+
# Legacy form: exact `ip daddr <host_ip>` payload match.
|
|
500
512
|
if (
|
|
501
513
|
"match" in e
|
|
502
514
|
and e["match"]["op"] == "=="
|
|
@@ -684,6 +696,71 @@ class NetworkManager:
|
|
|
684
696
|
)
|
|
685
697
|
return False
|
|
686
698
|
|
|
699
|
+
def _build_prerouting_rule(
|
|
700
|
+
self,
|
|
701
|
+
id: str,
|
|
702
|
+
family: str,
|
|
703
|
+
host_port: int,
|
|
704
|
+
dest_ip: str,
|
|
705
|
+
dest_port: int,
|
|
706
|
+
protocol: str = "tcp",
|
|
707
|
+
):
|
|
708
|
+
"""Build the PREROUTING DNAT rule for a port forward.
|
|
709
|
+
|
|
710
|
+
The destination-address match uses ``fib daddr type local`` instead of a
|
|
711
|
+
single detected host IP. This matches any address the host owns, on every
|
|
712
|
+
provider, with zero host-IP detection -- fixing the ``ip daddr 0.0.0.0``
|
|
713
|
+
bug where a failed/empty public-IP lookup produced a rule that never fired.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
id (str): Machine ID, used in the rule comment.
|
|
717
|
+
family (str): nftables family ("ip" or "ip6").
|
|
718
|
+
host_port (int): Host port to forward.
|
|
719
|
+
dest_ip (str): IP address to forward to.
|
|
720
|
+
dest_port (int): Port to forward to.
|
|
721
|
+
protocol (str): Protocol to forward (default: "tcp").
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
dict: An nftables ``add`` rule object for the PREROUTING chain.
|
|
725
|
+
"""
|
|
726
|
+
return {
|
|
727
|
+
"add": {
|
|
728
|
+
"rule": {
|
|
729
|
+
"family": family,
|
|
730
|
+
"table": "nat",
|
|
731
|
+
"chain": "PREROUTING",
|
|
732
|
+
"comment": f"machine_id={id} host_port={host_port} vm_port={dest_port}",
|
|
733
|
+
"expr": [
|
|
734
|
+
{
|
|
735
|
+
"match": {
|
|
736
|
+
"op": "==",
|
|
737
|
+
"left": {
|
|
738
|
+
"fib": {
|
|
739
|
+
"result": "type",
|
|
740
|
+
"flags": ["daddr"],
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
"right": "local",
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
"match": {
|
|
748
|
+
"op": "==",
|
|
749
|
+
"left": {
|
|
750
|
+
"payload": {
|
|
751
|
+
"protocol": protocol,
|
|
752
|
+
"field": "dport",
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
"right": host_port,
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
{"dnat": {"addr": dest_ip, "port": dest_port}},
|
|
759
|
+
],
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
687
764
|
def add_port_forward(
|
|
688
765
|
self,
|
|
689
766
|
id: str,
|
|
@@ -695,8 +772,13 @@ class NetworkManager:
|
|
|
695
772
|
):
|
|
696
773
|
"""Port forward a port to a new IP and port.
|
|
697
774
|
|
|
775
|
+
The PREROUTING rule matches ``fib daddr type local`` (any address the host
|
|
776
|
+
owns), so ``host_ip`` is used only to detect the IP family (IPv4 vs IPv6);
|
|
777
|
+
its value no longer appears in the rule match. This avoids the
|
|
778
|
+
``ip daddr 0.0.0.0`` bug on hosts where public-IP detection fails.
|
|
779
|
+
|
|
698
780
|
Args:
|
|
699
|
-
host_ip (str): IP address to
|
|
781
|
+
host_ip (str): IP address used only to detect IPv4/IPv6 family.
|
|
700
782
|
host_port (int): Port to forward.
|
|
701
783
|
dest_ip (str): IP address to forward to.
|
|
702
784
|
dest_port (int): Port to forward to.
|
|
@@ -767,45 +849,11 @@ class NetworkManager:
|
|
|
767
849
|
}
|
|
768
850
|
)
|
|
769
851
|
|
|
770
|
-
# Add PREROUTING rule
|
|
852
|
+
# Add PREROUTING rule (fib daddr type local -- no host-IP detection)
|
|
771
853
|
rules["nftables"].append(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
"family": family,
|
|
776
|
-
"table": "nat",
|
|
777
|
-
"chain": "PREROUTING",
|
|
778
|
-
"comment": f"machine_id={id} host_port={host_port} vm_port={dest_port}",
|
|
779
|
-
"expr": [
|
|
780
|
-
{
|
|
781
|
-
"match": {
|
|
782
|
-
"op": "==",
|
|
783
|
-
"left": {
|
|
784
|
-
"payload": {
|
|
785
|
-
"protocol": family,
|
|
786
|
-
"field": "daddr",
|
|
787
|
-
}
|
|
788
|
-
},
|
|
789
|
-
"right": host_ip,
|
|
790
|
-
}
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
"match": {
|
|
794
|
-
"op": "==",
|
|
795
|
-
"left": {
|
|
796
|
-
"payload": {
|
|
797
|
-
"protocol": protocol,
|
|
798
|
-
"field": "dport",
|
|
799
|
-
}
|
|
800
|
-
},
|
|
801
|
-
"right": host_port,
|
|
802
|
-
}
|
|
803
|
-
},
|
|
804
|
-
{"dnat": {"addr": dest_ip, "port": dest_port}},
|
|
805
|
-
],
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
854
|
+
self._build_prerouting_rule(
|
|
855
|
+
id, family, host_port, dest_ip, dest_port, protocol
|
|
856
|
+
)
|
|
809
857
|
)
|
|
810
858
|
|
|
811
859
|
# Only add POSTROUTING rule if it doesn't already exist
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import random
|
|
3
3
|
import string
|
|
4
4
|
import signal
|
|
5
|
+
import ipaddress
|
|
5
6
|
import requests
|
|
6
7
|
import subprocess
|
|
7
8
|
import socket
|
|
@@ -185,8 +186,20 @@ def get_public_ip(timeout: int = 5):
|
|
|
185
186
|
|
|
186
187
|
for url in URLS:
|
|
187
188
|
try:
|
|
188
|
-
|
|
189
|
+
ip = _try_get_ip_from_url(url, timeout)
|
|
189
190
|
except requests.RequestException:
|
|
190
191
|
continue
|
|
191
192
|
|
|
193
|
+
# Reject empty/garbage and unspecified (0.0.0.0 / ::) responses so a bad
|
|
194
|
+
# service can never silently produce a dead "daddr 0.0.0.0" forward rule.
|
|
195
|
+
try:
|
|
196
|
+
ip_obj = ipaddress.ip_address(ip)
|
|
197
|
+
except ValueError:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if ip_obj.is_unspecified:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
return str(ip_obj)
|
|
204
|
+
|
|
192
205
|
raise RuntimeError("Failed to get public IP")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hysn-firecracker-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A Python client library to interact with Firecracker microVMs
|
|
5
|
-
Author:
|
|
5
|
+
Author: PDSO Platform
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
8
8
|
Copyright (c) 2024 Practical DevSecOps
|
|
@@ -7,7 +7,7 @@ name = "hysn-firecracker-python"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "A Python client library to interact with Firecracker microVMs"
|
|
9
9
|
authors = [
|
|
10
|
-
{name = "
|
|
10
|
+
{name = "PDSO Platform"}
|
|
11
11
|
]
|
|
12
12
|
requires-python = ">=3.9"
|
|
13
13
|
dependencies = [
|
|
@@ -4,7 +4,7 @@ from setuptools import setup
|
|
|
4
4
|
setup(
|
|
5
5
|
name="hysn-firecracker-python",
|
|
6
6
|
description="A Python client library to interact with Firecracker microVMs",
|
|
7
|
-
author="
|
|
7
|
+
author="PDSO Platform",
|
|
8
8
|
packages=find_packages(),
|
|
9
9
|
install_requires=[
|
|
10
10
|
"requests==2.32.3",
|
|
@@ -2,10 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
from unittest.mock import patch
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
7
8
|
|
|
8
9
|
from firecracker import MicroVM
|
|
10
|
+
from firecracker.network import NetworkManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestPreroutingRuleBuild:
|
|
14
|
+
"""Unit tests for the PREROUTING DNAT rule builder (no nftables/KVM needed).
|
|
15
|
+
|
|
16
|
+
Guards the `ip daddr 0.0.0.0` regression: the rule must match
|
|
17
|
+
`fib daddr type local`, never an exact host-IP daddr payload.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def _build(self, **kwargs):
|
|
21
|
+
with patch("firecracker.network.IPRoute"):
|
|
22
|
+
nm = NetworkManager()
|
|
23
|
+
defaults = dict(
|
|
24
|
+
id="9fngkuf7",
|
|
25
|
+
family="ip",
|
|
26
|
+
host_port=37699,
|
|
27
|
+
dest_ip="172.115.48.209",
|
|
28
|
+
dest_port=22,
|
|
29
|
+
protocol="tcp",
|
|
30
|
+
)
|
|
31
|
+
defaults.update(kwargs)
|
|
32
|
+
return nm._build_prerouting_rule(**defaults)
|
|
33
|
+
|
|
34
|
+
def test_uses_fib_daddr_type_local(self):
|
|
35
|
+
"""Rule matches any local address via fib, not a single host IP."""
|
|
36
|
+
expr = self._build()["add"]["rule"]["expr"]
|
|
37
|
+
fib_matches = [
|
|
38
|
+
e
|
|
39
|
+
for e in expr
|
|
40
|
+
if "match" in e and "fib" in e["match"].get("left", {})
|
|
41
|
+
]
|
|
42
|
+
assert len(fib_matches) == 1
|
|
43
|
+
m = fib_matches[0]["match"]
|
|
44
|
+
assert m["op"] == "=="
|
|
45
|
+
assert m["left"]["fib"]["result"] == "type"
|
|
46
|
+
assert "daddr" in m["left"]["fib"]["flags"]
|
|
47
|
+
assert m["right"] == "local"
|
|
48
|
+
|
|
49
|
+
def test_no_exact_daddr_payload_match(self):
|
|
50
|
+
"""Regression: no `ip daddr <addr>` payload match remains in the rule."""
|
|
51
|
+
expr = self._build()["add"]["rule"]["expr"]
|
|
52
|
+
for e in expr:
|
|
53
|
+
left = e.get("match", {}).get("left", {})
|
|
54
|
+
if "payload" in left:
|
|
55
|
+
assert left["payload"]["field"] != "daddr"
|
|
56
|
+
|
|
57
|
+
def test_dport_and_dnat_preserved(self):
|
|
58
|
+
"""dport match and dnat target unchanged."""
|
|
59
|
+
expr = self._build()["add"]["rule"]["expr"]
|
|
60
|
+
dport = next(
|
|
61
|
+
e["match"]
|
|
62
|
+
for e in expr
|
|
63
|
+
if "match" in e
|
|
64
|
+
and e["match"]["left"].get("payload", {}).get("field") == "dport"
|
|
65
|
+
)
|
|
66
|
+
assert dport["right"] == 37699
|
|
67
|
+
dnat = next(e["dnat"] for e in expr if "dnat" in e)
|
|
68
|
+
assert dnat == {"addr": "172.115.48.209", "port": 22}
|
|
69
|
+
|
|
70
|
+
def test_comment_format_unchanged(self):
|
|
71
|
+
"""Comment key is unchanged -- deletion/idempotency paths depend on it."""
|
|
72
|
+
rule = self._build()["add"]["rule"]
|
|
73
|
+
assert rule["comment"] == "machine_id=9fngkuf7 host_port=37699 vm_port=22"
|
|
74
|
+
assert rule["chain"] == "PREROUTING"
|
|
75
|
+
assert rule["table"] == "nat"
|
|
9
76
|
|
|
10
77
|
KERNEL_FILE = "/var/lib/firecracker/vmlinux-6.1.159"
|
|
11
78
|
BASE_ROOTFS = "/var/lib/firecracker/devsecops-box.img"
|
|
@@ -167,3 +167,51 @@ class TestUtilsFunctions:
|
|
|
167
167
|
|
|
168
168
|
with pytest.raises(RuntimeError, match="VMM ID required"):
|
|
169
169
|
test_func(None)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestGetPublicIP:
|
|
173
|
+
"""Tests for get_public_ip validation (never yield 0.0.0.0/empty)."""
|
|
174
|
+
|
|
175
|
+
def test_returns_valid_ip(self):
|
|
176
|
+
"""A valid IP from the first service is returned."""
|
|
177
|
+
from unittest.mock import patch
|
|
178
|
+
from firecracker.utils import get_public_ip
|
|
179
|
+
|
|
180
|
+
with patch(
|
|
181
|
+
"firecracker.utils._try_get_ip_from_url", return_value="138.201.226.62"
|
|
182
|
+
):
|
|
183
|
+
assert get_public_ip() == "138.201.226.62"
|
|
184
|
+
|
|
185
|
+
def test_skips_unspecified_then_returns_valid(self):
|
|
186
|
+
"""0.0.0.0 from one service is skipped; next valid IP wins."""
|
|
187
|
+
from unittest.mock import patch
|
|
188
|
+
from firecracker.utils import get_public_ip
|
|
189
|
+
|
|
190
|
+
with patch(
|
|
191
|
+
"firecracker.utils._try_get_ip_from_url",
|
|
192
|
+
side_effect=["0.0.0.0", "203.0.113.5", "203.0.113.9"],
|
|
193
|
+
):
|
|
194
|
+
assert get_public_ip() == "203.0.113.5"
|
|
195
|
+
|
|
196
|
+
def test_skips_empty_and_garbage(self):
|
|
197
|
+
"""Empty/garbage responses are skipped, not returned."""
|
|
198
|
+
from unittest.mock import patch
|
|
199
|
+
from firecracker.utils import get_public_ip
|
|
200
|
+
|
|
201
|
+
with patch(
|
|
202
|
+
"firecracker.utils._try_get_ip_from_url",
|
|
203
|
+
side_effect=["", "not-an-ip", "203.0.113.7"],
|
|
204
|
+
):
|
|
205
|
+
assert get_public_ip() == "203.0.113.7"
|
|
206
|
+
|
|
207
|
+
def test_raises_when_all_unspecified_or_invalid(self):
|
|
208
|
+
"""All services returning 0.0.0.0/garbage -> RuntimeError, never 0.0.0.0."""
|
|
209
|
+
from unittest.mock import patch
|
|
210
|
+
from firecracker.utils import get_public_ip
|
|
211
|
+
|
|
212
|
+
with patch(
|
|
213
|
+
"firecracker.utils._try_get_ip_from_url",
|
|
214
|
+
side_effect=["0.0.0.0", "", "::"],
|
|
215
|
+
):
|
|
216
|
+
with pytest.raises(RuntimeError, match="Failed to get public IP"):
|
|
217
|
+
get_public_ip()
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/.github/ISSUE_TEMPLATE/bug_report.md
RENAMED
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/.github/workflows/pypi-release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/examples/configure_vm_network.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/scripts/cleanup_resources.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_docker_integration.py
RENAMED
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_error_handling.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_microvm_error_paths.py
RENAMED
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_microvm_initialization.py
RENAMED
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_network_error_paths.py
RENAMED
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_process_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.7}/tests/test_vm_configuration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|