Skip to content

Commit

Permalink
Public release of the scripts.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zorlin committed Nov 28, 2022
1 parent c48ac73 commit 2f77c86
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 39 deletions.
66 changes: 54 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ Demo:

[![asciicast](https://asciinema.org/a/X5bkycTrWOCod7bljEaRsN5Qv.svg)](https://asciinema.org/a/X5bkycTrWOCod7bljEaRsN5Qv)

## Configuration
Global configuration:

* Edit equinix-elastic-ip and replace EQUINIX_PROJECT= as appropriate

* If you need to use public IP addresses instead of private ones, search and replace "private_ipv4" with "public_ipv4" (or remove that "type" specification if you need to use both)

## Getting started
For each machine you want to participate in Pacemaker:

* Install jq and crudini
Expand All @@ -20,17 +14,20 @@ For each machine you want to participate in Pacemaker:

* Create the folder /usr/lib/ocf/resource.d/equinix/

* Copy equinix-elastic-ip to /usr/lib/ocf/resource.d/equinix/equinix-elastic-ip
* Copy equinix-elastic-ip and equinix-elastic-ip-public to /usr/lib/ocf/resource.d/equinix/

Set up a file /etc/ansible/facts.d/uuid.fact as such:

```
equinix_uuid=this-is-not-a-real-uuid
equinix_project=this-is-not-a-real-project
equinix_token=this-is-not-a-real-token
```

where equinix_uuid is the UUID of the machine in question (this is necessary to be able to move the IP to it)

equinix_project is the UUID of the project that machine lives in (to be able to find IP assignments)

and equinix_token is a valid token with read/write scope for your Equinix Metal account.

## Using the resource agent
Expand All @@ -40,18 +37,63 @@ This resource is best used *in combination* with the `ocf:heartbeat:IPaddr2` res
This agent will handle moving the elastic IP between machines in the API, while the `ocf:heartbeat:IPaddr2`
will handle adding the IP itself to an interface on whichever machine you failover to.

Create an `ocf:heartbeat:IPaddr2` resource (replace ip with your needed IP, bond0 with the interface it should live on):
### Public Elastic IP
Create an `ocf:heartbeat:IPaddr2` resource (replace ip with your chosen IP, bond0 with the interface it should live on):

`pcs resource create virtual_ip_public ocf:heartbeat:IPaddr2 ip=198.70.197.254 cidr_netmask=32 nic=bond0`

Create the Equinix Metal Elastic IP resource, replacing `198.70.197.254` with your chosen IP.

`pcs resource create elastic_ip_metal_public ocf:equinix:equinix-elastic-ip-public ip=198.70.197.254`

Create a colocation rule that says they must always "live together"

`pcs constraint colocation add virtual_ip_public with elastic_ip_metal_public INFINITY`

`pcs resource create virtual_ip ocf:heartbeat:IPaddr2 ip=10.70.197.254 cidr_netmask=32 nic=bond0`
### Private Elastic IP
Create an `ocf:heartbeat:IPaddr2` resource (replace ip with your chosen IP, bond0 with the interface it should live on):

Create the Equinix Metal Elastic IP resource (replace ip with your needed IP)
`pcs resource create virtual_ip ocf:heartbeat:IPaddr2 ip=10.20.30.254 cidr_netmask=32 nic=bond0`

`pcs resource create elastic_ip_metal ocf:equinix:equinix-elastic-ip ip=10.70.197.254`
Create the Equinix Metal Elastic IP resource, replacing `10.20.30.254` with your chosen IP and "10.20.30.128" with your chosen subnet.

`pcs resource create elastic_ip_metal ocf:equinix:equinix-elastic-ip ip=10.20.30.254 subnet=10.20.30.128`

Create a colocation rule that says they must always "live together"

`pcs constraint colocation add virtual_ip with elastic_ip_metal INFINITY`

## Operation
Once you've installed the resource agents, added your configuration and created all the required resources, run `pcs status`. You should see something like this:

```
root@db-staging-lb01:~# pcs status
Cluster name: db-loadbalancer
Cluster Summary:
* Stack: corosync
* Current DC: db-staging-lb03 (version 2.1.2-ada5c3b36e2) - partition with quorum
* Last updated: Mon Nov 28 05:26:40 2022
* Last change: Mon Nov 28 04:54:58 2022 by hacluster via crmd on db-staging-lb01
* 3 nodes configured
* 5 resource instances configured
Node List:
* Online: [ db-staging-lb01 db-staging-lb02 db-staging-lb03 ]
Full List of Resources:
* virtual_ip (ocf:heartbeat:IPaddr2): Started db-staging-lb01
* Clone Set: loadbalancer-clone [loadbalancer]:
* Started: [ db-staging-lb01 db-staging-lb02 db-staging-lb03 ]
* elastic_ip_metal (ocf:equinix:equinix-floating-ip): Started db-staging-lb01
Daemon Status:
corosync: active/enabled
pacemaker: active/enabled
pcsd: active/enabled
```

If you see a virtual_ip resource, and an elastic_ip_metal resource (or the public equivalents thereof) started on the same machine, and traffic is flowing, congratulations, you've set up a HA cluster on Equinix Metal! Try rebooting the active machine while pinging the elastic IP and see what happens - failover should happen within 4-12 seconds. Enjoy!

## Credits and License

Copyright (c) 2022 Benjamin Arntzen & Protocol Labs, licensed under the GNU General Public License v2.
Expand Down
45 changes: 29 additions & 16 deletions equinix-elastic-ip
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@
# Defaults
OCF_RESKEY_equinixfact_default="/etc/ansible/facts.d/uuid.fact"
OCF_RESKEY_ip_default=""
OCF_RESKEY_subnet_default=""

: ${OCF_RESKEY_equinixfact=${OCF_RESKEY_equinixfact_default}}
: ${OCF_RESKEY_ip=${OCF_RESKEY_ip_default}}

# Load in Equinix machine UUID and token
MACHINE_UUID=$(crudini --get $OCF_RESKEY_equinixfact '' equinix_uuid)
EQUINIX_TOKEN=$(crudini --get $OCF_RESKEY_equinixfact '' equinix_token)
# Set this yourself
EQUINIX_PROJECT="5f3c29b7-bda3-4f3b-8957-19b1f4b4b6ec"
EQUINIX_PROJECT=$(crudini --get $OCF_RESKEY_equinixfact '' equinix_project)
#######################################################################


Expand Down Expand Up @@ -86,14 +86,22 @@ Path to an INI-style fact file in which a valid UUID for the node and a valid Eq
<parameter name="ip" unique="1" required="1">
<longdesc lang="en">
The IPv4 (dotted quad notation) or IPv6 address (colon hexadecimal notation)
example IPv4 "192.168.1.1".
example IPv6 "2001:db8:DC28:0:0:FC57:D4C8:1FFF".
The IPv4 (dotted quad notation) address
example "192.168.1.1".
</longdesc>
<shortdesc lang="en">IPv4 or IPv6 address</shortdesc>
<shortdesc lang="en">IPv4 address</shortdesc>
<content type="string" default="${OCF_RESKEY_ip_default}" />
</parameter>
<parameter name="subnet" unique="1" required="1">
<longdesc lang="en">
The IPv4 subnet you're using for your elastic IP.
example "10.70.197.128".
</longdesc>
<shortdesc lang="en">IPv4 subnet</shortdesc>
<content type="string" default="${OCF_RESKEY_subnet_default}" />
</parameter>
</parameters>
<actions>
Expand All @@ -118,6 +126,11 @@ emflip_validate() {
return $OCF_ERR_CONFIGURED
fi

if [ -z "$OCF_RESKEY_ip" ] ; then
ocf_exit_reason "IP not set"
return $OCF_ERR_CONFIGURED
fi

return $OCF_SUCCESS
}

Expand All @@ -132,28 +145,28 @@ emflip_monitor() {
currently_assigned=$(curl -X GET \
-H "Content-Type: application/json" \
-H "X-Auth-Token: $EQUINIX_TOKEN" \
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=private_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "10.70.197.128")' | jq -r '.assignments[] | select(.address == "10.70.197.254")' | jq -r '.assigned_to.hostname')
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=private_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "$OCF_RESKEY_subnet")' | jq -r '.assignments[] | select(.address == "$OCF_RESKEY_ip")' | jq -r '.assigned_to.hostname')

assignment_id=$(curl -X GET \
-H "Content-Type: application/json" \
-H "X-Auth-Token: $EQUINIX_TOKEN" \
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=private_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "10.70.197.128")' | jq -r '.assignments[] | select(.address == "10.70.197.254")' | jq -r '.id')
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=private_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "$OCF_RESKEY_subnet")' | jq -r '.assignments[] | select(.address == "$OCF_RESKEY_ip")' | jq -r '.id')

if [ "$currently_assigned" = "$crm_node" ] ; then
ocf_log info "$OCF_RESKEY_IP is attached to this machine"
ocf_log info "$OCF_RESKEY_ip is attached to this machine"
return $OCF_SUCCESS
else
ocf_log warn "$OCF_RESKEY_IP is not attached to this machine"
ocf_log warn "$OCF_RESKEY_ip is not attached to this machine"
return $OCF_NOT_RUNNING
fi
}

