Newer
Older

Arthur Le Bars
committed
#!/usr/bin/env python3

Arthur Le Bars
committed
import argparse
import os

Arthur Le Bars
committed
import re

Arthur Le Bars
committed
import subprocess

Arthur Le Bars
committed
import sys

Arthur Le Bars
committed
import yaml

Arthur Le Bars
committed
from pathlib import Path
from jinja2 import Environment, FileSystemLoader

Arthur Le Bars
committed
import utilities

Loraine Gueguen
committed
import constants

Arthur Le Bars
committed

Arthur Le Bars
committed
Usage: $ python3 gga_init.py -i input_example.yml --config config.yml [OPTIONS]

Arthur Le Bars
committed

Arthur Le Bars
committed
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
committed
class DeploySpeciesStack(speciesData.SpeciesData):

Arthur Le Bars
committed
"""

Arthur Le Bars
committed
Child of SpeciesData

Arthur Le Bars
committed
Contains methods and attributes to deploy a stack of services for a given organism, from creating/updating

Arthur Le Bars
committed
the organism's directory tree to create the required docker-compose files and stack deployment

Arthur Le Bars
committed
"""

Arthur Le Bars
committed
def make_directory_tree(self):

Arthur Le Bars
committed
"""

Arthur Le Bars
committed
Generate the directory tree for an organism

Arthur Le Bars
committed
"""

Arthur Le Bars
committed
os.chdir(self.main_dir)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Create the species main directory (name of the dir: genus_species)

Arthur Le Bars
committed
try:
os.mkdir(self.species_dir)
except FileExistsError:

Arthur Le Bars
committed
logging.info("Updating directory tree of %s" % self.genus_species)

Arthur Le Bars
committed
try:
os.chdir(self.species_dir)

Arthur Le Bars
committed
except OSError as exc:
logging.critical("Cannot access %s" % self.genus_species)
sys.exit(exc)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Copy the custom banner to the species dir (banner used in tripal pages)

Arthur Le Bars
committed
# If the path specified is invalid (because it's empty or is still the default demo one),
# use the default banner instead

Loraine Gueguen
committed
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"))

Loraine Gueguen
committed
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])

Arthur Le Bars
committed
else:

Loraine Gueguen
committed
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)

Arthur Le Bars
committed
else:
logging.debug("Using default banner for Tripal pages")

Loraine Gueguen
committed
self.config.pop(constants.CONF_TRIPAL_BANNER_PATH, None)

Arthur Le Bars
committed
# Create nginx dirs and write/re-write nginx conf

Arthur Le Bars
committed
make_dirs(dir_paths_li=["./nginx", "./nginx/conf"])
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)
logging.critical(exc)

Arthur Le Bars
committed
# Return to main directory

Arthur Le Bars
committed
os.chdir(self.main_dir)
logging.info("Directory tree generated for %s %s", self.genus, self.species)

Arthur Le Bars
committed

Arthur Le Bars
committed
"""

Arthur Le Bars
committed
Create a formatted copy of the template compose file inside a species directory tree

Arthur Le Bars
committed
:return:
"""

Arthur Le Bars
committed
os.chdir(self.main_dir)
try:
os.chdir(self.species_dir)

Arthur Le Bars
committed
except OSError as exc:
logging.critical("Cannot access %s" % self.species_dir)
sys.exit(exc)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Jinja2 templating, handled using the python "jinja2" module

Arthur Le Bars
committed
file_loader = FileSystemLoader(self.script_dir + "/templates")
env = Environment(loader=file_loader, trim_blocks=True, lstrip_blocks=True)

Arthur Le Bars
committed
# 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),

Arthur Le Bars
committed
"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}

Arthur Le Bars
committed
# Render the gspecies docker-compose file and write it

Loraine Gueguen
committed
gspecies_compose_template = env.get_template("gspecies_compose.yml.j2")

Arthur Le Bars
committed
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)

Arthur Le Bars
committed

Loraine Gueguen
committed
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)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Create the volumes (directory) of the species docker-compose file
create_mounts(working_dir=".", main_dir=self.main_dir)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Return to main directory
os.chdir(self.main_dir)

Arthur Le Bars
committed
def make_orthology_compose_files(self):

Arthur Le Bars
committed
"""
Create/update orthology compose files

Arthur Le Bars
committed
:return:
"""

Arthur Le Bars
committed
os.chdir(self.main_dir)

Arthur Le Bars
committed

Arthur Le Bars
committed
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):

Arthur Le Bars
committed
"""
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)

Arthur Le Bars
committed
# Create directory tree

Arthur Le Bars
committed
make_dirs(["./traefik", "./traefik/authelia"])

Arthur Le Bars
committed

Arthur Le Bars
committed
# 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

Arthur Le Bars
committed
# Jinja2 templating, handled using the python "jinja2" module

Arthur Le Bars
committed
file_loader = FileSystemLoader(script_dir + "/templates")

Loraine Gueguen
committed
env = Environment(loader=file_loader, trim_blocks=True, lstrip_blocks=True)

Arthur Le Bars
committed

Arthur Le Bars
committed
if not os.path.isfile("./traefik/docker-compose.yml"):

Loraine Gueguen
committed
traefik_compose_template = env.get_template("traefik_compose.yml.j2")

Arthur Le Bars
committed
traefik_compose_output = traefik_compose_template.render(render_vars)

Arthur Le Bars
committed
with open(os.path.join(main_dir, "traefik/docker-compose.yml"), 'w') as traefik_compose_file:

Arthur Le Bars
committed
logging.info("Writing traefik docker-compose.yml")
traefik_compose_file.truncate(0)
traefik_compose_file.write(traefik_compose_output)

Loraine Gueguen
committed
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)")

Arthur Le Bars
committed

Arthur Le Bars
committed
# Create the mounts for the traefik and authelia services

Arthur Le Bars
committed
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)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Return to main directory

Arthur Le Bars
committed
os.chdir(main_dir)

Arthur Le Bars
committed
def create_mounts(working_dir, main_dir):

Arthur Le Bars
committed
"""

Arthur Le Bars
committed
Create the folders (volumes) required by a container (to see required volumes, check their compose file)

Arthur Le Bars
committed
:return:
"""
# Change directory to create mount points for the container
try:
os.chdir(os.path.abspath(working_dir))

Arthur Le Bars
committed
except Exception as exc:

Arthur Le Bars
committed
logging.critical("Cannot access %s, exiting" % working_dir)

Arthur Le Bars
committed
sys.exit(exc)

Arthur Le Bars
committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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:

Arthur Le Bars
committed
os.chdir(os.path.abspath(main_dir))

Arthur Le Bars
committed
except OSError as exc:

Arthur Le Bars
committed
logging.critical("Cannot access %s, exiting" % main_dir)

Arthur Le Bars
committed
sys.exit(exc)

Arthur Le Bars
committed
def deploy_stacks(input_list, main_dir, deploy_traefik):

Arthur Le Bars
committed
"""
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

Arthur Le Bars
committed

Arthur Le Bars
committed
:return:
"""

Arthur Le Bars
committed

Arthur Le Bars
committed
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)

Arthur Le Bars
committed

Arthur Le Bars
committed
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)

Arthur Le Bars
committed
# Deploy individual species stacks
for sp in to_deploy_species_li:
os.chdir(sp)

Arthur Le Bars
committed
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

Arthur Le Bars
committed
os.chdir("./traefik")
subprocess.call(["docker", "stack", "deploy", "-c", "./docker-compose.yml", "traefik"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=".")
os.chdir(main_dir)

Arthur Le Bars
committed
if __name__ == "__main__":

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

Arthur Le Bars
committed
parser.add_argument("input",
type=str,
help="Input file (yml)")
parser.add_argument("-v", "--verbose",
help="Increase output verbosity",

Arthur Le Bars
committed
action="store_true")
parser.add_argument("--config",
type=str,

Loraine Gueguen
committed
help="Config path, default to 'examples/config.yml'")

Arthur Le Bars
committed
parser.add_argument("--main-directory",
type=str,

Arthur Le Bars
committed
help="Where the stack containers will be located, defaults to current directory")
parser.add_argument("--force-traefik",
help="Force overwrite traefik directory all docker-compose and conf files in the traefik and authelia directories (default=False)",

Arthur Le Bars
committed
action="store_true")
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)

Loraine Gueguen
committed
# 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)

Arthur Le Bars
committed

Arthur Le Bars
committed
main_dir = None

Arthur Le Bars
committed
if not args.main_directory:

Arthur Le Bars
committed
main_dir = os.getcwd()

Arthur Le Bars
committed
else:

Arthur Le Bars
committed
main_dir = os.path.abspath(args.main_directory)

Arthur Le Bars
committed
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)

Arthur Le Bars
committed
unique_sp_dict_list = utilities.get_unique_species_dict_list(sp_dict_list=sp_dict_list)

Arthur Le Bars
committed

Arthur Le Bars
committed
logging.info("Deploying stacks for organisms in input file %s" % args.input)

Arthur Le Bars
committed
for sp_dict in unique_sp_dict_list:

Arthur Le Bars
committed

Arthur Le Bars
committed
# Init instance
deploy_stack_for_current_organism = DeploySpeciesStack(parameters_dictionary=sp_dict)
# Setting some of the instance attributes

Arthur Le Bars
committed
deploy_stack_for_current_organism.main_dir = main_dir

Arthur Le Bars
committed
deploy_stack_for_current_organism.species_dir = os.path.join(deploy_stack_for_current_organism.main_dir,
deploy_stack_for_current_organism.genus_species +
"/")

Arthur Le Bars
committed
# Parse the config yaml file

Loraine Gueguen
committed

Arthur Le Bars
committed
# Starting
logging.info("gga_init.py called for %s %s", deploy_stack_for_current_organism.genus, deploy_stack_for_current_organism.species)

Loraine Gueguen
committed
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)

Arthur Le Bars
committed

Arthur Le Bars
committed
# 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)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Make compose files
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)

Arthur Le Bars
committed

Arthur Le Bars
committed
logging.info("Deploying stacks")

Arthur Le Bars
committed
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)

Arthur Le Bars
committed
logging.info("All stacks deployed for organisms in input file %s" % args.input)