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

Arthur Le Bars
committed
import utilities

Arthur Le Bars
committed

Arthur Le Bars
committed
Usage: $ python3 gga_init.py -i input_example.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
the organism's directory tree to create the required docker-compose files

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
try:
os.mkdir(self.species_dir)

Arthur Le Bars
committed
logging.info("Making directory tree for %s" % self.full_name)

Arthur Le Bars
committed
except FileExistsError:

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

Arthur Le Bars
committed
try:
os.chdir(self.species_dir)
except OSError:
logging.critical("Cannot access " + self.species_dir + ", run with higher privileges")

Arthur Le Bars
committed
sys.exit()

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

Arthur Le Bars
committed
if not self.config["custom_banner"] or not self.config["custom_banner"] == "/path/to/banner" or not self.config["custom_banner"] == "":
try:

Arthur Le Bars
committed
logging.debug("Custom banner path: %s" % self.config["custom_banner"])
if os.path.isfile(os.path.abspath(self.config["custom_banner"])):
shutil.copy(os.path.abspath(self.config["custom_banner"]), "%s/banner.png" % self.species_dir)

Arthur Le Bars
committed
except FileNotFoundError:

Arthur Le Bars
committed
logging.warning("Specified banner not found (%s), skipping" % self.config["custom_banner"])
# Copy nginx conf
try:
os.mkdir("./nginx/")
os.mkdir("./nginx/conf")
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
logging.debug("Nginx conf already exists, skipping")

Arthur Le Bars
committed
organism_annotation_dir, organism_genome_dir = None, None

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

Arthur Le Bars
committed
# Depth 0

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)
except PermissionError:
logging.critical("Insufficient permission to create src_data directory tree")
sys.exit()
# Depth 1
try:
os.mkdir("./src_data/annotation")
os.mkdir("./src_data/genome")
os.mkdir("./src_data/tracks")

Arthur Le Bars
committed
except FileExistsError:

Arthur Le Bars
committed
logging.debug("Depth 1 src_data folder(s) already exist for %s" % self.full_name)

Arthur Le Bars
committed
except PermissionError:
logging.critical("Insufficient permission to create src_data directory tree")
sys.exit()
# Depth 2
try:
os.mkdir("./src_data/annotation/" + self.species_folder_name)
os.mkdir("./src_data/genome/" + self.species_folder_name)

Arthur Le Bars
committed
except FileExistsError:

Arthur Le Bars
committed
logging.debug("Depth 2 src_data folder(s) already exist for %s" % self.full_name)

Arthur Le Bars
committed
except PermissionError:
logging.critical("Insufficient permission to create src_data directory tree")
sys.exit()
# Depth 3
try:
os.mkdir("./src_data/annotation/" + self.species_folder_name + "/OGS" + self.ogs_version)
os.mkdir("./src_data/genome/" + self.species_folder_name + "/v" + self.genome_version)
organism_annotation_dir = os.path.abspath("./src_data/annotation/" + self.species_folder_name + "/OGS" + self.genome_version)
organism_genome_dir = os.path.abspath("./src_data/genome/" + self.species_folder_name + "/v" + self.genome_version)

Arthur Le Bars
committed
except FileExistsError:

Arthur Le Bars
committed
logging.debug("Depth 3 src_data folder(s) already exist for %s" % self.full_name)

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

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

