1#!/bin/sh 2#- 3# Copyright (c) 2016-2024 Devin Teske <dteske@FreeBSD.org> 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 1. Redistributions of source code must retain the above copyright 10# notice, this list of conditions and the following disclaimer. 11# 2. Redistributions in binary form must reproduce the above copyright 12# notice, this list of conditions and the following disclaimer in the 13# documentation and/or other materials provided with the distribution. 14# 15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25# SUCH DAMAGE. 26# 27############################################################ IDENT(1) 28# 29# $Title: netgraph(4) management script for vnet jails $ 30# $Version: 2.0 $ 31# 32############################################################ INFORMATION 33# 34# Use this tool with jail.conf(5) (or rc.conf(5) ``legacy'' configuration) to 35# manage `vnet' interfaces for jails. Designed to automate the creation of vnet 36# interface(s) during jail `prestart' and destroy said interface(s) during jail 37# `poststop'. 38# 39# In jail.conf(5) format: 40# 41# ### BEGIN EXCERPT ### 42# 43# xxx { 44# host.hostname = "xxx.yyy"; 45# path = "/vm/xxx"; 46# 47# # 48# # NB: Below 2-lines required 49# # NB: The number of ngN_xxx interfaces should match the number of 50# # arguments given to `jng bridge xxx' in exec.prestart value. 51# # 52# vnet; 53# vnet.interface = ng0_xxx, ng1_xxx, ...; 54# 55# exec.clean; 56# exec.system_user = "root"; 57# exec.jail_user = "root"; 58# 59# # 60# # NB: Below 2-lines required 61# # NB: The number of arguments after `jng bridge xxx' should match 62# # the number of ngN_xxx arguments in vnet.interface value. 63# # 64# exec.prestart += "jng bridge xxx em0 em1 ..."; 65# exec.poststop += "jng shutdown xxx"; 66# 67# # Standard recipe 68# exec.start += "/bin/sh /etc/rc"; 69# exec.stop = "/bin/sh /etc/rc.shutdown jail"; 70# exec.consolelog = "/var/log/jail_xxx_console.log"; 71# mount.devfs; 72# 73# # Optional (default off) 74# #allow.mount; 75# #allow.set_hostname = 1; 76# #allow.sysvipc = 1; 77# #devfs_ruleset = "11"; # rule to unhide bpf for DHCP 78# } 79# 80# ### END EXCERPT ### 81# 82# In rc.conf(5) ``legacy'' format (used when /etc/jail.conf does not exist): 83# 84# ### BEGIN EXCERPT ### 85# 86# jail_enable="YES" 87# jail_list="xxx" 88# 89# # 90# # Global presets for all jails 91# # 92# jail_devfs_enable="YES" # mount devfs 93# 94# # 95# # Global options (default off) 96# # 97# #jail_mount_enable="YES" # mount /etc/fstab.{name} 98# #jail_set_hostname_allow="YES" # Allow hostname to change 99# #jail_sysvipc_allow="YES" # Allow SysV Interprocess Comm. 100# 101# # xxx 102# jail_xxx_hostname="xxx.shxd.cx" # hostname 103# jail_xxx_rootdir="/vm/xxx" # root directory 104# jail_xxx_vnet_interfaces="ng0_xxx ng1xxx ..." # vnet interface(s) 105# jail_xxx_exec_prestart0="jng bridge xxx em0 em1 ..." # bridge interface(s) 106# jail_xxx_exec_poststop0="jng shutdown xxx" # destroy interface(s) 107# #jail_xxx_mount_enable="YES" # mount /etc/fstab.xxx 108# #jail_xxx_devfs_ruleset="11" # rule to unhide bpf for DHCP 109# 110# ### END EXCERPT ### 111# 112# Note that the legacy rc.conf(5) format is converted to 113# /var/run/jail.{name}.conf by /etc/rc.d/jail if jail.conf(5) is missing. 114# 115# ASIDE: dhclient(8) inside a vnet jail... 116# 117# To allow dhclient(8) to work inside a vnet jail, make sure the following 118# appears in /etc/devfs.rules (which should be created if it doesn't exist): 119# 120# [devfsrules_jail=11] 121# add include $devfsrules_hide_all 122# add include $devfsrules_unhide_basic 123# add include $devfsrules_unhide_login 124# add path 'bpf*' unhide 125# 126# And set ether devfs.ruleset="11" (jail.conf(5)) or 127# jail_{name}_devfs_ruleset="11" (rc.conf(5)). 128# 129# NB: While this tool can't create every type of desirable topology, it should 130# handle most setups, minus some which considered exotic or purpose-built. 131# 132############################################################ CONFIGURATION 133 134# 135# Netgraph node type. Can be `iface' or `eiface' and refers to whether 136# ng_iface(4) or ng_eiface(4) is used with ng_bridge(4). The advantages of 137# choosing iface over eiface is that with iface you can utilize ng_tcpmss(4) 138# to limit the TCP MSS for operating in environments that clamp down on ICMP. 139# 140# NB: iface/tcpmss support is EXPERIMENTAL 141# 142NG_TYPE=eiface # Can be iface or eiface 143 144# 145# Clamp TCP Maximum Segment Size to reasonably below standard MTU 146# NB: Fixes TCP hangup issue in environments where ICMP is restricted 147# NB: Be liberal about MSS (RFC 879, section 7) 148# NB: Unused unless NG_TYPE=iface 149# 150NG_TCPMSS_CONFIG='{ inHook="bridge" outHook="'$NG_TYPE'" maxMSS=1280 }' 151 152############################################################ GLOBALS 153 154pgm="${0##*/}" # Program basename 155 156# 157# Global exit status 158# 159SUCCESS=0 160FAILURE=1 161 162# 163# Command-line options 164# 165STATS_FMT=text # -j for JSON 166 167############################################################ FUNCTIONS 168 169quietly(){ "$@" > /dev/null 2>&1; } 170 171usage() 172{ 173 local fmt="$1" 174 local action usage descr 175 exec >&2 176 if [ "$fmt" ]; then 177 shift 1 # fmt 178 printf "%s: $fmt\n" "$pgm" "$@" 179 fi 180 echo "Usage: $pgm [-h] action [arguments]" 181 echo "Options:" 182 printf "\t-h Print usage statement and exit.\n" 183 echo "Actions:" 184 for action in \ 185 bridge \ 186 graph \ 187 show \ 188 show1 \ 189 shutdown \ 190 stats \ 191 ; do 192 eval usage=\"\$jng_${action}_usage\" 193 [ "$usage" ] || continue 194 eval descr=\"\$jng_${action}_descr\" 195 printf "\t%s\n\t\t%s\n" "$usage" "$descr" 196 done 197 exit $FAILURE 198} 199 200action_usage() 201{ 202 local usage descr action="$1" fmt="$2" 203 shift 1 # action 204 if [ "$fmt" ]; then 205 shift 1 # fmt 206 printf "%s: %s: $fmt\n" "$pgm" "$action" "$@" >&2 207 fi 208 eval usage=\"\$jng_${action}_usage\" 209 echo "Usage: $pgm $usage" >&2 210 eval descr=\"\$jng_${action}_descr\" 211 printf "\t%s\n" "$descr" 212 exit $FAILURE 213} 214 215derive_mac() 216{ 217 local OPTIND=1 OPTARG __flag 218 local __mac_num= __make_pair= 219 while getopts 2n: __flag; do 220 case "$__flag" in 221 2) __make_pair=1 ;; 222 n) __mac_num=${OPTARG%%[^0-9]*} ;; 223 esac 224 done 225 shift $(( $OPTIND - 1 )) 226 227 if [ ! "$__mac_num" ]; then 228 eval __mac_num=\${_${iface}_num:--1} 229 __mac_num=$(( $__mac_num + 1 )) 230 eval _${iface}_num=\$__mac_num 231 fi 232 233 local __iface="$1" __name="$2" __var_to_set="$3" __var_to_set_b="$4" 234 local __iface_devid __new_devid __num __new_devid_b 235 # 236 # Calculate MAC address derived from given iface. 237 # 238 # The formula I'm using is ``NP:SS:SS:II:II:II'' where: 239 # + N denotes 4 bits used as a counter to support branching 240 # each parent interface up to 15 times under the same jail 241 # name (see S below). 242 # + P denotes the special nibble whose value, if one of 243 # 2, 6, A, or E (but usually 2) denotes a privately 244 # administered MAC address (while remaining routable). 245 # + S denotes 16 bits, the sum(1) value of the jail name. 246 # + I denotes bits that are inherited from parent interface. 247 # 248 # The S bits are a CRC-16 checksum of NAME, allowing the jail 249 # to change link numbers in ng_bridge(4) without affecting the 250 # MAC address. Meanwhile, if... 251 # + the jail NAME changes (e.g., it was duplicated and given 252 # a new name with no other changes) 253 # + the underlying network interface changes 254 # + the jail is moved to another host 255 # the MAC address will be recalculated to a new, similarly 256 # unique value preventing conflict. 257 # 258 __iface_devid=$( ifconfig $__iface ether | awk '/ether/,$0=$2' ) 259 # ??:??:??:II:II:II 260 __new_devid=${__iface_devid#??:??:??} # => :II:II:II 261 # => :SS:SS:II:II:II 262 __num=$( set -- `echo -n "$__name" | sum` && echo $1 ) 263 __new_devid=$( printf :%02x:%02x \ 264 $(( $__num >> 8 & 255 )) $(( $__num & 255 )) )$__new_devid 265 # => P:SS:SS:II:II:II 266 case "$__iface_devid" in 267 ?2:*) __new_devid=a$__new_devid __new_devid_b=e$__new_devid ;; 268 ?[Ee]:*) __new_devid=2$__new_devid __new_devid_b=6$__new_devid ;; 269 *) __new_devid=2$__new_devid __new_devid_b=e$__new_devid 270 esac 271 # => NP:SS:SS:II:II:II 272 __new_devid=$( printf %x $(( $__mac_num & 15 )) )$__new_devid 273 __new_devid_b=$( printf %x $(( $__mac_num & 15 )) )$__new_devid_b 274 275 # 276 # Return derivative MAC address(es) 277 # 278 if [ "$__make_pair" ]; then 279 if [ "$__var_to_set" -a "$__var_to_set_b" ]; then 280 eval $__var_to_set=\$__new_devid 281 eval $__var_to_set_b=\$__new_devid_b 282 else 283 echo $__new_devid $__new_devid_b 284 fi 285 else 286 if [ "$__var_to_set" ]; then 287 eval $__var_to_set=\$__new_devid 288 else 289 echo $__new_devid 290 fi 291 fi 292} 293 294mustberoot_to_continue() 295{ 296 if [ "$( id -u )" -ne 0 ]; then 297 echo "Must run as root!" >&2 298 exit $FAILURE 299 fi 300} 301 302jng_bridge_usage="bridge [-h] [-b BRIDGE_NAME] NAME [!|=]iface0 [[!|=]iface1 ...]" 303jng_bridge_descr="Create ng0_NAME [ng1_NAME ...]" 304jng_bridge() 305{ 306 local OPTIND=1 OPTARG flag bridge=bridge 307 while getopts b:h flag; do 308 case "$flag" in 309 b) bridge="$OPTARG" 310 [ "$bridge" ] || 311 action_usage bridge "-b argument cannot be empty" 312 ;; # NOTREACHED 313 *) action_usage bridge # NOTREACHED 314 esac 315 done 316 shift $(( $OPTIND - 1 )) 317 318 [ $# -gt 0 ] || action_usage bridge "too few arguments" # NOTREACHED 319 320 local name="$1" 321 [ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] || 322 action_usage bridge "invalid bridge name: %s" "$name" 323 # NOTREACHED 324 shift 1 # name 325 326 mustberoot_to_continue 327 328 local iface parent jiface jiface_devid 329 local new clone_mac no_derive num quad i=0 330 for iface in $*; do 331 332 clone_mac= 333 no_derive= 334 case "$iface" in 335 =*) iface=${iface#=} clone_mac=1 ;; 336 !*) iface=${iface#!} no_derive=1 ;; 337 esac 338 339 # Make sure the interface doesn't exist already 340 jiface=ng${i}_$name 341 if quietly ngctl msg "$jiface:" getifname; then 342 i=$(( $i + 1 )) 343 continue 344 fi 345 346 # Bring the interface up 347 ifconfig $iface up || return 348 349 # Set promiscuous mode and don't overwrite src addr 350 ngctl msg $iface: setpromisc 1 || return 351 ngctl msg $iface: setautosrc 0 || return 352 353 # Make sure the interface has been bridged 354 if ! quietly ngctl info ${iface}bridge:; then 355 ngctl mkpeer $iface: bridge lower link0 || return 356 ngctl connect $iface: $iface:lower upper link1 || 357 return 358 ngctl name $iface:lower ${iface}bridge || return 359 fi 360 361 mtu=$(ifconfig ${iface} | sed -n '1s/^.*mtu //p;') || return 362 363 # Optionally create a secondary bridge 364 if [ "$bridge" != "bridge" ] && 365 ! quietly ngctl info "$iface$bridge:" 366 then 367 num=2 368 while quietly ngctl msg ${iface}bridge: getstats $num 369 do 370 num=$(( $num + 1 )) 371 done 372 ngctl mkpeer $iface:lower bridge link$num link1 || 373 return 374 ngctl name ${iface}bridge:link$num "$iface$bridge" || 375 return 376 fi 377 378 # Create a new interface to the bridge 379 num=2 380 while quietly ngctl msg "$iface$bridge:" getstats $num; do 381 num=$(( $num + 1 )) 382 done 383 local hook peerhook 384 case "$NG_TYPE" in 385 eiface) 386 # Hook the eiface directly to the bridge 387 hook=link$num peerhook=ether 388 ngctl mkpeer "$iface$bridge:" \ 389 $NG_TYPE $hook $peerhook || return 390 ;; 391 iface) 392 # Hook tcpmss<->iface to bridge 393 hook=link$num peerhook=bridge 394 ngctl mkpeer "$iface$bridge:" \ 395 tcpmss $hook $peerhook || return 396 hook=iface peerhook=inet 397 ngctl mkpeer "$iface$bridge:link$num" \ 398 $NG_TYPE $hook $peerhook || return 399 ;; 400 *) return $FAILURE 401 esac 402 403 # Rename the new interface 404 while [ ${#jiface} -gt 15 ]; do # OS limitation 405 jiface=${jiface%?} 406 done 407 case "$NG_TYPE" in 408 eiface) 409 new=$( ngctl show -n "$iface$bridge:link$num" ) || 410 return 411 new=$( set -- $new; echo $2 ) 412 ngctl name "$iface$bridge:link$num" $jiface || return 413 ;; 414 iface) 415 ngctl name "$iface$bridge:link$num" $jiface-mss || 416 return 417 new=$( ngctl show -n "$jiface-mss:$hook" ) || return 418 new=$( set -- $new; echo $2 ) 419 ngctl name $jiface-mss:$hook $jiface || return 420 ngctl msg $jiface: broadcast || return 421 ngctl msg $jiface-mss: config "$NG_TCPMSS_CONFIG" || 422 return 423 ;; 424 esac 425 ifconfig $new name $jiface || return 426 ifconfig $jiface mtu $mtu || return 427 ifconfig $jiface up || return 428 429 # 430 # Set the MAC address of the new interface using a sensible 431 # algorithm to prevent conflicts on the network. 432 # 433 jiface_devid= 434 if [ "$clone_mac" ]; then 435 jiface_devid=$( ifconfig $iface ether | 436 awk '/ether/,$0=$2' ) 437 elif [ ! "$no_derive" ]; then 438 derive_mac $iface "$name" jiface_devid 439 fi 440 [ "$jiface_devid" ] && 441 quietly ifconfig $jiface ether $jiface_devid 442 443 i=$(( $i + 1 )) 444 done # for iface 445} 446 447jng_graph_usage="graph [-fh] [-T type] [-o output]" 448jng_graph_descr="Generate network graph (default output is \`jng.svg')" 449jng_graph() 450{ 451 local OPTIND=1 OPTARG flag 452 local output=jng.svg output_type= force= 453 while getopts fho:T: flag; do 454 case "$flag" in 455 f) force=1 ;; 456 o) output="$OPTARG" ;; 457 T) output_type="$OPTARG" ;; 458 *) action_usage graph # NOTREACHED 459 esac 460 done 461 shift $(( $OPTIND - 1 )) 462 463 [ $# -eq 0 ] || action_usage graph "too many arguments" # NOTREACHED 464 465 mustberoot_to_continue 466 467 if [ -e "$output" -a ! "$force" ]; then 468 echo "$output: Already exists (use \`-f' to overwrite)" >&2 469 return $FAILURE 470 fi 471 if [ ! "$output_type" ]; then 472 local valid suffix 473 valid=$( dot -Txxx 2>&1 ) 474 for suffix in ${valid##*:}; do 475 [ "$output" != "${output%.$suffix}" ] || continue 476 output_type=$suffix 477 break 478 done 479 fi 480 ngctl dot | dot ${output_type:+-T "$output_type"} -o "$output" 481} 482 483jng_show_usage="show [-h]" 484jng_show_descr="List possible NAME values for \`show NAME'" 485jng_show1_usage="show [-h] NAME ..." 486jng_show1_descr="Lists ng0_NAME [ng1_NAME ...]" 487jng_show2_usage="show [NAME ...]" 488jng_show2_descr="List NAME values or show interfaces associated with NAME." 489jng_show() 490{ 491 local OPTIND=1 OPTARG flag 492 local name 493 while getopts h flag; do 494 case "$flag" in 495 *) action_usage show2 # NOTREACHED 496 esac 497 done 498 shift $(( $OPTIND - 1 )) 499 500 mustberoot_to_continue 501 502 if [ $# -eq 0 ]; then 503 ngctl ls | awk '$4=="bridge",$0=$2' | 504 xargs -rn1 -Ibridge ngctl show bridge: | 505 awk 'sub(/^ng[[:digit:]]+_/, "", $2), $0 = $2' | 506 sort -u 507 return 508 fi 509 for name in "$@"; do 510 ngctl ls | awk -v name="$name" ' 511 BEGIN { N = length(name) + 1 } 512 !match(ng = $2, /^ng[[:digit:]]+_/) { next } 513 { _name = substr(ng, S = RSTART + RLENGTH) } 514 _name != name && substr(_name, 1, N) != name "-" { next } 515 (type = $4) ~ /^(e?iface|tcpmss)$/, $0 = ng 516 ' | sort 517 done 518} 519 520jng_shutdown_usage="shutdown [-h] NAME ..." 521jng_shutdown_descr="Shutdown ng0_NAME [ng1_NAME ...]" 522jng_shutdown() 523{ 524 local OPTIND=1 OPTARG flag 525 while getopts h flag; do 526 case "$flag" in 527 *) action_usage shutdown # NOTREACHED 528 esac 529 done 530 shift $(( $OPTIND -1 )) 531 532 [ $# -gt 0 ] || action_usage shutdown "too few arguments" # NOTREACHED 533 534 mustberoot_to_continue 535 536 local name 537 for name in "$@"; do 538 [ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] || 539 action_usage shutdown "invalid name: %s" "$name" 540 # NOTREACHED 541 jng_show "$name" | xargs -rn1 -I jiface ngctl shutdown jiface: 542 done 543} 544 545jng_stats_usage="stats [-hj] {-a | NAME ...}" 546jng_stats_descr="Show ng_bridge link statistics for NAME interfaces" 547jng_stats() 548{ 549 local OPTIND=1 OPTARG flag 550 local show_all= 551 local name iface ether= 552 while getopts ahj flag; do 553 case "$flag" in 554 a) show_all=1 ;; 555 j) STATS_FMT=json 556 export pgm 557 : "${HOSTNAME:=$( hostname )}" 558 export HOSTNAME 559 ;; 560 *) action_usage stats # NOTREACHED 561 esac 562 done 563 shift $(( $OPTIND -1 )) 564 if [ "$show_all" ]; then 565 [ $# -eq 0 ] || 566 action_usage stats "too many arguments" # NOTREACHED 567 568 # Get a list of bridged ng_ether(4) devices 569 for iface in $( ifconfig -l ); do 570 quietly ngctl info ${iface}bridge: || continue 571 ether="$ether $iface" 572 done 573 set -- $ether $( "$0" show ) 574 [ $# -gt 0 ] || 575 action_usage stats "no bridged interfaces" # NOTREACHED 576 else 577 [ $# -gt 0 ] || 578 action_usage stats "too few arguments" # NOTREACHED 579 fi 580 581 mustberoot_to_continue 582 583 local now="$( date +%s )" 584 for name in "$@"; do 585 [ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] || 586 action_usage stats "invalid name: %s" "$name" 587 # NOTREACHED 588 if ifconfig -l | xargs -n1 2> /dev/null | fgrep -qw "$name" 589 then 590 [ "$STATS_FMT" != "text" ] || 591 echo "${name}bridge:link0 [lower]" 592 ngctl msg ${name}bridge: getstats 0 | 593 fmt_stats -n "${name}.lower" -t "$now" 594 595 [ "$STATS_FMT" != "text" ] || 596 echo "${name}bridge:link0 [lower]" 597 ngctl msg ${name}bridge: getstats 1 | 598 fmt_stats -n "${name}.upper" -t "$now" 599 fi 600 local jiface 601 for jiface in $( jng_show "$name" ); do 602 [ "$STATS_FMT" != "text" ] || echo "$jiface:" 603 ngctl show $jiface: | awk ' 604 $3 == "bridge" && $5 ~ /^link/ { 605 bridge = $2 606 link = substr($5, 5) 607 system(sprintf("ngctl msg %s: getstats %u", 608 bridge, link)) 609 }' | fmt_stats -n "$jiface" -t "$now" 610 done 611 done 612} 613fmt_stats() 614{ 615 local OPTIND=1 OPTARG flag 616 local time= 617 while getopts n:t: flag; do 618 case "$flag" in 619 n) name="$OPTARG" ;; 620 t) time="$OPTARG" ;; 621 *) break 622 esac 623 done 624 shift $(( OPTIND - 1 )) 625 fmt 2 | awk -v fmt="$STATS_FMT" -v name="$name" -v tm="$time" ' 626 function json_add_str(pre, k, s) 627 { 628 return sprintf("%s,\"%s\":\"%s\"", pre, k, s) 629 } 630 function json_add_int(pre, k, i) 631 { 632 return sprintf("%s,\"%s\":%d", pre, k, i) 633 } 634 BEGIN { 635 if (fmt == "json") { 636 if (tm == "") srand() # Time-seed 637 js = json_add_int(js, "epoch", 638 tm != "" ? tm : srand()) 639 js = json_add_str(js, "hostname", 640 ENVIRON["HOSTNAME"]) 641 js = json_add_str(js, "program", 642 ENVIRON["pgm"]) 643 js = json_add_str(js, "name", name) 644 } 645 } 646 /=/ && fl = index($0, "=") { 647 key = substr($0, 0, fl-1) 648 val = substr($0, fl+1) 649 if (fmt == "json") { 650 js = json_add_int(js, key, val) 651 } else { # Multi-line text 652 printf "%20s = %s\n", key, val 653 } 654 } 655 END { 656 if (fmt == "json") { 657 print "{" substr(js, 2) "}" 658 } 659 } 660 ' # END-QUOTE 661} 662 663############################################################ MAIN 664 665# 666# Command-line arguments 667# 668[ $# -gt 0 ] || usage "too few arguments" # NOTREACHED 669action="$1" 670[ "$action" ] || usage # NOTREACHED 671 672# 673# Validate action argument 674# 675if [ "$BASH_VERSION" ]; then 676 type="$( type -t "jng_$action" )" || usage # NOTREACHED 677else 678 type="$( type "jng_$action" 2> /dev/null )" || usage # NOTREACHED 679fi 680case "$type" in 681*function) 682 shift 1 # action 683 eval "jng_$action" \"\$@\" 684 ;; 685*) usage # NOTREACHED 686esac 687 688################################################################################ 689# END 690################################################################################ 691