SUSE Conversations


Advanced Patch Management Using the New Subscription Management Tool (SMT)



By: hvermeulen

December 17, 2008 11:20 am

Reads:479

Comments:1

Rating:0

This AppNote will describe how you can add additional patch environments to SMT.

An increasing number of companies have their systems divided over a test, acceptance and production environment. Patches and updates should be provided to those environments in turn.

In other words, a single test environment is not enough, additional staging environments are required. Patches need to be tested first, then copied over to the acceptance environment. After acceptance testing is done, patches can finally be copied over the production environment for all remaining servers.

Because SMT is a fairly new concept, we will describe shortly how to install and set-up SMT itself as well.

Contents:

Installing SMT

The SMT is the Subscription Management Toolbox and can be used to create a local / internal repository for patches and installation sources.

This enables you to deploy patches within the organization from a single point. No other servers need Internet Access, which is a great advantage.

Installing SMT

To install the SMT, please follow these steps:

  • Download the SMT from http://download.novell.com Use smt as the search keyword.
  • Start yast and go to Software / Add-on Products
  • Select “Local Directory”
  • Select “iso image” and enter the path to the iso file.
  • Accept the license agreement.
  • Use search and enter smt, ensure that the SMT packages have been selected (+ sign)
  • Select Accept.
  • A screen detailing all dependencies /added packages will show Apache, many perl packages and mysql to be added. Select Ok.

The SMT configuration wizard will now start. Enter the following:

  • Enable the “Enable SMT service.”
  • For the NU User and Password, use your mirrored credentials. The mirrored credentials can be obtained from http://www.novell.com/center
  • Hover over “My Products” on the left side and choose “Mirror Credentials.”
  • Select Test to verify the credentials are OK.
  • Enter your email address.
  • Enter your internal server URL. This is the URL other servers will be using to use the SMT.
  • Select Next.
  • Enter a password for the database.
  • Enter any email addresses to send reports to.
  • Select Finish and enter a MySQL root password.

The SMT installation has now finished.

Next, you are prompted to configure NCC. If you have already done so on this server, you can select “Configure later.”

If you have not yet configured your NCC settings, you will be prompted to do so now. Please see Appendix A for details.

Configuring SMT

All the configuration information is stored in either /etc/smt.conf or in the database.

In most cases, a few changes need to be made in /etc/smt.conf:

  • Change MirrorSRC to false, unless you want sources to be mirrored as well.
  • Enter proxy server details if required in your environment:
	HTTPProxy=
	HTTPPSroxy=
	ProxyUser=
  • Enter the mail server configuration details for reports. For example:
	reportEmailFrom=smat@your_doman,com
	mailServer=smtp.your_domain.com
	mailServerPort=25
	mailServerUser=SMT	
	mailServerPassword=SendMailSecure
  • If you have SLES9 products to mirror, find the corresponding section, select your appropriate architecture and set mirror to true. For example:
 	[YOU9-SUSE-CORE]
	mirror_prod=SUSE-CORE
	mirror_archs=i386,x86_64
	mirror_version=9
	mirror=true
	credentials=YOUuser:YOUpassword

Run the following command to see if SMT is properly registered:

 	smt-catalogs

If the list is empty, make sure your ncc registration is correct. Next, sync smt with ncc using the following command:

 	smt ncc-sync

To enable the catalogs to mirror, issue the following commands:

	smt-catalogs -m

Note the names of the catalogs you want to mirror and enable them by using:

 	smt-catalogs -e "catalog name"

A list with possible architectures(targets) is displayed. Enter the ID number for the target you want to enable. For SLES10 x86_64, this would be:

	smt-catalogs -e SLES10-SP2-Online 
	"5"
	smt-catalogs -e SLES10-SP2-Updates  
	"5"
Note: Use “smt-catalogs -o” to list the subscribed catalogs.

Mirroring

By default Mirroring is scheduled to run every night.

SMT configuration and scheduled jobs can be seen using the “yast2 smt” command and selecting the “Scheduled SMT Jobs” tab.

To manually start mirroring the enabled catalogs from the update server, use the command

 smt-mirror

Connecting Clients

smt-list-registrations shows all registered clients from the server side.

There is a script, called “clientSetup4SMT.sh” that prepares the client for SMT usage. The script is available inside the “/usr/share/doc/packages/smt/” folder on the SMT server. The clientSetup4SMT.sh script works with SUSE Linux Enterprise 10 SP1 and SP2 systems.

To configure a client machine to use SMT with the clientSetup4SMT.sh script, follow these steps:

  • Copy the /usr/share/doc/packages/smt/clientSetup4SMT.sh script from your SMT server to the client machine.
  • As root, execute the script on the client machine. The script can be executed in two ways.
  • In the first case, the script name is followed by the registration URL: ./clientSetup4SMT.sh registration_URL, for example: ./clientSetup4SMT.sh https://smt.example.com/center/regsvc.
  • In the second case, the script name is followed by the –host option followed by hostname of the SMT server: ./clientSetup4SMT.sh –host server_hostname, for example: ./clientSetup4SMT.sh –host smt.example.com.
  • The script downloads the server’s CA certificate. Accept it by pressing “y”
  • The script performs all necessary modifications on the client. However, the registration itself is not performed by the script.
  • Perform a registration by executing “suse_register” or running “yast2 inst_suse_register” on the client.

On the server run “smt-list-registrations” to confirm the client was added properly.

Using a Test and Acceptance environment with SMT

Using a test environment with default SMT options

Using a test environment is an option of SMT. To enable the usage of a test environment, the following has to be modified inside “/etc/smt.conf”

 MirrorTo = /srv/www/htdocs

has to be replaced by:

 MirrorTo = /srv/www/htdocs/testing

Patches will be downloaded into the testing folder and can be copied at a specific point in time, after they have been verified, to the “/srv/www/htdocs/repo” folder for production systems.

To configure a client to register against the test environment instead of the production environment, modify “/etc/suseRegister.conf” on the client machine and add “&testenv=1″ to the register command. For example:

 register = command=register&testenv=1

Clients can now register against thetest environment by using the following command:

 yast2 inst_suse_register

or

 suse_register --nozypp --restore-repos --no-hw-data  --no-optional

When testing is complete, all patches and updates inside the testing folder should be copied from “/srv/www/htdocs/testing/repo” to the “/srv/www/htdocs/repo” folder and all production systems can be updated.

Using a test environment with custom options

By default, SMT will mirror the latest patches each night. These jobs are scheduled using cron and can be modified through YaST by issuing the following command:

 yast2 smt

On the “Scheduled SMT jobs” tab, the “Synchronisation of Updates” job can be modified. These jobs can also be directly modified by editing the “/etc/smt.d/novell.com-smt” file.

As testing might take some time to complete, it is highly recommended to use a “point in time” copy to prevent new patches from being added during your test cycle. There are two options to achieve this.

  • Either modify the cron job to disable the automatic mirror and mirror patches and updates manually once before your test cycle using the command “smt-mirror”
  • Or mirror the patches to a different folder, say “/srv/www/htdocs/mirror” and manually copy the contents of this folder to the “/srv/www/htdocs/testing” folder before your test cycle.

For the second option, please take the following steps:

  • Create the folder “/srv/www/htdocs/mirror/repo”
  • Modify “/etc/smt.conf” and replace
 MirrorTo = /srv/www/htdocs

by

 MirrorTo = /srv/www/htdocs/mirror

Patches will be downloaded daily into this mirror folder and can be copied at a specific point in time, after they have been verified, to the “/srv/www/htdocs/testing” folder for your test cycle.

To configure a client to register against the test environment instead of the production environment, modify “/etc/suseRegister.conf” on the client machine and add “&testenv=1″ to the register command. For example:

 register = command=register&testenv=1

Clients can now register against the test environment by using the following command:

 yast2 inst_suse_register

or

 suse_register --nozypp --restore-repos --no-hw-data  --no-optional

When testing is complete, all patches and updates inside the testing folder should be copied from “/srv/www/htdocs/testing/repo” to the “/srv/www/htdocs/repo” folder and all production systems can be updated.

Using an Acceptance environment

An increasing number of companies have their systems divided over a test, acceptance and production environment. Patches and updates should be provided to those environments in turn.

In other words, a single test environment is not enough, additional staging environments are required. Patches need to be tested first, then copied over to the acceptance environment. After acceptance testing is done, patches can finally be copied over the production environment for all remaining servers.

You can add additional environments to your SMT system with a little tweaking.

This is not a default option of SMT and therefore requires you to edit some of the configuration files. The following files need to be edited. Please create a copy of those files first for safekeeping.

On the SMT server:

  • /etc/smt.d/nu_server.conf
  • “/etc/smt.d/smt_mod_perl.conf”
  • /srv/www/perl-lib/SMT/Registration.pm

On the SMT clients:

  • /etc/suseRegister.conf
Note: Please note that any changes to the “/etc/smt.d/nu_server.conf” and “/srv/www/perl-lib/SMT/Registration.pm” files could be overwritten when SMT itself is updated. You should always check for SMT updates and test if your modifications are still in place, or if you need to re-add them.

Creating an additional environment

In this example we will call our new environment “acceptance” but you can obviously use any name you want, or add multiple environments.

Creating the storage area and granting access to it

Create the following path on the SMT server:

/srv/www/htdocs/acceptance/repo

Granting access to the storage area for SMT clients

Since clients will need to be able to connect to the acceptance environment on the SMT server, we need to grant them access.

Edit “/etc/smt.d/nu_server.conf” and add the following at the end of this file:

	Alias acceptance/repo
	"/srv/www/htdocs/acceptance/repo"
	<Directory "/srv/www/htdocs/acceptance/repo">
        Options +Indexes +FollowSymLinks
        IndexOptions +NameWidth=*
        Order allow,deny
        Allow from all
	</Directory>

Edit “/etc/smt.d/smt_mod_perl.conf” and add the following at the end of this file:

    <Location /acceptance/repo/repoindex.xml>
        # perl cgi mode
        SetHandler  perl-script
        # PerlInitHandler Apache2::Reload
        PerlResponseHandler NU::RepoIndex
        PerlAuthenHandler NU::SMTAuth
        AuthName SMTAuth
        AuthType Basic
        Require valid-user
    </Location>

Adding the environment to SMT scripts

Now we need to add an environment to SMT itself. For this we need to edit the “srv/www/perl-lib/SMT/Registration.pm” file as follows:

Find the following text:

	if(exists $hargs->{testenv} && $hargs->{testenv})
	   {
	$usetestenv = 1;       
	   }

Add the following behind this text:

	# Adding additional environments
	# 1 = testing
	# 2 = acceptance
	# 3 = ...
	    if(exists $hargs->{accenv} && $hargs->{accenv})
	    {
	        $usetestenv = 2;
	    }
	# End of modifications

What we have done here is adding the “accenv” option and use the existing “usetestenv” variable. Instead of being either not present (0) or 1, we just increase the number for each environment we want to create.

Find the following text:

	if($usetestenv)
	    {
	        $LocalNUUrl    .="/testing/";
	    }

and replace it with the following:

	# Adding additional environments
	# Deciding what path to use
	# 1 is the testing environment
	    if($usetestenv == 1)
	    {
	        $LocalNUUrl    .= "/testing/";
	    }
	# 2 is the acceptance environment
	    if($usetestenv == 2)
	    {
	        $LocalNUUrl    .= "/acceptance/";
	    }
	# End of modifications

Based on the “usetestenv” value, we now decide where to attach SMT clients to.

Finaly, please issue a “rcapache2 restart” command.

An example of a modified script is provided in Appendix B: Example of the Registration.pm script.

Configuring SMT clients for the new environment

We created the “accenv” option that can now be used for the registration process. To configure a client to register against the newly created acceptance environment instead of the production or test environment, modify “/etc/suseRegister.conf” on the client machine and add “&accenv=1″ to the register command. For example:

 register = command=register&accenv=1

Clients can now register against the new acceptance environment by using the following command:

 yast2 inst_suse_register

or

suse_register --nozypp --restore-repos --no-hw-data  --no-optional

Appendix A: NCC registration process

Note: If you are not running in a graphical shell, please select Configure later and start the NCC registration from a graphical shell. This can be done by starting yast2 / Software / Novell Customer Center Configuration. When prompted by the SMT installer to configure NCC, enter the following settings:
  • On the “Novell Customer Center Configuration” page, select “Registration Code” and select next.
  • Fill in the registration details and click next.

Alternatively, use the following command to register the server:

 suse_register -a key=<your reg key> -a email=<your email address>

Appendix B: Example of the Registration.pm script

package SMT::Registration;

use strict;
use warnings;

use APR::Brigade ();
use APR::Bucket ();
use Apache2::Filter ();

use Apache2::RequestRec ();
use Apache2::RequestIO ();

use Apache2::Const -compile => qw(OK SERVER_ERROR :log MODE_READBYTES);
use APR::Const     -compile => qw(:error SUCCESS BLOCK_READ);

use constant IOBUFSIZE => 8192;

use SMT::Utils;
use DBI qw(:sql_types);

use Data::Dumper;
use DBI;
use XML::Writer;
use XML::Parser;

sub handler {
    my $r = shift;

    $r->content_type('text/xml');

    my $args = $r->args();
    my $hargs = {};

    if(! defined $args)
    {
        $r->log_error("Registration called without args.");
        return Apache2::Const::SERVER_ERROR;
    }

    foreach my $a (split(/\&/, $args))
    {
        chomp($a);
        my ($key, $value) = split(/=/, $a, 2);
        $hargs->{$key} = $value;
    }
    $r->log->info("Registration called with command: ".$hargs->{command});

    if(exists $hargs->{command} && defined $hargs->{command})
    {
        if($hargs->{command} eq "register")
        {
            SMT::Registration::register($r, $hargs);
        }
        elsif($hargs->{command} eq "listproducts")
        {
            SMT::Registration::listproducts($r, $hargs);
        }
        elsif($hargs->{command} eq "listparams")
        {
            SMT::Registration::listparams($r, $hargs);
        }
        else
        {
            $r->log_error("Unknown command: ".$hargs->{command});
            return Apache2::Const::SERVER_ERROR;
        }
    }
    else
    {
        $r->log_error("Missing command");
        return Apache2::Const::SERVER_ERROR;
    }

    return Apache2::Const::OK;
}

#
# called from handler if client wants to register
# command=register argument given
#
sub register
{
    my $r          = shift;
    my $hargs      = shift;

    my $usetestenv = 0;

    $r->log->info("register called");

    if(exists $hargs->{testenv} && $hargs->{testenv})
    {
	$usetestenv = 1;        
    }

# Added for acceptance environment    
# 1 = testing
# 2 = acceptance
    if(exists $hargs->{accenv} && $hargs->{accenv})
    {
        $usetestenv = 2;
    }
# end of modifications

    my $data = read_post($r);
    my $dbh = SMT::Utils::db_connect();
    if(!$dbh)
    {
        $r->log_error("Cannot open Database");
        die "Please contact your administrator.";
    }

    my $regroot = { ACCEPTOPT => 1, CURRENTELEMENT => "", PRODUCTATTR => {}, register => {}};
    my $regparser = XML::Parser->new( Handlers =>
                                      { Start => sub { reg_handle_start_tag($regroot, @_) },
                                        Char  => sub { reg_handle_char_tag($regroot, @_) },
                                        End   => sub { reg_handle_end_tag($regroot, @_) }
                                      });

    eval {
        $regparser->parse( $data );
    };
    if($@) {
        # ignore the errors, but print them
        chomp($@);
        $r->log_error("SMT::Registration::register Invalid XML: $@");
    }

    my $needinfo = SMT::Registration::parseFromProducts($r, $dbh, $regroot->{register}->{product}, "NEEDINFO");

    #$r->log_error("REGROOT:".Data::Dumper->Dump([$regroot]));

    my $output = "";
    my $writer = XML::Writer->new(NEWLINES => 0, OUTPUT => \$output);
    $writer->xmlDecl("UTF-8");

    my $dat = { CACHE => [],
                WRITECACHE => 0,
                INFOCOUNT => 0, 
                REGISTER => $regroot,
                PARAMDEPTH => 0,
                PARAMISMAND => 0,
                R => $r,
                WRITER => $writer
               };

    my $parser = XML::Parser->new( Handlers =>
                                   { Start=> sub { nif_handle_start_tag($dat, @_) },
                                     End=>   sub { nif_handle_end_tag($dat, @_) }
                                   });
    eval {
        $parser->parse( $needinfo );
    };
    if($@) {
        # ignore the errors, but print them
        chomp($@);
        $r->log_error("SMT::Registration::register Invalid XML: $@");
    }

    #$r->log_error("INFOCOUNT: ".$dat->{INFOCOUNT});

    if($dat->{INFOCOUNT} > 0)
    {
        $r->log->info("Return NEEDINFO: $output");

        # we need to send the <needinfo>
        print $output;
    }
    else
    {
        # we have all data; store it and send <zmdconfig>

        # get the os-target

        my $target = SMT::Registration::findTarget($r, $dbh, $regroot);

        # insert new registration data

        my $pidarr = SMT::Registration::insertRegistration($r, $dbh, $regroot, $target);

        # get the catalogs

        my $catalogs = SMT::Registration::findCatalogs($r, $dbh, $target, $pidarr);

        # send new <zmdconfig>

	my $zmdconfig = SMT::Registration::buildZmdConfig($r, $regroot->{register}->{guid}, $catalogs, $usetestenv);

        $r->log->info("Return ZMDCONFIG: $zmdconfig");

        print $zmdconfig;
    }
    $dbh->disconnect();

    return;
}

#
# called from handler if client wants the product list
# command=listproducts argument given
#
sub listproducts
{
    my $r     = shift;
    my $hargs = shift;

    $r->log->info("listproducts called");

    my $dbh = SMT::Utils::db_connect();
    if(!$dbh)
    {
        $r->log_error("Cannot connect to database");
        die "Please contact your administrator";
    }

    my $sth = $dbh->prepare("SELECT DISTINCT PRODUCT FROM Products where product_list = 'Y'");
    $sth->execute();

    my $output = "";
    my $writer = new XML::Writer(NEWLINES => 1, OUTPUT => \$output);
    $writer->xmlDecl('UTF-8');

    $writer->startTag("productlist",
                      "xmlns" => "http://www.novell.com/xml/center/regsvc-1_0",
                      "lang"  => "en");

    while ( my @row = $sth->fetchrow_array ) 
    {
        $writer->startTag("product");
        $writer->characters($row[0]);
        $writer->endTag("product");
    }
    $writer->endTag("productlist");

    $r->log->info("Return PRODUCTLIST: $output");

    print $output;

    $dbh->disconnect();

    return;
}

