Group
Extension

RundeckAPI/lib/RundeckAPI.pm

#!/usr/bin/perl -w

###########################################################################
# $Id: rundeckAPI.pm, v1.0 r1 04/02/2020 13:58:58 CET XH Exp $
#
# Copyright 2020 Xavier Humbert <xavier.humbert@ac-nancy-metz.fr>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program ;  if not, write to the
# Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA
#
###########################################################################

#** @class RundeckAPI
# RundeckAPI - simplifies authenticate, connect, queries to a Rundeck instance via REST API
#*

package RundeckAPI;


use strict;
use warnings;
use POSIX qw(setlocale strftime);
use File::Basename;
use LWP::UserAgent;
use Data::Dumper;
use HTTP::Cookies;
use REST::Client;
use Scalar::Util qw(reftype);
use JSON;
use Storable qw(dclone);
use Exporter qw(import);

our @EXPORT_OK = qw(get post put delete postData putData);

#####
## CONSTANTS
#####
our $TIMEOUT = 10;
our $VERSION = "1.3.8.0";
our $APIVERSION = '37';

#####
## VARIABLES
#####

#####
## CONSTRUCTOR
#####

#** @method public new (parameters)
# @ creates a connection
#
# @param value  %options
# @retval value the created class
#*


sub new {
	my($class, %args) = @_;
	my $rc = 403;
	my $self = {
		'url'		=> $args{'url'},
		'login'		=> $args{'login'},
		'password'	=> $args{'password'},
		'token'		=> $args{'token'},
		'debug'		=> $args{'debug'} //= 0,
		'verbose'	=> $args{'verbose'} //= 0,
		'result'	=> undef,
		'apivers'	=> $args{'apivers'} //= $APIVERSION,
		'timeout'	=> $args{'timeout'} //= $TIMEOUT,
	};
# create and store a cookie jar
	my $cookie_jar = HTTP::Cookies->new(
		autosave		=> 1,
		ignore_discard	=> 1,
	);
	$self->{'cookie_jar'} = $cookie_jar;

# with this cookie, cretae an User-Agent
	my($prog, $dirs, $suffix) = fileparse($0, (".pl"));
	my $ua = LWP::UserAgent->new(
		'agent'			=> $prog . "-" . $VERSION,
		'timeout'		=> $self->{'timeout'},
		'cookie_jar'	=> $self->{'cookie_jar'},
		'requests_redirectable' => ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
		);
	$ua->show_progress ($args{'verbose'});
 	$ua->proxy( ['http', 'https'], $args{'proxy'}) if (defined $args{'proxy'});
	$self->{'ua'} = $ua;

# connect to the client
	my $client = REST::Client->new(
		host		=> $self->{'url'},
		timeout		=> $self->{'timeout'},
		useragent	=> $ua,
		follow		=> 1,
	);
	$client->addHeader ("Content-Type", 'application/x-www-form-urlencoded');
	$client->addHeader ("Accept", "application/json");
	$self->{'client'} = $client;

# if we have a token, use it
	if (defined $self->{'token'}) {
		$client->addHeader ("X-Rundeck-Auth-Token", $self->{'token'});
		$client->GET("/api/$self->{'apivers'}/tokens/$self->{'login'}");
		$rc = $client->responseCode ();
		if (($rc-$rc%100 == 200) && (index($client->{'_res'}{'_content'}, 'alert alert-danger') == -1)) {
			$rc = 200;
		} else {
			$rc = 403;
		}
	} else {
# post user/passwd
		$client->POST(
			"j_security_check",
			"j_username=$self->{'login'}" . "&" . "j_password=$self->{'password'}",
		);
		$rc = $client->responseCode ();
	}
	if ($rc-$rc%100 != 200) {
		$self->{'result'}->{'reqstatus' } = 'UNKN';
	} else {
		$self->{'result'}->{'reqstatus'} = 'OK';
	}
	$self->{'result'}->{'httpstatus'} = $rc;

# done, bless object and return it
	bless ($self, $class);
	$self->_logV1 ("Connected to $self->{'url'}") if ($rc-$rc%100 == 200);
	$self->_logD($self);
	return $self;
}

#####
## METHODS
#####

#** @method public get (parameters)
# @ sends a GET query
#
# @param  endpoint
# @retval the response
#*

sub get (){		# endpoint
	my $self = shift;
	my $endpt = shift;
	my $endpoint = "";

	if ($endpt =~ /^\/api\/[0-9]+\/(.*)$/) {
		$endpoint = "/api/$self->{'apivers'}/$1";
	} else {
		$endpoint = "/api/$self->{'apivers'}/$endpt";
	}

	my $responsehash = ();
	my $rc = 0;

	# Handle secial case where endpoint is /api/XX/job, returns YAML
	# Obsolete
	#~ if ($endpoint =~ /api\/[0-9]+\/job/) {
		#~ $endpoint .= '?format=yaml';
	#~ }
	$self->_logV2("endpoint = $endpoint");
	$self->{'client'}->GET($endpoint);
	$rc = $self->{'client'}->responseCode ();
	$responsehash->{'httpstatus'} = $rc;

	if ($rc-$rc%100 != 200) {
		$responsehash->{'reqstatus'} = 'CRIT';
		$responsehash->{'httpstatus'} = $rc;
	} else {
		my $responseType = $self->{'client'}->responseHeader('content-type');
		my $responseContent = $self->{'client'}->responseContent();
		$responsehash = $self->_handleResponse($rc, $responseType, $responseContent);

	}
	return dclone ($responsehash);
}

#** @method public post (parameters)
# @ sends a POST request
#
# @param  endpoint, the JSON to POST
# @retval the response
#*

sub post(){		# endpoint, json
	my $self = shift;
	my $endpt = shift;
	my $json = shift;

	my $endpoint = "";

	if ($endpt =~ /^\/api\/[0-9]+\/(.*)$/) {
		$endpoint = "/api/$self->{'apivers'}/$1";
	} else {
		$endpoint = "/api/$self->{'apivers'}/$endpt";
	}

	my $responsehash = ();
	my $rc = 0;

	$self->{'client'}->addHeader ("Content-Type", 'application/json');
	$self->_logV2("endpoint = $endpoint");
	$self->{'client'}->POST($endpoint, $json);
	$rc = $self->{'client'}->responseCode ();
	$self->{'result'}->{'httpstatus'} = $rc;

	if ($rc-$rc%100 != 200) {
		$responsehash->{'reqstatus'} = 'CRIT';
		$responsehash->{'httpstatus'} = $rc;
	} else {
		my $responseType = $self->{'client'}->responseHeader('content-type');
		my $responseContent = $self->{'client'}->responseContent();
		$responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
	}
	return dclone ($responsehash);
}

#** @method public put (parameters)
# @ sends a PUT query
#
# @param  endpoint, the JSON to PUT
# @retval the response
#*

sub put(){		# endpoint, json
	my $self = shift;
	my $endpt = shift;
	my $json = shift;

	my $endpoint = "";

	if ($endpt =~ /^\/api\/[0-9]+\/(.*)$/) {
		$endpoint = "/api/$self->{'apivers'}/$1";
	} else {
		$endpoint = "/api/$self->{'apivers'}/$endpt";
	}

	my $responsehash = ();
	my $rc = 0;

	$self->{'client'}->addHeader ("Content-Type", 'application/json');
	$self->_logV2("endpoint = $endpoint");
	$self->{'client'}->PUT($endpoint, $json);
	$rc = $self->{'client'}->responseCode ();
	$self->{'result'}->{'httpstatus'} = $rc;

	if ($rc-$rc%100 != 200) {
		$responsehash->{'reqstatus'} = 'CRIT';
		$responsehash->{'httpstatus'} = $rc;
	} else {
		my $responseType = $self->{'client'}->responseHeader('content-type');
		my $responseContent = $self->{'client'}->responseContent();
		$responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
	}
	return dclone ($responsehash);
}

#** @method public delete (parameters)
# @ sends a DELETE query
#
# @param  endpoint
# @retval the response
#*

