diff --git a/anvil-python/anvil/commands/haproxy.py b/anvil-python/anvil/commands/haproxy.py index 964bc05..0306a60 100644 --- a/anvil-python/anvil/commands/haproxy.py +++ b/anvil-python/anvil/commands/haproxy.py @@ -114,15 +114,11 @@ def haproxy_install_steps( client: Client, manifest: Manifest, jhelper: JujuHelper, - deployment: LocalDeployment, + model: str, fqdn: str, ) -> List[BaseStep]: return [ TerraformInitStep(manifest.get_tfhelper("haproxy-plan")), - DeployHAProxyApplicationStep( - client, manifest, jhelper, deployment.infrastructure_model - ), - AddHAProxyUnitsStep( - client, fqdn, jhelper, deployment.infrastructure_model - ), + DeployHAProxyApplicationStep(client, manifest, jhelper, model), + AddHAProxyUnitsStep(client, fqdn, jhelper, model), ] diff --git a/anvil-python/anvil/commands/maas_agent.py b/anvil-python/anvil/commands/maas_agent.py index 6b2b632..aaad565 100644 --- a/anvil-python/anvil/commands/maas_agent.py +++ b/anvil-python/anvil/commands/maas_agent.py @@ -116,15 +116,11 @@ def maas_agent_install_steps( client: Client, manifest: Manifest, jhelper: JujuHelper, - deployment: LocalDeployment, + model: str, fqdn: str, ) -> List[BaseStep]: return [ TerraformInitStep(manifest.get_tfhelper("maas-agent-plan")), - DeployMAASAgentApplicationStep( - client, manifest, jhelper, deployment.infrastructure_model - ), - AddMAASAgentUnitsStep( - client, fqdn, jhelper, deployment.infrastructure_model - ), + DeployMAASAgentApplicationStep(client, manifest, jhelper, model), + AddMAASAgentUnitsStep(client, fqdn, jhelper, model), ] diff --git a/anvil-python/anvil/commands/maas_region.py b/anvil-python/anvil/commands/maas_region.py index e955f33..83d11c7 100644 --- a/anvil-python/anvil/commands/maas_region.py +++ b/anvil-python/anvil/commands/maas_region.py @@ -124,15 +124,11 @@ def maas_region_install_steps( client: Client, manifest: Manifest, jhelper: JujuHelper, - deployment: LocalDeployment, + model: str, fqdn: str, ) -> List[BaseStep]: return [ TerraformInitStep(manifest.get_tfhelper("maas-region-plan")), - DeployMAASRegionApplicationStep( - client, manifest, jhelper, deployment.infrastructure_model - ), - AddMAASRegionUnitsStep( - client, fqdn, jhelper, deployment.infrastructure_model - ), + DeployMAASRegionApplicationStep(client, manifest, jhelper, model), + AddMAASRegionUnitsStep(client, fqdn, jhelper, model), ] diff --git a/anvil-python/anvil/commands/postgresql.py b/anvil-python/anvil/commands/postgresql.py index 5a3809f..f2a9de9 100644 --- a/anvil-python/anvil/commands/postgresql.py +++ b/anvil-python/anvil/commands/postgresql.py @@ -13,12 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List +import logging +from typing import Any, List, Optional +from rich.status import Status from sunbeam.clusterd.client import Client from sunbeam.commands.terraform import TerraformInitStep +from sunbeam.jobs import questions +from sunbeam.jobs.common import BaseStep, Result, ResultType from sunbeam.jobs.juju import JujuHelper -from sunbeam.jobs.manifest import BaseStep from sunbeam.jobs.steps import ( AddMachineUnitsStep, DeployMachineApplicationStep, @@ -28,8 +31,10 @@ from anvil.jobs.manifest import Manifest from anvil.provider.local.deployment import LocalDeployment +LOG = logging.getLogger(__name__) APPLICATION = "postgresql" CONFIG_KEY = "TerraformVarsPostgresqlPlan" +POSTGRESQL_CONFIG_KEY = "TerraformVarsPostgresql" POSTGRESQL_APP_TIMEOUT = ( 180 # 3 minutes, managing the application should be fast ) @@ -38,15 +43,66 @@ ) +def postgresql_install_steps( + client: Client, + manifest: Manifest, + jhelper: JujuHelper, + model: str, + fqdn: str, + accept_defaults: bool, + preseed: dict[Any, Any], +) -> List[BaseStep]: + return [ + TerraformInitStep(manifest.get_tfhelper("postgresql-plan")), + DeployPostgreSQLApplicationStep( + client, + manifest, + jhelper, + model, + accept_defaults=accept_defaults, + deployment_preseed=preseed, + ), + AddPostgreSQLUnitsStep(client, fqdn, jhelper, model), + ] + + +def postgresql_questions() -> dict[str, questions.PromptQuestion]: + return { + "max_connections": questions.PromptQuestion( + "Maximum number of concurrent connections to allow to the database server", + default_value="default", + validation_function=validate_max_connections, + ), + } + + +def validate_max_connections(value: str) -> str | ValueError: + if value in ["default", "dynamic"]: + return value + try: + if 100 <= int(value) <= 500: + return value + else: + raise ValueError + except ValueError: + raise ValueError( + "Please provide either a number between 1 and 500 or 'default' for system default or 'dynamic' for calculating max_connections relevant to maas regions" + ) + + class DeployPostgreSQLApplicationStep(DeployMachineApplicationStep): """Deploy PostgreSQL application using Terraform""" + _CONFIG = POSTGRESQL_CONFIG_KEY + def __init__( self, client: Client, manifest: Manifest, jhelper: JujuHelper, model: str, + deployment_preseed: dict[Any, Any] | None = None, + accept_defaults: bool = False, refresh: bool = False, ): super().__init__( @@ -62,9 +118,92 @@ def __init__( refresh, ) + self.preseed = deployment_preseed or {} + self.accept_defaults = accept_defaults + + def get_application_timeout(self) -> int: + return POSTGRESQL_APP_TIMEOUT + + def prompt(self, console: questions.Console | None = None) -> None: + variables = questions.load_answers(self.client, self._CONFIG) + variables.setdefault("max_connections", "default") + + # Set defaults + self.preseed.setdefault("max_connections", "default") + + postgresql_config_bank = questions.QuestionBank( + questions=postgresql_questions(), + console=console, + preseed=self.preseed, + previous_answers=variables, + accept_defaults=self.accept_defaults, + ) + max_connections = postgresql_config_bank.max_connections.ask() + variables["max_connections"] = max_connections + + LOG.debug(variables) + questions.write_answers(self.client, self._CONFIG, self.variables) + + def extra_tfvars(self) -> dict[str, Any]: + variables: dict[str, Any] = questions.load_answers( + self.client, self._CONFIG + ) + variables["maas_region_nodes"] = ( + self.client.cluster.list_nodes_by_role("region") + ) + return variables + + def has_prompts(self) -> bool: + return True + + +class ReapplyPostgreSQLTerraformPlanStep(DeployMachineApplicationStep): + """Reapply PostgreSQL Terraform plan""" + + _CONFIG = POSTGRESQL_CONFIG_KEY + + def __init__( + self, + client: Client, + manifest: Manifest, + jhelper: JujuHelper, + model: str, + ): + super().__init__( + client, + manifest, + jhelper, + CONFIG_KEY, + APPLICATION, + model, + "postgresql-plan", + "Reapply PostgreSQL Terraform plan", + "Reapplying PostgreSQL Terraform plan", + True, + ) + def get_application_timeout(self) -> int: return POSTGRESQL_APP_TIMEOUT + def extra_tfvars(self) -> dict[str, Any]: + variables: dict[str, Any] = questions.load_answers( + self.client, self._CONFIG + ) + variables["maas_region_nodes"] = ( + self.client.cluster.list_nodes_by_role("region") + ) + return variables + + def is_skip(self, status: Status | None = None) -> Result: + variables: dict[str, Any] = questions.load_answers( + self.client, self._CONFIG + ) + variables.setdefault("max_connections", "default") + if variables["max_connections"] != "dynamic": + return Result(ResultType.SKIPPED) + else: + return super().is_skip(status) + class AddPostgreSQLUnitsStep(AddMachineUnitsStep): """Add PostgreSQL Unit.""" @@ -110,21 +249,3 @@ def __init__( def get_unit_timeout(self) -> int: return POSTGRESQL_UNIT_TIMEOUT - - -def postgresql_install_steps( - client: Client, - manifest: Manifest, - jhelper: JujuHelper, - deployment: LocalDeployment, - fqdn: str, -) -> List[BaseStep]: - return [ - TerraformInitStep(manifest.get_tfhelper("postgresql-plan")), - DeployPostgreSQLApplicationStep( - client, manifest, jhelper, deployment.infrastructure_model - ), - AddPostgreSQLUnitsStep( - client, fqdn, jhelper, deployment.infrastructure_model - ), - ] diff --git a/anvil-python/anvil/provider/local/commands.py b/anvil-python/anvil/provider/local/commands.py index 6a4b908..2eba5e6 100644 --- a/anvil-python/anvil/provider/local/commands.py +++ b/anvil-python/anvil/provider/local/commands.py @@ -83,6 +83,7 @@ maas_region_install_steps, ) from anvil.commands.postgresql import ( + ReapplyPostgreSQLTerraformPlanStep, RemovePostgreSQLUnitStep, postgresql_install_steps, ) @@ -264,24 +265,42 @@ def bootstrap( jhelper = JujuHelper(deployment.get_connected_controller()) plan4 = postgresql_install_steps( - client, manifest_obj, jhelper, deployment, fqdn + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + fqdn, + accept_defaults, + preseed, ) if is_haproxy_node: plan4.extend( haproxy_install_steps( - client, manifest_obj, jhelper, deployment, fqdn + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + fqdn, ) ) if is_region_node: plan4.extend( maas_region_install_steps( - client, manifest_obj, jhelper, deployment, fqdn + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + fqdn, ) ) if is_agent_node: plan4.extend( maas_agent_install_steps( - client, manifest_obj, jhelper, deployment, fqdn + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + fqdn, ) ) run_plan(plan4, console) @@ -365,6 +384,7 @@ def join( ctx: click.Context, token: str, roles: List[Role], + accept_defaults: bool = False, ) -> None: """Join node to the cluster. @@ -413,6 +433,7 @@ def join( manifest_obj = Manifest.load_latest_from_clusterdb( deployment, include_defaults=True ) + preseed = manifest_obj.deployment_config machine_id = -1 machine_id_result = get_step_message(plan1_results, AddJujuMachineStep) @@ -425,25 +446,48 @@ def join( if is_database_node: plan2.extend( postgresql_install_steps( - client, manifest_obj, jhelper, deployment, name + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + name, + accept_defaults, + preseed, ) ) if is_haproxy_node: plan2.extend( haproxy_install_steps( - client, manifest_obj, jhelper, deployment, name + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + name, ) ) if is_region_node: plan2.extend( maas_region_install_steps( - client, manifest_obj, jhelper, deployment, name + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + name, + ) + ) + plan2.append( + ReapplyPostgreSQLTerraformPlanStep( + client, manifest_obj, jhelper, deployment.infrastructure_model ) ) if is_agent_node: plan2.extend( maas_agent_install_steps( - client, manifest_obj, jhelper, deployment, name + client, + manifest_obj, + jhelper, + deployment.infrastructure_model, + name, ) ) diff --git a/anvil-python/anvil/provider/local/deployment.py b/anvil-python/anvil/provider/local/deployment.py index 8a35741..4af40ea 100644 --- a/anvil-python/anvil/provider/local/deployment.py +++ b/anvil-python/anvil/provider/local/deployment.py @@ -26,6 +26,11 @@ LocalDeployment as SunbeamLocalDeployment, ) +from anvil.commands.postgresql import ( + POSTGRESQL_CONFIG_KEY, + postgresql_questions, +) + LOG = logging.getLogger(__name__) LOCAL_TYPE = "local" @@ -51,5 +56,19 @@ def generate_preseed(self, console: Console) -> str: show_questions(bootstrap_bank, section="bootstrap") ) + # PostgreSQL questions + try: + variables = load_answers(client, POSTGRESQL_CONFIG_KEY) + except ClusterServiceUnavailableException: + variables = {} + postgresql_config_bank = QuestionBank( + questions=postgresql_questions(), + console=console, + previous_answers=variables, + ) + preseed_content.extend( + show_questions(postgresql_config_bank, section="postgres") + ) + preseed_content_final = "\n".join(preseed_content) return preseed_content_final diff --git a/cloud/etc/deploy-maas-region/main.tf b/cloud/etc/deploy-maas-region/main.tf index 6434fcf..c08dd5e 100644 --- a/cloud/etc/deploy-maas-region/main.tf +++ b/cloud/etc/deploy-maas-region/main.tf @@ -58,8 +58,8 @@ resource "juju_application" "pgbouncer" { } config = merge({ - pool_mode = "transaction" - max_db_connections = floor(90 / max(length(var.machine_ids), 1)) + pool_mode = "session" + max_db_connections = var.max_connections_per_region }, var.charm_pgbouncer_config) } diff --git a/cloud/etc/deploy-maas-region/variables.tf b/cloud/etc/deploy-maas-region/variables.tf index 9822e2f..7927056 100644 --- a/cloud/etc/deploy-maas-region/variables.tf +++ b/cloud/etc/deploy-maas-region/variables.tf @@ -65,3 +65,9 @@ variable "charm_pgbouncer_config" { type = map(string) default = {} } + +variable "max_connections_per_region" { + description = "Maximum number of concurrent connections to allow to the database server per region" + type = string + default = 40 +} diff --git a/cloud/etc/deploy-postgresql/main.tf b/cloud/etc/deploy-postgresql/main.tf index ec16ab0..cea60d3 100644 --- a/cloud/etc/deploy-postgresql/main.tf +++ b/cloud/etc/deploy-postgresql/main.tf @@ -30,6 +30,12 @@ data "juju_model" "machine_model" { name = var.machine_model } +locals { + max_connections = var.max_connections == "default" ? "" : ( + var.max_connections == "dynamic" ? max(100, 10 + var.max_connections_per_region * var.maas_region_nodes) : var.max_connections + ) +} + resource "juju_application" "postgresql" { name = "postgresql" model = data.juju_model.machine_model.name @@ -42,5 +48,7 @@ resource "juju_application" "postgresql" { base = "ubuntu@22.04" } - config = var.charm_postgresql_config + config = merge({ + max_connections = local.max_connections + }, var.charm_postgresql_config) } diff --git a/cloud/etc/deploy-postgresql/variables.tf b/cloud/etc/deploy-postgresql/variables.tf index 8989b9a..f873df7 100644 --- a/cloud/etc/deploy-postgresql/variables.tf +++ b/cloud/etc/deploy-postgresql/variables.tf @@ -41,3 +41,21 @@ variable "machine_model" { description = "Model to deploy to" type = string } + +variable "max_connections" { + description = "Maximum number of concurrent connections to allow to the database server" + type = string + default = "default" +} + +variable "maas_region_nodes" { + description = "Total number of MAAS region nodes" + type = number + default = 0 +} + +variable "max_connections_per_region" { + description = "Maximum number of concurrent connections to allow to the database server per region" + type = string + default = 40 +}