Skip to content

Instantly share code, notes, and snippets.

@aaronwalker
Created July 23, 2015 13:08
Show Gist options
  • Select an option

  • Save aaronwalker/dbfd7aa257dedf6841ff to your computer and use it in GitHub Desktop.

Select an option

Save aaronwalker/dbfd7aa257dedf6841ff to your computer and use it in GitHub Desktop.
#goes into $HOME/.jenkins
baseuri: https://jenkins.example.com/
jobs:
- Build-Master
stoplight: 1
# ssl_opts is any option listed in "perldoc IO::Socket::SSL"
ssl_opts:
verify_hostname: 0
# # by default we ignore invalid certs; set to nil to validate
# SSL_verify_callback:
# SSL_version: SSLv3
#!/usr/bin/env perl
package WWW::Jenkins::Job;
# Copyright 2012 Netflix, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
use strict;
use warnings;
sub new {
my $class = shift;
my $self = bless { @_ }, $class;
for my $required ( qw(name jenkins url color) ) {
defined $self->{$required} || die "no $required parameter to WWW::Jenkins::Job->new()";
}
$self->{inQueue} ||= 0;
return $self;
}
sub copy {
my ( $self ) = shift;
return bless { %$self, @_ }, ref($self);
}
sub j {
return shift->{jenkins};
}
sub ua {
return shift->j->{ua};
}
sub name {
return shift->{"name"};
}
# turns the jenkins 'color' attribute into a color
# suitable for encoding in Term::ANSIColor.
# All aborted builds are marked as red
# ... these are the options we have to work with in ANSIColor
# attributes: reset, bold, dark, faint, underline, blink, reverse and concealed.
# foreground: black, red, green, yellow, blue, magenta, cyan and white.
# background: on_black, on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan and on_white
sub color {
my ( $self ) = @_;
my $color = $self->{color};
$color =~ s/_anime$//;
$color = "green" if $self->j->{stoplight} && $color eq 'blue';
$color = 'faint' if $color eq 'disabled';
$color = 'red' if $color eq 'aborted';
$color = 'faint' if $color eq 'grey';
$color = 'faint' if $color eq 'notbuilt';
return $color;
}
sub number {
my ( $self ) = @_;
my $lb = $self->{lastBuild} ||= {};
return $lb->{number} if defined $lb->{number};
if( defined $lb->{url} ) {
my ( $num ) = ( $lb->{url} =~ m{/(\d+)/?$} );
return $lb->{number} = $num if defined $num;
}
$self->_load_lastBuild;
return $lb->{number};
}
sub started {
my ( $self ) = shift;
my $lb = $self->{lastBuild} ||= {};
return $lb->{timestamp} / 1000 if defined $lb->{timestamp};
$self->_load_lastBuild;
return $lb->{timestamp} / 1000;
}
sub duration {
my ( $self ) = shift;
my $lb = $self->{lastBuild} ||= {};
return $lb->{duration} / 1000 if defined $lb->{duration};
$self->_load_lastBuild;
return $lb->{duration} / 1000;
}
sub _load_lastBuild {
my ( $self ) = shift;
my $uri = "$self->{url}/api/json?depth=0&tree=lastBuild[url,duration,timestamp,number]";
my $res = $self->ua->get($uri);
my $data = WWW::Jenkins::parse_json($res->decoded_content());
return %{$self->{lastBuild}} = %{$data->{lastBuild}};
}
sub start {
my ( $self, $params ) = @_;
$self->j->login();
my @params;
if ( $self->{actions} ) {
for my $action ( @{$self->{actions}} ) {
if ( exists $action->{parameterDefinitions} ) {
for my $param ( @{$action->{parameterDefinitions}} ) {
if ( exists $params->{$param->{name}} ) {
push @params, { "name" => $param->{name}, "value" => $params->{$param->{name}} };
}
else {
push @params, { "name" => $param->{name}, "value" => $param->{defaultParameterValue}->{value} };
}
}
}
}
}
my $resp = $self->ua->post("$self->{url}/build", {delay => "0sec", json=> WWW::Jenkins::encode_json({"parameter" => \@params})});
if( $resp->is_error ) {
die "Failed to start $self->{name}, got error: " . $resp->status_line;
}
return 1;
}
sub stop {
my ( $self ) = @_;
die "job " . $self->name() . " has never been run"
unless $self->{lastBuild};
# dont stop something not running
return 1 unless $self->is_running();
$self->j->login();
my $resp = $self->ua->post("$self->{lastBuild}->{url}/stop", {});
if( $resp->is_error ) {
die "Failed to stop $self->{name}, got error: " . $resp->status_line;
}
return 1;
}
sub is_running {
my ( $self ) = @_;
return $self->{color} =~ /_anime/ ? 1 : 0;
}
sub is_queued {
my ( $self ) = @_;
return $self->{inQueue} ? 1 : 0;
}
sub was_aborted {
my ( $self ) = @_;
return $self->{color} eq 'aborted';
}
sub wipeout {
my ( $self ) = @_;
$self->j->login();
my $resp = $self->ua->post("$self->{url}/doWipeOutWorkspace", {});
if( $resp->is_error ) {
die "Failed to wipeout $self->{name}, got error: " . $resp->status_line;
}
return 1;
}
sub logCursor {
my ( $self ) = @_;
die "job " . $self->name() . " has never been run"
unless $self->{lastBuild};
my $res = undef;
return sub {
if( !$res ) {
$res = $self->ua->post(
"$self->{lastBuild}->{url}/logText/progressiveText", { start => 0 }
)
}
elsif( $res->header("X-More-Data") && $res->header("X-More-Data") eq 'true' ) {
$res = $self->ua->post(
"$self->{lastBuild}->{url}/logText/progressiveText", {
start => $res->header("X-Text-Size")
}
);
}
else {
# there was a previous response but X-More-Data not set, so we are done
return undef;
}
return $res->decoded_content || "";
}
}
sub disable {
my ( $self ) = @_;
$self->j->login();
my $resp = $self->ua->post("$self->{url}/disable", {});
if( $resp->is_error ) {
die "Failed to disable $self->{name}, got error: " . $resp->status_line;
}
return 1;
}
sub enable {
my ( $self ) = @_;
$self->j->login();
my $resp = $self->ua->post("$self->{url}/enable", {});
if( $resp->is_error ) {
die "Failed to enable $self->{name}, got error: " . $resp->status_line;
}
return 1;
}
sub config {
my ( $self, $content ) = @_;
$self->j->login();
my $url = "$self->{url}/config.xml";
my $resp;
if( $content ) {
my $req = HTTP::Request->new("POST", $url);
$req->content($content);
$resp = $self->ua->request($req);
}
else {
$resp = $self->ua->get($url, {});
}
if( $resp->is_error ) {
die "Failed get/set config.xml for $self->{name}, got error: " . $resp->status_line;
}
return $resp->decoded_content();
}
sub millis {
eval "use Time::HiRes";
if( $@ ) {
return time() * 1000;
}
else {
return Time::HiRes::time() * 1000
}
}
sub history {
my ( $self ) = @_;
my $res = $self->ua->get("$self->{url}/api/json?depth=11&tree=builds[result,url,number,building,timestamp,duration]");
my $data = WWW::Jenkins::parse_json($res->decoded_content());
my @out;
for my $build ( @{$data->{builds}} ) {
my $color;
if( $build->{building} ) {
$color = $self->{color};
$build->{duration} ||= $self->millis - $build->{timestamp}
}
else {
$color =
$build->{result} =~ /SUCCESS/ ? "blue" :
$build->{result} =~ /ABORTED/ ? "aborted" :
$build->{result} =~ /FAILURE/ ? "red" :
"grey" ;
}
warn("unknown result: $build->{result}\n") if $color eq 'grey';
push @out, $self->copy(
color => $color,
inQueue => 0,
lastBuild => $build,
);
}
return wantarray ? @out : \@out;
}
1;
package WWW::Jenkins;
use strict;
use warnings;
# Copyright 2012 Netflix, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
use LWP::UserAgent qw();
use HTTP::Cookies qw();
use HTTP::Request qw();
use Carp qw(croak);
use URI;
our @CLEANUP;
sub new {
my $class = shift;
my $self = bless {
# defaults
stoplight => 0,
user => $ENV{USER},
@_
}, $class;
my $UA = $self->{verbose}
? "WWW::Jenkins::UserAgent"
: "LWP::UserAgent";
$self->{baseuri} || die "baseuri option required";
$self->{ua} ||= $UA->new(
cookie_jar => HTTP::Cookies->new(
file => "$ENV{HOME}/.$self->{user}-".URI->new($self->{baseuri})->host()."-cookies.txt",
autosave => 1,
),
ssl_opts => {
SSL_verify_callback => sub { 1 },
$self->{ssl_opts} ? %{$self->{ssl_opts}} : ()
}
);
return $self;
}
sub create {
my ($self, $job, $config) = @_;
if( ref($job) ) {
return $job->config($config);
}
$self->login();
my $req = HTTP::Request->new("POST", "$self->{baseuri}/createItem?name=$job");
$req->header("Content-Type" => "application/xml");
$req->content($config);
my $resp = $self->{ua}->request($req);
if( $resp->is_error ) {
die "Failed to create new job $job, got error: " . $resp->as_string;
}
}
sub search {
my ($self, $filter) = @_;
my $uri = "$self->{baseuri}/api/json?tree=jobs[name]";
my $res = $self->{ua}->get($uri);
my @out = ();
if( $res->is_success ) {
my $data = parse_json($res->decoded_content());
@out = grep { /$filter/ } map { $_->{name} } @{$data->{jobs}};
}
return wantarray ? @out : \@out;
}
sub jobs {
my ($self,@jobs) = @_;
my @out = ();
for my $job ( @jobs ) {
my $uri = "$self->{baseuri}/job/$job/api/json?depth=0&tree=name,inQueue,url,lastBuild[number,url],color,actions[parameterDefinitions[defaultParameterValue[value],name]]";
my $res = $self->{ua}->get($uri);
if( $res->is_success ) {
my $data = parse_json($res->decoded_content());
push @out, WWW::Jenkins::Job->new(%$data, jenkins => $self);
}
}
return wantarray ? @out : \@out;
}
sub views {
my ($self,@views) = @_;
my @out = ();
for my $view ( @views ) {
# turns A/B into A/view/B which is needed
# in jenkins uri for subviews
my $viewPath = join("/view/", split '/', $view);
my $uri = "$self->{baseuri}/view/$viewPath/api/json?depth=1&tree=views[name,url],jobs[name,inQueue,url,lastBuild[number,url,timestamp,duration],color]";
my $res = $self->{ua}->get($uri);
my $data = parse_json($res->decoded_content());
# we dont know if the view has subviews or it it has jobs, so try for both
# and recurse if we find a subview
if( $data->{jobs} ) {
push @out, WWW::Jenkins::Job->new(%$_, jenkins => $self) for @{$data->{jobs}};
}
if( $data->{views} ) {
push @out, $self->views("$view/$_->{name}") for @{$data->{views}};
}
}
return wantarray ? @out : \@out;
}
sub queue {
my ( $self ) = @_;
my $uri = "$self->{baseuri}/queue/api/json?depth=0&tree=items[task[color,name,url],why,stuck]";
my $res = $self->{ua}->get($uri);
#print $res->decoded_content;
my $data = parse_json($res->decoded_content());
my %blocked;
my %stuck;
my @running;
my @quieted;
for my $item ( @{$data->{items}} ) {
my $job = WWW::Jenkins::Job->new(
%{$item->{task}},
inQueue => 1,
jenkins => $self
);
if( $item->{stuck} ) {
if( !$item->{why} ) {
warn "no reason given why $item->{task}->{name} is stuck\n";
next;
}
if( $item->{why} =~ /([^ ]+) (is|are) offline/ ) {
push @{$stuck{$1}}, $job;
}
else {
warn "don't understand why $item->{task}->{name} is stuck: $item->{why}\n";
}
}
else {
if( !$item->{why} ) {
warn "no reason given why $item->{task}->{name} is enqueued\n";
next;
}
if( $item->{why} =~ /Waiting for next available executor on (.*)/ ) {
push @{$blocked{$1}}, $job;
}
elsif( $item->{why} =~ /already in progress/ ) {
push @running, $job;
}
elsif( $item->{why} =~ /quiet period/ ) {
push @quieted, $job;
}
else {
warn "don't understand why $item->{task}->{name} is enqueued: $item->{why}\n";
}
}
}
return {
blocked => \%blocked,
stuck => \%stuck,
running => \@running,
quieted => \@quieted,
};
}
sub login {
my ( $self ) = @_;
return if $self->{logged_in};
# FIXME there has to be a better way to tell if we
# are already logged in ...
# just load the page with the smallest content that I could find
# and check for a "log in" string to indicate the user is not
# logged in already
my $res = $self->{ua}->get("$self->{baseuri}/user/$self->{user}/?");
if ( $res->decoded_content =~ />log in</ ) {
$res = $self->{ua}->post(
"$self->{baseuri}/j_acegi_security_check", {
j_username => $self->{user},
j_password => $self->password(),
}
);
$self->{ua}->get("$self->{baseuri}/user/$self->{user}/?");
$self->{ua}->cookie_jar->scan(
sub {
my @args = @_;
# dont discard cookies, so we dont get prompted for a password everytime
$args[9] = 0;
$self->{ua}->cookie_jar->set_cookie(@args);
}
);
}
$self->{logged_in}++;
return;
}
sub stdio {
my $self = shift;
my ($in, $out);
if( !-t STDIN || !-t STDOUT ) {
# stdio missing, so try to use tty directly
my $tty = "/dev/tty" if -e "/dev/tty";
if( !$tty ) {
my ($ttyBin) = grep { -x $_ } qw(/bin/tty /usr/bin/tty);
if ( $ttyBin ) {
$tty = qx{$ttyBin};
chomp($tty);
}
}
if( !$tty ) {
die "Could not determine TTY to read password from, aborting";
}
open $in, "<$tty" or die "Failed to open tty $tty for input: $!";
open $out, ">$tty" or die "Failed to open tty $tty for output: $!";
}
else {
# using stdio
$in = \*STDIN;
$out = \*STDOUT;
}
return ($in, $out);
}
sub password {
my ( $self ) = @_;
if ( ref($self) && defined $self->{password} ) {
if ( ref($self->{password}) eq 'CODE' ) {
return $self->{password}->($self);
}
return $self->{password};
}
my ($in, $out) = $self->stdio;
my $old = select $out;
eval "use Term::ReadKey";
if( $@ ) {
# no readkey, so try for stty to turn off echoing
my ($sttyBin) = grep { -x $_ } qw(/bin/stty /usr/bin/stty);
if( $sttyBin ) {
push @CLEANUP, sub {
system($sttyBin, "echo");
};
system($sttyBin, "-echo");
}
else {
die "Unable to disable echo on your tty while reading password, aborting";
}
}
else {
# use readkey to turn off echoing
push @CLEANUP, sub {
Term::ReadKey::ReadMode("restore", $out);
};
Term::ReadKey::ReadMode("noecho", $out);
}
my $user = ref($self) eq 'HASH' ? $self->{user} : $ENV{USER};
print $out "Jenkins Password [$user]: ";
my $pass = <$in>;
$CLEANUP[-1]->();
print $out "\n";
chomp($pass);
select $old;
return $pass;
}
{
my $parser;
my $encoder;
sub init_json {
# no parser, so find one
eval "use JSON::XS qw()";
unless( $@ ) {
$parser = JSON::XS->can("decode_json") || JSON::XS->can("from_json");
$encoder = JSON::XS->can("encode_json") || JSON::XS->can("to_json");
return;
}
eval "use JSON qw()";
unless ( $@ ) {
$parser = JSON->can("decode_json") || JSON->can("jsonToObj");
$encoder = JSON->can("encode_json") || JSON->can("objToJson");
return;
}
eval "use JSON::DWIW qw()";
unless ( $@ ) {
$parser = JSON::DWIW->can("from_json");
$encoder = JSON::DWIW->can("to_json");
return;
}
eval "use JSON::Syck qw()";
unless ( $@ ) {
$parser = JSON::Syck->can("Load");
$encoder = JSON::Syck->can("Dump");
return;
}
die "No valid JSON parser found, try JSON::XS, JSON, JSON::DWIW, or JSON::Syck";
}
sub parse_json {
$parser or init_json();
my $output = eval {
$parser->(@_)
};
if( $@ ) {
croak "Failed to parse JSON:\n", @_;
}
return $output;
}
sub encode_json {
$encoder or init_json();
my $output = eval {
$encoder->(@_)
};
if ( $@ ) {
croak "Failed to generate JSON:\n", @_;
}
return $output;
}
}
END {
for my $cleaner ( @CLEANUP ) {
$cleaner->();
}
}
# silly class to make debugging easier
package WWW::Jenkins::UserAgent;
use base qw(LWP::UserAgent);
sub request {
my $self = shift;
my $req = shift;
my $resp = $self->SUPER::request($req, @_);
print "======================================>\n";
print $req->as_string;
print "<======================================\n";
print $resp->as_string;
return $resp;
}
1;
package main;
#!/usr/bin/env perl
# Copyright 2012 Netflix, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
use strict;
use warnings;
use YAML::Syck;
use Term::ANSIColor;
use Getopt::Long;
use File::Basename qw();
$|++;
sub usage {
my $err = shift || 0;
my $io = $err ? *STDERR : *STDOUT;
print $io ("-")x76,"\n" if $err;
print $io <<EOM;
Usage: $0 [options] [command] [<pattern>]
Global Options:
--all: search jenkins for job names, ignore jobs in ~/.jenkins
--baseuri=<uri>: base uri to jenkins server [http://jenkins]
--stoplight: make blue builds green [off]
--job=<name>: specify a job name, can be repeated
--view=<name>: speficy a list of jobs by view
--yes: always answer yes to any question
--password: prompt for password once
Commands:
ls|list [<pattern>]: show status of builds, optionally filter on pattern
login: login to all configured jenkins masters
start <pattern>: start job
stop <pattern>: stop job
tail <pattern>: tail the most recent build log for a job
disable <pattern>: disable job
enable <pattern>: enable a job
wipeout <pattern>: delete current build workspace for a job
q|queue: shows pending build queue grouped by build-slaves
hist|history: list history of builds for a job
conf|config: dump config.xml for a job
create <name|pattern> <config.xml|-> : create/update new jenkins job from config.xml
* Note <pattern> can be any regular expression to match jobs in your
default job list/view
EOM
exit $err;
}
my @actions;
my @commands = qw(ls list login start stop tail disable enable wipeout q queue hist history conf config create);
my %options;
$options{$_} = sub { push @actions, shift } for @commands;
my %opts = ();
GetOptions(
"help" => sub { usage(0) },
"all" => \$opts{all},
"baseuri=s" => \$opts{baseuri},
"stoplight" => \$opts{stoplight},
"job=s@" => \$opts{jobs},
"view=s@" => \$opts{views},
"yes" => \$opts{yes},
"color!" => \$opts{color},
"verbose" => \$opts{verbose},
"user" => \$opts{user},
"stuck" => \$opts{stuck},
"param|p=s@" => \$opts{params},
"password" => \$opts{password},
"yes" => \$opts{yes},
%options,
) || usage(1);
for my $key ( keys %opts ) {
delete $opts{$key} unless defined $opts{$key}
}
my @jenkins = ();
if( $opts{password} ) {
$opts{password} = WWW::Jenkins->password();
}
for my $cfg ( glob("$ENV{HOME}/.jenkins*" ) ) {
if( ! $opts{baseuri} && -f $cfg ) {
my $config = YAML::Syck::LoadFile($cfg);
$config->{baseuri} ||= "http://jenkins";
$config->{user} ||= $ENV{USER};
push @jenkins, WWW::Jenkins->new(%$config, %opts);
}
}
if( !@jenkins ) {
push @jenkins, WWW::Jenkins->new( baseuri => "http://jenkins", user => $ENV{USER}, %opts)
}
my @args;
for my $arg ( @ARGV ) {
if( grep { $arg eq $_ } @commands ) {
push @actions, $arg;
}
else {
push @args, $arg;
}
}
my $filter = shift @args || ".";
if( !@actions ) {
list(load("list"), @args);
}
else {
for my $action ( @actions ) {
no strict "refs";
my $func = *{"main::$action"};
$func->(load($action), \@args);
}
}
BEGIN {
# create function aliases
no warnings "once";
*ls = \&list;
*q = \&queue;
*hist = \&history;
*conf = \&config;
# these routines are all the same, just loop over all jobs
# and try to run the operation on each job.
for my $func (qw(start stop disable enable wipeout)) {
no strict 'refs';
*{"main::$func"} = sub {
my ( $jobs, $args ) = @_;
confirm_multiple($func, $jobs) if @$jobs > 1;
for my $job ( @$jobs ) {
eval {
if ( $func eq 'start') {
$job->$func( { map { split /=/, $_, 2 } @{$opts{params}} } )
}
else {
$job->$func()
}
};
printf "%s %s: %s\n", ucfirst($func), $job->name(), $@ ? "ERROR: $@" : "OK";
}
}
}
};
{
my $jobs;
sub load {
my $action = shift;
return [] if $action eq 'q' || $action eq 'queue';
return $jobs if $jobs;
my @jobs;
for my $jenkins ( @jenkins ) {
my $jobNames = $opts{all} ? $jenkins->search($filter) : $jenkins->{jobs};
my %uniq;
push @jobs, grep { !$uniq{$_->name}++ } $jenkins->jobs(grep{ /$filter/ } @$jobNames);
push @jobs, grep { !$uniq{$_->name}++ } $jenkins->views(@{$jenkins->{views}});
}
if( $filter ) {
@jobs = grep { $_->name =~ /$filter/ } @jobs;
my @exact = grep { $_->name eq $filter } @jobs;
@jobs = @exact if @exact;
if( ! @jobs ) {
# perhaps the name is not in the config file
# but is a job on the jenkins master
for my $jenkins ( @jenkins ) {
push @jobs, $jenkins->jobs($filter);
}
}
if( ! @jobs ) {
return [$filter] if $action eq 'create';
die "No jobs found for pattern /$filter/\n";
}
}
return $jobs = \@jobs;
}
}
sub login {
for my $jenkins ( @jenkins ) {
$jenkins->login();
}
}
sub list {
my ( $jobs, $args ) = @_;
for my $job ( @$jobs ) {
my $markers = "";
$markers .= "*" if $job->is_running;
$markers .= "?" if $job->was_aborted;
$markers .= "+" if $job->is_queued;
print colorize($job->color, $job->name), "$markers $job->{url}\n";
}
}
sub queue {
for my $jenkins ( @jenkins ) {
my $queue = $jenkins->queue();
for my $host ( keys %{$queue->{blocked}} ) {
my $hostStr = $host;
# if hostname will wrap, just truncate the middle
if ( length($host) > 76 ) {
$hostStr = substr($host,0,31) . "..." . substr($host,-31,31);
}
print colorize("bold", colorize("underline", $hostStr)), "\n";
print " ", colorize($_->color, $_->name), "\n" for @{$queue->{blocked}->{$host}};
}
if ( $jenkins->{stuck} ) {
for my $host ( keys %{$queue->{stuck}} ) {
my $hostStr = $host;
# if hostname will wrap, just truncate the middle
if ( length($host) > 76 ) {
$hostStr = substr($host,0,31) . "..." . substr($host,-31,31);
}
print colorize("bold", colorize("red", colorize("underline", $hostStr))), "\n";
print " ", colorize($_->color, $_->name), "\n" for @{$queue->{stuck}->{$host}};
}
}
if ( @{$queue->{running}} ) {
print colorize("bold", colorize("underline", "ALREADY RUNNING")), "\n";
print " ", colorize($_->color, $_->name), "\n" for @{$queue->{running}};
}
if ( @{$queue->{quieted}} ) {
print colorize("bold", colorize("underline", "QUIETED")), "\n";
print " ", colorize($_->color, $_->name), "\n" for @{$queue->{quieted}};
}
}
}
sub tail {
my ( $jobs, $args ) = @_;
require_one($jobs);
my $job = $jobs->[0];
my $cursor = $job->logCursor;
while(1) {
my $content = $cursor->();
last unless defined $content;
print $content;
sleep 1;
}
}
sub history {
my ( $jobs, $args ) = @_;
require_one($jobs);
my $job = $jobs->[0];
my @jobs = $job->history();
for my $job ( @jobs ) {
my $markers = "";
$markers .= "*" if $job->is_running;
$markers .= "?" if $job->was_aborted;
print "#" , $job->number(),
" - ", colorize($job->color, scalar localtime($job->started)),
sprintf("%-2s [%07.03f sec]\n", $markers, $job->duration);
}
}
sub config {
my ( $jobs, $args ) = @_;
require_one($jobs);
my $job = $jobs->[0];
print $job->config();
}
sub create {
my ( $jobs, $args ) = @_;
require_one($jobs);
my $job = $jobs->[0];
my $configFile = shift @$args;
my $config = do {
local $/;
open my $fh, "<$configFile" or die "Could not read config file $configFile: $!";
<$fh>;
};
$jenkins[0]->create($job,$config);
}
sub require_one {
my ( $jobs ) = @_;
if ( @$jobs > 1 ) {
my $prog = File::Basename::basename($0);
my ($pkg, $func) = (caller(1))[0,3];
$func =~ s/$pkg\:://;
die scalar(@$jobs)
. " matches for pattern /$filter/ but only one job can be sent to: $prog $func\nMatches:\n\t"
. join("\n\t", map { $_->{name} } @$jobs )
. "\n";
}
}
sub confirm_multiple {
my ($operation, $jobs) = @_;
return if $jenkins[0]->{yes};
my ($in, $out) = $jenkins[0]->stdio;
while(1 && ! $opts{yes} ) {
print $out "Mutliple jobs found for pattern /$filter/\n";
print $out " ", $_->name, "\n" for @$jobs;
print $out "Do you want to $operation all of them? [y/N]: ";
my $ans = <$in>;
chomp($ans);
if( !$ans || $ans =~ /^n/i ) {
exit;
}
if( $ans =~ /^y/i ) {
return;
}
print $out "Sorry, didn't understand \"$ans\"\n";
}
}
sub colorize {
my ( $color ) = shift;
# dont colorize when redirected to non-tty
return @_ unless -t STDOUT;
return color($color), @_, color("reset");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment