#!/usr/bin/perl -w
#
# A tool to fetch and upload job configurations to a jenkins instance
#
# The aim of this tool is to fetch job configurations, edit them locally
# (by script) and push them back to the jenkins server.
#
# Note: make sure to fetch the latest changes before you edit and push!
#
# 2013, J. Daniel Schmidt <jdsn@suse.de>

use strict;
use XML::LibXML;
use JSON;

my $USER;
my $PASSWORD; # obsolete but we keep the variable
my $APIKEY;
my $SCHEME="https";
my $JURL="ci.opensuse.org";
my $CRUMB;
my $METHOD='POST';

# read the api credentials from files
eval `cat /etc/jenkinsapi.cred ./jenkinsapi.cred 2>/dev/null`;

if ( ! $APIKEY ) {
  print STDERR "You have not defined your APIKEY variable in the config.\n";
  print STDERR "Login in jenkins and find your api key.";
  print STDERR "Please add this api key to your jenkinsapi.cred file:\n";
  print STDERR ' $APIKEY="987zyx654wvu"';
}

if ( $ARGV[0] ne "getcrumb" && ! $CRUMB ) {
  print STDERR "You have not defined your CRUMB variable in the config.\n";
  print STDERR "Please create your own crumb:\n";
  print STDERR " $0 getcrumb\n";
  print STDERR "And then add it to your jenkinsapi.cred file:\n";
  print STDERR ' $CRUMB="Jenkins-Crumb:123abc456def"';
}

$METHOD |= 'POST';
$SCHEME |= 'https';
if ($JURL =~ /river\.suse\.de/)
{
  $SCHEME='http';
}

mkdir $JURL;

my $UP='';
$UP="${USER}:${APIKEY}\@" if (defined ${USER} && defined ${APIKEY} && ${USER} ne '' && ${APIKEY} ne '');
my $jurlbase="${SCHEME}://${UP}${JURL}";
my $jurl=$jurlbase."/api/xml";

# get 'one' parser
my $parser=XML::LibXML->new();

sub usage()
{
  return "Usage: $0 <command> [<parameter> ..]
  commands:
    fetch <jobname> : fetch job configuration of one or more jobs
    push  <jobname> : push one or more job configurations
    fetch-all <prefix> : fetch all jobs that match one of the <prefix>es
    push-all  <prefix> : push all jobs that match one of the <prefix>es
    reformat  <file>   : reformat the XML after manual changes
    getdescription <build> : get description of a build
    setdescription <build> <description> : set description of a build
    getbuilds <jobname> : get build numbers
    getqueue [jobname]: get queue numbers
    cancelItem <itemID>: cancel queued job

  The <jobname> has to be the exact job name (no directory, no trailing .xml)
  The <prefix> will match all
   * server side jobs starting with <prefix> (in case of fetch-all)
   * currently fetched jobs starting with <prefix> (in case of push-all)

  All jobs will be fetched to/from the directory: $JURL/
  You can configure the jenkins url and credentials in either
   ./jenkinsapi.cred or /etc/jenkinsapi.cred (see example).

  Note!
  Archiving is the primary purpose of japi. Anyhow you can
  make changes locally and push them to the server.
  Please fetch and reformat a job before you change and push it.
  Otheriwse server-side changes would be lost.

  Example config file:
cat > jenkinsapi.cred <<EOCONF
\$USER='myusername';
\$APIKEY='987zyx654wvu';
\$SCHEME='https';
\$JURL='ci.opensuse.org';
\$CRUMB='Jenkins-Crumb:123abc456def';
EOCONF

Create the Crumb string:
 $0 getcrumb
";
}


sub xml_get_elements($$;$)
{
  my ($node, $path, $attribute) = @_;
  die "Error: node or path undefined." unless ($node && $path);
  my $nodeL = $node->find($path);
  my $onenode;
  my @results=();
  while ($nodeL->size() > 0) {
    $onenode = $nodeL->pop();
    push @results, ($attribute ? $onenode->getAttribute($attribute) : $onenode->textContent());
  }
  return @results;
}

sub file_read_xml($)
{
  my $file = shift || die "Error: no input file to read the service xml data from.";
  open (my $FH, '<', $file) or die $!;
  binmode $FH;
  my $xml = $parser->load_xml(
    IO => $FH,
    { no_blanks => 1 }
  );
  close $FH;
  return $xml;
}

sub file_write($$)
{
  my ($file, $data) = @_;
  open (my $FH, '>', $file) or die $!;
  binmode $FH;
  print $FH $data;
  close $FH;
}

sub reformat($)
{
  # just reformat the xml, this will make sure the LibXML output gets versioned
  # the output of the jenkins api is a slightly different and would create useless diffs over and over
  # otherwise we could not edit the xml via LibXML automatically
  my $tufile=shift || die "Error: no file set to be cleaned";
  my $xmldom=file_read_xml($tufile);
  file_write($tufile, $xmldom->toString(1));
}

sub curl(@)
{
  my @cmd=(qw"curl --silent --show-error -H Expect: -H", $CRUMB, @_);
  print join(" ", @cmd)."\n" if ($ENV{debug});
  open(my $pipe, "-|", @cmd);
  local $/;
  my $ret=<$pipe>;
  close $pipe;
  if($?>>8) { print "ERROR ".($?>>8)."\n$ret" }
  return $ret;
}

sub get_cloud_job_list
{
  my $jenkinscfg=curl("$jurl/config.xml");
  my $xml = $parser->parse_string($jenkinscfg);
  return xml_get_elements($xml,"/hudson/job/name");
}

sub fetch_job($)
{
  my $jobname=shift || die "Error: no job name specified";
  my $fetchurl=$jurlbase."/job/$jobname/config.xml";
  print "fetching $jobname\n";
  curl("--output", "./${JURL}/${jobname}.xml", "$fetchurl");
  # beautify the xml in order to have consistent LibXML output and prevent useless diffs
  reformat("./${JURL}/${jobname}.xml");
}

sub fetch_jobs
{
  my @jobs=@_;
  die "Error: no jobs specified" unless @jobs;
  foreach my $j (@jobs) {
    fetch_job($j);
  }
}

sub fetchall_jobs
{
  my @prefixes=@_;
  my @joblist = get_cloud_job_list();
  my @joblist_filtered=();
  foreach my $j (@joblist) {
    foreach my $p (@prefixes) {
      if ( $j =~ /^$p/i )
      {
        push @joblist_filtered, $j;
      }
    }
  }
  @joblist_filtered=keys %{{ map {$_ => 1} @joblist_filtered }};
  fetch_jobs(@joblist_filtered);
}

sub has_error
{
  return $_[0]=~/(error|<title>[45]\d\d )/i;
}

sub push_job($)
{
  my $jobname=shift || die "Error: no jobname specified";
  my $pushurl=$jurlbase."/job/$jobname/config.xml";
  print "processing job: $jobname:\n";
  # make sure the output is reformatted by libXML
  reformat("${JURL}/${jobname}.xml");
  # update
  my $result=curl("-X", "$METHOD", "--data-binary", "\@${JURL}/${jobname}.xml", $pushurl);
  if (has_error($result)) {
    print "  updating failed .. ";
    # create
    $result=curl("-X", "$METHOD", "-H", "Content-Type: text/xml", "--data-binary", "\@${JURL}/${jobname}.xml", "${jurlbase}/createItem?name=${jobname}");
    if (has_error($result)) {
      print "creating failed as well .. -> ERROR!\n";
    } else {
      print "job successfully created\n";
    }
  } else {
    print "  job successfully updated\n";
  }
}

sub push_jobs
{
  my @jobs=@_;
  die "Error: no jobs specified" unless @jobs;
  foreach my $j (@jobs) {
    push_job($j);
  }
}

sub pushall_jobs
{
  my @prefixes=@_;
  my @joblist=();
  chdir $JURL;
  foreach my $prefix (@prefixes) {
    push @joblist, glob("${prefix}*");
  }
  chdir "..";
  @joblist = map { $_ =~ s/\.xml$//; $_; } @joblist;
  push_jobs(@joblist);
}

sub urlencode($)
{
  my $x=shift;
  $x =~ s/[^\w.-]/sprintf("%%%02X",ord($&))/ge;
  return $x;
}

sub getdescription
{
  my $build=shift; # e.g. "openstack-mkcloud/3668"
  my $xmlstr=curl("${jurlbase}/job/${build}/api/xml");
  my $xml = $parser->parse_string($xmlstr);
  print xml_get_elements($xml,"/freeStyleBuild/description");
}

sub setdescription
{
  my $build=shift; # e.g. "openstack-mkcloud/3668"
  my $descr=urlencode(shift); # Text to set
  curl(qw"-X POST -d", "description=$descr", "${jurlbase}/job/${build}/submitDescription");
}

sub getbuilds
{
  my $jobname=shift;
  $jobname or die "needs jobname";
  my $buildxml=curl("${jurlbase}/job/${jobname}/api/xml?xpath=//number&wrapper=builds");
  my $xml = $parser->parse_string($buildxml);
  print map {"$_\n"} xml_get_elements($xml, "/builds/number");
}

sub getqueue
{
  my $jobname=shift;
  my $queuejson=`wget -q -O- "${jurlbase}/queue/api/json?tree=items[id,task[name]]"`;
  my $data=decode_json($queuejson);
  $data=$data->{items};
  if($jobname) {
    $data = [map {$_->{id}} grep({$_->{task}->{name} eq $jobname} @$data)];
  }
  print JSON->new->allow_nonref->pretty->encode($data);
}

sub cancelItem
{
  my $item=shift;
  $item or die "needs numeric queue ID";
  my $out=curl(qw"-X POST -d", "id=${item}", "${jurlbase}/queue/cancelItem");
  print $out;
}


sub getcrumb
{
  $CRUMB||="crumb:dummy";
  print curl(qq'${jurlbase}/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'), "\n";
}


### MAIN ###

my $cmd=shift || die usage();
my %handlefunc=();
foreach my $func (qw(reformat getdescription setdescription getbuilds getqueue cancelItem getcrumb)) {
  $handlefunc{$func} = eval "\\&$func";
}
foreach my $c (qw(fetch fetch-all push push-all)) {
  my $func = "${c}_jobs";
  $func =~ s/-//;
  $handlefunc{$c} = eval "\\&$func";
}

if (my $func = $handlefunc{$cmd}) {
  &$func(@ARGV);
} else {
  die usage();
}