Arthur Le Bars
committed
def make_compose_files(self):
"""

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)
except OSError:
logging.critical("Cannot access " + self.species_dir)
sys.exit(0)
# Path to the templates used to generate the custom docker-compose files for an input species
stack_template_path = self.script_dir + "/templates/gspecies_compose_template.yml"
traefik_template_path = self.script_dir + "/templates/traefik_compose_template.yml"

Arthur Le Bars
committed
# authelia_config_path = self.script_dir + "/templates/authelia_config_example.yml" # Do not copy the authelia config!
authelia_users_path = self.script_dir + "/templates/authelia_users_template.yml"

Arthur Le Bars
committed

Arthur Le Bars
committed
# Set the genus_species_strain_sex var, used
genus_species_strain_sex = "{0}_{1}".format(self.genus.lower(), self.species)

Arthur Le Bars
committed
if self.sex and self.strain:

Arthur Le Bars
committed
genus_species_strain_sex = "_".join([self.genus.lower(), self.species, self.strain, self.sex])

Arthur Le Bars
committed
elif self.sex and not self.strain:
genus_species_strain_sex = "_".join([self.genus.lower(), self.species, self.sex])

Arthur Le Bars
committed
else:

Arthur Le Bars
committed
genus_species_strain_sex = "{0}_{1}".format(self.genus.lower(), self.species)
with open(stack_template_path, 'r') as infile:
organism_content = list()
for line in infile:

Arthur Le Bars
committed
# Replace placeholders in the compose file and append line to output

Arthur Le Bars
committed
line.replace("genus_species",
str(self.genus.lower() + "_" + self.species)).replace("Genus species",
str(self.genus_uppercase + " " + self.species)).replace("Genus/species",
str(self.genus_uppercase + "/" + self.species)).replace("gspecies",
str(self.genus.lower()[0] + self.species)).replace("genus_species_strain_sex",
genus_species_strain_sex))

Arthur Le Bars
committed
# Write/format the output compose file

Arthur Le Bars
committed
outfile.truncate(0) # Delete file content
for line in organism_content: # Replace env variables by those in the config file

Arthur Le Bars
committed
for env_variable, value in self.config.items(): # env variables are stored in this dict
# print("ENV VARIABLE: " + env_variable + "\t VALUE: " + value)

Arthur Le Bars
committed
if env_variable in line:
line = line.replace(env_variable, value)
break

Arthur Le Bars
committed
# Write the new line in the docker-compose

Arthur Le Bars
committed

Arthur Le Bars
committed
# Create mounts for the current docker-compose

Arthur Le Bars
committed
self.create_mounts(working_dir=self.species_dir)

Arthur Le Bars
committed
os.chdir(os.path.abspath(self.main_dir))
os.mkdir("./traefik")
os.mkdir("./traefik/authelia")
if self.config["custom_authelia_config_path"]:
if os.path.isfile(os.path.abspath(self.config["custom_authelia_config_path"])):
try:
shutil.copy(os.path.abspath(self.config["custom_authelia_config_path"]), "./traefik/authelia")
except FileNotFoundError:
logging.critical("Cannot copy custom Authelia config file (%s)" % self.config["custom_authelia_config_path"])
sys.exit()
else:
logging.critical("Custom Authelia config file not found (%s)" % self.config["custom_authelia_config_path"])

Arthur Le Bars
committed
if not os.path.isfile("./traefik/authelia/users.yml"):
shutil.copy(authelia_users_path, "./traefik/authelia/users.yml")

Arthur Le Bars
committed
logging.debug("Traefik directory already exists: %s" % os.path.abspath("../traefik"))

Arthur Le Bars
committed
if not os.path.isfile("./traefik/docker-compose.yml"):
shutil.copy(traefik_template_path, "./traefik/docker-compose.yml")
else:
logging.debug("Traefik compose file already exists: %s" % os.path.abspath("./traefik/docker-compose.yml"))

Arthur Le Bars
committed
logging.debug("Traefik compose file already exists: %s" % os.path.abspath("./traefik/docker-compose.yml"))
traefik_dir = os.path.abspath(os.path.join(self.main_dir, "traefik"))

Arthur Le Bars
committed
# Create the mounts for the traefik+authelia containers

Arthur Le Bars
committed
self.create_mounts(working_dir=traefik_dir)

Arthur Le Bars
committed

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

Arthur Le Bars
committed

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
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
:return:
"""
# Change directory to create mount points for the container
try:
os.chdir(os.path.abspath(working_dir))
except Exception:
logging.critical("Cannot access %s, exiting" % working_dir)
sys.exit()
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))
except Exception:
logging.critical("Cannot access main directory (%s), exiting" % self.main_dir)
sys.exit()

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

Arthur Le Bars
committed
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")
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))
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():
if env_variable == "custom_host":
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
deploy_stack_for_current_organism.make_compose_files()
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)
deploy_stack_for_current_organism.deploy_stack()
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)
# TODO: IF GENUS°1 == GENUS°2 AND SP°1 == SP°2 --> SKIP INIT, CONTINUE TO NEXT ITEM IN INPUT (DEPLOY AT THE END)

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)