コード例 #1
0
    def build(self, *args, **kwargs):
        services = []
        db = DB()
        db.query("""
            SELECT id, service_type, host, title, autostart, state, last_seen
            FROM services ORDER BY id
            """)
        for id, service_type, host, title, autostart, state, last_seen in db.fetchall(
        ):
            service = {
                "id": id,
                "service_type": service_type,
                "host": host,
                "title": title,
                "autostart": autostart,
                "state": state,
                "last_seen": last_seen,
                "message": "",
            }
            if time.time() - last_seen > 120:
                nrtime = s2words(time.time() - last_seen)
                service["message"] = f"Not responding for {nrtime}"
            services.append(service)

        self["name"] = "services"
        self["title"] = "Services"
        self["js"] = ["/static/js/services.js"]
        self["data"] = services
コード例 #2
0
ファイル: __init__.py プロジェクト: nebulabroadcast/nebula
    def channel_recover(self):
        logging.warning("Performing recovery")

        db = DB()
        db.query(
            """
            SELECT id_item, start FROM asrun
            WHERE id_channel = %s ORDER BY id DESC LIMIT 1
            """,
            [self.id_channel],
        )
        try:
            last_id_item, last_start = db.fetchall()[0]
        except IndexError:
            logging.error("Unable to perform recovery.")
        last_item = Item(last_id_item, db=db)
        last_item.asset

        self.controller.current_item = last_item
        self.controller.cued_item = False
        self.controller.cued_fname = False

        if last_start + last_item.duration <= time.time():
            logging.info(f"Last {last_item} has been broadcasted.")
            new_item = self.cue_next(item=last_item, db=db, play=True)
        else:
            logging.info(f"Last {last_item} has not been fully broadcasted.")
            new_item = self.cue_next(item=last_item, db=db)

        if not new_item:
            logging.error("Recovery failed. Unable to cue")
            return

        self.on_change()
コード例 #3
0
 def on_init(self):
     self.actions = []
     db = DB()
     db.query("SELECT id, title, settings FROM actions")
     for id, title, settings in db.fetchall():
         settings = xml(settings)
         self.actions.append(Action(id, title, settings))
コード例 #4
0
 def on_main(self):
     db = DB()
     db.query("SELECT id, meta FROM assets WHERE status=%s",
              [ObjectStatus.ONLINE])
     for _, meta in db.fetchall():
         asset = Asset(meta=meta, db=db)
         self.proc(asset)
コード例 #5
0
ファイル: passwd.py プロジェクト: nebulabroadcast/nebula
def passwd(*args):
    print()
    try:
        login = input("Login: "******"Password (will be echoed): ").strip()
        is_admin = input("Admin (yes/no): ").strip()
    except KeyboardInterrupt:
        print()
        logging.warning("Interrupted by user")
        sys.exit(0)

    db = DB()
    db.query("SELECT id FROM users WHERE login=%s", [login])
    res = db.fetchall()
    if not res:
        critical_error("Unable to set password: no such user")

    u = User(res[0][0], db=db)
    if login:
        u["login"] = u["full_name"] = login
    u["is_admin"] = 1 if is_admin == "yes" else 0
    u.set_password(password)
    u.save()
    print()
    logging.goodnews("Password changed")
コード例 #6
0
def meta_exists(key, value, db=False):
    if not db:
        db = DB()
    db.query("SELECT id, meta FROM assets WHERE meta->>%s = %s",
             [str(key), str(value)])
    for _, meta in db.fetchall():
        return Asset(meta=meta, db=db)
    return False
コード例 #7
0
 def shutdown(self, no_restart=False):
     logging.info("Shutting down")
     if no_restart:
         db = DB()
         db.query("UPDATE services SET autostart=FALSE WHERE id=%s",
                  [self.id_service])
         db.commit()
     self.on_shutdown()
     sys.exit(0)
