def enable_swarm_mode(advertise_addr: str): log.info("Enabling Docker Swarm...") if swarmadmin.swarm_is_active(): log.info("Docker Swarm is enabled") else: res = swarmadmin.init_docker_swarm(advertise_addr) fish.handle(res)
def check_secrets(docker_compose_path: str, no_prompt: bool = False): log.info("Checking Coral secrets...") required_secrets = swarmadmin.get_objs_from_compose( "secret", docker_compose_path) missing_secrets = list( filter(lambda x: not swarmadmin.obj_exists("secret", x), required_secrets)) if len(missing_secrets) > 0: log.info("Some required secrets are missing") if no_prompt: raise Exception( "When running in test mode, all secrets must be manually created beforehand." ) else: prompt_secrets(missing_secrets) else: if not no_prompt: question = "All required secrets are defined. Do you wish to redefine them? (y/n) " answer = fish.valid_answer_from_prompt( question, valid_answers=settings.get("prompt_boolean_answers")) log_file.info(question + answer) if fish.user_prompt_yes(answer): manage.remove_coral_objects("secret", silent=True, list_found_objs=False, prompt=False) prompt_secrets(required_secrets)
def start(quiet: bool = False): # TODO: check if all Coral services exist if not quiet: log.info("Starting Coral...") status = scale_all_services(1) if not quiet and status == 0: log.info("Coral stack successfully started!") return status
def config_depl_params(domain: str, email: str, cert_type: str, stack_name: str, advertise_addr: str = None, http_proxy: str = None, https_proxy: str = None, no_port_binding: bool = False, dev: bool = False, central: bool = False): log.info("Configuring deployment parameters...") depl_conf_path = settings.get_conf_file_path("deployment") depl_conf_template = settings.get("deployment_template") # set basic configurations basic_depl_conf = depl_conf_template["basic"] basic_depl_conf["domain"] = domain basic_depl_conf["email"] = email basic_depl_conf["cert_type"] = cert_type basic_depl_conf["stack_name"] = stack_name # set advanced configurations adv_depl_conf = depl_conf_template["advanced"] adv_depl_conf["advertise_addr"] = advertise_addr adv_depl_conf["http_proxy"] = http_proxy adv_depl_conf["https_proxy"] = https_proxy adv_depl_conf["no_port_binding"] = no_port_binding adv_depl_conf["dev_images"] = dev adv_depl_conf["central"] = central depl_conf = {"basic": basic_depl_conf, "advanced": adv_depl_conf} fish.write_json(depl_conf, depl_conf_path, indent=2)
def exec_cmd(cmd, suppress_stdout=False, suppress_stderr=True): process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = stderr = prev_line_stdout = "" while True: nextline_stdout = process.stdout.readline() nextline_stderr = process.stderr.readline() if nextline_stdout == b'' and nextline_stderr == b'' and process.poll() is not None: break line_stdout = nextline_stdout.decode('utf-8') line_stderr = nextline_stderr.decode('utf-8') stdout += line_stdout stderr += line_stderr if not suppress_stdout and line_stdout != prev_line_stdout and line_stdout != "\n": prev_line_stdout = line_stdout log.info(line_stdout.rstrip('\n')) # sys.stdout.write(line_stdout) # sys.stdout.flush() exit_code = process.returncode return { "cmd": cmd, "exit_code": exit_code, "stdout": stdout.rstrip('\n'), "stderr": stderr.rstrip('\n') }
def restart(): log.info("Restarting Coral...") status = stop(quiet=True) if status != 0: return status status = start(quiet=True) if status == 0: log.info("Coral stack successfully restarted!") return status
def set_env_vars(): log.info("Setting environment variables...") depl_config = get_curr_depl_config() env_file_path = settings.get_conf_file_path("stack") fish.replace_regex(env_file_path, r"^(SERVER_SYSTEM_NAME=).*$", fr"\1{settings.get('name')}", multiline=True) fish.replace_regex(env_file_path, r"^(SERVER_SYSTEM_VERSION=).*$", fr"\g<1>{settings.get('version')}", multiline=True) fish.replace_regex(env_file_path, r"^(DOMAIN=).*$", fr"\1{depl_config['basic']['domain']}", multiline=True) fish.replace_regex(env_file_path, r"^(WEBMASTER_MAIL=).*$", fr"\1{depl_config['basic']['email']}", multiline=True) fish.replace_regex(env_file_path, r"^(CERT_TYPE=).*$", fr"\1{depl_config['basic']['cert_type']}", multiline=True) if depl_config['advanced']['advertise_addr'] is not None: fish.replace_regex( env_file_path, r"^(MICA_DOMAIN=).*$", fr"\g<1>{depl_config['advanced']['advertise_addr']}/pub", multiline=True) fish.replace_regex( env_file_path, r"^(BASE_URL=).*$", fr"\g<1>https://{depl_config['advanced']['advertise_addr']}/cat", multiline=True) else: fish.replace_regex(env_file_path, r"^(MICA_DOMAIN=).*$", fr"\1{depl_config['basic']['domain']}/pub", multiline=True) fish.replace_regex(env_file_path, r"^(BASE_URL=).*$", fr"\1https://{depl_config['basic']['domain']}/cat", multiline=True) if depl_config['advanced']['http_proxy'] is not None: fish.replace_regex(env_file_path, r"^((HTTP_PROXY|http_proxy)=).*$", fr"\1{depl_config['advanced']['http_proxy']}", multiline=True) if depl_config['advanced']['https_proxy'] is not None: fish.replace_regex(env_file_path, r"^((HTTPS_PROXY|https_proxy)=).*$", fr"\1{depl_config['advanced']['https_proxy']}", multiline=True)
def registry_login(): resgistry = settings.get('docker_registry') log.info(f"Credentials for {resgistry}:") while True: try: swarmadmin.login(resgistry) break except Exception as e: log_file.warning("{0}: {1}".format(type(e).__name__, e))
def main(parsed_args, skip_depl_conf=False): status = None if parsed_args.deploy: depl_args = { "docker_compose_path": settings.get("compose_path"), "domain": parsed_args.domain, "email": parsed_args.email, "cert_type": "letsencrypt" if parsed_args.letsencrypt else "custom", "stack_name": settings.get("name") if parsed_args.stack_name is None else parsed_args.stack_name, "addr": parsed_args.addr, "http_proxy": parsed_args.http_proxy, "https_proxy": parsed_args.https_proxy, "no_port_binding": parsed_args.no_port_binding, "dev": parsed_args.dev, "no_prompt": parsed_args.test, "central": parsed_args.central } deploy.prepare(skip_depl_config=skip_depl_conf, is_update=False, **depl_args) elif parsed_args.list: manage.list_(parsed_args.list) elif parsed_args.remove: manage.remove(parsed_args.remove) elif parsed_args.logs: manage.logs(parsed_args.logs) elif parsed_args.update: manage.update() else: if parsed_args.start: status = manage.start() elif parsed_args.stop: status = manage.stop() elif parsed_args.restart: status = manage.restart() if status == 1: log.info( "No Coral services were found (Coral stack is not currently deployed)" )
def start(docker_compose_path: str, docker_compose_override_path: str, stack_name: str, enable_mon: bool): log.info("Deploying Coral...") swarmadmin.deploy_stack(docker_compose_path, docker_compose_override_path, stack_name) if (enable_mon): log_stdout.info("") log.info("Deploying Monitor stack...") swarmadmin.deploy_stack("./monitor/docker-compose.monitor.yml", None, f"{stack_name}_monitor")
def remove_coral_objects(obj_type: str, silent: bool = False, list_found_objs: bool = True, prompt: bool = True, is_dep: bool = False): if not silent: log.info( f"Removing{' dependent ' if is_dep else ' '}Coral {obj_type}s...") cleanup_params = settings.get("cleanup_params")[obj_type] rm_confirmation_msg, all_, quiet, format_, filters, force = [ value for value in cleanup_params.values() ] obj_list = get_coral_objs(obj_type, all_=all_, quiet=quiet, format_=f"\"{format_}\"", filters=filters) if obj_list: bullets_obj_list = "\n".join( [f"- {line}" for line in obj_list.split("\n")]) if list_found_objs and not silent: log.info( f"The following Coral {obj_type}s were found:\n{bullets_obj_list}" ) log_stdout.info("") if prompt: question = f"{rm_confirmation_msg}\nAre you sure you want to remove these {obj_type}s? (y/n) " answer = fish.valid_answer_from_prompt( question, valid_answers=settings.get("prompt_boolean_answers")) log_file.info(question.replace("\n", " ") + answer) if not prompt or fish.user_prompt_yes(answer): remove_coral_obj_dependencies(obj_type, silent=silent) obj_ids = [ i for i, j in (obj.split("\t") for obj in obj_list.split("\n")) ] if not silent: log.info(f"Removing {obj_type}s...") res = swarmadmin.rm(obj_type, obj_ids, force=force, suppress_stdout=silent) fish.handle(res) if not silent: log.info(f"Done!") else: if not silent: log.info(f"No Coral {obj_type}s were found. Skipping...")
def check_skip_depl_config(no_prompt: bool = False): curr_depl_conf = get_curr_depl_config() if curr_depl_conf is None: return False curr_depl_conf = json.dumps(curr_depl_conf, indent=2) log.info( "Previous deployment configuration found:\n{0}".format(curr_depl_conf)) if no_prompt: return True else: question = f"Do you wish to reuse the configuration above? (y/n) " answer = fish.valid_answer_from_prompt( question, valid_answers=settings.get("prompt_boolean_answers")) log_file.info(question + answer) return fish.user_prompt_yes(answer)
def prompt_secrets(secrets: list): log_stdout.info("Please provide the secrets below:") for secret_name in secrets: secret_content = getpass.getpass(prompt=f" {secret_name}: ") min_secret_len = settings.get("min_secret_len") while len(secret_content) < min_secret_len: log.info( f"Secrets must use at least {min_secret_len} characters. Try again:" ) secret_content = getpass.getpass(prompt=f" {secret_name}: ") swarmadmin.create_secret(secret_content, secret_name, "system=Coral") log_stdout.warn( "\n\033[1;31mIf you forget your passwords, you will loose access to your data.\nIt is highly recommended that you also manually store the passwords somewhere else safe.\033[0m" ) input("Press Enter to continue...")
def remove_coral_obj_dependencies(obj_type: str, silent: bool = False): if obj_type == "container": # must remove services first, or else containers will be automatically recreated if not silent: log.info("Must remove dependent services first") remove_coral_objects("service", silent=silent, list_found_objs=False, prompt=False, is_dep=True) if obj_type in ["image", "volume", "secret"]: # must remove associated containers first if not silent: log.info("Must remove dependent containers first") remove_coral_objects("container", silent=silent, list_found_objs=False, prompt=False, is_dep=True)
def scale_all_services(n_replicas: int, quiet: bool = False): coral_services = get_coral_objs("service", quiet=True, format_="{{.Name}}") if coral_services: coral_services_list = coral_services.split('\n') n_services = len(coral_services_list) # scale services one by one so progress can be displayed in stdout for i, service in enumerate(coral_services_list): # skip if service is already stopped/started res = swarmadmin.get_service_replicas(service) fish.handle(res) curr_replicas = int(res["stdout"]) if n_replicas == 0 and curr_replicas == 0 or n_replicas == 1 and curr_replicas == 1: state = "stopped" if n_replicas == 0 else "running" if not quiet: log.info(f"{service} is already {state}. Skipping...") fish.print_progress_bar(i + 1, n_services, prefix=f"Progress:", suffix="Complete\n", length=50) continue # TODO: check if service image needs to be pulled and warn user res = swarmadmin.scale([service], n_replicas, suppress_stdout=quiet, suppress_stderr=quiet, timeout=settings.get("scaling_timeout")) if not quiet: fish.print_progress_bar(i + 1, n_services, prefix=f"Progress:", suffix="Complete\n", length=50) fish.handle(res) if not quiet: log_stdout.info("") return 0 else: return 1
def check_central_mon(env_file_path: str, no_prompt: bool = False): log.info("Checking for central monitoring configuration...") central_mon_url = get_central_monitor_url() if (len(central_mon_url["value"]) == 0): log.warn( f"A central monitoring URL has not been defined in {env_file_path}, line {str(central_mon_url['index'] + 1)}.\nThe monitoring features described in the 'Coral Monitor Stack' section of README.md will be disabled." ) if not no_prompt: question = "Do you wish to continue? (y/n) " answer = fish.valid_answer_from_prompt( question, valid_answers=settings.get("prompt_boolean_answers")) log_file.info(question + answer) if fish.user_prompt_no(answer): sys.exit(0) return False else: log.info(f"Using {central_mon_url['value']} for central monitoring...") return True
def list_(obj_type: str): log.info(f"Listing Coral {obj_type}:") objs_found = get_coral_objs(obj_type.rstrip("s")) if objs_found.count("\n") == 0: log.info(f"No Coral {obj_type} found.") else: log.info("\n{0}\n".format(objs_found))
def get_coral_logs(obj_type: str): log.info(f"Getting {obj_type} logs...") volumes_dir = os.path.join(swarmadmin.get_data_root(), "volumes") stack_name = get_stack_name() if stack_name == None: raise Exception( "Unable to retrieve logs, No Coral deployment configuration found." ) if obj_type in {"agate", "mica"}: fish.copy_folder( os.path.join(volumes_dir, f"{stack_name}_{obj_type}", "_data", "logs"), settings.get_logs_tmp_folder(obj_type)) save_docker_service_log(f"{stack_name}_mongo", settings.get_logs_tmp_folder("mongodb")) elif obj_type == "apache": fish.copy_folder( os.path.join(volumes_dir, f"{stack_name}_{obj_type}-logs", "_data"), settings.get_logs_tmp_folder(obj_type)) elif obj_type == "opal": fish.copy_folder( os.path.join(volumes_dir, f"{stack_name}_{obj_type}", "_data", "logs"), settings.get_logs_tmp_folder(obj_type)) fish.copy_folder( os.path.join(volumes_dir, f"{stack_name}_rserver", "_data", "logs"), settings.get_logs_tmp_folder("rserver")) save_docker_service_log(f"{stack_name}_{obj_type}-data", settings.get_logs_tmp_folder("opal-data")) save_docker_service_log(f"{stack_name}_{obj_type}-ids", settings.get_logs_tmp_folder("opal-ids")) elif obj_type == "drupal": save_docker_service_log(f"{stack_name}_mica-{obj_type}", settings.get_logs_tmp_folder(obj_type)) save_docker_service_log( f"{stack_name}_mica-{obj_type}-data", settings.get_logs_tmp_folder("mica-drupal-data"))
def update(): question = "Coral will be stopped in order to perform the update. Do you wish to continue? (y/n) " answer = fish.valid_answer_from_prompt( question, valid_answers=settings.get("prompt_boolean_answers")) log_file.info(question + answer) if fish.user_prompt_yes(answer): docker_compose_path = settings.get("compose_path") original_compose_file = None log.info("Updating Coral Docker images...") # check if a current deployment is using dev images curr_depl_conf = deploy.get_curr_depl_config() if curr_depl_conf is not None: original_compose_file = fish.read_file(docker_compose_path) dev = curr_depl_conf["advanced"]["dev_images"] if dev: deploy.switch_to_dev_images( docker_compose_path) # switch to dev images try: registry_login() pull_coral_images(docker_compose_path) finally: if original_compose_file is not None: fish.write_file(original_compose_file, docker_compose_path) log_stdout.info("") log.info("Coral Docker images successfully updated!") curr_depl_conf = deploy.get_curr_depl_config() if (curr_depl_conf is None): log.info("Cannot start Coral. Please deploy first.") else: domain = curr_depl_conf["basic"][ "domain"] # needed for logs after deployment deploy.prepare(skip_depl_config=True, is_update=True, docker_compose_path=docker_compose_path, domain=domain, email=None, cert_type=None, stack_name=None, no_prompt=True)
def logs(obj_types: list, is_all: bool = False, zip_name: str = ""): obj_types = list(set(obj_types)) if not is_all: shutil.rmtree(settings.get_logs_tmp_folder("coral"), True) pathlib.Path(settings.get_logs_tmp_folder("coral")).mkdir( parents=True, exist_ok=True) log.info(f"Getting coral.log...") shutil.copyfile(settings.get_log_file_path("coral"), settings.get_log_tmp_file_path()) log.info(f"Getting docker.log...") save_mixed_docker_command_logs(settings.get_logs_tmp_folder("docker")) if "all" in obj_types: zip_name = datetime.today().strftime("%Y%m%d") + "_" + time.strftime( '%H%M%S', time.gmtime()) + "_all_logs" logs(["agate", "opal", "mica", "drupal", "apache"], True, zip_name) else: if "agate" in obj_types: get_coral_logs("agate") if not is_all: zip_name += "_agate" if "opal" in obj_types: get_coral_logs("opal") if not is_all: zip_name += "_opal" if "mica" in obj_types: get_coral_logs("mica") if not is_all: zip_name += "_mica" if "drupal" in obj_types: get_coral_logs("drupal") if not is_all: zip_name += "_drupal" if "apache" in obj_types: get_coral_logs("apache") if not is_all: zip_name += "_apache" if not is_all: zip_name = datetime.today().strftime( "%Y%m%d") + "_" + time.strftime( '%H%M%S', time.gmtime()) + zip_name + "_logs" file_name = shutil.make_archive(f"./logs/{zip_name}", "zip", settings.get_logs_tmp_folder("coral")) log.info( f"A zip file containing the requested services logs was saved in {file_name}" ) shutil.rmtree(settings.get_logs_tmp_folder("coral"), True)
def parse_args(parser, skip_depl_config=False): parsed_args = parser.parse_args() help_msg = "Run with the '-h' flag for additional help." if len(sys.argv) == 1: # print help when no args are provided parser.print_help() else: try: # if the version arg is present, print version and finish if parsed_args.version: log.info(settings.get("version")) return parsed_args used_depl_args = get_used_args_from_group(parsed_args, "deployment") used_mgmt_args = get_used_args_from_group(parsed_args, "management") # do not allow deployment args without using '--deploy' if len(used_depl_args) > 0 and "deploy" not in used_depl_args: first_offending_arg = get_first_used_arg_from_group( parsed_args, "deployment") parser.error( "Deployment arguments, such as '{0}', require '--deploy'.". format(first_offending_arg)) # do not allow management args if '--deploy' is used if parsed_args.deploy and len(used_mgmt_args) > 0: first_offending_arg = get_first_used_arg_from_group( parsed_args, "management") parser.error( "Management arguments, such as '{0}', are not allowed when using '--deploy'." .format(first_offending_arg)) str_central_conf = settings.get("custom_apache")["central_rule"] original_custom_file = fish.read_file( settings.get_custom_apache_conf_file_path()) # check for presence of the central arg if parsed_args.deploy and parsed_args.central: if str_central_conf not in original_custom_file: log.info( "Including apache rule for central monitoring in " + settings.get_custom_apache_conf_file_path()) original_custom_file = original_custom_file + str_central_conf fish.write_file( original_custom_file, settings.get_custom_apache_conf_file_path()) else: original_custom_file = original_custom_file.replace( str_central_conf, "") fish.write_file(original_custom_file, settings.get_custom_apache_conf_file_path()) if parsed_args.deploy and not skip_depl_config: # check for presence of required deployment args and validate them required_depl_args = get_required_depl_args() missing = list( filter(lambda x: x not in used_depl_args, required_depl_args)) if len(missing) > 0: parser.error( "Missing required deployment arguments: --{0}".format( ", --".join(missing))) else: if not fish.is_valid("domain", parsed_args.domain): parser.error("Invalid domain: '{0}'".format( parsed_args.domain)) if not fish.is_valid("email", parsed_args.email): parser.error("Invalid email: '{0}'".format( parsed_args.email)) # make '--letsencrypt' and '--no-port-binding' incompatible if parsed_args.letsencrypt and parsed_args.no_port_binding: parser.error( "Incompatible arguments: '--letsencrypt' and '--no-port-binding'. Cannot issue a Let's Encrypt certificate if port binding to host is disabled." ) # do not allow dots in the stack name if parsed_args.stack_name is not None and "." in parsed_args.stack_name: parser.error("The stack name cannot contain dots.") # validate advertise and proxy addresses, if present if parsed_args.addr and not fish.is_valid( "ip", parsed_args.addr): parser.error("Invalid IP address: '{0}'".format( parsed_args.addr)) if parsed_args.http_proxy and not fish.is_valid( "address", parsed_args.http_proxy): parser.error("Invalid proxy address: '{0}'".format( parsed_args.http_proxy)) if parsed_args.https_proxy and not fish.is_valid( "address", parsed_args.https_proxy): parser.error("Invalid proxy address: '{0}'".format( parsed_args.https_proxy)) else: # only allow one management argument at a time if len(used_mgmt_args) > 1: parser.error( "Only one management argument at a time is allowed.") except SystemExit as e: log_stdout.info("\n{0}".format(help_msg)) sys.exit(e) return parsed_args
def stop(quiet: bool = False): if not quiet: log.info("Stopping Coral...") status = scale_all_services(0, quiet=quiet) if not quiet and status == 0: log.info("Coral stack successfully stopped!") return status
def prepare(skip_depl_config: bool, is_update: bool, docker_compose_path: str, domain: str, email: str, cert_type: str, stack_name: str, addr: str = None, http_proxy: str = None, https_proxy: str = None, no_port_binding: bool = False, dev: bool = False, no_prompt: bool = False, central: bool = False): env_file_path = settings.get_conf_file_path("stack") original_env_file = fish.read_file(env_file_path) original_compose_file = fish.read_file(docker_compose_path) deploy_monitor_stack = True try: if skip_depl_config: log.info("Skipping configuration of deployment parameters...") log_stdout.info("") curr_depl_conf = get_curr_depl_config() domain = curr_depl_conf["basic"]["domain"] stack_name = curr_depl_conf["basic"]["stack_name"] addr = curr_depl_conf["advanced"]["advertise_addr"] dev = curr_depl_conf["advanced"]["dev_images"] no_port_binding = curr_depl_conf["advanced"]["no_port_binding"] central = curr_depl_conf["advanced"]["central"] enable_swarm_mode(addr) log_stdout.info("") # remove any current Coral services if not is_update: log.info("Cleaning up any previously deployed Coral services...") manage.remove_coral_objects("service", list_found_objs=False, prompt=False) log_stdout.info("") # save deployment parameters if not skip_depl_config: config_depl_params(domain, email, cert_type, stack_name, addr, http_proxy, https_proxy, no_port_binding, dev, central) log_stdout.info("") # set environment variables using deployment perameters set_env_vars() log_stdout.info("") # check central monitoring deploy_monitor_stack = check_central_mon(env_file_path, no_prompt) log_stdout.info("") # make sure all required secrets exist (if not, prompt) check_secrets(docker_compose_path, no_prompt) log_stdout.info("") # temporarilly edit compose file to set dev images or unbind ports to host if dev: switch_to_dev_images(docker_compose_path) if no_port_binding: unbind_ports(docker_compose_path) # pull Coral images if not is_update: log.info("Pulling Coral Docker images...") manage.registry_login() manage.pull_coral_images(docker_compose_path) log_stdout.info("") # check_volumes() # deploy Coral docker_compose_override_path = get_compose_override_path( docker_compose_path) start(docker_compose_path, docker_compose_override_path, stack_name, deploy_monitor_stack) log_stdout.info("") # track progress #track_progress(stack_name, docker_compose_path) log.info("\033[1;32mCoral has been deployed!\033[0m") log_stdout.info( "After a few minutes, you should have access to the following services:\n" f" \033[1;37mAgate\033[0m\t\t https://{domain}/auth\n" f" \033[1;37mOpal\033[0m\t\t https://{domain}/repo\n" f" \033[1;37mMica\033[0m\t\t https://{domain}/pub\n" f" \033[1;37mMica Drupal\033[0m\t https://{domain}/cat (or just https://{domain})\n" ) finally: # restore original env and docker-compose files fish.write_file(original_env_file, env_file_path) fish.write_file(original_compose_file, docker_compose_path) return 0