Skip to content
Snippets Groups Projects
gga_init.py 17.9 KiB
Newer Older
# -*- coding: utf-8 -*-
Arthur Le Bars's avatar
Arthur Le Bars committed
import logging
Arthur Le Bars's avatar
Arthur Le Bars committed
import shutil
from jinja2 import Environment, FileSystemLoader
import speciesData
gga_init.py
Usage: $ python3 gga_init.py -i input_example.yml --config config.yml [OPTIONS]

TODO
- Exclude traefik dir tree creation and file writing from the loop (make make_dirs() an external func and write a func similar to make_compose_files()
  for traefik and authelia as an external func)

Arthur Le Bars's avatar
Arthur Le Bars committed
"""
class DeploySpeciesStack(speciesData.SpeciesData):
    Contains methods and attributes to deploy a stack of services for a given organism, from creating/updating
    the organism's directory tree to create the required docker-compose files and stack deployment

        Generate the directory tree for an organism
Arthur Le Bars's avatar
Arthur Le Bars committed

        # Create the species main directory (name of the dir: genus_species)
        try:
            os.mkdir(self.species_dir)
        except FileExistsError:
            logging.info("Updating directory tree of %s" % self.genus_species)
        except OSError as exc:
            logging.critical("Cannot access %s" % self.genus_species)
            sys.exit(exc)
        # Copy the custom banner to the species dir (banner used in tripal pages)
        # If the path specified is invalid (because it's empty or is still the default demo one),
        # use the default banner instead
        if constants.CONF_TRIPAL_BANNER_PATH in self.config.keys():
            if not config[constants.CONF_TRIPAL_BANNER_PATH] == "" and os.path.isfile(os.path.abspath(config[constants.CONF_TRIPAL_BANNER_PATH])):
                banner_dest_path = os.path.join(self.species_dir, os.path.abspath("banner.png"))
                if not os.path.isfile(banner_dest_path) or not os.path.samefile(os.path.abspath(config[constants.CONF_TRIPAL_BANNER_PATH]),banner_dest_path):
                    os.symlink(os.path.abspath(self.config[constants.CONF_TRIPAL_BANNER_PATH]), banner_dest_path)
                    logging.info("Custom banner added: symlink from %s" % self.config[constants.CONF_TRIPAL_BANNER_PATH])
                logging.debug("Using default banner for Tripal pages because %s is not valid in 'config' file" % constants.CONF_TRIPAL_BANNER_PATH)
                self.config.pop(constants.CONF_TRIPAL_BANNER_PATH, None)
        else:
            logging.debug("Using default banner for Tripal pages")
            self.config.pop(constants.CONF_TRIPAL_BANNER_PATH, None)
        make_dirs(dir_paths_li=["./nginx", "./nginx/conf"])
Arthur Le Bars's avatar
Arthur Le Bars committed
        try:
            shutil.copy(os.path.join(self.script_dir, "files/nginx_download.conf"), os.path.abspath("./nginx/conf/default.conf"))
        except Exception as exc:
            logging.critical("Could not copy nginx configuration file for %s %s", self.genus, self.species)
Arthur Le Bars's avatar
Arthur Le Bars committed

        logging.info("Directory tree generated for %s %s", self.genus, self.species)
    def make_compose_files(self):
        Create a formatted copy of the template compose file inside a species directory tree

        os.chdir(self.main_dir)
        try:
            os.chdir(self.species_dir)
        except OSError as exc:
            logging.critical("Cannot access %s" % self.species_dir)
            sys.exit(exc)
        # Jinja2 templating, handled using the python "jinja2" module
        file_loader = FileSystemLoader(self.script_dir + "/templates")
        env = Environment(loader=file_loader, trim_blocks=True, lstrip_blocks=True)

        # We need a dict holding all key (variables) - values that needs to be replaced in the template as our rendering dict
        # To do so we need both input file vars and config vars
        # Create input file vars dict
        input_vars = {"genus": self.genus_lowercase, "Genus": self.genus_uppercase, "species": self.species,
                      "genus_species": self.genus_species, "genus_species_strain_sex": self.species_folder_name,
                      "genus_species_sex": "{0}_{1}_{2}".format(self.genus_lowercase, self.species.lower(), self.sex),
                      "strain": self.strain, "sex": self.sex, "Genus_species": self.genus_species[0].upper() + self.genus_species[1:]}
        # Merge the two dicts
        render_vars = {**self.config, **input_vars}

        # Render the gspecies docker-compose file and write it
        gspecies_compose_template = env.get_template("gspecies_compose.yml.j2")
        gspecies_compose_output = gspecies_compose_template.render(render_vars)
        with open(os.path.join(self.species_dir, "docker-compose.yml"), "w") as gspecies_compose_file:
            logging.info("Writing %s docker-compose.yml" % self.genus_species)
            gspecies_compose_file.truncate(0)
            gspecies_compose_file.write(gspecies_compose_output)
        if not os.path.isfile(os.path.join(self.main_dir, "galaxy_nginx.conf")):
            galaxy_nginx_conf_template = env.get_template("galaxy_nginx.conf.j2")
            galaxy_nginx_conf_output = galaxy_nginx_conf_template.render(render_vars)
            with open(os.path.join(self.main_dir, "galaxy_nginx.conf"), "w") as galaxy_nginx_conf_file:
                logging.debug("Writing the galaxy_nginx.conf file for %s" % self.genus_species)
                galaxy_nginx_conf_file.truncate(0)
                galaxy_nginx_conf_file.write(galaxy_nginx_conf_output)
        # Create the volumes (directory) of the species docker-compose file
Loraine Gueguen's avatar
Loraine Gueguen committed
        create_mounts(working_dir=".", main_dir=self.main_dir)
        # Return to main directory
        os.chdir(self.main_dir)

        Create/update orthology compose files
        make_dirs["./orthology", "./orthology/src_data", "./orthology/src_data/genomes", 
                       "./orthology/src_data/gff", "./orthology/src_data/newicks", "./orthology/src_data/proteomes"]


def make_dirs(dir_paths_li):
    """
    Recursively create directories from a list of paths with a try-catch condition

    :param dir_paths_li:
    :return:
    """
    created_dir_paths_li = []

    for dir_path in dir_paths_li:
        try:
            os.mkdir(dir_path)
        except FileExistsError:
            logging.debug("%s directory already exists" % dir_path)
        except PermissionError as exc:
            logging.critical("Insufficient permission to create %s" % dir_path)
            sys.exit(exc)
        created_dir_paths_li.append(dir_path)

    return created_dir_paths_li

def make_traefik_compose_files(config, main_dir):
        """
        Create or update the traefik directory, docker-compose file and authelia conf files
        Only called when the argument "--traefik" is specified
        
        :param config:
        :param main_dir:
        :return:
        """

        script_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
        render_vars = config

        os.chdir(main_dir)

        make_dirs(["./traefik", "./traefik/authelia"])
        # Render and try to write the traefik docker-compose file
        # This new docker-compose file will not overwrite the one already present in the traefik dir
        # unless the argument "--overwrite-all" is specified

        # Jinja2 templating, handled using the python "jinja2" module
        file_loader = FileSystemLoader(script_dir + "/templates")
        env = Environment(loader=file_loader, trim_blocks=True, lstrip_blocks=True)
        if not os.path.isfile("./traefik/docker-compose.yml"):
            traefik_compose_template = env.get_template("traefik_compose.yml.j2")
            traefik_compose_output = traefik_compose_template.render(render_vars)
            with open(os.path.join(main_dir, "traefik/docker-compose.yml"), 'w') as traefik_compose_file:
                logging.info("Writing traefik docker-compose.yml")
                traefik_compose_file.truncate(0)
                traefik_compose_file.write(traefik_compose_output)

        if constants.CONF_ALL_HTTPS_PORT in config.keys():
            logging.info("HTTPS mode (with Authelia)")
            if constants.CONF_ALL_AUTHELIA_CONFIG_PATH in config.keys():
                if not config[constants.CONF_ALL_AUTHELIA_CONFIG_PATH] == "" and os.path.isfile(os.path.abspath(config[constants.CONF_ALL_AUTHELIA_CONFIG_PATH])):
                    try:
                        shutil.copy(os.path.abspath(config[constants.CONF_ALL_AUTHELIA_CONFIG_PATH]), "./traefik/authelia/configuration.yml")
                    except Exception as exc:
                        logging.critical("Could not copy authelia configuration file")
                        sys.exit(exc)
                else:
                    logging.critical("Invalid authelia configuration path (%s)" % config[constants.CONF_ALL_AUTHELIA_CONFIG_PATH])
                    sys.exit()

            # Path to the authelia users in the repo
            authelia_users_path = script_dir + "/files/authelia_users.yml"
            # Copy authelia "users" file
            if not os.path.isfile("./traefik/authelia/users.yml"):
                shutil.copy(authelia_users_path, "./traefik/authelia/users.yml")
        else:
            logging.info("HTTP mode (without Authelia)")
        # Create the mounts for the traefik and authelia services
        traefik_dir = os.path.abspath(os.path.join(main_dir, "traefik"))
        if not os.path.isdir(os.path.join(traefik_dir, "docker_data")):
            create_mounts(working_dir=traefik_dir, main_dir=main_dir)
def create_mounts(working_dir, main_dir):
        Create the folders (volumes) required by a container (to see required volumes, check their compose file)

        :return:
        """

        # Change directory to create mount points for the container
        try:
            os.chdir(os.path.abspath(working_dir))
            logging.critical("Cannot access %s, exiting" % working_dir)
        compose_yml = os.path.abspath("./docker-compose.yml")
        if not os.path.isfile(compose_yml):
            raise Exception("Could not find docker-compose.yml at %s" % compose_yml)

        with open(compose_yml) as f:
            compose = yaml.safe_load(f)

        if 'services' not in compose:
            raise Exception("Could not find services tag in docker-compose.yml")

        # Iterate over all services to find the "volumes" we need to create
        for service in compose['services']:
            if 'volumes' in compose['services'][service]:
                for volume in compose['services'][service]['volumes']:
                    # regex to match the volumes of the services
                    reg = re.match(r"^(\./[^:]+/):[^:]+(:\w+)?$", volume)
                    if reg:
                        vol_dir = os.path.abspath('./' + reg.group(1))
                        if not os.path.exists(vol_dir):
                            os.makedirs(vol_dir, exist_ok=True)
                    else:
                        reg = re.match(r"^(\./[^:]+):[^:]+(:\w+)?$", volume)
                        if reg:
                            vol_file = os.path.abspath('./' + reg.group(1))
                            vol_dir = os.path.dirname(vol_file)
                            if not os.path.exists(vol_dir):
                                os.makedirs(vol_dir, exist_ok=True)
                            if not os.path.exists(vol_file):
                                Path(vol_file).touch()

        # Go back to the "main" directory
        try:
            logging.critical("Cannot access %s, exiting" % main_dir)
