Home | History | Annotate | Download | only in shell
      1 #!/bin/sh
      2 #
      3 # CDDL HEADER START
      4 #
      5 # The contents of this file are subject to the terms of the
      6 # Common Development and Distribution License (the "License").
      7 # You may not use this file except in compliance with the License.
      8 #
      9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
     10 # or http://www.opensolaris.org/os/licensing.
     11 # See the License for the specific language governing permissions
     12 # and limitations under the License.
     13 #
     14 # When distributing Covered Code, include this CDDL HEADER in each
     15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     16 # If applicable, add the following below this CDDL HEADER, with the
     17 # fields enclosed by brackets "[]" replaced with your own identifying
     18 # information: Portions Copyright [yyyy] [name of copyright owner]
     19 #
     20 # CDDL HEADER END
     21 #
     22 #
     23 # Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
     24 # Use is subject to license terms.
     25 #
     26 # Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T.
     27 # All rights reserved.
     28 #
     29 
     30 # Print warnings to console
     31 warn_failed_ifs() {
     32 	echo "Failed to $1 interface(s):$2" >/dev/msglog
     33 }
     34 
     35 #
     36 # shcat file
     37 #   Simulates cat in sh so it doesn't need to be on the root filesystem.
     38 #
     39 shcat() {
     40         while [ $# -ge 1 ]; do
     41                 while read i; do
     42                         echo "$i"
     43                 done < $1
     44                 shift
     45         done
     46 }
     47 
     48 #
     49 # inet_list	list of IPv4 interfaces.
     50 # inet6_list	list of IPv6 interfaces.
     51 # ipmp_list	list of IPMP IPv4 interfaces.
     52 # ipmp6_list	list of IPMP IPv6 interfaces.
     53 # inet_plumbed	list of plumbed IPv4 interfaces.
     54 # inet6_plumbed list of plumbed IPv6 interfaces.
     55 # ipmp_created 	list of created IPMP IPv4 interfaces.
     56 # ipmp6_created	list of created IPMP IPv6 interfaces.
     57 # inet_failed	list of IPv4 interfaces that failed to plumb.
     58 # inet6_failed	list of IPv6 interfaces that failed to plumb.
     59 # ipmp_failed 	list of IPMP IPv4 interfaces that failed to be created.
     60 # ipmp6_failed	list of IPMP IPv6 interfaces that failed to be created.
     61 #
     62 unset inet_list inet_plumbed inet_failed \
     63 	inet6_list inet6_plumbed inet6_failed \
     64 	ipmp_list ipmp_created ipmp_failed \
     65 	ipmp6_list ipmp6_created ipmp6_failed
     66 
     67 #
     68 # get_physical interface
     69 #
     70 # Return physical interface corresponding to the given interface.
     71 #
     72 get_physical()
     73 {
     74 	ORIGIFS="$IFS"
     75 	IFS="${IFS}:"
     76 	set -- $1
     77 	IFS="$ORIGIFS"
     78 
     79 	echo $1
     80 }
     81 
     82 #
     83 # get_logical interface
     84 #
     85 # Return logical interface number.  Zero will be returned
     86 # if there is no explicit logical number.
     87 #
     88 get_logical()
     89 {
     90 	ORIGIFS="$IFS"
     91 	IFS="${IFS}:"
     92 	set -- $1
     93 	IFS="$ORIGIFS"
     94 
     95 	if [ -z "$2" ]; then
     96 		echo 0
     97 	else
     98 		echo $2
     99 	fi
    100 }
    101 
    102 #
    103 # if_comp if1 if2
    104 #
    105 # Compare interfaces.  Do the physical interface names and logical interface
    106 # numbers match?
    107 #
    108 if_comp()
    109 {
    110 	physical_comp $1 $2 && [ `get_logical $1` -eq `get_logical $2` ]
    111 }
    112 
    113 #
    114 # physical_comp if1 if2
    115 # 
    116 # Do the two interfaces share a physical interface?
    117 #
    118 physical_comp()
    119 {
    120 	[ "`get_physical $1`" = "`get_physical $2`" ]
    121 }
    122 
    123 #
    124 # in_list op item list
    125 #
    126 # Is "item" in the given list?  Use "op" to do the test, applying it to
    127 # "item" and each member of the list in turn until it returns success.
    128 #
    129 in_list()
    130 {
    131 	op=$1
    132 	item=$2
    133 	shift 2
    134 
    135 	while [ $# -gt 0 ]; do
    136 		$op $item $1 && return 0
    137 		shift
    138 	done
    139 
    140 	return 1
    141 }
    142 
    143 #
    144 # get_groupifname groupname
    145 #
    146 # Return the IPMP meta-interface name for the group, if it exists.
    147 #
    148 get_groupifname()
    149 {
    150 	/sbin/ipmpstat -gP -o groupname,group | while IFS=: read name ifname; do
    151 		if [ "$name" = "$1" ]; then
    152 			echo "$ifname"
    153 			return
    154 		fi
    155 	done
    156 }
    157 
    158 #
    159 # create_ipmp ifname groupname type
    160 #
    161 # Helper function for create_groupifname() that returns zero if it's able
    162 # to create an IPMP interface of the specified type and place it in the
    163 # specified group, or non-zero otherwise.
    164 #
    165 create_ipmp()
    166 {
    167 	/sbin/ifconfig $1 >/dev/null 2>&1 && return 1
    168 	/sbin/ifconfig $1 inet6 >/dev/null 2>&1 && return 1
    169 	/sbin/ifconfig $1 $3 ipmp group $2 2>/dev/null
    170 }
    171 
    172 #
    173 # create_groupifname groupname type 
    174 #
    175 # Create an IPMP meta-interface name for the group.  We only use this
    176 # function if all of the interfaces in the group failed at boot and there
    177 # were no /etc/hostname[6].<if> files for the IPMP meta-interface.
    178 #
    179 create_groupifname()
    180 {
    181 	#
    182 	# This is a horrible way to count from 0 to 999, but in sh and
    183 	# without necessarily having /usr mounted, what else can we do?
    184 	#
    185 	for a in "" 1 2 3 4 5 6 7 8 9; do
    186 		for b in 0 1 2 3 4 5 6 7 8 9; do
    187 			for c in 0 1 2 3 4 5 6 7 8 9; do
    188 				# strip leading zeroes
    189 				[ "$a" = "" ] && [ "$b" = 0 ] && b=""
    190 				if create_ipmp ipmp$a$b$c $1 $2; then
    191 					echo ipmp$a$b$c
    192 					return
    193 				fi
    194 			done
    195 		done
    196 	done
    197 }
    198 
    199 #
    200 # get_hostname_ipmpinfo interface type
    201 #
    202 # Return all requested IPMP keywords from hostname file for a given interface.
    203 #
    204 # Example:
    205 #	get_hostname_ipmpinfo hme0 inet keyword [ keyword ... ]
    206 #
    207 get_hostname_ipmpinfo()
    208 {
    209 	case "$2" in
    210 		inet)	file=/etc/hostname.$1
    211 			;;
    212 		inet6)	file=/etc/hostname6.$1
    213 			;;
    214 		*)
    215 			return
    216 			;;
    217 	esac
    218 
    219 	[ -r "$file" ] || return 
    220 
    221 	type=$2
    222 	shift 2
    223 
    224 	#
    225 	# Read through the hostname file looking for the specified
    226 	# keywords.  Since there may be several keywords that cancel
    227 	# each other out, the caller must post-process as appropriate.
    228 	#
    229 	while read line; do
    230 		[ -z "$line" ] && continue
    231 		/sbin/ifparse -s "$type" $line
    232 	done < "$file" | while read one two; do
    233 		for keyword in "$@"; do
    234 			[ "$one" = "$keyword" ] && echo "$one $two"
    235 		done
    236 	done
    237 }
    238 
    239 #
    240 # get_group_for_type interface type list
    241 #
    242 # Look through the set of hostname files associated with the same physical
    243 # interface as "interface", and determine which group they would configure.
    244 # Only hostname files associated with the physical interface or logical
    245 # interface zero are allowed to set the group.
    246 #
    247 get_group_for_type()
    248 {
    249 	physical=`get_physical $1`
    250 	type=$2
    251 	group=""
    252 
    253 	#
    254 	# The last setting of the group is the one that counts, which is
    255 	# the reason for the second while loop.
    256 	#
    257 	shift 2
    258 	for ifname in "$@"; do
    259 		if if_comp "$physical" $ifname; then 
    260 			get_hostname_ipmpinfo $ifname $type group
    261 		fi
    262 	done | while :; do
    263 		read keyword grname || {
    264 			echo "$group"
    265 			break
    266 		}
    267 		group="$grname"
    268 	done
    269 }
    270 
    271 #
    272 # get_group interface
    273 #
    274 # If there is both an inet and inet6 version of an interface, the group
    275 # could be set in either set of hostname files.  Since inet6 is configured
    276 # after inet, if there's a setting in both files, inet6 wins.
    277 #
    278 get_group()
    279 {
    280 	group=`get_group_for_type $1 inet6 $inet6_list`
    281 	[ -z "$group" ] && group=`get_group_for_type $1 inet $inet_list`
    282 	echo $group
    283 }
    284 
    285 #
    286 # Given the interface name and the address family (inet or inet6), determine
    287 # whether this is a VRRP VNIC.
    288 #
    289 # This is used to determine whether to bring the interface up
    290 #
    291 not_vrrp_interface() {
    292 	macaddrtype=`/sbin/dladm show-vnic $1 -o MACADDRTYPE -p 2>/dev/null`
    293 
    294 	case "$macaddrtype" in
    295 	'vrrp'*''$2'')	vrrp=1
    296 			;;
    297         *)		vrrp=0
    298 			;;
    299 	esac
    300 	return $vrrp
    301 }
    302 
    303 # doDHCPhostname interface
    304 # Pass to this function the name of an interface.  It will return
    305 # true if one should enable the use of DHCP client-side host name
    306 # requests on the interface, and false otherwise.
    307 #
    308 doDHCPhostname()
    309 {
    310 	if [ -f /etc/dhcp.$1 ] && [ -f /etc/hostname.$1 ]; then
    311                 set -- `shcat /etc/hostname.$1`
    312                 [ $# -eq 2 -a "$1" = "inet" ]
    313                 return $?      
    314         fi
    315         return 1
    316 }
    317 
    318 #
    319 # inet_process_hostname processor [ args ]
    320 #
    321 # Process an inet hostname file.  The contents of the file
    322 # are taken from standard input. Each line is passed
    323 # on the command line to the "processor" command.
    324 # Command line arguments can be passed to the processor.
    325 #
    326 # Examples:
    327 #	inet_process_hostname /sbin/ifconfig hme0 < /etc/hostname.hme0
    328 #	
    329 #	inet_process_hostname /sbin/ifparse -f < /etc/hostname.hme0
    330 #
    331 # If there is only line in an hostname file we assume it contains
    332 # the old style address which results in the interface being brought up 
    333 # and the netmask and broadcast address being set ($inet_oneline_epilogue).
    334 #
    335 # Note that if the interface is a VRRP interface, do not bring the address
    336 # up ($inet_oneline_epilogue_no_up).
    337 #
    338 # If there are multiple lines we assume the file contains a list of
    339 # commands to the processor with neither the implied bringing up of the
    340 # interface nor the setting of the default netmask and broadcast address.
    341 #
    342 # Return non-zero if any command fails so that the caller may alert
    343 # users to errors in the configuration.
    344 #
    345 inet_oneline_epilogue_no_up="netmask + broadcast +"
    346 inet_oneline_epilogue="netmask + broadcast + up"
    347 
    348 inet_process_hostname()
    349 {
    350 	if doDHCPhostname $2; then
    351 		:
    352 	else
    353 		#
    354 		# Redirecting input from a file results in a sub-shell being
    355 		# used, hence this outer loop surrounding the "multiple_lines"
    356 		# and "ifcmds" variables.
    357 		#
    358 		while :; do
    359 			multiple_lines=false
    360 			ifcmds=""
    361 			retval=0
    362 
    363 			while read one rest; do
    364 				if [ -n "$ifcmds" ]; then
    365 					#
    366 					# This handles the first N-1
    367 					# lines of a N-line hostname file.
    368 					#
    369 					$* $ifcmds || retval=$?
    370 					multiple_lines=true
    371 				fi
    372 
    373 				#
    374 				# Strip out the "ipmp" keyword if it's the
    375 				# first token, since it's used to control
    376 				# interface creation, not configuration.
    377 				#
    378 				[ "$one" = ipmp ] && one=
    379 				ifcmds="$one $rest"
    380 			done
    381 
    382 			#
    383 			# If the hostname file is empty or consists of only
    384 			# blank lines, break out of the outer loop without
    385 			# configuring the newly plumbed interface.
    386 			#
    387 			[ -z "$ifcmds" ] && return $retval
    388 			if [ $multiple_lines = false ]; then
    389 				#
    390 				# The traditional one-line hostname file.
    391 				# Note that we only bring it up if the
    392 				# interface is not a VRRP VNIC.
    393 				#
    394 				if not_vrrp_interface $2 $3; then
    395 					estr="$inet_oneline_epilogue"
    396 				else
    397 					estr="$inet_oneline_epilogue_no_up"
    398 				fi
    399 				ifcmds="$ifcmds $estr"
    400 			fi
    401 
    402 			#
    403 			# This handles either the single-line case or
    404 			# the last line of the N-line case.
    405 			#
    406 			$* $ifcmds || return $?
    407 			return $retval
    408 		done
    409 	fi
    410 }
    411 
    412 #
    413 # inet6_process_hostname processor [ args ]
    414 #
    415 # Process an inet6 hostname file.  The contents of the file
    416 # are taken from standard input. Each line is passed
    417 # on the command line to the "processor" command.
    418 # Command line arguments can be passed to the processor.
    419 #
    420 # Examples:
    421 #	inet6_process_hostname /sbin/ifconfig hme0 inet6 < /etc/hostname6.hme0
    422 #	
    423 #	inet6_process_hostname /sbin/ifparse -f inet6 < /etc/hostname6.hme0
    424 #
    425 # Return non-zero if any of the commands fail so that the caller may alert
    426 # users to errors in the configuration.
    427 #
    428 inet6_process_hostname()
    429 {
    430     	retval=0
    431 	while read one rest; do
    432 		#
    433 	    	# See comment in inet_process_hostname for details.
    434 	        #
    435 		[ "$one" = ipmp ] && one=
    436 		ifcmds="$one $rest"
    437 
    438 		if [ -n "$ifcmds" ]; then
    439 			$* $ifcmds || retval=$?
    440 		fi
    441 	done
    442 	return $retval
    443 }
    444 
    445 #
    446 # Process interfaces that failed to plumb.  Find the IPMP meta-interface
    447 # that should host the addresses.  For IPv6, only static addresses defined
    448 # in hostname6 files are moved, autoconfigured addresses are not moved.
    449 #
    450 # Example:
    451 #	move_addresses inet6
    452 #
    453 move_addresses()
    454 {
    455 	type="$1"
    456 	eval "failed=\"\$${type}_failed\""
    457 	eval "list=\"\$${type}_list\""
    458 	process_func="${type}_process_hostname"
    459 	processed=""
    460 
    461 	if [ "$type" = inet ]; then
    462 	        typedesc="IPv4"
    463 		zaddr="0.0.0.0"
    464 		hostpfx="/etc/hostname"
    465 	else
    466 	        typedesc="IPv6"
    467 		zaddr="::"
    468 		hostpfx="/etc/hostname6"
    469 	fi
    470 
    471 	echo "Moving addresses from missing ${typedesc} interface(s):\c" \
    472 	    >/dev/msglog
    473 
    474 	for ifname in $failed; do
    475 		in_list if_comp $ifname $processed && continue
    476 
    477 		group=`get_group $ifname`
    478 		if [ -z "$group" ]; then
    479 			in_list physical_comp $ifname $processed || { 
    480 				echo " $ifname (not moved -- not" \
    481 				    "in an IPMP group)\c" >/dev/msglog
    482 				processed="$processed $ifname"
    483 			}
    484 			continue
    485 		fi
    486 
    487 		#
    488 		# Lookup the IPMP meta-interface name.  If one doesn't exist,
    489 		# create it.
    490 		#
    491 		grifname=`get_groupifname $group`
    492 		[ -z "$grifname" ] && grifname=`create_groupifname $group $type`
    493 
    494 		#
    495 		# The hostname files are processed twice.  In the first
    496 		# pass, we are looking for all commands that apply to the
    497 		# non-additional interface address.  These may be
    498 		# scattered over several files.  We won't know whether the
    499 		# address represents a failover address or not until we've
    500 		# read all the files associated with the interface.
    501 		#
    502 		# In the first pass through the hostname files, all
    503 		# additional logical interface commands are removed.  The
    504 		# remaining commands are concatenated together and passed
    505 		# to ifparse to determine whether the non-additional
    506 		# logical interface address is a failover address.  If it
    507 		# as a failover address, the address may not be the first
    508 		# item on the line, so we can't just substitute "addif"
    509 		# for "set".  We prepend an "addif $zaddr" command, and
    510 		# let the embedded "set" command set the address later.
    511 		#
    512 		/sbin/ifparse -f $type `
    513 			for item in $list; do
    514 				if_comp $ifname $item && $process_func \
    515 				    /sbin/ifparse $type < $hostpfx.$item 
    516 			done | while read three four; do
    517 				[ "$three" != addif ] && echo "$three $four \c"
    518 			done` | while read one two; do
    519 				[ -z "$one" ] && continue
    520 				[ "$one $two" = "$inet_oneline_epilogue" ] && \
    521 				    continue
    522 				line="addif $zaddr $one $two"
    523 				/sbin/ifconfig $grifname $type $line >/dev/null
    524 			done
    525 
    526 		#
    527 		# In the second pass, look for the the "addif" commands
    528 		# that configure additional failover addresses.  Addif
    529 		# commands are not valid in logical interface hostname
    530 		# files.
    531 		#
    532 		if [ "$ifname" = "`get_physical $ifname`" ]; then
    533 			$process_func /sbin/ifparse -f $type < $hostpfx.$ifname \
    534 			| while read one two; do
    535 				[ "$one" = addif ] && \
    536 					/sbin/ifconfig $grifname $type \
    537 				    	    addif $two >/dev/null
    538 			done
    539 		fi
    540 
    541 		in_list physical_comp $ifname $processed || { 
    542 			processed="$processed $ifname"
    543 			echo " $ifname (moved to $grifname)\c" > /dev/msglog
    544 		}
    545 	done
    546 	echo "." >/dev/msglog
    547 }
    548 
    549 #
    550 # if_configure type class interface_list
    551 #
    552 # Configure all of the interfaces of type `type' (e.g., "inet6") in
    553 # `interface_list' according to their /etc/hostname[6].* files.  `class'
    554 # describes the class of interface (e.g., "IPMP"), as a diagnostic aid.
    555 # For inet6 interfaces, the interface is also brought up.
    556 #
    557 if_configure()
    558 {
    559 	fail=
    560 	type=$1
    561 	class=$2
    562 	process_func=${type}_process_hostname
    563 	shift 2
    564 
    565 	if [ "$type" = inet ]; then
    566 	        desc="IPv4"
    567 		hostpfx="/etc/hostname"
    568 	else
    569 	        desc="IPv6"
    570 		hostpfx="/etc/hostname6"
    571 	fi
    572 	[ -n "$class" ] && desc="$class $desc"
    573 
    574 	echo "configuring $desc interfaces:\c"
    575 	while [ $# -gt 0 ]; do
    576 		$process_func /sbin/ifconfig $1 $type < $hostpfx.$1 >/dev/null
    577 		if [ $? != 0 ]; then
    578 			fail="$fail $1"
    579 		elif [ "$type" = inet6 ]; then
    580 			#
    581 			# only bring the interface up if it is not a
    582 			# VRRP VNIC
    583 			#
    584 			if not_vrrp_interface $1 $type; then
    585 			    	/sbin/ifconfig $1 inet6 up || fail="$fail $1"
    586 			fi
    587 		fi
    588 		echo " $1\c"
    589 		shift
    590 	done
    591 	echo "."
    592 
    593 	[ -n "$fail" ] && warn_failed_ifs "configure $desc" "$fail"
    594 }
    595 
    596 #
    597 # net_reconfigure is called from the network/physical service (by the
    598 # net-physical and net-nwam method scripts) to perform tasks that only
    599 # need to be done during a reconfigure boot.  This needs to be
    600 # isolated in a function since network/physical has two instances
    601 # (default and nwam) that have distinct method scripts that each need
    602 # to do these things.
    603 #
    604 net_reconfigure ()
    605 {
    606 	#
    607 	# Is this a reconfigure boot?  If not, then there's nothing
    608 	# for us to do.
    609 	#
    610 	reconfig=`svcprop -c -p system/reconfigure \
    611 	    system/svc/restarter:default 2>/dev/null`
    612 	if [ $? -ne 0 -o "$reconfig" = false ]; then
    613 		return 0
    614 	fi
    615 
    616 	#
    617 	# Ensure that the datalink-management service is running since
    618 	# manifest-import has not yet run for a first boot after
    619 	# upgrade.  We wouldn't need to do that if manifest-import ran
    620 	# earlier in boot, since there is an explicit dependency
    621 	# between datalink-management and network/physical.
    622 	#
    623 	svcadm enable -ts network/datalink-management:default
    624 
    625 	#
    626 	# There is a bug in SMF which causes the svcadm command above
    627 	# to exit prematurely (with an error code of 3) before having
    628 	# waited for the service to come online after having enabled
    629 	# it.  Until that bug is fixed, we need to have the following
    630 	# loop to explicitly wait for the service to come online.
    631 	#
    632 	i=0
    633 	while [ $i -lt 30 ]; do
    634 		i=`expr $i + 1`
    635 		sleep 1
    636 		state=`svcprop -p restarter/state \
    637 		    network/datalink-management:default 2>/dev/null`
    638 		if [ $? -ne 0 ]; then
    639 			continue
    640 		elif [ "$state" = "online" ]; then
    641 			break
    642 		fi
    643 	done
    644 	if [ "$state" != "online" ]; then
    645 		echo "The network/datalink-management service \c"
    646 		echo "did not come online."
    647 		return 1
    648 	fi
    649 
    650 	#
    651 	# Initialize the set of physical links, and validate and
    652 	# remove all the physical links which were removed during the
    653 	# system shutdown.
    654 	#
    655 	/sbin/dladm init-phys
    656 	return 0
    657 }
    658 
    659 #
    660 # Check for use of the default "Port VLAN Identifier" (PVID) -- VLAN 1.
    661 # If there is one for a given interface, then warn the user and force the
    662 # PVID to zero (if it's not already set).  We do this by generating a list
    663 # of interfaces with VLAN 1 in use first, and then parsing out the
    664 # corresponding base datalink entries to check for ones without a
    665 # "default_tag" property.
    666 #
    667 update_pvid()
    668 {
    669 	datalink=/etc/dladm/datalink.conf
    670 
    671 	(
    672 		# Find datalinks using VLAN 1 explicitly
    673 		# configured by dladm
    674 		/usr/bin/nawk '
    675 			/^#/ || NF < 2 { next }
    676 			{ linkdata[$1]=$2; }
    677 			/;vid=int,1;/ {
    678 				sub(/.*;linkover=int,/, "", $2);
    679 				sub(/;.*/, "", $2);
    680 				link=linkdata[$2];
    681 				sub(/name=string,/, "", link);
    682 				sub(/;.*/, "", link);
    683 				print link;
    684 			}' $datalink
    685 	) | ( /usr/bin/sort -u; echo END; cat $datalink ) | /usr/bin/nawk '
    686 	    /^END$/ { state=1; }
    687 	    state == 0 { usingpvid[++nusingpvid]=$1; next; }
    688 	    /^#/ || NF < 2 { next; }
    689 	    {
    690 		# If it is already present and has a tag set,
    691 		# then believe it.
    692 		if (!match($2, /;default_tag=/))
    693 			next;
    694 		sub(/name=string,/, "", $2);
    695 		sub(/;.*/, "", $2);
    696 		for (i = 1; i <= nusingpvid; i++) {
    697 			if (usingpvid[i] == $2)
    698 				usingpvid[i]="";
    699 		}
    700 	    }
    701 	    END {
    702 		for (i = 1; i <= nusingpvid; i++) {
    703 			if (usingpvid[i] != "") {
    704 				printf("Warning: default VLAN tag set to 0" \
    705 				    " on %s\n", usingpvid[i]);
    706 				cmd=sprintf("dladm set-linkprop -p " \
    707 				    "default_tag=0 %s\n", usingpvid[i]);
    708 				system(cmd);
    709 			}
    710 		}
    711 	    }'
    712 }
    713