Skip to content

Commit

Permalink
feat: Add keepalived to Anvil (#17)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Stamatis Katsaounis <stamatis.katsaounis@canonical.com>
  • Loading branch information
SK1Y101 and skatsaounis authored Jul 8, 2024
1 parent 9ad0ca5 commit 0ca79d8
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 20 deletions.
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ ubuntu@infra1:~$ maas-anvil cluster bootstrap \
--accept-defaults
```

Note: You will be asked for a `virtual_ip` during installation of the HAProxy charm, if `accept-defaults` is omitted.
Pass an empty value to disable it, or any valid IP to enable; the Keepalived charm will be installed to enable connecting to HA MAAS using the VIP.

### Add new nodes to the MAAS cluster

```bash
Expand Down Expand Up @@ -74,14 +77,6 @@ ubuntu@infra1:~$ maas-anvil cluster list
ubuntu@infra1:~$ juju run maas-region/0 create-admin username=admin password=pass email=admin@maas.io ssh-import=lp:maasadmin
```

### (Optional) Add VIP functionality through Keepalived charm

```bash
ubuntu@infra1:~$ juju deploy containers/keepalived
ubuntu@infra1:~$ juju relate haproxy:juju-info keepalived:juju-info
ubuntu@infra1:~$ juju config keepalived virtual_ip=<ip_address>
```

### Charm documentation

- MAAS Region: <https://charmhub.io/maas-region>
Expand Down
88 changes: 80 additions & 8 deletions anvil-python/anvil/commands/haproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,69 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List
import ipaddress
import logging
from typing import Any, List

from rich.console import Console
from sunbeam.clusterd.client import Client
from sunbeam.commands.terraform import TerraformInitStep
from sunbeam.jobs import questions
from sunbeam.jobs.common import BaseStep
from sunbeam.jobs.juju import JujuHelper
from sunbeam.jobs.manifest import BaseStep
from sunbeam.jobs.steps import (
AddMachineUnitsStep,
DeployMachineApplicationStep,
RemoveMachineUnitStep,
)

from anvil.jobs.manifest import Manifest
from anvil.provider.local.deployment import LocalDeployment

LOG = logging.getLogger(__name__)

APPLICATION = "haproxy"
CONFIG_KEY = "TerraformVarsHaproxyPlan"
HAPROXY_CONFIG_KEY = "TerraformVarsHaproxy"
KEEPALIVED_CONFIG_KEY = "TerraformVarsKeepalived"
HAPROXY_APP_TIMEOUT = 180 # 3 minutes, managing the application should be fast
HAPROXY_UNIT_TIMEOUT = (
1200 # 15 minutes, adding / removing units can take a long time
)


def keepalived_questions() -> dict[str, questions.PromptQuestion]:
return {
"virtual_ip": questions.PromptQuestion(
"Virtual IP to use for the Cluster in HA",
default_value="",
validation_function=validate_virtual_ip,
),
}


def validate_virtual_ip(value: str) -> str:
"""We allow passing an empty IP for virtual_ip"""
if value == "":
return ""
try:
return ipaddress.ip_address(value).exploded
except ValueError as e:
raise ValueError(f"{value} is not a valid IP address: {e}")


class DeployHAProxyApplicationStep(DeployMachineApplicationStep):
"""Deploy HAProxy application using Terraform"""

_KEEPALIVED_CONFIG = KEEPALIVED_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__(
Expand All @@ -59,10 +90,46 @@ def __init__(
"Deploying HAProxy",
refresh,
)
self.preseed = deployment_preseed or {}
self.accept_defaults = accept_defaults

def get_application_timeout(self) -> int:
return HAPROXY_APP_TIMEOUT

def has_prompts(self) -> bool:
return True

def prompt(self, console: Console | None = None) -> None:
variables = questions.load_answers(
self.client, self._KEEPALIVED_CONFIG
)
variables.setdefault("virtual_ip", "")

# Set defaults
self.preseed.setdefault("virtual_ip", "")

keepalived_config_bank = questions.QuestionBank(
questions=keepalived_questions(),
console=console,
preseed=self.preseed.get("haproxy"),
previous_answers=variables,
accept_defaults=self.accept_defaults,
)

variables["virtual_ip"] = keepalived_config_bank.virtual_ip.ask()

LOG.debug(variables)
questions.write_answers(
self.client, self._KEEPALIVED_CONFIG, variables
)

def extra_tfvars(self) -> dict[str, Any]:
variables: dict[str, Any] = questions.load_answers(
self.client, self._KEEPALIVED_CONFIG
)
variables["haproxy_port"] = 80
return variables


class AddHAProxyUnitsStep(AddMachineUnitsStep):
"""Add HAProxy Unit."""
Expand Down Expand Up @@ -114,15 +181,20 @@ def haproxy_install_steps(
client: Client,
manifest: Manifest,
jhelper: JujuHelper,
deployment: LocalDeployment,
model: str,
fqdn: str,
accept_defaults: bool,
preseed: dict[Any, Any],
) -> List[BaseStep]:
return [
TerraformInitStep(manifest.get_tfhelper("haproxy-plan")),
DeployHAProxyApplicationStep(
client, manifest, jhelper, deployment.infrastructure_model
),
AddHAProxyUnitsStep(
client, fqdn, jhelper, deployment.infrastructure_model
client,
manifest,
jhelper,
model,
accept_defaults=accept_defaults,
deployment_preseed=preseed,
),
AddHAProxyUnitsStep(client, fqdn, jhelper, model),
]
2 changes: 1 addition & 1 deletion anvil-python/anvil/jobs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Role(enum.Enum):
"""The role that the current node will play
This determines if the role will be a region node, a rack/agent node,
r database node or a haproxy node. The role will help determine which
database node, or a haproxy node. The role will help determine which
particular services need to be configured and installed on the system.
"""

Expand Down
19 changes: 17 additions & 2 deletions anvil-python/anvil/provider/local/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,13 @@ def bootstrap(
if is_haproxy_node:
plan4.extend(
haproxy_install_steps(
client, manifest_obj, jhelper, deployment, fqdn
client,
manifest_obj,
jhelper,
deployment.infrastructure_model,
fqdn,
accept_defaults,
preseed,
)
)
if is_region_node:
Expand Down Expand Up @@ -348,6 +354,7 @@ def _print_output(token: str) -> None:


@click.command()
@click.option("-a", "--accept-defaults", help="Accept all defaults.", is_flag=True)
@click.option("--token", type=str, help="Join token")
@click.option(
"--role",
Expand All @@ -365,6 +372,7 @@ def join(
ctx: click.Context,
token: str,
roles: List[Role],
accept_defaults: bool = False,
) -> None:
"""Join node to the cluster.
Expand Down Expand Up @@ -413,6 +421,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)
Expand All @@ -431,7 +440,13 @@ def join(
if is_haproxy_node:
plan2.extend(
haproxy_install_steps(
client, manifest_obj, jhelper, deployment, name
client,
manifest_obj,
jhelper,
deployment.infrastructure_model,
name,
accept_defaults,
preseed,
)
)
if is_region_node:
Expand Down
16 changes: 16 additions & 0 deletions anvil-python/anvil/provider/local/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
LocalDeployment as SunbeamLocalDeployment,
)

from anvil.commands.haproxy import KEEPALIVED_CONFIG_KEY, keepalived_questions

LOG = logging.getLogger(__name__)
LOCAL_TYPE = "local"

Expand All @@ -51,5 +53,19 @@ def generate_preseed(self, console: Console) -> str:
show_questions(bootstrap_bank, section="bootstrap")
)

# HAProxy questions
try:
variables = load_answers(client, KEEPALIVED_CONFIG_KEY)
except ClusterServiceUnavailableException:
variables = {}
keepalived_config_bank = QuestionBank(
questions=keepalived_questions(),
console=console,
previous_answers=variables,
)
preseed_content.extend(
show_questions(keepalived_config_bank, section="haproxy")
)

preseed_content_final = "\n".join(preseed_content)
return preseed_content_final
9 changes: 8 additions & 1 deletion anvil-python/anvil/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
POSTGRESQL_CHANNEL = "14/candidate"
PGBOUNCER_CHANNEL = "1/candidate"
HAPROXY_CHANNEL = "latest/stable"
KEEPALIVED_CHANNEL = "latest/stable"

MACHINE_CHARMS = {
"maas-region": MAAS_REGION_CHANNEL,
"maas-agent": MAAS_AGENT_CHANNEL,
"haproxy": HAPROXY_CHANNEL,
"postgresql": POSTGRESQL_CHANNEL,
"pgbouncer": PGBOUNCER_CHANNEL,
"keepalived": KEEPALIVED_CHANNEL,
}
K8S_CHARMS: dict[str, str] = {}

Expand Down Expand Up @@ -70,7 +72,12 @@
"channel": "charm_haproxy_channel",
"revision": "charm_haproxy_revision",
"config": "charm_haproxy_config",
}
},
"keepalived": {
"channel": "charm_keepalived_channel",
"revision": "charm_keepalived_revision",
"config": "charm_keepalived_config",
},
}
}

Expand Down
38 changes: 38 additions & 0 deletions cloud/etc/deploy-haproxy/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ data "juju_model" "machine_model" {
name = var.machine_model
}

locals {
virtual_ip = var.virtual_ip != "" ? { virtual_ip = var.virtual_ip } : {}
}

resource "juju_application" "haproxy" {
name = "haproxy"
model = data.juju_model.machine_model.name
Expand All @@ -44,3 +48,37 @@ resource "juju_application" "haproxy" {

config = var.charm_haproxy_config
}

resource "juju_application" "keepalived" {
count = min(length(var.virtual_ip), 1)
name = "keepalived"
model = data.juju_model.machine_model.name

charm {
name = "keepalived"
channel = var.charm_keepalived_channel
revision = var.charm_keepalived_revision
base = "ubuntu@22.04"
}

config = merge(
{ port = var.haproxy_port },
local.virtual_ip,
var.charm_keepalived_config,
)
}

resource "juju_integration" "maas-region-haproxy" {
count = min(length(var.virtual_ip), 1)
model = data.juju_model.machine_model.name

application {
name = juju_application.haproxy.name
endpoint = "juju-info"
}

application {
name = juju_application.keepalived[0].name
endpoint = "juju-info"
}
}
30 changes: 30 additions & 0 deletions cloud/etc/deploy-haproxy/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,33 @@ variable "machine_model" {
description = "Model to deploy to"
type = string
}

variable "charm_keepalived_channel" {
description = "Operator channel for Keepalived"
type = string
default = "latest/stable"
}

variable "charm_keepalived_revision" {
description = "Operator channel revision for Keepalived"
type = number
default = null
}

variable "virtual_ip" {
description = "Virtual IP Address for Keepalived"
type = string
default = ""
}

variable "haproxy_port" {
description = "The port that HAProxy listens on"
type = string
default = 80
}

variable "charm_keepalived_config" {
description = "Operator config for Keepalived deployment"
type = map(string)
default = {}
}

0 comments on commit 0ca79d8

Please sign in to comment.