#
# called from handler if client wants to fetch the parameter list
# command=listparams argument given
#
sub listparams
{
    my $r     = shift;
    my $hargs = shift;

    $r->log->info("listparams called");

    my $lpreq = read_post($r);
    my $dbh = SMT::Utils::db_connect();

    my $data  = {STATE => 0, PRODUCTS => []};
    my $parser = XML::Parser->new( Handlers =>
                                   { Start=> sub { prod_handle_start_tag($data, @_) },
                                     Char => sub { prod_handle_char($data, @_) },
                                     End=>   sub { prod_handle_end_tag($data, @_) }
                                   });
    eval {
        $parser->parse( $lpreq );
    };
    if($@) {
        # ignore the errors, but print them
        chomp($@);
        $r->log_error("SMT::Registration::parseFromProducts Invalid XML: $@");
    }

    my $xml = SMT::Registration::parseFromProducts($r, $dbh, $data->{PRODUCTS}, "PARAMLIST");

    $r->log->info("Return PARAMLIST: $xml");

    print $xml;

    $dbh->disconnect();

    return;
}

###############################################################################

sub parseFromProducts
{
    my $r      = shift;
    my $dbh    = shift;
    my $productarray = shift;
    my $column = shift;

    my @list = findColumnsForProducts($r, $dbh, $productarray, $column);

    if(uc($column) eq "PARAMLIST" || uc($column) eq "NEEDINFO")
    {
        return SMT::Registration::mergeDocuments($r, \@list);
    }

    return "";
}

sub writeXML
{
    my $node = shift;
    my $writer = shift;

    my $element = ref($node);
    $element =~ s/^smt:://;

    return if($element eq "Characters");

    my %attr = %{$node};
    delete $attr{Kids};

    $writer->startTag($element, %attr);

    foreach my $child (@{$node->{Kids}})
    {
        writeXML($child, $writer);
    }

    $writer->endTag($element);
}

sub mergeXML
{
    my $node1 = shift;
    my $node2 = shift;

    foreach my $child2 (@{$node2->{Kids}})
    {
        my $found = 0;

        foreach my $child1 (@{$node1->{Kids}})
        {

            if(ref($child2) eq ref($child1))
            {
                if(ref($child2) eq "smt::param")
                {
                    # we have to match the id
                    if($child2->{id} eq $child1->{id})
                    {
                        $found = 1;
                        mergeXML($child1, $child2);
                    }
                }
                else
                {
                    $found = 1;
                    mergeXML($child1, $child2);

                }
            }
        }
        if(!$found)
        {
            # found something new in child2 - put it in child 1
            push @{$node1->{Kids}}, $child2;
        }
    }
}

sub mergeDocuments
{
    my $r    = shift;
    my $list = shift;

    my $basedoc = "";

    my $root1;
    my $node1;

    foreach my $other (@$list)
    {
        next if(!defined $other || $other eq "");
        if($basedoc eq "")
        {
            $basedoc = $other;
            my $p1 = XML::Parser->new(Style => 'Objects', Pkg => 'smt');
            eval {
                $root1 = $p1->parse( $basedoc );
                $node1 = $root1->[0];
            };
            if($@) {
                # ignore the errors, but print them
                chomp($@);
                $r->log_error("SMT::Registration::mergeDocuments Invalid XML: $@");
            }

            next;
        }
        next if($basedoc eq $other);

        my $p2 = XML::Parser->new(Style => 'Objects', Pkg => 'smt');
        eval {
            my $root2 = $p2->parse( $other );
            my $node2;

            if(ref($root1->[0]) eq ref($root2->[0]))
            {
                $node1 = $root1->[0];
                $node2 = $root2->[0];

                mergeXML($node1, $node2);
            }
        };
        if($@) {
            # ignore the errors, but print them
            chomp($@);
            $r->log_error("SMT::Registration::register Invalid XML: $@");
        }
    }

    my $output = "";
    my $w = XML::Writer->new(NEWLINES => 0, OUTPUT => \$output);
    $w->xmlDecl("UTF-8");

    writeXML($node1, $w);

    return $output;
}

sub insertRegistration
{
    my $r       = shift;
    my $dbh     = shift;
    my $regdata = shift;
    my $target  = shift || '';

    my $cnt     = 0;
    my $existingpids = {};
    my $regtimestring = "";
    my $hostname = "";

    my @list = findColumnsForProducts($r, $dbh, $regdata->{register}->{product}, "PRODUCTDATAID");

    my $statement = sprintf("SELECT PRODUCTID from Registration where GUID=%s", $dbh->quote($regdata->{register}->{guid}));
    $r->log->info("STATEMENT: $statement");
    eval {
        $existingpids = $dbh->selectall_hashref($statement, "PRODUCTID");

    };
    if($@)
    {
        $r->log_error("DBERROR: ".$dbh->errstr);
    }

    # store the regtime
    $regtimestring = SMT::Utils::getDBTimestamp();

    my @insert = ();
    my @update = ();

    foreach my $pnum (@list)
    {
        if(exists $existingpids->{$pnum})
        {
            # reg exists, do update
            push @update, $pnum;
            delete $existingpids->{$pnum};
        }
        else
        {
            # reg does not exist, do insert
            push @insert, $pnum;
        }
    }

    my @delete = keys %{$existingpids};

    if(@delete > 0)
    {
        $statement = sprintf("DELETE from Registration where GUID=%s AND PRODUCTID ", $dbh->quote($regdata->{register}->{guid}));
        if(@delete > 1)
        {
            $statement .= "IN (".join(",", @delete).")";
        }
        else
        {
            $statement .= "= ".$delete[0];
        }

        eval {
            $cnt = $dbh->do($statement);
            $r->log->info("STATEMENT: $statement  Affected rows: $cnt");
        };
        if($@)
        {
            $r->log_error("DBERROR: ".$dbh->errstr);
        }
    }

    foreach my $id (@insert)
    {
        eval {
            my $sth = $dbh->prepare("INSERT into Registration (GUID, PRODUCTID, REGDATE) VALUES (?, ?, ?)");
            $sth->bind_param(1, $regdata->{register}->{guid});
            $sth->bind_param(2, $id, SQL_INTEGER);
            $sth->bind_param(3, $regtimestring, SQL_TIMESTAMP);
            $cnt = $sth->execute;

            $r->log->info("STATEMENT: ".$sth->{Statement}." Affected rows: $cnt");
        };
        if($@)
        {
            $r->log_error("DBERROR: ".$dbh->errstr);
        }
    }

    if(@update > 0)
    {
        $statement = "UPDATE Registration SET REGDATE=? WHERE GUID=? AND PRODUCTID "; 

        if(@update > 1)
        {
            $statement .= "IN (".join(",", @update).")";
        }
        else
        {
            $statement .= "= ".$update[0];
        }

        eval {
            my $sth = $dbh->prepare($statement);
            $sth->bind_param(1, $regtimestring, SQL_TIMESTAMP);
            $sth->bind_param(2, $regdata->{register}->{guid});
            $cnt = $sth->execute;
            $r->log->info("STATEMENT: ".$sth->{Statement}."  Affected rows: $cnt");
        };
        if($@)
        {
            $r->log_error("DBERROR: ".$dbh->errstr);
        }
    }

    #
    # clean old machinedata
    #
    $cnt = 0;
    $statement = sprintf("DELETE from MachineData where GUID=%s", $dbh->quote($regdata->{register}->{guid}));
    eval {
        $cnt = $dbh->do($statement);
        $r->log->info("STATEMENT: $statement  Affected rows: $cnt");
    };
    if($@)
    {
        $r->log_error("DBERROR: ".$dbh->errstr);
    }

    #
    # insert new machinedata
    #
    foreach my $key (keys %{$regdata->{register}})
    {
        next if($key eq "guid" || $key eq "product" || $key eq "mirrors");
        if($key eq "hostname")
        {
            $hostname = $regdata->{register}->{$key};
        }

        my $statement = sprintf("INSERT into MachineData (GUID, KEYNAME, VALUE) VALUES (%s, %s, %s)",
                                $dbh->quote($regdata->{register}->{guid}), 
                                $dbh->quote($key),
                                $dbh->quote($regdata->{register}->{$key}));
        $r->log->info("STATEMENT: $statement");
        eval {
            $dbh->do($statement);
        };
        if($@)
        {
            $r->log_error("DBERROR: ".$dbh->errstr);
        }
    }

    #
    # if we do not have the hostname, try to get the IP address
    #
    if($hostname eq "")
    {
        $hostname = $r->connection()->remote_ip();
    }

    #
    # update Clients table
    #
    my $aff = 0;
    if($hostname ne "")
    {
        eval
        {
            my $sth = $dbh->prepare("UPDATE Clients SET HOSTNAME=?, TARGET=?, LASTCONTACT=? WHERE GUID=?");
            $sth->bind_param(1, $hostname);
            $sth->bind_param(2, $target);
            $sth->bind_param(3, $regtimestring, SQL_TIMESTAMP);
            $sth->bind_param(4, $regdata->{register}->{guid});
            $aff = $sth->execute;

            $r->log->info("STATEMENT: $statement");
        };
        if($@)
        {
            $r->log_error("DBERROR: ".$dbh->errstr);
            $aff = 0;
        }
        if($aff == 0)
        {
            # New registration; we need an insert
            $statement = sprintf("INSERT INTO Clients (GUID, HOSTNAME, TARGET) VALUES (%s, %s, %s)", 
                                 $dbh->quote($regdata->{register}->{guid}),
                                 $dbh->quote($hostname),
                                 $dbh->quote($target));
            $r->log->info("STATEMENT: $statement");
            eval
            {
                $aff = $dbh->do($statement);
            };
            if($@)
            {
                $r->log_error("DBERROR: ".$dbh->errstr);
                $aff = 0;
            }
        }
    }
    else
    {
        eval
        {
            my $sth = $dbh->prepare("UPDATE Clients SET TARGET=?, LASTCONTACT=? WHERE GUID=?");
            $sth->bind_param(1, $target);
            $sth->bind_param(2, $regtimestring, SQL_TIMESTAMP);
            $sth->bind_param(3, $regdata->{register}->{guid});
            $aff = $sth->execute;

            $r->log->info("STATEMENT: $statement");
        };
        if($@)
        {
            $r->log_error("DBERROR: ".$dbh->errstr);
            $aff = 0;
        }
        if($aff == 0)
        {
            # New registration; we need an insert
            $statement = sprintf("INSERT INTO Clients (GUID, TARGET) VALUES (%s, %s)", 
                                 $dbh->quote($regdata->{register}->{guid}),
                                 $dbh->quote($target));
            $r->log->info("STATEMENT: $statement");
            eval
            {
                $aff = $dbh->do($statement);
            };
            if($@)
            {
                $r->log_error("DBERROR: ".$dbh->errstr);
                $aff = 0;
            }
        }
    }

    return \@list;
}

sub findTarget
{
    my $r       = shift;
    my $dbh     = shift;
    my $regroot = shift;

    my $result  = undef;

    if(exists $regroot->{register}->{ostarget} && defined $regroot->{register}->{ostarget} &&
       $regroot->{register}->{ostarget} ne "")
    {
        my $statement = sprintf("SELECT TARGET from Targets WHERE OS=%s", $dbh->quote($regroot->{register}->{ostarget})) ;
        $r->log->info("STATEMENT: $statement");

        my $target = $dbh->selectcol_arrayref($statement);

        if(exists $target->[0])
        {
            $result = $target->[0];
        }
    }
    elsif(exists $regroot->{register}->{"ostarget-bak"} && defined $regroot->{register}->{"ostarget-bak"} &&
          $regroot->{register}->{"ostarget-bak"} ne "")
    {
        my $targetString = $regroot->{register}->{"ostarget-bak"};
        $targetString =~ s/^\s*"//;
        $targetString =~ s/"\s*$//;

        my $statement = sprintf("SELECT TARGET from Targets WHERE OS=%s", $dbh->quote($targetString)) ;
        $r->log->info("STATEMENT: $statement");

        my $target = $dbh->selectcol_arrayref($statement);

        if(exists $target->[0])
        {
            $result = $target->[0];
        }
    }
    return $result;
}

sub findCatalogs
{
    my $r      = shift;
    my $dbh    = shift;
    my $target = shift;
    my $productids = shift;

    my $result = {};
    my $statement ="";

    # get catalog values (only for the once we DOMIRROR)

    $statement  = "SELECT c.CATALOGID, c.NAME, c.DESCRIPTION, c.TARGET, c.LOCALPATH, c.CATALOGTYPE from Catalogs c, ProductCatalogs pc WHERE ";

    $statement .= "pc.OPTIONAL='N' AND c.DOMIRROR='Y' AND c.CATALOGID=pc.CATALOGID ";
    $statement .= "AND (c.TARGET IS NULL ";
    if(defined $target && $target ne "")
    {
        $statement .= sprintf("OR c.TARGET=%s", $dbh->quote($target));
    }
    $statement .= ") AND ";

    if(@{$productids} > 1)
    {
        $statement .= "pc.PRODUCTDATAID IN (".join(",", @{$productids}).") ";
    }
    elsif(@{$productids} == 1)
    {
        $statement .= "pc.PRODUCTDATAID = ".$productids->[0]." ";
    }
    else
    {
        # This should not happen
        $r->log_error("No productids found");
        return $result;
    }

    $r->log->info("STATEMENT: $statement");

    $result = $dbh->selectall_hashref($statement, "CATALOGID");

    $r->log->info("RESULT: ".Data::Dumper->Dump([$result]));

    return $result;
}

sub buildZmdConfig
{
    my $r          = shift;
    my $guid       = shift;
    my $catalogs   = shift;
    my $usetestenv = shift || 0;

    my $cfg = undef;

    eval
    {
        $cfg = SMT::Utils::getSMTConfig();
    };
    if($@ || !defined $cfg)
    {
        $r->log_error("Cannot read the SMT configuration file: ".$@);
        die "SMT server is missconfigured. Please contact your administrator.";
    }

    my $LocalNUUrl = $cfg->val('LOCAL', 'url');
    $LocalNUUrl =~ s/\s*$//;
    if(!defined $LocalNUUrl || $LocalNUUrl !~ /^http/)
    {
        $r->log_error("Invalid url parameter in smt.conf. Please fix the url parameter in the [LOCAL] section.");
        die "SMT server is missconfigured. Please contact your administrator.";
    }

# Original
# if($usetestenv)
#    {
#        $LocalNUUrl    .= "/testing/";
#    }
# added for acceptance environment
    if($usetestenv == 1)
    {
        $LocalNUUrl    .= "/testing/";
    }

    if($usetestenv == 2)
    {
        $LocalNUUrl    .= "/acceptance/";
    }
# End of modifications

    my $nuCatCount = 0;
    foreach my $cat (keys %{$catalogs})
    {
        $nuCatCount++ if(lc($catalogs->{$cat}->{CATALOGTYPE}) eq "nu");
    }

    my $output = "";
    my $writer = new XML::Writer(OUTPUT => \$output);

    $writer->xmlDecl("UTF-8");
    $writer->startTag("zmdconfig", 
                      "xmlns" => "http://www.novell.com/xml/center/regsvc-1_0",
                      "lang"  => "en");

    $writer->startTag("guid");
    $writer->characters($guid);
    $writer->endTag("guid");

    # first write all catalogs of type NU
    if($nuCatCount > 0)
    {
        $writer->startTag("service", 
                          "id"          => "local_nu_server",
                          "description" => "Local NU Server",
                          "type"        => "nu");
        $writer->startTag("param", "id" => "url");
        $writer->characters($LocalNUUrl);
        $writer->endTag("param");

        foreach my $cat (keys %{$catalogs})
        {
            next if(lc($catalogs->{$cat}->{CATALOGTYPE}) ne "nu");
            if(! exists $catalogs->{$cat}->{LOCALPATH} || ! defined $catalogs->{$cat}->{LOCALPATH} ||
               $catalogs->{$cat}->{LOCALPATH} eq "")
            {
                $r->log_error("Path for catalog '$cat' does not exists. Skipping Catalog.");
                next;
            }

            $writer->startTag("param", 
                              "name" => "catalog",
                              "url"  => "$LocalNUUrl/repo/".$catalogs->{$cat}->{LOCALPATH}
                             );
            $writer->characters($catalogs->{$cat}->{NAME});
            $writer->endTag("param");
        }
        $writer->endTag("service");
    }

    # and now the zypp Repositories

    foreach my $cat (keys %{$catalogs})
    {
        next if(lc($catalogs->{$cat}->{CATALOGTYPE}) ne "zypp");
        if(! exists $catalogs->{$cat}->{LOCALPATH} || ! defined $catalogs->{$cat}->{LOCALPATH} ||
           $catalogs->{$cat}->{LOCALPATH} eq "")
        {
            $r->log_error("Path for catalog '$cat' does not exists. Skipping Catalog.");
            next;
        }

        $writer->startTag("service", 
                          "id"          => $catalogs->{$cat}->{NAME},
                          "description" => $catalogs->{$cat}->{DESCRIPTION},
                          "type"        => "zypp");
        $writer->startTag("param", "id" => "url");
        $writer->characters("$LocalNUUrl/repo/".$catalogs->{$cat}->{LOCALPATH});
        $writer->endTag("param");

        $writer->startTag("param", "name" => "catalog");
        $writer->characters($catalogs->{$cat}->{NAME});
        $writer->endTag("param");

        $writer->endTag("service");
    }

    $writer->endTag("zmdconfig");

    return $output;
}

sub findColumnsForProducts
{
    my $r      = shift;
    my $dbh    = shift;
    my $parray = shift;
    my $column = shift;

    my @list = ();

    foreach my $phash (@{$parray})
    {
        my $statement = "SELECT $column, PRODUCTLOWER, VERSIONLOWER, RELLOWER, ARCHLOWER FROM Products where ";

        $statement .= "PRODUCTLOWER = ".$dbh->quote(lc($phash->{name}));

        $statement .= " AND (";
        $statement .= "VERSIONLOWER=".$dbh->quote(lc($phash->{version}))." OR " if(defined $phash->{version} && $phash->{version} ne "");
        $statement .= "VERSIONLOWER IS NULL)";

        $statement .= " AND (";
        $statement .= "RELLOWER=".$dbh->quote(lc($phash->{release}))." OR " if(defined $phash->{release} && $phash->{release} ne "");
        $statement .= "RELLOWER IS NULL)";

        $statement .= " AND (";
        $statement .= "ARCHLOWER=".$dbh->quote(lc($phash->{arch}))." OR " if(defined $phash->{arch} && $phash->{arch} ne "");
        $statement .= "ARCHLOWER IS NULL)";

        $r->log_rerror(Apache2::Log::LOG_MARK, Apache2::Const::LOG_INFO,
                       APR::Const::SUCCESS, "STATEMENT: $statement");

        my $pl = $dbh->selectall_arrayref($statement, {Slice => {}});

        #$r->log_error("RESULT: ".Data::Dumper->Dump([$pl]));
        #$r->log_error("RESULT: not defined ") if(!defined $pl);
        #$r->log_error("RESULT: empty ") if(@$pl == 0);

        if(@$pl == 1)
        {
            # Only one match found. 
            push @list, $pl->[0]->{$column};
        }
        elsif(@$pl > 1)
        {
            my $found = 0;
            # Do we have an exact match?
            foreach my $prod (@$pl)
            {
                if(lc($prod->{VERSIONLOWER}) eq lc($phash->{version}) &&
                   lc($prod->{ARCHLOWER}) eq  lc($phash->{arch})&&
                   lc($prod->{RELLOWER}) eq lc($phash->{release}))
                {
                    # Exact match found.
                    push @list, $prod->{$column};
                    $found = 1;
                    last;
                }
            }
            if(!$found)
            {
                $r->log_error("No exact match found: ".$phash->{name}." ".$phash->{version}." ".$phash->{release}." ".$phash->{arch}." Choose the first one.");
                push @list, $pl->[0]->{$column};
            }
        }
        else
        {
            $r->log_error("No Product match found: ".$phash->{name}." ".$phash->{version}." ".$phash->{release}." ".$phash->{arch});
        }
    }
    return @list;
}

#
# read the content of a POST and return the data
#
sub read_post {
    my $r = shift;

    my $bb = APR::Brigade->new($r->pool,
                               $r->connection->bucket_alloc);

    my $data = '';
    my $seen_eos = 0;
    do {
        $r->input_filters->get_brigade($bb, Apache2::Const::MODE_READBYTES,
                                       APR::Const::BLOCK_READ, IOBUFSIZE);

        for (my $b = $bb->first; $b; $b = $bb->next($b)) {
            if ($b->is_eos) {
                $seen_eos++;
                last;
            }

            if ($b->read(my $buf)) {
                $data .= $buf;
            }

            $b->remove; # optimization to reuse memory
        }

    } while (!$seen_eos);

    $bb->destroy;

    $r->log->info("Got content: $data");

    return $data;
}

###############################################################################
### XML::Parser Handler
###############################################################################

sub prod_handle_start_tag
{
    my $data = shift;
    my( $expat, $element, %attrs ) = @_;

    if(lc($element) eq "product")
    {
        $data->{STATE} = 1;
        foreach (keys %attrs)
        {
            $data->{CURRENT}->{lc($_)} = $attrs{$_};
        }
    }
}

sub prod_handle_char
{
    my $data = shift;
    my( $expat, $string) = @_;

    if($data->{STATE} == 1)
    {
        chomp($string);
        if(!exists $data->{CURRENT}->{name} || !defined $data->{CURRENT}->{name})
        {
            $data->{CURRENT}->{name} = $string;
        }
        else
        {
            $data->{CURRENT}->{name} .= $string;
        }
    }
}

sub prod_handle_end_tag
{
    my $data = shift;
    my( $expat, $element) = @_;

    if($data->{STATE} == 1)
    {
        push @{$data->{PRODUCTS}}, $data->{CURRENT};
        $data->{CURRENT} = undef;
        $data->{STATE} = 0;
    }
}

sub reg_handle_start_tag
{
    my $data = shift;
    my( $expat, $element, %attrs ) = @_;

    if(lc($element) eq "param")
    {
        if(exists $attrs{id} && defined $attrs{id} && $attrs{id} ne "")
        {
            $data->{CURRENTELEMENT} = $attrs{id};
            # empty params are allowed, so we create the node here
            $data->{register}->{$data->{CURRENTELEMENT}} = "";
        }
    }
    elsif(lc($element) eq "product")
    {
        $data->{CURRENTELEMENT} = lc($element);
        $data->{PRODUCTATTR} = \%attrs;
    }
    elsif(lc($element) eq "register")
    {
        if(exists $attrs{accept} && defined $attrs{accept} && lc($attrs{accept}) eq "mandatory")
        {
            $data->{ACCEPTOPT} = 0;
        }        
    }
    elsif(lc($element) eq "host")
    {
        $data->{CURRENTELEMENT} = lc($element);
        # empty host is allowed, so we create the node here
        $data->{register}->{$data->{CURRENTELEMENT}} = "";
    }
    else
    {
        $data->{CURRENTELEMENT} = lc($element);
    }
}

sub reg_handle_char_tag
{
    my $data = shift;
    my( $expat, $string) = @_;

    if($data->{CURRENTELEMENT} ne "" && $data->{CURRENTELEMENT} ne "product")
    {
        if(exists $data->{register}->{$data->{CURRENTELEMENT}} &&
           defined $data->{register}->{$data->{CURRENTELEMENT}} &&
           $data->{register}->{$data->{CURRENTELEMENT}} ne "")
        {
            $data->{register}->{$data->{CURRENTELEMENT}} .= $string;
        }
        else
        {
            $data->{register}->{$data->{CURRENTELEMENT}} = $string;
        }
    }
    elsif($data->{CURRENTELEMENT} eq "product")
    {
        if(exists $data->{PRODUCTATTR}->{name} &&
           defined $data->{PRODUCTATTR}->{name} &&
           $data->{PRODUCTATTR}->{name} ne "")
        {
            $data->{PRODUCTATTR}->{name} .= $string;
        }
        else
        {
            $data->{PRODUCTATTR}->{name} = $string;
        }
    }
}

sub reg_handle_end_tag
{
    my $data = shift;
    my( $expat, $element) = @_;

    if(lc($element) eq "product")
    {
        if(!exists $data->{register}->{product})
        {
            $data->{register}->{product} = [];
        }
        push @{$data->{register}->{product}}, $data->{PRODUCTATTR};
    }
}

