Skip to content

Commit

Permalink
Add/Refactor instances related endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
TheophileDiot committed Aug 15, 2024
1 parent 47aa48f commit cf822d9
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 48 deletions.
27 changes: 27 additions & 0 deletions src/common/db/Database.py
Original file line number Diff line number Diff line change
Expand Up @@ -3097,6 +3097,33 @@ def add_instance(self, hostname: str, port: int, server_name: str, method: str,

return ""

def delete_instance(self, hostname: str, changed: Optional[bool] = True) -> str:
"""Delete instance."""
with self._db_session() as session:
if self.readonly:
return "The database is read-only, the changes will not be saved"

db_instance = session.query(Instances).filter_by(hostname=hostname).first()

if db_instance is None:
return f"Instance {hostname} does not exist, will not be deleted."

session.delete(db_instance)

if changed:
with suppress(ProgrammingError, OperationalError):
metadata = session.query(Metadata).get(1)
if metadata is not None:
metadata.instances_changed = True
metadata.last_instances_change = datetime.now()

try:
session.commit()
except BaseException as e:
return f"An error occurred while deleting the instance {hostname}.\n{e}"

return ""

def update_instances(self, instances: List[Dict[str, Any]], method: str, changed: Optional[bool] = True) -> str:
"""Update instances."""
to_put = []
Expand Down
188 changes: 141 additions & 47 deletions src/ui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from string import ascii_letters, digits
from sys import path as sys_path, modules as sys_modules
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union
from typing import Any, Dict, Literal, Optional, Tuple, Union
from uuid import uuid4


Expand Down Expand Up @@ -47,21 +47,20 @@
from src.totp import Totp
from src.ui_data import UIData

# TODO: rename to bans
from builder.bans2 import bans_builder # type: ignore

# TODO: rename to reports
from builder.reports2 import reports_builder # type: ignore
from builder.advanced_mode import advanced_mode_builder # type: ignore
from builder.bans import bans_builder # type: ignore

# from builder.configs import configs_builder # type: ignore
from builder.easy_mode import easy_mode_builder # type: ignore
from builder.global_config import global_config_builder # type: ignore
from builder.home import home_builder # type: ignore
from builder.instances import instances_builder # type: ignore
from builder.global_config import global_config_builder # type: ignore
from builder.jobs import jobs_builder # type: ignore
from builder.services import services_builder # type: ignore
from builder.raw_mode import raw_mode_builder # type: ignore
from builder.advanced_mode import advanced_mode_builder # type: ignore
from builder.easy_mode import easy_mode_builder # type: ignore
from builder.logs import logs_builder # type: ignore
from builder.raw_mode import raw_mode_builder # type: ignore
from builder.reports import reports_builder # type: ignore
from builder.services import services_builder # type: ignore


from common_utils import get_version # type: ignore
from logger import setup_logger # type: ignore
Expand Down Expand Up @@ -245,6 +244,12 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
operation = instance.restart()
else:
operation = "The instance does not exist."
elif operation == "ping":
instance = Instance.from_hostname(args[0], app.db)
if instance:
operation = instance.ping()[0]
else:
operation = "The instance does not exist."
elif not error:
operation = "The scheduler will be in charge of applying the changes."

Expand Down Expand Up @@ -364,6 +369,9 @@ def run_action(plugin: str, function_name: str = "", *, tmp_dir: Optional[Path]
def verify_data_in_form(
data: Optional[Dict[str, Union[Tuple, Any]]] = None, err_message: str = "", redirect_url: str = "", next: bool = False
) -> Union[bool, Response]:
app.logger.debug(f"Verifying data in form: {data}")
app.logger.debug(f"Request form: {request.form}")

# Loop on each key in data
for key, values in (data or {}).items():
if key not in request.form:
Expand Down Expand Up @@ -987,49 +995,135 @@ def update_global_config(threaded: bool = False):
)


@app.route("/instances", methods=["GET", "POST"])
### * INSTANCES


@app.route("/instances", methods=["GET"])
@login_required
def instances():
# Manage instances
if request.method == "POST":

verify_data_in_form(data={"INSTANCE_ID": None}, err_message="Missing instance id parameter on /instances.", redirect_url="instances", next=True)
verify_data_in_form(
data={
"operation": (
"reload",
"start",
"stop",
"restart",
)
},
err_message="Missing operation parameter on /instances.",
redirect_url="instances",
next=True,
instances = []
instances_types = set()
instances_methods = set()
instances_healths = set()

for instance in app.bw_instances_utils.get_instances():
instances.append(
{
"hostname": instance.hostname,
"name": instance.name,
"method": instance.method,
"health": instance.status,
"type": instance.type,
"creation_date": instance.creation_date.strftime("%Y-%m-%d %H:%M:%S"),
"last_seen": instance.last_seen.strftime("%Y-%m-%d %H:%M:%S"),
}
)

app.data["RELOADING"] = True
app.data["LAST_RELOAD"] = time()
Thread(
target=manage_bunkerweb,
name="Reloading instances",
args=("instances", request.form["INSTANCE_ID"]),
kwargs={"operation": request.form["operation"], "threaded": True},
).start()
instances_types.add(instance.type)
instances_methods.add(instance.method)
instances_healths.add(instance.status)

return redirect(
url_for(
"loading",
next=url_for("instances"),
message=(f"{request.form['operation'].title()}ing" if request.form["operation"] != "stop" else "Stopping") + " instance",
)
)
builder = instances_builder(instances, list(instances_types), list(instances_methods), list(instances_healths))
return render_template("instances.html", title="Instances", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))

# Display instances
instances = app.bw_instances_utils.get_instances()

builder = instances_builder(instances)
return render_template("instances.html", title="Instances", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
@app.route("/instances/new", methods=["PUT"])
@login_required
def instances_new():
verify_data_in_form(
data={"csrf_token": None},
err_message="Missing csrf_token parameter on /instances/new.",
redirect_url="instances",
next=True,
)
verify_data_in_form(
data={"instance_hostname": None},
err_message="Missing instance hostname parameter on /instances/new.",
redirect_url="instances",
next=True,
)
verify_data_in_form(
data={"instance_name": None},
err_message="Missing instance name parameter on /instances/new.",
redirect_url="instances",
next=True,
)

db_config = app.bw_config.get_config(global_only=True, methods=False, filtered_settings=("API_HTTP_PORT", "API_SERVER_NAME"))

instance = {
"hostname": request.form["instance_hostname"].replace("http://", "").replace("https://", ""),
"name": request.form["instance_name"],
"port": db_config["API_HTTP_PORT"],
"server_name": db_config["API_SERVER_NAME"],
"method": "ui",
}

for db_instance in app.bw_instances_utils.get_instances():
if db_instance.hostname == instance["hostname"]:
return handle_error(f"The hostname {instance['hostname']} is already in use.", "instances", True)

ret = app.db.add_instance(**instance)
if ret:
return handle_error(f"Couldn't create the instance in the database: {ret}", "instances", True)

return redirect(url_for("loading", next=url_for("instances"), message=f"Creating new instance {instance['hostname']}"))


@app.route("/instances/<string:instance_hostname>", methods=["DELETE"])
@login_required
def instances_delete(instance_hostname: str):
verify_data_in_form(
data={"csrf_token": None},
err_message="Missing csrf_token parameter on /instances/delete.",
redirect_url="instances",
next=True,
)

delete_instance = None
for instance in app.bw_instances_utils.get_instances():
if instance.hostname == instance_hostname:
delete_instance = instance
break

if not delete_instance:
return handle_error(f"Instance {instance_hostname} not found.", "instances", True)
if delete_instance.method != "ui":
return handle_error(f"Instance {instance_hostname} is not a UI instance.", "instances", True)

ret = app.db.delete_instance(instance_hostname)
if ret:
return handle_error(f"Couldn't delete the instance in the database: {ret}", "instances", True)

return redirect(url_for("loading", next=url_for("instances"), message=f"Deleting instance {instance_hostname}"))


@app.route("/instances/<string:action>", methods=["POST"])
@login_required
def instances_action(action: Literal["ping", "reload", "start", "stop", "restart"]):
verify_data_in_form(
data={"instance_hostname": None, "csrf_token": None},
err_message="Missing instance hostname parameter on /instances/reload.",
redirect_url="instances",
next=True,
)

app.data["RELOADING"] = True
app.data["LAST_RELOAD"] = time()
Thread(
target=manage_bunkerweb,
name=f"Reloading instance {request.form['instance_hostname']}",
args=("instances", request.form["instance_hostname"]),
kwargs={"operation": action, "threaded": True},
).start()

return redirect(
url_for(
"loading",
next=url_for("instances"),
message=(f"{action.title()}ing" if action != "stop" else "Stopping") + " instance",
)
)


def get_service_data(page_name: str):
Expand Down
11 changes: 10 additions & 1 deletion src/ui/src/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,16 @@ def metrics(self, plugin_id) -> Tuple[bool, dict[str, Any]]:
def metrics_redis(self) -> Tuple[bool, dict[str, Any]]:
return self.apiCaller.send_to_apis("GET", "/redis/stats", response=True)

def ping(self, plugin_id) -> Tuple[bool, dict[str, Any]]:
def ping(self, plugin_id: Optional[str] = None) -> Tuple[bool, dict[str, Any]]:
if not plugin_id:
try:
result = self.apiCaller.send_to_apis("GET", "/ping")[0]
except BaseException as e:
return f"Can't ping {self.hostname}: {e}", {}

if result:
return f"Instance {self.hostname} is up", {}
return f"Can't ping {self.hostname}", {}
return self.apiCaller.send_to_apis("POST", f"/{plugin_id}/ping", response=True)

def data(self, plugin_endpoint) -> Tuple[bool, dict[str, Any]]:
Expand Down

0 comments on commit cf822d9

Please sign in to comment.