This is an automated email from the ASF dual-hosted git repository.

DaanHoogland pushed a commit to branch vmnet-network-extension
in repository https://gitbox.apache.org/repos/asf/cloudstack-extensions.git

commit 8f3fedad57d74e8251ed29bb93cd6df577cbaa45
Author: Daan Hoogland <[email protected]>
AuthorDate: Thu Oct 8 18:15:41 2026 +0200

    vmnet: initial version for containers
---
 vmnet/README.txt    | 128 +++++++++++++++++++++++++++++++++++++++++++
 vmnet/test_vmnet.py |  74 +++++++++++++++++++++++++
 vmnet/vmnet.py      | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 355 insertions(+)

diff --git a/vmnet/README.txt b/vmnet/README.txt
new file mode 100644
index 0000000..fd11732
--- /dev/null
+++ b/vmnet/README.txt
@@ -0,0 +1,128 @@
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+
+vmnet Network Extension
+=======================
+
+NetworkOrchestrator extension that gives CloudStack L2 networks to the
+orchestrator extensions running instances on Macs, as macOS vmnet
+networks. Each CloudStack network becomes its own vmnet network, so
+instances on different CloudStack networks cannot reach each other.
+
+The networks are local to the Macs, so this extension only names them.
+When CloudStack implements a network, the script returns the vmnet network
+name and mode, which CloudStack stores as the network's broadcast URI:
+
+  vs://cs-net-<network id>?mode=<nat|internal>
+
+Every NIC of an instance carries that URI. The orchestrator extension
+running the instance reads it, creates the vmnet network on the Mac the
+first time an instance needs it, attaches the NIC to it, and deletes the
+network with its last instance.
+
+Orchestrator extensions that read these URIs:
+  apple-container   container networks ("container network create")
+
+Requirements
+------------
+
+Management server:
+  - python3, the script uses only the standard library
+  - CloudStack with NetworkOrchestrator extensions
+
+Mac: whatever the orchestrator extension needs; for apple-container, macOS
+26 or later, which can create vmnet networks with their own subnet.
+
+Supported networks and services
+-------------------------------
+
+  - L2 networks only. The vmnet networks hand out their own IP addresses
+    with DHCP, so CloudStack cannot manage the addresses of Isolated or
+    Shared networks; implementing such a network fails.
+  - Modes: nat, where instances reach the outside world through the Mac,
+    and internal (host-only), where they reach only the Mac and each other.
+  - UserData is declared but not delivered yet. CloudStack only uses an
+    extension for networks whose offering names it as a service provider,
+    and L2 offerings may only have the UserData service. All commands
+    besides ensure-network-device and implement-network are no-ops.
+
+Configuration details
+---------------------
+
+Physical network registration details (registerExtension):
+  mode          Optional, nat (default) or internal
+  name_prefix   Optional, prefix of the vmnet network names, default
+                cs-net-. Use another prefix per CloudStack installation when
+                several share a Mac.
+
+Setup
+-----
+
+1. Copy vmnet.py to every management server:
+
+     mkdir -p /usr/share/cloudstack-management/extensions/vmnet
+     cp vmnet.py /usr/share/cloudstack-management/extensions/vmnet/vmnet.py
+     chmod 755 /usr/share/cloudstack-management/extensions/vmnet/vmnet.py
+     chown -R cloud:cloud /usr/share/cloudstack-management/extensions/vmnet
+
+   The unit tests run with: python3 -m unittest test_vmnet.py
+
+2. Create the extension:
+
+     cmk create extension name=vmnet type=NetworkOrchestrator path=vmnet.py \
+         "details[0].network.services=UserData" \
+         "details[0].network.service.capabilities={}" \
+         "details[0].network.isolation.method=NetworkExtension" \
+         "details[0].network.allocate.extension.ip=false"
+
+   network.isolation.method=NetworkExtension makes CloudStack store the
+   broadcast URI the script returns, instead of allocating a VLAN.
+
+3. Register it to the zone's physical network; this also adds the vmnet
+   network service provider, enabled:
+
+     cmk register extension extensionid=<extension id> \
+         resourcetype=PhysicalNetwork resourceid=<physical network id> \
+         "details[0].mode=nat"
+
+4. Create and enable an L2 network offering with UserData from vmnet:
+
+     cmk create networkoffering name=vmnet-l2 displaytext="L2 on vmnet" \
+         guestiptype=L2 traffictype=GUEST supportedservices=UserData \
+         "serviceproviderlist[0].service=UserData" \
+         "serviceproviderlist[0].provider=vmnet"
+     cmk update networkoffering id=<offering id> state=Enabled
+
+5. Create networks from that offering and deploy instances on them to
+   clusters of an orchestrator extension that supports vmnet networks.
+
+Limitations
+-----------
+
+  - CloudStack does not know the instances' IP addresses; use the
+    orchestrator extension's GetIpAddresses custom action.
+  - The mode applies to the whole physical network. Changing it affects
+    networks created on the Mac afterwards; an existing network keeps its
+    mode until its last instance is deleted.
+  - A vmnet network exists per Mac: instances of one CloudStack network on
+    different Macs are on different segments.
+  - No user data, DHCP reservations, port forwarding or other services.
+  - Tested on macOS 26.6 with Apple container 1.5.0 and a local management
+    server: two instances on one internal network reach each other, not
+    the internet, and are not reachable from containers on other networks;
+    the network is created with the first instance, created again when it
+    was removed, and deleted with the last instance.
diff --git a/vmnet/test_vmnet.py b/vmnet/test_vmnet.py
new file mode 100644
index 0000000..75a2039
--- /dev/null
+++ b/vmnet/test_vmnet.py
@@ -0,0 +1,74 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Unit tests for vmnet.py, run with: python3 -m unittest test_vmnet.py"""
+
+import json
+import os
+import tempfile
+import unittest
+
+import vmnet
+
+
+class VmnetTest(unittest.TestCase):
+
+    def run_command(self, command, data):
+        with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as 
fh:
+            json.dump(data, fh)
+        try:
+            return vmnet.execute(command, fh.name)
+        finally:
+            os.unlink(fh.name)
+
+    def test_implements_l2_network_with_default_mode(self):
+        output = self.run_command("implement-network", {
+            "physical-network-extension-details": {},
+            "payload": {"network_id": 42, "guest_type": "l2"},
+        })
+        self.assertEqual(output, {"network.broadcast_domain_type": "Vswitch",
+                                  "network.broadcast_uri": 
"vs://cs-net-42?mode=nat"})
+
+    def test_reads_registration_details_given_as_json_string(self):
+        output = self.run_command("implement-network", {
+            "physical-network-extension-details": json.dumps({"mode": 
"Internal", "name_prefix": "lab-"}),
+            "payload": {"network_id": "7", "guest_type": "l2"},
+        })
+        self.assertEqual(output["network.broadcast_uri"], 
"vs://lab-7?mode=internal")
+
+    def test_rejects_networks_with_cloudstack_managed_addresses(self):
+        with self.assertRaisesRegex(vmnet.VmnetError, "only L2"):
+            self.run_command("implement-network", {"payload": {"network_id": 
1, "guest_type": "isolated"}})
+
+    def test_rejects_invalid_settings(self):
+        with self.assertRaisesRegex(vmnet.VmnetError, "Invalid mode"):
+            vmnet.network_settings({"physical-network-extension-details": 
{"mode": "bridged"}})
+        with self.assertRaisesRegex(vmnet.VmnetError, "Invalid name_prefix"):
+            vmnet.network_settings({"physical-network-extension-details": 
{"name_prefix": "a_b"}})
+        with self.assertRaisesRegex(vmnet.VmnetError, "network_id"):
+            vmnet.network_name("cs-net-", "")
+
+    def test_other_commands(self):
+        self.assertEqual(self.run_command("ensure-network-device", {"payload": 
{}}), {})
+        self.assertIsNone(self.run_command("destroy-network", {"payload": 
{"network_id": 1}}))
+        self.assertIsNone(self.run_command("save-userdata", {"payload": 
{"network_id": 1}}))
+        with self.assertRaisesRegex(vmnet.VmnetError, "Unsupported command"):
+            vmnet.execute("custom-action", "/nonexistent")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/vmnet/vmnet.py b/vmnet/vmnet.py
new file mode 100755
index 0000000..4a11ac8
--- /dev/null
+++ b/vmnet/vmnet.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+CloudStack NetworkOrchestrator extension for macOS vmnet networks.
+
+The networks are local to the Macs that run the instances, so this provider
+only names them: implement-network gives every CloudStack network a vmnet
+network name and mode, which CloudStack stores as the network's broadcast
+URI, e.g. vs://cs-net-42?mode=nat. The orchestrator extensions running the
+instances (e.g. apple-container) read that URI from each NIC and create the
+network on the Mac on first use. All other network commands are no-ops.
+
+Only L2 networks are accepted: the vmnet networks hand out their own IP
+addresses, so CloudStack cannot manage them. CloudStack only uses an
+extension for networks whose offering names it as a service provider, and
+L2 offerings may only have the UserData service, so the extension declares
+UserData. It does not deliver user data yet: those commands are no-ops.
+
+Physical network registration details (registerExtension):
+  mode          optional, nat (default) or internal; nat networks reach the
+                outside world through the Mac, internal (host-only) networks
+                reach only the Mac and each other's instances
+  name_prefix   optional, prefix of the vmnet network names, default cs-net-
+
+Invocation: vmnet.py <command> <payload-file> <timeout-seconds>
+"""
+
+import json
+import re
+import sys
+
+MODES = ("nat", "internal")
+DEFAULT_MODE = "nat"
+DEFAULT_PREFIX = "cs-net-"
+BROADCAST_SCHEME = "vs"
+BROADCAST_DOMAIN_TYPE = "Vswitch"
+
+NO_OP_COMMANDS = {
+    "shutdown-network", "destroy-network", "restore-network",
+    "prepare-nic", "release-nic",
+    "assign-ip", "release-ip",
+    "add-static-nat", "delete-static-nat", "add-port-forward", 
"delete-port-forward",
+    "apply-fw-rules", "apply-network-acl", "apply-lb-rules",
+    "add-dhcp-entry", "remove-dhcp-entry", "config-dhcp-subnet", 
"remove-dhcp-subnet", "set-dhcp-options",
+    "add-dns-entry", "remove-dns-entry", "config-dns-subnet", 
"remove-dns-subnet",
+    "save-vm-data", "save-password", "save-userdata", "save-sshkey", 
"save-hypervisor-hostname",
+}
+
+
+class VmnetError(Exception):
+    pass
+
+
+def as_object(value):
+    """Extension details arrive as a JSON object or as a JSON encoded 
string."""
+    if isinstance(value, str):
+        try:
+            value = json.loads(value) if value.strip() else {}
+        except ValueError:
+            return {}
+    return value if isinstance(value, dict) else {}
+
+
+def text(value):
+    return "" if value is None else str(value).strip()
+
+
+def network_settings(data):
+    details = as_object(data.get("physical-network-extension-details"))
+    mode = text(details.get("mode")).lower() or DEFAULT_MODE
+    if mode not in MODES:
+        raise VmnetError(f"Invalid mode '{mode}', expected one of {', 
'.join(MODES)}")
+    prefix = text(details.get("name_prefix")) or DEFAULT_PREFIX
+    # The name ends up as the host part of the broadcast URI.
+    if not re.fullmatch(r"[a-z0-9][a-z0-9-]*", prefix):
+        raise VmnetError(f"Invalid name_prefix '{prefix}', use lowercase 
letters, digits and dashes")
+    return mode, prefix
+
+
+def network_name(prefix, network_id):
+    network_id = text(network_id)
+    if not network_id.isdigit():
+        raise VmnetError(f"Missing or invalid network_id '{network_id}'")
+    return f"{prefix}{network_id}"
+
+
+def broadcast_uri(name, mode):
+    return f"{BROADCAST_SCHEME}://{name}?mode={mode}"
+
+
+def implement_network(data):
+    payload = as_object(data.get("payload"))
+    guest_type = text(payload.get("guest_type")).lower()
+    if guest_type and guest_type != "l2":
+        raise VmnetError(f"vmnet networks assign their own IP addresses, so 
only L2 networks are supported, "
+                         f"not {guest_type} network 
{payload.get('network_id')}")
+    mode, prefix = network_settings(data)
+    name = network_name(prefix, payload.get("network_id"))
+    return {"network.broadcast_domain_type": BROADCAST_DOMAIN_TYPE,
+            "network.broadcast_uri": broadcast_uri(name, mode)}
+
+
+def execute(command, payload_file):
+    if command not in NO_OP_COMMANDS and command not in 
("ensure-network-device", "implement-network"):
+        raise VmnetError(f"Unsupported command '{command}'")
+    try:
+        with open(payload_file, encoding="utf-8") as fh:
+            data = json.load(fh)
+    except OSError as e:
+        raise VmnetError(f"Cannot read payload file {payload_file}: {e}")
+    except ValueError:
+        raise VmnetError(f"Invalid JSON in payload file {payload_file}")
+
+    if command == "implement-network":
+        return implement_network(data)
+    if command == "ensure-network-device":
+        # There is no central device: the networks live on the Macs.
+        return {}
+    return None
+
+
+def main(argv):
+    if len(argv) < 3:
+        print("Usage: vmnet.py <command> <payload-file> [timeout-seconds]", 
file=sys.stderr)
+        return 1
+    try:
+        output = execute(argv[1], argv[2])
+    except VmnetError as e:
+        print(str(e), file=sys.stderr)
+        return 1
+    if output is not None:
+        print(json.dumps(output, separators=(",", ":")))
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))

Reply via email to