emflip_stop() {
ocf_log info "Bringing down IP address $OCF_RESKEY_ip_id"
ocf_log info "Bringing down IP address $OCF_RESKEY_ip"

emflip_monitor
if [ $? = $OCF_NOT_RUNNING ]; then
ocf_log info "Address $OCF_RESKEY_ip_id already down"
ocf_log info "Address $OCF_RESKEY_ip already down"
return $OCF_SUCCESS
fi

Expand All @@ -166,11 +179,11 @@ emflip_stop() {

emflip_monitor
if [ $? != $OCF_NOT_RUNNING ]; then
ocf_log error "Couldn't unset IP address $OCF_RESKEY_ip_id."
ocf_log error "Couldn't unset IP address $OCF_RESKEY_ip."
return $OCF_ERR_GENERIC
fi

ocf_log info "Successfully brought unset $OCF_RESKEY_ip_id"
ocf_log info "Successfully brought unset $OCF_RESKEY_ip"
return $OCF_SUCCESS
}

Expand All @@ -195,10 +208,10 @@ emflip_start() {
-H "X-Auth-Token: $EQUINIX_TOKEN" \
"https://api.equinix.com/metal/v1/devices/$MACHINE_UUID/ips" \
-d '{
"address": "10.70.197.254/32"
"address": "$OCF_RESKEY_ip/32"
}'

if [ $? == 0 ]; then
if [ $? = 0 ]; then
ocf_log info "$OCF_RESKEY_ip successfully assigned!"
return $OCF_SUCCESS
fi
Expand Down
20 changes: 9 additions & 11 deletions equinix-elastic-ip-public
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ OCF_RESKEY_ip_default=""
# Load in Equinix machine UUID and token
MACHINE_UUID=$(crudini --get $OCF_RESKEY_equinixfact '' equinix_uuid)
EQUINIX_TOKEN=$(crudini --get $OCF_RESKEY_equinixfact '' equinix_token)
# Set this yourself
EQUINIX_PROJECT="5f3c29b7-bda3-4f3b-8957-19b1f4b4b6ec"
EQUINIX_PROJECT=$(crudini --get $OCF_RESKEY_equinixfact '' equinix_project)
#######################################################################


Expand All @@ -65,10 +64,10 @@ metadata() {
cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="equinix-elastic-ip">
<resource-agent name="equinix-elastic-ip-public">
<version>2.0</version>
<longdesc lang="en">
Resource Agent to move a elastic IP address from an instance to another one.
Resource Agent to move a (public) elastic IP address from an instance to another one.
It relies on an Ansible-style fact stored as /etc/ansible/facts.d/uuid.fact to retrieve
a machine UUID and API token with which to move that token to that machine, as well as
Crudini being installed to make it reasonably safe to parse the facts file.
Expand All @@ -86,11 +85,10 @@ Path to an INI-style fact file in which a valid UUID for the node and a valid Eq
<parameter name="ip" unique="1" required="1">
<longdesc lang="en">
The IPv4 (dotted quad notation) or IPv6 address (colon hexadecimal notation)
example IPv4 "192.168.1.1".
example IPv6 "2001:db8:DC28:0:0:FC57:D4C8:1FFF".
The IPv4 (dotted quad notation) address
example "192.168.1.1".
</longdesc>
<shortdesc lang="en">IPv4 or IPv6 address</shortdesc>
<shortdesc lang="en">IPv4 address</shortdesc>
<content type="string" default="${OCF_RESKEY_ip_default}" />
</parameter>
Expand Down Expand Up @@ -137,12 +135,12 @@ emflip_monitor() {
currently_assigned=$(curl -X GET \
-H "Content-Type: application/json" \
-H "X-Auth-Token: $EQUINIX_TOKEN" \
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=public_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "139.178.84.65")' | jq -r '.assignments[] | select(.address == "139.178.84.65")' | jq -r '.assigned_to.hostname')
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=public_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "$OCF_RESKEY_ip")' | jq -r '.assignments[] | select(.address == "$OCF_RESKEY_ip")' | jq -r '.assigned_to.hostname')

assignment_id=$(curl -X GET \
-H "Content-Type: application/json" \
-H "X-Auth-Token: $EQUINIX_TOKEN" \
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=public_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "139.178.84.65")' | jq -r '.assignments[] | select(.address == "139.178.84.65")' | jq -r '.id')
"https://api.equinix.com/metal/v1/projects/$EQUINIX_PROJECT/ips?types=public_ipv4&include=assignments.assigned_to" | jq -r '.ip_addresses[] | select(.network == "$OCF_RESKEY_ip")' | jq -r '.assignments[] | select(.address == "$OCF_RESKEY_ip")' | jq -r '.id')

if [ "$currently_assigned" = "$crm_node" ] ; then
ocf_log info "$OCF_RESKEY_ip is attached to this machine"
Expand Down Expand Up @@ -200,7 +198,7 @@ emflip_start() {
-H "X-Auth-Token: $EQUINIX_TOKEN" \
"https://api.equinix.com/metal/v1/devices/$MACHINE_UUID/ips" \
-d '{
"address": "139.178.84.65/32"
"address": "$OCF_RESKEY_ip/32"
}'

if [ $? = 0 ]; then
Expand Down

0 comments on commit 2f77c86

Please sign in to comment.