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 (