Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ def ping_server(password: str):
@click.option("--proxy-ip", required=False, type=str, default=None)
@click.option("--sentry-dsn", required=False, type=str)
@click.option("--press-url", required=False, type=str)
def config(name, user, workers, proxy_ip=None, sentry_dsn=None, press_url=None):
@click.option("--is-nfs-server", required=False, is_flag=True, type=bool, default=False)
def config(
name, user, workers, proxy_ip=None, sentry_dsn=None, press_url=None, is_nfs_server: bool | None = None
) -> None:
config = {
"benches_directory": f"/home/{user}/benches",
"benches_directory": f"/home/{user}/{'benches' if not is_nfs_server else 'nfs'}",
"name": name,
"tls_directory": f"/home/{user}/agent/tls",
"nginx_directory": f"/home/{user}/agent/nginx",
Expand All @@ -87,6 +90,8 @@ def config(name, user, workers, proxy_ip=None, sentry_dsn=None, press_url=None):
"web_port": 25052,
"press_url": "https://frappecloud.com",
}
if is_nfs_server:
config["exports_file"] = f"/home/{user}/exports"
if press_url:
config["press_url"] = press_url
if proxy_ip:
Expand Down
14 changes: 6 additions & 8 deletions agent/nfs_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
class NFSHandler:
def __init__(self, server: "Server"):
self.server = server
self.exports_file = "/home/frappe/exports"
self.shared_directory = "/home/frappe/nfs"
self.options = "rw,sync,no_subtree_check"

def reload_exports(self):
Expand All @@ -26,14 +24,14 @@ def add_to_acl(
"""
Updates the exports file on the nfs host server
"""
server_shared_directory = os.path.join(self.shared_directory, shared_directory)
server_shared_directory = os.path.join(self.server.benches_directory, shared_directory)

os.makedirs(server_shared_directory)
self.server.execute(f"chown -R frappe:frappe {server_shared_directory}")

lock = filelock.SoftFileLock(self.exports_file + ".lock")
lock = filelock.SoftFileLock(self.server.config["exports_file"] + ".lock")

with lock.acquire(timeout=10), open(self.exports_file, "a+") as f:
with lock.acquire(timeout=10), open(self.server.config["exports_file"], "a+") as f:
f.write(f"{server_shared_directory} {primary_server_private_ip}({self.options})\n")
f.write(f"{server_shared_directory} {secondary_server_private_ip}({self.options})\n")

Expand All @@ -43,14 +41,14 @@ def remove_from_acl(
self, shared_directory: str, primary_server_private_ip: str, secondary_server_private_ip: str
):
"""Unsubscrible a given private IP from a give file system"""
server_shared_directory = os.path.join(self.shared_directory, shared_directory)
server_shared_directory = os.path.join(self.server.benches_directory, shared_directory)
remove_lines = [
f"{server_shared_directory} {primary_server_private_ip}({self.options})",
f"{server_shared_directory} {secondary_server_private_ip}({self.options})",
]
for line in remove_lines:
lock = filelock.SoftFileLock(self.exports_file + ".lock")
lock = filelock.SoftFileLock(self.server.config["exports_file"] + ".lock")
with lock.acquire(timeout=10):
self.server.execute(f"sed -i '\\|{line}|d' {self.exports_file}")
self.server.execute(f"sed -i '\\|{line}|d' {self.server.config['exports_file']}")

self.reload_exports()
23 changes: 18 additions & 5 deletions agent/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import time
from contextlib import suppress
from datetime import datetime
from typing import Any
from urllib.parse import urlparse

from jinja2 import Environment, PackageLoader
Expand All @@ -33,10 +34,11 @@


class Server(Base):
def __init__(self, directory=None):
def __init__(self, directory=None, primary_server: str | None = None):
super().__init__()

self.directory = directory or os.getcwd()
self.primary_server = primary_server
self.set_config_attributes()

self.job = None
Expand All @@ -45,8 +47,12 @@ def __init__(self, directory=None):
def set_config_attributes(self):
"""Setting config attributes here to enable easy config reloads"""
self.config_file = os.path.join(self.directory, "config.json")
self.name = self.config["name"]
self.benches_directory = self.config["benches_directory"]
self.name = self.primary_server or self.config["name"]
self.benches_directory = (
os.path.join(self.config["benches_directory"], self.primary_server)
if self.primary_server
else self.config["benches_directory"]
)
self.archived_directory = os.path.join(os.path.dirname(self.benches_directory), "archived")
self.nginx_directory = self.config["nginx_directory"]
self.hosts_directory = os.path.join(self.nginx_directory, "hosts")
Expand Down Expand Up @@ -75,7 +81,6 @@ def establish_connection_with_registry(self, max_retries: int, registry: dict[st

time.sleep(60)

@step("Initialize Bench")
def bench_init(self, name, config, registry: dict[str, str]):
self.establish_connection_with_registry(max_retries=3, registry=registry)
bench_directory = os.path.join(self.benches_directory, name)
Expand Down Expand Up @@ -116,6 +121,15 @@ def dump(self):
"config": self.config,
}

@job("Setup Bench", priority="low")
def setup_bench(self, name: str, bench_config: dict[str, Any], registry: dict[str, Any]):
self._setup_bench(name, bench_config, registry)

@step("Setup Bench")
def _setup_bench(self, name: str, bench_config: dict[str, Any], registry: dict[str, Any]):
self.docker_login(registry)
self.bench_init(name, bench_config, registry)

@job("New Bench", priority="low")
def new_bench(
self,
Expand All @@ -126,7 +140,6 @@ def new_bench(
mounts=None,
):
self.docker_login(registry)
self.bench_init(name, bench_config, registry)
bench = Bench(name, self, mounts=mounts)
bench.update_config(common_site_config, bench_config)
if bench.bench_config.get("single_container"):
Expand Down
52 changes: 52 additions & 0 deletions agent/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from shlex import quote
from typing import TYPE_CHECKING

import pytz
import requests

from agent.base import AgentException, Base
Expand Down Expand Up @@ -457,6 +458,57 @@ def update_saas_plan(self, plan):
def update_plan(self, plan):
self.bench_execute(f"update-site-plan {plan}")

def _generate_private_and_public_tar(self, current_date: str, backup_dir: str) -> None:
"""Generate private and public files tarfile at the backup location
ref: https://github.com/frappe/frappe/blob/7b8c9132f88b5b45a5ce5f9b404db93b36eb2d0b/frappe/utils/backups.py#L349
"""
for folder in ("public", "private"):
files_path = os.path.join(self.directory, folder, "files")
backup_path = os.path.join(
backup_dir, f"{current_date}-{self.name}-{'private-' if folder == 'private' else ''}files.tar"
)

cmd_string = f"tar -cf {backup_path} {files_path}"
self.execute(cmd_string)

def _generate_site_config_backup(self, current_date: str, backup_dir: str) -> None:
"""Dump the current site config"""
file_path = os.path.join(backup_dir, f"{current_date}-{self.name}-site_config_backup.json")

with open(file_path, "w") as f:
json.dump(self.config, f, indent=2)

def _take_database_dump(self, current_date: str, backup_dir: str) -> None:
"""Take mariadb dump of the database
ref: https://github.com/frappe/frappe/blob/7b8c9132f88b5b45a5ce5f9b404db93b36eb2d0b/frappe/utils/backups.py#L382
"""
gzip = self.execute("which gzip")["output"]
mysqldump = self.execute("which mysqldump")["output"] # This is symlinked in the image

backup_path = os.path.join(backup_dir, f"{current_date}-{self.name}-database.sql.gz")
cmd_string = (
f"set -o pipefail; {mysqldump} --user={self.user} "
f"--host={self.host} --port {self.bench.common_site_config['db_port']} "
f"--password={self.password} --single-transaction --quick --lock-tables=false "
f"{self.user} | {gzip} >> {backup_path}"
)

self.execute(cmd_string, executable="/bin/bash")

def _backup(self, with_files: bool) -> None:
current_date = datetime.now(pytz.UTC)
current_date = current_date.strftime("%Y%m%d_%H%M%S")
backup_dir = os.path.join(self.directory, "private", "backups")

if not os.path.exists(backup_dir):
os.makedirs(backup_dir, exist_ok=True)

if with_files:
self._generate_private_and_public_tar(current_date, backup_dir)
self._generate_site_config_backup(current_date, backup_dir)

self._take_database_dump(current_date, backup_dir)

@step("Backup Site")
def backup(self, with_files=False):
with_files = "--with-files" if with_files else ""
Expand Down
12 changes: 11 additions & 1 deletion agent/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,17 @@ def get_site_sid(bench, site):
return {"sid": Server().benches[bench].sites[site].sid(user=user)}


@application.route("/benches", methods=["POST"])
@application.route("/benches/setup", methods=["POST"])
def setup_bench():
data = request.json
primary_server = data.pop("primary_server", None)
job = Server(primary_server=primary_server).setup_bench(
**data
) # This could be running on the nfs server as well
return {"job": job}


@application.route("/benches/new", methods=["POST"])
def new_bench():
data = request.json
job = Server().new_bench(**data)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ sql_metadata==2.15.0
py-spy==0.4.0
mariadb-binlog-indexer==0.0.8
psutil==7.0.0
pytz==2025.2