Perl DBD::Oracle on Solaris 8 with Apache

Introduction

This post is part two of my “series” on Oracle on Solaris 8. In my first post, I discussed installing Solaris 8 on VirtualBox and then installing Oracle 8i. In this post, I will discussed how I installed the Oracle database driver for the Perl DBI module and also write some simple Perl programs.

This post assumes that all of the steps from the last post have been completed, such as installing Oracle 8i.

Installing GCC and Make

Solaris 8 did not come with GCC installed. To add this to a Solaris installation, one would have had to purchase the Sun ONE Studio suite from Sun. If you didn’t have this or your organization was unwilling to purchase it, you would have had to install a GCC compiler from SunFreeware. However, as of 2026, SunFreeware is behind a paywall. But there is another easier option: you can install GCC from the OpenCSW Solaris package archive, which provides it free of charge through a APT-style package manager. In my last post, I discussed using OpenCSW to install OpenSSH on Solaris. Below are the steps I used for setting this up. I also installed gmake, even though Make is available on Solaris 8 at /usr/ccs/bin/make.

  • Download the pkgutil package from OpenCSW. It can be found here.
  • You will need to transfer pkgutil.pkg to the Solaris system. I used an NFS share, because I have a Linux NFS server and Solaris 8 has a native NFS client. You can mount up an an NFS share like: mount 192.168.1.1:/software /mnt.
  • cd to /mnt and install the pkgutil package: pkgadd -d pkgutil.pkg
  • After the package is installed, edit /etc/opt/csw/pkgutil.conf. Uncomment the “mirror” and change “testing” to “dublin”. This will pull the packages from the Internet. My Solaris 8 system isn’t configured to connect to the Internet, so I mirrored the packages locally on an Nginx server. In the Nginx document root directory, I ran sudo mkdir -p opencsw/dublin/i386/5.8, then sudo rsync -av rsync://mirrors.dotsrc.org/opencsw/dublin/i386/5.8/ opencsw/dublin/i386/5.8/. The mirror instead was then set to: mirror=http://192.168.1.1/opencsw/dublin.
  • Install GCC and Make with /opt/csw/bin/pkgutil -y -i gcc3 gmake. I also used pkgutil to install Vim, which is more convenient to use than the system’s native Vi from 2000.

The gcc3 executable can be found in /opt/csw/gcc3/bin, while gmake can be found in /opt/csw/bin. I added both to the PATH variable in .profile for the oracle user, which I will be using to compile DBD::Oracle.

PATH=$PATH:$ORACLE_HOME/bin:/opt/csw/bin:/opt/csw/gcc3/bin
export PATH

The Java runtime error

After compiling the DBD::Oracle module on Solaris 8, you may get the following error when attempting to use it in a Perl script:

You must install a Solaris patch to run this version of the Java runtime

If you look in the README.java.txt file in the DBD-Oracle-1.14 or 1.17 .tar.gz archive, you will see an email correspondence from 25+ years ago discussing the issue and various workarounds. The solution I ended up choosing was to uncheck Oracle Advanced Security on the Available Product Components screen during the 8i installation process. This was the simplest solution for me, as I don’t need RADIUS authentication for my testing purposes. It did require reinstalling Oracle in my case, which wasn’t a hassle to do on a virtual machine with snapshots. However, I would imagine that it would have been a major headache to deal with on a production server in the late 1990s or early 2000s.

Failing at compiling DBD::Oracle with the OS Perl

Feel free to skip this section, as it’s a waste of time. I first wanted to try using the Perl that is built into Solaris 8, which is 5.005_03. Version 1.14 of the DBD::Oracle module is the last version that supposedly supported 5.005_3; versions 1.15 and up require Perl 5.6. In a previous post on Solaris 2.6 and Oracle 8i, I discussed compiling this same module on SPARC. Below is a list of Perl modules I downloaded for this:

  • DBI version 1.28, which can be downloaded from here.
  • DBD::Oracle version 1.14, which can be downloaded from here

I SCP’d both of these packages to the oracle user and extracted them with the below commands. tar -xzf does not work on Solaris 8.

gunzip -c DBI-1.28.tar.gz | tar -xf -
gunzip -c DBD-Oracle-1.14.tar.gz | tar -xf -

To compile the DBI module, use the below commands. The variable overrides on the command for gmake are necessary because the Makefile generation assumes that you’re using Sun ONE Studio’s C compiler.

cd DBI-1.28
perl Makefile.PL
gmake CC="/opt/csw/gcc3/bin/gcc" LD="/opt/csw/gcc3/bin/gcc" CCCDLFLAGS="-fPIC"
gmake test
# Install as root, entering the root password when prompted
su -u root -c "cd /export/home/oracle/DBI-1.28 && /opt/csw/bin/gmake install"
# Test. Invocation of the module should produce no output.
perl -e 'use DBI;'

Unfortunately I couldn’t get the DBD::Oracle module to compile under the below commands:

cd DBD-Oracle-1.14
perl Makefile.PL
gmake CC="/opt/csw/gcc3/bin/gcc" LD="/opt/csw/gcc3/bin/gcc" CCCDLFLAGS="-fPIC"

The compilation would fail at the below step:

/opt/csw/gcc3/bin/gcc -c -I/u01/app/oracle/product/8.1.6/rdbms/demo -I/u01/app/oracle/product/8.1.6/rdbms/public -I/u01/app/oracle/product/8.1.6/plsql/public -I/u01/app/oracle/product/8.1.6/network/public -I/u01/app/oracle/product/8.1.6/rdbms/demo -I/u01/app/oracle/product/8.1.6/rdbms/demo -I/u01/app/oracle/product/8.1.6/rdbms/public -I/usr/perl5/site_perl/5.005/i86pc-solaris/auto/DBI  -O    -DVERSION=\"1.14\" -DXS_VERSION=\"1.14\" -fPIC -I/usr/perl5/5.00503/i86pc-solaris/CORE  dbdimp.c
dbdimp.c: In function `ora_db_destroy':
dbdimp.c:602: error: `dTHX' undeclared (first use in this function)
dbdimp.c:602: error: (Each undeclared identifier is reported only once
dbdimp.c:602: error: for each function it appears in.)
dbdimp.c: In function `ora_st_destroy':
dbdimp.c:1902: error: `dTHX' undeclared (first use in this function)
gmake: *** [dbdimp.o] Error 1

Apprently this is because the version of Perl on Solaris 8 isn’t threaded. For the time being I decided to give up on compiling DBD::Oracle under the OS Perl and use the newer Perl that can be installed from OpenCSW. I realize of course that this isn’t an “authentic” late 90s/early 2000s Perl, Oracle, and Apache on Solaris experience, and more like a late 2000s Debian experience. However, I do plan to revisit this at a later date, perhaps on SPARC, for which I have a slightly older version of Oracle 8i.

Compiling DBD::Oracle with the OpenCSW Perl

If you have pkgutil already set up, installing the prerequisite packages is as straightforward as doing it with Yum/DNF. I also installed Config::Tiny and Template::Toolkit, as I’ll be using these later:

/opt/csw/bin/pkgutil -i perl pm_configtiny pm_dbi pm_tt2

Note that while OpenCSW includes a DBD::Oracle package, it is version 1.23, which is incompatible with Oracle 8i. You will still need to compile DBD::Oracle manually. For the DBD::Oracle version, I went with the slightly newer 1.17, which can be downloaded from here

. I SCP’d it to the Solaris VM as the oracle user and extracted the files with:

gunzip -c DBD-Oracle-1.17.tar.gz | tar -xf -
cd DBD-Oracle-1.17

Compiling the module requires overriding six variables in the generated Makefile. I found the cleanest way was to put all of these in a Bash script in the DBD-Oracle-1.17 and build it that way. The -e option tells gmake to use the environment variables.

#!/bin/bash

export CC="/opt/csw/gcc3/bin/gcc"
export CCCDLFLAGS="-fPIC"
export LD="/opt/csw/gcc3/bin/gcc"
export LDFLAGS="-L/opt/csw/lib/libperl.so -L/opt/csw/lib -L/opt/csw/bdb47/lib -L/usr/lib -L/usr/ccs/lib -L/lib"
export CCFLAGS="-D_REENTRANT -I/opt/csw/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64"
export OPTIMIZE=""

/opt/csw/bin/perl Makefile.PL
/opt/csw/bin/gmake -e
su -u root -c "cd /export/home/oracle/DBD-Oracle-1.17 && /opt/csw/bin/gmake install"

The script can then be run with bash makeme.sh or ./makeme.sh if you made it executable. The script will prompt you for the root password at some point. After it completes, you can test whether the module successfully installed by running the Perl one-liner: /opt/csw/bin/perl -e ‘use DBD::Oracle;’. It should produce no output.

Setting up Apache

In my case, Apache was already installed on Solaris 8, as I chose to install the entire distribution. To enable it, first mv /etc/apache/httpd.conf-example /etc/apache/httpd.conf. If you want to run Oracle Perl CGI scripts, add the below to the bottom of /etc/apache/httpd.conf:

SetEnv LD_LIBRARY_PATH "/u01/app/oracle/product/8.1.6/lib"
SetEnv ORACLE_HOME "/u01/app/oracle/product/8.1.6"

Then run: /etc/init.d/apache start. Of course, the Apache version running on Solaris 8 should never be exposed to the Internet or an insecure public network for that matter.

It even contains a test-cgi script in /var/apache/cgi-bin with the server version and environment; I’m sure this was very convenient for hackers back in the day.

Running some Perl CGI scripts

As is the case with most of my blog posts on Perl and Oracle, I’ve used the “corporate asset inventory system” demo for this. It’s not the most exciting or creative thing, but it works for what I need it to do. The updated schema below includes date columns for creation and updates. One day I will put all of this code out on GitHub, for anyone like me who is interested in reliving the “old days.”

DROP USER inventory CASCADE;
CREATE USER inventory IDENTIFIED BY "abc1234";
GRANT connect, resource TO inventory;
ALTER USER inventory QUOTA 100M ON users;
CONNECT inventory/abc1234@mattdb;

SET AUTOCOMMIT ON;

CREATE SEQUENCE domains_seq;

CREATE TABLE domains (
    domain_id NUMBER,
    domain_name VARCHAR2(30) NOT NULL,
    create_dt DATE DEFAULT SYSDATE NOT NULL,
    PRIMARY KEY (domain_id),
    UNIQUE (domain_name)
);

CREATE OR REPLACE TRIGGER domains_on_insert
  BEFORE INSERT ON domains
  FOR EACH ROW
BEGIN
  SELECT domains_seq.nextval
  INTO :new.domain_id
  FROM dual;
END;
/

CREATE SEQUENCE locations_seq;
 
CREATE TABLE locations (
    location_id NUMBER,
    location_name VARCHAR2(50) NOT NULL,
    create_dt DATE DEFAULT SYSDATE NOT NULL,
    PRIMARY KEY (location_id),
    UNIQUE (location_name)
);

CREATE OR REPLACE TRIGGER locations_on_insert
  BEFORE INSERT ON locations
  FOR EACH ROW
BEGIN
  SELECT locations_seq.nextval
  INTO :new.location_id
  FROM dual;
END;
/

CREATE SEQUENCE hw_models_seq;
 
CREATE TABLE hw_models (
    hw_model_id NUMBER,
    hw_manufacturer VARCHAR2(30) NOT NULL,
    hw_model VARCHAR2(30) NOT NULL,
    create_dt DATE DEFAULT SYSDATE NOT NULL,
    PRIMARY KEY (hw_model_id)
);

CREATE OR REPLACE TRIGGER hw_models_on_insert
  BEFORE INSERT ON hw_models
  FOR EACH ROW
BEGIN
  SELECT hw_models_seq.nextval
  INTO :new.hw_model_id
  FROM dual;
END;
/
 
CREATE SEQUENCE os_seq;
 
CREATE TABLE operating_systems (
    os_id NUMBER,
    os_vendor VARCHAR2(30) NOT NULL,
    os_name VARCHAR2(30) NOT NULL,
    os_version VARCHAR2(10) NOT NULL,
    create_dt DATE DEFAULT SYSDATE NOT NULL,
    PRIMARY KEY (os_id)
);

CREATE OR REPLACE TRIGGER os_on_insert
  BEFORE INSERT ON operating_systems
  FOR EACH ROW
BEGIN
  SELECT os_seq.nextval
  INTO :new.os_id
  FROM dual;
END;
/
 
CREATE SEQUENCE inventory_seq START WITH 200000;
 
CREATE TABLE inventory (
    inventory_id NUMBER,
    name VARCHAR2(30) NOT NULL,
    serial_num VARCHAR2(30) NOT NULL,
    domain_id NUMBER NOT NULL,
    location_id NUMBER NOT NULL,
    hw_model_id NUMBER NOT NULL,
    os_id NUMBER NOT NULL,
    static_ip VARCHAR2(15),
    create_dt DATE DEFAULT SYSDATE NOT NULL,
    update_dt DATE NULL,
    PRIMARY KEY (inventory_id),
    UNIQUE (name),
    UNIQUE (serial_num),
    FOREIGN KEY (domain_id) REFERENCES domains(domain_id),
    FOREIGN KEY (location_id) REFERENCES locations(location_id),
    FOREIGN KEY (hw_model_id) REFERENCES hw_models(hw_model_id),
    FOREIGN KEY (os_id) REFERENCES operating_systems(os_id)
);

CREATE OR REPLACE TRIGGER inventory_on_insert
  BEFORE INSERT ON inventory
  FOR EACH ROW
BEGIN
  SELECT inventory_seq.nextval
  INTO :new.inventory_id
  FROM dual;
END;
/
 
INSERT INTO domains (domain_name) VALUES ('dev.example.net');
INSERT INTO domains (domain_name) VALUES ('prod.example.net');
INSERT INTO domains (domain_name) VALUES ('ad.example.net');
 
INSERT INTO locations (location_name) VALUES ('New York Sales Office');
INSERT INTO locations (location_name) VALUES ('San Francisco Sales Office');
INSERT INTO locations (location_name) VALUES ('Raleigh, NC Office');
INSERT INTO locations (location_name) VALUES ('Atlanta Data Center');
INSERT INTO locations (location_name) VALUES ('Sacramento Data Center');
 
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Sun Microsystems','V210');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Sun Microsystems','V240');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Sun Microsystems','X2100');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Sun Microsystems','SunBlade 100');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Sun Microsystems','SunBlade 150');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Sun Microsystems','Ultra 5');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Hewlett-Packard','Proliant DL360 G4');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Hewlett-Packard','Proliant DL360 G5');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Hewlett-Packard','Proliant DL360 G6');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Lenovo','Thinkpad T400');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Lenovo','Thinkpad T410');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Lenovo','Thinkpad T420');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Lenovo','Thinkpad X61');
INSERT INTO hw_models (hw_manufacturer,hw_model) VALUES ('Lenovo','Thinkpad X220');
 
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Sun Microsystems','Solaris','8');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Sun Microsystems','Solaris','9');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Sun Microsystems','Solaris','10');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Red Hat','Red Hat Linux','6.2');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Red Hat','RHEL','4.8');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Red Hat','RHEL','5.4');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Open Source','CentOS','4.8');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Open Source','CentOS','5.6');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Microsoft','Windows Server','2008R2');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Microsoft','Windows','XP');
INSERT INTO operating_systems (os_vendor,os_name,os_version) VALUES ('Sun Microsystems','Solaris','2.6');
 
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('jasmith-x61',3,1,13,10,NULL,'0RQ0VYOPSO');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('ddraper-x220',3,1,14,10,NULL,'UJBOLIP5IC');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('pmueller-sb100',1,3,4,2,'10.2.0.77','YMEVKDCVA2');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('pmueller-t420',3,3,12,10,NULL,'P1P7Z5WZZC');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('sac-prod-web-1',2,5,9,6,'10.0.0.5','KCJQI0GBT4');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('sac-prod-db-1',2,5,9,6,'10.0.0.15','22AZCLKDS9');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('sac-prod-fs-1',2,5,2,3,'10.0.0.22','WH52HKBUJ0');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('atl-dev-web-1',1,4,7,8,'10.10.0.50','SUVJ2H44GY');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('atl-dev-ldap-1',1,4,7,7,'10.10.0.52','02187Z7HTN');
INSERT INTO inventory (name,domain_id,location_id,hw_model_id,os_id,static_ip,serial_num) VALUES ('atl-dev-db-1',1,4,7,8,'10.10.0.54','RDH3MOM9KP');

CREATE TABLE recycling_bin (
    inventory_id NUMBER,
    name VARCHAR2(30) NOT NULL,
    serial_num VARCHAR2(30) NOT NULL,
    domain_id NUMBER NOT NULL,
    location_id NUMBER NOT NULL,
    hw_model_id NUMBER NOT NULL,
    os_id NUMBER NOT NULL,
    static_ip VARCHAR2(15),
    create_dt DATE DEFAULT SYSDATE NOT NULL,
    delete_dt DATE NOT NULL,
    PRIMARY KEY (inventory_id),
    UNIQUE (name),
    UNIQUE (serial_num)
);

CREATE OR REPLACE TRIGGER inventory_on_delete
  BEFORE DELETE ON inventory
  FOR EACH ROW 
BEGIN
  INSERT INTO recycling_bin
  (inventory_id,
    name,
    serial_num,
    domain_id,
    location_id,
    hw_model_id,
    os_id,
    static_ip,
    create_dt,
    delete_dt
   )
  values(:old.inventory_id,
            :old.name,
            :old.serial_num,
            :old.domain_id,
            :old.location_id,
            :old.hw_model_id,
            :old.os_id,
            :old.static_ip,
            :old.create_dt,
            SYSDATE);
END;
/

SET AUTOCOMMIT OFF;

The file can be loaded in from SQLPlus as the SYS user with the statement @./inventory.sql. After creating the schema, I created the directories /var/apache/etc and /var/apache/lib, and then added the configuration and module files. The configuration file /var/apache/etc/inventory.cfg contains the database name, username, and password.

db_name=mattdb
db_user=inventory
db_pass=abc1234

/var/apache/lib/InventoryCGI.pm contains subroutines used by all of the CGI scripts. It is practically the same as the example I used in my post Building a Perl DBD::Oracle Apache Docker container, with the only modification I had to make was changing two instances of my $sql = <<~"SQL"; to (my $sql = <<" SQL") =~ s/^\s+//g;. This is one thing I love about Perl: while new features occasionally get added, the older method of doing something will still work. I dislike how other languages like Python and PHP will break backward compatibility with new versions, though I suppose that has the upside of creating jobs for people (or AI nowadays). I did also set the location of inventory to /var/apache/conf instead of /usr/local/etc. On the Oracle SQL side, I had to remove any instances of the INNER JOIN clause, as that wasn’t introduced until 9i.

use strict;
use warnings;
use Template;
use Config::Tiny;
use DBI;

package InventoryCGI;
require Exporter;

our @ISA = qw(Exporter);
our @EXPORT = qw(conn_db parse_tt);
our @EXPORT_OK = qw(inventory_all_rows get_inventory_rec select_all_rows select_row_by_id);

sub get_config {
    my $Config = Config::Tiny->new();
    $Config = Config::Tiny->read('/var/apache/conf/inventory.cfg') || die "Unable to read inventory.cfg\n";
    return (
        $Config->{_}->{db_name},
        $Config->{_}->{db_user},
        $Config->{_}->{db_pass}
    );
}

sub conn_db {
    my $driver = 'Oracle';
    my($db, $user, $pw) = &get_config;
    my $dbh = DBI->connect("dbi:${driver}:${db}", $user, $pw, { AutoCommit => 0 , RaiseError => 1}) || die( $DBI::errstr . "\n" );
    return $dbh;
}

sub get_inventory_rec {
    my $dbh = $_[0];
    my $val = $_[1];
    my $col = $_[2] || 'INVENTORY_ID';
    (my $sql = <<"    SQL") =~ s/^\s+//g; 
        SELECT i.INVENTORY_ID, i.NAME, DOMAIN_ID, LOCATION_ID,
        HW_MODEL_ID, OS_ID, NVL(i.STATIC_IP,'DHCP') AS IP,
        d.DOMAIN_NAME, l.LOCATION_NAME, i.SERIAL_NUM,
        h.HW_MANUFACTURER || ' ' || h.HW_MODEL AS HW_MODEL,
        o.OS_VENDOR || ' ' || o.OS_NAME || ' ' || o.OS_VERSION AS OS
        FROM inventory i, domains d, locations l, hw_models h,
        operating_systems o
        WHERE i.domain_id = d.domain_id
        AND i.location_id = l.location_id
        AND i.hw_model_id = h.hw_model_id
        AND i.os_id = o.os_id
        AND i.${col} = '${val}'
    SQL

    my $hash_ref = $dbh->selectrow_hashref($sql);
    return $hash_ref;
}

sub inventory_all_rows {
    my $dbh = $_[0];
    (my $sql = <<"    SQL") =~ s/^\s+//g; 
        SELECT i.INVENTORY_ID, i.NAME, d.DOMAIN_NAME, l.LOCATION_NAME,
        h.HW_MANUFACTURER || ' ' || h.HW_MODEL AS HW_MODEL,
        o.OS_VENDOR || ' ' || o.OS_NAME || ' ' || o.OS_VERSION AS OS,
        NVL(i.STATIC_IP,'DHCP') AS IP
        FROM inventory i, domains d, locations l, hw_models h,
        operating_systems o
        WHERE i.domain_id = d.domain_id
        AND i.location_id = l.location_id
        AND i.hw_model_id = h.hw_model_id
        AND i.os_id = o.os_id
    SQL

    my $hash_ref = $dbh->selectall_hashref($sql, 'INVENTORY_ID');
    return $hash_ref;
}

sub select_all_rows {
    my $dbh    = $_[0];
    my $table  = $_[1];
    my $id_col = $_[2] || substr($table, 0, -1) . '_ID';
    my $sql    = "SELECT * FROM $table";

    my $hash_ref = $dbh->selectall_hashref($sql, $id_col);
    return $hash_ref;
}

sub select_row_by_id {
    my $dbh    = $_[0];
    my $table  = $_[1];
    my $id     = $_[2];
    my $id_col = $_[3] || substr($table, 0, -1) . '_ID';
    my $sql    = "SELECT * FROM $table WHERE $id_col = $id";

    my $hash_ref = $dbh->selectrow_hashref($sql);
    return $hash_ref;
}

sub parse_tt {
    my($template, $vars) = @_;
    die "Template file $template not found!\n" unless (-f $template);

    my $tt = Template->new();
    $tt->process($template, $vars) || die $tt->error;
}

The homepage script, inventory.cgi, and template, inventory_list.tt, are practically the same as they were in my Docker post. I only changed the path to Perl and set the lib. These were placed in /var/apache/cgi-bin and inventory.cgi was set to be executable.

#!/opt/csw/bin/perl -w

use strict;
use CGI;
use lib qw(/var/apache/lib);
use InventoryCGI qw(conn_db parse_tt inventory_all_rows);

my $cgi = CGI->new;
print $cgi->header('text/html');

my $dbh = &conn_db;
my $rows = &inventory_all_rows($dbh);

my $tt_vars = {rows => $rows};
&parse_tt('inventory_list.tt', $tt_vars);
$dbh->disconnect;
<!DOCTYPE html>
<html>
<head>
  <title>Corporate Inventory System</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  </style>
</head>
<body>
<h1>Corporate Inventory System</h1>
<p>
Below is the list of items in the corporate inventory system.
</p>
<table>
  <tr>
    <th></th>
    <th></th>
    <th>Asset Tag</th>
    <th>Name</th>
    <th>Domain</th>
    <th>Location</th>
    <th>HW Model</th>
    <th>OS</th>
    <th>IP</th>
  </tr>
[% FOREACH row IN rows -%]
  <tr>
    <td><a href="delete_rec.cgi?id=[% row.key %]">Delete</a></td>
    <td><a href="update_rec.cgi?id=[% row.key %]">Update</a></td>
    <td>[% row.key %]</td>
    <td>[% row.value.NAME %]</td>
    <td>[% row.value.DOMAIN_NAME %]</td>
    <td>[% row.value.LOCATION_NAME %]</td>
    <td>[% row.value.HW_MODEL %]</td>
    <td>[% row.value.OS %]</td>
    <td>[% row.value.IP %]</td>
  </tr>
[% END -%]
</table>
<p><a href="add_record.cgi">Add a new record</a> | <a href="add_new_hw_model.cgi">Add a new hardware model</a></p>
</body>
</html>

Other examples pages are used to add/update/delete records. Once again, I only needed to make minimal modifications to the Perl code to get it to work. However, the OpenCSW version of Perl is from 2009, so it really isn’t surprising.

#!/opt/csw/bin/perl -w

use strict;
use CGI;
use lib qw(/var/apache/lib);
use InventoryCGI qw(conn_db parse_tt get_inventory_rec select_all_rows select_row_by_id);

my $dbh = &conn_db;

sub add_rec {
    my($name,$serial,$domain_id,$location_id,$hw_model_id,$os_id,$static_ip) = @_;

    my $sql = 'INSERT INTO INVENTORY (NAME,SERIAL_NUM,DOMAIN_ID,LOCATION_ID,HW_MODEL_ID,OS_ID,STATIC_IP) VALUES ';
    $sql .= "('${name}','${serial}',${domain_id},${location_id},${hw_model_id},${os_id},'$static_ip')";

    eval {
        $dbh->do($sql) || die "Statement failed: ${sql}. " . $DBI::errstr . "\n";
        $dbh->commit || die $dbh->errstr;
    };

    if ($@) {
        warn "Transaction failed: $@. Rolling back changes.\n";
        $dbh->rollback;
    }
}

sub check_uniq {
    my $unique_cols = $_[0];
    my $return;

    while (my($col, $val) = each %{$unique_cols}) {
        if (&get_inventory_rec($dbh, $val, uc($col))) {
            $return->{$col} = $val;
        }
    }

    return $return;
}

my $cgi = CGI->new;
print $cgi->header('text/html');
my $action      = $cgi->param('action') || '';
my $name        = $cgi->param('name') || '';
my $serial      = uc($cgi->param('serial')) || '';
my $domain_id   = $cgi->param('domain_id') || 0;
my $location_id = $cgi->param('location_id') || 0;
my $hw_model_id = $cgi->param('hw_model_id') || 0;
my $os_id       = $cgi->param('os_id') || 0;
my $static_ip   = $cgi->param('static_ip') || '';

if (($action eq 'add') && !($cgi->url_param('action'))) {
    my $unique_cols = { name => $name, serial_num => $serial };
    my $unique_violated = &check_uniq($unique_cols);
    if ($unique_violated) {
        my $tt_vars = {
            action => 'added',
            fields => $unique_violated
        };
        &parse_tt('unique_const.tt', $tt_vars);
    } else {
        &add_rec($name,$serial,$domain_id,$location_id,$hw_model_id,$os_id,$static_ip);
        my $row = &get_inventory_rec($dbh, $name, 'NAME');
        my $tt_vars = {
            action => 'added',
            row    => $row
        };
        &parse_tt('record_added_changed.tt', $tt_vars);
    }
} elsif (($action eq 'review') && !($cgi->url_param('action'))) {
    my $tt_vars = {
        name        => $name,
        serial      => $serial,
        domain      => &select_row_by_id($dbh,'DOMAINS', $domain_id),
        domain_id   => $domain_id,
        location    => &select_row_by_id($dbh,'LOCATIONS', $location_id),
        location_id => $location_id,
        hw_model    => &select_row_by_id($dbh,'HW_MODELS', $hw_model_id),
        hw_model_id => $hw_model_id,
        os          => &select_row_by_id($dbh,'OPERATING_SYSTEMS', $os_id, 'OS_ID'),
        os_id       => $os_id,
        static_ip   => $static_ip
    };
    &parse_tt('review_add.tt', $tt_vars);
} else {
    my $tt_vars = {
        domains   => &select_all_rows($dbh, 'DOMAINS'),
        locations => &select_all_rows($dbh, 'LOCATIONS'),
        hw_models => &select_all_rows($dbh, 'HW_MODELS'), 
        oses      => &select_all_rows($dbh, 'OPERATING_SYSTEMS', 'OS_ID')
    };
    &parse_tt('add_rec.tt', $tt_vars);
}

$dbh->disconnect;
<!DOCTYPE html>
<html>
<head>
  <title>Add new inventory record</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Add new inventory record</h1>
<p>
Please review the record before adding it. Note: serial numbers can only be updated on the "back end".
</p>
<form autocomplete="off" action="/cgi-bin/add_record.cgi" method="POST">
<table>
    <tr>
        <td>Name</td>
        <td><input type="text" name="name" size="30" required/></td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td><input type="text" name="serial" size="30" required/></td>
    </tr>
    <tr>
        <td>Domain</td>
        <td>
            <select id="domain_id" name="domain_id">
            [% FOREACH domain IN domains -%]
                <option value="[% domain.key %]">[% domain.value.DOMAIN_NAME %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
    <tr>
        <td>Location</td>
        <td>
            <select id="location_id" name="location_id">
            [% FOREACH location IN locations -%]
                <option value="[% location.key %]">[% location.value.LOCATION_NAME %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
    <tr>
        <td>Model</td>
        <td>
            <select id="hw_model_id" name="hw_model_id">
            [% FOREACH hw_model IN hw_models -%]
                <option value="[% hw_model.key %]">[% hw_model.value.HW_MANUFACTURER %] [% hw_model.value.HW_MODEL %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
    <tr>
        <td>OS</td>
        <td>
            <select id="os_id" name="os_id">
            [% FOREACH os IN oses -%]
                <option value="[% os.key %]">[% os.value.OS_VENDOR %] [% os.value.OS_NAME %] [% os.value.OS_VERSION %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
</table>
    <input type="hidden" name="action" value="review"/>
    <button type="submit">Review</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>Review record to be added to inventory</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Review record to be added to inventory</h1>
<p>
Please review the below record before submitting it. The asset tag will be assigned when the record is added.
</p>
<form action="/cgi-bin/add_record.cgi" method="POST">
<table>
    <tr>
        <td>Name</td>
        <td><input type="hidden" name="name" value="[% name %]"/>[% name %]</td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td><input type="hidden" name="serial" value="[% serial %]"/>[% serial %]</td>
    </tr>
    <tr>
        <td>Domain</td>
        <td>[% domain.DOMAIN_NAME %]<input type="hidden" name="domain_id" value="[% domain_id %]"/></td>
    </tr>
    <tr>
        <td>Location</td>
        <td>[% location.LOCATION_NAME %]<input type="hidden" name="location_id" value="[% location_id %]"/></td>
    </tr>
    <tr>
        <td>Model</td>
        <td>[% hw_model.HW_MANUFACTURER %] [% hw_model.HW_MODEL %]<input type="hidden" name="hw_model_id" value="[% hw_model_id %]"/></td>
    </tr>
    <tr>
        <td>OS</td>
        <td>[% os.OS_VENDOR %] [% os.OS_NAME %] [% os.OS_VERSION %]<input type="hidden" name="os_id" value="[% os_id %]"/></td>
    </tr>
    </table>
    <input type="hidden" name="action" value="add"/>
    <button type="submit">Add Record</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>Record [% action %] successfully</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Record [% action %] successfully</h1>
<p>
The following record was [% action %]:
</p>
<table>
    <tr>
        <td>Asset Tag</td>
        <td>[% row.INVENTORY_ID %]</td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td>[% row.SERIAL_NUM %]</td>
    </tr>
    <tr>
        <td>Name</td>
        <td>[% row.NAME %]</td>
    </tr>
    <tr>
        <td>Domain</td>
        <td>[% row.DOMAIN_NAME %]</td>
    </tr>
    <tr>
        <td>Location</td>
        <td>[% row.LOCATION_NAME %]</td>
    </tr>
    <tr>
        <td>Model</td>
        <td>[% row.HW_MODEL %]</td>
    </tr>
    <tr>
        <td>OS</td>
        <td>[% row.OS %]</td>
    </tr>
</table>
<p><a href="inventory.cgi"><strong>Return to the main page</strong></a></p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>Value(s) already in use by another record</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Value(s) already in use by another record</h1>
<p>
The below values are already in use by another record.
</p>
<table>
[% FOREACH field IN fields -%]
  <tr>
    <td>[% field.key %]</td>
    <td><strong>[% field.value %]</strong></td>
  </tr>
[% END -%]
</table>
<p>Due to this, the record cannot be [% action %]. Please choose a unique value and try again.</p>
<p><a href="inventory.cgi"><strong>Return to the main page</strong></a></p>
</body>
</html>
#!/opt/csw/bin/perl -w

use strict;
use CGI;
use lib qw(/var/apache/lib);
use InventoryCGI qw(conn_db parse_tt get_inventory_rec select_all_rows select_row_by_id);

my $dbh = &conn_db;

sub edit_screen {
    my $rec = &get_inventory_rec($dbh, $_[0]);
    my $tt_vars = {
        rec       => $rec,
        domains   => &select_all_rows($dbh, 'DOMAINS'),
        locations => &select_all_rows($dbh, 'LOCATIONS'),
        hw_model  => &select_row_by_id($dbh,'HW_MODELS', $rec->{'HW_MODEL_ID'}), 
        oses      => &select_all_rows($dbh, 'OPERATING_SYSTEMS', 'OS_ID')
    };
    &parse_tt('update_rec.tt', $tt_vars);
}

sub update_rec {
    my @updates;
    push(@updates, "NAME = '$_[1]'") unless ($_[1] eq '');
    push(@updates, "DOMAIN_ID = $_[2]") unless ($_[2] == 0);
    push(@updates, "LOCATION_ID = $_[3]") unless ($_[3] == 0);
    push(@updates, "OS_ID = $_[5]") unless ($_[5] == 0);

    if (scalar(@updates) > 0) {
        my $sql = 'UPDATE INVENTORY SET update_dt = SYSDATE, ' . join(', ', @updates) . ' WHERE INVENTORY_ID = ' . $_[0];
        eval {
            $dbh->do($sql) || die "Statement failed: ${sql}. " . $DBI::errstr . "\n";
            $dbh->commit || die $dbh->errstr;
        };

        if ($@) {
            warn "Transaction failed: $@. Rolling back changes.\n";
            $dbh->rollback;
        }
  }
}

my $cgi = CGI->new;
print $cgi->header('text/html');
my $id          = $cgi->param('id') || 0;
my $action      = $cgi->param('action') || '';
my $name        = $cgi->param('name') || '';
my $domain_id   = $cgi->param('domain_id') || 0;
my $location_id = $cgi->param('location_id') || 0;
my $os_id       = $cgi->param('os_id') || 0;

if (($action eq 'update') && !($cgi->url_param('action'))) {
    &update_rec($id,$name,$domain_id,$location_id,$os_id);
    my $row = &get_inventory_rec($dbh, $id);
    my $tt_vars = {
        action => 'updated',
        row    => $row
    };
    &parse_tt('record_added_changed.tt', $tt_vars);
} elsif (($action eq 'review') && !($cgi->url_param('action'))) {
    my $rec = &get_inventory_rec($dbh, $id);
    if (($name ne '') && ($name ne $rec->{'NAME'})) {
        if (&get_inventory_rec($dbh, $name, 'NAME')) {
            my $tt_vars = {
                action => 'updated',
                fields => { 'name' => $name }
            };
            &parse_tt('unique_const.tt', $tt_vars);
        }
    } else {
        my $tt_vars = {
            rec      => $rec,
            name     => $name,
            domain   => &select_row_by_id($dbh, 'DOMAINS', $domain_id),
            location => &select_row_by_id($dbh, 'LOCATIONS', $location_id),
            hw_model => &select_row_by_id($dbh, 'HW_MODELS', $rec->{'HW_MODEL_ID'}),
            os       => &select_row_by_id($dbh, 'OPERATING_SYSTEMS', $os_id, 'OS_ID')
        };
        &parse_tt('review_update.tt', $tt_vars);
    }
} else {
    &edit_screen($id);
}

$dbh->disconnect;
<!DOCTYPE html>
<html>
<head>
  <title>Update record in inventory</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Update record in inventory</h1>
<p>
Please review the below record before updating it. Note: serial numbers can only be updated on the "back end".
</p>
<form autocomplete="off" action="/cgi-bin/update_rec.cgi" method="POST">
<table>
    <tr>
        <td>Asset Tag</td>
        <td>[% rec.INVENTORY_ID %]</td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td>[% rec.SERIAL_NUM %]</td>
    </tr>
    <tr>
        <td>Name</td>
        <td><input type="text" name="name" size="25" value="[% rec.NAME %]" required/></td>
    </tr>
    <tr>
        <td>Created</td>
        <td>[% rec.CREATE_DT %]</td>
    </tr>
    <tr>
        <td>Last Updated</td>
        <td>[% rec.UPDATE_DT %]</td>
    </tr>
    <tr>
        <td>Domain</td>
        <td>
            <select id="domain_id" name="domain_id">
            [% FOREACH domain IN domains -%]
                <option value="[% domain.key %]" [% IF domain.key == rec.DOMAIN_ID %]selected[% END %]>[% domain.value.DOMAIN_NAME %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
    <tr>
        <td>Location</td>
        <td>
            <select id="location_id" name="location_id">
            [% FOREACH location IN locations -%]
                <option value="[% location.key %]" [% IF location.key == rec.LOCATION_ID %]selected[% END %]>[% location.value.LOCATION_NAME %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
    <tr>
        <td>Model</td>
        <td>[% hw_model.HW_MANUFACTURER %] [% hw_model.HW_MODEL %]</td>
    </tr>
    <tr>
        <td>OS</td>
        <td>
            <select id="os_id" name="os_id">
            [% FOREACH os IN oses -%]
                <option value="[% os.key %]" [% IF os.key == rec.OS_ID %]selected[% END %]>[% os.value.OS_VENDOR %] [% os.value.OS_NAME %] [% os.value.OS_VERSION %]</option>
            [% END -%]
            </select>
        </td>
    </tr>
</table>
    <input type="hidden" name="id" value="[% rec.INVENTORY_ID %]"/>
    <input type="hidden" name="action" value="review"/>
    <button type="submit">Review</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>Review changes for record in inventory</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Review changes for record in inventory</h1>
<p>
Please review the below record before submitting it. Changes are in bold.
</p>
<form action="/cgi-bin/update_rec.cgi" method="POST">
<table>
    <tr>
        <td>Asset Tag</td>
        <td>[% rec.INVENTORY_ID %]</td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td>[% rec.SERIAL_NUM %]</td>
    </tr>
    <tr>
        <td>Name</td>
        [% IF name == rec.NAME -%]
        <td>[% rec.NAME %]</td>
        [% ELSE -%]
        <td><strong>[% name %]</strong><input type="hidden" name="name" value="[% name %]"/></td>
        [% END -%]
    </tr>
    <tr>
        <td>Domain</td>
        [% IF domain.DOMAIN_ID == rec.DOMAIN_ID -%]
        <td>[% domain.DOMAIN_NAME %]</td>
        [% ELSE -%]
        <td><strong>[% domain.DOMAIN_NAME %]</strong><input type="hidden" name="domain_id" value="[% domain.DOMAIN_ID %]"/></td>
        [% END -%]
    </tr>
    <tr>
        <td>Location</td>
        [% IF location.LOCATION_ID == rec.LOCATION_ID -%]
        <td>[% location.LOCATION_NAME %]</td>
        [% ELSE -%]
        <td><strong>[% location.LOCATION_NAME %]</strong><input type="hidden" name="location_id" value="[% location.LOCATION_ID %]"/></td>
        [% END -%]
    </tr>
    <tr>
        <td>Model</td>
        <td>[% hw_model.HW_MANUFACTURER %] [% hw_model.HW_MODEL %]</td>
    </tr>
    <tr>
        <td>OS</td>
        [% IF os.OS_ID == rec.OS_ID -%]
        <td>[% os.OS_VENDOR %] [% os.OS_NAME %] [% os.OS_VERSION %]</td>
        [% ELSE -%]
        <td><strong>[% os.OS_VENDOR %] [% os.OS_NAME %] [% os.OS_VERSION %]</strong><input type="hidden" name="os_id" value="[% os.OS_ID %]"/></td>
        [% END -%]
    </tr>
    </table>
    <input type="hidden" name="id" value="[% rec.INVENTORY_ID %]"/>
    <input type="hidden" name="action" value="update"/>
    <button type="submit">Update Record</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
</body>
</html>
#!/opt/csw/bin/perl -w

use strict;
use CGI;
use lib qw(/var/apache/lib);
use InventoryCGI qw(conn_db parse_tt get_inventory_rec);

my $dbh = &conn_db;

sub delete_rec {
  my $id  = $_[0];
  my $sql = "DELETE FROM INVENTORY WHERE INVENTORY_ID = $id";
  eval {
    $dbh->do($sql) || die "Statement failed: ${sql}. " . $DBI::errstr . "\n";
    $dbh->commit || die $dbh->errstr;
  };

  if ($@) {
    warn "Transaction failed: $@. Rolling back changes.\n";
    $dbh->rollback;
  }
}

sub recycling_bin {
    my $id  = $_[0];
    (my $sql = <<"    SQL") =~ s/^\s+//g;
        SELECT r.INVENTORY_ID, r.NAME, d.DOMAIN_NAME, l.LOCATION_NAME,
        h.HW_MANUFACTURER || ' ' || h.HW_MODEL AS HW_MODEL, r.SERIAL_NUM,
        o.OS_VENDOR || ' ' || o.OS_NAME || ' ' || o.OS_VERSION AS OS,
        NVL(r.STATIC_IP,'DHCP') AS IP,
        TO_CHAR(r.DELETE_DT, 'MM/DD/YYYY') AS DELETE_DT
        FROM recycling_bin r, domains d, locations l, hw_models h,
        operating_systems o
        WHERE r.domain_ID = d.domain_ID
        AND r.location_id = l.location_id
        AND r.hw_model_id = h.hw_model_id
        AND r.os_id = o.os_id
        AND r.INVENTORY_ID = $id
    SQL
    my $hash_ref = $dbh->selectrow_hashref($sql);
    return $hash_ref;
}  

my $cgi = CGI->new;
print $cgi->header('text/html');
my $id = $cgi->param('id') || 0;
my $confirm = $cgi->param('confirm') || '';

if (($confirm eq 'true') && !($cgi->url_param('confirm'))) {
    &delete_rec($id);
    my $row = &recycling_bin($id);
    my $tt_vars = {row => $row};
    &parse_tt('record_deleted.tt', $tt_vars);
} else {
    my $row = &get_inventory_rec($dbh, $id);
    my $tt_vars = {row => $row};
    &parse_tt('delete_rec.tt', $tt_vars);
}

$dbh->disconnect;
<!DOCTYPE html>
<html>
<head>
  <title>Delete record from inventory</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Delete record from inventory</h1>
<p>
Please review the below record before deleting it. The record will be moved to the recycling bin in the event it needs to be restored.
</p>
<table>
    <tr>
        <td>Asset Tag</td>
        <td>[% row.INVENTORY_ID %]</td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td>[% row.SERIAL_NUM %]</td>
    </tr>
    <tr>
        <td>Name</td>
        <td>[% row.NAME %]</td>
    </tr>
    <tr>
        <td>Domain</td>
        <td>[% row.DOMAIN_NAME %]</td>
    </tr>
    <tr>
        <td>Location</td>
        <td>[% row.LOCATION_NAME %]</td>
    </tr>
    <tr>
        <td>Model</td>
        <td>[% row.HW_MODEL %]</td>
    </tr>
    <tr>
        <td>OS</td>
        <td>[% row.OS %]</td>
    </tr>
</table>
<form action="/cgi-bin/delete_rec.cgi" method="POST" onsubmit="return checkForm()">
    <input type="hidden" name="id" value="[% row.INVENTORY_ID %]"/>
    <input type="hidden" name="confirm" value="true"/>
    <button type="submit">Delete</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
<script>
function checkForm() {
    if (confirm("Are you sure you want to delete this record?")) {
        return true; // Form submits
    } else {
        return false; // Form submission is canceled
    }
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>Record deleted successfully</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Record deleted successfully</h1>
<p>
The following record was deleted and moved to the recycling bin:
</p>
<table>
    <tr>
        <td>Asset Tag</td>
        <td>[% row.INVENTORY_ID %]</td>
    </tr>
    <tr>
        <td>Serial Number</td>
        <td>[% row.SERIAL_NUM %]</td>
    </tr>
    <tr>
        <td>Name</td>
        <td>[% row.NAME %]</td>
    </tr>
    <tr>
        <td>Deleted</td>
        <td>[% row.DELETE_DT %]</td>
    </tr>
    <tr>
        <td>Domain</td>
        <td>[% row.DOMAIN_NAME %]</td>
    </tr>
    <tr>
        <td>Location</td>
        <td>[% row.LOCATION_NAME %]</td>
    </tr>
    <tr>
        <td>Model</td>
        <td>[% row.HW_MODEL %]</td>
    </tr>
    <tr>
        <td>OS</td>
        <td>[% row.OS %]</td>
    </tr>
</table>
<p><a href="inventory.cgi"><strong>Return to the main page</strong></a></p>
</body>
</html>
#!/opt/csw/bin/perl -w

use strict;
use CGI;
use lib qw(/var/apache/lib);
use InventoryCGI qw(conn_db parse_tt);

my $dbh = &conn_db;

my $cgi = CGI->new;
print $cgi->header('text/html');
my $action      = $cgi->param('action') || '';
my $model_id     = $cgi->param('model_id') || 0;
my $manufacturer = $cgi->param('manufacturer') || '';
my $model        = $cgi->param('model') || '';

my $tt_vars = {
    manufacturer => $manufacturer,
    model        => $model
};

if (($action eq 'add') && !($cgi->url_param('action'))) {
    my $uniq_sql = "SELECT * FROM HW_MODELS WHERE HW_MANUFACTURER = '${manufacturer}' AND HW_MODEL = '${model}'";
    if ($dbh->selectrow_hashref($uniq_sql)) {
        &parse_tt('unable_to_add_hw.tt', $tt_vars);
    } else {
        my $sql = "INSERT INTO HW_MODELS (HW_MANUFACTURER,HW_MODEL) VALUES ('${manufacturer}','${model}')";
        eval {
            $dbh->do($sql) || die "Statement failed: ${sql}. " . $DBI::errstr . "\n";
            $dbh->commit || die $dbh->errstr;
        };

        if ($@) {
            warn "Transaction failed: $@. Rolling back changes.\n";
            $dbh->rollback;
        }
        (my $html = <<"        HTML")  =~ s/^\s+//g;
        <!DOCTYPE html>
        <html>
        <head>
        <title>Hardware Model added successfully</title>
        </head>
        <body>
        <h1>Hardware Model added successfully</h1>
        <p><a href="inventory.cgi"><strong>Return to the main page</strong></a></p>
        </body>
        </html>
        HTML
        print $html;
    }
} elsif (($action eq 'review') && !($cgi->url_param('action'))) {
    &parse_tt('review_add_hw.tt', $tt_vars);
} else {
    &parse_tt('add_hw.tt');
}

$dbh->disconnect;
<!DOCTYPE html>
<html>
<head>
  <title>Add a new hardware model</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Add a new hardware model</h1>
<p/>
<form autocomplete="off" action="/cgi-bin/add_new_hw_model.cgi" method="POST">
<table>
    <tr>
        <td>Manufacturer</td>
        <td><input type="text" name="manufacturer" size="30" required/></td>
    </tr>
    <tr>
        <td>Model</td>
        <td><input type="text" name="model" size="30" required/></td>
    </tr>
</table>
    <input type="hidden" name="action" value="review"/>
    <button type="submit">Review</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>Review hardware model to be added</title>
  <style>
  table, th, td {
    border: 1px solid black;
  }
  th, td {
    padding: 5px;
  }
  td:nth-child(1) {
    text-align: right;
  }
  </style>
</head>
<body>
<h1>Review hardware model to be added</h1>
<p>
Please review the below hardware model before submitting it.
</p>
<form action="/cgi-bin/add_new_hw_model.cgi" method="POST">
<table>
    <tr>
        <td>Manufacturer</td>
        <td><input type="hidden" name="manufacturer" value="[% manufacturer %]"/>[% manufacturer %]</td>
    </tr>
    <tr>
        <td>Model</td>
        <td><input type="hidden" name="model" value="[% model %]"/>[% model %]</td>
    </tr>
    </table>
    <input type="hidden" name="action" value="add"/>
    <button type="submit">Add Model</button>
    <button type="button" onclick="window.location.href='/cgi-bin/inventory.cgi';">Cancel</button>
</form>
</body>
</html>

Conclusion

I wasn’t entirely happy with how this post turned out, but I wanted to get something out there. My next post on this topic will be trying to run code like this on SPARC, as I seem to have more success with that. I hope what I have written here is of interest to someone, at the very least for nostalgic reasons. As always, thanks for reading!