From e464b651e9bcd91307f74d607ca98439acbcb2ce Mon Sep 17 00:00:00 2001 From: Stephen L Johnson Date: Mon, 25 Sep 2000 20:53:33 +0000 Subject: [PATCH] added program into CVS tree --- src/wap-spong.pl | 819 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 819 insertions(+) create mode 100644 src/wap-spong.pl diff --git a/src/wap-spong.pl b/src/wap-spong.pl new file mode 100644 index 0000000..0efbf7f --- /dev/null +++ b/src/wap-spong.pl @@ -0,0 +1,819 @@ +#@@PERL@@ + +# $Id: wap-spong.pl,v 1.1 2000/09/25 20:53:33 sljohnson Exp $ + +# +# This program is used to display information collected by the spong server to +# people using WAP enabled clients. This provides the same type of interface to +# the data as the text based client does. You can run this program as a CGI +# program to provide an interactive experience, or you can use it from a cron +# job, or some other automated process to just generate static web pages. +# + +# !!!!! This is a very beta program. Not all of the WAP display options +# are present. The old HTML code is present in several cases. + +use Sys::Hostname; +use Getopt::Long; +use Socket; +use POSIX; + +# Load our configuration variables, including the user specified configuration +# information (spong.conf, spong.hosts, and spong.groups files). + +$|++; +$conf_file = "@@ETCDIR@@/spong.conf"; +$hosts_file = "@@ETCDIR@@/spong.hosts"; +$groups_file = "@@ETCDIR@@/spong.groups"; +$SPONGVER = "@@SPONGVER@@"; +($HOST) = gethostbyname(&Sys::Hostname::hostname()); +$HOST =~ tr/A-Z/a-z/; +$view = ""; +$actionbar = 1; +$interactive = 0; + +%HUMANS = (); +%HOSTS = (); +%GROUPS = (); + +&load_config_files(); # Loads the user specified configuration information + +# Check to see if I am being run as a command line program (in which case I +# just generate static WML documents), or a CGI program (in which case I +# present back an interactive WAP interface). The command line arguments are +# the same as the text based spong program, and produce similar results + +if( $ENV{'SCRIPT_NAME'} eq "" ) { + # Get the user options and spit back a message if they do something silly + + my %opt; + my @options = ( "help", "summary:s", "problems:s", "history:s", "host=s", + "services=s", "stats=s", "config=s", "info=s", "service=s", + "histservice=s", "grp-summary:s", "grp-problems", + "brief", "standard", "full" ); + + if( ! GetOptions( \%opt, @options )) {warn "Incorrect usage:\n\n"; &help();} + + &help if defined $opt{'help'}; + + if( defined $opt{'brief'} ) { $view = "brief"; } + if( defined $opt{'standard'} ) { $view = "standard"; } + if( defined $opt{'full'} ) { $view = "full"; } + + if( defined $opt{'problems'} ) { &problems( $opt{'problems'} ); $opt = 1; } + if( defined $opt{'summary'} ) { &summary( $opt{'summary'} ); $opt = 1; } + if( defined $opt{'history'} ) { &history( $opt{'history'} ); $opt = 1; } + + if( defined $opt{'host'} ) { &host( $opt{'host'} ); $opt = 1; } + if( defined $opt{'services'} ) { &services( $opt{'services'} ); $opt = 1; } + if( defined $opt{'stats'} ) { &stats( $opt{'stats'} ); $opt = 1; } + if( defined $opt{'config'} ) { &config( $opt{'config'} ); $opt = 1; } + if( defined $opt{'info'} ) { &info( $opt{'info'} ); $opt = 1; } + + if( defined $opt{'service'} ) { + my( $host, $service ) = split( ':', $opt{'service'} ); + &service( $host, $service ); + $opt = 1; + } + + if ( defined $opt{'histservice'} ) { + my( $host, $service, $time ) = split ( ':', $opt{'histservice'} ); + &histservice( $host, "$service $time" ); + $opt=1; + } + + if ( defined $opt{'grp-summary'} ) { &grp_summary( $opt{'grp-summary'} ); $opt= 1; } + if ( defined $opt{'grp-problems'} ) { &grp_problems( $opt{'grp-problems'} ); $opt= 1; } + + if( ! $opt ) { &summary( "all" ); } + + exit(0); +} + + +# If we make it to this point, then we are a CGI program so pull apart the +# commands given to us via the URL path, and treat them as if they are command +# line options + +$interactive = 1; + +$cmd = $ENV{'PATH_INFO'}; + +# Commands that are more applicable to spong running in interactive mode. + +if( $cmd eq "" || $cmd eq "/" ) { + $view = "brief"; + grp_summary("ALL"); + exit; +} +if ($cmd =~ m!^/title$! ) { &title(); exit; } + +if( $cmd =~ m!^/group/(.*)$! ) { &interactive( $1 ); exit; } +if( $cmd =~ m!^/bygroup/(.*)$! ) { &ovinteractive( $1 ); exit; } +if( $cmd =~ m!^/commands/(.*)$! ) { &commands( $1 ); exit; } + +if( $cmd =~ m!^/ovcommands/(.*)$! ) { &ovcommands( $1 ); exit; } +if( $cmd =~ m!^/igrp-summary/(.*)$! ) { &igrp_summary( $1 ); exit; } +if( $cmd =~ m!^/igrp-overview/(.*)$! ) { &igrp_overview( $1 ); exit; } + +if( $cmd =~ m!^/isummary/(.*)$! ) { &isummary( $1 ); exit; } +if( $cmd =~ m!^/ihistory/(.*)$! ) { &ihistory( $1 ); exit; } + +if( $cmd =~ m!^/groups$! ) { &groups(); exit; } +if( $cmd =~ m!^/groups-doit$! ) { &groups_doit(); exit; } + +if( $cmd =~ m!^/help$! ) { &show( "help" ); exit; } +if( $cmd =~ m!^/help/(.*)$! ) { &show( "$1" ); exit; } + +# Simple commands to just display a specific host attribute, or other specific +# information. These commands can easily be called by pages outside of spong + +if( $cmd =~ m!^/brief/(.*)$! ) { $view = "brief"; $cmd = "/$1"; } +if( $cmd =~ m!^/standard/(.*)$! ) { $view = "standard"; $cmd = "/$1"; } +if( $cmd =~ m!^/full/(.*)$! ) { $view = "full"; $cmd = "/$1"; } + +if( $cmd =~ m!^/problems/(.*)$! ) { &problems($1); exit;} +if( $cmd =~ m!^/summary/(.*)$! ) { &summary($1); exit;} +if( $cmd =~ m!^/history/(.*)$! ) { &history($1); exit;} + +if( $cmd =~ m!^/host/(.*)$! ) { &host($1); exit;} +if( $cmd =~ m!^/services/(.*)$! ) { &services($1); exit;} +if( $cmd =~ m!^/stats/(.*)$! ) { &stats($1); exit;} +if( $cmd =~ m!^/config/(.*)$! ) { &config($1); exit;} +if( $cmd =~ m!^/info/(.*)$! ) { &info($1); exit;} + +if( $cmd =~ m!^/service/(.*)/(.*)$! ) { &service( $1, $2 ); exit; } + +if( $cmd =~ m!^/histservice/(.*)/(.*)/(.*)$! ) { &histservice( $1, "$2 $3" ); exit; } + +if( $cmd =~ m!^/grp-summary/(.*)$! ) { &grp_summary( $1 ); exit;} +if( $cmd =~ m!^/grp-problems/(.*)$! ) { &grp_problems( $1 ); exit;} + +# Need to do something when an invalid request comes through... +exit(0); + + + +# --------------------------------------------------------------------------- +# Functions that support the interactive WWWSPONG client +# --------------------------------------------------------------------------- + +# This sets up the wwwspong interface, it defines the frame that both the +# commands & error summary information is shown, and the frame for more +# detailed host information. + + +sub toplevel { + + print "Content-type: text/html\n\n"; + print "\n"; + print "Spong v$SPONGVER - System Status Monitor\n"; + print ""; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "Frameless version not currently available.\n"; + print "\n\n"; +} + +sub interactive { + my $group = shift; + + print "Content-type: text/html\n\n"; + print "\n"; + print "Spong v$SPONGVER - System Status Monitor\n"; + print ""; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "Frameless version not currently available.\n"; + print "\n\n"; +} + +sub ovinteractive { + my $group = shift; + + print "Content-type: text/html\n\n"; + print "\n"; + print "Spong v$SPONGVER - System Status Monitor\n"; + print ""; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "Frameless version not currently available.\n"; + print "\n\n"; +} + +# This function fills out the Title header. It had a command bar that changes +# the the current View type in the 'view' frame + +sub title { + &header(0); + + my $me = $main::WWWSPONG; + + print "\n"; +# print "
"; + print "Spong v$SPONGVER\n"; +# print "
\n"; +# print "
"; + print "Groups || \n"; + print "Hosts \n"; + +# print "Extra Tool Bar Commands: "; +# print " Spong RRD Charts\n"; +# print " || System Summaries\n"; + + if ( $main::WWW_TITLE_ACTIONBAR ) { print $main::WWW_TITLE_ACTIONBAR,"\n"; } +} + + +# This function fills out the action bar of the interactive spong display. +# This lists the functions that you can perform via the web interface. + +sub commands { + my $group = shift; + my $me = $main::WWWSPONG; + + $group = "all" unless $group; + my $gname = $main::GROUPS{$group}->{'name'} if $main::GROUPS{$group}; + $gname = "Selected Hosts" unless $gname; + + &header( 1 ); + + print "\n"; +# print "Spong v$SPONGVER\n"; + print "Hosts View\n"; + unless ($WWWFRAMES == 3) { + print "
\n"; + print "Views: Groups || \n"; + print "Hosts \n"; + } + print "
\n"; + + print "Ack || \n"; + print "Summary || \n"; + print "History || \n"; + print "Help\n"; + print "
\n