sub delete () {		# endpoint
	my $self = shift;
	my $endpt = shift;

	my $endpoint = "";

	if ($endpt =~ /^\/api\/[0-9]+\/(.*)$/) {
		$endpoint = "/api/$self->{'apivers'}/$1";
	} else {
		$endpoint = "/api/$self->{'apivers'}/$endpt";
	}

	my $responsehash = ();
	my $rc = 0;

	$self->_logV2("endpoint = $endpoint");
	$self->{'client'}->DELETE($endpoint);
	$rc = $self->{'client'}->responseCode ();
	$responsehash->{'httpstatus'} = $rc;

	if ($rc-$rc%100 != 200) {
		$responsehash->{'reqstatus'} = 'CRIT';
		$responsehash->{'httpstatus'} = $rc;
	} else {
		my $responseType = $self->{'client'}->responseHeader('content-type');
		my $responseContent = $self->{'client'}->responseContent();
		$responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
	}
	return dclone ($responsehash);
}

#** @method public postData (parameters)
# @ sends a POST query
#
# @param  endpoint, mimetype, data
# @retval the response
#*

sub postData() {		# endpoint, mimetype, data
	my $self = shift;
	my $endpt = shift;
	my $mimetype = shift;
	my $data = shift;

	my $endpoint = "";

	if ($endpt =~ /^\/api\/[0-9]+\/(.*)$/) {
		$endpoint = "/api/$self->{'apivers'}/$1";
	} else {
		$endpoint = "/api/$self->{'apivers'}/$endpt";
	}

	my $responsehash = ();
	my $rc = 0;

	$self->{'client'}->addHeader ("Content-Type", $mimetype);
	$self->_logV2("endpoint = $endpoint");
	$self->{'client'}->POST($endpoint, $data);
	$rc = $self->{'client'}->responseCode ();
	$self->{'result'}->{'httpstatus'} = $rc;

	if ($rc-$rc%100 != 200) {
		$responsehash->{'reqstatus'} = 'CRIT';
		$responsehash->{'httpstatus'} = $rc;
	} else {
		my $responseType = $self->{'client'}->responseHeader('content-type');
		my $responseContent = $self->{'client'}->responseContent();
		$responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
	}
	return dclone ($responsehash);
}

#** @method public putData (parameters)
# @ sends a PUT query
#
# @param  endpoint, mimetype, data
# @retval the response
#*

sub putData() {		# endpoint, mimetype, data
	my $self = shift;
	my $endpt = shift;
	my $mimetype = shift;
	my $data = shift;

	my $endpoint = "";

	if ($endpt =~ /^\/api\/[0-9]+\/(.*)$/) {
		$endpoint = "/api/$self->{'apivers'}/$1";
	} else {
		$endpoint = "/api/$self->{'apivers'}/$endpt";
	}

	my $responsehash = ();
	my $rc = 0;

	$self->{'client'}->addHeader ("Content-Type", $mimetype);
	$self->_logV2("endpoint = $endpoint");
	$self->{'client'}->PUT($endpoint, $data);
	$rc = $self->{'client'}->responseCode ();
	$self->{'result'}->{'httpstatus'} = $rc;

	if ($rc-$rc%100 != 200) {
		$responsehash->{'reqstatus'} = 'CRIT';
		$responsehash->{'httpstatus'} = $rc;
	} else {
		my $responseType = $self->{'client'}->responseHeader('content-type');
		my $responseContent = $self->{'client'}->responseContent();
		$responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
	}
	return dclone ($responsehash);
}

#** @method private _handleResponse (parameters)
# @ manage with the various responses, build a hash with the data received
#
# @param  rc, responseType, responseContent
# @retval the response
#*