コード例 #8
0
    def on_main(self):
        if not self.import_dir:
            return

        if not os.path.isdir(self.import_dir):
            logging.error("Import directory does not exist. Shutting down.")
            self.import_path = False
            self.shutdown(no_restart=True)
            return

        db = DB()
        for import_file in get_files(self.import_dir, exts=self.exts):
            idec = import_file.base_name
            try:
                with import_file.open("rb") as f:
                    f.seek(0, 2)
                    fsize = f.tell()
            except IOError:
                logging.debug(f"Import file {import_file.base_name} is busy.")
                continue

            if not (import_file.path in self.filesizes
                    and self.filesizes[import_file.path] == fsize):
                self.filesizes[import_file.path] = fsize
                logging.debug(f"New file '{import_file.base_name}' detected")
                continue

            db.query(
                """
                SELECT meta FROM assets
                WHERE meta->>%s = %s
                """,
                [self.identifier, idec],
            )
            for (meta, ) in db.fetchall():
                asset = Asset(meta=meta, db=db)

                if not (asset["id_storage"] and asset["path"]):
                    mk_error(import_file, "This file has no target path.")
                    continue

                if self.versioning and os.path.exists(asset.file_path):
                    version_backup(asset)

                do_import(self, import_file, asset)
                break
            else:
                mk_error(import_file, "This file is not expected.")

        for fname in os.listdir(self.import_dir):
            if not fname.endswith(".error.txt"):
                continue
            idec = fname.replace(".error.txt", "")
            if idec not in [
                    os.path.splitext(f)[0] for f in os.listdir(self.import_dir)
            ]:
                os.remove(os.path.join(self.import_dir, fname))
コード例 #9
0
 def on_init(self):
     self.services = {}
     db = DB()
     db.query("SELECT id, pid FROM services WHERE host=%s", [config["host"]])
     for _, pid in db.fetchall():
         if pid:
             self.kill_service(pid)
     db.query("UPDATE services SET state = 0 WHERE host=%s", [config["host"]])
     db.commit()
コード例 #10
0
    def build(self, *args, **kwargs):
        db = DB()
        id_user = int(kwargs.get("id_user", 0))
        if id_user and self["user"]["is_admin"]:
            user = User(id_user, db=db)
        else:
            user = self["user"]

        self["user"] = user

        password = kwargs.get("password", False)
        full_name = kwargs.get("full_name", False)
        dashboard = kwargs.get("dashboard", "")
        user_changed = False

        if cherrypy.request.method == "POST":
            if full_name:
                user["full_name"] = kwargs["full_name"]
                user_changed = True

            if password:
                if len(password) < 8:
                    self.context.message(WEAK_PASS_MSG, "error")
                    return
                user.set_password(kwargs["password"])
                user_changed = True

            if dashboard != user["dashboard"]:
                user["dashboard"] = dashboard
                user_changed = True

            if user_changed:
                user.save()
                if self["user"].id == user.id:
                    self.context["user"] = user.meta
                self.context.message("User profile saved")

        db.query("SELECT meta FROM users WHERE meta->>'is_admin' = 'true'")

        self["admins"] = [User(meta=meta) for meta, in db.fetchall()]
        self["name"] = "profile"
        self["title"] = "User profile"
        self["rights"] = [
            ["asset_edit", "Edit assets", "folders"],
            ["asset_create", "Create assets", "folders"],
            ["rundown_view", "View rundown", "playout_channels"],
            ["rundown_edit", "Edit rundown", "playout_channels"],
            ["scheduler_view", "View scheduler", "playout_channels"],
            ["scheduler_edit", "Modify scheduler", "playout_channels"],
            ["job_control", "Control jobs", "actions"],
            ["service_control", "Control services", "services"],
            ["mcr", "Control playout", "playout_channels"],
        ]
