diff --git a/yocto/cli.py b/yocto/cli.py index 0f1e3ae5..739a9e87 100644 --- a/yocto/cli.py +++ b/yocto/cli.py @@ -25,7 +25,7 @@ def main() -> int: should_deploy = maybe_build(configs) if not should_deploy: - return + return 0 assert configs.deploy # should never happen diff --git a/yocto/cloud/gcp/api.py b/yocto/cloud/gcp/api.py index 422b5a1f..86f15ebd 100644 --- a/yocto/cloud/gcp/api.py +++ b/yocto/cloud/gcp/api.py @@ -12,6 +12,7 @@ import time from pathlib import Path +from google.api_core.extended_operation import ExtendedOperation from google.cloud import compute_v1, resourcemanager_v3, storage from yocto.cloud.azure.api import AzureApi @@ -34,7 +35,7 @@ # Disk Operations def wait_for_extended_operation( - operation: compute_v1.Operation, + operation: ExtendedOperation, operation_name: str, timeout: int = 600, ) -> None: @@ -46,19 +47,14 @@ def wait_for_extended_operation( operation_name: Human-readable name for logging timeout: Maximum time to wait in seconds """ - start_time = time.time() - - while not operation.done(): - if time.time() - start_time > timeout: + try: + operation.result(timeout=timeout) + except Exception as e: + if "timeout" in str(e).lower(): raise TimeoutError( f"{operation_name} timed out after {timeout} seconds" ) - - time.sleep(5) - logger.info(f"Waiting for {operation_name}...") - - if operation.error: - raise RuntimeError(f"{operation_name} failed: {operation.error}") + raise RuntimeError(f"{operation_name} failed: {e}") class GcpApi(CloudApi): diff --git a/yocto/config/mode.py b/yocto/config/mode.py index 2817734f..4c294bbd 100644 --- a/yocto/config/mode.py +++ b/yocto/config/mode.py @@ -42,7 +42,7 @@ def deploy_only() -> "Mode": delete_artifact=None, ) - def to_dict(self) -> dict[str, str | bool]: + def to_dict(self) -> dict[str, bool | dict[str, str]]: delete_kwargs = {} if self.delete_vm: delete_kwargs["vm"] = self.delete_vm diff --git a/yocto/deployment/deploy_bob.py b/yocto/deployment/deploy_bob.py index b59c39d9..0657bd11 100755 --- a/yocto/deployment/deploy_bob.py +++ b/yocto/deployment/deploy_bob.py @@ -13,6 +13,7 @@ from pathlib import Path from yocto.cloud.azure.api import AzureApi +from yocto.cloud.cloud_config import CloudProvider # Import defaults here to avoid circular imports from yocto.cloud.azure.defaults import ( @@ -133,6 +134,8 @@ def deploy_bob_vm( # Convert to Configs object to access vm/deploy attributes cfg = config.to_configs() + if cfg.deploy is None: + raise ValueError("Deploy config is None") deploy_cfg = cfg.deploy logger.info(f"Config:\n{json.dumps(cfg.to_dict(), indent=2)}") @@ -357,6 +360,7 @@ def main(): try: # Create config (similar to genesis but without domain/DNS) config = DeploymentConfig( + cloud=CloudProvider.AZURE, vm_name=args.name, region=args.region or DEFAULT_REGION, vm_size=args.vm_size or DEFAULT_VM_SIZE, diff --git a/yocto/deployment/validators.py b/yocto/deployment/validators.py index 88ba0e84..ef201ce1 100644 --- a/yocto/deployment/validators.py +++ b/yocto/deployment/validators.py @@ -81,7 +81,7 @@ def _post_shares( node_to_pubkey: dict[int, str], ): genesis_file = f"{tmpdir}/genesis.toml" - genesis_toml = SummitClient.load_genesis_toml(genesis_file) + genesis_toml = SummitClient.load_genesis_toml(Path(genesis_file)) validators = genesis_toml["validators"] for node, client in node_clients: @@ -140,7 +140,7 @@ def main(): _post_shares(tmpdir, node_clients, node_to_pubkey) for _, client in node_clients: - client.post_genesis_filepath(f"{tmpdir}/genesis.toml") + client.post_genesis_filepath(Path(f"{tmpdir}/genesis.toml")) if __name__ == "__main__": diff --git a/yocto/genesis_deploy.py b/yocto/genesis_deploy.py index 741fc0dc..594b337b 100755 --- a/yocto/genesis_deploy.py +++ b/yocto/genesis_deploy.py @@ -25,7 +25,7 @@ class GenesisIPManager: """Manages persistent IP addresses for genesis nodes.""" - def __init__(self, cloud_api: CloudApi, ip_rg: str): + def __init__(self, cloud_api: type[CloudApi], ip_rg: str): self.cloud_api = cloud_api self.ip_rg = ip_rg @@ -68,8 +68,11 @@ def deploy_genesis_vm(args: DeploymentConfig) -> None: node = args.node cfg = args.to_configs() deploy_cfg = cfg.deploy + if deploy_cfg is None: + raise ValueError("Deploy config is None") print(f"Config:\n{json.dumps(cfg.to_dict(), indent=2)}") + cloud_api = get_cloud_api(deploy_cfg.vm.cloud) genesis_ip_manager = GenesisIPManager(cloud_api, args.resource_group) @@ -98,9 +101,12 @@ def deploy_genesis_vm(args: DeploymentConfig) -> None: logger.info("Not creating machines (used --ip-only flag)") return - image_path, measurements = maybe_build(cfg) + build_result = maybe_build(cfg) + if build_result is None: + raise ValueError("Build result is None") + image_path, measurements = build_result deployer = Deployer( - configs=cfg.deploy, + configs=deploy_cfg, image_path=image_path, measurements=measurements, home=cfg.home, diff --git a/yocto/image/__init__.py b/yocto/image/__init__.py index 2aa30e3b..8a2a7196 100644 --- a/yocto/image/__init__.py +++ b/yocto/image/__init__.py @@ -7,6 +7,10 @@ - from yocto.image.measurements import ... """ +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from yocto.image import build, git, measurements + # Only export module names, not individual functions __all__ = [ "build", diff --git a/yocto/utils/__init__.py b/yocto/utils/__init__.py index a487ba63..f29357ce 100644 --- a/yocto/utils/__init__.py +++ b/yocto/utils/__init__.py @@ -7,6 +7,10 @@ - etc. """ +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from yocto.utils import artifact, logging_setup, metadata, parser, paths, summit_client + # Only export module names, not individual functions __all__ = [ "artifact", diff --git a/yocto/utils/metadata.py b/yocto/utils/metadata.py index 67874b06..5671765f 100644 --- a/yocto/utils/metadata.py +++ b/yocto/utils/metadata.py @@ -57,13 +57,24 @@ def load_artifact_measurements( msg = f"Could not find artifact {artifact} in {metadata_path}" raise ValueError(msg) image_path = BuildPaths(home).artifacts / artifact - artifact = artifacts[artifact] + artifact_data = artifacts[artifact] if not image_path.exists(): raise FileNotFoundError( f"Artifact {artifact} is defined in the deploy metadata, " "but the corresponding file was not found on the machine" ) - return image_path, artifact["image"] + + if not isinstance(artifact_data, dict): + raise TypeError(f"Artifact data for {artifact} is not a dict") + if "image" not in artifact_data: + raise ValueError(f"Artifact {artifact} missing 'image' key") + + measurements = artifact_data["image"] + if not isinstance(measurements, dict): + raise TypeError(f"Measurements for {artifact} is not a dict") + + return image_path, measurements + def get_cloud_resources(home: str, cloud: str) -> dict[str, dict]: