Last active
August 29, 2015 14:13
-
-
Save XavM/b3032be9f38e4cdf4f82 to your computer and use it in GitHub Desktop.
dhcp_ct : A "dhcp like" client written in bash and relying on consul catalog (Requires curl, jq, ping and ... consul)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
############# | |
## dhcp_ct : A "dhcp like" client written in bash and relying on consul catalog (Requires curl, jq and ... consul) | |
## Usage : $> ./dhcp_ct datacenter host_name_prefix ip_pool | |
## Ex : $> ./dhcp_ct dc1 ct 192.168.0.{1..254} # <- Would register a new node in consul catalog using a free IP and HOSTNAME | |
## STDOUT : ct1 192.168.0.88 | |
############# | |
## Fail fast and be aware of exit codes | |
set -eo pipefail | |
## Define globals | |
declare -r SCRIPT_NAME="${0##*/}"; | |
declare -r CURL="curl -sS --connect-timeout 2"; | |
declare -r CONSUL_ENDPOINT="http://consul.service.lan.com/v1"; | |
declare SESSION_ID=""; | |
declare RESERVED_IP=""; | |
declare RESERVED_HOSTNAME=""; | |
declare -a BLACKLISTED_IP=""; | |
declare -a BLACKLISTED_HOSTNAME=""; | |
## Delete consul session and locked keys on exit | |
trap clean_up EXIT | |
clean_up() { | |
[[ ${RESERVED_IP} != "" ]] \ | |
&& delete_key "${SCRIPT_NAME}/${RESERVED_IP}" | |
[[ ${RESERVED_HOSTNAME} != "" ]] \ | |
&& delete_key "${SCRIPT_NAME}/${RESERVED_HOSTNAME}" | |
[[ ${#BLACKLISTED_IP[@]} != 0 ]] \ | |
&& for key in ${BLACKLISTED_IP[@]}; do | |
delete_key "${SCRIPT_NAME}/${key}"; | |
done | |
[[ ${#BLACKLISTED_HOSTNAME[@]} != 0 ]] \ | |
&& for key in ${BLACKLISTED_HOSTNAME[@]}; do | |
delete_key "${SCRIPT_NAME}/${key}"; | |
done | |
[[ ${SESSION_ID} != "" ]] \ | |
&& destroy_session "${SESSION_ID}" | |
} | |
## Get a free IP | |
get_ip() { | |
local ip_pool=${1}; | |
## Extract known IPs as a list | |
local used_ips | |
used_ips="$(get_nodes_liste '.Address')"; | |
## Get the first un used IP from the provided ${ip_pool} | |
for ip in ${ip_pool}; do | |
[[ ${used_ips} == *" ${ip} "* ]] \ | |
|| { | |
## Temporary lock this free IP in kv store | |
reserve_resource ${ip} \ | |
&& { | |
## Make sure the found IP is not Pingable | |
is_pingable "${ip}" \ | |
&& { | |
echo >&2 -e "[WARNING] ${ip} already in use but unknown from consul"; | |
BLACKLISTED_IP=("${BLACKLISTED_IP[@]}" "${ip}") | |
} \ | |
|| { | |
RESERVED_IP=${ip}; | |
break; | |
} | |
} | |
}; | |
done; | |
## Exit 1 when no IP is available | |
[[ ${RESERVED_IP} == "" ]] \ | |
&& { | |
echo >&2 "[CRITICAL] IP pool exhausted"; | |
exit 1; | |
} \ | |
|| return 0; | |
} | |
## Get a free HOSTNAME | |
get_hostname() { | |
local hostname_prefix=${1}; | |
## Extract known host_names as a list | |
local used_hostnames; | |
used_hostnames=" $(get_nodes_liste '.Node')"; | |
## Get the first un used host_name from the host_name_prefix | |
for host in ${hostname_prefix}{1..254}; do | |
[[ ${used_hostnames} == *" ${host} "* ]] \ | |
|| { | |
## Temporary lock this free HOSTNAME in kv store | |
reserve_resource ${host} \ | |
&& { | |
## Make sure the found hostname is not Pingable | |
is_pingable "${host}" \ | |
&& { | |
echo >&2 "[WARNING] ${host} already in use but unknown from consul"; | |
BLACKLISTED_HOSTNAME=("${BLACKLISTED_HOSTNAME[@]}" "${host}") | |
} \ | |
|| { | |
RESERVED_HOSTNAME=${host}; | |
break; | |
} | |
} | |
}; | |
done; | |
## Exit 1 when no hostname is available | |
[[ ${RESERVED_HOSTNAME} == "" ]] \ | |
&& { | |
echo >&2 "[CRITICAL] hostname pool exhausted"; | |
exit 1; | |
} \ | |
|| return 0; | |
} | |
## Extract known nodes from consul catalog | |
get_nodes_liste() { | |
local filter="\" \"+ .[]${1} +\" \""; | |
${CURL} ${CONSUL_ENDPOINT}/catalog/nodes \ | |
| jq -j "${filter}" | |
} | |
is_pingable() { | |
local resource=${1}; | |
ping -c 3 -i 0.5 -W 3 ${resource} > /dev/null 2>&1 \ | |
&& return 0 \ | |
|| return 1 | |
} | |
## Create a consul session (needed for locks) | |
create_session() { | |
[[ "${SESSION_ID}" == "" ]] \ | |
&& { | |
local consul_session | |
consul_session=$(${CURL} -X PUT ${CONSUL_ENDPOINT}/session/create) | |
SESSION_ID=$(echo ${consul_session} | /usr/bin/jq -r '.ID') | |
} | |
} | |
## Destroy the consul session ${SESSION_ID} | |
destroy_session() { | |
[[ "${SESSION_ID}" != "" ]] \ | |
&& ${CURL} -X PUT "${CONSUL_ENDPOINT}/session/destroy/${SESSION_ID}" > /dev/null | |
} | |
## Create and lock a key in the consul kv store | |
lock_key() { | |
local key=$1 | |
local value=${2:-${SCRIPT_NAME}} | |
local consul_lock | |
consul_lock=$(${CURL} -X PUT "${CONSUL_ENDPOINT}/kv/${key}?acquire=${SESSION_ID}" -d "${value}") | |
## Return true or false wether we can get the lock | |
[[ ${consul_lock} == "true" ]] \ | |
|| { | |
#echo >&2 "[WARNING] Can't acquire lock on key ${key}"; | |
return 1 | |
} | |
} | |
## Delete (and unlock) a key in the consul kv store | |
delete_key() { | |
local key=$1 | |
${CURL} -X DELETE "${CONSUL_ENDPOINT}/kv/${key}" | |
} | |
## Avoid any race condition between multiple "dhcp_ct" client fighting for the same IP/HOSTNAME | |
reserve_resource() { | |
local resource="${1}"; | |
create_session; | |
lock_key "${SCRIPT_NAME}/${resource}" \ | |
|| return 1 | |
} | |
## This last one was necessary as '?cas=0' don't work with the consul catalog endpoint | |
resource_exists() { | |
local resource="${1}"; | |
local known_resources; | |
known_resources=$(${CURL} ${CONSUL_ENDPOINT}/catalog/nodes \ | |
| jq -j '.[] | .Address +" "+ .Node +" "'); | |
[[ ${known_resources} == *" ${resource} "* ]] \ | |
&& return 0 \ | |
|| return 1; | |
} | |
## Register a new node in consul catalog with ${RESERVED_IP} and ${RESERVED_HOSTNAME} | |
register_node() { | |
local datacenter="${1}" | |
local host_name="$2"; | |
local ip="$3"; | |
local payload='{ | |
"Datacenter": "'${datacenter}'", | |
"Node": "'${host_name}'", | |
"Address": "'${ip}'" | |
}' | |
## Don't register a node already know from consul catalog or using an IP already referenced in the catalog | |
( resource_exists ${host_name} || resource_exists ${ip} ) \ | |
&& { | |
echo >&2 "[CRTITICAL] node ${host_name} or ip ${ip} already registered in consul catalog"; | |
exit 1; | |
} \ | |
|| ${CURL} ${CONSUL_ENDPOINT}/catalog/register -d "$payload" > /dev/null | |
} | |
main() { | |
local datacenter="${1}"; shift | |
local host_name_prefix="${1}"; shift | |
local ip_pool="${@}"; | |
get_ip "${ip_pool}"; | |
get_hostname "${host_name_prefix}"; | |
register_node ${datacenter} ${RESERVED_HOSTNAME} ${RESERVED_IP}; | |
## Output hostname and IP to stdout | |
echo "${RESERVED_HOSTNAME} ${RESERVED_IP}" | |
} | |
main "$@" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment