Commit 5326988f authored by Kirk Webb's avatar Kirk Webb

New node_attributes facility and table.

Auxiliary node attributes, such as service tag #, BIOS version, etc., are
should now be placed into the node_attributes table.  This can be accomplished
by either using the node_attributes command line tool, or by using the
modnodeattributes_form.php3 form (not linked in anywhere yet, but will be
in a moment).  Attribute names and values are checked for sanity using
table_regex entries.  Also note that I started with the nodecontrol stuff
as a template.

The command line tool and web form (which simply calls the command line tool
to actually do the modifications) can add, delete, and/or remove attributes.

Finally, note that the bios_version column has been moved from the nodes
table to the node_attributes table.  The Node Information page will show
the list of current attributes at the bottom of the info table.
parent 98d9d9d3
......@@ -2263,7 +2263,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/savelogs tbsetup/setgroups tbsetup/websetgroups \
tbsetup/savelogs.proxy \
tbsetup/rmgroup tbsetup/webrmuser tbsetup/webrmgroup tbsetup/mkexpdir \
tbsetup/webnodecontrol tbsetup/node_control \
tbsetup/webnodecontrol tbsetup/node_control tbsetup/node_attributes \
tbsetup/webmkgroup tbsetup/mkgroup tbsetup/eventsys_start \
tbsetup/eventsys_control tbsetup/webeventsys_control \
tbsetup/webmkproj tbsetup/mkproj tbsetup/libtestbed.pm \
......@@ -2273,7 +2273,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/libreboot.pm tbsetup/libosload.pm \
tbsetup/sfskey_update tbsetup/sfskey_update.proxy \
tbsetup/idleswap tbsetup/webidleswap tbsetup/switchmac \
tbsetup/newnode_reboot \
tbsetup/newnode_reboot tbsetup/webnodeattributes \
tbsetup/libtestbed.py \
tbsetup/tarfiles_setup tbsetup/webtarfiles_setup \
tbsetup/fetchtar.proxy tbsetup/webfrisbeekiller \
......
......@@ -701,7 +701,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/savelogs tbsetup/setgroups tbsetup/websetgroups \
tbsetup/savelogs.proxy \
tbsetup/rmgroup tbsetup/webrmuser tbsetup/webrmgroup tbsetup/mkexpdir \
tbsetup/webnodecontrol tbsetup/node_control \
tbsetup/webnodecontrol tbsetup/node_control tbsetup/node_attributes \
tbsetup/webmkgroup tbsetup/mkgroup tbsetup/eventsys_start \
tbsetup/eventsys_control tbsetup/webeventsys_control \
tbsetup/webmkproj tbsetup/mkproj tbsetup/libtestbed.pm \
......@@ -711,7 +711,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/libreboot.pm tbsetup/libosload.pm \
tbsetup/sfskey_update tbsetup/sfskey_update.proxy \
tbsetup/idleswap tbsetup/webidleswap tbsetup/switchmac \
tbsetup/newnode_reboot \
tbsetup/newnode_reboot tbsetup/webnodeattributes \
tbsetup/libtestbed.py \
tbsetup/tarfiles_setup tbsetup/webtarfiles_setup \
tbsetup/fetchtar.proxy tbsetup/webfrisbeekiller \
......
......@@ -10,4 +10,6 @@ TBAUDITEMAIL=kwebb@flux.utah.edu
TBSTATEDEMAIL=kwebb@flux.utah.edu
TBTESTSUITEEMAIL=kwebb@flux.utah.edu
WWW=www.emulab.net/dev/kwebb
FS_WITH_QUOTAS="/q /groups /users"
PLABSUPPORT=1
PLAB_ROOTBALL="plabroot-kwebb.tar.bz2"
#!/usr/bin/perl -w
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2004 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
use lib "/usr/testbed/lib";
use libdb;
#
# Turn off line buffering on output
#
$| = 1;
#
# Untaint the path
#
$ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
my $query_result =
DBQueryFatal("select node_id,bios_version from nodes ".
"order by node_id");
while (my @row = $query_result->fetchrow_array()) {
if ($row[1]) {
print "REPLACE INTO node_attributes VALUES ".
"('$row[0]', 'bios_version', '$row[1]');\n";
}
}
......@@ -977,6 +977,18 @@ CREATE TABLE node_activity (
PRIMARY KEY (node_id)
) TYPE=MyISAM;
--
-- Table structure for table `node_attributes`
--
CREATE TABLE node_attributes (
node_id varchar(32) NOT NULL default '',
attrkey varchar(32) NOT NULL default '',
attrvalue tinytext NOT NULL,
PRIMARY KEY (node_id,attrkey),
KEY node_id (node_id)
) TYPE=MyISAM;
--
-- Table structure for table `node_auxtypes`
--
......@@ -1211,7 +1223,6 @@ CREATE TABLE nodes (
failureaction enum('fatal','nonfatal','ignore') NOT NULL default 'fatal',
routertype enum('none','ospf','static','manual','static-ddijk','static-old') NOT NULL default 'none',
next_pxe_boot_path text,
bios_version varchar(64) default NULL,
eventstate varchar(20) default NULL,
state_timestamp int(10) unsigned default NULL,
op_mode varchar(20) default NULL,
......@@ -1240,8 +1251,6 @@ CREATE TABLE nodes (
destination_x float default NULL,
destination_y float default NULL,
destination_orientation float default NULL,
serial varchar(32) default NULL,
service_tag varchar(32) default NULL,
PRIMARY KEY (node_id),
KEY phys_nodeid (phys_nodeid),
KEY node_id (node_id,phys_nodeid),
......
......@@ -654,7 +654,6 @@ REPLACE INTO table_regex VALUES ('experiments','jail_osname','text','redirect','
REPLACE INTO table_regex VALUES ('experiments','delay_osname','text','redirect','os_info:osname',0,0,NULL);
REPLACE INTO table_regex VALUES ('experiments','use_ipassign','int','redirect','default:boolean',0,0,NULL);
REPLACE INTO table_regex VALUES ('experiments','ipassign_args','text','regex','^[\\w\\s-]*$',0,255,NULL);
REPLACE INTO table_regex VALUES ('nodes','bios_version','text','regex','^[-\\w\\.+]+$',0,64,NULL);
REPLACE INTO table_regex VALUES ('os_info','osid','text','regex','^[-\\w\\.+]+$',2,35,NULL);
REPLACE INTO table_regex VALUES ('experiments','expt_name','text','redirect','default:tinytext',1,255,NULL);
REPLACE INTO table_regex VALUES ('experiments','noswap_reason','text','redirect','default:tinytext',1,255,NULL);
......@@ -740,8 +739,8 @@ REPLACE INTO table_regex VALUES ('virt_firewalls','type','text','regex','^(ipfw|
REPLACE INTO table_regex VALUES ('virt_firewalls','style','text','regex','^(open|closed|basic|emulab)$',0,0,NULL);
REPLACE INTO table_regex VALUES ('mailman_listnames','listname','text','regex','^[-\\w\\.\\+]+$',3,64,NULL);
REPLACE INTO table_regex VALUES ('default','fulltext','text','regex','^[\\040-\\176\\012\\015\\011]*$',0,20000,NULL);
REPLACE INTO table_regex VALUES ('nodes','serial','text','regex','^[-\\w\\.+]+$',0,32,NULL);
REPLACE INTO table_regex VALUES ('nodes','service_tag','text','regex','^[-\\w\\.+]+$',0,32,NULL);
REPLACE INTO table_regex VALUES ('node_attributes','attrkey','text','regex','^[-\\w]+$',1,32,NULL);
REPLACE INTO table_regex VALUES ('node_attributes','attrvalue','text','regex','^[-\\w\\.+,\\s]+$',0,255,NULL);
--
-- Dumping data for table `testsuite_preentables`
......
......@@ -2830,12 +2830,7 @@ last_net_act,last_cpu_act,last_ext_act);
alter table global_policies change auxdata \
auxdata varchar(128) NOT NULL default '';
4.13: Add serial and service_tag columns to nodes table.
alter table nodes add serial varchar(32) \
default NULL after destination_orientation;
alter table nodes add service_tag varchar(32) \
default NULL after serial;
4.13: Skip to 4.14
4.14: Add disk loader and admin MFS fields to node_types.
......@@ -2843,3 +2838,23 @@ last_net_act,last_cpu_act,last_ext_act);
default 'FREEBSD-MFS' after bios_waittime;
alter table node_types add diskloadmfs_osid varchar(35) \
default 'FRISBEE-MFS' after adminmfs_osid;
4.15 Create node_attributes table; migrate a column from the nodes table.
CREATE TABLE node_attributes (
node_id varchar(32) NOT NULL default '',
attrkey varchar(32) NOT NULL default '',
attrvalue tinytext NOT NULL,
PRIMARY KEY (node_id,attrkey),
KEY node_id (node_id)
) TYPE=MyISAM;
Migrate bios_version info:
Run:
./bios_move.pl | mysql tbdb
Check the node_attributes table to be sure it contains your bios
version entries. Then remove the column from the nodes table:
alter table nodes drop column bios_version;
......@@ -19,7 +19,8 @@ BIN_STUFF = power snmpit tbend tbprerun tbreport \
os_load endexp batchexp swapexp \
node_reboot nscheck node_update savelogs node_control \
portstats checkports eventsys_control os_select tbrestart \
tbswap nseswap tarfiles_setup node_history tbrsync
tbswap nseswap tarfiles_setup node_history tbrsync \
node_attributes
SBIN_STUFF = resetvlans console_setup.proxy sched_reload named_setup \
batch_daemon exports_setup reload_daemon sched_reserve \
......@@ -28,7 +29,7 @@ SBIN_STUFF = resetvlans console_setup.proxy sched_reload named_setup \
exports_setup.proxy vnode_setup eventsys_start \
sfskey_update sfskey_update.proxy rmuser idleswap \
newnode_reboot savelogs.proxy eventsys.proxy \
elabinelab snmpit.proxy panic repos_daemon
elabinelab snmpit.proxy panic repos_daemon node_attributes
CTRLBIN_STUFF = console_setup.proxy sfskey_update.proxy \
savelogs.proxy eventsys.proxy
......@@ -44,7 +45,8 @@ LIBEXEC_STUFF = rmproj wanlinksolve wanlinkinfo \
webmkgroup websetgroups webmkproj webmodgroups \
spewlogfile staticroutes routecalc wanassign \
webnodereboot webrmuser webidleswap switchmac \
spewrpmtar webtarfiles_setup webfrisbeekiller gentopofile
spewrpmtar webtarfiles_setup webfrisbeekiller gentopofile \
webnodeattributes
LIB_STUFF = libtbsetup.pm exitonwarn.pm libtestbed.pm snmpit_intel.pm \
snmpit_cisco.pm snmpit_lib.pm snmpit_apc.pm power_rpc27.pm \
......
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2002, 2004 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
use Getopt::Std;
#
# This script is invoked from ops and from the web interface. Must check
# all the args.
#
sub usage()
{
print("Usage: node_attributes {-m|a} name=value [name=value ...] node [node ...]\n".
" node_attributes -r attr [attr ... ] node\n".
" node_attributes -r attr [attr ... ] nodelist: node [node ...]\n".
" node_attributes -l node [node ...]\n".
"\n".
"Must specify one of: ".
" -m modify attributes\n".
" -a add attributes\n".
" -r remove attributes\n".
" -l list attributes\n".
"\n".
"For multi-node attribute removal, use the \"nodelist:\" syntax");
exit(-1);
}
my $optlist = "mardl";
#
# Define a few constants
#
my $NODEATTRS_TABLE = "node_attributes";
my $NODEATTRS_KEY = "attrkey";
my $NODEATTRS_VAL = "attrvalue";
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $TBLOGS = "@TBLOGSEMAIL@";
my @nodes = ();
my %attrs = ();
my $debug = 0;
my $errors = 0;
my $modify_attrs = 0;
my $add_attrs = 0;
my $remove_attrs = 0;
my $list_attrs = 0;
#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use libdb;
use libtestbed;
if (!TBAdmin($UID)) {
print "Error: You must be an admin to use this command!\n";
exit(-1);
}
# un-taint path
$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
# Turn off line buffering on output
$| = 1;
#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
my $operation = 0;
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"m"})) {
$modify_attrs = 1;
$operation++;
}
if (defined($options{"a"})) {
$add_attrs = 1;
$operation++;
}
if (defined($options{"r"})) {
$remove_attrs = 1;
$operation++;
}
if (defined($options{"l"})) {
$list_attrs = 1;
$operation++;
}
#
# Sanity checks.
#
if ($operation > 1) {
print "Error: Only one of -m, -a, -r, or -l may be specified!\n";
usage();
}
elsif ($operation == 0) {
print "Error: One of -m, -a, -r, or -l MUST be specified!\n";
usage();
}
if (! @ARGV) {
usage();
}
#
# Shift off the set strings (name=value). Verify that each one is in the
# proper format.
#
while (@ARGV) {
my $string = $ARGV[0];
#
# Attributes are bare when in removal mode.
# If the 'nodelist:' token is given, then the remainder
# of the command line contains nodes. Otherwise, the last
# argument must be the node to operate on.
#
if ($remove_attrs) {
if ($string =~ /nodelist:/) {
shift @ARGV;
last;
}
elsif (@ARGV == 1) {
last;
}
else {
$attrs{$string} = "remove";
}
}
else {
if (! ($string =~ /([-\w]*)=[\']?([^\']*)[\']?/)) {
last;
}
$attrs{$1} = "$2";
}
shift @ARGV;
}
if ($debug) {
foreach my $option (keys(%attrs)) {
print "Will set $option to '$attrs{$option}'\n";
}
}
# Be sure at least one node was specified.
if (! @ARGV) {
print "You must specify one or more nodes!\n";
usage();
}
# Untaint the nodes.
foreach my $node ( @ARGV ) {
if ($node =~ /^([-\w]+)$/) {
$node = $1;
}
else {
die("Bad node name: $node.");
}
if (!TBValidNodeName($node)) {
die("Node is not a valid node: $node\n");
}
push(@nodes, $node);
}
if ($debug) {
print "node list: @nodes\n";
}
# If this is a attribute listing command, then let's list 'em!
# XXX: this is done in a very lame way right now.
if ($list_attrs) {
my $nodelist = join("','", @nodes);
my $query_result =
DBQueryFatal("select * from node_attributes where ".
"node_id in ('$nodelist') order by node_id");
print "node_id \t attribute \t value\n";
while (my $row = join("\t",$query_result->fetchrow_array())) {
print "$row\n";
}
exit(0);
}
#
# Process the attributes to add, mod, or remove
#
foreach my $attr (keys(%attrs)) {
my $value = $attrs{$attr};
#
# Do a checkslot on the key and value to make sure they are
# valid.
#
if ($attr ne "" &&
!TBcheck_dbslot($attr, $NODEATTRS_TABLE, $NODEATTRS_KEY,
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
die("*** $0:\n".
" Illegal attribute name: '$attr'\n");
}
if ($value ne "" &&
!TBcheck_dbslot($value, $NODEATTRS_TABLE, $NODEATTRS_VAL,
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
die("*** $0:\n".
" Illegal value contents: '$value'\n");
}
#
# If this is a modify operation, then just build up a single clause to
# execute later. Otherwise perform the insert/delete operation
# for each node now.
#
if ($modify_attrs) {
foreach my $node (@nodes) {
DBQueryFatal("replace into $NODEATTRS_TABLE values ".
"('$node','$attr','$value')");
}
}
# XXX: maybe just merge this with modify operation.
elsif ($add_attrs) {
foreach my $node (@nodes) {
DBQueryFatal("insert into $NODEATTRS_TABLE values ".
"('$node','$attr','$value')");
}
}
elsif ($remove_attrs) {
foreach my $node (@nodes) {
DBQueryFatal("delete from $NODEATTRS_TABLE where ".
"node_id='$node' and $NODEATTRS_KEY='$attr'");
}
}
}
exit(0);
......@@ -56,12 +56,6 @@ my %controlset =
[1, 0, "next_boot_cmd_line", undef, 0, "", "virt_nodes:cmd_line"],
temp_boot_osid =>
[1, 0, "temp_boot_osid", undef, 1, "-t", "os_info:osid"],
bios_version =>
[1, 0, "bios_version", undef, 0, "", "nodes:bios_version"],
serial =>
[1, 0, "serial", undef, 0, "", "nodes:serial"],
service_tag =>
[1, 0, "service_tag", undef, 0, "", "nodes:service_tag"],
);
#
......
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2002, 2004 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
include("showstuff.php3");
#
# Standard Testbed Header
#
PAGEHEADER("Modify Node Attributes Form");
#
# Only known and logged in users can do this.
#
$uid = GETLOGIN();
LOGGEDINORDIE($uid);
#
# Check to make sure that this is a valid nodeid
#
$query_result =
DBQueryFatal("SELECT * FROM nodes WHERE node_id='$node_id'");
if (mysql_num_rows($query_result) == 0) {
USERERROR("The node $node_id is not a valid nodeid!", 1);
}
$noderow = mysql_fetch_array($query_result);
#
# Get current set of attributes for node - used in comparison below
#
$node_attrs = array();
$attr_result =
DBQueryFatal("select attrkey,attrvalue from node_attributes ".
"where node_id='$node_id'");
while($row = mysql_fetch_array($attr_result)) {
$cur_node_attrs[$row[attrkey]] = $row[attrvalue];
}
#
# Only admin users may modify node attributes.
#
$isadmin = ISADMIN($uid);
if (! $isadmin) {
USERERROR("You do not have permission to modify node $node_id!", 1);
}
#
# Command strings are empty initially
#
$mod_command_string = "";
$add_command_string = "";
$del_command_string = "";
#
# Figure out which attributes are to be modified, added, or deleted
#
# Find attributes needing modification - make sure they are actually
# different than the current value before adding them to the command string.
if ($_modattrs) {
foreach ($_modattrs as $attrkey => $attrval) {
if ($cur_node_attrs[$attrkey] != $attrval) {
$mod_command_string .= "$attrkey='$attrval' ";
}
}
}
# Check for new attributes - make sure they are unique.
if ($_newattrs) {
for ($i = 0; $i < count($_newattrs); $i++) {
if ($cur_node_attrs &&
array_key_exists($_newattrs[$i], $cur_node_attrs)) {
USERERROR("You cannot add a key that already exists!",1);
}
if ($_newattrs[$i]) {
$add_command_string .= "$_newattrs[$i]='$_newvals[$i]' ";
}
}
}
# Finally, see if any attributes need to be deleted.
if ($_delattrs) {
foreach ($_delattrs as $attrkey => $attrval) {
$del_command_string .= "$attrkey ";
}
}
#
# Pass commands off to the script. It will check the arguments.
# NB: This is how nodcontrol.php3 does it, but it may not be safe;
# Shell command injection may be possible. This is an admin-only
# command though.
#
# Fire off the modify operation first
if ($mod_command_string) {
SUEXEC($uid, "nobody", "webnodeattributes -m ".
"$mod_command_string $node_id",
SUEXEC_ACTION_DIE);
}
# Next, add attributes
if ($add_command_string) {
SUEXEC($uid, "nobody", "webnodeattributes -a ".
"$add_command_string $node_id",
SUEXEC_ACTION_DIE);
}
# Finally, delete attributes.
if ($del_command_string) {
SUEXEC($uid, "nobody", "webnodeattributes -r ".
"$del_command_string $node_id",
SUEXEC_ACTION_DIE);
}
echo "<center>
<br>
<h3>Node attributes successfully modified!</h3><p>
</center>\n";
SHOWNODE($node_id, SHOWNODE_NOFLAGS);
#
# Edit option.
#
echo "<br><center>
<A href='modnodeattributes_form.php3?node_id=$node_id'>
Edit this node's attributes again?</a>
</center>\n";
#
# Standard Testbed Footer
#
PAGEFOOTER();
?>
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2002, 2004 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
#
# Standard Testbed Header
#
PAGEHEADER("Modify Node Attributes Form");
#
# Only known and logged in users can do this.
#
$uid = GETLOGIN();
LOGGEDINORDIE($uid);
#
# Verify form arguments.
#
if (!isset($node_id) ||
strcmp($node_id, "") == 0) {
USERERROR("You must provide a node ID.", 1);
}
#
# Check to make sure that this is a valid nodeid
#
$query_result =
DBQueryFatal("select * from nodes where node_id='$node_id'");
if (mysql_num_rows($query_result) == 0) {
USERERROR("The node $node_id is not a valid nodeid!", 1);
}
$noderow = mysql_fetch_array($query_result);
#
# Only admin users can modify node attributes.
#
$isadmin = ISADMIN($uid);
if (! $isadmin) {
USERERROR("You do not have permission to modify node $node_id!", 1);
}
#
# Set up some variables from the nodes table
#
$type = $noderow[type];
#
# Get any node attributes that might exist
#
$node_attrs = array();
$attr_result =
DBQueryFatal("select attrkey,attrvalue from node_attributes ".
"where node_id='$node_id'");
while($row = mysql_fetch_array($attr_result)) {
$node_attrs[$row[attrkey]] = $row[attrvalue];
}
#
# Print out node id and type
#
echo "<table border=0 cellpadding=0 cellspacing=2
align='center'>\n";
echo "<tr>
<td align=\"center\"><b>Node ID: $node_id</b></td>
</tr>\n";
echo "<tr>
<td align=\"center\"><b>Node Type: $type</b></td>
</tr>\n";
echo "</table><br><br><br><br>\n";
#
# Generate the form. Note that $refer is set by the caller so we know
# how we got to the webmodnodeattributes page.
#
echo "<table border=2 cellpadding=0 cellspacing=2
align='center'>\n";
echo "<form action=\"modnodeattributes.php3?refer=$refer\"
method=\"post\">\n";
echo "<input type=\"hidden\" name=\"node_id\" value=\"$node_id\">\n";
#
# Print out any node attributes already set
#
if ($node_attrs) {
echo "<tr><td><table border=2 cellpadding=0 cellspacing=2
align='left'>\n";
echo "<tr>
<td align=\"center\" colspan=\"0\">
<b>Current Node Attributes</b></td>
</tr>
<tr>
<td>Del?</td><td>Attribute</td><td>Value</td>
</tr>\n";
foreach ($node_attrs as $attrkey => $attrval) {
echo "<tr>
<td><input type=\"checkbox\" name=\"_delattrs[$attrkey]\">
<td>$attrkey</td>
<td class=\"left\">
<input type=\"text\" name=\"_modattrs[$attrkey]\" size=\"60\"
value=\"$attrval\"></td>
</tr>\n";
}
echo "</table></td></tr>\n";
}
#
# Print out fields for adding new attributes.
# The number of attributes defaults to 1, but this can be changed
# by specifying the number to add in the "add_numattrs" CGI variable.
#
echo "<tr><td><table border=2 cellpadding=0 cellspacing=2
align='left'>\n";
echo "<tr>
<td align=\"center\" colspan=\"0\"><b>Add Node Attribute</b></td>
</tr>\n";
if (!$add_numattrs) {
global $add_numattrs;
$add_numattrs = 1;
}
for ($i = 0; $i < $add_numattrs; $i++) {
echo "<tr>
<td class=\"left\">
<input type=\"text\" name=\"_newattrs[$i]\" size=\"32\"></td>
<td class=\"left\">
<input type=\"text\" name=\"_newvals[$i]\" size=\"60\"></td>
</td>
</tr>\n";
}