コード例 #11
0
ファイル: j.py プロジェクト: nebulabroadcast/nebula
def j(*args):
    print
    db = DB()
    db.query("""
        SELECT
            j.id,
            j.id_action,
            j.settings,
            j.priority,
            j.retries,
            j.status,
            j.progress,
            j.message,
            j.creation_time,
            j.start_time,
            j.end_time,
            a.meta
        FROM
            jobs AS j,
            assets AS a
        WHERE
            a.id = j.id_asset
        AND j.status in (0,1,5)

        ORDER BY
            id DESC LIMIT 50
            """)

    for (
            id,
            id_action,
            settings,
            priority,
            retries,
            status,
            progress,
            message,
            creation_time,
            start_time,
            end_time,
            meta,
    ) in db.fetchall():
        asset = Asset(meta=meta)

        line = "{:<30}".format(asset)
        line += "{} {:.02f}%\n".format(status, progress)

        try:
            sys.stdout.write(line)
            sys.stdout.flush()
        except IOError:
            pass
コード例 #12
0
ファイル: __init__.py プロジェクト: nebulabroadcast/nebula
 def on_init(self):
     self.service_type = "conv"
     self.actions = []
     db = DB()
     db.query("""
         SELECT id, title, service_type, settings
         FROM actions ORDER BY id
         """)
     for id_action, title, service_type, settings in db.fetchall():
         if service_type == self.service_type:
             logging.debug(f"Registering action {title}")
             self.actions.append(Action(id_action, title, xml(settings)))
     self.reset_jobs()
コード例 #13
0
def get_bin_first_item(id_bin, db=False):
    if not db:
        db = DB()
    db.query(
        """
        SELECT id, meta FROM items
        WHERE id_bin=%s
        ORDER BY position LIMIT 1
        """,
        [id_bin],
    )
    for _, meta in db.fetchall():
        return Item(meta=meta, db=db)
    return False
コード例 #14
0
 def on_init(self):
     self.last_update = 0
     db = DB()
     try:
         db.query(
             """
             INSERT INTO hosts(hostname, last_seen)
             VALUES (%s, %s) ON CONFLICT DO NOTHING
             """,
             [config["host"], time.time()],
         )
     except psycopg2.IntegrityError:
         pass
     else:
         db.commit()
コード例 #15
0
def asset_by_path(id_storage, path, db=False):
    id_storage = str(id_storage)
    path = path.replace("\\", "/")
    if not db:
        db = DB()
    db.query(
        """
            SELECT id, meta FROM assets
                WHERE media_type = %s
                AND meta->>'id_storage' = %s
                AND meta->>'path' = %s
        """,
        [MediaType.FILE, id_storage, path],
    )
    for id, meta in db.fetchall():
        return Asset(meta=meta, db=db)
    return False
コード例 #16
0
def get_day_events(id_channel, date, num_days=1):
    chconfig = config["playout_channels"][id_channel]
    start_time = datestr2ts(date, *chconfig.get("day_start", [6, 0]))
    end_time = start_time + (3600 * 24 * num_days)
    db = DB()
    db.query(
        """
        SELECT id, meta
        FROM events
        WHERE id_channel=%s
        AND start > %s
        AND start < %s
        """,
        (id_channel, start_time, end_time),
    )
    for _, meta in db.fetchall():
        yield Event(meta=meta)
コード例 #17
0
def get_user(login, password, db=False):
    if not db:
        db = DB()
    try:
        db.query(
            """
            SELECT meta FROM users
            WHERE login=%s AND password=%s
            """,
            [login, get_hash(password)],
        )
    except ValueError:
        return False
    res = db.fetchall()
    if not res:
        return False
    return User(meta=res[0][0])
コード例 #18
0
    def heartbeat(self):
        db = DB()
        db.query("SELECT state FROM services WHERE id=%s", [self.id_service])
        try:
            state = db.fetchall()[0][0]
        except IndexError:
            state = ServiceState.KILL
        else:
            if state == 0:
                state = 1
            db.query(
                "UPDATE services SET last_seen=%s, state=%s WHERE id=%s",
                [time.time(), state, self.id_service],
            )
            db.commit()

        if state in [
                ServiceState.STOPPED, ServiceState.STOPPING, ServiceState.KILL
        ]:
            self.shutdown()
コード例 #19
0
ファイル: __init__.py プロジェクト: nebulabroadcast/nebula
 def reset_jobs(self):
     db = DB()
     db.query(
         """
         UPDATE jobs SET
             id_service=NULL,
             progress=0,
             retries=0,
             status=5,
             message='Restarting after service restart',
             start_time=0,
             end_time=0
         WHERE
             id_service=%s AND STATUS IN (0,1,5)
         RETURNING id
         """,
         [self.id_service],
     )
     for (id_job, ) in db.fetchall():
         logging.info(f"Restarting job ID {id_job} (converter restarted)")
     db.commit()
コード例 #20
0
    def __init__(self, id_service, settings=False):
        logging.debug(f"Initializing service ID {id_service}")
        self.id_service = id_service
        self.settings = settings
        config["id_service"] = id_service

        try:
            self.on_init()
        except SystemExit:
            sys.exit(1)
        except Exception:
            log_traceback(f"Unable to initialize service ID {id_service}")
            self.shutdown()
        else:
            db = DB()
            db.query(
                "UPDATE services SET last_seen = %s, state=1 WHERE id=%s",
                [time.time(), self.id_service],
            )
            db.commit()
        logging.goodnews("Service started")
コード例 #21
0
ファイル: __init__.py プロジェクト: nebulabroadcast/nebula
    def on_change(self):
        if not self.controller.current_item:
            return

        item = self.controller.current_item
        db = DB()

        self.current_asset = item.asset or Asset()
        self.current_event = item.event or Event()

        logging.info(f"Advanced to {item}")

        if self.last_run:
            db.query(
                """
                UPDATE asrun SET stop = %s
                WHERE id = %s""",
                [int(time.time()), self.last_run],
            )
            db.commit()

        if self.current_item:
            db.query(
                """
                INSERT INTO asrun (id_channel, id_item, start)
                VALUES (%s, %s, %s)
                """,
                [self.id_channel, item.id,
                 time.time()],
            )
            self.last_run = db.lastid()
            db.commit()
        else:
            self.last_run = False

        for plugin in self.plugins:
            try:
                plugin.on_change()
            except Exception:
                log_traceback("Plugin on-change failed")
コード例 #22
0
 def set_progress(self, progress, message="In progress"):
     db = DB()
     progress = round(progress, 2)
     db.query(
         """
         UPDATE jobs SET
             status=1,
             progress=%s,
             message=%s
         WHERE id=%s
         """,
         [progress, message, self.id],
     )
     db.commit()
     messaging.send(
         "job_progress",
         id=self.id,
         id_asset=self.id_asset,
         id_action=self.id_action,
         status=JobState.IN_PROGRESS,
         progress=progress,
         message=message,
     )
コード例 #23
0
ファイル: s.py プロジェクト: nebulabroadcast/nebula
def s(*args):
    print()
    db = DB()

    if len(args) >= 2:
        try:
            services = tuple([int(i.strip()) for i in args[1:]])
        except ValueError:
            critical_error("Wrong service ID")

        if args[0] == "start":
            db.query("UPDATE services SET state=2 WHERE id IN %s AND state=0",
                     [services])
            db.commit()
        elif args[0] == "stop":
            db.query("UPDATE services SET state=3 WHERE id IN %s AND state=1",
                     [services])
            db.commit()
        elif args[0] == "kill":
            db.query(
                "UPDATE services SET state=4 WHERE id IN %s AND state IN (1,3)",
                [services],
            )
            db.commit()
        elif args[0] == "auto":
            db.query("UPDATE services SET autostart=TRUE WHERE id IN %s",
                     [services])
            db.commit()
        elif args[0] == "noauto":
            db.query("UPDATE services SET autostart=FALSE WHERE id IN %s",
                     [services])
            db.commit()
        else:
            critical_error("Unsupported command: {}".format(args[0]))
        time.sleep(1)

    show_services(db)
コード例 #24
0
def update_host_info():
    hostname = config["host"]

    mp2id = {}
    for id_storage, storage in storages.items():
        mp2id[storage.local_path] = id_storage

    p = Promexp(provider_settings={"casparcg": None})
    p.metrics.add("runtime_seconds", time.time() - NEBULA_START_TIME)

    p.collect()
    for metric in list(p.metrics.data.keys()):
        mname, tags = metric
        if mname in [
                "storage_bytes_total", "storage_bytes_free", "storage_usage"
        ]:
            id_storage = mp2id.get(tags["mountpoint"])
            if id_storage is None:
                continue
            value = p.metrics.data[metric]
            del p.metrics.data[metric]
            p.metrics.add(
                f"shared_{mname}",
                value,
                id=id_storage,
                title=storages[id_storage].title,
            )

    status = {"metrics": p.metrics.dump()}

    db = DB()
    db.query(
        "UPDATE hosts SET last_seen=%s, status=%s WHERE hostname=%s",
        [time.time(), json.dumps(status), hostname],
    )
    db.commit()
コード例 #25
0
ファイル: system.py プロジェクト: nebulabroadcast/nebula
def api_system(**kwargs):
    """
    Returns system status and controls services
    Arguments:

    request       list of information to show.
                  defaults to everything ["services", "hosts"]
    stop          stop service by its ID
    start         start service by its ID
    kill          kill service by its ID
    set_autostart toggle service autostart param (True/False)
    """

    user = kwargs.get("user", anonymous)
    message = ""
    if not user:
        return NebulaResponse(401)

    db = DB()

    request = kwargs.get("request", ["services", "hosts"])

    if "stop" in kwargs:
        id_service = kwargs["stop"]
        if not type(id_service) == int:
            return NebulaResponse(400, "Invalid ID service to stop")
        if not user.has_right("service_control", id_service):
            return NebulaResponse(
                403, "You are not allowed to control this service")
        db.query("UPDATE services SET state=3 WHERE id=%s AND state = 1",
                 [id_service])
        db.commit()
        logging.info(f"{user} requested service ID {id_service} "
                     f"({config['services'][id_service]['title']}) stop")
        message = "Service is stopping"

    if "start" in kwargs:
        id_service = kwargs["start"]
        if not type(id_service) == int:
            return NebulaResponse(400, "Invalid ID service to start")
        if not user.has_right("service_control", id_service):
            return NebulaResponse(
                403, "You are not allowed to control this service")
        db.query("UPDATE services SET state=2 WHERE id=%s AND state = 0",
                 [id_service])
        db.commit()
        logging.info(f"{user} requested service ID {id_service} "
                     f"({config['services'][id_service]['title']}) start")
        message = "Service is starting"

    if "kill" in kwargs:
        id_service = kwargs["kill"]
        if not type(id_service) == int:
            return NebulaResponse(400, "Invalid ID service to kill")
        if not user.has_right("service_control", id_service):
            return NebulaResponse(
                403, "You are not allowed to control this service")
        db.query("UPDATE services SET state=4 WHERE id=%s AND state = 3",
                 [id_service])
        db.commit()
        logging.info(f"{user} requested service ID {id_service} "
                     f"({config['services'][id_service]['title']}) kill")
        message = "Attempting to kill the service"

    if "autostart" in kwargs:
        id_service = kwargs["autostart"]
        if not type(id_service) == int:
            return NebulaResponse(400, "Invalid ID service to set autostart")
        if not user.has_right("service_control", id_service):
            return NebulaResponse(
                403, "You are not allowed to control this service")
        db.query("UPDATE services SET autostart=NOT autostart WHERE id=%s",
                 [id_service])
        logging.info(f"{user} requested service ID {id_service} "
                     f"({config['services'][id_service]['title']}) autostart")
        db.commit()
        message = "Service auto-start updated"

    result = {}
    if "services" in request:
        services = []
        db.query("""
            SELECT id, service_type, host, title, autostart, state, last_seen
            FROM services ORDER BY id ASC
            """)
        for id, service_type, host, title, autostart, state, last_seen in db.fetchall(
        ):
            service = {
                "id": id,
                "type": service_type,
                "host": host,
                "title": title,
                "autostart": autostart,
                "state": state,
                "last_seen": last_seen,
                "last_seen_before": time.time() - last_seen,
            }
            services.append(service)
        result["services"] = services

    if "hosts" in request:
        hosts = []
        db.query(
            "SELECT hostname, last_seen, status FROM hosts ORDER BY hostname")
        for hostname, last_seen, status in db.fetchall():
            host = status
            host["hostname"] = hostname
            host["last_seen"] = last_seen
            hosts.append(host)
        result["hosts"] = hosts

    return NebulaResponse(200, data=result, message=message)
