hysn-firecracker-python 1.0.5__tar.gz → 1.0.6__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.
Files changed (86) hide show
  1. {hysn_firecracker_python-1.0.5/hysn_firecracker_python.egg-info → hysn_firecracker_python-1.0.6}/PKG-INFO +1 -1
  2. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/_version.py +3 -3
  3. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/network.py +87 -39
  4. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/utils.py +14 -1
  5. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6/hysn_firecracker_python.egg-info}/PKG-INFO +1 -1
  6. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_port_forward.py +67 -0
  7. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_utils.py +48 -0
  8. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  9. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  10. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/.github/workflows/pypi-release.yml +0 -0
  11. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/.gitignore +0 -0
  12. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/AGENTS.md +0 -0
  13. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/Dockerfile.test +0 -0
  14. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/FIRECRACKER_SETUP.md +0 -0
  15. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/LICENSE +0 -0
  16. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/Makefile +0 -0
  17. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/QUICKSTART.md +0 -0
  18. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/README.md +0 -0
  19. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/TODO.md +0 -0
  20. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/assets/img/firecracker.png +0 -0
  21. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/assets/rootfs/setup-firecracker-official.sh +0 -0
  22. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/cleanup_firecracker.py +0 -0
  23. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/debian/compat +0 -0
  24. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/debian/control +0 -0
  25. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/debian/copyright +0 -0
  26. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/debian/rules +0 -0
  27. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docker-compose.test.yml +0 -0
  28. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/README.md +0 -0
  29. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/api-reference.md +0 -0
  30. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/cleanup.md +0 -0
  31. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/configuration.md +0 -0
  32. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/examples.md +0 -0
  33. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/getting-started.md +0 -0
  34. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/index.md +0 -0
  35. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/network.md +0 -0
  36. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/docs/testing-in-docker.md +0 -0
  37. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/Dockerfile +0 -0
  38. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/configure_vm_network.py +0 -0
  39. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/create_vm.py +0 -0
  40. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/create_vm_with_filesystem_format.py +0 -0
  41. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/id_rsa +0 -0
  42. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/id_rsa.pub +0 -0
  43. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/init +0 -0
  44. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/load_snapshot.py +0 -0
  45. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/sample.py +0 -0
  46. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/examples/setup.sh +0 -0
  47. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/__init__.py +0 -0
  48. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/api.py +0 -0
  49. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/config.py +0 -0
  50. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/exceptions.py +0 -0
  51. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/logger.py +0 -0
  52. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/microvm.py +0 -0
  53. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/process.py +0 -0
  54. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/scripts.py +0 -0
  55. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/firecracker/vmm.py +0 -0
  56. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/hysn_firecracker_python.egg-info/SOURCES.txt +0 -0
  57. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/hysn_firecracker_python.egg-info/dependency_links.txt +0 -0
  58. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/hysn_firecracker_python.egg-info/entry_points.txt +0 -0
  59. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/hysn_firecracker_python.egg-info/requires.txt +0 -0
  60. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/hysn_firecracker_python.egg-info/top_level.txt +0 -0
  61. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/pyproject.toml +0 -0
  62. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/requirements.txt +0 -0
  63. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/scripts/cleanup_resources.py +0 -0
  64. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/scripts/run-tests-docker.sh +0 -0
  65. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/setup.cfg +0 -0
  66. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/setup.py +0 -0
  67. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/README.md +0 -0
  68. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/conftest.py +0 -0
  69. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_api_client.py +0 -0
  70. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_cleanup.py +0 -0
  71. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_docker_integration.py +0 -0
  72. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_error_handling.py +0 -0
  73. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_logger.py +0 -0
  74. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_microvm.py +0 -0
  75. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_microvm_error_paths.py +0 -0
  76. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_microvm_initialization.py +0 -0
  77. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_network_error_paths.py +0 -0
  78. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_networking.py +0 -0
  79. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_process_manager.py +0 -0
  80. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_rootfs_format.py +0 -0
  81. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_scripts.py +0 -0
  82. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_snapshots.py +0 -0
  83. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_vm_configuration.py +0 -0
  84. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/tests/test_vmm_manager.py +0 -0
  85. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/uv.lock +0 -0
  86. {hysn_firecracker_python-1.0.5 → hysn_firecracker_python-1.0.6}/verify-setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hysn-firecracker-python
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: A Python client library to interact with Firecracker microVMs
5
5
  Author: Muhammad Yuga Nugraha
6
6
  License: MIT License
@@ -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.5'
22
- __version_tuple__ = version_tuple = (1, 0, 5)
21
+ __version__ = version = '1.0.6'
22
+ __version_tuple__ = version_tuple = (1, 0, 6)
23
23
 
24
- __commit_id__ = commit_id = 'g01bf110fc'
24
+ __commit_id__ = commit_id = 'g3b21589ba'
@@ -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 forward from.
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
- "add": {
774
- "rule": {
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
- return _try_get_ip_from_url(url, timeout)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hysn-firecracker-python
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: A Python client library to interact with Firecracker microVMs
5
5
  Author: Muhammad Yuga Nugraha
6
6
  License: MIT License
@@ -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()