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")
async def test_deploy_checks(gstate: GlobalState, nodemgr: NodeMgr) -> None: from gravel.controllers.nodes.deployment import NodeStageEnum from gravel.controllers.nodes.mgr import ( DeployParamsModel, NodeCantDeployError, NodeInitStage, NodeNotStartedError, ) nodemgr._init_stage = NodeInitStage.NONE throws = False try: await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr") ) except NodeNotStartedError: throws = True assert throws nodemgr._init_stage = NodeInitStage.PREPARE throws = False try: await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr") ) except NodeNotStartedError: throws = True assert throws nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._deployment._state._stage = NodeStageEnum.ERROR throws = False try: await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr") ) except NodeCantDeployError: throws = True assert throws nodemgr._deployment._state._stage = NodeStageEnum.NONE throws = False try: await nodemgr.deploy( DeployParamsModel(hostname="", ntpaddr="my.ntp.addr") ) except NodeCantDeployError as e: assert e.message == "missing hostname parameter" throws = True assert throws throws = False try: await nodemgr.deploy(DeployParamsModel(hostname="barbaz", ntpaddr="")) except NodeCantDeployError as e: assert e.message == "missing ntp server address" throws = True assert throws
def test_init(gstate: GlobalState, fs: fake_filesystem.FakeFilesystem) -> None: nodemgr = NodeMgr(gstate) nodemgr.init() assert fs.exists("/etc/aquarium/node.json") # clean up for f in fs.listdir("/etc/aquarium"): fs.remove(f"/etc/aquarium/{f}")
def test_generate_token(gstate: GlobalState) -> None: nodemgr = NodeMgr(gstate) token = nodemgr._generate_token() assert len(token) > 0 res = token.split("-") assert len(res) == 4 for s in res: assert len(s) == 4
def nodemgr(gstate: GlobalState) -> NodeMgr: from gravel.controllers.nodes.mgr import NodeStateModel nodemgr = NodeMgr(gstate) nodemgr._state = NodeStateModel( uuid="bba35d93-d4a5-48b3-804b-99c406555c89", address="1.2.3.4", hostname="foobar", ) yield nodemgr
async def test_deploy_check_disk_solution(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.nodes.disks import DiskSolution from gravel.controllers.nodes.mgr import NodeCantDeployError, NodeInitStage nodemgr._init_stage = NodeInitStage.AVAILABLE def empty_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution() def fail_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution(possible=True) mocker.patch("gravel.controllers.nodes.disks.Disks.gen_solution", new=empty_solution) throws = False try: await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr")) except NodeCantDeployError as e: assert e.message == "No possible deployment solution found." throws = True assert throws mocker.patch("gravel.controllers.nodes.disks.Disks.gen_solution", new=fail_solution) throws = False try: await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr")) except AssertionError: throws = True assert throws
async def test_postbootstrap_config(mocker: MockerFixture, gstate: GlobalState) -> None: config_keys: Dict[str, Tuple[str, str]] = {} def config_set(cls: Any, who: str, name: str, value: str) -> bool: config_keys[name] = (who, value) return True def expect_key(who: str, name: str, value: str) -> None: assert name in config_keys scope, val = config_keys[name] assert scope == who assert val == value from gravel.controllers.nodes.mgr import NodeMgr from gravel.controllers.orch.ceph import Mon mocker.patch.object(NodeMgr, "_init_state") mocker.patch.object(Mon, "config_set", new=config_set) mocker.patch.object(Mon, "call") mocker.patch.object(Mon, "set_default_ruleset") # ignore default ruleset mgr = NodeMgr(gstate) await mgr._post_bootstrap_config() expect_key("global", "mon_allow_pool_size_one", "true") expect_key("global", "mon_warn_on_pool_no_redundancy", "false") expect_key("mgr", "mgr/cephadm/manage_etc_ceph_ceph_conf", "true") expect_key("global", "auth_allow_insecure_global_id_reclaim", "false")
async def aquarium_startup(_: FastAPI, aquarium_api: FastAPI): lvl = "INFO" if not os.getenv("AQUARIUM_DEBUG") else "DEBUG" setup_logging(lvl) logger.info("Aquarium startup!") deployment = DeploymentMgr() try: await deployment.preinit() except DeploymentError as e: logger.error(f"Unable to pre-init the node: {e.message}") sys.exit(1) config: Config = Config() kvstore: KV = KV() gstate: GlobalState = GlobalState(config, kvstore) nodemgr: NodeMgr = NodeMgr(gstate) gstate_preinit(gstate) global _main_task _main_task = asyncio.create_task( aquarium_main_task( aquarium_api, config, kvstore, gstate, nodemgr, deployment ) )
def test_fail_init(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 nodemgr = NodeMgr(gstate) try: nodemgr.init() except NodeError: throws = True except Exception: assert False assert throws # clean up fs.rmdir("/etc/aquarium/node.json")
async def aquarium_main_task( app: FastAPI, config: Config, kvstore: KV, gstate: GlobalState, nodemgr: NodeMgr, deployment: DeploymentMgr, ) -> None: logger.debug("Starting main Aquarium task.") app.state.deployment = deployment app.state.nodemgr = nodemgr while not _shutting_down and not deployment.installed: logger.debug("Waiting for node to be installed.") await asyncio.sleep(1.0) if _shutting_down: return assert deployment.installed try: await deployment.init() except InitError as e: logger.error(f"Unable to init node: {e.message}") sys.exit(1) logger.info("Init Node Manager.") config.init() kvstore.init() gstate_init(gstate, nodemgr) nodemgr.init() logger.info("Starting Node Manager.") await nodemgr.start() await gstate.start() logger.info("Post-Init Deployment.") deployment.postinit(gstate, nodemgr) app.state.gstate = gstate
async def test_join_checks(gstate: GlobalState) -> None: from gravel.controllers.nodes.mgr import ( JoinParamsModel, NodeCantJoinError, NodeError, NodeInitStage, NodeNotStartedError, ) nodemgr = NodeMgr(gstate) throws = False nodemgr._init_stage = NodeInitStage.NONE try: await nodemgr.join( "1.2.3.4", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar") ) except NodeNotStartedError: throws = True assert throws throws = False nodemgr._init_stage = NodeInitStage.STARTED try: await nodemgr.join( "1.2.3.4", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar") ) except NodeCantJoinError: throws = True assert throws throws = False nodemgr._init_stage = NodeInitStage.AVAILABLE try: await nodemgr.join( "1.2.3.4", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="") ) except NodeError as e: throws = True assert "hostname" in e.message assert throws
async def aquarium_startup(_: FastAPI, aquarium_api: FastAPI): lvl = "INFO" if not os.getenv("AQUARIUM_DEBUG") else "DEBUG" setup_logging(lvl) logger.info("Aquarium startup!") gstate: GlobalState = GlobalState() # init node mgr logger.info("starting node manager") nodemgr: NodeMgr = NodeMgr(gstate) # Prep cephadm cephadm: Cephadm = Cephadm(gstate.config.options.containers) gstate.add_cephadm(cephadm) # Set up Ceph connections ceph: Ceph = Ceph() 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 = Storage(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
async def test_node_start( gstate: GlobalState, mocker: MockerFixture, fs: fake_filesystem.FakeFilesystem, nodemgr: NodeMgr, ) -> None: from gravel.controllers.nodes.deployment import NodeStageEnum from gravel.controllers.nodes.mgr import NodeInitStage nodemgr._deployment._state._stage = NodeStageEnum.READY nodemgr._obtain_state = mocker.AsyncMock() nodemgr._load = mocker.AsyncMock() nodemgr._incoming_msg_task = mocker.AsyncMock() nodemgr._connmgr.start_receiving = mocker.MagicMock() await nodemgr._node_start() assert nodemgr._init_stage == NodeInitStage.STARTED nodemgr._obtain_state.assert_called_once() # type: ignore nodemgr._load.assert_called_once() # type: ignore nodemgr._incoming_msg_task.assert_called_once() # type: ignore nodemgr._connmgr.start_receiving.assert_called_once() # type: ignore
async def test_bootstrap_finisher_cb(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.nodes.mgr import NodeInitStage nodemgr._init_stage = NodeInitStage.NONE assert await expect_assertion(nodemgr._post_bootstrap_finisher(True, None)) nodemgr._init_stage = NodeInitStage.PREPARE assert await expect_assertion(nodemgr._post_bootstrap_finisher(True, None)) nodemgr._init_stage = NodeInitStage.STARTED assert await expect_assertion(nodemgr._post_bootstrap_finisher(True, None)) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._save_state = mocker.AsyncMock() nodemgr._post_bootstrap_config = mocker.AsyncMock() await nodemgr._post_bootstrap_finisher(True, None) nodemgr._save_state.assert_called_once() # type: ignore nodemgr._post_bootstrap_config.assert_called_once() # type: ignore
async def test_finish_deployment_cb(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.nodes.mgr import NodeInitStage nodemgr._init_stage = NodeInitStage.NONE assert await expect_assertion(nodemgr._finish_deployment(True, None)) nodemgr._init_stage = NodeInitStage.PREPARE assert await expect_assertion(nodemgr._finish_deployment(True, None)) nodemgr._init_stage = NodeInitStage.STARTED assert await expect_assertion(nodemgr._finish_deployment(True, None)) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._deployment.finish_deployment = mocker.MagicMock() nodemgr._load = mocker.AsyncMock() nodemgr._node_start = mocker.AsyncMock() await nodemgr._finish_deployment(True, None) nodemgr._deployment.finish_deployment.assert_called_once() # type: ignore nodemgr._load.assert_called_once() # type: ignore nodemgr._node_start.assert_called_once() # type: ignore
async def test_join_check_disk_solution( gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr ) -> None: from gravel.controllers.nodes.disks import DiskSolution from gravel.controllers.nodes.mgr import ( JoinParamsModel, NodeCantJoinError, NodeInitStage, ) nodemgr._init_stage = NodeInitStage.AVAILABLE def empty_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution() mocker.patch( "gravel.controllers.nodes.disks.Disks.gen_solution", new=empty_solution ) throws = False try: await nodemgr.join( "1.2.3.4", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar") ) except NodeCantJoinError as e: assert e.message == "no disk deployment solution found" throws = True assert throws def fail_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution(possible=True) mocker.patch( "gravel.controllers.nodes.disks.Disks.gen_solution", new=fail_solution ) throws = False try: await nodemgr.join( "1.2.3.4", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar") ) except AssertionError: throws = True assert throws
async def test_start_ceph(gstate: GlobalState, mocker: MockerFixture) -> None: from gravel.controllers.nodes.mgr import NodeError called = False async def mock_call( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: nonlocal called called = True assert cmd[0] == "systemctl" assert cmd[1] == "start" assert cmd[2] == "ceph.target" return 0, None, None mocker.patch("gravel.controllers.nodes.mgr.aqr_run_cmd", new=mock_call) nodemgr = NodeMgr(gstate) await nodemgr._start_ceph() assert called called = False async def fail_call( cmd: List[str], ) -> Tuple[int, Optional[str], Optional[str]]: nonlocal called called = True return 1, None, "oops" mocker.patch("gravel.controllers.nodes.mgr.aqr_run_cmd", new=fail_call) throwed = False try: await nodemgr._start_ceph() except NodeError as e: assert "oops" in e.message throwed = True assert called assert throwed
def test_node_shutdown(gstate: GlobalState, mocker: MockerFixture) -> None: from gravel.controllers.nodes.mgr import NodeInitStage, NodeMgr class FakeTask: called = False def cancel(self) -> None: self.called = True nodemgr = NodeMgr(gstate) nodemgr._incoming_task = FakeTask() # type: ignore nodemgr._init_stage = NodeInitStage.NONE nodemgr._node_shutdown() assert nodemgr._init_stage == NodeInitStage.STOPPING assert nodemgr._incoming_task.called
async def test_obtain_images( gstate: GlobalState, mocker: MockerFixture ) -> None: orig_cephadm_pull_img = gstate.cephadm.pull_images gstate.cephadm.pull_images = mocker.AsyncMock() nodemgr = NodeMgr(gstate) ret = await nodemgr._obtain_images() assert ret gstate.cephadm.pull_images.assert_called_once() # type: ignore from gravel.cephadm.cephadm import CephadmError gstate.cephadm.pull_images = mocker.AsyncMock( side_effect=CephadmError("foobar") ) ret = await nodemgr._obtain_images() assert not ret gstate.cephadm.pull_images.assert_called_once() # type: ignore gstate.cephadm.pull_images = orig_cephadm_pull_img
async def test_mgr_start( gstate: GlobalState, fs: fake_filesystem.FakeFilesystem, mocker: MockerFixture, ) -> None: from gravel.controllers.nodes.deployment import NodeStageEnum from gravel.controllers.nodes.mgr import NodeError, NodeStateModel nodemgr = NodeMgr(gstate) assert nodemgr._state assert nodemgr.deployment_state.can_start() orig = nodemgr.deployment_state.can_start nodemgr.deployment_state.can_start = mocker.MagicMock(return_value=False) throws = False try: await nodemgr.start() except NodeError as e: assert "unstartable" in e.message throws = True assert throws nodemgr.deployment_state.can_start = orig nodemgr._deployment._state._stage = NodeStageEnum.NONE nodemgr._node_prepare = mocker.AsyncMock() await nodemgr.start() nodemgr._node_prepare.assert_called_once() # type: ignore nodemgr._deployment._state._stage = NodeStageEnum.READY nodemgr._state = NodeStateModel( uuid="bba35d93-d4a5-48b3-804b-99c406555c89", address="1.2.3.4", hostname="foobar", ) nodemgr._start_ceph = mocker.AsyncMock() nodemgr._node_start = mocker.AsyncMock() await nodemgr.start() nodemgr._start_ceph.assert_called_once() # type: ignore nodemgr._node_start.assert_called_once() # type: ignore
async def test_handle_join( gstate: GlobalState, mocker: MockerFixture, fs: fake_filesystem.FakeFilesystem, nodemgr: NodeMgr, ) -> None: from fastapi import status from gravel.controllers.nodes.messages import ( ErrorMessageModel, JoinMessageModel, MessageModel, MessageTypeEnum, WelcomeMessageModel, ) nodemgr._token = "751b-51fd-10d7-f7b4" # test wrong token # failed_token = False def fail_token(data: MessageModel) -> None: assert data.type == MessageTypeEnum.ERROR msg = cast(ErrorMessageModel, data.data) assert msg.what == "bad token" assert msg.code == status.HTTP_401_UNAUTHORIZED nonlocal failed_token failed_token = True fail_conn = fake_conn(fail_token) await nodemgr._handle_join( fail_conn, JoinMessageModel( uuid="aaaaaaaa-d4a5-48b3-804b-99c406555c89", hostname="barbaz", address="5.6.7.8", token="failtoken", ), ) assert failed_token # test missing address and hostname # failed_bad_addr_hostname = False def bad_addr_hostname(data: MessageModel) -> None: assert data.type == MessageTypeEnum.ERROR msg = cast(ErrorMessageModel, data.data) assert msg.what == "missing address or hostname" assert msg.code == status.HTTP_400_BAD_REQUEST nonlocal failed_bad_addr_hostname failed_bad_addr_hostname = True fail_conn = fake_conn(bad_addr_hostname) await nodemgr._handle_join( fail_conn, JoinMessageModel( uuid="aaaaaaaa-d4a5-48b3-804b-99c406555c89", hostname="", address="5.6.7.8", token=nodemgr._token, ), ) assert failed_bad_addr_hostname failed_bad_addr_hostname = False fail_conn = fake_conn(bad_addr_hostname) await nodemgr._handle_join( fail_conn, JoinMessageModel( uuid="aaaaaaaa-d4a5-48b3-804b-99c406555c89", hostname="barbaz", address="", token=nodemgr._token, ), ) assert failed_bad_addr_hostname # test add new member # called_conn_cb = False def conn_cb(data: MessageModel) -> None: assert data.type == MessageTypeEnum.WELCOME msg = WelcomeMessageModel.parse_obj(data.data) assert msg.pubkey == "mypubkey" assert msg.cephconf == "mycephconf" assert msg.keyring == "mycephkeyring" nonlocal called_conn_cb called_conn_cb = True mocker.patch( "gravel.controllers.orch.orchestrator.Orchestrator.get_public_key", new=mocker.MagicMock(return_value="mypubkey"), # type: ignore ) fs.create_file("/etc/ceph/ceph.conf") fs.create_file("/etc/ceph/ceph.client.admin.keyring") with open("/etc/ceph/ceph.conf", mode="w") as f: f.write("mycephconf") with open("/etc/ceph/ceph.client.admin.keyring", mode="w") as f: f.write("mycephkeyring") conn = fake_conn(conn_cb) await nodemgr._handle_join( conn, JoinMessageModel( uuid="aaaaaaaa-d4a5-48b3-804b-99c406555c89", hostname="barbaz", address="5.6.7.8", token=nodemgr._token, ), ) assert called_conn_cb assert "placeholder" in nodemgr._joining assert nodemgr._joining["placeholder"].address == "5.6.7.8" assert nodemgr._joining["placeholder"].hostname == "barbaz"
async def test_deploy(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from gravel.controllers.auth import UserMgr, UserModel from gravel.controllers.inventory.disks import DiskDevice from gravel.controllers.nodes.deployment import DeploymentConfig from gravel.controllers.nodes.disks import DiskSolution from gravel.controllers.nodes.mgr import NodeInitStage called_mock_deploy = False def mock_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution( systemdisk=DiskDevice( id="foo01", name="foo", path="/dev/foo", product="Foo", vendor="Foo Inc", size=1000, rotational=False, available=True, rejected_reasons=[], ), storage=[ DiskDevice( id="bar01", name="bar", path="/dev/bar", product="Bar", vendor="Bar LLC", size=2000, rotational=False, available=True, rejected_reasons=[], ), DiskDevice( id="baz01", name="baz", path="/dev/baz", product="Baz", vendor="Baz Ltd", size=2000, rotational=False, available=True, rejected_reasons=[], ), ], storage_size=4000, possible=True, ) async def mock_deploy( config: DeploymentConfig, post_bootstrap_cb: Callable[[bool, Optional[str]], Awaitable[None]], finisher: Callable[[bool, Optional[str]], Awaitable[None]], ) -> None: import inspect nonlocal called_mock_deploy called_mock_deploy = True assert config.hostname == "barbaz" assert config.address == "1.2.3.4" assert config.token == "751b-51fd-10d7-f7b4" assert config.ntp_addr == "my.ntp.addr" assert config.disks.system == "/dev/foo" assert len(config.disks.storage) == 2 assert "/dev/bar" in config.disks.storage assert "/dev/baz" in config.disks.storage assert post_bootstrap_cb is not None assert finisher is not None assert inspect.iscoroutinefunction(post_bootstrap_cb) assert inspect.iscoroutinefunction(finisher) mocker.patch("gravel.controllers.nodes.disks.Disks.gen_solution", new=mock_solution) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._generate_token = mocker.MagicMock( return_value="751b-51fd-10d7-f7b4") nodemgr._save_token = mocker.AsyncMock() nodemgr._deployment.deploy = mock_deploy await gstate.store.ensure_connection() await nodemgr.deploy( DeployParamsModel(hostname="barbaz", ntpaddr="my.ntp.addr")) assert called_mock_deploy assert nodemgr._token == "751b-51fd-10d7-f7b4" assert nodemgr._state.hostname == "barbaz" nodemgr._save_token.assert_called_once() # type: ignore ntpaddr = await gstate.store.get("/nodes/ntp_addr") assert ntpaddr == "my.ntp.addr" usermgr = UserMgr(gstate.store) assert await usermgr.exists("admin") user: Optional[UserModel] = await usermgr.get("admin") assert user is not None assert user.username == "admin" # We can't test the plain password here because it will fail # and we don't care particularly about the password itself, just that # the user has been populated. We'll leave for the 'UserMgr' tests to # validate the correctness of its operations. assert len(user.password) > 0
async def test_join(gstate: GlobalState, mocker: MockerFixture, nodemgr: NodeMgr) -> None: from uuid import UUID from gravel.controllers.inventory.disks import DiskDevice from gravel.controllers.nodes.deployment import DeploymentDisksConfig from gravel.controllers.nodes.disks import DiskSolution from gravel.controllers.nodes.mgr import JoinParamsModel, NodeInitStage def mock_solution(gstate: GlobalState) -> DiskSolution: return DiskSolution( systemdisk=DiskDevice( id="foo01", name="foo", path="/dev/foo", product="Foo", vendor="Foo Inc", size=1000, rotational=False, available=True, rejected_reasons=[], ), storage=[ DiskDevice( id="bar01", name="bar", path="/dev/bar", product="Bar", vendor="Bar LLC", size=2000, rotational=False, available=True, rejected_reasons=[], ), DiskDevice( id="baz01", name="baz", path="/dev/baz", product="Baz", vendor="Baz Ltd", size=2000, rotational=False, available=True, rejected_reasons=[], ), ], storage_size=4000, possible=True, ) async def mock_join( leader_address: str, token: str, uuid: UUID, hostname: str, address: str, disks: DeploymentDisksConfig, ) -> bool: assert leader_address == "10.1.2.3" assert token == "751b-51fd-10d7-f7b4" assert str(uuid) == "bba35d93-d4a5-48b3-804b-99c406555c89" assert hostname == "foobar" assert address == "1.2.3.4" assert disks.system == "/dev/foo" assert len(disks.storage) == 2 assert "/dev/bar" in disks.storage assert "/dev/baz" in disks.storage return True mocker.patch("gravel.controllers.nodes.disks.Disks.gen_solution", new=mock_solution) nodemgr._init_stage = NodeInitStage.AVAILABLE nodemgr._deployment.join = mocker.AsyncMock(side_effects=Exception()) throws = False try: await nodemgr.join( "10.1.2.3", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar"), ) except Exception: throws = True assert throws nodemgr._deployment.join.assert_called_once() # type: ignore nodemgr._deployment.join = mocker.AsyncMock(return_value=False) res = await nodemgr.join("10.1.2.3", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar")) assert not res nodemgr._deployment.join.assert_called_once() # type: ignore nodemgr._save_state = mocker.AsyncMock() nodemgr._node_start = mocker.AsyncMock() nodemgr._deployment.join = mock_join res = await nodemgr.join("10.1.2.3", "751b-51fd-10d7-f7b4", JoinParamsModel(hostname="foobar")) assert res assert nodemgr._token == "751b-51fd-10d7-f7b4" nodemgr._save_state.assert_called_once() # type: ignore nodemgr._node_start.assert_called_once() # type: ignore