コード例 #26
0
 def soft_stop(self):
     logging.info("Soft stop requested")
     db = DB()
     db.query("UPDATE services SET state=3 WHERE id=%s", [self.id_service])
     db.commit()
コード例 #27
0
    def main(self):
        db = DB()
        db.query(
            """
            SELECT
                id,
                title,
                autostart,
                state,
                last_seen
            FROM services
            WHERE host=%s
            """,
            [config["host"]],
        )

        #
        # Start / stop service
        #

        for id, title, autostart, state, last_seen in db.fetchall():
            messaging.send(
                "service_state",
                id=id,
                state=state,
                autostart=autostart,
                last_seen=last_seen,
                last_seen_before=max(0, int(time.time() - last_seen)),
            )
            if state == ServiceState.STARTING:  # Start service
                if id not in self.services.keys():
                    self.start_service(id, title, db=db)

            elif state == ServiceState.KILL:  # Kill service
                if id in self.services.keys():
                    self.kill_service(self.services[id][0].pid)

        #
        # Real service state
        #

        service_list = [i for i in self.services.keys()]
        for id_service in service_list:
            proc, title = self.services[id_service]
            if proc.poll() is None:
                continue
            del self.services[id_service]
            logging.warning(f"Service ID {id_service} ({title}) terminated")
            db.query("UPDATE services SET state=0 WHERE id = %s", [id_service])
            db.commit()

        #
        # Autostart
        #

        db.query(
            """
            SELECT id, title, state, autostart
            FROM services
            WHERE host=%s AND state=0 AND autostart=true
            """,
            [config["host"]],
        )
        for id, title, state, autostart in db.fetchall():
            if id not in self.services.keys():
                logging.debug(f"AutoStarting service ID {id} ({title})")
                self.start_service(id, title)
コード例 #28
0
 def load(self, id_action):
     db = DB()
     db.query("SELECT title, settings FROM actions WHERE id = %s",
              [id_action])
     for title, settings in db.fetchall():
         self.data[id_action] = Action(id_action, title, xml(settings))
