diff --git a/src/cfengine_cli/commands.py b/src/cfengine_cli/commands.py index 9e1149c..c2a88ad 100644 --- a/src/cfengine_cli/commands.py +++ b/src/cfengine_cli/commands.py @@ -15,7 +15,7 @@ format_policy_fin_fout, ) from cfengine_cli.utils import UserError -from cfengine_cli.up import validate_config +from cfengine_cli.up import validate_config, spawn_from_config from cfbs.commands import build_command from cf_remote.commands import deploy as deploy_command @@ -178,12 +178,13 @@ def up(args) -> int: with open(args.config, "r") as f: content = yaml.safe_load(f) except yaml.YAMLError: - raise UserError("'%s' is not a valid yaml config" % args.config) + raise UserError("'%s' is not valid yaml" % args.config) except FileNotFoundError: raise UserError("'%s' doesn't exist" % args.config) - validate_config(content) + state = validate_config(content) if args.validate: return 0 - print("Starting VMs...") + + spawn_from_config(state) return 0 diff --git a/src/cfengine_cli/up.py b/src/cfengine_cli/up.py index 2e404a6..4a249ad 100644 --- a/src/cfengine_cli/up.py +++ b/src/cfengine_cli/up.py @@ -1,11 +1,18 @@ from pydantic import BaseModel, model_validator, ValidationError, Field from typing import Union, Literal, Optional, List, Annotated from functools import reduce -from cf_remote import log import cfengine_cli.validate as validate from cfengine_cli.utils import UserError +from cf_remote import log +from cf_remote import commands +from cf_remote import utils +from cf_remote import paths +from cf_remote import aramid + +import itertools +import time # Forces pydantic to throw validation error if config contains unknown keys class NoExtra(BaseModel, extra="forbid"): @@ -18,7 +25,8 @@ class Config(NoExtra): class AWSConfig(Config): image: str - size: Literal["micro", "xlarge"] = "micro" + size: Optional[Literal["micro", "xlarge"]] = None + region: Optional[str] = None @model_validator(mode="after") def check_aws_config(self): @@ -49,7 +57,8 @@ class GCPConfig(Config): image: str # There is no list of available GCP platforms to validate against yet network: Optional[str] = None public_ip: bool = True - size: str = "n1-standard-1" + size: Optional[str] = None + region: Optional[str] = None class AWSProvider(Config): @@ -194,6 +203,10 @@ def validate_config(content): raise UserError("Missing 'groups' key in spawn config") groups = content["groups"] + + if groups is None: + return {} + templates = content.get("templates") if templates: _resolve_templates(groups, templates) @@ -219,3 +232,105 @@ def validate_config(content): f"{err['msg']}. Input '{err['input']}' at location '{err['loc']}'" ) raise UserError("\n".join(msgs)) + return state + +def three_dots(): + for _ in range(3): + print(".", end="", flush=True) + time.sleep(0.3) + +def spawn_from_config(state): + + data = utils.read_json(paths.CLOUD_STATE_FPATH) + if data is None: + data = {} + + spawn = [] + destroy = [] + + for old, new in itertools.zip_longest(data.keys(), state.keys()): + + if old is not None: + if old[1:] not in state: + destroy.append(old) + print("<<< {}".format(old[1:])) + + if new is not None: + if "@{}".format(new) not in data: + spawn.append(new) + print(">>> {}".format(new)) + + for k in spawn: + config = state[k].config + + match config.source.mode: + case "spawn": + args = { + "group_name": k, + "count": config.source.count, + "role": config.role, + } + + match config.source.spawn.provider: + case "vagrant": + args |= { + "provider": commands.Providers.VAGRANT, + "size": config.source.spawn.vagrant.memory, + "platform": config.source.spawn.vagrant.box, + "vagrant_cpus": config.source.spawn.vagrant.cpus, + "vagrant_sync_folder": config.source.spawn.vagrant.sync_folder, + "vagrant_provision": config.source.spawn.vagrant.provision, + } + case "aws": + args |= { + "provider": commands.Providers.AWS, + "platform": config.source.spawn.aws.image, + "size": config.source.spawn.aws.size, + "region": config.source.spawn.aws.region, + } + case "gcp": + args |= { + "provider": commands.Providers.GCP, + "platform": config.source.spawn.gcp.image, + "network": config.source.spawn.gcp.network, + "public_ip": config.source.spawn.gcp.public_ip, + "size": config.source.spawn.gcp.size, + "region": config.source.spawn.gcp.region, + } + + commands.spawn(**args) + case "save": + commands.save(name=k, hosts=config.source.hosts, role=config.role) + + for k in destroy: + commands.destroy(k) + + data = utils.read_json(paths.CLOUD_STATE_FPATH) + if data is None: + data = {} + + if spawn: + print("Waiting for VMs to be accessible", end="") + three_dots() + print() + + for k,v in data.items(): + + if k[1:] not in spawn: + continue + + ips = [h_info["public_ips"][0] for h, h_info in v.items() if h != "meta"] + hosts = [aramid.Host(host_name=ip, user="admin") for ip in ips] + + while True: + try: + aramid.execute(hosts, "echo", ignore_failed=False, echo=False, echo_cmd=False) + break + except aramid.ExecutionError: + three_dots() + print("\r\033[2K", end="", flush=True) + + commands.info(ips) + + print("done") +