sub _handleResponse () {
	my $self = shift;
	my $rc = shift;
	my $responseType = shift;
	my $responseContent = shift;

	my $responseJSON = ();
	my $responsehash = ();
	$responsehash->{'reqstatus'} = 'OK';
	$responsehash->{'httpstatus'} = $rc;

	# is data JSON ?

	if (defined $responseType) {
		$self->_logV2($responseType);
		if ($responseType =~ /^application\/json.*/) {
			$self->_logV2($responseContent);
			$responseJSON = decode_json($responseContent) if $responseContent ne '';
			my $reftype = reftype($responseJSON);
			if (not defined $reftype) {
				$responsehash->{'reqstatus'} = 'CRIT';
				$responsehash->{'httpstatus'} = 415;
			} elsif ($reftype eq 'ARRAY') {
				$self->_logV2("copying array");
				$responsehash->{'content'}{'arraycount'} = $#$responseJSON+1;
				for (my $i = 0; $i <= $#$responseJSON; $i++) {
					$responsehash->{'content'}{$i} = $responseJSON->[$i];
				}
			} elsif ($reftype eq 'SCALAR') {
				$responsehash->{'reqstatus'} = 'CRIT';
				$responsehash->{'httpstatus'} = 415;
			} elsif ($reftype eq 'HASH') {
				$self->_logV2("copying hash");
				$responsehash->{'content'} = $responseJSON;
			}
		} elsif ($responseType =~ /text\/plain.*/) {
			$self->_logV2($responseContent);
			$responsehash->{'content'} = $responseContent;
		} else { # assume binary, like text, but do not log
			$responsehash->{'content'} = $responseContent;
		}
	}
	return $responsehash;
}

#** @method private _logV1
# @ print a message if verbose > 1
#
# @param  message
#*

sub _logV1() {
	my $self = shift;
	my $msg = shift;

	if ($self->{'verbose'} >= 1) {
		if (defined $msg) {
			print "$msg\n";
		} else {
			print "unknown $!";
		}
	}
}

#** @method private _logV2 (parameters)
# @ dumps an object if verbose > 2
#
# @param  message
#*

sub _logV2() {
	my $self = shift;
	my $obj = shift;

	if ($self->{'verbose'} >= 2) {
		if (defined $obj) {
			print Dumper ($obj);
		} else {
			print "unknown object $!";
		}
	}
}

#** @method private _logD (parameters)
# @ dumps an object if debug enabled
#
# @param  object
#*

sub _logD() {
	my $self = shift;
	my $object = shift;

	print Dumper ($object) if $self->{'debug'};
}

1;

=pod

=head1 NAME

RundeckAPI - simplifies authenticate, connect, queries to a Rundeck instance via REST API

=head1 SYNOPSIS
	use RundeckAPI;

	# create an object of type RundeckAPI :
	my $api = RundeckAPI->new(
		'url'		=> "https://my.rundeck.instance:4440",
		'login'		=> "admin",
		'token'		=> <token as generated with GUI, as an admin>
		'debug'		=> 1,
		'apivers'	=> '42',
 		'proxy'		=> "http://proxy.mycompany.com/",
	);
	my $hashRef = $api->get("system/info");
	my $json = '{some: value}';
	$hashRef = $api->put(endpoint_for_put, $json);

=head1 METHODS

=over 12

=item C<new>

Returns an object authenticated and connected to a Rundeck Instance.
The field 'login' is not stricto sensu required, but it is a good security mesure to check if login/token match

=item C<get>

Sends a GET query. Request one argument, the enpoint to the API. Returns a hash reference

=item C<post>

Sends a POST query. Request two arguments, the enpoint to the API an the data in json format. Returns a hash reference

=item C<put>

Sends a PUT query. Similar to post

=item C<delete>

Sends a DELETE query. Similar to get

=item C<postData>

POST some data. Request three arguments : endpoint, mime-type and the appropriate data. Returns a hash reference.

=item C<putData>

PUT some data. Similar to postData

=item C<postFile>

Alias for compatibility for postData

=item C<putFile>

Alias for compatibility for putData

=back

=head1 RETURN VALUE

Returns a hash reference containing the data sent by Rundeck.

The returned value is structured like the following :

the fields `httpstatus` (200, 403, etc) and `requstatus` (OK, CRIT) are always present.

the field `content` is a hash (if the mime-type of the result is JSON), text or binary


=head1 SEE ALSO

See documentation for Rundeck's API https://docs.rundeck.com/docs/api/rundeck-api.html and returned data

=head1 AUTHOR
	Xavier Humbert <xavier.humbert-at-ac-nancy-metz-dot-fr>

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.