コード例 #29
0
    def build(self, *args, **kwargs):
        self["name"] = "passreset"
        self["title"] = "Password reset"
        self["mode"] = "email-entry"

        #
        # REQUEST EMAIL
        #

        if "email" in kwargs:
            email = kwargs["email"].strip()

            if not re.match(EMAIL_REGEXP, email):
                self.context.message("Invalid e-mail address specified", "error")
                return

            db = DB()
            db.query(
                """
                SELECT meta
                FROM users where LOWER(meta->>'email') = LOWER(%s)
                """,
                [email],
            )
            try:
                user = User(meta=db.fetchall()[0][0], db=db)
            except IndexError:
                self.context.message("No such user", "error")
                return

            if time.time() - user.meta.get("pass_reset_time", 0) < 3600:
                self.context.message(
                    "Only one password reset request per hour is allowed", "error"
                )
                return

            token = get_guid()

            user["pass_reset_time"] = time.time()
            user["pass_reset_code"] = token

            mailvars = {
                "name": user["full_name"] or user["login"],
                "site_name": config["site_name"],
                "hub_url": config.get(
                    "hub_url", f"https://{config['site_name']}.nbla.cloud"
                ),
                "token": token,
            }

            body = MAIL_BODY.format(**mailvars)

            try:
                send_mail(email, "Nebula password reset", body)
            except Exception:
                log_traceback()
                self.context.message(
                    """Unable to send password reset email.
                Please contact your system administrator""",
                    "error",
                )
                return

            user.save()

            self["mode"] = "mail-sent"
            return

        #
        # GOT TOKEN
        #

        elif "token" in kwargs:
            token = kwargs["token"].strip()
            self["mode"] = False
            self["token"] = token

            if not re.match(GUID_REGEXP, token):
                self.context.message("Invalid token specified", "error")
                return

            db = DB()
            db.query(
                "SELECT meta FROM users WHERE meta->>'pass_reset_code' = %s", [token]
            )
            try:
                user = User(meta=db.fetchall()[0][0], db=db)
            except IndexError:
                self.context.message("No such token", "error")
                return

            if user["pass_reset_time"] < time.time() - 3600:
                self.context.message("Token expired.", "error")
                self["mode"] = "email-entry"
                return

            pass1 = kwargs.get("pass1", False)
            pass2 = kwargs.get("pass2", False)
            if pass1 and pass2:
                if pass1 != pass2:
                    self["mode"] = "pass-entry"
                    self.context.message("Passwords don't match", "error")
                    return

                if len(pass1) < 8:
                    self["mode"] = "pass-entry"
                    self.context.message(
                        "The password is weak. Must be at least 8 characters", "error"
                    )
                    return

                user.set_password(pass1)
                del user.meta["pass_reset_code"]
                del user.meta["pass_reset_time"]
                user.save()

                self["mode"] = "finished"
                return

            self["mode"] = "pass-entry"
            return
コード例 #30
0
    def build(self, *args, **kwargs):

        #
        # If user has custom dashboard, load webtool plugin
        #

        custom_dash = self["user"]["dashboard"]

        if custom_dash:
            try:
                Plugin, title = self["site"]["webtools"].tools[custom_dash]
            except KeyError:
                raise cherrypy.HTTPError(404, f"No such tool {custom_dash}")

            try:
                args = args[1:]
            except IndexError:
                args = []
            plugin = Plugin(self, custom_dash)
            self["title"] = title
            self.view = "tool"

            body = plugin.build(*args, **kwargs)
            if plugin.native:
                self.is_raw = False
                self["body"] = body
            else:
                self.is_raw = True
                self.body = body
            return

        #
        # Hosts information (node status)
        #

        storage_info = {}

        db = DB()
        hosts = {}
        db.query("""
            SELECT hostname, last_seen, status
            FROM hosts ORDER BY hostname ASC
            """)

        tagmap = {
            "cpu_usage": "cpu",
            "memory_bytes_total": "mem_total",
            "memory_bytes_free": "mem_free",
            "swap_bytes_total": "swp_total",
            "swap_bytes_free": "swp_free",
        }

        sinfo = {id: {"title": storages[id].title} for id in storages}

        for hostname, last_seen, status in db.fetchall():
            host_info = {}
            for name, tags, value in status.get("metrics", []):
                if name == "storage_bytes_total" and tags.get(
                        "mountpoint") == "/":
                    host_info["root_total"] = value
                elif name == "storage_bytes_free" and tags.get(
                        "mountpoint") == "/":
                    host_info["root_free"] = value

                elif name == "shared_storage_bytes_total":
                    sinfo[int(tags.get("id"))]["total"] = value

                elif name == "shared_storage_bytes_free":
                    sinfo[int(tags.get("id"))]["free"] = value

                elif name in tagmap:
                    host_info[tagmap[name]] = value
            hosts[hostname] = host_info
        storage_info = [{
            "id": id,
            **d
        } for id, d in sinfo.items() if d.get("total") and d.get("free")]

        #
        # MAM statistics
        #

        object_counts = {}
        for obj_type in ["assets", "items", "bins", "events"]:
            db.query("SELECT COUNT(id) FROM {}".format(obj_type))
            object_counts[obj_type] = db.fetchall()[0][0]

        self["name"] = "dashboard"
        self["title"] = "Dashboard"
        self["js"] = []
        self["hosts"] = hosts
        self["storages"] = storage_info
        self["object_counts"] = object_counts