sub nif_handle_start_tag
{
    my $data = shift;
    my( $expat, $element, %attrs ) = @_;

    if(lc($element) eq "needinfo")
    {
        $data->{WRITER}->startTag(lc($element), %attrs);
    }
    elsif(lc($element) eq "guid")
    {
        if(!exists $data->{REGISTER}->{register}->{lc($element)})
        {
            $data->{WRITER}->emptyTag(lc($element), %attrs);
            $data->{INFOCOUNT} += 1;
        }           
    }
    elsif(lc($element) eq "host")
    {
        if(!exists $data->{REGISTER}->{register}->{lc($element)})
        {
            $data->{WRITER}->emptyTag(lc($element), %attrs);
            $data->{INFOCOUNT} += 1;
        }              
    }
    elsif(lc($element) eq "product")
    {
        if(!exists $data->{REGISTER}->{register}->{lc($element)})
        {
            $data->{WRITER}->emptyTag(lc($element), %attrs);
            $data->{INFOCOUNT} += 1;
        }
    }
    elsif(lc($element) eq "privacy")
    {
        if(!exists $data->{REGISTER}->{register}->{lc($element)})
        {
            $data->{WRITER}->emptyTag(lc($element), %attrs);
        }
    }
    elsif(lc($element) eq "param")
    {
        my $resetmandhere = 0;

        $data->{PARAMDEPTH} += 1;

        if($#{$data->{CACHE}} >= 0)
        {
            $data->{CACHE}->[$#{$data->{CACHE}}]->{SKIP} = 0;
        }

        if(exists $attrs{class} && defined $attrs{class} && lc($attrs{class}) eq "mandatory")
        {
            $data->{PARAMISMAND} = 1;
            $resetmandhere = 1;
        }

        if(exists $attrs{id} && defined $attrs{id})
        {
            if(exists $data->{REGISTER}->{register}->{$attrs{id}})
            {
                # skip this, it is already there
            }
            elsif(exists $attrs{command} && defined $attrs{command})
            {
                if($data->{REGISTER}->{ACCEPTOPT} || (!$data->{REGISTER}->{ACCEPTOPT} && $data->{PARAMISMAND}))
                {
                    # we do not have a value for this command
                    # if we accept optional => write it
                    #   OR
                    # if the param is mandatory

                    push @{$data->{CACHE}}, { SKIP      => 0, 
                                              MUST      => 1,
                                              ELEMENT   => lc($element),
                                              ATTRS     => \%attrs,
                                              WRITTEN   => 0,
                                              RESETMAND => $resetmandhere
                                            };
                    $data->{WRITECACHE} = 1;
                    $data->{INFOCOUNT} += 1;
                    return;
                }
            }
            else
            {
                # Hmmm, maybe we need this later. We need to switch SKIP to 0 if the next
                # element is a start element
                push @{$data->{CACHE}}, { SKIP      => 1,
                                          MUST      => 0,
                                          ELEMENT   => lc($element),
                                          ATTRS     => \%attrs,
                                          WRITTEN   => 0,
                                          RESETMAND => $resetmandhere
                                        };
                return;
            }
        }
        push @{$data->{CACHE}}, { SKIP      => 1, 
                                  MUST      => 0,
                                  ELEMENT   => lc($element),
                                  ATTRS     => \%attrs,
                                  WRITTEN   => 0,
                                  RESETMAND => $resetmandhere
                                };
    }
    elsif(lc($element) eq "select")
    {
        my $resetmandhere = 0;

        $data->{PARAMDEPTH} += 1;

        if(exists $attrs{class} && defined $attrs{class} && lc($attrs{class}) eq "mandatory")
        {
            $data->{PARAMISMAND} = 1;
            $resetmandhere = 1;
        }
        push @{$data->{CACHE}}, { SKIP      => 0, 
                                  MUST      => 0,
                                  ELEMENT   => lc($element),
                                  ATTRS     => \%attrs,
                                  WRITTEN   => 0,
                                  RESETMAND => $resetmandhere
                                };
    }

}

sub nif_handle_end_tag
{
    my $data = shift;
    my( $expat, $element) = @_;

    if(lc($element) eq "param" || lc($element) eq "select")
    {
        $data->{PARAMDEPTH} -= 1;

        if($data->{CACHE}->[(@{$data->{CACHE}}-1)]->{SKIP})
        {
            my $msg = "SKIP CACHE element:".$data->{CACHE}->[(@{$data->{CACHE}}-1)]->{ELEMENT};
            if(exists $data->{CACHE}->[(@{$data->{CACHE}}-1)]->{ATTRS}->{id})
            {
                $msg .= " id=".$data->{CACHE}->[(@{$data->{CACHE}}-1)]->{ATTRS}->{id}
            }
            $data->{R}->log->info($msg);
            my $entry = pop @{$data->{CACHE}};
            if($entry->{RESETMAND})
            {
                $data->{PARAMISMAND} = 0;
            }
            return;
        }

        my $mustwrite = 0;
        for(my $i = 0; $i < @{$data->{CACHE}}; $i++)
        {
            $mustwrite = 1 if($data->{CACHE}->[$i]->{MUST});
        }

        if(!$mustwrite)
        {
            # skip last 
            my $msg = "SKIP CACHE (No MUST) element:".$data->{CACHE}->[(@{$data->{CACHE}}-1)]->{ELEMENT};
            if(exists $data->{CACHE}->[(@{$data->{CACHE}}-1)]->{ATTRS}->{id})
            {
                $msg .= " id=".$data->{CACHE}->[(@{$data->{CACHE}}-1)]->{ATTRS}->{id}
            }
            $data->{R}->log->info($msg);
            my $entry = pop @{$data->{CACHE}};
            if($entry->{RESETMAND})
            {
                $data->{PARAMISMAND} = 0;
            }
            return;
        }

        for(my $i = 0; $i < @{$data->{CACHE}}; $i++)
        {
            if(!$data->{CACHE}->[$i]->{WRITTEN})
            {
                my $msg = "Write CACHE START element:".$data->{CACHE}->[$i]->{ELEMENT};
                if(exists $data->{CACHE}->[$i]->{ATTRS}->{id})
                {
                    $msg .= " id=".$data->{CACHE}->[$i]->{ATTRS}->{id}
                }

                $data->{R}->log->info($msg);
                $data->{WRITER}->startTag($data->{CACHE}->[$i]->{ELEMENT}, %{$data->{CACHE}->[$i]->{ATTRS}});
                $data->{CACHE}->[$i]->{WRITTEN} = 1;
                $data->{CACHE}->[$i]->{MUST} = 1;
            }
        }

        # write the last end element
        my $d = pop @{$data->{CACHE}};

        if($d->{WRITTEN})
        {
            $data->{R}->log->info("Write CACHE END element:".$d->{ELEMENT});
            $data->{WRITER}->endTag($d->{ELEMENT});
        }
        if($d->{RESETMAND})
        {
            $data->{PARAMISMAND} = 0;
        }

        if($data->{PARAMDEPTH} <= 0)
        {
            $data->{PARAMDEPTH}  = 0;
            $data->{CACHE} = [];
        }

    }
    elsif(lc($element) eq "needinfo")
    {
        $data->{WRITER}->endTag(lc($element));
    }
}

1;
VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Tags: ,
Categories: SUSE Linux Enterprise, SUSE Linux Enterprise Server, Technical Solutions

Disclaimer: As with everything else at SUSE Conversations, this content is definitely not supported by SUSE (so don't even think of calling Support if you try something and it blows up).  It was contributed by a community member and is published "as is." It seems to have worked for at least one person, and might work for you. But please be sure to test, test, test before you do anything drastic with it.

1 Comment

  1. By:gsanjeev

    smt-catalogs -verbose helped me to get the url of downloads.

Comment

RSS