diff --git a/press/api/site.py b/press/api/site.py
index 79c005c5617..940989931cb 100644
--- a/press/api/site.py
+++ b/press/api/site.py
@@ -173,13 +173,14 @@ def _new(site, server: str | None = None, ignore_plan_validation: bool = False):
.where(Bench.status == "Active")
.where(Bench.group == site["group"])
.orderby(Case().when(Bench.cluster == cluster, 1).else_(0), order=frappe.qb.desc)
- .orderby(Server.use_for_new_sites, order=frappe.qb.desc)
.orderby(Bench.creation, order=frappe.qb.desc)
.limit(1)
)
if server:
bench_query = bench_query.where(Server.name == server)
+ else:
+ bench_query.orderby(Server.use_for_new_sites, order=frappe.qb.desc)
bench = bench_query.run(as_dict=True).pop()
diff --git a/press/press/doctype/server/server.json b/press/press/doctype/server/server.json
index 6ca58700b9b..8783747db3b 100644
--- a/press/press/doctype/server/server.json
+++ b/press/press/doctype/server/server.json
@@ -1,808 +1,806 @@
{
- "actions": [],
- "creation": "2019-12-09 12:34:13.844800",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "title",
- "status",
- "hostname",
- "hostname_abbreviation",
- "domain",
- "self_hosted_server_domain",
- "tls_certificate_renewal_failed",
- "is_unified_server",
- "column_break_4",
- "cluster",
- "provider",
- "virtual_machine",
- "ignore_incidents_till",
- "section_break_mequ",
- "is_server_setup",
- "is_server_prepared",
- "is_server_renamed",
- "is_provisioning_press_job_completed",
- "is_self_hosted",
- "keep_files_on_server_in_offsite_backup",
- "public",
- "column_break_laiq",
- "use_agent_job_callbacks",
- "is_pyspy_setup",
- "halt_agent_jobs",
- "stop_deployments",
- "is_for_recovery",
- "is_monitoring_disabled",
- "enable_on_prem_failover_support",
- "billing_section",
- "team",
- "plan",
- "column_break_11",
- "auto_increase_storage",
- "auto_add_storage_min",
- "auto_add_storage_max",
- "networking_section",
- "ip",
- "is_static_ip",
- "ipv6",
- "nat_server",
- "column_break_3",
- "private_ip",
- "private_mac_address",
- "private_vlan_id",
- "agent_section",
- "agent_password",
- "column_break_pdbx",
- "disable_agent_job_auto_retry",
- "reverse_proxy_section",
- "proxy_server",
- "column_break_12",
- "is_upstream_setup",
- "database_section",
- "database_server",
- "self_hosted_mariadb_server",
- "is_managed_database",
- "enable_logical_replication_during_site_update",
- "column_break_jdiy",
- "self_hosted_mariadb_root_password",
- "managed_database_service",
- "replication",
- "is_primary",
- "is_replication_setup",
- "column_break_24",
- "primary",
- "auto_scale_section",
- "secondary_server",
- "is_secondary",
- "benches_on_shared_volume",
- "scaled_up",
- "column_break_ywnx",
- "auto_scale_trigger",
- "ssh_section",
- "ssh_user",
- "ssh_port",
- "frappe_user_password",
- "frappe_public_key",
- "column_break_20",
- "bastion_server",
- "root_public_key",
- "section_break_22",
- "use_for_new_benches",
- "use_for_new_sites",
- "staging",
- "use_for_build",
- "platform",
- "column_break_ktkv",
- "new_worker_allocation",
- "set_bench_memory_limits",
- "ram",
- "backups_section",
- "skip_scheduled_backups",
- "standalone_section",
- "is_standalone",
- "column_break_edyf",
- "is_standalone_setup",
- "tags_section",
- "tags",
- "mounts_section",
- "has_data_volume",
- "mounts",
- "notifications_section",
- "communication_infos"
- ],
- "fields": [
- {
- "fetch_from": "virtual_machine.public_ip_address",
- "fieldname": "ip",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "IP"
- },
- {
- "fieldname": "proxy_server",
- "fieldtype": "Link",
- "label": "Proxy Server",
- "options": "Proxy Server"
- },
- {
- "fetch_from": "virtual_machine.private_ip_address",
- "fieldname": "private_ip",
- "fieldtype": "Data",
- "label": "Private IP"
- },
- {
- "fieldname": "agent_password",
- "fieldtype": "Password",
- "label": "Agent Password",
- "read_only": 1
- },
- {
- "collapsible": 1,
- "fieldname": "agent_section",
- "fieldtype": "Section Break",
- "label": "Agent"
- },
- {
- "default": "0",
- "fieldname": "is_server_setup",
- "fieldtype": "Check",
- "label": "Is Server Setup",
- "read_only": 1
- },
- {
- "default": "0",
- "fieldname": "is_upstream_setup",
- "fieldtype": "Check",
- "label": "Upstream Setup",
- "read_only": 1
- },
- {
- "default": "Pending",
- "fieldname": "status",
- "fieldtype": "Select",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Status",
- "options": "Pending\nInstalling\nActive\nBroken\nArchived",
- "read_only": 1,
- "reqd": 1
- },
- {
- "fieldname": "column_break_3",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "column_break_12",
- "fieldtype": "Column Break"
- },
- {
- "collapsible": 1,
- "fieldname": "reverse_proxy_section",
- "fieldtype": "Section Break",
- "label": "Reverse Proxy"
- },
- {
- "fieldname": "database_section",
- "fieldtype": "Section Break",
- "label": "Database"
- },
- {
- "depends_on": "eval:!doc.is_managed_database",
- "fieldname": "database_server",
- "fieldtype": "Link",
- "label": "Database Server",
- "options": "Database Server"
- },
- {
- "collapsible": 1,
- "fieldname": "ssh_section",
- "fieldtype": "Section Break",
- "label": "SSH"
- },
- {
- "fieldname": "root_public_key",
- "fieldtype": "Code",
- "label": "Root Public Key",
- "read_only": 1
- },
- {
- "fieldname": "frappe_public_key",
- "fieldtype": "Code",
- "label": "Frappe Public Key",
- "read_only": 1
- },
- {
- "fieldname": "column_break_20",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "section_break_22",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "fieldname": "use_for_new_benches",
- "fieldtype": "Check",
- "label": "Use For New Benches",
- "read_only": 1
- },
- {
- "fieldname": "hostname",
- "fieldtype": "Data",
- "label": "Hostname",
- "reqd": 1,
- "set_only_once": 1
- },
- {
- "fieldname": "domain",
- "fieldtype": "Link",
- "hidden": 1,
- "label": "Domain",
- "options": "Root Domain",
- "set_only_once": 1
- },
- {
- "default": "0",
- "fieldname": "use_for_new_sites",
- "fieldtype": "Check",
- "label": "Use For New Sites",
- "read_only": 1
- },
- {
- "fieldname": "cluster",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Cluster",
- "options": "Cluster",
- "set_only_once": 1
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "collapsible": 1,
- "fieldname": "networking_section",
- "fieldtype": "Section Break",
- "label": "Networking"
- },
- {
- "depends_on": "eval: doc.provider === \"Scaleway\"",
- "fieldname": "private_mac_address",
- "fieldtype": "Data",
- "label": "Private Mac Address",
- "mandatory_depends_on": "eval: doc.provider === \"Scaleway\"",
- "set_only_once": 1
- },
- {
- "depends_on": "eval: doc.provider === \"Scaleway\"",
- "fieldname": "private_vlan_id",
- "fieldtype": "Data",
- "label": "Private VLAN ID",
- "mandatory_depends_on": "eval: doc.provider === \"Scaleway\"",
- "set_only_once": 1
- },
- {
- "default": "Generic",
- "fieldname": "provider",
- "fieldtype": "Select",
- "label": "Provider",
- "options": "Generic\nScaleway\nAWS EC2\nOCI\nHetzner\nVodacom\nDigitalOcean\nFrappe Compute",
- "set_only_once": 1
- },
- {
- "fieldname": "frappe_user_password",
- "fieldtype": "Password",
- "label": "Frappe User Password",
- "set_only_once": 1
- },
- {
- "collapsible": 1,
- "fieldname": "replication",
- "fieldtype": "Section Break",
- "label": "Replication"
- },
- {
- "default": "1",
- "fieldname": "is_primary",
- "fieldtype": "Check",
- "label": "Is Primary"
- },
- {
- "fieldname": "column_break_24",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval: !doc.is_primary",
- "fieldname": "primary",
- "fieldtype": "Link",
- "label": "Primary",
- "mandatory_depends_on": "eval: !doc.is_primary",
- "options": "Server"
- },
- {
- "default": "0",
- "depends_on": "eval: !doc.is_primary",
- "fieldname": "is_replication_setup",
- "fieldtype": "Check",
- "label": "Is Replication Setup",
- "read_only": 1
- },
- {
- "default": "0",
- "fieldname": "staging",
- "fieldtype": "Check",
- "label": "Staging"
- },
- {
- "depends_on": "eval:[\"AWS EC2\", \"OCI\", \"Hetzner\", \"DigitalOcean\", \"Frappe Compute\"].includes(doc.provider)",
- "fieldname": "virtual_machine",
- "fieldtype": "Link",
- "label": "Virtual Machine",
- "mandatory_depends_on": "eval:[\"AWS EC2\", \"OCI\"].includes(doc.provider)",
- "options": "Virtual Machine"
- },
- {
- "default": "1",
- "fieldname": "new_worker_allocation",
- "fieldtype": "Check",
- "label": "New Worker Allocation"
- },
- {
- "fieldname": "ram",
- "fieldtype": "Float",
- "label": "RAM"
- },
- {
- "fieldname": "team",
- "fieldtype": "Link",
- "label": "Team",
- "options": "Team"
- },
- {
- "fieldname": "billing_section",
- "fieldtype": "Section Break",
- "label": "Billing"
- },
- {
- "fieldname": "column_break_11",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "plan",
- "fieldtype": "Link",
- "label": "Plan",
- "options": "Server Plan"
- },
- {
- "default": "0",
- "fieldname": "is_server_prepared",
- "fieldtype": "Check",
- "label": "Is Server Prepared",
- "read_only": 1
- },
- {
- "default": "0",
- "fieldname": "is_server_renamed",
- "fieldtype": "Check",
- "label": "Is Server Renamed",
- "read_only": 1
- },
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "label": "Title"
- },
- {
- "default": "0",
- "fieldname": "is_self_hosted",
- "fieldtype": "Check",
- "label": "Is Self Hosted"
- },
- {
- "default": "root",
- "fieldname": "ssh_user",
- "fieldtype": "Data",
- "label": "SSH User"
- },
- {
- "depends_on": "eval:doc.is_self_hosted==true && !doc.is_managed_database",
- "fieldname": "self_hosted_mariadb_server",
- "fieldtype": "Data",
- "label": "Self Hosted MariaDB Server IP"
- },
- {
- "depends_on": "eval:doc.is_self_hosted==true",
- "fieldname": "column_break_jdiy",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:doc.is_self_hosted==true && !doc.is_managed_database",
- "fieldname": "self_hosted_mariadb_root_password",
- "fieldtype": "Password",
- "label": "Self Hosted MariaDB Root Password"
- },
- {
- "depends_on": "eval:doc.is_self_hosted",
- "fieldname": "self_hosted_server_domain",
- "fieldtype": "Data",
- "label": "Self Hosted Server Domain"
- },
- {
- "default": "22",
- "fieldname": "ssh_port",
- "fieldtype": "Int",
- "label": "SSH Port"
- },
- {
- "collapsible": 1,
- "fieldname": "standalone_section",
- "fieldtype": "Section Break",
- "label": "Standalone"
- },
- {
- "default": "0",
- "fieldname": "is_standalone",
- "fieldtype": "Check",
- "label": "Is Standalone"
- },
- {
- "fieldname": "column_break_edyf",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "is_standalone_setup",
- "fieldtype": "Check",
- "label": "Is Standalone Setup",
- "read_only": 1
- },
- {
- "fieldname": "tags_section",
- "fieldtype": "Section Break",
- "label": "Tags"
- },
- {
- "fieldname": "tags",
- "fieldtype": "Table",
- "label": "Tags",
- "options": "Resource Tag"
- },
- {
- "fieldname": "column_break_ktkv",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "set_bench_memory_limits",
- "fieldtype": "Check",
- "label": "Set Bench Memory Limits"
- },
- {
- "fieldname": "hostname_abbreviation",
- "fieldtype": "Data",
- "label": "Hostname Abbreviation"
- },
- {
- "collapsible": 1,
- "fieldname": "backups_section",
- "fieldtype": "Section Break",
- "label": "Backups"
- },
- {
- "default": "0",
- "fieldname": "skip_scheduled_backups",
- "fieldtype": "Check",
- "label": "Skip Scheduled Backups"
- },
- {
- "fieldname": "column_break_pdbx",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "disable_agent_job_auto_retry",
- "fieldtype": "Check",
- "label": "Disable Agent Job Auto Retry"
- },
- {
- "default": "0",
- "description": "If user opts DBaaS eg. RDS",
- "fieldname": "is_managed_database",
- "fieldtype": "Check",
- "label": "Is Managed Database"
- },
- {
- "fieldname": "managed_database_service",
- "fieldtype": "Link",
- "label": "Managed Database Service",
- "options": "Managed Database Service"
- },
- {
- "default": "0",
- "description": "Public release groups will be deployed here",
- "fieldname": "public",
- "fieldtype": "Check",
- "label": "Public"
- },
- {
- "default": "0",
- "description": "If checked, server will be used to run Docker builds.",
- "fieldname": "use_for_build",
- "fieldtype": "Check",
- "label": "Use For Build",
- "search_index": 1
- },
- {
- "default": "25",
- "description": "Minimum storage to add automatically each time",
- "fieldname": "auto_add_storage_min",
- "fieldtype": "Int",
- "label": "Auto Add Storage Min",
- "non_negative": 1
- },
- {
- "default": "250",
- "description": "Maximum storage to add automatically each time",
- "fieldname": "auto_add_storage_max",
- "fieldtype": "Int",
- "label": "Auto Add Storage Max",
- "non_negative": 1
- },
- {
- "fieldname": "mounts_section",
- "fieldtype": "Section Break",
- "label": "Mounts"
- },
- {
- "fieldname": "mounts",
- "fieldtype": "Table",
- "label": "Mounts",
- "options": "Server Mount"
- },
- {
- "default": "0",
- "fetch_from": "virtual_machine.has_data_volume",
- "fieldname": "has_data_volume",
- "fieldtype": "Check",
- "label": "Has Data Volume",
- "read_only": 1
- },
- {
- "fieldname": "ipv6",
- "fieldtype": "Data",
- "label": "IPv6"
- },
- {
- "default": "0",
- "fieldname": "use_agent_job_callbacks",
- "fieldtype": "Check",
- "label": "Use Agent Job Callbacks"
- },
- {
- "default": "0",
- "fieldname": "is_pyspy_setup",
- "fieldtype": "Check",
- "label": "Is PySpy Setup",
- "read_only": 1
- },
- {
- "fieldname": "section_break_mequ",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "column_break_laiq",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "description": "Stop polling and queuing agent jobs",
- "fieldname": "halt_agent_jobs",
- "fieldtype": "Check",
- "label": "Halt Agent Jobs"
- },
- {
- "default": "x86_64",
- "fieldname": "platform",
- "fieldtype": "Select",
- "label": "Platform",
- "options": "x86_64\narm64"
- },
- {
- "default": "1",
- "fieldname": "auto_increase_storage",
- "fieldtype": "Check",
- "label": "Auto Increase Storage"
- },
- {
- "default": "0",
- "description": "Stop all deployments on this server.",
- "fieldname": "stop_deployments",
- "fieldtype": "Check",
- "label": "Stop Deployments"
- },
- {
- "default": "0",
- "fieldname": "keep_files_on_server_in_offsite_backup",
- "fieldtype": "Check",
- "label": "Keep Backup Files Onsite"
- },
- {
- "default": "0",
- "fieldname": "is_for_recovery",
- "fieldtype": "Check",
- "label": "Is for Recovery"
- },
- {
- "default": "0",
- "fieldname": "enable_logical_replication_during_site_update",
- "fieldtype": "Check",
- "label": "Enable Logical Replication During Site Update"
- },
- {
- "fieldname": "ignore_incidents_till",
- "fieldtype": "Datetime",
- "label": "Ignore Incidents Till"
- },
- {
- "default": "0",
- "fieldname": "tls_certificate_renewal_failed",
- "fieldtype": "Check",
- "label": "TLS Certificate Renewal Failed",
- "read_only": 1
- },
- {
- "default": "0",
- "fieldname": "is_static_ip",
- "fieldtype": "Check",
- "label": "Is Static IP",
- "read_only": 1
- },
- {
- "fieldname": "notifications_section",
- "fieldtype": "Section Break",
- "label": "Notifications"
- },
- {
- "fieldname": "communication_infos",
- "fieldtype": "Table",
- "label": "Communication Infos",
- "options": "Communication Info"
- },
- {
- "fieldname": "bastion_server",
- "fieldtype": "Link",
- "label": "Bastion Server",
- "options": "Bastion Server"
- },
- {
- "description": "Used during horizontal scaling.",
- "fieldname": "secondary_server",
- "fieldtype": "Link",
- "label": "Secondary Server",
- "options": "Server",
- "read_only": 1
- },
- {
- "collapsible": 1,
- "fieldname": "auto_scale_section",
- "fieldtype": "Section Break",
- "label": "Auto Scale"
- },
- {
- "default": "0",
- "fieldname": "is_monitoring_disabled",
- "fieldtype": "Check",
- "label": "Is Monitoring Disabled",
- "search_index": 1
- },
- {
- "default": "0",
- "description": "Is this a secondary server",
- "fieldname": "is_secondary",
- "fieldtype": "Check",
- "label": "Is Secondary",
- "read_only": 1
- },
- {
- "default": "0",
- "description": "Are the benches running on a shared volume",
- "fieldname": "benches_on_shared_volume",
- "fieldtype": "Check",
- "label": "Benches on Shared Volume",
- "read_only": 1
- },
- {
- "fieldname": "auto_scale_trigger",
- "fieldtype": "Table",
- "label": "Auto Scale Trigger",
- "options": "Auto Scale Trigger"
- },
- {
- "default": "0",
- "description": "Check if the benches are running on the secondary server",
- "fieldname": "scaled_up",
- "fieldtype": "Check",
- "label": "Scaled Up",
- "read_only": 1
- },
- {
- "fieldname": "column_break_ywnx",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "description": "Checked if database and app are hosted on the server server",
- "fieldname": "is_unified_server",
- "fieldtype": "Check",
- "label": "Is Unified Server"
- },
- {
- "default": "0",
- "fieldname": "is_provisioning_press_job_completed",
- "fieldtype": "Check",
- "label": "Is Provisioning Job Completed"
- },
- {
- "fieldname": "nat_server",
- "fieldtype": "Link",
- "label": "NAT Server",
- "options": "NAT Server"
- },
- {
- "default": "0",
- "fieldname": "enable_on_prem_failover_support",
- "fieldtype": "Check",
- "label": "Enable On-Prem Failover Support"
- }
- ],
- "links": [
- {
- "link_doctype": "Auto Scale Record",
- "link_fieldname": "primary_server"
- },
- {
- "link_doctype": "On-Prem Failover",
- "link_fieldname": "app_server"
- }
- ],
- "modified": "2026-03-18 02:33:48.730257",
- "modified_by": "Administrator",
- "module": "Press",
- "name": "Server",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "read": 1,
- "role": "Press Admin",
- "write": 1
- },
- {
- "create": 1,
- "read": 1,
- "role": "Press Member",
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "rows_threshold_for_grid_search": 20,
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": [],
- "title_field": "title",
- "track_changes": 1
-}
\ No newline at end of file
+ "actions": [],
+ "creation": "2019-12-09 12:34:13.844800",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "status",
+ "hostname",
+ "hostname_abbreviation",
+ "domain",
+ "self_hosted_server_domain",
+ "tls_certificate_renewal_failed",
+ "is_unified_server",
+ "column_break_4",
+ "cluster",
+ "provider",
+ "virtual_machine",
+ "ignore_incidents_till",
+ "section_break_mequ",
+ "is_server_setup",
+ "is_server_prepared",
+ "is_server_renamed",
+ "is_provisioning_press_job_completed",
+ "is_self_hosted",
+ "keep_files_on_server_in_offsite_backup",
+ "public",
+ "column_break_laiq",
+ "use_agent_job_callbacks",
+ "is_pyspy_setup",
+ "halt_agent_jobs",
+ "stop_deployments",
+ "is_for_recovery",
+ "is_monitoring_disabled",
+ "enable_on_prem_failover_support",
+ "billing_section",
+ "team",
+ "plan",
+ "column_break_11",
+ "auto_increase_storage",
+ "auto_add_storage_min",
+ "auto_add_storage_max",
+ "networking_section",
+ "ip",
+ "is_static_ip",
+ "ipv6",
+ "nat_server",
+ "column_break_3",
+ "private_ip",
+ "private_mac_address",
+ "private_vlan_id",
+ "agent_section",
+ "agent_password",
+ "column_break_pdbx",
+ "disable_agent_job_auto_retry",
+ "reverse_proxy_section",
+ "proxy_server",
+ "column_break_12",
+ "is_upstream_setup",
+ "database_section",
+ "database_server",
+ "self_hosted_mariadb_server",
+ "is_managed_database",
+ "enable_logical_replication_during_site_update",
+ "column_break_jdiy",
+ "self_hosted_mariadb_root_password",
+ "managed_database_service",
+ "replication",
+ "is_primary",
+ "is_replication_setup",
+ "column_break_24",
+ "primary",
+ "auto_scale_section",
+ "secondary_server",
+ "is_secondary",
+ "benches_on_shared_volume",
+ "scaled_up",
+ "column_break_ywnx",
+ "auto_scale_trigger",
+ "ssh_section",
+ "ssh_user",
+ "ssh_port",
+ "frappe_user_password",
+ "frappe_public_key",
+ "column_break_20",
+ "bastion_server",
+ "root_public_key",
+ "section_break_22",
+ "use_for_new_benches",
+ "use_for_new_sites",
+ "staging",
+ "use_for_build",
+ "platform",
+ "column_break_ktkv",
+ "new_worker_allocation",
+ "set_bench_memory_limits",
+ "ram",
+ "backups_section",
+ "skip_scheduled_backups",
+ "standalone_section",
+ "is_standalone",
+ "column_break_edyf",
+ "is_standalone_setup",
+ "tags_section",
+ "tags",
+ "mounts_section",
+ "has_data_volume",
+ "mounts",
+ "notifications_section",
+ "communication_infos"
+ ],
+ "fields": [
+ {
+ "fetch_from": "virtual_machine.public_ip_address",
+ "fieldname": "ip",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "IP"
+ },
+ {
+ "fieldname": "proxy_server",
+ "fieldtype": "Link",
+ "label": "Proxy Server",
+ "options": "Proxy Server"
+ },
+ {
+ "fetch_from": "virtual_machine.private_ip_address",
+ "fieldname": "private_ip",
+ "fieldtype": "Data",
+ "label": "Private IP"
+ },
+ {
+ "fieldname": "agent_password",
+ "fieldtype": "Password",
+ "label": "Agent Password",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "agent_section",
+ "fieldtype": "Section Break",
+ "label": "Agent"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_server_setup",
+ "fieldtype": "Check",
+ "label": "Is Server Setup",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_upstream_setup",
+ "fieldtype": "Check",
+ "label": "Upstream Setup",
+ "read_only": 1
+ },
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Pending\nInstalling\nActive\nBroken\nArchived",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "reverse_proxy_section",
+ "fieldtype": "Section Break",
+ "label": "Reverse Proxy"
+ },
+ {
+ "fieldname": "database_section",
+ "fieldtype": "Section Break",
+ "label": "Database"
+ },
+ {
+ "depends_on": "eval:!doc.is_managed_database",
+ "fieldname": "database_server",
+ "fieldtype": "Link",
+ "label": "Database Server",
+ "options": "Database Server"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "ssh_section",
+ "fieldtype": "Section Break",
+ "label": "SSH"
+ },
+ {
+ "fieldname": "root_public_key",
+ "fieldtype": "Code",
+ "label": "Root Public Key",
+ "read_only": 1
+ },
+ {
+ "fieldname": "frappe_public_key",
+ "fieldtype": "Code",
+ "label": "Frappe Public Key",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_22",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_for_new_benches",
+ "fieldtype": "Check",
+ "label": "Use For New Benches"
+ },
+ {
+ "fieldname": "hostname",
+ "fieldtype": "Data",
+ "label": "Hostname",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "domain",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Domain",
+ "options": "Root Domain",
+ "set_only_once": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_for_new_sites",
+ "fieldtype": "Check",
+ "label": "Use For New Sites"
+ },
+ {
+ "fieldname": "cluster",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Cluster",
+ "options": "Cluster",
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "networking_section",
+ "fieldtype": "Section Break",
+ "label": "Networking"
+ },
+ {
+ "depends_on": "eval: doc.provider === \"Scaleway\"",
+ "fieldname": "private_mac_address",
+ "fieldtype": "Data",
+ "label": "Private Mac Address",
+ "mandatory_depends_on": "eval: doc.provider === \"Scaleway\"",
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval: doc.provider === \"Scaleway\"",
+ "fieldname": "private_vlan_id",
+ "fieldtype": "Data",
+ "label": "Private VLAN ID",
+ "mandatory_depends_on": "eval: doc.provider === \"Scaleway\"",
+ "set_only_once": 1
+ },
+ {
+ "default": "Generic",
+ "fieldname": "provider",
+ "fieldtype": "Select",
+ "label": "Provider",
+ "options": "Generic\nScaleway\nAWS EC2\nOCI\nHetzner\nVodacom\nDigitalOcean\nFrappe Compute",
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "frappe_user_password",
+ "fieldtype": "Password",
+ "label": "Frappe User Password",
+ "set_only_once": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "replication",
+ "fieldtype": "Section Break",
+ "label": "Replication"
+ },
+ {
+ "default": "1",
+ "fieldname": "is_primary",
+ "fieldtype": "Check",
+ "label": "Is Primary"
+ },
+ {
+ "fieldname": "column_break_24",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: !doc.is_primary",
+ "fieldname": "primary",
+ "fieldtype": "Link",
+ "label": "Primary",
+ "mandatory_depends_on": "eval: !doc.is_primary",
+ "options": "Server"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: !doc.is_primary",
+ "fieldname": "is_replication_setup",
+ "fieldtype": "Check",
+ "label": "Is Replication Setup",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "staging",
+ "fieldtype": "Check",
+ "label": "Staging"
+ },
+ {
+ "depends_on": "eval:[\"AWS EC2\", \"OCI\", \"Hetzner\", \"DigitalOcean\", \"Frappe Compute\"].includes(doc.provider)",
+ "fieldname": "virtual_machine",
+ "fieldtype": "Link",
+ "label": "Virtual Machine",
+ "mandatory_depends_on": "eval:[\"AWS EC2\", \"OCI\"].includes(doc.provider)",
+ "options": "Virtual Machine"
+ },
+ {
+ "default": "1",
+ "fieldname": "new_worker_allocation",
+ "fieldtype": "Check",
+ "label": "New Worker Allocation"
+ },
+ {
+ "fieldname": "ram",
+ "fieldtype": "Float",
+ "label": "RAM"
+ },
+ {
+ "fieldname": "team",
+ "fieldtype": "Link",
+ "label": "Team",
+ "options": "Team"
+ },
+ {
+ "fieldname": "billing_section",
+ "fieldtype": "Section Break",
+ "label": "Billing"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "plan",
+ "fieldtype": "Link",
+ "label": "Plan",
+ "options": "Server Plan"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_server_prepared",
+ "fieldtype": "Check",
+ "label": "Is Server Prepared",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_server_renamed",
+ "fieldtype": "Check",
+ "label": "Is Server Renamed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_self_hosted",
+ "fieldtype": "Check",
+ "label": "Is Self Hosted"
+ },
+ {
+ "default": "root",
+ "fieldname": "ssh_user",
+ "fieldtype": "Data",
+ "label": "SSH User"
+ },
+ {
+ "depends_on": "eval:doc.is_self_hosted==true && !doc.is_managed_database",
+ "fieldname": "self_hosted_mariadb_server",
+ "fieldtype": "Data",
+ "label": "Self Hosted MariaDB Server IP"
+ },
+ {
+ "depends_on": "eval:doc.is_self_hosted==true",
+ "fieldname": "column_break_jdiy",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.is_self_hosted==true && !doc.is_managed_database",
+ "fieldname": "self_hosted_mariadb_root_password",
+ "fieldtype": "Password",
+ "label": "Self Hosted MariaDB Root Password"
+ },
+ {
+ "depends_on": "eval:doc.is_self_hosted",
+ "fieldname": "self_hosted_server_domain",
+ "fieldtype": "Data",
+ "label": "Self Hosted Server Domain"
+ },
+ {
+ "default": "22",
+ "fieldname": "ssh_port",
+ "fieldtype": "Int",
+ "label": "SSH Port"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "standalone_section",
+ "fieldtype": "Section Break",
+ "label": "Standalone"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standalone",
+ "fieldtype": "Check",
+ "label": "Is Standalone"
+ },
+ {
+ "fieldname": "column_break_edyf",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standalone_setup",
+ "fieldtype": "Check",
+ "label": "Is Standalone Setup",
+ "read_only": 1
+ },
+ {
+ "fieldname": "tags_section",
+ "fieldtype": "Section Break",
+ "label": "Tags"
+ },
+ {
+ "fieldname": "tags",
+ "fieldtype": "Table",
+ "label": "Tags",
+ "options": "Resource Tag"
+ },
+ {
+ "fieldname": "column_break_ktkv",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "set_bench_memory_limits",
+ "fieldtype": "Check",
+ "label": "Set Bench Memory Limits"
+ },
+ {
+ "fieldname": "hostname_abbreviation",
+ "fieldtype": "Data",
+ "label": "Hostname Abbreviation"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "backups_section",
+ "fieldtype": "Section Break",
+ "label": "Backups"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_scheduled_backups",
+ "fieldtype": "Check",
+ "label": "Skip Scheduled Backups"
+ },
+ {
+ "fieldname": "column_break_pdbx",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_agent_job_auto_retry",
+ "fieldtype": "Check",
+ "label": "Disable Agent Job Auto Retry"
+ },
+ {
+ "default": "0",
+ "description": "If user opts DBaaS eg. RDS",
+ "fieldname": "is_managed_database",
+ "fieldtype": "Check",
+ "label": "Is Managed Database"
+ },
+ {
+ "fieldname": "managed_database_service",
+ "fieldtype": "Link",
+ "label": "Managed Database Service",
+ "options": "Managed Database Service"
+ },
+ {
+ "default": "0",
+ "description": "Public release groups will be deployed here",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "label": "Public"
+ },
+ {
+ "default": "0",
+ "description": "If checked, server will be used to run Docker builds.",
+ "fieldname": "use_for_build",
+ "fieldtype": "Check",
+ "label": "Use For Build",
+ "search_index": 1
+ },
+ {
+ "default": "25",
+ "description": "Minimum storage to add automatically each time",
+ "fieldname": "auto_add_storage_min",
+ "fieldtype": "Int",
+ "label": "Auto Add Storage Min",
+ "non_negative": 1
+ },
+ {
+ "default": "250",
+ "description": "Maximum storage to add automatically each time",
+ "fieldname": "auto_add_storage_max",
+ "fieldtype": "Int",
+ "label": "Auto Add Storage Max",
+ "non_negative": 1
+ },
+ {
+ "fieldname": "mounts_section",
+ "fieldtype": "Section Break",
+ "label": "Mounts"
+ },
+ {
+ "fieldname": "mounts",
+ "fieldtype": "Table",
+ "label": "Mounts",
+ "options": "Server Mount"
+ },
+ {
+ "default": "0",
+ "fetch_from": "virtual_machine.has_data_volume",
+ "fieldname": "has_data_volume",
+ "fieldtype": "Check",
+ "label": "Has Data Volume",
+ "read_only": 1
+ },
+ {
+ "fieldname": "ipv6",
+ "fieldtype": "Data",
+ "label": "IPv6"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_agent_job_callbacks",
+ "fieldtype": "Check",
+ "label": "Use Agent Job Callbacks"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_pyspy_setup",
+ "fieldtype": "Check",
+ "label": "Is PySpy Setup",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_mequ",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_laiq",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Stop polling and queuing agent jobs",
+ "fieldname": "halt_agent_jobs",
+ "fieldtype": "Check",
+ "label": "Halt Agent Jobs"
+ },
+ {
+ "default": "x86_64",
+ "fieldname": "platform",
+ "fieldtype": "Select",
+ "label": "Platform",
+ "options": "x86_64\narm64"
+ },
+ {
+ "default": "1",
+ "fieldname": "auto_increase_storage",
+ "fieldtype": "Check",
+ "label": "Auto Increase Storage"
+ },
+ {
+ "default": "0",
+ "description": "Stop all deployments on this server.",
+ "fieldname": "stop_deployments",
+ "fieldtype": "Check",
+ "label": "Stop Deployments"
+ },
+ {
+ "default": "0",
+ "fieldname": "keep_files_on_server_in_offsite_backup",
+ "fieldtype": "Check",
+ "label": "Keep Backup Files Onsite"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_for_recovery",
+ "fieldtype": "Check",
+ "label": "Is for Recovery"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_logical_replication_during_site_update",
+ "fieldtype": "Check",
+ "label": "Enable Logical Replication During Site Update"
+ },
+ {
+ "fieldname": "ignore_incidents_till",
+ "fieldtype": "Datetime",
+ "label": "Ignore Incidents Till"
+ },
+ {
+ "default": "0",
+ "fieldname": "tls_certificate_renewal_failed",
+ "fieldtype": "Check",
+ "label": "TLS Certificate Renewal Failed",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_static_ip",
+ "fieldtype": "Check",
+ "label": "Is Static IP",
+ "read_only": 1
+ },
+ {
+ "fieldname": "notifications_section",
+ "fieldtype": "Section Break",
+ "label": "Notifications"
+ },
+ {
+ "fieldname": "communication_infos",
+ "fieldtype": "Table",
+ "label": "Communication Infos",
+ "options": "Communication Info"
+ },
+ {
+ "fieldname": "bastion_server",
+ "fieldtype": "Link",
+ "label": "Bastion Server",
+ "options": "Bastion Server"
+ },
+ {
+ "description": "Used during horizontal scaling.",
+ "fieldname": "secondary_server",
+ "fieldtype": "Link",
+ "label": "Secondary Server",
+ "options": "Server",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "auto_scale_section",
+ "fieldtype": "Section Break",
+ "label": "Auto Scale"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_monitoring_disabled",
+ "fieldtype": "Check",
+ "label": "Is Monitoring Disabled",
+ "search_index": 1
+ },
+ {
+ "default": "0",
+ "description": "Is this a secondary server",
+ "fieldname": "is_secondary",
+ "fieldtype": "Check",
+ "label": "Is Secondary",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "description": "Are the benches running on a shared volume",
+ "fieldname": "benches_on_shared_volume",
+ "fieldtype": "Check",
+ "label": "Benches on Shared Volume",
+ "read_only": 1
+ },
+ {
+ "fieldname": "auto_scale_trigger",
+ "fieldtype": "Table",
+ "label": "Auto Scale Trigger",
+ "options": "Auto Scale Trigger"
+ },
+ {
+ "default": "0",
+ "description": "Check if the benches are running on the secondary server",
+ "fieldname": "scaled_up",
+ "fieldtype": "Check",
+ "label": "Scaled Up",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_ywnx",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Checked if database and app are hosted on the server server",
+ "fieldname": "is_unified_server",
+ "fieldtype": "Check",
+ "label": "Is Unified Server"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_provisioning_press_job_completed",
+ "fieldtype": "Check",
+ "label": "Is Provisioning Job Completed"
+ },
+ {
+ "fieldname": "nat_server",
+ "fieldtype": "Link",
+ "label": "NAT Server",
+ "options": "NAT Server"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_on_prem_failover_support",
+ "fieldtype": "Check",
+ "label": "Enable On-Prem Failover Support"
+ }
+ ],
+ "links": [
+ {
+ "link_doctype": "Auto Scale Record",
+ "link_fieldname": "primary_server"
+ },
+ {
+ "link_doctype": "On-Prem Failover",
+ "link_fieldname": "app_server"
+ }
+ ],
+ "modified": "2026-04-23 12:34:18.205971",
+ "modified_by": "Administrator",
+ "module": "Press",
+ "name": "Server",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "read": 1,
+ "role": "Press Admin",
+ "write": 1
+ },
+ {
+ "create": 1,
+ "read": 1,
+ "role": "Press Member",
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "title",
+ "track_changes": 1
+}
diff --git a/press/press/doctype/server/server.py b/press/press/doctype/server/server.py
index 676b141b4e9..80d97d06b0d 100644
--- a/press/press/doctype/server/server.py
+++ b/press/press/doctype/server/server.py
@@ -14,6 +14,7 @@
import boto3
import frappe
+import requests
import semantic_version
from frappe import _
from frappe.core.utils import find, find_all
@@ -2688,6 +2689,7 @@ class Server(BaseServer):
keep_files_on_server_in_offsite_backup: DF.Check
managed_database_service: DF.Link | None
mounts: DF.Table[ServerMount]
+ nat_server: DF.Link | None
new_worker_allocation: DF.Check
plan: DF.Link | None
platform: DF.Literal["x86_64", "arm64"]
@@ -2763,6 +2765,9 @@ def on_update(self):
self.update_db_server()
self.set_bench_memory_limits_if_needed(save=False)
+
+ self.validate_public_server_exists_for_site_or_bench_placement()
+
if self.public:
self.auto_add_storage_min = max(self.auto_add_storage_min, PUBLIC_SERVER_AUTO_ADD_STORAGE_MIN)
@@ -2905,6 +2910,47 @@ def _setup_logrotate(self):
except Exception:
log_error("Logrotate Setup Exception", server=self.as_dict())
+ def validate_public_server_exists_for_site_or_bench_placement(self) -> None:
+ """Ensure at least one public server is available in the cluster for:
+ 1. New site placement (use_for_new_sites)
+ 2. New bench deployment (use_for_new_benches)
+ These flags are maintained by refresh_new_bench_and_site_server_pool background job.
+ This validation prevents failures for newly created clusters before the job runs.
+ """
+
+ if not (self.has_value_changed("public") and self.team == "team@erpnext.com" and self.public):
+ return
+
+ servers = frappe.get_all(
+ "Server",
+ filters={"cluster": self.cluster, "public": 1},
+ fields=["use_for_new_sites", "use_for_new_benches"],
+ )
+
+ has_site_server = any(s.use_for_new_sites for s in servers)
+ has_bench_server = any(s.use_for_new_benches for s in servers)
+
+ if has_site_server and has_bench_server:
+ return
+
+ messages = []
+
+ if not has_site_server:
+ messages.append(
+ "There are no public servers in this cluster with Use For New Sites enabled."
+ )
+
+ if not has_bench_server:
+ messages.append(
+ "There are no public servers in this cluster with Use For New Benches enabled."
+ )
+
+ if messages:
+ frappe.throw(
+ " ".join(messages)
+ + " Enable these flags to allow site creation and bench deployment on shared servers in this cluster."
+ )
+
@dashboard_whitelist()
@frappe.whitelist()
def setup_secondary_server(self, server_plan: str):
@@ -3982,43 +4028,83 @@ def is_dedicated_server(server_name):
def refresh_new_bench_and_site_server_pool() -> None:
"""Refresh `use_for_new_benches` and `use_for_new_sites` flags for public clusters
- 1. Consider active, public servers for each cluster
- 2. Compute server score as sum of site plan cpu_time_per_day for sites that aren't archived/suspended
- 3. Mark least-loaded server per cluster as eligible for new benches/sites
+ 1. Consider active, public primary servers for each cluster
+ 2. Fetch available memory and available vCPU for all servers in bulk from Prometheus
+ 3. Mark the server with most memory for new benches and the one with most vCPU for new sites
"""
server_names, servers_by_cluster = _get_public_primary_servers_by_cluster()
if not server_names:
return
- server_score_map = _get_server_cpu_time_across_sites(server_names)
- selected_servers: set[str] = set()
-
- for cluster_servers in servers_by_cluster.values():
- if not cluster_servers:
- continue
+ memory_map, vcpu_map = _get_public_server_available_resources(server_names)
+ if not memory_map and not vcpu_map:
+ return
- least_loaded_server = min(
- cluster_servers,
- key=lambda name: (server_score_map[name], name),
+ if memory_map:
+ _refresh_bench_pool_and_raise_capacity_incidents(
+ server_names=server_names,
+ servers_by_cluster=servers_by_cluster,
+ memory_map=memory_map,
)
- selected_servers.add(least_loaded_server)
- if selected_servers:
- other_servers = list(set(server_names) - selected_servers)
+ if vcpu_map:
+ selected_site_servers = {
+ _get_server_with_most_resource(cluster_servers, vcpu_map)
+ for cluster_servers in servers_by_cluster.values()
+ if cluster_servers
+ }
+ other_servers = list(set(server_names) - selected_site_servers)
if other_servers:
frappe.db.set_value(
"Server",
{"name": ["in", other_servers]},
- {"use_for_new_benches": 0, "use_for_new_sites": 0},
+ {"use_for_new_sites": 0},
update_modified=False,
)
+ frappe.db.set_value(
+ "Server",
+ {"name": ["in", list(selected_site_servers)]},
+ {"use_for_new_sites": 1},
+ update_modified=False,
+ )
+
+def _refresh_bench_pool_and_raise_capacity_incidents(
+ server_names: list[str],
+ servers_by_cluster: dict[str, list[str]],
+ memory_map: dict[str, float],
+) -> None:
+ minimum_bench_memory_bytes = 300 * 1024 * 1024
+ selected_bench_server_by_cluster = {
+ cluster: _get_server_with_most_resource(cluster_servers, memory_map)
+ for cluster, cluster_servers in servers_by_cluster.items()
+ if cluster_servers
+ }
+ selected_bench_servers = set(selected_bench_server_by_cluster.values())
+ other_servers = list(set(server_names) - selected_bench_servers)
+ if other_servers:
frappe.db.set_value(
"Server",
- {"name": ["in", list(selected_servers)]},
- {"use_for_new_benches": 1, "use_for_new_sites": 1},
+ {"name": ["in", other_servers]},
+ {"use_for_new_benches": 0},
update_modified=False,
)
+ frappe.db.set_value(
+ "Server",
+ {"name": ["in", list(selected_bench_servers)]},
+ {"use_for_new_benches": 1},
+ update_modified=False,
+ )
+
+ for cluster, selected_server in selected_bench_server_by_cluster.items():
+ available_memory = memory_map.get(selected_server, 0.0)
+ if available_memory < minimum_bench_memory_bytes:
+ _create_capacity_incident_for_cluster(
+ cluster=cluster,
+ server=selected_server,
+ available_memory_bytes=available_memory,
+ minimum_required_memory_bytes=minimum_bench_memory_bytes,
+ )
def _get_public_primary_servers_by_cluster() -> tuple[list[str], dict[str, list[str]]]:
@@ -4034,31 +4120,114 @@ def _get_public_primary_servers_by_cluster() -> tuple[list[str], dict[str, list[
return server_names, servers_by_cluster
-def _get_server_cpu_time_across_sites(server_names: list[str]) -> dict[str, float]:
- from pypika.functions import Coalesce, Sum
+def _get_server_with_most_resource(server_names: list[str], resource_map: dict[str, float]) -> str:
+ return max(
+ sorted(server_names),
+ key=lambda name: resource_map.get(name, 0.0),
+ )
+
- score_map: dict[str, float] = {}
+def _get_public_server_available_resources(
+ server_names: list[str],
+) -> tuple[dict[str, float] | None, dict[str, float] | None]:
+ """Fetch available memory and vCPU for servers from Prometheus"""
if not server_names:
- return score_map
-
- Site = frappe.qb.DocType("Site")
- SitePlan = frappe.qb.DocType("Site Plan")
-
- server_scores = (
- frappe.qb.from_(Site)
- .left_join(SitePlan)
- .on(Site.plan == SitePlan.name)
- .select(
- Site.server,
- Coalesce(Sum(SitePlan.cpu_time_per_day), 0).as_("cpu_time_per_day"),
- )
- .where(Site.server.isin(server_names))
- .where(~Site.status.isin(["Archived", "Suspended"]))
- .groupby(Site.server)
- .run(as_dict=True)
+ return None, None
+
+ monitor_server = frappe.db.get_single_value("Press Settings", "monitor_server")
+ if not monitor_server:
+ return None, None
+
+ url = f"https://{monitor_server}/prometheus/api/v1/query"
+ password = get_decrypted_password("Monitor Server", monitor_server, "grafana_password")
+ auth = ("frappe", str(password))
+
+ instance_matcher = "|".join(name.replace(".", "[.]") for name in server_names)
+
+ memory_query = f'avg_over_time(node_memory_MemAvailable_bytes{{instance=~"^({instance_matcher})$", job="node"}}[60m])'
+ vcpu_query = f'sum by (instance) (rate(node_cpu_seconds_total{{instance=~"^({instance_matcher})$", job="node", mode="idle"}}[60m]))'
+
+ memory_results = _query_prometheus_vector(memory_query, url, auth)
+ vcpu_results = _query_prometheus_vector(vcpu_query, url, auth)
+
+ def _build_server_map(results: list[dict] | None) -> dict[str, float] | None:
+ if results is None:
+ return None
+ server_map: dict[str, float] = {name: 0.0 for name in server_names}
+ for result in results:
+ instance = result.get("metric", {}).get("instance")
+ if not instance or instance not in server_map:
+ continue
+ with suppress(KeyError, TypeError, ValueError):
+ server_map[instance] = float(result["value"][1])
+ return server_map
+
+ return _build_server_map(memory_results), _build_server_map(vcpu_results)
+
+
+def _query_prometheus_vector(query: str, url: str, auth: tuple[str, str]) -> list[dict] | None:
+ try:
+ response = requests.get(url, params={"query": query}, auth=auth).json()
+ except requests.exceptions.RequestException as exc:
+ log_error("Public Server Pool Prometheus Query Failed", query=query, exception=exc)
+ return None
+
+ if response.get("status") != "success":
+ log_error("Public Server Pool Prometheus Query Failed", query=query, response=response)
+ return None
+
+ return response.get("data", {}).get("result")
+
+
+def _create_capacity_incident_for_cluster(
+ cluster: str,
+ server: str,
+ available_memory_bytes: float,
+ minimum_required_memory_bytes: int,
+) -> None:
+ """Create an incident when selected bench server has insufficient memory capacity"""
+
+ subject = f"Insufficient bench capacity in cluster {cluster}"
+
+ open_incident_exists = frappe.db.exists(
+ "Incident",
+ {
+ "cluster": cluster,
+ "server": server,
+ "subject": subject,
+ "status": ["not in", ["Resolved", "Auto-Resolved", "Press-Resolved"]],
+ },
)
+ if open_incident_exists:
+ return
- for server_score in server_scores:
- score_map[server_score.server] = float(server_score.cpu_time_per_day or 0.0)
+ available_memory_mb = round(available_memory_bytes / 1024 / 1024, 2)
+ minimum_required_memory_mb = round(minimum_required_memory_bytes / 1024 / 1024, 2)
- return score_map
+ description = (
+ "Public server pool capacity check detected insufficient memory for new bench placement. "
+ f"Selected server: {server}. "
+ f"Available memory: {available_memory_mb} MiB. "
+ f"Minimum required per bench: {minimum_required_memory_mb} MiB. "
+ "Action required: provision a new server in this cluster or increase capacity of existing servers."
+ )
+
+ try:
+ incident = frappe.get_doc(
+ {
+ "doctype": "Incident",
+ "server": server,
+ "cluster": cluster,
+ "type": "Server Down",
+ "subject": subject,
+ "description": description,
+ }
+ )
+ incident.insert(ignore_permissions=True)
+ except Exception as exc:
+ log_error(
+ "Failed to create cluster bench capacity incident",
+ cluster=cluster,
+ server=server,
+ exception=exc,
+ )
diff --git a/press/press/doctype/server/test_server.py b/press/press/doctype/server/test_server.py
index 293a41e07f7..8fc7f0a5e78 100644
--- a/press/press/doctype/server/test_server.py
+++ b/press/press/doctype/server/test_server.py
@@ -74,6 +74,8 @@ def create_test_server(
"team": team,
"plan": plan,
"public": public,
+ "use_for_new_sites": 1 if public else 0,
+ "user_new_benches": 1 if public else 0,
"virtual_machine": create_test_virtual_machine(
platform=plan_doc.platform if plan_doc else "x86_64",
disk_size=plan_doc.disk if plan_doc else 25,
@@ -330,3 +332,54 @@ def test_process_running_benches_on_server(self, mock_get):
self.assertEqual(
len(agent_job_created), 1
) # Benches marked as archived, so agent job should be created to force remove zombie benches
+
+ def test_server_with_more_memory_is_shortlisted_for_new_benches_and_incident_created_against_shortlisted_server_with_insufficient_memory(
+ self,
+ ):
+ """The server with higher available memory must be selected (use_for_new_benches=1)."""
+ from press.press.doctype.cluster.test_cluster import create_test_cluster
+ from press.press.doctype.incident.incident import Incident
+ from press.press.doctype.server.server import _refresh_bench_pool_and_raise_capacity_incidents
+
+ self.cluster = create_test_cluster("Default", public=True)
+ # Two servers in the same cluster with different memory levels
+ self.low_mem_server = create_test_server(cluster=self.cluster.name, public=True)
+ self.high_mem_server = create_test_server(cluster=self.cluster.name, public=True)
+
+ memory_map = {
+ self.low_mem_server.name: 200 * 1024 * 1024, # 200 MiB
+ self.high_mem_server.name: 500 * 1024 * 1024, # 500 MiB
+ }
+
+ _refresh_bench_pool_and_raise_capacity_incidents(
+ server_names=[self.low_mem_server.name, self.high_mem_server.name],
+ servers_by_cluster={self.cluster.name: [self.low_mem_server.name, self.high_mem_server.name]},
+ memory_map=memory_map,
+ )
+
+ self.assertEqual(frappe.db.get_value("Server", self.high_mem_server.name, "use_for_new_benches"), 1)
+ self.assertEqual(frappe.db.get_value("Server", self.low_mem_server.name, "use_for_new_benches"), 0)
+
+ # Set both servers below threshold; high_mem_server is still the best candidate
+ memory_map = {
+ self.low_mem_server.name: 50 * 1024 * 1024, # 50 MiB
+ self.high_mem_server.name: 100 * 1024 * 1024, # 100 MiB — best, but still < 300 MiB
+ }
+
+ with patch.object(Incident, "after_insert", new=Mock()):
+ _refresh_bench_pool_and_raise_capacity_incidents(
+ server_names=[self.low_mem_server.name, self.high_mem_server.name],
+ servers_by_cluster={self.cluster.name: [self.low_mem_server.name, self.high_mem_server.name]},
+ memory_map=memory_map,
+ )
+
+ incidents = frappe.get_all(
+ "Incident",
+ {
+ "cluster": self.cluster.name,
+ "subject": f"Insufficient bench capacity in cluster {self.cluster.name}",
+ },
+ ["name", "server"],
+ )
+ self.assertEqual(len(incidents), 1)
+ self.assertEqual(incidents[0].server, self.high_mem_server.name)
diff --git a/press/press/doctype/site/site.py b/press/press/doctype/site/site.py
index dfb2c220e0a..a3226ab2525 100644
--- a/press/press/doctype/site/site.py
+++ b/press/press/doctype/site/site.py
@@ -2918,12 +2918,13 @@ def _get_benches_for_(self, proxy_servers, release_group_names=None, host_on_sha
.where(servers.proxy_server.isin(proxy_servers))
.where(benches.status == "Active")
.orderby(PseudoColumn("in_primary_cluster"), order=frappe.qb.desc)
- .orderby(servers.use_for_new_sites, order=frappe.qb.desc)
.orderby(benches.creation, order=frappe.qb.desc)
.limit(1)
)
+
if host_on_shared_server:
bench_query = bench_query.where(servers.public == 1)
+ bench_query = bench_query.orderby(servers.use_for_new_sites, order=frappe.qb.desc)
if release_group_names:
groups = frappe.qb.DocType("Release Group")
@@ -2944,8 +2945,10 @@ def _get_benches_for_(self, proxy_servers, release_group_names=None, host_on_sha
f"Site can't be deployed on this release group {self.group} due to restrictions. Please try again later or choose a different release group."
)
bench_query = bench_query.where(benches.group == self.group)
+
if self.server:
bench_query = bench_query.where(servers.name == self.server)
+
return bench_query.run(as_dict=True)
def set_bench_for_server(self):
diff --git a/press/press/doctype/site_group_deploy/site_group_deploy.py b/press/press/doctype/site_group_deploy/site_group_deploy.py
index 6a3f08ce594..6955e8fec38 100644
--- a/press/press/doctype/site_group_deploy/site_group_deploy.py
+++ b/press/press/doctype/site_group_deploy/site_group_deploy.py
@@ -81,33 +81,20 @@ def check_if_rg_or_site_exists(self):
frappe.throw(f"Site with subdomain {self.subdomain} already exists")
def get_optimal_server_for_private_bench(self):
- servers = frappe.get_all(
- "Server",
- filters={
- "status": "Active",
- "cluster": self.cluster,
- "provider": self.provider,
- "public": True,
- },
- fields=["name", "ram"],
- )
-
- if not servers:
- return None
-
- server_stats = []
- for server in servers:
- bench_count = frappe.db.count("Bench", {"server": server.name, "status": "Active"})
- resource_ratio = server.ram / (bench_count + 1)
- server_stats.append(
- {
- "name": server.name,
- "resource_ratio": resource_ratio,
- }
+ Server = frappe.qb.doctype("Server")
+ server = (
+ frappe.qb.from_(Server)
+ .select(Server.name)
+ .where(
+ (Server.status == "Active")
+ & (Server.cluster == self.cluster)
+ & (Server.provider == self.provider)
+ & (Server.public == 1)
)
+ .orderby(Server.use_for_new_benches, order=frappe.qb.desc)
+ ).run(pluck=True)
- server_stats.sort(key=lambda x: -x["resource_ratio"])
- return server_stats[0]["name"] if server_stats else None
+ return server[0] if server else None
def create_release_group(self):
from press.press.doctype.release_group.release_group import (