#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Isomer - The distributed application framework
# ==============================================
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <riot@c-base.org> and others.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Module: Configuration
=====================
Classic installer tidbits that should probably be moved to places elsewhere,
i.e. isomer.tool.instance and isomer.tool.environment
"""
import click
import os
import shutil
import sys
from distutils.dir_util import copy_tree
from subprocess import Popen
from click_didyoumean import DYMGroup
from isomer.tool.etc import NonExistentKey, instance_template
from isomer.logger import error, warn, debug
from isomer.tool import check_root, log, finish
from isomer.ui.builder import install_frontend
from isomer.provisions.base import provision
from git import Repo, exc
from isomer.version import version
@click.group(cls=DYMGroup)
@click.option("--port", help="Specify local Isomer port", default=8055)
@click.pass_context
def install(ctx, port):
"""[GROUP] Install various aspects of Isomer"""
# TODO: Make this a shortcut for a full instance install
# set_instance(ctx.obj['instance'], "blue") # Initially start with a blue instance
log("Configuration:", ctx.obj["config"])
log("Instance:", ctx.obj["instance"])
try:
instance = ctx.obj["instances"][ctx.obj["instance"]]
except NonExistentKey:
log("Instance unknown, so far.", lvl=warn)
instance = instance_template
log("New instance configuration:", instance)
environment_name = instance["environment"]
environment = instance["environments"][environment_name]
environment["port"] = port
# TODO: Remove sparse&superfluous environment info from context
ctx.obj["port"] = port
try:
repository = Repo("./")
ctx.obj["repository"] = repository
log("Repo:", repository)
environment["version"] = repository.git.describe()
except exc.GitError:
log("Not running from a git repository or there is a problem with it; "
"Using isomer.version", lvl=warn)
environment["version"] = version
ctx.obj["environment"] = environment
@install.command(short_help="build and install frontend")
@click.option(
"--dev", help="Use frontend development location", default=False, is_flag=True
)
@click.option(
"--rebuild",
help="Rebuild frontend before installation",
default=False,
is_flag=True,
)
@click.option(
"--no-install", help="Do not install requirements", default=False, is_flag=True
)
@click.option(
"--build-type",
help="Specify frontend build type. Either dist(default) or build",
default="dist",
)
@click.pass_context
def frontend(ctx, dev, rebuild, no_install, build_type):
"""Build and install frontend"""
# TODO: Move this to the environment handling and deprecate it here
install_frontend(
force_rebuild=rebuild,
development=dev,
install=not no_install,
build_type=build_type,
)
@install.command(short_help="build and install docs")
@click.option(
"--clear-target",
"--clear",
help="Clears target documentation " "folders",
default=False,
is_flag=True,
)
@click.pass_context
def docs(ctx, clear_target):
"""Build and install documentation"""
# TODO: Move this to the environment handling and deprecate it here
install_docs(str(ctx.obj["instance"]), clear_target)
finish(ctx)
[docs]def install_docs(instance, clear_target):
"""Builds and installs the complete Isomer documentation."""
check_root()
def make_docs():
"""Trigger a Sphinx make command to build the documentation."""
log("Generating HTML documentation")
try:
build = Popen(["make", "html"], cwd="docs/")
build.wait()
except Exception as e:
log(
"Problem during documentation building: ",
e,
type(e),
exc=True,
lvl=error,
)
return False
return True
make_docs()
# If these need changes, make sure they are watertight and don't remove
# wanted stuff!
target = os.path.join("/var/lib/isomer", instance, "frontend/docs")
source = "docs/build/html"
log("Updating documentation directory:", target)
if not os.path.exists(os.path.join(os.path.curdir, source)):
log(
"Documentation not existing yet. Run python setup.py "
"build_sphinx first.",
lvl=error,
)
return
if os.path.exists(target):
log("Path already exists: " + target)
if clear_target:
log("Cleaning up " + target, lvl=warn)
shutil.rmtree(target)
log("Copying docs to " + target)
copy_tree(source, target)
@install.command(short_help="install provisions")
@click.option(
"--package",
"-p",
help="Specify a package to provision (default=install all)",
default=None,
metavar="<name>",
)
@click.option(
"--clear-existing",
"--clear",
help="Clears already existing collections (DANGER!)",
is_flag=True,
default=False,
)
@click.option(
"--overwrite",
"-o",
help="Overwrites existing provisions",
is_flag=True,
default=False,
)
@click.option(
"--list-provisions",
"-l",
help="Only list available provisions",
is_flag=True,
default=False,
)
@click.pass_context
def provisions(ctx, package, clear_existing, overwrite, list_provisions):
"""Install default provisioning data"""
# TODO: Move this to the environment handling and deprecate it here
install_provisions(ctx, package, clear_existing, overwrite, list_provisions)
finish(ctx)
[docs]def install_provisions(
ctx, package, clear_provisions=False, overwrite=False, list_provisions=False
):
"""Install default provisioning data"""
log("Installing Isomer default provisions")
# from isomer.logger import verbosity, events
# verbosity['console'] = verbosity['global'] = events
from isomer import database
log("Instance settings:", ctx.obj, pretty=True, lvl=debug)
database.initialize(ctx.obj["dbhost"], ctx.obj["dbname"])
provision(list_provisions, overwrite, clear_provisions, package)
@install.command(short_help="install modules (DEPRECATED)", deprecated=True)
@click.option(
"--wip",
help="Install Work-In-Progress (alpha/beta-state) modules as well",
is_flag=True,
)
def modules(wip):
"""Install the plugin modules"""
# TODO: Remove altogether, this should be done via instance/environment only
install_modules(wip)
log("Done: Install Modules")
[docs]def install_modules(wip):
"""Install the plugin modules"""
def install_module(isomer_module):
"""Install a single module via setuptools"""
try:
setup = Popen(
[sys.executable, "setup.py", "develop"],
cwd="modules/" + isomer_module + "/",
)
setup.wait()
except Exception as e:
log(
"Problem during module installation: ",
isomer_module,
e,
type(e),
exc=True,
lvl=error,
)
return False
return True
# TODO: Sort module dependencies via topological sort or let pip do this in future.
# # To get the module dependencies:
# packages = {}
# for provision_entrypoint in iter_entry_points(group='isomer.provisions',
# name=None):
# log("Found packages: ", provision_entrypoint.dist.project_name, lvl=warn)
#
# _package_name = provision_entrypoint.dist.project_name
# _package = pkg_resources.working_set.by_key[_package_name]
#
# print([str(r) for r in _package.requires()]) # retrieve deps from setup.py
modules_production = [
# TODO: Poor man's dependency management, as long as the modules are
# installed from local sources and they're not available on pypi,
# which would handle real dependency management for us:
"navdata",
# Now all the rest:
"alert",
"automat",
"busrepeater",
"calendar",
"countables",
"dash",
# 'dev',
"enrol",
"mail",
"maps",
"nmea",
"nodestate",
"project",
"webguides",
"wiki",
]
modules_wip = [
"calc",
"camera",
"chat",
"comms",
"contacts",
"crew",
"equipment",
"filemanager",
"garden",
"heroic",
"ldap",
"library",
"logbook",
"protocols",
"polls",
"mesh",
"robot",
"switchboard",
"shareables",
]
installables = modules_production
if wip:
installables.extend(modules_wip)
success = []
failed = []
for installable in installables:
log("Installing module ", installable)
if install_module(installable):
success.append(installable)
else:
failed.append(installable)
log("Installed modules: ", success)
if len(failed) > 0:
log("Failed modules: ", failed)