Skip to content
Merged
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
1 change: 1 addition & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ RUN podman network create net \
-o mode=unmanaged && rm -rf /run/containers /run/libpod
COPY container/podman.json /etc/containers/networks/podman.json
COPY container/podman.json /srv/whs/containers/networks/podman.json
RUN cp /srv/whs/containers/networks/net.json /etc/containers/networks
LABEL run_whs 'podman run -d -ti --privileged -p 8080:8080 --group-add=keep-groups -v$NAME:/srv/whs --name $NAME $IMAGE'
LABEL develop_whs 'podman run -d -ti --privileged -p 8080:8080 --group-add=keep-groups -v${PWD}:/app -v$NAME:/srv/whs --name $NAME $IMAGE'
VOLUME /srv/whs
5 changes: 5 additions & 0 deletions container/network/macvlan.network
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Match]
Name=xi-*
Kind=macvlan
[Network]
Bridge=whs-lab
108 changes: 70 additions & 38 deletions layout/python/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,71 @@
from carthage.modeling import *
from carthage.podman import *
from carthage.oci import *
from carthage.network import V4Config
from carthage.network import V4Config, persistent_random_mac, NetworkConfig
from carthage.systemd import SystemdNetworkModelMixin
from carthage.modeling import NetworkConfigModel, injector_access
from carthage.dependency_injection import inject, InjectionKey
from carthage_base import *
from .images import WhsRouter
from .web_backend import web_server_key
from .models import ModelStore, VmImage
from pathlib import Path
from typing import Optional


root_path = Path(__file__).parent.parent
assignments_path = root_path/"assignments.yml"

class AlreadyRunningMachine(Machine):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.running = True


@inject(device_model=InjectionKey('device_model'))
def build_v4_config(device_model) -> Optional[V4Config]:
'''Build V4Config from device model, only setting non-falsy values.

This function only sets v4_config attributes if the device model properties
are non-falsy. Gateway and DNS servers are typically not set on devices
(we don't want to override the V4Config in this case).

dhcp is deprecated in the device model and is ignored here.
'''
if not device_model:
return None

kwargs = {}

# Only set address if non-falsy
if device_model.ipv4_manual:
kwargs['address'] = str(device_model.ipv4_manual)

# Only set gateway if non-falsy (typically not set on devices)
if device_model.gateway:
kwargs['gateway'] = str(device_model.gateway)

# Only set dns_servers if non-empty
if device_model.dns_servers:
kwargs['dns_servers'] = tuple(str(s) for s in device_model.dns_servers)

# Only create V4Config if we have static configuration
if kwargs.get('address') or kwargs.get('gateway') or kwargs.get('dns_servers'):
return V4Config(**kwargs)
return None


@inject(device_model=InjectionKey('device_model'))
def build_mac(device_model) -> str:
'''Build MAC address from device model.'''
if device_model.mac_address:
return device_model.mac_address
return persistent_random_mac


class DeviceNetworkConfig(NetworkConfigModel):
add('eth0', mac=build_mac, v4_config=build_v4_config, net=injector_access('bridge_net'))


@inject(model_store=ModelStore, ainjector=AsyncInjector)
async def build_layout(model_store, ainjector) -> CarthageLayout:
Expand All @@ -32,6 +85,7 @@ class layout(CarthageLayout):
add_provider(podman_container_host, LocalPodmanContainerHost)
add_provider(persistent_seed_path, assignments_path)
add_provider(MachineDependency(f'router.{domain}'))
add_provider(InjectionKey(NetworkConfig), DeviceNetworkConfig, allow_multiple=True)

@provides('bridge_net')
class net(NetworkModel):
Expand Down Expand Up @@ -82,31 +136,25 @@ def build_container(device):
device_dns_servers = device.dns_servers
device_image = model_store.get_device_container_image(device)

