1#!@BASH_SHELL@
2#
3#   Copyright 2017 Amazon.com, Inc. and its affiliates. All Rights Reserved.
4#   Licensed under the MIT License.
5#
6#  Copyright 2017 Amazon.com, Inc. and its affiliates
7
8# Permission is hereby granted, free of charge, to any person obtaining a copy of
9# this software and associated documentation files (the "Software"), to deal in
10# the Software without restriction, including without limitation the rights to
11# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12# of the Software, and to permit persons to whom the Software is furnished to do
13# so, subject to the following conditions:
14
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
22# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
23# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24# OTHER DEALINGS IN THE SOFTWARE.
25
26#
27#
28#
29# OCF resource agent to move an IP address within a VPC in the AWS
30# Written by Stefan Schneider , Martin Tegmeier (AWS)
31# Based on code of Markus Guertler#
32#
33#
34# OCF resource agent to move an IP address within a VPC in the AWS
35# Written by Stefan Schneider (AWS) , Martin Tegmeier (AWS)
36# Based on code of Markus Guertler (SUSE)
37#
38# Mar. 15, 2017, vers 1.0.2
39
40
41#######################################################################
42# Initialization:
43
44: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
45. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
46
47# Defaults
48OCF_RESKEY_awscli_default="/usr/bin/aws"
49OCF_RESKEY_profile_default="default"
50OCF_RESKEY_hostedzoneid_default=""
51OCF_RESKEY_fullname_default=""
52OCF_RESKEY_ip_default="local"
53OCF_RESKEY_ttl_default=10
54
55: ${OCF_RESKEY_awscli=${OCF_RESKEY_awscli_default}}
56: ${OCF_RESKEY_profile=${OCF_RESKEY_profile_default}}
57: ${OCF_RESKEY_hostedzoneid:=${OCF_RESKEY_hostedzoneid_default}}
58: ${OCF_RESKEY_fullname:=${OCF_RESKEY_fullname_default}}
59: ${OCF_RESKEY_ip:=${OCF_RESKEY_ip_default}}
60: ${OCF_RESKEY_ttl:=${OCF_RESKEY_ttl_default}}
61#######################################################################
62
63
64AWS_PROFILE_OPT="--profile $OCF_RESKEY_profile --cli-connect-timeout 10"
65#######################################################################
66
67
68usage() {
69	cat <<-EOT
70	usage: $0 {start|stop|status|monitor|validate-all|meta-data}
71	EOT
72}
73
74metadata() {
75cat <<END
76<?xml version="1.0"?>
77<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
78<resource-agent name="aws-vpc-route53">
79<version>1.0</version>
80<longdesc lang="en">
81Update Route53 record of Amazon Webservices EC2 by updating an entry in a
82hosted zone ID table.
83
84AWS instances will require policies which allow them to update Route53 ARecords:
85{
86	"Version": "2012-10-17",
87	"Statement": [
88		{
89			"Sid": "Stmt1471878724000",
90			"Effect": "Allow",
91			"Action": [
92				"route53:ChangeResourceRecordSets",
93				"route53:GetChange",
94				"route53:ListResourceRecordSets",
95			],
96			"Resource": [
97				"*"
98			]
99		}
100	]
101}
102
103Example Cluster Configuration:
104
105Use a configuration in "crm configure edit" which looks as follows. Replace
106hostedzoneid, fullname and profile with the appropriate values:
107
108primitive res_route53 ocf:heartbeat:aws-vpc-route53 \
109		params hostedzoneid=EX4MPL3EX4MPL3 fullname=service.cloud.example.corp. profile=cluster \
110		op start interval=0 timeout=180 \
111		op stop interval=0 timeout=180 \
112		op monitor interval=300 timeout=180 \
113		meta target-role=Started
114</longdesc>
115<shortdesc lang="en">Update Route53 VPC record for AWS EC2</shortdesc>
116
117<parameters>
118<parameter name="awscli">
119<longdesc lang="en">
120Path to command line tools for AWS
121</longdesc>
122<shortdesc lang="en">Path to AWS CLI tools</shortdesc>
123<content type="string" default="${OCF_RESKEY_awscli_default}" />
124</parameter>
125
126<parameter name="profile">
127<longdesc lang="en">
128The name of the AWS CLI profile of the root account. This
129profile will have to use the "text" format for CLI output.
130The file /root/.aws/config should have an entry which looks
131like:
132
133  [profile cluster]
134	region = us-east-1
135	output = text
136
137"cluster" is the name which has to be used in the cluster
138configuration. The region has to be the current one. The
139output has to be "text".
140</longdesc>
141<shortdesc lang="en">AWS Profile Name</shortdesc>
142<content type="string" default="${OCF_RESKEY_profile_default}" />
143</parameter>
144
145<parameter name="hostedzoneid" required="1">
146<longdesc lang="en">
147Hosted zone ID of Route 53. This is the table of
148the Route 53 record.
149</longdesc>
150<shortdesc lang="en">AWS hosted zone ID</shortdesc>
151<content type="string" default="${OCF_RESKEY_hostedzoneid_default}" />
152</parameter>
153
154<parameter name="fullname" required="1">
155<longdesc lang="en">
156The full name of the service which will host the IP address.
157Example: service.cloud.example.corp.
158Note: The trailing dot is important to Route53!
159</longdesc>
160<shortdesc lang="en">Full service name</shortdesc>
161<content type="string" default="${OCF_RESKEY_fullname_default}" />
162</parameter>
163
164<parameter name="ip" required="0">
165<longdesc lang="en">
166IP (local (default), public or secondary private IP address (e.g. 10.0.0.1).
167
168A secondary private IP can be setup with the awsvip agent.
169</longdesc>
170<shortdesc lang="en">Type of IP or secondary private IP address (local, public or e.g. 10.0.0.1)</shortdesc>
171<content type="string" default="${OCF_RESKEY_ip_default}" />
172</parameter>
173
174<parameter name="ttl" required="0">
175<longdesc lang="en">
176Time to live for Route53 ARECORD
177</longdesc>
178<shortdesc lang="en">ARECORD TTL</shortdesc>
179<content type="string" default="${OCF_RESKEY_ttl_default}" />
180</parameter>
181</parameters>
182
183<actions>
184<action name="start" timeout="180s" />
185<action name="stop" timeout="180s" />
186<action name="monitor" depth="0" timeout="180s" interval="300s" />
187<action name="validate-all" timeout="5s" />
188<action name="meta-data" timeout="5s" />
189</actions>
190</resource-agent>
191END
192}
193
194r53_validate() {
195	ocf_log debug "function: validate"
196
197	# Check for required binaries
198	ocf_log debug "Checking for required binaries"
199	for command in curl dig; do
200		check_binary "$command"
201	done
202
203	# Full name
204	[[ -z "$OCF_RESKEY_fullname" ]] && ocf_log error "Full name parameter not set $OCF_RESKEY_fullname!" && exit $OCF_ERR_CONFIGURED
205
206	# Hosted Zone ID
207	[[ -z "$OCF_RESKEY_hostedzoneid" ]] && ocf_log error "Hosted Zone ID parameter not set $OCF_RESKEY_hostedzoneid!" && exit $OCF_ERR_CONFIGURED
208
209	# Type of IP/secondary IP address
210	case $OCF_RESKEY_ip in
211		local|public|*.*.*.*)
212			;;
213		*)
214			ocf_exit_reason "Invalid value for ip: ${OCF_RESKEY_ip}"
215			exit $OCF_ERR_CONFIGURED
216	esac
217
218	# profile
219	[[ -z "$OCF_RESKEY_profile" ]] && ocf_log error "AWS CLI profile not set $OCF_RESKEY_profile!" && exit $OCF_ERR_CONFIGURED
220
221	# TTL
222	[[ -z "$OCF_RESKEY_ttl" ]] && ocf_log error "TTL not set $OCF_RESKEY_ttl!" && exit $OCF_ERR_CONFIGURED
223
224	ocf_log debug "Testing aws command"
225	$OCF_RESKEY_awscli --version 2>&1
226	if [ "$?" -gt 0 ]; then
227		ocf_log error "Error while executing aws command as user root! Please check if AWS CLI tools (Python flavor) are properly installed and configured." && exit $OCF_ERR_INSTALLED
228	fi
229	ocf_log debug "ok"
230
231	return $OCF_SUCCESS
232}
233
234r53_start() {
235	#
236	# Start agent and config DNS in Route53
237	#
238	ocf_log info "Starting Route53 DNS update...."
239	_get_ip
240	r53_monitor
241	if [ $? != $OCF_SUCCESS ]; then
242		ocf_log info "Could not start agent - check configurations"
243		return $OCF_ERR_GENERIC
244	fi
245	return $OCF_SUCCESS
246}
247
248r53_stop() {
249	#
250	# Stop operation doesn't perform any API call or try to remove the DNS record
251	# this mostly because this is not necessarily mandatory or desired
252	# the start and monitor functions will take care of changing the DNS record
253	# if the agent starts in a different cluster node
254	#
255	ocf_log info "Bringing down Route53 agent. (Will NOT remove Route53 DNS record)"
256	return $OCF_SUCCESS
257}
258
259r53_monitor() {
260	#
261	# For every start action the agent  will call Route53 API to check for DNS record
262	# otherwise it will try to get results directly by querying the DNS using "dig".
263	# Due to complexity in some DNS architectures "dig" can fail, and if this happens
264	# the monitor will fallback to the Route53 API call.
265	#
266	# There will be no failure, failover or restart of the agent if the monitor operation fails
267	# hence we only return $OCF_SUCESS in this function
268	#
269	# In case of the monitor operation detects a wrong or non-existent Route53 DNS entry
270	# it will try to fix the existing one, or create it again
271	#
272	#
273	ARECORD=""
274	IPREGEX="^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$"
275	r53_validate
276	ocf_log debug "Checking Route53 record sets"
277	#
278	_get_ip
279	#
280	if [ "$__OCF_ACTION" = "start" ] || ocf_is_probe ; then
281		#
282		cmd="$OCF_RESKEY_awscli $AWS_PROFILE_OPT route53 list-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --query ResourceRecordSets[?Name=='$OCF_RESKEY_fullname']"
283		ocf_log info "Route53 Agent Starting or probing - executing monitoring API call: $cmd"
284		CLIRES="$($cmd 2>&1)"
285		rc=$?
286		ocf_log debug "awscli returned code: $rc"
287		if [ $rc -ne 0 ]; then
288			CLIRES=$(echo $CLIRES | grep -v '^$')
289			ocf_log warn "Route53 API returned an error: $CLIRES"
290			ocf_log warn "Skipping cluster action due to API call error"
291			return $OCF_ERR_GENERIC
292		fi
293		ARECORD=$(echo $CLIRES | grep RESOURCERECORDS | awk '{ print $5 }')
294		#
295		if ocf_is_probe; then
296			#
297			# Prevent R53 record change during probe
298			#
299			if [[ $ARECORD =~ $IPREGEX ]] && [ "$ARECORD" != "$IPADDRESS" ]; then
300				ocf_log debug "Route53 DNS record $ARECORD found at probing, disregarding"
301				return $OCF_NOT_RUNNING
302			fi
303		fi
304	else
305		#
306		cmd="dig +retries=3 +time=5 +short $OCF_RESKEY_fullname 2>/dev/null"
307		ocf_log info "executing monitoring command : $cmd"
308		ARECORD="$($cmd)"
309		rc=$?
310		ocf_log debug "dig return code: $rc"
311		#
312		if  [[ ! $ARECORD =~ $IPREGEX ]] || [ $rc -ne 0 ]; then
313			ocf_log info "Fallback to Route53 API query due to DNS resolution failure"
314			cmd="$OCF_RESKEY_awscli $AWS_PROFILE_OPT route53 list-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --query ResourceRecordSets[?Name=='$OCF_RESKEY_fullname']"
315			ocf_log debug "executing monitoring API call: $cmd"
316			CLIRES="$($cmd 2>&1)"
317			rc=$?
318			ocf_log debug "awscli return code: $rc"
319			if [ $rc -ne 0 ]; then
320				CLIRES=$(echo $CLIRES | grep -v '^$')
321				ocf_log warn "Route53 API returned an error: $CLIRES"
322				ocf_log warn "Monitor skipping cluster action due to API call error"
323				return $OCF_SUCCESS
324			fi
325			ARECORD=$(echo $CLIRES | grep RESOURCERECORDS | awk '{ print $5 }')
326		fi
327		#
328	fi
329	ocf_log info "Route53 DNS record pointing $OCF_RESKEY_fullname to IP address $ARECORD"
330	#
331	if [ "$ARECORD" == "$IPADDRESS" ]; then
332		ocf_log info "Route53 DNS record $ARECORD found"
333		return $OCF_SUCCESS
334	elif [[ $ARECORD =~ $IPREGEX ]] && [ "$ARECORD" != "$IPADDRESS" ]; then
335		ocf_log info "Route53 DNS record points to a different host, setting DNS record on Route53 to this host"
336		_update_record "UPSERT" "$IPADDRESS"
337		return $OCF_SUCCESS
338	else
339		ocf_log info "No Route53 DNS record found, setting DNS record on Route53 to this host"
340		_update_record "UPSERT" "$IPADDRESS"
341		return $OCF_SUCCESS
342	fi
343
344	return $OCF_SUCCESS
345}
346
347_get_ip() {
348	case $OCF_RESKEY_ip in
349		local|public)
350			TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
351			IPADDRESS=$(curl -s http://169.254.169.254/latest/meta-data/${OCF_RESKEY_ip}-ipv4 -H "X-aws-ec2-metadata-token: $TOKEN");;
352		*.*.*.*)
353			IPADDRESS="${OCF_RESKEY_ip}";;
354	esac
355}
356
357_update_record() {
358	#
359	# This function is the one that will actually execute Route53's API call
360	# and configure the DNS record using the correct API calls and parameters
361	#
362	# It creates a temporary JSON file under /tmp with the required API payload
363	#
364	# Failures in this function are critical and will cause the agent to fail
365	#
366	update_action="$1"
367	IPADDRESS="$2"
368	ocf_log info "Updating Route53 $OCF_RESKEY_hostedzoneid with $IPADDRESS for $OCF_RESKEY_fullname"
369	ROUTE53RECORD="$(maketempfile)"
370	if [ $? -ne 0 ] || [ -z "$ROUTE53RECORD" ]; then
371		ocf_exit_reason "Failed to create temporary file for record update"
372		exit $OCF_ERR_GENERIC
373	fi
374	cat >>"$ROUTE53RECORD" <<-EOF
375	{
376		  "Comment": "Update record to reflect new IP address for a system ",
377		  "Changes": [
378			  {
379				  "Action": "$update_action",
380				  "ResourceRecordSet": {
381					  "Name": "$OCF_RESKEY_fullname",
382					  "Type": "A",
383					  "TTL": $OCF_RESKEY_ttl,
384					  "ResourceRecords": [
385						  {
386							  "Value": "$IPADDRESS"
387						  }
388					  ]
389				  }
390			  }
391		  ]
392	}
393	EOF
394	cmd="$OCF_RESKEY_awscli $AWS_PROFILE_OPT route53 change-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --change-batch file://$ROUTE53RECORD "
395	ocf_log debug "Executing command: $cmd"
396	CLIRES="$($cmd 2>&1)"
397	rc=$?
398	ocf_log debug "awscli returned code: $rc"
399	if [ $rc -ne 0 ]; then
400		CLIRES=$(echo $CLIRES | grep -v '^$')
401		ocf_log warn "Route53 API returned an error: $CLIRES"
402		ocf_log warn "Skipping cluster action due to API call error"
403		return $OCF_ERR_GENERIC
404	fi
405	CHANGEID=$(echo $CLIRES | awk '{ print $12 }')
406	ocf_log debug "Change id: $CHANGEID"
407	rmtempfile $ROUTE53RECORD
408	CHANGEID=$(echo $CHANGEID | cut -d'/' -f 3 | cut -d'"' -f 1 )
409	ocf_log debug "Change id: $CHANGEID"
410	STATUS="PENDING"
411	MYSECONDS=20
412	while [ "$STATUS" = 'PENDING' ]; do
413		sleep $MYSECONDS
414		STATUS="$($OCF_RESKEY_awscli $AWS_PROFILE_OPT route53 get-change --id $CHANGEID | grep CHANGEINFO | awk -F'\t' '{ print $4 }' |cut -d'"' -f 2 )"
415		ocf_log debug "Waited for $MYSECONDS seconds and checked execution of Route 53 update status: $STATUS "
416	done
417}
418
419###############################################################################
420
421case $__OCF_ACTION in
422	usage|help)
423		usage
424		exit $OCF_SUCCESS
425		;;
426	meta-data)
427		metadata
428		exit $OCF_SUCCESS
429		;;
430	start)
431		r53_validate || exit $?
432		r53_start
433		;;
434	stop)
435		r53_stop
436		;;
437	monitor)
438		r53_monitor
439		;;
440	validate-all)
441		r53_validate
442		;;
443	*)
444		usage
445		exit $OCF_ERR_UNIMPLEMENTED
446		;;
447esac
448
449exit $?
450