# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 CERN.
# Copyright (C) 2021 Esteban J. G. Gabancho.
#
# Invenio-Cli is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
"""Invenio module to ease the creation and management of applications."""
import click
from invenio_cli.commands.translations import TranslationsCommands
from ..helpers.docker_helper import DockerHelper
from ..helpers.process import ProcessResponse
from ..helpers.rdm import rdm_version
from .commands import Commands
from .services_health import HEALTHCHECKS, ServicesHealthCommands
from .steps import CommandStep, FunctionStep
[docs]class ServicesCommands(Commands):
"""Service CLI commands."""
def __init__(self, cli_config, docker_helper=None):
"""Constructor."""
super().__init__(cli_config)
self.docker_helper = docker_helper or DockerHelper(
cli_config.get_project_shortname(), local=True
)
[docs] def ensure_containers_running(self):
"""Ensures containers are running."""
project_shortname = self.cli_config.get_project_shortname()
self.docker_helper.start_containers()
services = ["redis", self.cli_config.get_db_type(), "search"]
for service in services:
ready = ServicesHealthCommands.wait_for_service(
service,
project_shortname=project_shortname,
print_func=lambda msg: click.secho(msg, fg="yellow"),
)
if not ready:
return ProcessResponse(
error=f"Unable to boot up {service}",
status_code=1,
)
else:
# We should not use `click` outside the `cli` context, but
# the return signature of this method does not support a list
# of `ProcessResponse` objs, so it is printed directly here.
click.secho(f"{service} up and running!", fg="green")
return ProcessResponse(
output="Containers started and healthy.",
status_code=0,
)
[docs] def services_expected_status(self, expected):
"""Checks if the services have the expected status."""
if not self.cli_config.get_services_setup() == expected:
return ProcessResponse(
error="Services status inconsistent."
+ f"Expected {expected} obtained {not expected}",
status_code=1,
)
return ProcessResponse(
output=f"Services setup status consistent.", status_code=0
)
def _cleanup(self):
"""Services cleanup steps."""
steps = [
CommandStep(
cmd=[
"pipenv",
"run",
"invenio",
"shell",
"--no-term-title",
"-c",
"import redis; redis.StrictRedis.from_url(app.config['CACHE_REDIS_URL']).flushall(); print('Cache cleared')",
], # noqa
env={"PIPENV_VERBOSITY": "-1"},
message="Flushing Redis...",
skippable=True,
),
CommandStep(
cmd=["pipenv", "run", "invenio", "db", "destroy", "--yes-i-know"],
env={"PIPENV_VERBOSITY": "-1"},
message="Destroying database...",
skippable=True,
),
CommandStep(
cmd=[
"pipenv",
"run",
"invenio",
"index",
"destroy",
"--force",
"--yes-i-know",
],
env={"PIPENV_VERBOSITY": "-1"},
message="Destroying indices...",
skippable=True,
),
CommandStep(
cmd=["pipenv", "run", "invenio", "index", "queue", "init", "purge"],
env={"PIPENV_VERBOSITY": "-1"},
message="Purging queues...",
skippable=True,
),
FunctionStep(
func=self.cli_config.update_services_setup,
args={"is_setup": False},
message="Updating service setup status (False)...",
),
]
return steps
def _default_location_path(self):
"""Build default location path based on file storage selection."""
file_storage = self.cli_config.get_file_storage()
if file_storage == "local":
return "{}/data".format(self.cli_config.get_instance_path())
return "{}://default".format(self.cli_config.get_file_storage().lower())
def _setup(self):
"""Services initialization steps."""
steps = [
FunctionStep(
func=self.services_expected_status,
args={"expected": False},
message="Checking services are not setup...",
),
CommandStep(
cmd=["pipenv", "run", "invenio", "db", "init", "create"],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating database...",
),
CommandStep(
cmd=[
"pipenv",
"run",
"invenio",
"files",
"location",
"create",
"--default",
"default-location",
self._default_location_path(),
],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating files location...",
),
CommandStep(
cmd=["pipenv", "run", "invenio", "roles", "create", "admin"],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating admin role...",
),
CommandStep(
cmd=[
"pipenv",
"run",
"invenio",
"access",
"allow",
"superuser-access",
"role",
"admin",
],
env={"PIPENV_VERBOSITY": "-1"},
message="Allowing superuser access to admin role...",
),
CommandStep(
cmd=["pipenv", "run", "invenio", "index", "init"],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating indices...",
),
]
if rdm_version()[0] >= 10:
steps.extend(
[
CommandStep(
cmd=[
"pipenv",
"run",
"invenio",
"rdm-records",
"custom-fields",
"init",
],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating custom fields for records...",
),
CommandStep(
cmd=[
"pipenv",
"run",
"invenio",
"communities",
"custom-fields",
"init",
],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating custom fields for communities...",
),
]
)
if rdm_version()[0] >= 11:
steps.extend(self.rdm_fixtures())
steps.extend(self.translations())
if rdm_version()[0] >= 12:
steps.extend(self.declare_queues())
steps.append(
FunctionStep(
func=self.cli_config.update_services_setup,
args={"is_setup": True},
message="Updating service setup status (True)...",
),
)
return steps
[docs] def demo(self):
"""Steps to add demo records into the instance."""
steps = [
CommandStep(
cmd=["pipenv", "run", "invenio", "rdm-records", "demo"],
env={"PIPENV_VERBOSITY": "-1"},
message="Creating demo records...",
)
]
return steps
[docs] def declare_queues(self):
"""Steps to declare the MQ queues required for statistics, etc."""
command = ["pipenv", "run", "invenio", "queues", "declare"]
steps = [CommandStep(cmd=command, message="Declaring queues...")]
return steps
[docs] def fixtures(self):
"""Steps to set up the required fixtures for the instance."""
command = ["pipenv", "run", "invenio", "rdm-records", "fixtures"]
steps = [
CommandStep(
cmd=command,
env={"PIPENV_VERBOSITY": "-1"},
message="Creating records fixtures...",
)
]
return steps
[docs] def rdm_fixtures(self):
"""Steps to set up the rdm fixtures for the instance."""
command = ["pipenv", "run", "invenio", "rdm", "fixtures"]
steps = [
CommandStep(
cmd=command,
env={"PIPENV_VERBOSITY": "-1"},
message="Creating rdm fixtures...",
)
]
return steps
[docs] def translations(self):
"""Steps to compile translations."""
commands = TranslationsCommands(
project_path=self.cli_config.get_project_dir(),
instance_path=self.cli_config.get_instance_path(),
)
return commands.compile(symlink=False)
[docs] def setup(self, force, demo_data=True, stop=False, services=True):
"""Steps to setup services' containers.
A check in invenio-cli's config file is done to see if one-time setup
has been executed before.
"""
steps = []
if services:
steps.append(
FunctionStep(
func=self.ensure_containers_running,
message="Making sure containers are up...",
)
)
if force:
steps.extend(self._cleanup())
steps.extend(self._setup())
steps.extend(self.fixtures())
if demo_data:
steps.extend(self.demo())
if stop:
steps.append(
FunctionStep(
func=self.docker_helper.stop_containers,
message="Stopping containers....",
)
)
return steps
[docs] def start(self):
"""Steps to start services' containers."""
steps = [
FunctionStep(
func=self.ensure_containers_running,
message="Making sure containers are up...",
)
]
return steps
[docs] def stop(self):
"""Stops containers."""
steps = [
FunctionStep(
func=self.docker_helper.stop_containers,
message="Stopping containers...",
)
]
return steps
[docs] def destroy(self):
"""Steps to destroy the services's containers."""
steps = [
FunctionStep(
func=self.docker_helper.destroy_containers,
message="Destroying containers...",
),
FunctionStep(
func=self.cli_config.update_services_setup,
args={"is_setup": False},
message="Updating service setup status (False)...",
),
]
return steps
[docs] def status(self, services, verbose):
"""Checks the status of the given service.
:returns: A list of the same length than services. Each item will be a
code corresponding to: 0 success, 1 failure, 2 healthcheck
not defined.
"""
project_shortname = self.cli_config.get_project_shortname()
statuses = []
for service in services:
check = HEALTHCHECKS.get(service)
if check:
check_func = check["func"]
response = check_func(
filepath="docker-services.yml",
verbose=verbose,
project_shortname=project_shortname,
)
# Append 0 if OK, else 1
# FIXME: Deal with codes higher than 1. Needed?
code = 0 if response.status_code == 0 else 1
statuses.append((service, code))
else:
statuses.append((service, None))
return statuses