@dynamic_name(device.name)
class whs_container(MachineModel):
device_model = device
add_provider(machine_implementation_key, dependency_quote(PodmanContainer))
add_provider(oci_container_image, device_image.name)
name = device_name
class net_config(NetworkConfigModel):
if not device_dhcp:
add(
'eth0',
mac=device_mac,
net=injector_access('bridge_net'),
v4_config=V4Config(dhcp=False,
address=device_ipv4,
gateway=device_gateway,
dns_servers=device_dns_servers)
)
else:
add(
'eth0',
mac=device_mac,
net=injector_access('bridge_net'),
v4_config=V4Config(dhcp=device_dhcp),
)

return whs_container


def build_bare_metal(device):
@dynamic_name(device.name)
class whs_bare_metal(MachineModel):
device_model = device
name = device.name
add_provider(machine_implementation_key, dependency_quote(BareMetalMachine))
machine_mixins = (AlreadyRunningMachine,)

return whs_bare_metal

def build_vm(device):

device_dhcp = device.dhcp
Expand Down Expand Up @@ -139,30 +187,14 @@ class whs_vm(MachineModel):
add_provider(machine_implementation_key, dependency_quote(carthage.libvirt.Vm))
add_provider(carthage.libvirt.vm_image_key, vm_image)

class net_config(NetworkConfigModel):
if not device_dhcp:
add(
'eth0',
mac=device_mac,
net=injector_access('bridge_net'),
v4_config=V4Config(dhcp=False,
address=device_ipv4,
gateway=device_gateway,
dns_servers=device_dns_servers)
)
else:
add(
'eth0',
mac=device_mac,
net=injector_access('bridge_net'),
v4_config=V4Config(dhcp=device_dhcp),
)
return whs_vm

for id, device in model_store.devices.items():
if device.type == 'vm':
new_vm = build_vm(device)
elif device.type == 'container':
new_container = build_container(device)
elif device.type == 'bareMetal':
new_bare_metal = build_bare_metal(device)

return await ainjector(layout)
40 changes: 40 additions & 0 deletions patch_whs/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ def build_parser() -> argparse.ArgumentParser:
metavar="DIR",
help="Bind mount DIR to /run/libvirt inside the container",
)
start_parser.add_argument(
"--nic",
metavar="IFNAME",
action="append",
help="Attach real device(s) via macvlan on specified interface(s)",
)
start_parser.set_defaults(handler=handle_start)
return parser

Expand Down Expand Up @@ -89,11 +95,33 @@ def parse_command(command: str) -> list[str]:
return shlex.split(command, posix=True)


def ensure_macvlan_network(nic: str) -> None:
"""Ensure the macvlan network for the given NIC exists."""
net_name = f"whs-{nic}"
run_podman_command([
"podman", "network", "create",
"--driver", "macvlan",
"-o", f"parent={nic}",
"-o", "mode=passthru",
"--ipam-driver", "none",
"--ignore",
net_name
])


def build_runtime_options(args: argparse.Namespace) -> list[str]:
options: list[str] = []
if args.expose_libvirt:
libvirt_dir = Path(args.expose_libvirt)
options.extend(["-v", f"{libvirt_dir}:/run/libvirt"])

nics = args.nic or []
if nics:
# Add default network first
options.extend(["--network", get_default_network()])
for nic in nics:
ensure_macvlan_network(nic)
options.extend(["--network", f"whs-{nic}:interface_name=xi-{nic}"])
return options


Expand Down Expand Up @@ -188,9 +216,20 @@ def ensure_podman_ready() -> None:
return


def get_default_network() -> str:
"""Return the default podman network name."""
result = run_podman_command(["podman", "info", "--format", "json"], capture_output=True)
try:
data = json.loads(result.stdout)
return data["host"]["networkBackendInfo"]["defaultNetwork"]
except (json.JSONDecodeError, KeyError):
return "podman"


def handle_start(args: argparse.Namespace) -> int:
ensure_podman_ready()
ensure_windows_nested_virtualization()

run_podman_command(["podman", "pull", args.image])
labels = inspect_image_labels(args.image)
label_name = DEVELOP_LABEL if args.develop else RUN_LABEL
Expand All @@ -206,6 +245,7 @@ def handle_start(args: argparse.Namespace) -> int:
"NAME": args.name,
},
)

argv = strip_podman_remote_command_args(parse_command(expanded_command))
run_podman_command(argv)
return 0
Expand Down
Loading