Commit 72d0c68b authored by David Huss's avatar David Huss 💬
Browse files

Allow multiple endpoints in user configuration

parent a92f06ee
......@@ -39,5 +39,22 @@ Run bbbmon with:
poetry run
```
For bbbmon to run you need to have a `bbbmon.properties` file at the path specified. In this file there should be your servers secret and the server URL. You can find this secret on your server in the file `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties` (look for a line starting with `securitySalt=` and copy it to). If in doubt just follow the instructions the CLI gives you.
# Configuration
Just run `bbbmon` and it will tell you what it needs.
You can define one or more endpoints in the `bbbmon.properties` file at the path bbbmon tells you about. In this file you can specify multiple bbb servers each with it's secret and bigbluebutton-URL. You can find the secret on your server in it's config-file via `cat /usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties | grep securitySalt=`
A example configuration file could look like this:
```toml
[bbb.example.com]
securitySalt=MY_SUPER_SECRET_SECRET
bigbluebutton.web.serverURL=https://bbb.example.com/
[Foo's private bbb-server]
securitySalt=MY_SUPER_SECRET_SECRET2
bigbluebutton.web.serverURL=https://bbb.foo.com/
```
The section names in the square brackets will be used as display names (these support utf-8)
\ No newline at end of file
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import hashlib
from datetime import datetime, timedelta
import requests
import appdirs
from xml.etree import cElementTree as ElementTree
from typing import NewType, Optional, Tuple, Iterable
from typing import NewType, Optional, Tuple, Iterable, List
# Local module imports
from bbbmon.xmldict import XmlListConfig, XmlDictConfig
from bbbmon.configuration import Config, Endpoint, SERVER_PROPERTIES_FILE, Url, Secret
# Default path
SERVER_PROPERTIES_FILE = "/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties"
# Type definitions
Secret = NewType('Secret', str)
Url = NewType('Url', str)
FRIENDLY_KEYNAMES = {
"participantCount": "Participants",
......@@ -27,73 +23,6 @@ FRIENDLY_KEYNAMES = {
}
class XmlListConfig(list):
"""
Helper class to convert XML to python dicts
"""
def __init__(self, aList):
for element in aList:
if element:
# treat like dict
if len(element) == 1 or element[0].tag != element[1].tag:
self.append(XmlDictConfig(element))
# treat like list
elif element[0].tag == element[1].tag:
self.append(XmlListConfig(element))
elif element.text:
text = element.text.strip()
if text:
self.append(text)
class XmlDictConfig(dict):
'''
Example usage:
>>> tree = ElementTree.parse('your_file.xml')
>>> root = tree.getroot()
>>> xmldict = XmlDictConfig(root)
Or, if you want to use an XML string:
>>> root = ElementTree.XML(xml_string)
>>> xmldict = XmlDictConfig(root)
And then use xmldict for what it is... a dict.
'''
def __init__(self, parent_element):
if parent_element.items():
self.update(dict(parent_element.items()))
for element in parent_element:
if element:
# treat like dict - we assume that if the first two tags
# in a series are different, then they are all different.
if len(element) == 1 or element[0].tag != element[1].tag:
aDict = XmlDictConfig(element)
# treat like list - we assume that if the first two tags
# in a series are the same, then the rest are the same.
else:
# here, we put the list in dictionary; the key is the
# tag name the list elements all share in common, and
# the value is the list itself
aDict = {element[0].tag: XmlListConfig(element)}
# if the tag has attributes, add those to the dict
if element.items():
aDict.update(dict(element.items()))
self.update({element.tag: aDict})
# this assumes that if you've got an attribute in a tag,
# you won't be having any text. This may or may not be a
# good idea -- time will tell. It works for the way we are
# currently doing XML configuration files...
elif element.items():
self.update({element.tag: dict(element.items())})
# finally, if there are no child tags and no attributes, extract
# the text
else:
self.update({element.tag: element.text})
def generate_checksum(call_name: str, query_string: str, secret: Secret) -> str:
"""
Generate Checksum for the request header (passed as value for `?checksum=`)
......@@ -134,8 +63,7 @@ def get_meetings(secret: Secret, bbb_url: Url) -> Iterable[XmlDictConfig]:
d = request_meetings(secret, bbb_url)
if d["meetings"] is None:
print("There are no active meetings currently.")
exit()
return []
if type(d["meetings"]["meeting"]) is XmlListConfig:
meetings = sorted([m for m in d["meetings"]["meeting"] if m["running"] == "true"], key=lambda x:int(x['participantCount']), reverse=True)
......@@ -181,6 +109,9 @@ def strfdelta(duration: timedelta) -> str:
def format_duration(meeting: XmlDictConfig) -> str:
"""
Helper functions for duration
"""
duration = get_duration(meeting)
return strfdelta(duration)
......@@ -220,48 +151,55 @@ def print_duration_leaderboard(meetings: Iterable[XmlDictConfig]):
print("{:>12} {:<45} {}".format(format_duration(m), m["meetingName"], get_formated_presenter_name(m)))
def print_overview(secret: Secret, bbb_url: Url):
def print_overview(config: Config):
"""
Get the meetings and print out an overview of the current bbb-usage
For each endpoint in the configuration get the active meetings and print
out an overview of the current bbb-usage
"""
meetings = get_meetings(secret, bbb_url)
for i, endpoint in enumerate(config.endpoints):
meetings = get_meetings(endpoint.secret, endpoint.url)
if len(meetings) == 0:
print("There are no meetings running now.")
exit()
# Print divider if there is more than one endpoint
if i > 0:
print()
print("="*80)
print()
# If there are no meetings, skip to next endpoint
if len(meetings) == 0:
print("MEETINGS on [{}]: None".format(endpoint.name))
continue
n_running = len(meetings)
n_recording = len([m for m in meetings if m["recording"] == "true"])
n_participants = sum([int(m["participantCount"]) for m in meetings])
n_listeners = sum([int(m["listenerCount"]) for m in meetings])
n_voice = sum([int(m["voiceParticipantCount"]) for m in meetings])
n_video = sum([int(m["videoCount"]) for m in meetings])
n_moderator = sum([int(m["moderatorCount"]) for m in meetings])
print("MEETINGS on {}:".format(bbb_url))
print(" ├─── {:>4} running".format(n_running))
print(" └─── {:>4} recording".format(n_recording))
print()
print("PARTICIPANTS across all {} rooms".format(n_running))
print(" └─┬─ {:>4} total".format(n_participants))
print(" ├─ {:>4} listening only".format(n_listeners))
print(" ├─ {:>4} mic on".format(n_voice))
print(" ├─ {:>4} video on".format(n_video))
print(" └─ {:>4} moderators".format(n_moderator))
print()
print_leaderboard(meetings, "participantCount")
print()
print_leaderboard(meetings, "videoCount")
print()
print_leaderboard(meetings, "voiceParticipantCount")
print()
print_duration_leaderboard(meetings)
def init_variables() -> Optional[Tuple[Secret, Url]]:
n_running = len(meetings)
n_recording = len([m for m in meetings if m["recording"] == "true"])
n_participants = sum([int(m["participantCount"]) for m in meetings])
n_listeners = sum([int(m["listenerCount"]) for m in meetings])
n_voice = sum([int(m["voiceParticipantCount"]) for m in meetings])
n_video = sum([int(m["videoCount"]) for m in meetings])
n_moderator = sum([int(m["moderatorCount"]) for m in meetings])
print("MEETINGS on [{}]:".format(endpoint.name))
print(" ├─── {:>4} running".format(n_running))
print(" └─── {:>4} recording".format(n_recording))
print()
print("PARTICIPANTS across all {} rooms".format(n_running))
print(" └─┬─ {:>4} total".format(n_participants))
print(" ├─ {:>4} listening only".format(n_listeners))
print(" ├─ {:>4} mic on".format(n_voice))
print(" ├─ {:>4} video on".format(n_video))
print(" └─ {:>4} moderators".format(n_moderator))
print()
print_leaderboard(meetings, "participantCount")
print()
print_leaderboard(meetings, "videoCount")
print()
print_leaderboard(meetings, "voiceParticipantCount")
print()
print_duration_leaderboard(meetings)
def init_config() -> Optional[Config]:
"""
Read the config either from the servers bigbluebutton.properties-file or from
the user config path. Display a message if neither of these files exist.
......@@ -272,31 +210,28 @@ def init_variables() -> Optional[Tuple[Secret, Url]]:
# Check if we are on the server and try to read that properties file first
if os.path.isfile(SERVER_PROPERTIES_FILE):
with open(SERVER_PROPERTIES_FILE, "r") as f:
lines = [l for l in f.readlines()]
secret = Secret([l for l in lines if l.startswith("securitySalt=")][0].replace("securitySalt=", "")).strip()
bbb_url = Url([l for l in lines if l.startswith("bigbluebutton.web.serverURL=")][0].replace("bigbluebutton.web.serverURL=", "")).strip()
bbb_url = "{}/bigbluebutton".format(bbb_url.rstrip('/'))
return (secret, bbb_url)
return Config().from_server()
elif os.path.isfile(user_config_path):
with open(user_config_path, "r") as f:
lines = [l for l in f.readlines()]
secret = Secret([l for l in lines if l.startswith("securitySalt=")][0].replace("securitySalt=", "")).strip()
bbb_url = Url([l for l in lines if l.startswith("bigbluebutton.web.serverURL=")][0].replace("bigbluebutton.web.serverURL=", "")).strip()
bbb_url = "{}/bigbluebutton".format(bbb_url.rstrip('/'))
return (secret, bbb_url)
return Config().from_config(user_config_path)
else:
print("ERROR: There was no config file found. Make sure it exists and is readable:")
print("[0] {}".format(SERVER_PROPERTIES_FILE))
print("[1] {}".format(user_config_path))
print()
print("For now the file just needs to contain two lines:")
print("For now the file just needs to contain three lines:")
print("[myservername]")
print("securitySalt=YOURSUPERSECRETSECRET")
print("bigbluebutton.web.serverURL=https://bbb.example.com/")
print()
print("(You can define multiple server-blocks however)")
exit()
def main():
secret, bbb_url = init_variables()
print_overview(secret, bbb_url)
\ No newline at end of file
Config = init_config()
print_overview(Config)
if __name__ == "__main__":
main()
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import configparser
from typing import NewType, Optional, Tuple, Iterable, List
# Default path
SERVER_PROPERTIES_FILE = "/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties"
# Type definitions
Secret = NewType('Secret', str)
Url = NewType('Url', str)
class Config():
"""
Holds the Server Configurations for multiple endpoints
"""
def __init__(self):
self.endpoints = []
def from_server(self, path: str=SERVER_PROPERTIES_FILE) -> 'Config':
"""
If bbbmon is executed on the server, it uses this method to extract the
Url (bigbluebutton.web.serverURL) and the Secret (securitySalt) from the
server. Additionally this method is used as a legacy fallback for user
configuration files that are not a valid ini with [section headers]
"""
with open(path, "r") as f:
lines = [l for l in f.readlines()]
secret = Secret([l for l in lines if l.startswith("securitySalt=")][0].replace("securitySalt=", "")).strip()
bbb_url = Url([l for l in lines if l.startswith("bigbluebutton.web.serverURL=")][0].replace("bigbluebutton.web.serverURL=", "")).strip()
bbb_url = "{}/bigbluebutton".format(bbb_url.rstrip('/'))
endpoint = Endpoint(url=bbb_url, secret=secret)
self.endpoints.append(endpoint)
return self
def from_config(self, path: str) -> 'Config':
"""
Read config from a given path. If the file has no section headers, try
to use the .from_server(path) method instead
"""
config = configparser.ConfigParser()
try:
config.read(path, encoding='utf-8')
for section in config.sections():
bbb_url = Url(config[section]["bigbluebutton.web.serverURL"])
bbb_url = "{}/bigbluebutton".format(bbb_url.rstrip('/'))
secret = Secret(config[section]["securitySalt"]).strip()
endpoint = Endpoint(url=bbb_url, secret=secret, name=section)
self.endpoints.append(endpoint)
return self
# Fallback for config files without sections
except configparser.MissingSectionHeaderError:
self = self.from_server(path)
return self
def __len__(self):
"""
The length of a Config is represented by the number of endpoints
"""
return len(self.endpoints)
def __str__(self):
"""
Allow a Config to be represented by a string quickly
"""
l = ["Config"]
for e in ["Endpoint[{}]: {}, SECRET OMITTED".format(e.name, e.url) for e in self.endpoints]:
l.append(e)
return "\n".join(l)
class Endpoint():
"""
Objects of this class represent a single endpoint which runs a bigbluebutton
instance. The relevant fields are the url and the secret, the name is either
extracted from the section header of the user configuration file, or – as a
fallback – from the URL
"""
def __init__(self, url: Url, secret: Secret, name: str=None):
self.url = url
self.secret = secret
if name is None:
self.name = url.lower()\
.lstrip("http://")\
.lstrip("https://")\
.rstrip("/bigbluebutton")
else:
self.name = name
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class XmlListConfig(list):
"""
Helper class to convert XML to python dicts
"""
def __init__(self, aList):
for element in aList:
if element:
# treat like dict
if len(element) == 1 or element[0].tag != element[1].tag:
self.append(XmlDictConfig(element))
# treat like list
elif element[0].tag == element[1].tag:
self.append(XmlListConfig(element))
elif element.text:
text = element.text.strip()
if text:
self.append(text)
class XmlDictConfig(dict):
'''
Example usage:
>>> tree = ElementTree.parse('your_file.xml')
>>> root = tree.getroot()
>>> xmldict = XmlDictConfig(root)
Or, if you want to use an XML string:
>>> root = ElementTree.XML(xml_string)
>>> xmldict = XmlDictConfig(root)
And then use xmldict for what it is... a dict.
'''
def __init__(self, parent_element):
if parent_element.items():
self.update(dict(parent_element.items()))
for element in parent_element:
if element:
# treat like dict - we assume that if the first two tags
# in a series are different, then they are all different.
if len(element) == 1 or element[0].tag != element[1].tag:
aDict = XmlDictConfig(element)
# treat like list - we assume that if the first two tags
# in a series are the same, then the rest are the same.
else:
# here, we put the list in dictionary; the key is the
# tag name the list elements all share in common, and
# the value is the list itself
aDict = {element[0].tag: XmlListConfig(element)}
# if the tag has attributes, add those to the dict
if element.items():
aDict.update(dict(element.items()))
self.update({element.tag: aDict})
# this assumes that if you've got an attribute in a tag,
# you won't be having any text. This may or may not be a
# good idea -- time will tell. It works for the way we are
# currently doing XML configuration files...
elif element.items():
self.update({element.tag: dict(element.items())})
# finally, if there are no child tags and no attributes, extract
# the text
else:
self.update({element.tag: element.text})
\ No newline at end of file
[tool.poetry]
name = "bbbmon"
version = "0.1.5"
version = "0.1.6"
description = "A small CLI utility to monitor bbb usage"
authors = ["David Huss <david.huss@hfbk-hamburg.de>"]
maintainers = ["David Huss <david.huss@hfbk-hamburg.de>"]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment