#!/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/>.
"""
Provisioning: Basic Functionality
=================================
Contains
--------
Basic functionality around provisioning.
"""
from networkx import DiGraph, is_directed_acyclic_graph, simple_cycles
from networkx.algorithms import topological_sort
from jsonschema import ValidationError
from isomer.logger import isolog, debug, verbose, warn, error
[docs]def log(*args, **kwargs):
"""Log as Emitter:MANAGE"""
kwargs.update({"emitter": "PROVISIONS", "frame_ref": 2})
isolog(*args, **kwargs)
[docs]def provisionList(
items, database_name, overwrite=False, clear=False, skip_user_check=False
):
"""Provisions a list of items according to their schema
:param items: A list of provisionable items.
:param database_name:
:param overwrite: Causes existing items to be overwritten
:param clear: Clears the collection first (Danger!)
:param skip_user_check: Skips checking if a system user is existing already
(for user provisioning)
:return:
"""
log("Provisioning", items, database_name, lvl=debug)
def get_system_user():
"""Retrieves the node local system user"""
user = objectmodels["user"].find_one({"name": "System"})
try:
log("System user uuid: ", user.uuid, lvl=verbose)
return user.uuid
except AttributeError as system_user_error:
log("No system user found:", system_user_error, lvl=warn)
log(
"Please install the user provision to setup a system user or "
"check your database configuration",
lvl=error,
)
return False
# TODO: Do not check this on specific objects but on the model (i.e. once)
def needs_owner(obj):
"""Determines whether a basic object has an ownership field"""
for privilege in obj._fields.get("perms", None):
if "owner" in obj._fields["perms"][privilege]:
return True
return False
import pymongo
from isomer.database import objectmodels, dbhost, dbport, dbname
database_object = objectmodels[database_name]
log(dbhost, dbname)
# TODO: Fix this to make use of the dbhost
client = pymongo.MongoClient(dbhost, dbport)
db = client[dbname]
if not skip_user_check:
system_user = get_system_user()
if not system_user:
return
else:
# TODO: Evaluate what to do instead of using a hardcoded UUID
# This is usually only here for provisioning the system user
# One way to avoid this, is to create (instead of provision)
# this one upon system installation.
system_user = "0ba87daa-d315-462e-9f2e-6091d768fd36"
col_name = database_object.collection_name()
if clear is True:
log("Clearing collection for", col_name, lvl=warn)
db.drop_collection(col_name)
counter = 0
for no, item in enumerate(items):
new_object = None
item_uuid = item["uuid"]
log("Validating object (%i/%i):" % (no + 1, len(items)), item_uuid, lvl=debug)
if database_object.count({"uuid": item_uuid}) > 0:
log("Object already present", lvl=warn)
if overwrite is False:
log("Not updating item", item, lvl=warn)
else:
log("Overwriting item: ", item_uuid, lvl=warn)
new_object = database_object.find_one({"uuid": item_uuid})
new_object._fields.update(item)
else:
new_object = database_object(item)
if new_object is not None:
try:
if needs_owner(new_object):
if not hasattr(new_object, "owner"):
log("Adding system owner to object.", lvl=verbose)
new_object.owner = system_user
except Exception as e:
log("Error during ownership test:", e, type(e), exc=True, lvl=error)
try:
new_object.validate()
new_object.save()
counter += 1
except ValidationError as e:
raise ValidationError(
"Could not provision object: " + str(item_uuid), e
)
log("Provisioned %i out of %i items successfully." % (counter, len(items)))
[docs]def provision(
list_provisions=False,
overwrite=False,
clear_provisions=False,
package=None,
installed=None,
):
from isomer.provisions import build_provision_store
from isomer.database import objectmodels
provision_store = build_provision_store()
if installed is None:
installed = []
def sort_dependencies(items):
"""Topologically sort the dependency tree"""
g = DiGraph()
log("Sorting dependencies")
for key, item in items:
log("key: ", key, "item:", item, pretty=True, lvl=debug)
dependencies = item.get("dependencies", [])
if isinstance(dependencies, str):
dependencies = [dependencies]
if key not in g:
g.add_node(key)
for link in dependencies:
g.add_edge(key, link)
if not is_directed_acyclic_graph(g):
log("Cycles in provisioning dependency graph detected!", lvl=error)
log("Involved provisions:", list(simple_cycles(g)), lvl=error)
topology = list(topological_sort(g))
topology.reverse()
topology = list(set(topology).difference(installed))
# log(topology, pretty=True)
return topology
sorted_provisions = sort_dependencies(provision_store.items())
# These need to be installed first in that order:
if "system" in sorted_provisions:
sorted_provisions.remove("system")
if "user" in sorted_provisions:
sorted_provisions.remove("user")
if "system" not in installed:
sorted_provisions.insert(0, "system")
if "user" not in installed:
sorted_provisions.insert(0, "user")
if list_provisions:
log(sorted_provisions, pretty=True)
exit()
def provision_item(provision_name):
"""Provision a single provisioning element"""
item = provision_store[provision_name]
method = item.get("method", provisionList)
model = item.get("model")
data = item.get("data")
method(data, model, overwrite=overwrite, clear=clear_provisions)
confirm_provision(provision_name)
def confirm_provision(provision_name):
if provision_name == "user":
log("Not confirming system user provision")
return
systemconfig = objectmodels["systemconfig"].find_one({"active": True})
if provision_name not in systemconfig.provisions["packages"]:
systemconfig.provisions["packages"].append(provision_name)
systemconfig.save()
if package is not None:
if package in provision_store:
log("Provisioning ", package, pretty=True)
provision_item(package)
else:
log(
"Unknown package: ",
package,
"\nValid provisionable packages are",
list(provision_store.keys()),
lvl=error,
emitter="MANAGE",
)
else:
for name in sorted_provisions:
log("Provisioning", name, pretty=True)
provision_item(name)