weegee: reorganise and add configuration files

This commit is contained in:
Shiz 2021-12-12 05:09:56 +01:00
parent f3842bd4dc
commit 2a92a330c7
6 changed files with 743 additions and 649 deletions

View File

@ -1,588 +1,44 @@
from __future__ import annotations
from logging import getLogger
from dataclasses import dataclass
from typing import Optional as O, Any
from .dazy import Instance, Config, Meta, Item, Template
from .wireguard import (
IPAddress, IPNetwork, IPInterface,
WireguardHostType, WireguardHost, WireguardPeer, WireguardConnection, which_connection,
)
from .desc import (
WEEGEE_HOOK, WEEGEE_HOST, WEEGEE_INTERFACE, WEEGEE_PEER, WEEGEE_CONNECTION, WEEGEE_CONFIG,
WEEGEE_INTERFACE_CONF_WG, WEEGEE_PEER_CONF_WG, WEEGEE_CONF_WG,
from .dazy import Meta, Template
from .desc import WEEGEE_INTERFACE_CONF_WG, WEEGEE_PEER_CONF_WG, WEEGEE_CONF_WG
from .config import WeegeeConfig, WeegeeContext
from .core import (
WeegeeBase, WeegeeHook, WeegeeHost, WeegeeInterface, WeegeePeer, WeegeeConnection,
do_config_interface, do_discover_interface,
find_host_interfaces, find_interface_connections, find_interface_other_peers,
do_sync_interface, sync_interface, sync_all_interfaces,
)
from .extra import WeegeeServer, WeegeeClient
logger = getLogger(__name__)
@dataclass(eq=False)
class WeegeeBase:
BASE = None
context: 'WeegeeContext'
item: Item
meta: Meta = None
def __post_init__(self) -> None:
if not self.meta:
self.meta = Meta.load(self.context.instance, self.BASE.get_name())
self.item = self.item.resolve(self.meta)
@classmethod
def get_name(cls, name: str) -> str:
return f'{cls.BASE.item_prefix}/{name}'
@classmethod
def get_meta(cls, context: 'WeegeeContext') -> Meta:
return Meta.load(context.instance, cls.BASE.get_name())
@classmethod
def exists(cls, context: 'WeegeeContext', name: str) -> bool:
return Item.exists(context.instance, cls.get_name(name))
@classmethod
def create(cls, context: 'WeegeeContext', name: str, **kwargs) -> 'WeegeeBase':
meta = cls.get_meta(context)
item = Item.make(context.instance, cls.get_name(name), **kwargs)
if not item.resolve(meta).is_value_complete(meta):
raise TypeError('internal error')
return cls(context, item, meta)
@classmethod
def filter(cls, context: 'WeegeeContext', pattern: str) -> list['WeegeeBase']:
return [cls(context, Item.load(context.instance, name)) for name in Item.filter(context.instance, cls.get_name(pattern))]
@classmethod
def load(cls, context: 'WeegeeContext', name: str) -> 'WeegeeBase':
return cls(context, Item.load(context.instance, cls.get_name(name)))
def save(self) -> None:
self.item.unresolve(self.meta).save()
def delete(self) -> None:
self.item.delete()
@property
def name(self) -> str:
return self.item_name[len(self.get_name('')):]
@property
def item_name(self) -> str:
return self.item.name
def __getattr__(self, name: str) -> Any:
if name not in self.item:
raise AttributeError(name)
return self.item[name]
def __setattr__(self, name: str, value: Any) -> None:
if 'item' in self.__dict__ and self.item.is_type_resolved(name):
self.item[name] = value
else:
super().__setattr__(name, value)
def __eq__(self, other: Any) -> bool:
return isinstance(other, self.__class__) and self.item.name == other.item.name
def __hash__(self) -> int:
return hash((self.__class__.__name__, self.item_name))
@dataclass(eq=False)
class WeegeeHook(WeegeeBase):
BASE = WEEGEE_HOOK
@classmethod
def create(cls, ctx: 'WeegeeContext', name: str, pre: list[str], post: list[str]) -> 'WeegeeHook':
return super().create(ctx, name,
pre=pre, post=post,
)
@dataclass
class WeegeeHookRunner:
hooks: list[WeegeeHook]
conn: WireguardConnection
kwargs: dict[str, Any]
def _run(self, cmd: str) -> None:
for k, v in self.kwargs.items():
cmd = cmd.replace('%' + k, str(v))
return self.conn._run(cmd, shell=True)
def __enter__(self) -> 'WeegeeHooks':
for h in self.hooks:
for cmd in h.pre:
self._run(cmd)
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if not exc_type:
for h in self.hooks:
for cmd in h.post:
self._run(cmd)
@dataclass(eq=False)
class WeegeeHost(WeegeeBase):
BASE = WEEGEE_HOST
conn: WireguardHost = None
@classmethod
def create(cls, ctx: 'WeegeeContext', name: str, type: WireguardHostType = WireguardHostType.Unsupported, host: O[str] = None, user: O[str] = None, elevate_user: O[str] = None, automanage: bool = False, autosync: bool = False) -> 'WeegeeHost':
return super().create(ctx, name,
automanage=automanage,
autosync=autosync,
type=type.value,
host=host,
user=user,
elevate_user=elevate_user,
)
@property
def type(self) -> WireguardHostType:
return WireguardHostType(self.item['type'])
def __post_init__(self) -> None:
super().__post_init__()
self.conn = WireguardHost(WeegeeHookedWireguardConnection(self, which_connection(self.type)(self.host, self.user, self.elevate_user)))
@classmethod
def list_hooks(cls) -> list[str]:
return ['interface_add', 'interface_del', 'address_add', 'address_del', 'route_add', 'route_del', 'configure']
def get_hooks(self, name: str, **kwargs) -> WeegeeHookRunner:
return WeegeeHookRunner([WeegeeHook(self.context, x) for x in getattr(self, f'{name}_hooks')], self.conn.conn, kwargs)
def get_peers(self, name: str) -> O[list[WireguardPeer]]:
interface = self.conn.get_interface(name)
if not interface:
return None
return interface.list_peers()
def sync_interface(self, name: str, addresses: list[IPInterface], routes: list[IPNetwork], config: str) -> None:
interface = self.conn.get_interface(name)
if not interface:
if not self.automanage:
return
interface = self.conn.create_interface(name)
if self.automanage:
interface.sync(addresses, routes)
interface.sync_config(config)
def delete_interface(self, name: str) -> None:
interface = self.conn.get_interface(name)
if interface:
interface.clear_config()
if self.automanage:
interface.delete()
@dataclass
class WeegeeHookedWireguardConnection(WireguardConnection):
host: WireguardHost
inner: WireguardConnection
def _run(self, *args, **kwargs) -> str:
return self.inner._run(*args, **kwargs)
def has_interface(self, name: str) -> bool:
return self.inner.has_interface(name)
def create_interface(self, name: str) -> None:
with self.host.get_hooks('interface_add', i=name):
return self.inner.create_interface(name)
def destroy_interface(self, name: str) -> None:
with self.host.get_hooks('interface_del', i=name):
return self.inner.destroy_interface(name)
def set_mtu(self, name: str, mtu: int) -> None:
return self.inner.set_mtu(name, mtu)
def set_up(self, name: str) -> None:
return self.inner.set_up(name)
def set_down(self, name: str) -> None:
return self.inner.set_down(name)
def get_addresses(self, name: str) -> list[IPInterface]:
return self.inner.get_addresses(name)
def add_address(self, name: str, address: IPInterface) -> None:
with self.host.get_hooks('address_add', i=name, a=str(address)):
return self.inner.add_address(name, address)
def delete_address(self, name: str, address: IPInterface) -> None:
with self.host.get_hooks('address_del', i=name, a=str(address)):
return self.inner.delete_address(name, address)
def delete_all_addresses(self, name: str) -> None:
for address in self.get_addresses(name):
self.delete_address(name, address)
def get_routes(self, name: str) -> list[IPNetwork]:
return self.inner.get_routes(name)
def add_route(self, name: str, route: IPNetwork) -> None:
with self.host.get_hooks('route_add', i=name, r=str(route)):
return self.inner.add_route(name, route)
def delete_route(self, name: str, route: IPNetwork) -> None:
with self.host.get_hooks('route_del', i=name, r=str(route)):
return self.inner.delete_route(name, route)
def delete_all_routes(self, name: str) -> None:
for route in self.get_routes(name):
self.delete_route(name, route)
@dataclass(eq=False)
class WeegeeInterface(WeegeeBase):
BASE = WEEGEE_INTERFACE
@classmethod
def filter_for_hosts(cls, ctx: 'WeegeeContext', hosts: set[WeegeeHost]) -> set['WeegeeInterface']:
return set(i for i in ctx.get_interfaces() if set(i.hosts) & hosts)
@classmethod
def create(cls, ctx: 'WeegeeContext', name: str, interface_name: str, private_key: O[str] = None, public_key: O[str] = None, port: O[int] = None, hosts: O[list[WeegeeHost]] = None, addresses: list[IPInterface] = []) -> 'WeegeeInterface':
for host in [ctx.get_local_host()] + hosts:
try:
private_key = private_key or host.conn.gen_private_key()
public_key = public_key or host.conn.get_public_key(private_key)
break
except Exception as e:
logger.warn(f'could not generate keypair on host {host.name!r}: {e!r}')
else:
logger.critical('could not generate public/private keypair automatically: please pass explicitly!')
raise ValueError()
return super().create(ctx, name,
hosts=[h.item for h in hosts],
interface_name=interface_name,
public_key=public_key, private_key=private_key,
addresses=addresses, port=port,
)
def save(self) -> None:
for host in self.hosts:
host.save()
super().save()
def delete(self) -> None:
peers = WeegeePeer.filter_for_interfaces(self.context, {self})
for p in peers:
p.delete()
for h in self.hosts:
if h.autosync:
h.delete_interface(self.interface_name)
super().delete()
@property
def hosts(self) -> list[WeegeeHost]:
return [WeegeeHost(self.context, x) for x in self.item['hosts']]
def gen_config(self) -> str:
template = Template.load(self.context.instance, WEEGEE_INTERFACE_CONF_WG.get_name())
config = WEEGEE_INTERFACE_CONF_WG.make_config(self.context.instance,
interface=self.item,
)
return template.render(config)
@dataclass(eq=False)
class WeegeePeer(WeegeeBase):
BASE = WEEGEE_PEER
@classmethod
def filter_for_interfaces(cls, ctx: 'WeegeeContext', interfaces: set[WeegeeInterface]) -> set['WeegeePeer']:
return set(p for p in ctx.get_peers() if p.interface in interfaces)
@classmethod
def create(cls, ctx: 'WeegeeContext', name: str, interface: WeegeeInterface, routes: list[IPNetwork], host: O[str] = None, port: O[int] = None) -> 'WeegeePeer':
return super().create(ctx, name,
interface=interface.item,
routes=routes,
host=host, port=port,
)
def save(self) -> None:
self.interface.save()
super().save()
self.sync(auto=True)
def sync(self, auto=False, log=None) -> None:
if not log:
log = set()
if self.item_name in log:
return
log.add(self.item_name)
logger.info(f'syncing peer: {self.name}')
sync_interface(self.interface, auto=auto, log=log)
def delete(self) -> None:
for c in WeegeeConnection.filter_for_peers(self.context, {self}):
c.peers.remove(self)
if len(c.peers) <= 1:
c.delete()
else:
c.save()
super().delete()
@property
def interface(self) -> WeegeeInterface:
return WeegeeInterface(self.context, self.item['interface'])
@dataclass(eq=False)
class WeegeeConnection(WeegeeBase):
BASE = WEEGEE_CONNECTION
@classmethod
def filter_for_peers(cls, ctx: 'WeegeeContext', peers: set[WeegeePeer]) -> set['WeegeeConnection']:
return set(c for c in ctx.get_connections() if set(c.peers) & peers)
@classmethod
def create(cls, ctx: 'WeegeeContext', name: str, peers: list[WeegeePeer], preshared_key: O[str] = None) -> 'WeegeeConnecton':
if not preshared_key:
hosts = {ctx.get_local_host()}
for p in peers:
hosts.update(p.interface.hosts)
for host in hosts:
try:
preshared_key = host.conn.gen_preshared_key()
break
except Exception as e:
logger.warn(f'could not generate preshared key on host {host.name!r}: {e!r}')
else:
logger.critical('could not generate preshared key automatically: please pass explicitly!')
raise ValueError()
return super().create(ctx, name,
peers=[p.item for p in peers],
preshared_key=preshared_key,
)
def save(self) -> None:
for peer in self.peers:
peer.save()
super().save()
@property
def peers(self) -> list[WeegeePeer]:
return [WeegeePeer(self.context, x) for x in self.item['peers']]
def gen_config(self, target: WeegeePeer) -> str:
peers = [p for p in self.peers if p != target]
template = Template.load(self.context.instance, WEEGEE_PEER_CONF_WG.get_name())
config = WEEGEE_PEER_CONF_WG.make_config(self.context.instance,
connection=self.item,
peers={p.name: p.item for p in peers},
)
return template.render(config)
def do_config_interface(interface: WeegeeInterface, peers: set[WeegeePeer], connections: set[WeegeeConnection]) -> str:
peer_configs = []
for c in connections:
for p in set(c.peers) & peers:
peer_configs.append(c.gen_config(p))
interface_config = interface.gen_config()
template = Template.load(interface.context.instance, WEEGEE_CONF_WG.get_name())
config = WEEGEE_CONF_WG.make_config(interface.context.instance,
interface_config=interface_config,
peer_configs=peer_configs,
)
return template.render(config)
def do_discover_interface(interface: WeegeeInterface, peers: set[WeegeePeer], connections: set[WeegeeConnection]) -> tuple[list[IPNetwork], set[WeegeePeer]]:
routes = []
other_peers = set()
for c in connections:
for p in set(c.peers) - peers:
routes.extend(p.routes)
routes.extend(a.network for a in p.interface.addresses)
other_peers.add(p)
return routes, other_peers
def find_host_interfaces(host: WeegeeHost) -> set[WeegeeInterface]:
return WeegeeInterface.filter_for_hosts(host.context, {host})
def find_interface_connections(interface: WeegeeInterface) -> tuple[set[WeegeePeer], set[WeegeeConnection]]:
peers = WeegeePeer.filter_for_interfaces(interface.context, {interface})
connections = WeegeeConnection.filter_for_peers(interface.context, peers)
return peers, connections
def find_interface_other_peers(interface: WeegeeInterface) -> set[WeegeePeer]:
own_peers, connections = find_interface_connections(interface)
_, other_peers = do_discover_interface(interface, own_peers, connections)
return other_peers
def do_sync_interface(interface: WeegeeInterface, peers: set[WeegeePeer], connections: set[WeegeeConnection], auto: bool = False) -> set[WeegeePeer]:
config = do_config_interface(interface, peers, connections)
routes, other_peers = do_discover_interface(interface, peers, connections)
for host in interface.hosts:
if auto and not host.autosync:
continue
host.sync_interface(interface.interface_name, interface.addresses, routes, config)
return other_peers
def sync_interface(interface: WeegeeInterface, auto=False, log=None) -> None:
if log is None:
log = set()
if interface.item_name in log:
return
log.add(interface.item_name)
logger.info(f'syncing interface: {interface.name}')
all_peers, all_connections = find_interface_connections(interface)
other_peers = do_sync_interface(interface, all_peers, all_connections, auto=auto)
for p in other_peers:
sync_interface(p.interface, auto=auto, log=log)
def sync_all_interfaces(context: 'WeegeeContext', auto=False, log=None) -> None:
if log is None:
log = set()
for interface in context.get_interfaces():
sync_interface(interface, auto=auto, log=log)
@dataclass(eq=False)
class WeegeeServer(WeegeePeer):
def get_client_name(self, name: str) -> str:
return f'{self.name}/{name}'
def get_clients(self) -> list[WeegeePeer]:
return WeegeeClient.filter(self.context, self.get_client_name('*'))
def get_client(self, name: str) -> O[WeegeePeer]:
return WeegeeClient.load(self.context, self.get_client_name(name))
def delete(self) -> None:
super().delete()
sync_all_interfaces(self.context, auto=True)
def gen_config(self) -> str:
peers, connections = find_interface_connections(self.interface)
return do_config_interface(self.interface, peers, connections)
@dataclass(eq=False)
class WeegeeClient(WeegeePeer):
@classmethod
def create(cls, ctx: 'WeegeeContext', name: str, server: WeegeeServer, preshared_key: O[str] = None, **kwargs) -> 'WeegeeClient':
name = server.get_client_name(name)
client = super().create(ctx, name, **kwargs)
connection = WeegeeConnection.create(ctx, name, peers=[server, client], preshared_key=preshared_key)
connection.save()
return client
@property
def connection(self) -> WeegeeConnection:
return WeegeeConnection.load(self.context, self.name)
@property
def server(self) -> WeegeeServer:
return next(p for p in self.connection.peers if p != self.peer)
def delete(self) -> None:
self.connection.delete()
super().delete()
sync_all_interfaces(self.context, auto=True)
def gen_config(self) -> str:
peers, connections = find_interface_connections(self.interface)
return do_config_interface(self.interface, peers, connections)
@dataclass
class WeegeeConfig:
context: 'WeegeeContext'
item: Item
@classmethod
def get_name(cls) -> str:
return WEEGEE_CONFIG.get_name()
@classmethod
def load(cls, context: 'WeegeeContext', name: str) -> 'WeegeeConfig':
return cls(context, Meta.load(context.instance, name))
def save(self) -> None:
self.item.save()
@property
def default_server_hosts(self) -> list[WeegeeHost]:
return [WeegeeHost(self.context, item) for item in self.item['default_server_hosts']]
@default_server_hosts.setter
def default_server_hosts(self, value: list[WeegeeHost]) -> None:
self.item['default_server_hosts'] = [x.item for x in value]
@property
def default_client_hosts(self) -> list[WeegeeHost]:
return [WeegeeHost(self.context, item) for item in self.item['default_client_hosts']]
@default_client_hosts.setter
def default_client_hosts(self, value: list[WeegeeHost]) -> None:
self.item['default_client_hosts'] = [x.item for x in value]
@dataclass
class WeegeeContext:
LOCAL_HOST_NAME = 'local'
instance: Instance
def setup(self) -> None:
logger.info('setup: metas')
for wmeta in (WEEGEE_HOOK, WEEGEE_HOST, WEEGEE_INTERFACE, WEEGEE_PEER, WEEGEE_CONNECTION, WEEGEE_CONFIG):
logger.debug(' ' + wmeta.name)
Meta.parse(self.instance, wmeta.get_name(), wmeta.spec).save()
logger.info('setup: templates')
for wtemp in (WEEGEE_INTERFACE_CONF_WG, WEEGEE_PEER_CONF_WG, WEEGEE_CONF_WG):
logger.debug(' ' + wtemp.name)
Template(wtemp.get_name(), wtemp.template, self.instance).save()
logger.info('setup: items')
if not WeegeeHost.exists(self, self.LOCAL_HOST_NAME):
logger.debug(f' {WeegeeHost.get_name(self.LOCAL_HOST_NAME)}')
localhost = WeegeeHost.create(self, self.LOCAL_HOST_NAME)
localhost.save()
else:
localhost = self.get_local_host()
logger.info('setup: config')
config = self.get_config()
config.default_server_hosts = [localhost]
config.save()
def get_local_host(self) -> WeegeeHost:
return WeegeeHost.load(self, self.LOCAL_HOST_NAME)
def get_config(self) -> WeegeeConfig:
return WeegeeConfig.load(self, WeegeeConfig.get_name())
def get_servers(self) -> list[WeegeeServer]:
return WeegeeServer.filter(self, '*')
def get_server(self, name: str) -> O[WeegeeServer]:
return WeegeeServer.load(self, name)
def get_connections(self) -> list[WeegeeConnection]:
return WeegeeConnection.filter(self, '*')
def get_connection(self, name: str) -> O[WeegeeConnection]:
return WeegeeConnection.load(self, name)
def get_peers(self) -> list[WeegeePeer]:
return WeegeePeer.filter(self, '*')
def get_peer(self, name: str) -> O[WeegeePeer]:
return WeegeePeer.load(self, name)
def get_interfaces(self) -> list[WeegeeInterface]:
return WeegeeInterface.filter(self, '*')
def get_interface(self, name: str) -> O[WeegeeInterface]:
return WeegeeInterface.load(self, name)
def get_hosts(self) -> list[WeegeeHost]:
return WeegeeHost.filter(self, '*')
def get_host(self, name: str) -> O[WeegeeHost]:
return WeegeeHost.load(self, name)
def setup(context: WeegeeContext) -> None:
logger.info('setup: metas')
for meta in (WeegeeHook, WeegeeHost, WeegeeInterface, WeegeePeer, WeegeeConnection):
logger.debug(' ' + meta.BASE.name)
Meta.parse(context.instance, meta.BASE.get_name(), meta.BASE.spec).save()
logger.info('setup: templates')
for temp in (WEEGEE_INTERFACE_CONF_WG, WEEGEE_PEER_CONF_WG, WEEGEE_CONF_WG):
logger.debug(' ' + temp.name)
Template(temp.get_name(), temp.template, context.instance).save()
logger.info('setup: items')
if not WeegeeHost.exists(context, WeegeeHost.LOCAL_HOST_NAME):
logger.debug(f' {WeegeeHost.get_name(WeegeeHost.LOCAL_HOST_NAME)}')
WeegeeHost.create(context, WeegeeHost.LOCAL_HOST_NAME).save()
__all__ = [x.__name__ for x in (
WeegeeConfig, WeegeeContext,
WeegeeHook, WeegeeHook, WeegeeInterface, WeegeePeer, WeegeeConnection,
WeegeeServer, WeegeeClient,
setup,
do_config_interface, do_discover_interface,
find_host_interfaces, find_interface_connections, find_interface_other_peers,
do_sync_interface, sync_interface, sync_all_interfaces,
)]

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import sys
import os.path
import argparse
import logging
import ipaddress
@ -8,14 +9,14 @@ from typing import Optional as O
logging.basicConfig(level=logging.DEBUG)
from .dazy import Instance
from .wireguard import WireguardHostType
from . import (
WeegeeContext,
WeegeeContext, WeegeeConfig,
WeegeeHook, WeegeeHost, WeegeeInterface, WeegeePeer, WeegeeConnection,
WeegeeServer, WeegeeClient,
find_interface_other_peers,
sync_all_interfaces,
setup,
)
@ -49,14 +50,14 @@ def timestampify(dt: datetime) -> str:
def main():
parser = argparse.ArgumentParser()
parser.set_defaults(func=None)
parser.add_argument('-d', '--base-dir', default='.')
parser.add_argument('-c', '--config', help='path to configuration file')
parser.add_argument('--yes-i-want-to-destroy-this', action='store_true', default=False)
commands = parser.add_subparsers(title='commands')
def do_status(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
success = True
for interface in ctx.get_interfaces():
for interface in WeegeeInterface.find_all(ctx):
if interface.hosts:
print(f'{interface.name}:')
for host in interface.hosts:
@ -97,42 +98,85 @@ def main():
system_commands = system.add_subparsers(title='system commands')
def do_setup(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
ctx.setup()
setup(ctx)
setup = system_commands.add_parser('setup')
setup.set_defaults(func=do_setup)
sys_setup = system_commands.add_parser('setup')
sys_setup.set_defaults(func=do_setup)
def do_migrate(arser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
def do_configure(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
config = ctx.get_config()
if args.config_dir:
config.meta_dir = args.config_dir
config.data_dir = args.data_dir or args.config_dir
if args.log_level:
config.log_level = args.log_level
if args.reset_default_client_hosts:
config.default_client_hosts = []
for host in args.del_default_client_host:
host_item = WeegeeHost.load(context, host).name
if host_item in config.default_client_hosts:
config.default_client_hosts.remove(host_item)
for host in args.add_default_client_host:
host_item = WeegeeHost.load(context, host).name
if host_item not in config.default_client_hosts:
config.default_client_hosts.append(host_item)
if args.reset_default_server_hosts:
config.default_server_hosts = []
for host in args.del_default_server_host:
host_item = WeegeeHost.load(context, host).name
if host_item in config.default_server_hosts:
config.default_server_hosts.remove(host_item)
for host in args.add_default_server_host:
host_item = WeegeeHost.load(context, host).name
if host_item not in config.default_server_hosts:
config.default_server_hosts.append(host_item)
config.save(args.file)
sys_configure = system_commands.add_parser('configure')
sys_configure.add_argument('-s', '--system-file', dest='file', action='store_const', const=WeegeeConfig.GLOBAL_PATH, help=f'store to system configuration file ({WeegeeConfig.GLOBAL_PATH})')
sys_configure.add_argument('-u', '--user-file', dest='file', action='store_const', const=WeegeeConfig.USER_PATH, help=f'store to user configuration file ({WeegeeConfig.USER_PATH})')
sys_configure.add_argument('-l', '--local-file', dest='file', action='store_const', const=WeegeeConfig.LOCAL_PATH, help=f'store to local configuration file ({WeegeeConfig.LOCAL_PATH})')
sys_configure.add_argument('-f', '--file', help='store to given configuration file')
sys_configure.add_argument('-d', '--config-dir', metavar='PATH', help='config base directory (optional)')
sys_configure.add_argument('-D', '--data-dir', metavar='PATH', help='data base directory (optional, defaults to config base directory)')
sys_configure.add_argument('-L', '--log-level', metavar='LEVEL', help='log level', choices=('debug', 'info', 'warning', 'error', 'critical'))
for x in ('client', 'server'):
sys_configure.add_argument(f'--reset-default-{x}-hosts', action='store_true')
sys_configure.add_argument(f'--add-default-{x}-host', metavar='HOST', action='append', default=[], help=f'add default host for new {x}s')
sys_configure.add_argument(f'--del-default-{x}-host', metavar='HOST', action='append', default=[], help=f'remove default host for new {x}s')
sys_configure.set_defaults(func=do_configure)
def do_migrate(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
pass
migrate = system_commands.add_parser('migrate')
migrate.set_defaults(func=do_migrate)
sys_migrate = system_commands.add_parser('migrate')
sys_migrate.set_defaults(func=do_migrate)
def do_clean(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
success = True
while success:
success = False
for peer in ctx.get_peers():
if not WeegeeConnection.filter_for_peers(ctx, {peer}):
for peer in WeegeePeer.find_all(ctx):
if not WeegeeConnection.find_for_peers(ctx, {peer}):
print(f'destroying peer: {peer.name}')
peer.delete()
success = True
for connection in ctx.get_connections():
for connection in WeegeeConnection.find_all(ctx):
if len(connection.peers) <= 1:
print(f'destroying connection: {connection.name}')
connection.delete()
success = True
for interface in ctx.get_interfaces():
if not WeegeePeer.filter_for_interfaces(ctx, {interface}):
for interface in WeegeeInterface.find_all(ctx):
if not WeegeePeer.find_for_interfaces(ctx, {interface}):
print(f'destroying interface: {interface.name}')
interface.delete()
success = True
clean = system_commands.add_parser('clean')
clean.set_defaults(func=do_clean)
sys_clean = system_commands.add_parser('clean')
sys_clean.set_defaults(func=do_clean)
# Host commands
@ -148,17 +192,6 @@ def main():
autosync=args.auto_sync, automanage=args.auto_manage,
)
host.save()
if any([args.set_default_server, args.set_default_client, args.add_default_server, args.add_default_client]):
config = ctx.get_config()
if args.set_default_server:
config.default_server_hosts = []
if args.set_default_client:
config.default_client_hosts = []
if args.set_default_server or args.add_default_server:
config.default_server_hosts = config.default_server_hosts + [host]
if args.set_default_client or args.add_default_client:
config.default_client_hosts = config.default_client_hosts + [host]
config.save()
add_host = host_commands.add_parser('create')
add_host.add_argument('-t', '--type', type=WireguardHostType, help='host type for auto-manage operations')
@ -167,15 +200,11 @@ def main():
add_host.add_argument('-U', '--elevate_user', metavar='USER', help='username to elevate privileges to')
add_host.add_argument('-a', '--auto-sync', action='store_true', default=False, help='whether to auto-synchronize config')
add_host.add_argument('-A', '--auto-manage', action='store_true', default=False, help='whether to auto-synchronize interfaces')
add_host.add_argument('--set-default-server', action='store_true', default=False, help='set as default server host')
add_host.add_argument('--set-default-client', action='store_true', default=False, help='set as default client host')
add_host.add_argument('--add-default-server', action='store_true', default=False, help='add to default server hosts')
add_host.add_argument('--add-default-client', action='store_true', default=False, help='add to default client hosts')
add_host.add_argument('name', help='host name')
add_host.set_defaults(func=do_add_host)
def do_set_host(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
host = ctx.get_host(args.name)
host = WeegeeHost.load(ctx, args.name)
if args.type is not None:
host.type = args.type
if args.host is not None:
@ -185,9 +214,9 @@ def main():
if args.elevate_user is not None:
host.elevate_user = args.elevate_user
if args.auto_sync is not None:
host.autosync = int(args.auto_sync)
host.autosync = args.auto_sync
if args.auto_manage is not None:
host.automanage = int(args.auto_manage)
host.automanage = args.auto_manage
for hook_name in host.list_hooks():
got_pre = getattr(args, f'hook_pre_{hook_name}')
@ -214,18 +243,6 @@ def main():
host.save()
if any([args.add_default_server, args.add_default_client, args.del_default_server, args.del_default_client]):
config = ctx.get_config()
if args.set_default_server or args.add_default_server:
config.default_server_hosts.append(host)
if args.set_default_client or args.add_default_client:
config.default_client_hosts.append(host)
if args.del_default_server:
config.default_server_hosts.remove(host)
if args.del_default_client:
config.default_client_hosts.remove(host)
config.save()
set_host = host_commands.add_parser('set')
set_host.add_argument('-t', '--type', type=WireguardHostType, help='host type for auto-manage operations')
set_host.add_argument('-H', '--host', help='remote host')
@ -235,10 +252,6 @@ def main():
set_host.add_argument('--no-auto-sync', dest='auto_sync', action='store_false', help='do not auto-synchronize config')
set_host.add_argument('-A', '--auto-manage', action='store_true', default=None, help='auto-synchronize interfaces')
set_host.add_argument('--no-auto-manage', dest='auto_manage', action='store_false', help='do not auto-synchronize interface')
set_host.add_argument('--add-default-server', action='store_true', default=False, help='add to default server hosts')
set_host.add_argument('--add-default-client', action='store_true', default=False, help='add to default client hosts')
set_host.add_argument('--del-default-server', action='store_true', default=False, help='remove from default server hosts')
set_host.add_argument('--del-default-client', action='store_true', default=False, help='remove from default client hosts')
for hook in WeegeeHost.list_hooks():
hook_arg = hook.replace('_', '-')
hook_desc = hook.replace('_', ' ')
@ -250,7 +263,7 @@ def main():
def do_del_host(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
if not args.yes_i_want_to_destroy_this:
parser.error('please pass --yes-i-want-to-destroy-this if you really want to destroy this host')
host = ctx.get_host(args.name)
host = WeegeeHost.load(ctx, args.name)
host.delete()
del_host = host_commands.add_parser('destroy')
@ -284,7 +297,7 @@ def main():
def do_del_interface(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
if not args.yes_i_want_to_destroy_this:
parser.error('please pass --yes-i-want-to-destroy-this if you really want to destroy this interface')
interface = ctx.get_interface(args.name)
interface = WeegeeInterface.load(ctx, args.name)
interface.delete()
del_interface = interface_commands.add_parser('destroy')
@ -292,12 +305,12 @@ def main():
del_interface.set_defaults(func=do_del_interface)
def do_connect_interface(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
left_interface = ctx.get_interface(args.left)
left_interface = WeegeeInterface.load(ctx, args.left)
left_peer = WeegeePeer.create(ctx, f'{args.left}-{args.right}', left_interface,
routes=args.left_route, host=args.left_host, port=args.left_port,
)
left_peer.save()
right_interface = ctx.get_interface(args.right)
right_interface = WeegeeInterface.load(ctx, args.right)
right_peer = WeegeePeer.create(ctx, f'{args.right}-{args.left}', right_interface,
routes=args.right_route, host=args.right_host, port=args.right_port,
)
@ -318,7 +331,7 @@ def main():
connect_interface.set_defaults(func=do_connect_interface)
def do_disconnect_interface(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
connection = ctx.get_connection(f'{args.left}-{args.right}')
connection = WeegeeConnection.load(ctx, f'{args.left}-{args.right}')
connection.delete()
disconnect_interface = interface_commands.add_parser('disconnect')
@ -336,7 +349,7 @@ def main():
interface_name = args.interface or f'wg-{args.name}'
interface = WeegeeInterface.create(ctx, args.name, interface_name,
private_key=args.private_key, public_key=args.public_key,
addresses=args.address, port=args.port, hosts=ctx.get_config().default_server_hosts,
addresses=args.address, port=args.port, hosts=[WeegeeHost.load(ctx, name) for name in ctx.get_config().default_server_hosts],
)
interface.save()
server = WeegeeServer.create(ctx, args.name, interface, routes=args.routes, host=args.host)
@ -356,7 +369,7 @@ def main():
def do_del_server(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
if not args.yes_i_want_to_destroy_this:
parser.error('please pass --yes-i-want-to-destroy-this if you really want to destroy this server')
server = ctx.get_server(args.name)
server = WeegeeServer.load(ctx, args.name)
server.delete()
del_server = server_commands.add_parser('destroy')
@ -364,7 +377,7 @@ def main():
del_server.set_defaults(func=do_del_server)
def do_conf_server(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
server = ctx.get_server(args.name)
server = WeegeeServer.load(ctx, args.name)
print(server.gen_config())
conf_server = server_commands.add_parser('config')
@ -378,12 +391,12 @@ def main():
client_commands = client.add_subparsers(title='client commands')
def do_add_client(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
server = ctx.get_server(args.server)
server = WeegeeServer.load(ctx, args.server)
full_name = server.get_client_name(args.name)
interface_name = args.interface or f'wg0'
interface = WeegeeInterface.create(ctx, full_name, interface_name,
private_key=args.private_key, public_key=args.public_key,
addresses=args.address, hosts=ctx.get_config().default_client_hosts,
addresses=args.address, hosts=[WeegeeHost.load(ctx, name) for name in ctx.get_config().default_client_hosts],
)
interface.save()
client = WeegeeClient.create(ctx, args.name, server, preshared_key=args.preshared_key, interface=interface, routes=[])
@ -402,7 +415,7 @@ def main():
def do_del_client(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
if not args.yes_i_want_to_destroy_this:
parser.error('please pass --yes-i-want-to-destroy-this if you really want to destroy this client')
server = ctx.get_server(args.server)
server = WeegeeServer.load(ctx, args.server)
client = server.get_client(args.name)
client.delete()
@ -412,7 +425,7 @@ def main():
del_client.set_defaults(func=do_del_client)
def do_conf_client(parser: argparse.ArgumentParser, args: argparse.Namespace, ctx: WeegeeContext) -> O[int]:
server = ctx.get_server(args.server)
server = WeegeeServer.load(ctx, args.server)
client = server.get_client(args.name)
print(client.gen_config())
@ -428,8 +441,11 @@ def main():
if not args.func:
parser.error('a subcommand must be provided')
instance = Instance(args.base_dir)
sys.exit(args.func(parser, args, WeegeeContext(instance)))
config = WeegeeConfig.find()
if args.config:
config.load(args.config)
context = WeegeeContext(config)
sys.exit(args.func(parser, args, context))
main()

97
weegee/config.py Normal file
View File

@ -0,0 +1,97 @@
from __future__ import annotations
from logging import getLogger
import os.path
from dataclasses import dataclass
from typing import Optional as O
from .dazy import Instance, Config, Item, Meta
from .desc import (
WEEGEE_CONFIG,
)
logger = getLogger()
@dataclass
class WeegeeConfig:
BASE = WEEGEE_CONFIG
item: Item
merges: list[tuple[str, Config]]
context: O['WeegeeContext'] = None
GLOBAL_PATH = '/etc/weegee.conf'
USER_PATH = os.path.expanduser('~/.weegee.conf')
LOCAL_PATH = os.path.abspath('./weegee.conf')
@classmethod
def get_meta(cls) -> Meta:
return Meta.parse(None, cls.BASE.get_name(), cls.BASE.spec)
@classmethod
def create(cls) -> 'WeegeeConfig':
meta = cls.get_meta()
item = Item.make(None, '').resolve(meta)
return cls(item, [(None, item, meta)])
@classmethod
def find(cls) -> 'WeegeeConfig':
self = cls.create()
for path in (cls.GLOBAL_PATH, cls.USER_PATH, cls.LOCAL_PATH):
if os.path.isfile(path):
self.load(path)
else:
self.merges.append((path, self.item, None))
return self
def load(self, path: str) -> None:
item = Item.loadfile(path, path)
self.item = item.resolve(self.item)
self.merges.append((path, self.item, item))
def save(self, path: O[str] = None) -> None:
if not path:
if len(self.merges) <= 1:
raise ValueError('must supply filename to save to')
path = self.merges[-1][0]
unmerges = [self.merges[-2][1]]
else:
for i, (mpath, _, _) in enumerate(self.merges):
if mpath == path:
unmerges = [self.merges[i-1][1]] + [partial for _, _, partial in self.merges[i + 1:]]
break
else:
unmerges = self.merges[-1][1]
item = self.item
for config in reversed(unmerges):
if config:
item = item.unresolve(config)
item.savefile(path)
def __dir__(self) -> list[str]:
return super().__dir__() + list(self.item.values)
def __getattr__(self, name: str) -> Any:
return self.item[name]
def __setattr__(self, name: str, value: Any) -> None:
if 'item' in self.__dict__ and name in self.item:
self.item[name] = value
else:
super().__setattr__(name, value)
class WeegeeContext:
config: WeegeeConfig
instance: Instance
def __init__(self, config: O[WeegeeConfig]) -> None:
self.config = config or WeegeeConfig.create()
self.config.context = self
self.instance = Instance(self.config.meta_path, self.config.data_path or self.config.meta_path)
logger.setLevel(self.config.log_level.upper())
def get_config(self) -> WeegeeConfig:
return self.config

462
weegee/core.py Normal file
View File

@ -0,0 +1,462 @@
from __future__ import annotations
from logging import getLogger
from dataclasses import dataclass
from typing import Optional as O, Any
from .dazy import Meta, Item, Template
from .wireguard import (
IPAddress, IPNetwork, IPInterface,
WireguardHostType, WireguardHost, WireguardPeer, WireguardConnection, which_connection,
)
from .config import WeegeeContext
from .desc import (
WEEGEE_HOOK, WEEGEE_HOST, WEEGEE_INTERFACE, WEEGEE_PEER, WEEGEE_CONNECTION,
WEEGEE_INTERFACE_CONF_WG, WEEGEE_PEER_CONF_WG, WEEGEE_CONF_WG,
)
logger = getLogger(__name__)
@dataclass(eq=False)
class WeegeeBase:
BASE = None
context: WeegeeContext
item: Item
meta: Meta = None
def __post_init__(self) -> None:
if not self.meta:
self.meta = self.get_meta(self.context)
self.item = self.item.resolve(self.meta)
@classmethod
def get_name(cls, name: str) -> str:
return f'{cls.BASE.item_prefix}/{name}'
@classmethod
def get_meta(cls, context: WeegeeContext) -> Meta:
return Meta.load(context.instance, cls.BASE.get_name())
@classmethod
def exists(cls, context: WeegeeContext, name: str) -> bool:
return Item.exists(context.instance, cls.get_name(name))
@classmethod
def create(cls, context: WeegeeContext, name: str, **kwargs) -> 'WeegeeBase':
meta = cls.get_meta(context)
item = Item.make(context.instance, cls.get_name(name), **kwargs)
if not item.resolve(meta).is_value_complete(meta):
raise TypeError('internal error')
return cls(context, item, meta)
@classmethod
def find_all(cls, context: WeegeeContext) -> list['WeegeeBase']:
return cls.find(context, '*')
@classmethod
def find(cls, context: WeegeeContext, pattern: str) -> list['WeegeeBase']:
return [cls(context, Item.load(context.instance, name)) for name in Item.filter(context.instance, cls.get_name(pattern))]
@classmethod
def load(cls, context: WeegeeContext, name: str, path: O[str] = None) -> 'WeegeeBase':
return cls(context, Item.load(context.instance, cls.get_name(name)) if not path else Item.loadfile(name, path))
def save(self, path: O[str] = None) -> None:
unresolved = self.item.unresolve(self.meta)
if path:
unresolved.savefile(path)
else:
unresolved.save()
def delete(self) -> None:
self.item.delete()
@property
def name(self) -> str:
return self.item_name[len(self.get_name('')):]
@property
def item_name(self) -> str:
return self.item.name
def __getattr__(self, name: str) -> Any:
if name not in self.item:
raise AttributeError(name)
return self.item[name]
def __setattr__(self, name: str, value: Any) -> None:
if 'item' in self.__dict__ and self.item.is_type_resolved(name):
self.item[name] = value
else:
super().__setattr__(name, value)
def __dir__(self) -> list[str]:
return super().__dir__() + list(self.item.values)
def __eq__(self, other: Any) -> bool:
return isinstance(other, self.__class__) and self.item.name == other.item.name
def __hash__(self) -> int:
return hash((self.__class__.__name__, self.item_name))
@dataclass(eq=False)
class WeegeeHook(WeegeeBase):
BASE = WEEGEE_HOOK
@classmethod
def create(cls, ctx: WeegeeContext, name: str, pre: list[str], post: list[str]) -> 'WeegeeHook':
return super().create(ctx, name,
pre=pre, post=post,
)
@dataclass
class WeegeeHookRunner:
hooks: list[WeegeeHook]
conn: WireguardConnection
kwargs: dict[str, Any]
def _run(self, cmd: str) -> None:
for k, v in self.kwargs.items():
cmd = cmd.replace('%' + k, str(v))
return self.conn._run(cmd, shell=True)
def __enter__(self) -> 'WeegeeHookRunner':
for h in self.hooks:
for cmd in h.pre:
self._run(cmd)
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if not exc_type:
for h in self.hooks:
for cmd in h.post:
self._run(cmd)
@dataclass(eq=False)
class WeegeeHost(WeegeeBase):
BASE = WEEGEE_HOST
LOCAL_HOST_NAME = 'local'
conn: WireguardHost = None
@classmethod
def create(cls, ctx: WeegeeContext, name: str, type: WireguardHostType = WireguardHostType.Unsupported, host: O[str] = None, user: O[str] = None, elevate_user: O[str] = None, automanage: bool = False, autosync: bool = False) -> 'WeegeeHost':
return super().create(ctx, name,
automanage=automanage,
autosync=autosync,
type=type.value,
host=host,
user=user,
elevate_user=elevate_user,
)
@property
def type(self) -> WireguardHostType:
return WireguardHostType(self.item['type'])
def __post_init__(self) -> None:
super().__post_init__()
self.conn = WireguardHost(WeegeeHookedWireguardConnection(self, which_connection(self.type)(self.host, self.user, self.elevate_user)))
@classmethod
def get_local_host(cls, ctx: WeegeeContext) -> 'WeegeeHost':
return cls.load(ctx, cls.LOCAL_HOST_NAME)
@classmethod
def list_hooks(cls) -> list[str]:
return ['interface_add', 'interface_del', 'address_add', 'address_del', 'route_add', 'route_del', 'configure']
def get_hooks(self, name: str, **kwargs) -> WeegeeHookRunner:
return WeegeeHookRunner([WeegeeHook(self.context, x) for x in getattr(self, f'{name}_hooks')], self.conn.conn, kwargs)
def get_peers(self, name: str) -> O[list[WireguardPeer]]:
interface = self.conn.get_interface(name)
if not interface:
return None
return interface.list_peers()
def sync_interface(self, name: str, addresses: list[IPInterface], routes: list[IPNetwork], config: str) -> None:
interface = self.conn.get_interface(name)
if not interface:
if not self.automanage:
return
interface = self.conn.create_interface(name)
if self.automanage:
interface.sync(addresses, routes)
interface.sync_config(config)
def delete_interface(self, name: str) -> None:
interface = self.conn.get_interface(name)
if interface:
interface.clear_config()
if self.automanage:
interface.delete()
@dataclass
class WeegeeHookedWireguardConnection(WireguardConnection):
host: WireguardHost
inner: WireguardConnection
def _run(self, *args, **kwargs) -> str:
return self.inner._run(*args, **kwargs)
def has_interface(self, name: str) -> bool:
return self.inner.has_interface(name)
def create_interface(self, name: str) -> None:
with self.host.get_hooks('interface_add', i=name):
return self.inner.create_interface(name)
def destroy_interface(self, name: str) -> None:
with self.host.get_hooks('interface_del', i=name):
return self.inner.destroy_interface(name)
def set_mtu(self, name: str, mtu: int) -> None:
return self.inner.set_mtu(name, mtu)
def set_up(self, name: str) -> None:
return self.inner.set_up(name)
def set_down(self, name: str) -> None:
return self.inner.set_down(name)
def get_addresses(self, name: str) -> list[IPInterface]:
return self.inner.get_addresses(name)
def add_address(self, name: str, address: IPInterface) -> None:
with self.host.get_hooks('address_add', i=name, a=str(address)):
return self.inner.add_address(name, address)
def delete_address(self, name: str, address: IPInterface) -> None:
with self.host.get_hooks('address_del', i=name, a=str(address)):
return self.inner.delete_address(name, address)
def delete_all_addresses(self, name: str) -> None:
for address in self.get_addresses(name):
self.delete_address(name, address)
def get_routes(self, name: str) -> list[IPNetwork]:
return self.inner.get_routes(name)
def add_route(self, name: str, route: IPNetwork) -> None:
with self.host.get_hooks('route_add', i=name, r=str(route)):
return self.inner.add_route(name, route)
def delete_route(self, name: str, route: IPNetwork) -> None:
with self.host.get_hooks('route_del', i=name, r=str(route)):
return self.inner.delete_route(name, route)
def delete_all_routes(self, name: str) -> None:
for route in self.get_routes(name):
self.delete_route(name, route)
@dataclass(eq=False)
class WeegeeInterface(WeegeeBase):
BASE = WEEGEE_INTERFACE
@classmethod
def find_for_hosts(cls, ctx: WeegeeContext, hosts: set[WeegeeHost]) -> set['WeegeeInterface']:
return set(i for i in cls.find_all(ctx) if set(i.hosts) & hosts)
@classmethod
def create(cls, ctx: WeegeeContext, name: str, interface_name: str, private_key: O[str] = None, public_key: O[str] = None, port: O[int] = None, hosts: O[list[WeegeeHost]] = None, addresses: list[IPInterface] = []) -> 'WeegeeInterface':
for host in [WeegeeHost.get_local_host(ctx)] + hosts:
try:
private_key = private_key or host.conn.gen_private_key()
public_key = public_key or host.conn.get_public_key(private_key)
break
except Exception as e:
logger.warn(f'could not generate keypair on host {host.name!r}: {e!r}')
else:
logger.critical('could not generate public/private keypair automatically: please pass explicitly!')
raise ValueError()
return super().create(ctx, name,
hosts=[h.item for h in hosts],
interface_name=interface_name,
public_key=public_key, private_key=private_key,
addresses=addresses, port=port,
)
def save(self) -> None:
for host in self.hosts:
host.save()
super().save()
def delete(self) -> None:
peers = WeegeePeer.find_for_interfaces(self.context, {self})
for p in peers:
p.delete()
for h in self.hosts:
if h.autosync:
h.delete_interface(self.interface_name)
super().delete()
@property
def hosts(self) -> list[WeegeeHost]:
return [WeegeeHost(self.context, x) for x in self.item['hosts']]
def gen_config(self) -> str:
template = Template.load(self.context.instance, WEEGEE_INTERFACE_CONF_WG.get_name())
config = WEEGEE_INTERFACE_CONF_WG.make_config(self.context.instance,
interface=self.item,
)
return template.render(config)
@dataclass(eq=False)
class WeegeePeer(WeegeeBase):
BASE = WEEGEE_PEER
@classmethod
def find_for_interfaces(cls, ctx: WeegeeContext, interfaces: set[WeegeeInterface]) -> set['WeegeePeer']:
return set(p for p in cls.find_all(ctx) if p.interface in interfaces)
@classmethod
def create(cls, ctx: WeegeeContext, name: str, interface: WeegeeInterface, routes: list[IPNetwork], host: O[str] = None, port: O[int] = None) -> 'WeegeePeer':
return super().create(ctx, name,
interface=interface.item,
routes=routes,
host=host, port=port,
)
def save(self) -> None:
self.interface.save()
super().save()
self.sync(auto=True)
def sync(self, auto=False, log=None) -> None:
if not log:
log = set()
if self.item_name in log:
return
log.add(self.item_name)
logger.info(f'syncing peer: {self.name}')
sync_interface(self.interface, auto=auto, log=log)
def delete(self) -> None:
for c in WeegeeConnection.find_for_peers(self.context, {self}):
c.peers.remove(self)
if len(c.peers) <= 1:
c.delete()
else:
c.save()
super().delete()
@property
def interface(self) -> WeegeeInterface:
return WeegeeInterface(self.context, self.item['interface'])
@dataclass(eq=False)
class WeegeeConnection(WeegeeBase):
BASE = WEEGEE_CONNECTION
@classmethod
def find_for_peers(cls, ctx: WeegeeContext, peers: set[WeegeePeer]) -> set['WeegeeConnection']:
return set(c for c in cls.find_all(ctx) if set(c.peers) & peers)
@classmethod
def create(cls, ctx: WeegeeContext, name: str, peers: list[WeegeePeer], preshared_key: O[str] = None) -> 'WeegeeConnecton':
if not preshared_key:
hosts = {WeegeeHost.get_local_host(ctx)}
for p in peers:
hosts.update(p.interface.hosts)
for host in hosts:
try:
preshared_key = host.conn.gen_preshared_key()
break
except Exception as e:
logger.warn(f'could not generate preshared key on host {host.name!r}: {e!r}')
else:
logger.critical('could not generate preshared key automatically: please pass explicitly!')
raise ValueError()
return super().create(ctx, name,
peers=[p.item for p in peers],
preshared_key=preshared_key,
)
def save(self) -> None:
for peer in self.peers:
peer.save()
super().save()
@property
def peers(self) -> list[WeegeePeer]:
return [WeegeePeer(self.context, x) for x in self.item['peers']]
def gen_config(self, target: WeegeePeer) -> str:
peers = [p for p in self.peers if p != target]
template = Template.load(self.context.instance, WEEGEE_PEER_CONF_WG.get_name())
config = WEEGEE_PEER_CONF_WG.make_config(self.context.instance,
connection=self.item,
peers={p.name: p.item for p in peers},
)
return template.render(config)
def do_config_interface(interface: WeegeeInterface, peers: set[WeegeePeer], connections: set[WeegeeConnection]) -> str:
peer_configs = []
for c in connections:
for p in set(c.peers) & peers:
peer_configs.append(c.gen_config(p))
interface_config = interface.gen_config()
template = Template.load(interface.context.instance, WEEGEE_CONF_WG.get_name())
config = WEEGEE_CONF_WG.make_config(interface.context.instance,
interface_config=interface_config,
peer_configs=peer_configs,
)
return template.render(config)
def do_discover_interface(interface: WeegeeInterface, peers: set[WeegeePeer], connections: set[WeegeeConnection]) -> tuple[list[IPNetwork], set[WeegeePeer]]:
routes = []
other_peers = set()
for c in connections:
for p in set(c.peers) - peers:
routes.extend(p.routes)
routes.extend(a.network for a in p.interface.addresses)
other_peers.add(p)
return routes, other_peers
def find_host_interfaces(host: WeegeeHost) -> set[WeegeeInterface]:
return WeegeeInterface.find_for_hosts(host.context, {host})
def find_interface_connections(interface: WeegeeInterface) -> tuple[set[WeegeePeer], set[WeegeeConnection]]:
peers = WeegeePeer.find_for_interfaces(interface.context, {interface})
connections = WeegeeConnection.find_for_peers(interface.context, peers)
return peers, connections
def find_interface_other_peers(interface: WeegeeInterface) -> set[WeegeePeer]:
own_peers, connections = find_interface_connections(interface)
_, other_peers = do_discover_interface(interface, own_peers, connections)
return other_peers
def do_sync_interface(interface: WeegeeInterface, peers: set[WeegeePeer], connections: set[WeegeeConnection], auto: bool = False) -> set[WeegeePeer]:
config = do_config_interface(interface, peers, connections)
routes, other_peers = do_discover_interface(interface, peers, connections)
for host in interface.hosts:
if auto and not host.autosync:
continue
host.sync_interface(interface.interface_name, interface.addresses, routes, config)
return other_peers
def sync_interface(interface: WeegeeInterface, auto=False, log=None) -> None:
if log is None:
log = set()
if interface.item_name in log:
return
log.add(interface.item_name)
logger.info(f'syncing interface: {interface.name}')
all_peers, all_connections = find_interface_connections(interface)
other_peers = do_sync_interface(interface, all_peers, all_connections, auto=auto)
for p in other_peers:
sync_interface(p.interface, auto=auto, log=log)
def sync_all_interfaces(context: WeegeeContext, auto=False, log=None) -> None:
if log is None:
log = set()
for interface in WeegeeInterface.find_all(context):
sync_interface(interface, auto=auto, log=log)

View File

@ -85,8 +85,11 @@ WEEGEE_CONFIG = WeegeeMeta(
name='wg/config',
version=1,
spec=[
f'default_server_hosts: [@{WEEGEE_HOST.get_name()}] = []',
f'default_client_hosts: [@{WEEGEE_HOST.get_name()}] = []',
f'default_server_hosts: [str] = []',
f'default_client_hosts: [str] = []',
'log_level: str = "info"',
'meta_path: str = "."',
'data_path: ?str = ',
],
)

60
weegee/extra.py Normal file
View File

@ -0,0 +1,60 @@
from __future__ import annotations
from logging import getLogger
from dataclasses import dataclass
from typing import Optional as O, Any
from .config import WeegeeContext
from .core import (
WeegeePeer, WeegeeConnection,
sync_all_interfaces, find_interface_connections, do_config_interface,
)
logger = getLogger(__name__)
@dataclass(eq=False)
class WeegeeServer(WeegeePeer):
def get_client_name(self, name: str) -> str:
return f'{self.name}/{name}'
def get_clients(self) -> list[WeegeePeer]:
return WeegeeClient.find(self.context, self.get_client_name('*'))
def get_client(self, name: str) -> O[WeegeePeer]:
return WeegeeClient.load(self.context, self.get_client_name(name))
def delete(self) -> None:
super().delete()
sync_all_interfaces(self.context, auto=True)
def gen_config(self) -> str:
peers, connections = find_interface_connections(self.interface)
return do_config_interface(self.interface, peers, connections)
@dataclass(eq=False)
class WeegeeClient(WeegeePeer):
@classmethod
def create(cls, ctx: WeegeeContext, name: str, server: WeegeeServer, preshared_key: O[str] = None, **kwargs) -> 'WeegeeClient':
name = server.get_client_name(name)
client = super().create(ctx, name, **kwargs)
connection = WeegeeConnection.create(ctx, name, peers=[server, client], preshared_key=preshared_key)
connection.save()
return client
@property
def connection(self) -> WeegeeConnection:
return WeegeeConnection.load(self.context, self.name)
@property
def server(self) -> WeegeeServer:
return next(p for p in self.connection.peers if p != self.peer)
def delete(self) -> None:
self.connection.delete()
super().delete()
sync_all_interfaces(self.context, auto=True)
def gen_config(self) -> str:
peers, connections = find_interface_connections(self.interface)
return do_config_interface(self.interface, peers, connections)