async def test_unmount_error( gstate: GlobalState, fs: fake_filesystem.FakeFilesystem, mocker: MockerFixture, ) -> None: async def mock_call( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: raise Exception("Failed unmount.") mocker.patch( "gravel.controllers.nodes.systemdisk.aqr_run_cmd", new=mock_call ) from gravel.controllers.nodes.systemdisk import MountError, SystemDisk systemdisk = SystemDisk(gstate) throws = False try: await systemdisk.unmount() except MountError as e: assert "does not exist" in e.message throws = True assert throws fs.create_dir("/var/lib/aquarium-system") throws = False try: await systemdisk.unmount() except MountError as e: assert "failed unmount" in e.message.lower() throws = True assert throws
def test_update_yaml(fs: FakeFilesystem) -> None: """ Test ``update_yaml``. """ path = Path("/path/to/blog/test.yaml") fs.create_dir(path.parent) # create new file with update_yaml(path) as config: config["foo"] = "bar" # check contents with update_yaml(path) as config: assert config["foo"] == "bar" # test error with open(path, "w", encoding="utf-8") as output: output.write("{{ test }}") with pytest.raises(yaml.constructor.ConstructorError) as excinfo: with update_yaml(path) as config: pass assert (str(excinfo.value) == """while constructing a mapping in "<unicode string>", line 1, column 1: {{ test }} ^ found unhashable key in "<unicode string>", line 1, column 2: {{ test }} ^""")
def test_init_state_fail(gstate: GlobalState, fs: fake_filesystem.FakeFilesystem) -> None: from gravel.controllers.nodes.mgr import NodeError if fs.exists("/etc/aquarium/node.json"): fs.remove("/etc/aquarium/node.json") nodemgr = NodeMgr(gstate) assert fs.exists("/etc/aquarium/node.json") for f in fs.listdir("/etc/aquarium"): fs.remove(f"/etc/aquarium/{f}") assert fs.exists("/etc/aquarium") fs.rmdir("/etc/aquarium") fs.create_dir("/etc/aquarium", perm_bits=0o500) throws = False try: nodemgr._init_state() except NodeError: throws = True assert throws # clean up for f in fs.listdir("/etc/aquarium"): fs.remove(f"/etc/aquarium/{f}") fs.rmdir("/etc/aquarium")
def test_find_modified_files( fs: FakeFilesystem, root: Path, config: Config, ) -> None: """ Test ``find_modified_files``. """ publisher = Publisher(root, config, "generic") fs.create_dir(root / "build/generic") fs.create_dir(root / "build/generic/subdir") with freeze_time("2021-01-01T00:00:00Z"): (root / "build/generic/one").touch() with freeze_time("2021-01-02T00:00:00Z"): (root / "build/generic/subdir/two").touch() since = datetime(2021, 1, 1, 12, 0, 0, tzinfo=timezone.utc) modified_files = list( publisher.find_modified_files(force=False, since=since)) assert modified_files == [Path("/path/to/blog/build/generic/subdir/two")] since = datetime(2021, 1, 2, 12, 0, 0, tzinfo=timezone.utc) modified_files = list( publisher.find_modified_files(force=False, since=since)) assert modified_files == [] since = datetime(2021, 1, 2, 12, 0, 0, tzinfo=timezone.utc) modified_files = list( publisher.find_modified_files(force=True, since=since)) assert modified_files == [ Path("/path/to/blog/build/generic/one"), Path("/path/to/blog/build/generic/subdir/two"), ]
def init_fs(fs: FakeFilesystem) -> None: # Fake as Linux so that all slashes in these test are forward fs.os = OSType.LINUX fs.path_separator = "/" fs.is_windows_fs = False fs.is_macos = False # Pre-create the output path, whereas it was not created before running # the detail file test above. fs.create_dir(path.join(OUTPUT_DIRECTORY, RUN_NAME)) # type: ignore
def test_find_directory(fs: FakeFilesystem) -> None: """ Test ``find_directory``. """ fs.create_dir("/path/to/blog/posts/first/css") fs.create_file("/path/to/blog/nefelibata.yaml") path = find_directory(Path("/path/to/blog/posts/first/css")) assert path == Path("/path/to/blog") with pytest.raises(SystemExit) as excinfo: find_directory(Path("/path/to")) assert str(excinfo.value) == "No configuration found!"
def mock_filesystem(fs: FakeFilesystem) -> FakeFilesystem: """A pytest fixture which mocks the filesystem before each test.""" # The "fs" argument triggers pyfakefs' own pytest fixture to register # After pyfakefs has started all filesystem actions will happen on a fake in-memory filesystem # Create a fake home directory and set the cwd to an empty directory fs.create_dir(Path.home() / "testing") os.chdir(Path.home() / "testing") # Reset singletons so that fresh Path instances get created container.reset_singletons() return fs
def cephadm_fs(fs: fake_filesystem.FakeFilesystem, ): """ use pyfakefs to stub filesystem calls """ uid = os.getuid() gid = os.getgid() with mock.patch('os.fchown'), \ mock.patch('cephadm.extract_uid_gid', return_value=(uid, gid)): fs.create_dir(cd.DATA_DIR) fs.create_dir(cd.LOG_DIR) fs.create_dir(cd.LOCK_DIR) fs.create_dir(cd.LOGROTATE_DIR) fs.create_dir(cd.UNIT_DIR) yield fs
def test_fail_ctor(gstate: GlobalState, fs: fake_filesystem.FakeFilesystem) -> None: from gravel.controllers.nodes.mgr import NodeError if fs.exists("/etc/aquarium/node.json"): fs.remove("/etc/aquarium/node.json") fs.create_dir("/etc/aquarium/node.json") throws = False try: NodeMgr(gstate) except NodeError: throws = True except Exception: assert False assert throws # clean up fs.rmdir("/etc/aquarium/node.json")
def root(fs: FakeFilesystem) -> Iterator[Path]: """ Create the blog root directory. """ # Add the templates to the fake filesystem, so builders can load them # during tests. The actual path depends if we're running in development # mode (``src/``) or installed (``site-packages``). root = get_project_root() locations = ("src/nefelibata/templates", "site-packages/nefelibata/templates") for location in locations: try: fs.add_real_directory(root / location) except FileNotFoundError: pass root = Path("/path/to/blog") fs.create_dir(root) yield root
async def test_get_posts(fs: FakeFilesystem, root: Path, config: Config) -> None: """ Test ``get_posts``. """ with freeze_time("2021-01-01T00:00:00Z"): fs.create_dir(root / "posts/one") fs.create_file(root / "posts/one/index.mkd") with freeze_time("2021-01-02T00:00:00Z"): fs.create_dir(root / "posts/two") fs.create_file(root / "posts/two/index.mkd") posts = get_posts(root, config) assert len(posts) == 2 assert posts[0].path == Path(root / "posts/two/index.mkd") assert posts[1].path == Path(root / "posts/one/index.mkd") # test limited number of posts returned posts = get_posts(root, config, 1) assert len(posts) == 1
def fake_filesystem(fs: FakeFilesystem) -> FakeFilesystem: """A pytest fixture which mocks the filesystem before each test.""" # The "fs" argument triggers pyfakefs' own pytest fixture to register # After pyfakefs has started all filesystem actions will happen on a fake in-memory filesystem # Proxy access to certifi's certificate authority bundle to the real filesystem # This is required to be able to send HTTP requests using requests fs.add_real_file(certifi.where()) # Proxy access to package data to the real filesystem fs.add_real_directory(os.path.join(os.path.dirname(__file__), "../lean/ssh")) # Create a fake home directory and set the cwd to an empty directory fs.create_dir(Path.home() / "testing") os.chdir(Path.home() / "testing") # Reset all singletons so Path instances get recreated # Path instances are bound to the filesystem that was active at the time of their creation # When the filesystem changes, old Path instances bound to previous filesystems may cause weird behavior container.reset_singletons() return fs
def host_sysfs(fs: fake_filesystem.FakeFilesystem): """Create a fake filesystem to represent sysfs""" enc_path = '/sys/class/scsi_generic/sg2/device/enclosure/0:0:1:0' dev_path = '/sys/class/scsi_generic/sg2/device' slot_count = 12 fs.create_dir(dev_path) fs.create_file(os.path.join(dev_path, 'vendor'), contents="EnclosuresInc") fs.create_file(os.path.join(dev_path, 'model'), contents="D12") fs.create_file(os.path.join(enc_path, 'id'), contents='1') fs.create_file(os.path.join(enc_path, 'components'), contents=str(slot_count)) for slot_num in range(slot_count): slot_dir = os.path.join(enc_path, str(slot_num)) fs.create_file(os.path.join(slot_dir, 'locate'), contents='0') fs.create_file(os.path.join(slot_dir, 'fault'), contents='0') fs.create_file(os.path.join(slot_dir, 'slot'), contents=str(slot_num)) if slot_num < 6: fs.create_file(os.path.join(slot_dir, 'status'), contents='Ok') slot_dev = os.path.join(slot_dir, 'device') fs.create_dir(slot_dev) fs.create_file(os.path.join(slot_dev, 'vpd_pg80'), contents=f'fake{slot_num:0>3}') else: fs.create_file(os.path.join(slot_dir, 'status'), contents='not installed') yield fs
def dataset_dir(fs: FakeFilesystem): path = "/tmp/dataset" fs.create_dir(path) return path
async def test_init(fs: fake_filesystem.FakeFilesystem) -> None: from gravel.controllers.deployment.mgr import ( AlreadyInitedError, DeploymentMgr, InitError, InitStateEnum, NotPreInitedError, ) mgr = DeploymentMgr() raised: bool = False try: await mgr.init() except NotPreInitedError: raised = True except Exception: assert False assert raised mgr._preinited = True mgr._inited = True raised = False try: await mgr.init() except AlreadyInitedError: raised = True except Exception: assert False assert raised mgr._preinited = True mgr._inited = False await mgr.init() # succeeds because we're not installed yet. assert mgr._init_state < InitStateEnum.INSTALLED # test canonical case, we're installed but don't have a state yet. fs.reset() mgr = DeploymentMgr() mgr._preinited = True mgr._inited = False mgr._init_state = InitStateEnum.INSTALLED assert not fs.exists("/etc/aquarium") await mgr.init() assert mgr._inited assert fs.exists("/etc/aquarium/") assert fs.isdir("/etc/aquarium") assert fs.exists("/etc/aquarium/state.json") # ensure we have a malformed file in /etc/aquarium/state.json fs.reset() mgr = DeploymentMgr() mgr._preinited = True mgr._inited = False mgr._init_state = InitStateEnum.INSTALLED fs.create_dir("/etc/aquarium") with open("/etc/aquarium/state.json", "w") as f: f.write("foobarbaz") raised = False try: await mgr.init() except InitError: raised = True except Exception: assert False assert raised # now we have something in state.json that is not a file fs.reset() mgr = DeploymentMgr() mgr._preinited = True mgr._inited = False mgr._init_state = InitStateEnum.INSTALLED fs.create_dir("/etc/aquarium/state.json") raised = False try: await mgr.init() except InitError: raised = True except Exception: assert False assert raised
async def aquarium_startup( get_data_contents: Callable[[str, str], str], mocker: MockerFixture, fs: fake_filesystem.FakeFilesystem, ): # Need the following to fake up KV mock_ceph_modules(mocker) fs.create_dir("/var/lib/aquarium") async def startup(aquarium_app: FastAPI, aquarium_api: FastAPI): from fastapi.logger import logger as fastapi_logger from gravel.cephadm.cephadm import Cephadm from gravel.controllers.inventory.inventory import Inventory from gravel.controllers.nodes.deployment import NodeDeployment from gravel.controllers.nodes.errors import NodeCantDeployError from gravel.controllers.nodes.mgr import ( NodeError, NodeInitStage, NodeMgr, ) from gravel.controllers.orch.ceph import Ceph, Mgr, Mon from gravel.controllers.resources.devices import Devices from gravel.controllers.resources.status import Status from gravel.controllers.resources.storage import Storage logger: logging.Logger = fastapi_logger class FakeNodeDeployment(NodeDeployment): # Do we still need this thing since removing etcd? pass class FakeNodeMgr(NodeMgr): def __init__(self, gstate: GlobalState): super().__init__(gstate) self._deployment = FakeNodeDeployment(gstate, self._connmgr) async def start(self) -> None: assert self._state logger.debug(f"start > {self._state}") if not self.deployment_state.can_start(): raise NodeError("unable to start unstartable node") assert self._init_stage == NodeInitStage.NONE if self.deployment_state.nostage: await self._node_prepare() else: assert (self.deployment_state.ready or self.deployment_state.deployed) assert self._state.hostname assert self._state.address await self.gstate.store.ensure_connection() async def _obtain_images(self) -> bool: return True class FakeCephadm(Cephadm): def __init__(self): super().__init__(ContainersOptionsModel()) async def call( self, cmd: List[str], noimage: bool = False, outcb: Optional[Callable[[str], None]] = None, ) -> Tuple[str, str, int]: # Implement expected calls to cephadm with testable responses if cmd[0] == "pull": return "", "", 0 elif cmd[0] == "gather-facts": return ( get_data_contents(DATA_DIR, "gather_facts_real.json"), "", 0, ) elif cmd == ["ceph-volume", "inventory", "--format", "json"]: return ( get_data_contents(DATA_DIR, "inventory_real.json"), "", 0, ) else: print(cmd) print(outcb) raise Exception("Tests should not get here") class FakeCeph(Ceph): def __init__(self, conf_file: str = "/etc/ceph/ceph.conf"): self.conf_file = conf_file self._is_connected = False def connect(self): if not self.is_connected(): self.cluster = mocker.Mock() self._is_connected = True class FakeStorage(Storage): # type: ignore available = 2000 # type: ignore total = 2000 # type: ignore gstate: GlobalState = GlobalState(FakeKV) # init node mgr nodemgr: NodeMgr = FakeNodeMgr(gstate) # Prep cephadm cephadm: Cephadm = FakeCephadm() gstate.add_cephadm(cephadm) # Set up Ceph connections ceph: Ceph = FakeCeph() ceph_mgr: Mgr = Mgr(ceph) gstate.add_ceph_mgr(ceph_mgr) ceph_mon: Mon = Mon(ceph) gstate.add_ceph_mon(ceph_mon) # Set up all of the tickers devices: Devices = Devices( gstate.config.options.devices.probe_interval, nodemgr, ceph_mgr, ceph_mon, ) gstate.add_devices(devices) status: Status = Status(gstate.config.options.status.probe_interval, gstate, nodemgr) gstate.add_status(status) inventory: Inventory = Inventory( gstate.config.options.inventory.probe_interval, nodemgr, gstate) gstate.add_inventory(inventory) storage: Storage = FakeStorage( gstate.config.options.storage.probe_interval, nodemgr, ceph_mon) gstate.add_storage(storage) await nodemgr.start() await gstate.start() # Add instances into FastAPI's state: aquarium_api.state.gstate = gstate aquarium_api.state.nodemgr = nodemgr yield startup
def test_get_enclosures(mocker: MockerFixture, fs: FakeFilesystem, root: Path) -> None: """ Test ``get_enclosures``. """ MP3 = mocker.patch("nefelibata.enclosure.MP3") metadata = { "TIT2": "A title", "TPE1": "An artist", "TALB": "An album", "TDRC": 2021, "TRCK": 1, } MP3.return_value.get.side_effect = metadata.get MP3.return_value.info.length = 123.0 piexif = mocker.patch("nefelibata.enclosure.piexif") piexif.ImageIFD.ImageDescription = 270 piexif.load.return_value = {"0th": {270: "This is a nice photo"}} fs.create_dir(root / "posts/first") path = root / "posts/first/index.mkd" # create supported files (root / "posts/first/song.mp3").touch() (root / "posts/first/photo.jpg").touch() (root / "posts/first/logo.png").touch() # create non-supported file (root / "posts/first/test.txt").touch() enclosures = get_enclosures(root, path.parent) assert len(enclosures) == 3 assert enclosures[0].dict() == { "path": Path("/path/to/blog/posts/first/song.mp3"), "description": '"A title" (2m3s) by An artist (An album, 2021)', "type": "audio/mpeg", "length": 0, "href": "first/song.mp3", "title": "A title", "artist": "An artist", "album": "An album", "year": 2021, "duration": 123.0, "track": 0, } assert enclosures[1].dict() == { "description": "This is a nice photo", "href": "first/photo.jpg", "length": 0, "path": Path("/path/to/blog/posts/first/photo.jpg"), "type": "image/jpeg", } assert enclosures[2].dict() == { "description": "Image logo.png", "href": "first/logo.png", "length": 0, "path": Path("/path/to/blog/posts/first/logo.png"), "type": "image/png", }
async def test_enable( gstate: GlobalState, fs: fake_filesystem.FakeFilesystem, mocker: MockerFixture, ) -> None: from gravel.controllers.nodes.systemdisk import ( MountError, OverlayError, SystemDisk, ) async def mount_fail() -> None: raise MountError("Failed mount.") overlayed_paths = [] bindmounts = [] async def mock_call( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: if cmd[2] == "overlay": assert "lower" in cmd[4] lowerstr = (cmd[4].split(","))[0] lower = (lowerstr.split("="))[1] overlayed_paths.append(lower) elif cmd[1] == "--bind": assert len(cmd) == 4 bindmounts.append(cmd[3]) else: raise Exception(f"Unknown call: {cmd}") return 0, None, None # ensure we don't have a mounted fs fs.add_real_file( source_path=os.path.join(DATA_DIR, "mounts_without_aquarium.raw"), target_path="/proc/mounts", ) systemdisk = SystemDisk(gstate) assert not systemdisk.mounted systemdisk.mount = mount_fail mocker.patch( "gravel.controllers.nodes.systemdisk.aqr_run_cmd", new=mock_call ) throws = False try: await systemdisk.enable() except OverlayError as e: assert "failed mount" in e.message.lower() throws = True assert throws systemdisk.mount = mocker.AsyncMock() for upper in systemdisk._overlaydirs.keys(): fs.create_dir(f"/var/lib/aquarium-system/{upper}/overlay") fs.create_dir(f"/var/lib/aquarium-system/{upper}/temp") for ours in systemdisk._bindmounts.keys(): fs.create_dir(f"/var/lib/aquarium-system/{ours}") await systemdisk.enable() for lower in systemdisk._overlaydirs.values(): assert fs.exists(lower) assert lower in overlayed_paths for theirs in systemdisk._bindmounts.values(): assert fs.exists(theirs) assert theirs in bindmounts
async def test_network( mocker: MockerFixture, fs: fake_filesystem.FakeFilesystem, ) -> None: async def mock_restart_network( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: assert cmd == ["systemctl", "restart", "network.service"] return 0, None, None mocker.patch( "gravel.controllers.resources.network.aqr_run_cmd", new=mock_restart_network, ) from gravel.controllers.resources.network import ( InterfaceConfigModel, InterfaceModel, Network, RouteModel, ) fs.create_dir( "/sys/devices/pci0000:00/0000:00:02.0/0000:01:00.0/net/enp1s0") fs.create_symlink( "/sys/class/net/enp1s0/device", "/sys/devices/pci0000:00/0000:00:02.0/0000:01:00.0/net/enp1s0", ) fs.create_dir("/sys/class/net/virbr0") fs.add_real_file( source_path=os.path.join(DATA_DIR, "ifcfg-eth0"), target_path="/etc/sysconfig/network/ifcfg-eth0", ) fs.create_file("/etc/sysconfig/network/ifcfg-eth0~") fs.create_file("/etc/sysconfig/network/ifcfg-eth0.rpmsave") fs.create_file("/etc/sysconfig/network/ifcfg-lo") fs.add_real_file( source_path=os.path.join(DATA_DIR, "config"), target_path="/etc/sysconfig/network/config", ) network = Network(5.0) assert await network._should_tick() await network._do_tick() ifaces = network.interfaces assert len(ifaces) > 0 assert "eth0" in ifaces assert ifaces["eth0"].name == "eth0" assert ifaces["eth0"].config.bootproto == "dhcp" assert ifaces["eth0"].config.ipaddr == "" assert ifaces["eth0"].config.bonding_slaves == [] assert "eth0~" not in ifaces assert "eth0.rpmsave" not in ifaces assert "enp1s0" in ifaces assert ifaces["enp1s0"].config is None assert "virbr0" not in ifaces assert "lo" not in ifaces assert network.nameservers == [] assert network.routes == [] ifaces["bond0"] = InterfaceModel( name="bond0", config=InterfaceConfigModel( bootproto="static", ipaddr="192.168.121.10/24", bonding_slaves=["enp1s0", "enp2s0"], ), ) await network.apply_config( ifaces, ["8.8.8.8", "1.1.1.1"], routes=[ RouteModel(destination="default", gateway="192.168.121.1"), RouteModel(destination="192.168.0.0/16", interface="bond0"), ], ) ifaces = network.interfaces assert "bond0" in ifaces assert os.path.exists("/etc/sysconfig/network/ifcfg-enp1s0") assert os.path.exists("/etc/sysconfig/network/ifcfg-enp2s0") assert network.nameservers == ["8.8.8.8", "1.1.1.1"] assert os.path.exists("/etc/sysconfig/network/routes") assert os.path.exists("/etc/sysconfig/network/ifroute-bond0") assert len(network.routes) == 2
async def aquarium_startup( get_data_contents: Callable[[str, str], str], mocker: MockerFixture, fs: fake_filesystem.FakeFilesystem, ): # Need the following to fake up KV mock_ceph_modules(mocker) fs.create_dir("/var/lib/aquarium") async def startup(aquarium_app: FastAPI, aquarium_api: FastAPI): from fastapi.logger import logger as fastapi_logger from gravel.cephadm.cephadm import Cephadm from gravel.controllers.ceph.ceph import Ceph, Mgr, Mon from gravel.controllers.config import Config from gravel.controllers.inventory.inventory import Inventory from gravel.controllers.nodes.mgr import NodeInitStage, NodeMgr from gravel.controllers.resources.devices import Devices from gravel.controllers.resources.network import Network from gravel.controllers.resources.status import Status from gravel.controllers.resources.storage import Storage logger: logging.Logger = fastapi_logger class FakeNodeMgr(NodeMgr): def __init__(self, gstate: GlobalState): super().__init__(gstate) async def start(self) -> None: assert self._state logger.debug(f"start > {self._state}") assert self._init_stage == NodeInitStage.INITED self._init_stage = NodeInitStage.AVAILABLE async def _obtain_images(self) -> bool: return True class FakeCephadm(Cephadm): def __init__(self): super().__init__() async def call( self, cmd: List[str], noimage: bool = False, outcb: Optional[Callable[[str], None]] = None, ) -> Tuple[str, str, int]: # Implement expected calls to cephadm with testable responses if cmd[0] == "pull": return "", "", 0 elif cmd[0] == "gather-facts": return ( get_data_contents(DATA_DIR, "gather_facts_real.json"), "", 0, ) elif cmd == ["ceph-volume", "inventory", "--format", "json"]: return ( get_data_contents(DATA_DIR, "inventory_real.json"), "", 0, ) else: print(cmd) print(outcb) raise Exception("Tests should not get here") class FakeCeph(Ceph): def __init__(self, conf_file: str = "/etc/ceph/ceph.conf"): self.conf_file = conf_file self._is_connected = False def connect(self): if not self.is_connected(): self.cluster = mocker.Mock() self._is_connected = True class FakeStorage(Storage): # type: ignore available = 2000 # type: ignore total = 2000 # type: ignore config = Config() kvstore = FakeKV() gstate: GlobalState = GlobalState(config, kvstore) config.init() kvstore.init() # init node mgr nodemgr: NodeMgr = FakeNodeMgr(gstate) nodemgr.init() # Prep cephadm cephadm: Cephadm = FakeCephadm() gstate.add_cephadm(cephadm) cephadm.set_config(gstate.config.options.containers) # Set up Ceph connections ceph: Ceph = FakeCeph() ceph_mgr: Mgr = Mgr(ceph) gstate.add_ceph_mgr(ceph_mgr) ceph_mon: Mon = Mon(ceph) gstate.add_ceph_mon(ceph_mon) # Set up all of the tickers devices: Devices = Devices( gstate.config.options.devices.probe_interval, nodemgr, ceph_mgr, ceph_mon, ) gstate.add_devices(devices) status: Status = Status(gstate.config.options.status.probe_interval, gstate, nodemgr) gstate.add_status(status) inventory: Inventory = Inventory( gstate.config.options.inventory.probe_interval, nodemgr, gstate) gstate.add_inventory(inventory) storage: Storage = FakeStorage( gstate.config.options.storage.probe_interval, nodemgr, ceph_mon) gstate.add_storage(storage) network: Network = Network( gstate.config.options.network.probe_interval) gstate.add_network(network) await nodemgr.start() await gstate.start() # Add instances into FastAPI's state: aquarium_api.state.gstate = gstate aquarium_api.state.nodemgr = nodemgr yield startup