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

Arthur Le Bars
committed
else:
logging.debug("Using default banner for Tripal pages")
self.config.pop("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"])
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:

Arthur Le Bars
committed
logging.debug("'src_data' directory 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)]

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

Arthur Le Bars
committed
def make_compose_files(self):

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

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

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

Arthur Le Bars
committed

Arthur Le Bars
committed
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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)

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

Arthur Le Bars
committed
env = Environment(loader=file_loader)

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

Arthur Le Bars
committed
traefik_compose_template = env.get_template("traefik_compose_template.yml.j2")
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)

Arthur Le Bars
committed
if config["authelia_config_path"]:
if not config["authelia_config_path"] == "" or not config["authelia_config_path"] == "/path/to/authelia/config":
if os.path.isfile(os.path.abspath(config["authelia_config_path"])):
try:

Arthur Le Bars
committed
shutil.copy(os.path.abspath(config["authelia_config_path"]), "./traefik/authelia")

Arthur Le Bars
committed
except Exception as exc:

Arthur Le Bars
committed
logging.critical("Cannot copy custom Authelia config file (%s)" % config["authelia_config_path"])

Arthur Le Bars
committed
sys.exit(exc)
else:

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

Arthur Le Bars
committed

Arthur Le Bars
committed
# Path to the authelia users in the repo

Arthur Le Bars
committed
authelia_users_path = script_dir + "/templates/authelia_users_template.yml"

Arthur Le Bars
committed
# Copy authelia "users" file

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

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

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
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
285
286
287
288
289
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):
"""
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
main_dir = os.path.abspath(main_dir)
os.chdir(main_dir)
# Get species for which to deploy the stacks
to_deploy_species_li = utilities.get_species_to_deploy(sp_dict_list=input_list)
# Create the swarm cluster if needed
subprocess.call(["docker", "swarm", "init"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=main_dir)
# Deploy 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)
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
os.chdir("./traefik")
subprocess.call(["docker", "stack", "deploy", "-c", "./docker-compose.yml", "traefik"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=".")
os.chdir(main_dir)
# # Using deploy.sh script (obsolete)
# 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)
#
# 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("--traefik",
help="Initialize/wverwrite 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)

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
# Create traefik directory and compose files if specified
if args.traefik:
config = utilities.parse_config(args.config)
make_traefik_compose_files(config=config, main_dir=main_dir)

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

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
logging.info("Deploying stacks")
deploy_stacks(input_list=sp_dict_list, main_dir=main_dir)

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