feat: Add storage management for VM creation and resizing
- Introduced storage parameter in VmCreateRequest and VmResizeRequest models. - Implemented storage validation to ensure minimum and maximum limits. - Enhanced CLI to support storage options during VM creation and resizing. - Updated lifecycle management to handle pending resize operations for VM resources. - Added methods for resizing VM disks and validating storage capacity. - Updated API endpoints to accommodate new storage functionality. - Refactored related validation and configuration logic to support storage management.
This commit is contained in:
+31
-19
@@ -161,8 +161,8 @@ class FirewallManager:
|
||||
address = str(address)
|
||||
for field in ("--orig-src", "--orig-dst", "--reply-src", "--reply-dst"):
|
||||
selector = ("--family", "ipv4", field, address)
|
||||
self._run(("conntrack", "--delete", *selector), check=False)
|
||||
remaining = self._run(("conntrack", "--list", *selector))
|
||||
self._run(("conntrack", "-D", *selector), check=False)
|
||||
remaining = self._run(("conntrack", "-L", *selector))
|
||||
if remaining.stdout.strip():
|
||||
raise UvmError(f"conntrack entries remain for {address}; do not reuse its IP")
|
||||
|
||||
@@ -408,8 +408,9 @@ class FirewallManager:
|
||||
def _normalize(objects: list[dict[str, Any]]) -> str:
|
||||
"""Canonical nft JSON, retaining every effective object, rule and verdict.
|
||||
|
||||
Ignore only handles, empty set presentation and nft's redundant protocol
|
||||
dependencies. Rule order is significant; pure conjunctions and sets are not.
|
||||
Normalize handles, empty set presentation and implied protocol dependencies
|
||||
without losing address-family constraints. Rule order is significant;
|
||||
pure conjunctions and sets are not.
|
||||
Unknown expressions/properties remain different rather than being ignored.
|
||||
"""
|
||||
def canonical(value: Any) -> Any:
|
||||
@@ -442,6 +443,18 @@ def _normalize(objects: list[dict[str, Any]]) -> str:
|
||||
return result
|
||||
return value
|
||||
|
||||
def payload_protocols(value: Any) -> set[str]:
|
||||
if not isinstance(value, dict):
|
||||
return set()
|
||||
payload = value.get("payload")
|
||||
if (set(value) == {"payload"} and isinstance(payload, dict)
|
||||
and set(payload) == {"protocol", "field"}
|
||||
and isinstance(payload["protocol"], str) and isinstance(payload["field"], str)):
|
||||
return {payload["protocol"]}
|
||||
if set(value) == {"&"} and isinstance(value["&"], list) and len(value["&"]) == 2:
|
||||
return payload_protocols(value["&"][0]) | payload_protocols(value["&"][1])
|
||||
return set()
|
||||
|
||||
definitions, rules = [], {}
|
||||
for item in objects:
|
||||
kind, raw = next(iter(item.items()))
|
||||
@@ -490,25 +503,24 @@ def _normalize(objects: list[dict[str, Any]]) -> str:
|
||||
left["&"][1] = bitmask(left["&"][1], tcp_flags)
|
||||
match["right"] = bitmask(match["right"], tcp_flags)
|
||||
|
||||
# nft's replayable JSON omits dependencies implied by named payloads.
|
||||
encoded = json.dumps(expressions, sort_keys=True)
|
||||
has_tcp = '"protocol": "tcp"' in encoded
|
||||
has_icmp = '"protocol": "icmp"' in encoded
|
||||
expressions = [expr for expr in expressions if not (
|
||||
(has_tcp and expr == _match(_payload("ip", "protocol"), "tcp"))
|
||||
or (has_icmp and expr == _match(_payload("ip", "protocol"), "icmp"))
|
||||
)]
|
||||
encoded = json.dumps(expressions, sort_keys=True)
|
||||
expressions = [expr for expr in expressions if not (
|
||||
('"protocol": "arp"' in encoded and expr == _match(_payload("ether", "type"), "arp"))
|
||||
or (('"protocol": "ip"' in encoded or has_icmp)
|
||||
and expr == _match(_payload("ether", "type"), "ip"))
|
||||
)]
|
||||
count = len(expressions)
|
||||
if expressions and (not isinstance(expressions[-1], dict) or set(expressions[-1]) != {"match"}):
|
||||
count -= 1
|
||||
if all(isinstance(expr, dict) and set(expr) == {"match"} for expr in expressions[:count]):
|
||||
expressions = sorted(expressions[:count], key=lambda expr: json.dumps(expr, sort_keys=True)) + expressions[count:]
|
||||
matches = expressions[:count]
|
||||
protocols = set().union(*(payload_protocols(expr["match"].get("left")) for expr in matches))
|
||||
# nft elides ether type ip before ip protocol tcp. Materialize its
|
||||
# IPv4 constraint BEFORE folding the transport dependency: a TCP
|
||||
# payload alone can also match IPv6 in a bridge table.
|
||||
for payloads, ethertype in (({"ip", "icmp"}, "ip"), ({"arp"}, "arp")):
|
||||
dependency = _match(_payload("ether", "type"), ethertype)
|
||||
if protocols & payloads and dependency not in matches:
|
||||
matches.append(dependency)
|
||||
matches = [expr for expr in matches if not (
|
||||
("tcp" in protocols and expr == _match(_payload("ip", "protocol"), "tcp"))
|
||||
or ("icmp" in protocols and expr == _match(_payload("ip", "protocol"), "icmp"))
|
||||
)]
|
||||
expressions = sorted(matches, key=lambda expr: json.dumps(expr, sort_keys=True)) + expressions[count:]
|
||||
obj["expr"] = expressions
|
||||
rules.setdefault(obj.get("chain", ""), []).append(obj)
|
||||
definitions.sort(key=lambda item: json.dumps(item, sort_keys=True))
|
||||
|
||||
Reference in New Issue
Block a user