def deploy_stacks(input_list, main_dir, deploy_traefik):
    """
    This function first deploys/redeploys the traefik stack, then deploys/redeploys the organism stack, then redeploys the traefik stack
    This function is executed outside the "main" loop of input species
    main_dir = os.path.abspath(main_dir)
    os.chdir(main_dir)

    # Get species for which to deploy the stacks
    # Uses the get_unique_species_list method from utilities to deploy a stack only for the "species" level (i.e genus_species)
    to_deploy_species_li = utilities.get_unique_species_str_list(sp_dict_list=input_list)
    if deploy_traefik:
        # Create the swarm cluster if needed
        logging.info("Initializing docker swarm (adding node)")
        subprocess.call(["docker", "swarm", "init"],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=main_dir)
        
        # Deploy traefik stack
        logging.info("Deploying traefik stack")
        os.chdir("./traefik")
        subprocess.call(["docker", "stack", "deploy", "-c", "./docker-compose.yml", "traefik"],
                                                stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=".")
        os.chdir(main_dir)

    # Deploy individual species stacks
    for sp in to_deploy_species_li:
        os.chdir(sp)
        logging.info("Deploying %s stack" % sp)
        subprocess.call(["docker", "stack", "deploy", "-c", "./docker-compose.yml", "{0}_{1}".format(sp.split("_")[0], sp.split("_")[1])],
                                           stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=".")
        logging.info("Deployed %s stack" % sp)
        os.chdir(main_dir)

    # Update traefik stack
    logging.info("Updating traefik stack")
    os.chdir("./traefik")
    subprocess.call(["docker", "stack", "deploy", "-c", "./docker-compose.yml", "traefik"],
                                            stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=".")
    os.chdir(main_dir)

    parser = argparse.ArgumentParser(description="Deploy GGA containers")

    parser.add_argument("input",
                        type=str,
                        help="Input file (yml)")

Arthur Le Bars's avatar
Arthur Le Bars committed
    parser.add_argument("-v", "--verbose",
                        help="Increase output verbosity",
                        action="store_true")

    parser.add_argument("--config",
                        type=str,
                        help="Config path, default to 'examples/config.yml'")
                        help="Where the stack containers will be located, defaults to current directory")
Arthur Le Bars's avatar
Arthur Le Bars committed

    parser.add_argument("--force-traefik",
                        help="Force overwrite traefik directory all docker-compose and conf files in the traefik and authelia directories (default=False)",
    args = parser.parse_args()

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.INFO)

    # Parsing the config file if provided, using the default config otherwise
    if args.config:
        config_file = os.path.abspath(args.config)
    else:
        config_file = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), constants.DEFAULT_CONFIG)
Loraine Gueguen's avatar
Loraine Gueguen committed
    config = utilities.parse_config(config_file)
        main_dir = os.path.abspath(args.main_directory)

    sp_dict_list = utilities.parse_input(os.path.abspath(args.input))

    # Create traefik directory and compose files if needed or specified
    if args.force_traefik or not os.path.isdir(os.path.join(os.path.abspath(main_dir), "traefik")):
        make_traefik_compose_files(config=config, main_dir=main_dir)

    unique_sp_dict_list = utilities.get_unique_species_dict_list(sp_dict_list=sp_dict_list)
    logging.info("Deploying stacks for organisms in input file %s" % args.input)
        # Init instance
        deploy_stack_for_current_organism = DeploySpeciesStack(parameters_dictionary=sp_dict)

        # Setting some of the instance attributes
        deploy_stack_for_current_organism.main_dir = main_dir
        deploy_stack_for_current_organism.species_dir = os.path.join(deploy_stack_for_current_organism.main_dir,
                                                                     deploy_stack_for_current_organism.genus_species +
                                                                     "/")
Loraine Gueguen's avatar
Loraine Gueguen committed
        deploy_stack_for_current_organism.config = config
        logging.info("gga_init.py called for %s %s", deploy_stack_for_current_organism.genus, deploy_stack_for_current_organism.species)
        logging.debug("Jbrowse url to %s %s %s %s", deploy_stack_for_current_organism.genus, deploy_stack_for_current_organism.species, deploy_stack_for_current_organism.sex, deploy_stack_for_current_organism.strain)
        # Make/update directory tree
        deploy_stack_for_current_organism.make_directory_tree()
        logging.info("Successfully generated the directory tree for %s %s", deploy_stack_for_current_organism.genus, deploy_stack_for_current_organism.species)
        deploy_stack_for_current_organism.make_compose_files()
        logging.info("Successfully generated the docker-compose files for %s %s", deploy_stack_for_current_organism.genus, deploy_stack_for_current_organism.species)
    if args.force_traefik:
        deploy_stacks(input_list=sp_dict_list, main_dir=main_dir, deploy_traefik=True)
    else:
        deploy_stacks(input_list=sp_dict_list, main_dir=main_dir, deploy_traefik=False)
    logging.info("All stacks deployed for organisms in input file %s" % args.input)