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