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

Arthur Le Bars
committed
from jinja2 import Template, Environment, FileSystemLoader

Arthur Le Bars
committed
import utilities

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
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
"""
Generate the directory tree for an organism and move datasets into src_data

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
if "banner_path" in self.config.keys():
if self.config["banner_path"] != "/path/to/banner" or self.config["banner_path"] != "":
try:
logging.debug("Custom banner path: %s" % self.config["banner_path"])
if os.path.isfile(os.path.abspath(self.config["banner_path"])):
shutil.copy(os.path.abspath(self.config["banner_path"]), "%s/banner.png" % self.species_dir)
except FileNotFoundError:
logging.warning("Specified banner not found (%s), using default banner instead" % self.config["banner_path"])
self.config.pop("banner_path", None)
else:
logging.debug("Using default banner for Tripal pages")
self.config.pop("banner_path", None)
# Create nginx dirs and write/re-write nginx conf
self.make_dirs(dir_paths_li=["./nginx", "./nginx/conf"])
try:
with open(os.path.abspath("./nginx/conf/default.conf"), 'w') as conf:
conf.write("server {\n\tlisten 80;\n\tserver_name ~.;\n\tlocation /download/ {\n\t\talias /project_data/; \n\t\tautoindex on;\n\t}\n}") # The species nginx conf

Arthur Le Bars
committed
except OSError as exc:
logging.critical("Cannot edit NginX conf file")
sys.exit(exc)

Arthur Le Bars
committed
# Creation (or updating) of the src_data directory tree

Arthur Le Bars
committed
try:
os.mkdir("./src_data")

Arthur Le Bars
committed
except FileExistsError:
logging.debug("src_data folder already exist for %s" % self.full_name)

Arthur Le Bars
committed
except PermissionError as exc:

Arthur Le Bars
committed
logging.critical("Insufficient permission to create src_data directory tree")

Arthur Le Bars
committed
sys.exit(exc)

Arthur Le Bars
committed

Arthur Le Bars
committed
# List of all the directories to create in src_data
src_data_dirs_li = ["./src_data", "./src_data/annotation", "./src_data/genome", "./src_data/tracks",
"./src_data/annotation/%s" % self.species_folder_name,
"./src_data/genome/%s" % self.species_folder_name,
"./src_data/annotation/{0}/OGS{1}/".format(self.species_folder_name, self.ogs_version),
"./src_data/genome/{0}/v{1}".format(self.species_folder_name, self.genome_version)]
self.make_dirs(dir_paths_li=src_data_dirs_li)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Return to main directory

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

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

Arthur Le Bars
committed

Arthur Le Bars
committed
def make_compose_files(self, force=False):

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)
# 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.full_name,
"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
gspecies_compose_template = env.get_template("gspecies_compose_template.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)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Create the volumes (directory) of the species docker-compose file
self.create_mounts(working_dir=".")
# Proceed to the traefik and authelia directories
os.chdir(self.main_dir)
self.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
if not os.path.isfile("./traefik/docker-compose.yml") or force:
traefik_compose_template = env.get_template("traefik_compose_template.yml.j2")
traefik_compose_output = traefik_compose_template.render(render_vars)
with open(os.path.join(self.main_dir, "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 self.config["authelia_config_path"]:
if not self.config["authelia_config_path"] == "" or not self.config["authelia_config_path"] == "/path/to/authelia/config":
if os.path.isfile(os.path.abspath(self.config["authelia_config_path"])):
try:

Arthur Le Bars
committed
shutil.copy(os.path.abspath(self.config["authelia_config_path"]), "./traefik/authelia")
except Exception as exc:
logging.critical("Cannot copy custom Authelia config file (%s)" % self.config["authelia_config_path"])
sys.exit(exc)
else:

Arthur Le Bars
committed
logging.critical("Custom Authelia config file not found (%s)" % self.config["authelia_config_path"])

Arthur Le Bars
committed

Arthur Le Bars
committed
# Path to the authelia users in the repo
authelia_users_path = self.script_dir + "/templates/authelia_users_template.yml"
# Copy authelia "users" file
if not os.path.isfile("./traefik/authelia/users.yml") or force:
shutil.copy(authelia_users_path, "./traefik/authelia/users.yml")

Arthur Le Bars
committed

Arthur Le Bars
committed
# Create the mounts for the traefik and authelia services
traefik_dir = os.path.abspath(os.path.join(self.main_dir, "traefik"))
if not os.path.isdir(os.path.join(traefik_dir, "docker_data")) or force:
self.create_mounts(working_dir=traefik_dir)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Return to main directory

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

Arthur Le Bars
committed
def create_mounts(self, working_dir):
"""

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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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:
os.chdir(os.path.abspath(self.main_dir))

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

Arthur Le Bars
committed
def deploy_stack(self, input_list):

Arthur Le Bars
committed
Call the script "deploy.sh" used to initiliaze the swarm cluster if needed and
launch/update the current organism's stack
This script first try to deploy the traefik stack, then deploy the organism stack, then update the traefik stack
The stacks are updated if already deployed

Arthur Le Bars
committed
to_deploy_species_li = []

Arthur Le Bars
committed
# # Create our swarm cluster if it doesn't exist
# subprocess.Popen(["docker", "swarm", "init"],
# stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.main_dir)
#
# # Deploy/update the stack for the current species
# subprocess.Popen(["docker", "stack", "deploy", "-c", "./docker-compose.yml", "{0}_{1}".format(self.genus_lowercase, self.species)],
# stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.main_dir)

Arthur Le Bars
committed
# Launch and update docker stacks
# noinspection PyArgumentList

Arthur Le Bars
committed
# deploy_stacks_popen = subprocess.Popen(["sh", self.script_dir + "/deploy.sh", self.genus_species,
# self.main_dir + "/traefik"],
# stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
# universal_newlines=True)
#
# for stdout_line in iter(deploy_stacks_popen.stdout.readline, ""):
# if "daemon" in stdout_line: # Ignore swarm init error output
# pass
# else:
# logging.info("\t%s" % stdout_line.strip())
# deploy_stacks_popen.stdout.close()

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

Arthur Le Bars
committed
parser = argparse.ArgumentParser(description="Automatic data loading in containers and interaction "
"with galaxy instances for GGA"
", following the protocol @ "
"http://gitlab.sb-roscoff.fr/abims/e-infra/gga")

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,
help="Config path, default to the 'config' file inside the script repository")
parser.add_argument("--main-directory",
type=str,

Arthur Le Bars
committed
help="Where the stack containers will be located, defaults to current directory")

Arthur Le Bars
committed
parser.add_argument("--overwrite-all",
help="Overwrite all docker-compose and conf files in the traefik and authelia directories (default=False)",
action="store_true")
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)

Arthur Le Bars
committed
# Parsing the config file if provided, using the default config otherwise

Arthur Le Bars
committed
if not args.config:
args.config = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "config")
else:
args.config = os.path.abspath(args.config)

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))

Arthur Le Bars
committed
print(sp_dict_list)
utilities.get_species_to_deploy(sp_dict_list=sp_dict_list)

Arthur Le Bars
committed
logging.info("Deploying stacks for organisms in input file %s" % args.input)
for sp_dict in 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

Arthur Le Bars
committed
deploy_stack_for_current_organism.config = utilities.parse_config(args.config)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Set the instance url attribute
for env_variable, value in deploy_stack_for_current_organism.config.items():

Arthur Le Bars
committed
if env_variable == "hostname":

Arthur Le Bars
committed
deploy_stack_for_current_organism.instance_url = value + \
deploy_stack_for_current_organism.genus_lowercase + \
"_" + deploy_stack_for_current_organism.species + \
"/galaxy/"

Arthur Le Bars
committed
break
else:
deploy_stack_for_current_organism.instance_url = "http://localhost:8888/sp/{0}_{1}/galaxy/".format(
deploy_stack_for_current_organism.genus_lowercase,
deploy_stack_for_current_organism.species)

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

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" % deploy_stack_for_current_organism.full_name)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Make compose files

Arthur Le Bars
committed
deploy_stack_for_current_organism.make_compose_files(force=args.overwrite_all)

Arthur Le Bars
committed
logging.info("Successfully generated the docker-compose files for %s" % deploy_stack_for_current_organism.full_name)

Arthur Le Bars
committed

Arthur Le Bars
committed
# Deploy the stack
logging.info("Deploying stack for %s..." % deploy_stack_for_current_organism.full_name)

Arthur Le Bars
committed
# deploy_stack_for_current_organism.deploy_stack()

Arthur Le Bars
committed
logging.info("Successfully deployed stack for %s" % deploy_stack_for_current_organism.full_name)

Arthur Le Bars
committed

Arthur Le Bars
committed
logging.info("Stack deployed for %s" % deploy_stack_for_current_organism.full_name)

Arthur Le Bars
committed
# TODO: if GENUS°1 == GENUS°2 AND SP°1 == SP°2 --> skip init, continue to next item and only deploy once the loop is done

Arthur Le Bars
committed

Arthur Le Bars
committed
# TODO: reload traefik outside loop

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