\n"; + + &problems( $group ); + + print "


Group: $gname\n
\n"; + print "Updated at ", POSIX::strftime( "$TIMEFMTNOSEC, on $DATEFMT", localtime() ), "\n"; + &footer(); +} + + +sub ovcommands { + my $group = shift; + my $me = $main::WWWSPONG; + + $group = "all" unless $group; +# my $gname = $main::GROUPS{$group}->{'name'} if $main::GROUPS{$group}; +# $gname = "Selected Hosts" unless $gname; + + &header( 1 ); + + print "\n"; +# print "Spong v$SPONGVER\n"; + print "Groups View\n"; + unless ($WWWFRAMES == 3) { + print "
\n"; + print "Views: Groups || \n"; + print "Hosts \n"; + } + print "
\n"; + + print "Groups || \n"; + print "Group Summary\n"; + print "
\n"; + + print "Ack || \n"; +# print "Summary || \n"; + print "History || \n"; + print "Help\n"; + print "
\n

\n"; + + &grp_problems( $group ); + + print "


"; +# print "Group: $gname\n
\n"; + print "Updated at ", POSIX::strftime( "$TIMEFMTNOSEC, on $DATEFMT", localtime() ), "\n"; + &footer(); +} + + +# A couple of slightly different functions to display summary and history +# information for people using the wwwspong program interactivly. This just +# puts a little header above each output, so that you know what group it +# corresponds to. + +sub isummary { + my $group = shift; + my $gname = $main::GROUPS{$group}->{'name'} if $main::GROUPS{$group}; + $gname = "Selected Hosts" unless $gname; + + &header( 1 ); + print "$gname\n
\n"; + &summary( $group ); + &footer(); +} + +sub ihistory { + my $group = shift; + my $gname = $main::GROUPS{$group}->{'name'} if $main::GROUPS{$group}; + $gname = "Selected Hosts" unless $gname; + + &header( 1 ); + print "$gname\n
\n"; + &history( $group ); + &footer(); +} + +sub igrp_summary { + my $group = shift; +# my $gname = $main::GROUPS{$group}->{'name'} if $main::GROUPS{$group}; +# $gname = "Selected Hosts" unless $gname; + + &header( 1 ); + print "Host Groups\n
\n"; + $main::view = "full"; + &grp_summary($group); + &footer(); +} + +sub igrp_overview { + my $group = shift; +# my $gname = $main::GROUPS{$group}->{'name'} if $main::GROUPS{$group}; +# $gname = "Selected Hosts" unless $gname; + + &header( 1 ); + print "Host Groups Summary\n
\n"; + $view = "standard"; + &grp_summary($group); + &footer(); +} + + + +# This provides a page that lists the groups that are defined in spong, and +# you can select a group to monitor (summary information will then only be +# shown about that group). + +sub groups { + my $group; + + &header( $group, "Groups", '', 0 ); + print "\n"; + print "Spong Groups\n
\n"; + + print "You can select a specific group to show only information about "; + print "those hosts. The groups below have been defined by the spong "; + print "administrator.

\n"; + + print "

    \n"; + foreach $group ( @main::GROUPS_LIST ) { + my $name = $main::GROUPS{$group}->{'name'}; + my $summary = $main::GROUPS{$group}->{'summary'}; + + print "
  • $name "; + print "($group)
    \n$summary

    \n"; + } + print "

\n"; + print "


\n"; + + print "You can also build a custom group to monitor by selecting one or "; + print "hosts from the list below.

\n"; + print "

\n"; + print "
\n"; + print "
\n"; + print "\n"; + print "\n"; + print "\n"; + print "
\n"; + print "\n"; + print "
\n"; + + &footer(); +} + +# The action part of the above form. The hosts are pulled out of the query +# string and passed as a group name to the &interactive() functions. + +sub groups_doit { + my $group = ""; + while( $ENV{'QUERY_STRING'} =~ /hosts=([^\&]+)/isg ) { $group .= "$1,"; } + chop $group; + &interactive( $group ); +} + + +# --------------------------------------------------------------------------- +# Functions that correspond to command line/URL line arguments +# --------------------------------------------------------------------------- + +sub problems { + my $group = shift; + my $view = $main::view || "full"; + + $group = "all" unless $group; + + &header( 1 ); + print &query( $SPONGSERVER, "problems", $group, "html", $view ); + &footer(); +} + +sub summary { + my $group = shift; + my $view = $main::view || "standard"; + + $group = "all" unless $group; + + &header( 1 ); + + print "

\n"; + print "Problems"; + print "
\n"; + print POSIX::strftime($DATETIMEFMT,localtime()),"

\n"; + print "

\n"; + print &query( $SPONGSERVER, "summary", $group, "wml", $view ); + print "

\n"; + &footer(); +} + +sub history { + my $group = shift; + my $view = $main::view || "standard"; + + $group = "all" unless $group; + + &header( 1 ); + print &query( $SPONGSERVER, "history", $group, "html", $view ); + &footer(); +} + + +sub host { + my $host = shift; + my $view = $main::view || "standard"; + + &header( 0 ); + print &query( $SPONGSERVER, "host", $host, "html", $view ); + &footer(); +} + +sub services { + my $host = shift; + my $view = $main::view || "standard"; + + header(1); + + print "

\n"; + print "Summary"; + print "
\n"; + print POSIX::strftime($DATETIMEFMT,localtime()),"

\n"; + print "

\n"; + print " $host

\n"; + print &query( $SPONGSERVER, "services", $host, "wml", $view ); + print "

\n"; + + &footer(); +} + +sub stats { + my $host = shift; + my $view = $main::view || "standard"; + + &header( 0 ); + print &query( $SPONGSERVER, "stats", $host, "html", $view ); + &footer(); +} + +sub config { + my $host = shift; + my $view = $main::view || "standard"; + + &header( 0 ); + print &query( $SPONGSERVER, "config", $host, "html", $view ); + &footer(); +} + +sub info { + my $host = shift; + my $view = $main::view || "standard"; + + &header( 0 ); + print &query( $SPONGSERVER, "info", $host, "html", $view ); + &footer(); +} + + +sub service { + my( $host, $service ) = @_; + my $view = $main::view || "full"; + + header(1); + + print "

\n"; + print "Host"; + print "
\n"; + print POSIX::strftime($DATETIMEFMT,localtime()),"

\n"; + print "

\n"; + print " $host
\n"; + print &query( $SPONGSERVER, "service", $host, "wml", $view, $service ); + print "

\n"; + &footer(); +} + +sub histservice { + my ($host, $service, $time ) = @_; + my $view = $main::view || "full"; + + &header( 0 ); + print &query( $SPONGSERVER, "histservice", $host, "html", $view, $service, + $time ); + &footer(); +} + +sub grp_summary { + my( $other ) = @_; + my $view = $main::view || 'standard'; + + &header(1); + + print "

\n"; + print "Grp Summary"; + print "
\n"; + print POSIX::strftime($DATETIMEFMT,localtime()),"

\n"; + print "


\n"; + print &query( $SPONGSERVER, "grpsummary", '', "wml", $view, $other); + print "

\n"; + + + &footer(); + +} + +sub grp_problems { + my( $other ) = @_; + my $view = $main::view || 'full'; + + &header(0); + print &query( $SPONGSERVER, "grpproblems", $other, "html", $view ); + &footer(); +} + +# Just print a little message to stdout showing what valid options are to +# the command line interface to spong, and then exit the program. + +sub help { + print <<'_EOF_'; +Usage: wwwspong [options] + +Where "options" are one of the arguments listed below. If no arguments are +supplied, then a table showing the status of all hosts is shown. + + --summary [hostlist] Summarizes the status of services on the host(s) + --problems [hostlist] Shows a summary of problems on the host(s) + --history [hostlist] Show history information for the host(s) + + --host host Shows all information available for the given host + --services host Shows detailed service info for the given host + --stats host Shows statistical information for the given host + --config host Shows configuration information for the given host + --info host Shows admin supplied text for the given host + + --service host:service Shows detailed info for the given service/host + + --brief Display output in a brief format + --standard Display output in standard format (the default) + --full Display more information then you probably want + +All host names used as options must be fully qualified domain names. For the +options above that take an optional hostlist, the hosts listed should be +either a group name, or a list of individual hosts seperated by commas. If +the host list is omitted, then information about all hosts monitored by spong +is returned. + +_EOF_ + exit(0); +} + +# --------------------------------------------------------------------------- +# Private/Internal functions +# --------------------------------------------------------------------------- + +# This function just loads in all the configuration information from the +# spong.conf, spong.hosts, and spong.groups files. + +sub load_config_files { + my( $evalme, $inhosts, $ingroups ); + + require $conf_file || die "Can't load $conf_file: $!"; + if( -f "$conf_file.$HOST" ) { + require "$conf_file.$HOST" || die "Can't load $conf_file.$HOST: $!"; + } else { + my $tmp = (split( /\./, $HOST ))[0]; + if( -f "$conf_file.$tmp" ) { # for lazy typist + require "$conf_file.$tmp" || die "Can't load $conf_file.$tmp: $!"; + } + } + + # Read in the spong.hosts file. We are a little nasty here in that we do + # some junk to scan through the file so that we can maintain the order of + # the hosts as they appear in the file. + + open( HOSTS, $hosts_file ) || die "Can't load $hosts_file: $!"; + while( ) { + $evalme .= $_; + if( /^\s*%HOSTS\s*=\s*\(/ ) { $inhosts = 1; } + if( $inhosts && /^\s*[\'\"]?([^\s\'\"]+)[\'\"]?\s*\=\>\s*\{/ ) { + push( @HOSTS_LIST, $1 ); } + } + close( HOSTS ); + eval $evalme || die "Invalid spong.hosts file: $@"; + + # Fallback, if we didn't read things correctly... + + if( sort @HOSTS_LIST != sort keys %HOSTS ) { + @HOSTS_LIST = sort keys %HOSTS; } + + # Do the same thing for the groups file. + + $evalme = ""; + open( GROUPS, $groups_file ) || die "Can't load $groups_file: $!"; + while( ) { + $evalme .= $_; + if( /^\s*%GROUPS\s*=\s*\(/ ) { $ingroups = 1; } + if( $ingroups && /^\s*[\'\"]?([^\s\'\"]+)[\'\"]?\s*\=\>\s*\{/ ) { + push( @GROUPS_LIST, $1 ); } + } + close( GROUPS ); + eval $evalme || die "Invalid spong.groups file: $@"; + + if( sort @GROUPS_LIST != sort keys %GROUPS ) { + @GROUPS_LIST = sort keys %GROUPS; } +} + + +# ---------------------------------------------------------------------------- +# Display helper functions +# ---------------------------------------------------------------------------- + +# These allow users to easily customize some aspects of spong, by providing +# their own header and footer information for each page. + +sub header { + my( $reload ) = shift; + + if( $main::header_printed == 1 ) { return; } + $main::header_printed = 1; + + my $datestr = POSIX::strftime($main::DATETIMEFMT,localtime()); + + print <
+ + + + + + +HEADER +; + + +# &show( "header", 1 ) if -f "$main::WWWHTML/header.html"; + +} + +sub footer { + if ($main::footer_printer == 1 ) { return; } + $main::footer_printer = 1; + +# &show( "footer", 1 ) if -f "$main::WWWHTML/footer.html"; + + print "\n"; + } + + +# This just takes a HTML template with a given name, and sends it to STDOUT. +# This is used primarily for the help documentation. + +sub show { + my ($file, $hf) = @_; + my $show = $main::WWWSPONG . "/help"; + + if( -f "$main::WWWHTML/$file.html" ) { + &header( '', "Help", '', 0 ) unless $hf; + open( FILE, "$main::WWWHTML/$file.html" ); + while( ) { s/!!WWWSHOW!!/$show/g; print $_; } + close( FILE ); + &footer() unless $hf; + } else { + &header( '', "Help", '', 0 ) unless $hf; + print "

Help Not Available

\n"; + print "Sorry, but no help has been provided for that topic.\n"; + &footer() unless $hf; + } +} + +# This checks to see if the person connecting should be given back pages that +# auto-matically reload (we don't want everyone to be banging against the +# server). + +sub can_reload { + my $ok = 0; + my $regex; + + foreach $regex ( @main::WWW_REFRESH_ALLOW ) { + if( $ENV{'REMOTE_ADDR'} =~ m/$regex/i ) { $ok = 1; } + if( $ENV{'REMOTE_HOST'} =~ m/$regex/i ) { $ok = 1; } + if( $ENV{'REMOTE_USER'} =~ m/$regex/i ) { $ok = 1; } + } + + foreach $regex ( @main::WWW_REFRESH_DENY ) { + if( $ENV{'REMOTE_ADDR'} =~ m/$regex/i ) { $ok = 0; last; } + if( $ENV{'REMOTE_HOST'} =~ m/$regex/i ) { $ok = 0; last; } + if( $ENV{'REMOTE_USER'} =~ m/$regex/i ) { $ok = 0; last; } + } + + return $ok; +} + +# ---------------------------------------------------------------------------- +# Networking functions... +# ---------------------------------------------------------------------------- + +# This function sends a query to the spong server. It takes the results it +# gets back based on the user's query and returns the string back to the +# code that called this function. +# +# This query is a slightly different then the text client query function in +# that it translates some template tags into directories on the www server, so +# that links and gifs appear in the correct place. + +sub query { + my( $addr, $query, $hosts, $display, $view, $other ) = @_; + my( $iaddr, $paddr, $proto, $line, $ip, $ok, $msg ); + + if( $addr =~ /^\s*((\d+\.){3}\d+)\s*$/ ) { + $ip = $addr; + } else { + my( @addrs ) = (gethostbyname($addr))[4]; + my( $a, $b, $c, $d ) = unpack( 'C4', $addrs[0] ); + $ip = "$a.$b.$c.$d"; + } + + $iaddr = inet_aton( $ip ) || die "no host: $host\n"; + $paddr = sockaddr_in( $SPONG_QUERY_PORT, $iaddr ); + $proto = getprotobyname( 'tcp' ); + + # Set an alarm so that if we can't connect "immediately" it times out. + + $SIG{'ALRM'} = sub { die }; + alarm(30); + + eval <<'_EOM_'; + socket( SOCK, PF_INET, SOCK_STREAM, $proto ) || die "socket: $!"; + connect( SOCK, $paddr ) || die "connect: $!"; + select((select(SOCK), $| = 1)[0]); + print SOCK "$query [$hosts] $display $view $other\n"; + while( ) { + s/!!WWWGIFS!!/$main::WWWGIFS/g; # Gif directory + s/!!WWWSPONG!!/$main::WWWSPONG/g; # Spong program + s/!!WAPSPONG!!/$main::WAPSPONG/g; # Spong program + s/!!WWWHTML!!/$main::WWWHTML/g; # Html help files + $msg .= $_; + } + close( SOCK ) || die "close: $!"; + $ok = 1; +_EOM_ + + alarm(0); + + return $msg if $ok; + return "Can't connect to spong server!"; +} + -- 2.30.2