#
# $Id: Device.pm,v 1.29 2003/03/02 11:12:09 dsw Exp $
#
# COPYRIGHT AND LICENSE
# Copyright (c) 2001-2003, Juniper Networks, Inc.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# 3. The name of the copyright owner may not be used to
# endorse or promote products derived from this software without specific
# prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
#
# JUNOS::Device: This package implements an interface to the JUNOScript (tm)
# XML-based API supported by Juniper Networks. Objects of this class represent
# the local side of connection to a Juniper Networks device running JUNOS,
# over which the JUNOScript protocol will be spoken. JUNOScript is
# described on http://xml.juniper.net.
#
package JUNOS::Device;
use strict;
use vars qw(@ISA $VERSION);
use JUNOS::Access;
use JUNOS::DOM::Parser;
use JUNOS::Response;
use JUNOS::Trace;
use JUNOS::Methods;
use XML::Parser;
@ISA = qw(JUNOS::Methods);
$| = 1;
use JUNOS::version;
#
# Create a new JUNOS::Device.
#
sub new
{
my($class, %args) = @_;
my $self = { %args };
my $version = $self->{version} || $VERSION;
# Initialize Methods
JUNOS::Methods::init($version);
# Register the default callbacks
$self->{JUNOS_CallbackHandler} ||= \&callbackHandler;
$self->{JUNOS_ReplyHandler} ||= \&replyHandler;
$self->{JUNOS_CharHandler} ||= \&charHandler;
# Mark ourselves with the proper class
$class = ref($class) || $class;
bless $self, $class;
# Bring up the connection
unless ($self->{Do_Not_Connect}) {
return $self->connect();
}
$self;
}
#
# Open the connection to the JUNOScript server. This is done by the 'new'
# operator, but can be performed explicitly by hand.
#
sub connect
{
my($self) = @_;
$self->clear_errors() if caller() ne __PACKAGE__;
# Have we already connected? Silently succeed
return $self if $self->{JUNOS_Conn};
# The access field/class/key/type is the handle we
# use to talk to the Juniper box.
my $conn = new JUNOS::Access($self);
# Need better error handling here....
unless (ref($conn)){
$self->report_error("Could not open connection");
return;
}
eval "use " . ref($conn);
# Record the connection; connect it, mark it
$self->{JUNOS_Conn} = $conn;
unless ($conn->connect()) {
$self->report_error("Could not connect");
return;
}
$self->{JUNOS_Connected} = 1;
# Kick off the XML parser
$self->parse_start();
trace("Trace", "starting connect::\n");
# We need to receive the server side of the initial handshake first
# (at least the part), so that we can avoid sending our
# handshake to the ssh processes initial prompts (password/etc).
# So we wait til we see the start of the real XML data flow....
until ($self->{JUNOS_Active}) {
my $in = $conn->recv();
my $waiting = 'waiting for xml';
if( $conn->{seen_xml} ) { $waiting = 'found xml'; }
trace("IO", "during connect - ($waiting) input:\n\t$in\n" );
if ($conn->{seen_xml}) {
# After we've seen xml, parse anything
} elsif ($in =~ /<\s*\?/) {
$in =~ s/^[\d\D]*(<\s*\?)/$1/;
$conn->{seen_xml} = 1;
} else {
if (not $conn->incoming($in) or $conn->eof) {
$self->report_error("initial handshake with JUNOScript server failed");
$self->disconnect;
return;
}
next;
}
if ($conn->eof) {
$self->parse_done($in);
last;
} else {
$self->parse_more($in);
}
}
# Send our half of the initial handshake
my $xml_decl = '';
my $junoscript = '';
$conn->send($xml_decl . "\n" . $junoscript . "\n");
if (!$conn->authenticate) {
$self->report_error("Authentication error");
exit(1);
}
return $self;
}
#
# Disconnect from the JUNOScript server. Destroy the parser and
# disconnect (and close) the connection.
# User program should first call 'request_end_session'
#
sub disconnect
{
my($self) = @_;
$self->clear_errors() if caller() ne __PACKAGE__;
my $conn = $self->{JUNOS_Conn};
$conn->disconnect if ($conn and $self->{JUNOS_Connected});
$self->parse_destroy();
$self->{JUNOS_Conn} = $self->{JUNOS_Connected} = undef;
1;
}
#
# Automatic form of destory
#
sub DESTROY
{
my($self) = @_;
$self->disconnect() if $self->{JUNOS_Connected};
}
#
# Send a request to the JUNOScript server and return the result.
# Since the normal DOM top level will always contain only the
# node for the rpc-reply, we assume the caller really just wants
# that node. If called in an scalar context, we only return this
# node. In an array context, we return the document and the node
# as an array.
#
sub request
{
my $rpc;
my($self, $request) = @_;
$self->clear_errors() if caller() ne __PACKAGE__;
tracept("Request");
$self->connect() || return unless $self->{JUNOS_Connected};
my $conn = $self->{JUNOS_Conn};
# Catches calling 'request_end_session' twice amongst other calamities
if ($conn->{seen_eof}) {
$self->report_error("connection ended unexpectedly");
return;
}
# If the caller gives us an object, turn it into a string.
if (ref($request)) {
$rpc = $request->toString;
} else {
$rpc = $request;
}
trace("Trace", "starting rpc; ", ref($request), "sending::\n", $rpc);
trace("Verbose", "--- begin request---\n",
$rpc, ($rpc =~ /\n$/) ? "" : "\n", "--- end request ---\n");
# Send the request to the JUNOScript server
my $back = $conn->send($rpc);
# Pull data off the server until we get a complete reply (or eof).
until ($self->{JUNOS_Reply}) {
my $in = $conn->recv();
trace("Trace", "during rpc; got::\n $in");
if ($conn->eof) {
# Make sure it's legit before handing it off
$self->parse_done($in) if (contains_end_tag($in));
$conn->{seen_eof} = 1;
last;
} else {
# This is for the case when using telnet & after we're done
# running 'junoscript' we get the shell char back
if ($self->parse_more($in))
{
$conn->{seen_eof} = 1;
last;
}
}
}
tracept("Request");
# Fetch the XML::DOM::Document, as saved by replyHandler()
my $doc = $self->{JUNOS_Reply};
trace("Trace", "reply is ", ref($doc) || "empty", "::", $doc);
unless ($doc) {
$self->report_error("reply is empty");
return;
}
# Clear the reply
undef $self->{JUNOS_Reply};
# Turn the response into a JUNOS::Response, allowing us
# to add methods on top of the DOM methods.
my $response = JUNOS::Response->new($doc->getDocumentElement());
trace("Verbose", "--- begin reply---\n",
$response->toString, ($response =~ /\n$/) ? "" : "\n",
"--- end reply ---\n");
# If the caller wants an array, we give them both the document and
# the response, which they must dispose of.
return ($doc, $response) if wantarray;
# Otherwise dispose of the document, which was only needed for DOM.
if ($#$response >= 0) {
$doc->removeChild($response);
$response->setOwnerDocument(undef);
$doc->dispose;
}
return $response;
}
#
# Perform an rpc using a raw command string. This is a mostly unsupported
# way of getting to any JUNOS command that is currently unsupported in
# JUNOScript. Caveat coder.
#
sub command
{
tracept("Request");
my($self, $request) = @_;
$self->clear_errors() if caller() ne __PACKAGE__;
my $rpc = "" . $request . "\n";
$self->request($rpc);
}
# These callback handlers are called by the parser to let
# us know important things about the connection.
#
# unsupported.callback
#
sub callbackHandler
{
my($self, $parser) = @_;
$self->report_error("unsupported callback");
}
#
# A reply is complete; record it for the main loop above.
#
sub replyHandler
{
tracept("Reply");
my($self, $parser, $reply) = @_;
$self->{JUNOS_Reply} = $reply;
}
#
# Raw character data is available that the parser does not want. This is
# normally because the connection is not yet in XML mode. Hand the data
# off to the access method so that it can deal with it.
#
sub charHandler
{
my($self, $parser, $data) = @_;
my $conn = $self->{JUNOS_Conn};
$conn->incoming($data);
}
#
# These functions are wrappers for the parser
#
#
# Start the parser: create an XML::Parser with style==JUNOS, pull
# a parser instance off this expat-parser-instance, and make
# the world even more confusing by making these objects refer
# to each other.
#
sub parse_start
{
tracept("Parse");
my($self, %args) = @_;
$args{Style} = "JUNOS::DOM::Parser";
#$args{Style} = "Debug";
my $expat = new XML::Parser(%args);
$self->{JUNOS_Expat} = $expat;
my $parser = $expat->parse_start();
$parser->{JUNOS_Device} = $self;
$self->{JUNOS_Parser} = $parser;
}
sub contains_end_tag
{
$_[0] =~ m##m;
}
#
# Parse some more input: toss the given input data to the parser.
#
sub parse_more
{
tracept("Parse");
my($self, $input) = @_;
my $done = 0;
# Get rid of any xtra stuff after closing tag if it's there
$done = 1 if ($input =~ s#(.*)$##ms);
$self->{sofar} .= $input;
my $parser = $self->{JUNOS_Parser};
while ($input =~ /<\/xnm:error>(?:\s*