Each row of the new Records table allows storing part of the details of an event or of the state of the TestBot. These pieces of information are put together into groups and timestamped through the RecordGroups table. Together these tables allow rebuilding the activity of the TestBot. The first user of these new tables is the job scheduler which uses them to store the new VM status if it changed. This will allow rebuilding and visualizing the activity of the TestBot for monitoring or debugging.
Signed-off-by: Francois Gouget fgouget@codeweavers.com ---
This patch requires updating the database with the update29.sql script and then restarting the TestBot Engine and web server.
testbot/bin/Janitor.pl | 21 ++ testbot/ddl/update29.sql | 20 ++ testbot/ddl/winetestbot.sql | 19 ++ testbot/doc/winetestbot-schema.dia | 338 ++++++++++++++++++++++++++++++++ testbot/lib/WineTestBot/Jobs.pm | 64 +++++- testbot/lib/WineTestBot/RecordGroups.pm | 117 +++++++++++ testbot/lib/WineTestBot/Records.pm | 126 ++++++++++++ testbot/lib/WineTestBot/VMs.pm | 60 ++++++ 8 files changed, 760 insertions(+), 5 deletions(-) create mode 100644 testbot/ddl/update29.sql create mode 100644 testbot/lib/WineTestBot/RecordGroups.pm create mode 100644 testbot/lib/WineTestBot/Records.pm
diff --git a/testbot/bin/Janitor.pl b/testbot/bin/Janitor.pl index c07c21bd..4f0e4de0 100755 --- a/testbot/bin/Janitor.pl +++ b/testbot/bin/Janitor.pl @@ -5,6 +5,7 @@ # archives old jobs and purges older jobs and patches. # # Copyright 2009 Ge van Geldorp +# Copyright 2017 Francois Gouget # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -45,6 +46,7 @@ use WineTestBot::Log; use WineTestBot::Patches; use WineTestBot::PendingPatchSets; use WineTestBot::CGI::Sessions; +use WineTestBot::RecordGroups; use WineTestBot::Users; use WineTestBot::VMs;
@@ -267,3 +269,22 @@ else { LogMsg "0Unable to open '$DataDir/staging': $!"; } + +# Delete obsolete record groups +if ($JobPurgeDays != 0) +{ + $DeleteBefore = time() - $JobPurgeDays * 86400; + my $RecordGroups = CreateRecordGroups(); + foreach my $RecordGroup (@{$RecordGroups->GetItems()}) + { + if ($RecordGroup->Timestamp < $DeleteBefore) + { + my $ErrMessage = $RecordGroups->DeleteItem($RecordGroup); + if (defined($ErrMessage)) + { + LogMsg $ErrMessage, "\n"; + } + } + } + $RecordGroups = undef; +} diff --git a/testbot/ddl/update29.sql b/testbot/ddl/update29.sql new file mode 100644 index 00000000..af49814f --- /dev/null +++ b/testbot/ddl/update29.sql @@ -0,0 +1,20 @@ +USE winetestbot; + +CREATE TABLE RecordGroups +( + Id INT(6) NOT NULL AUTO_INCREMENT, + Timestamp DATETIME NOT NULL, + PRIMARY KEY (Id) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE Records +( + RecordGroupId INT(6) NOT NULL, + Type ENUM('engine', 'tasks', 'vmresult', 'vmstatus') NOT NULL, + Name VARCHAR(96) NOT NULL, + Value VARCHAR(64) NULL, + PRIMARY KEY (RecordGroupId, Type, Name), + FOREIGN KEY (RecordGroupId) REFERENCES RecordGroups(Id) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/testbot/ddl/winetestbot.sql b/testbot/ddl/winetestbot.sql index 12c1b689..ad8d1a92 100644 --- a/testbot/ddl/winetestbot.sql +++ b/testbot/ddl/winetestbot.sql @@ -158,6 +158,25 @@ CREATE TABLE Tasks ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE RecordGroups +( + Id INT(6) NOT NULL AUTO_INCREMENT, + Timestamp DATETIME NOT NULL, + PRIMARY KEY (Id) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE Records +( + RecordGroupId INT(6) NOT NULL, + Type ENUM('engine', 'tasks', 'vmresult', 'vmstatus') NOT NULL, + Name VARCHAR(96) NOT NULL, + Value VARCHAR(64) NULL, + PRIMARY KEY (RecordGroupId, Type, Name), + FOREIGN KEY (RecordGroupId) REFERENCES RecordGroups(Id) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; + INSERT INTO Roles (Name, IsDefaultRole) VALUES('admin', 'N'); INSERT INTO Roles (Name, IsDefaultRole) VALUES('wine-devel', 'Y');
diff --git a/testbot/doc/winetestbot-schema.dia b/testbot/doc/winetestbot-schema.dia index 9092c417..ef7c3ca7 100644 --- a/testbot/doc/winetestbot-schema.dia +++ b/testbot/doc/winetestbot-schema.dia @@ -3218,5 +3218,343 @@ <dia:real val="0.10000000000000001"/> </dia:attribute> </dia:object> + <dia:object type="Database - Table" version="0" id="O23"> + <dia:attribute name="obj_pos"> + <dia:point val="14.12,3.7"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="14.12,3.7;24.66,8.3"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="elem_corner"> + <dia:point val="14.12,3.7"/> + </dia:attribute> + <dia:attribute name="elem_width"> + <dia:real val="10.539999999999999"/> + </dia:attribute> + <dia:attribute name="elem_height"> + <dia:real val="4.6000000000000005"/> + </dia:attribute> + <dia:attribute name="name"> + dia:string#Records#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="visible_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="underline_primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="tagging_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="bold_primary_keys"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="attributes"> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#RecordGroupId#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT(6)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Type#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#ENUM#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Name#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(96)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Value#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(64)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="name_font"> + <dia:font family="sans" style="80" name="Helvetica-Bold"/> + </dia:attribute> + <dia:attribute name="comment_font"> + <dia:font family="sans" style="8" name="Helvetica-Oblique"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.80000000000000004"/> + </dia:attribute> + <dia:attribute name="name_font_height"> + <dia:real val="0.99999999999999989"/> + </dia:attribute> + <dia:attribute name="comment_font_height"> + <dia:real val="0.69999999999999996"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="fill_colour"> + <dia:color val="#ffffffff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + </dia:object> + <dia:object type="Database - Table" version="0" id="O24"> + <dia:attribute name="obj_pos"> + <dia:point val="2.61,3.7125"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="2.61,3.7125;10.455,6.7125"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="elem_corner"> + <dia:point val="2.61,3.7125"/> + </dia:attribute> + <dia:attribute name="elem_width"> + <dia:real val="7.8449999999999998"/> + </dia:attribute> + <dia:attribute name="elem_height"> + <dia:real val="3"/> + </dia:attribute> + <dia:attribute name="name"> + dia:string#RecordGroups#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="visible_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="underline_primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="tagging_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="bold_primary_keys"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="attributes"> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Id#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT(6)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Timestamp#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#DATETIME#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="name_font"> + <dia:font family="sans" style="80" name="Helvetica-Bold"/> + </dia:attribute> + <dia:attribute name="comment_font"> + <dia:font family="sans" style="8" name="Helvetica-Oblique"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.80000000000000004"/> + </dia:attribute> + <dia:attribute name="name_font_height"> + <dia:real val="0.99999999999999989"/> + </dia:attribute> + <dia:attribute name="comment_font_height"> + <dia:real val="0.69999999999999996"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="fill_colour"> + <dia:color val="#ffffffff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + </dia:object> + <dia:object type="Database - Reference" version="0" id="O25"> + <dia:attribute name="obj_pos"> + <dia:point val="10.455,5.4125"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="10.405,4.75;14.17,5.4625"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="orth_points"> + <dia:point val="10.455,5.4125"/> + <dia:point val="11.95,5.4125"/> + <dia:point val="11.95,5.4"/> + <dia:point val="14.12,5.4"/> + </dia:attribute> + <dia:attribute name="orth_orient"> + <dia:enum val="0"/> + <dia:enum val="1"/> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="orth_autoroute"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + <dia:attribute name="line_style"> + <dia:enum val="0"/> + <dia:real val="1"/> + </dia:attribute> + <dia:attribute name="corner_radius"> + <dia:real val="0"/> + </dia:attribute> + <dia:attribute name="end_arrow"> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="start_point_desc"> + dia:string#1#</dia:string> + </dia:attribute> + <dia:attribute name="end_point_desc"> + dia:string#1..n#</dia:string> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.59999999999999998"/> + </dia:attribute> + dia:connections + <dia:connection handle="0" to="O24" connection="13"/> + <dia:connection handle="1" to="O23" connection="12"/> + </dia:connections> + </dia:object> </dia:layer> </dia:diagram> diff --git a/testbot/lib/WineTestBot/Jobs.pm b/testbot/lib/WineTestBot/Jobs.pm index 0144ddec..73fc34eb 100644 --- a/testbot/lib/WineTestBot/Jobs.pm +++ b/testbot/lib/WineTestBot/Jobs.pm @@ -377,6 +377,7 @@ use WineTestBot::WineTestBotObjects; use WineTestBot::Branches; use WineTestBot::Config; use WineTestBot::Patches; +use WineTestBot::RecordGroups; use WineTestBot::Steps; use WineTestBot::Users; use WineTestBot::VMs; @@ -488,10 +489,9 @@ kept on standby so they are ready when their turn comes. =back =cut
-sub ScheduleOnHost($$$) +sub ScheduleOnHost($$$$) { - my ($ScopeObject, $SortedJobs, $Hypervisors) = @_; - + my ($ScopeObject, $SortedJobs, $Hypervisors, $Records) = @_;
my $HostVMs = CreateVMs($ScopeObject); $HostVMs->FilterEnabledRole(); @@ -580,7 +580,7 @@ sub ScheduleOnHost($$$) { my $ErrMessage = $Task->Run($Step); return $ErrMessage if (defined $ErrMessage); - + $VM->RecordStatus($Records, join(" ", "running", $Job->Id, $Step->No, $Task->No)); $Job->UpdateStatus(); $IdleCount--; $RunningCount++; @@ -655,6 +655,7 @@ sub ScheduleOnHost($$$)
my $ErrMessage = $VM->RunPowerOff(); return $ErrMessage if (defined $ErrMessage); + $VM->RecordStatus($Records, "dirty poweroff"); }
# Power off some idle VMs we don't need immediately so we can revert more @@ -672,6 +673,7 @@ sub ScheduleOnHost($$$)
my $ErrMessage = $VM->RunPowerOff(); return $ErrMessage if (defined $ErrMessage); + $VM->RecordStatus($Records, "dirty poweroff"); $PlannedActiveCount--; last if ($PlannedActiveCount <= $MaxActiveVMs); } @@ -733,6 +735,8 @@ sub ScheduleOnHost($$$) return undef; }
+my $_LastTaskCounts = ""; + =pod =over 12
@@ -746,12 +750,40 @@ them using WineTestBot::Jobs::ScheduleOnHost().
sub ScheduleJobs() { + my $RecordGroups = CreateRecordGroups(); + my $RecordGroup = $RecordGroups->Add(); + my $Records = $RecordGroup->Records; + # Save the new RecordGroup now so its Id is lower than those of the groups + # created by the scripts called from the scheduler. + $RecordGroups->Save(); + my $Jobs = CreateJobs(); $Jobs->AddFilter("Status", ["queued", "running"]); my @SortedJobs = sort CompareJobPriority @{$Jobs->GetItems()}; # Note that even if there are no jobs to schedule # we should check if there are VMs to revert
+ # Count the runnable and queued tasks for the record + my ($RunnableTasks, $QueuedTasks) = (0, 0); + foreach my $Job (@SortedJobs) + { + my $Steps = $Job->Steps; + $Steps->AddFilter("Status", ["queued", "running"]); + my @SortedSteps = sort { $a->No <=> $b->No } @{$Steps->GetItems()}; + next if (!@SortedSteps); + + my $Tasks = $SortedSteps[0]->Tasks; + $Tasks->AddFilter("Status", ["queued"]); + $RunnableTasks += @{$Tasks->GetItems()}; + + foreach my $Step (@SortedSteps) + { + my $Tasks = $Step->Tasks; + $Tasks->AddFilter("Status", ["queued"]); + $QueuedTasks += scalar(@{$Tasks->GetItems()}); + } + } + my %Hosts; my $VMs = CreateVMs($Jobs); $VMs->FilterEnabledRole(); @@ -765,9 +797,31 @@ sub ScheduleJobs() foreach my $Host (keys %Hosts) { my @HostHypervisors = keys %{$Hosts{$Host}}; - my $HostErrMessage = ScheduleOnHost($Jobs, @SortedJobs, @HostHypervisors); + my $HostErrMessage = ScheduleOnHost($Jobs, @SortedJobs, @HostHypervisors, $Records); push @ErrMessages, $HostErrMessage if (defined $HostErrMessage); } + + # Note that any VM Status or Role change will trigger ScheduleJobs() so this + # records all VM state changes. + $VMs = CreateVMs(); + map { $_->RecordStatus($Records) } (@{$VMs->GetItems()}); + if (@{$Records->GetItems()}) + { + # FIXME Add the number of tasks scheduled to run on a maintenance, retired + # or deleted VM... + my $TaskCounts = "$RunnableTasks $QueuedTasks 0"; + if ($TaskCounts ne $_LastTaskCounts) + { + $Records->AddRecord('tasks', 'counters', $TaskCounts); + $_LastTaskCounts = $TaskCounts; + } + $RecordGroups->Save(); + } + else + { + $RecordGroups->DeleteItem($RecordGroup); + } + return @ErrMessages ? join("\n", @ErrMessages) : undef; }
diff --git a/testbot/lib/WineTestBot/RecordGroups.pm b/testbot/lib/WineTestBot/RecordGroups.pm new file mode 100644 index 00000000..b33d16eb --- /dev/null +++ b/testbot/lib/WineTestBot/RecordGroups.pm @@ -0,0 +1,117 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Copyright 2017 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +package WineTestBot::RecordGroup; + +=head1 NAME + +WineTestBot::RecordGroup - a group of related history records + +=head1 DESCRIPTION + +A RecordGroup is a group of WineTestBot::Record objects describing an event +or the state of the TestBot at at a given time. + +=cut + +use WineTestBot::WineTestBotObjects; +use WineTestBot::Config; + +use vars qw (@ISA @EXPORT); + +require Exporter; +@ISA = qw(WineTestBot::WineTestBotItem Exporter); + +sub InitializeNew($$) +{ + my ($self, $Collection) = @_; + + $self->Timestamp(time()); + + $self->SUPER::InitializeNew($Collection); +} + + +package WineTestBot::RecordGroups; + +=head1 NAME + +WineTestBot::RecordGroups - A collection of WineTestBot::RecordGroup objects + +=cut + +use ObjectModel::BasicPropertyDescriptor; +use ObjectModel::EnumPropertyDescriptor; +use ObjectModel::DetailrefPropertyDescriptor; +use ObjectModel::PropertyDescriptor; +use WineTestBot::WineTestBotObjects; +use WineTestBot::Records; + +use vars qw (@ISA @EXPORT @PropertyDescriptors); + +require Exporter; +@ISA = qw(WineTestBot::WineTestBotCollection Exporter); +@EXPORT = qw(&CreateRecordGroups &SaveRecord); + + +BEGIN +{ + @PropertyDescriptors = ( + CreateBasicPropertyDescriptor("Id", "Group id", 1, 1, "S", 6), + CreateBasicPropertyDescriptor("Timestamp", "Timestamp", !1, 1, "DT", 19), + CreateDetailrefPropertyDescriptor("Records", "Records", !1, !1, &CreateRecords), + ); +} + +sub CreateItem($) +{ + my ($self) = @_; + + return WineTestBot::RecordGroup->new($self); +} + +sub CreateRecordGroups(;$) +{ + my ($ScopeObject) = @_; + return WineTestBot::RecordGroups->new("RecordGroups", "RecordGroups", "RecordGroup", + @PropertyDescriptors, $ScopeObject); +} + +=pod +=over 12 + +=item C<SaveRecord()> + +Creates and saves a standalone record. + +=back +=cut + +sub SaveRecord($$;$) +{ + my ($Type, $Name, $Value) = @_; + + my $RecordGroups = CreateRecordGroups(); + my $Records = $RecordGroups->Add()->Records; + $Records->AddRecord($Type, $Name, $Value); + + return $RecordGroups->Save(); +} + +1; diff --git a/testbot/lib/WineTestBot/Records.pm b/testbot/lib/WineTestBot/Records.pm new file mode 100644 index 00000000..dd0fec97 --- /dev/null +++ b/testbot/lib/WineTestBot/Records.pm @@ -0,0 +1,126 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Copyright 2017 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +package WineTestBot::Record; + +=head1 NAME + +WineTestBot::Record - records part of an event or the state of the TestBot. + +=head1 DESCRIPTION + +A Record is created to save information about an event or part of the state of +the TestBot at a given time. A full description of said event or state may +require a variable number of Records so they are part of a RecordGroup which +identifies which other Records relate to the same event or state. +The RecordGroup also stores the timestamp of the event or state. + +The point of the Record objects is to keep a record of the activity of the +TestBot. By putting them together it is possible to rebuild what the TestBot +did and when, for debugging or performance analysis. The amount of details is +only limited by the amount of data dumped into the Records table. + +=cut + +use WineTestBot::Config; +use WineTestBot::WineTestBotObjects; + +use vars qw (@ISA @EXPORT); + +require Exporter; +@ISA = qw(WineTestBot::WineTestBotItem Exporter); + +sub InitializeNew($$) +{ + my ($self, $Collection) = @_; + + $self->SUPER::InitializeNew($Collection); +} + + +package WineTestBot::Records; + +=head1 NAME + +WineTestBot::Records - A collection of WineTestBot::Record objects + +=cut + +use ObjectModel::BasicPropertyDescriptor; +use ObjectModel::EnumPropertyDescriptor; +use ObjectModel::PropertyDescriptor; +use WineTestBot::WineTestBotObjects; + +use vars qw (@ISA @EXPORT @PropertyDescriptors); + +require Exporter; +@ISA = qw(WineTestBot::WineTestBotCollection Exporter); +@EXPORT = qw(&CreateRecords); + + +BEGIN +{ + @PropertyDescriptors = ( + CreateEnumPropertyDescriptor("Type", "Type", 1, 1, ['engine', 'tasks', 'vmresult', 'vmstatus']), + CreateBasicPropertyDescriptor("Name", "Name", 1, 1, "A", 96), + CreateBasicPropertyDescriptor("Value", "Value", !1, !1, "A", 64), + ); +} + +sub CreateItem($) +{ + my ($self) = @_; + + return WineTestBot::Record->new($self); +} + +sub CreateRecords(;$$) +{ + my ($ScopeObject, $RecordGroup) = @_; + return WineTestBot::Records->new("Records", "Records", "Record", + @PropertyDescriptors, $ScopeObject, + $RecordGroup); +} + +=pod +=over 12 + +=item C<AddRecord()> + +This is a convenience function for adding a new record to a Records collection +and setting its properties at the same time. + +=back +=cut + +sub AddRecord($$$;$) +{ + my ($self, $Type, $Name, $Value) = @_; + + my $Record = $self->Add(); + my $TemporaryKey = $Record->GetKey(); + $Record->Type($Type); + $Record->Name($Name); + $Record->Value($Value) if (defined $Value); + $self->KeyChanged($TemporaryKey, $Record->GetKey()); + + return $Record; +} + +1; diff --git a/testbot/lib/WineTestBot/VMs.pm b/testbot/lib/WineTestBot/VMs.pm index 7db0aa51..a8da842b 100644 --- a/testbot/lib/WineTestBot/VMs.pm +++ b/testbot/lib/WineTestBot/VMs.pm @@ -148,6 +148,7 @@ use ObjectModel::BackEnd; use WineTestBot::Config; use WineTestBot::Engine::Notify; use WineTestBot::LibvirtDomain; +use WineTestBot::RecordGroups; use WineTestBot::TestAgent; use WineTestBot::WineTestBotObjects;
@@ -175,6 +176,13 @@ sub InitializeNew($$) $self->SUPER::InitializeNew($Collection); }
+sub HasEnabledRole($) +{ + my ($self) = @_; + # Filter out the disabled VMs, that is the retired and deleted ones + return $self->Role ne "retired" && $self->Role ne "deleted"; +} + sub GetHost($) { my ($self) = @_; @@ -499,6 +507,58 @@ sub RunRevert($) return $self->_RunVMTool("reverting", ["--log-only", "revert", $self->GetKey()]); }
+=pod +=over 12 + +=item C<GetRecordName()> + +Provides the name to use for history records related to this VM. + +=back +=cut + +sub GetRecordName($) +{ + my ($self) = @_; + return $self->Name ." ". $self->GetHost(); +} + +my %_VMStatuses; + +=pod +=over 12 + +=item C<RecordStatus()> + +Adds a Record if the status of the VM changed since the last recorded status. + +=back +=cut + +sub RecordStatus($$;$) +{ + my ($self, $Records, $RecordStatus) = @_; + + $RecordStatus ||= $self->HasEnabledRole() ? $self->Status : $self->Role; + my $NewStatus = $self->GetHost() ." $RecordStatus"; + + my $LastStatus = $_VMStatuses{$self->Name} || ""; + # Don't add a record if nothing changed + return if ($LastStatus eq $NewStatus); + # Or if the new status is less complete + return if ($LastStatus =~ /^\Q$NewStatus \E/); + + $_VMStatuses{$self->Name} = $NewStatus; + if ($Records) + { + $Records->AddRecord('vmstatus', $self->GetRecordName(), $RecordStatus); + } + else + { + SaveRecord('vmstatus', $self->GetRecordName(), $RecordStatus); + } +} +
package WineTestBot::VMs;
This allows providing more information about the initial state of the VMs such as linking running VMs to the corresponding Task, and identifying the initial dirty status as running 'LibvirtTool checkidle'.
Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/bin/Engine.pl | 44 ++++++++++++++++++++++++++++++++++-------- testbot/lib/WineTestBot/VMs.pm | 7 +++++++ 2 files changed, 43 insertions(+), 8 deletions(-)
diff --git a/testbot/bin/Engine.pl b/testbot/bin/Engine.pl index 7dab98e6..36a18ade 100755 --- a/testbot/bin/Engine.pl +++ b/testbot/bin/Engine.pl @@ -52,6 +52,7 @@ use WineTestBot::Jobs; use WineTestBot::Log; use WineTestBot::Patches; use WineTestBot::PendingPatchSets; +use WineTestBot::RecordGroups; use WineTestBot::Utils; use WineTestBot::VMs;
@@ -102,7 +103,7 @@ sub Cleanup($;$$)
# Verify that the running tasks are still alive and requeue them if not. # Ignore the Job and Step status fields because they may be a bit out of date. - my %BusyVMs; + my %RunningVMs; foreach my $Job (@{CreateJobs()->GetItems()}) { my $CallUpdateStatus; @@ -135,7 +136,7 @@ sub Cleanup($;$$) { # This task is still running! LogMsg "$TaskKey is still running\n"; - $BusyVMs{$Task->VM->GetKey()} = 1; + $RunningVMs{$Task->VM->GetKey()} = join(" ", "running", $Job->Id, $Step->No, $Task->No); next; } if ($Requeue) @@ -158,16 +159,27 @@ sub Cleanup($;$$) }
# Get the VMs in order now + my $RecordGroups = CreateRecordGroups(); + my $Records = $RecordGroups->Add()->Records; + # Save the new RecordGroup now so its Id is lower than those of the groups + # created by the scripts called from Cleanup(). + $RecordGroups->Save(); + my $VMs = CreateVMs(); - $VMs->FilterEnabledRole(); - $VMs->FilterEnabledStatus(); foreach my $VM (@{$VMs->GetItems()}) { my $VMKey = $VM->GetKey(); - if ($BusyVMs{$VMKey}) + if (!$VM->HasEnabledRole() or !$VM->HasEnabledStatus()) + { + $VM->RecordStatus($Records); + next; + } + + if ($RunningVMs{$VMKey}) { # This VM is still running a task. Let it. - LogMsg "$VMKey is used by a task\n"; + LogMsg "$VMKey is $RunningVMs{$VMKey}\n"; + $VM->RecordStatus($Records, $RunningVMs{$VMKey}); next; }
@@ -177,27 +189,39 @@ sub Cleanup($;$$) { $VM->KillChild(); $VM->RunPowerOff(); + $VM->RecordStatus($Records, "dirty poweroff (kill tasks)"); } elsif ($KillVMs and $VM->Status ne "running") { $VM->KillChild(); # $KillVMs is normally used on shutdown so don't start a process that # will get stuck 'forever' waiting for an offline VM. - $VM->RunPowerOff() if ($VM->Status ne "offline"); + if ($VM->Status ne "offline") + { + $VM->RunPowerOff(); + $VM->RecordStatus($Records, "dirty poweroff (kill vms)"); + } } elsif (!$VM->CanHaveChild()) { # The VM should not have a process. $VM->KillChild(); $VM->RunPowerOff(); + $VM->RecordStatus($Records, "dirty poweroff (unexpected process)"); + } + elsif ($Starting) + { + # Let the process finish its work. Note that on shutdown we don't + # record the VM status if it did not change. + $VM->RecordStatus($Records); } - # else let the process finish its work } elsif ($Starting) { if ($VM->Status eq "idle") { $VM->RunCheckIdle(); + $VM->RecordStatus($Records, "dirty idle check"); } else { @@ -205,6 +229,7 @@ sub Cleanup($;$$) # This is the simplest way to resync the VM status field. # Also powering off a powered off VM will detect offline VMs. $VM->RunPowerOff(); + $VM->RecordStatus($Records, "dirty poweroff"); } } # $KillVMs is normally used on shutdown so don't start a process that @@ -212,8 +237,11 @@ sub Cleanup($;$$) elsif ($KillVMs and $VM->Status !~ /^(?:off|offline)$/) { $VM->RunPowerOff(); + $VM->RecordStatus($Records, "dirty poweroff (kill vms)"); } + # Note that on shutdown we don't record the VM status if it did not change. } + $RecordGroups->Save(); }
diff --git a/testbot/lib/WineTestBot/VMs.pm b/testbot/lib/WineTestBot/VMs.pm index a8da842b..961be52f 100644 --- a/testbot/lib/WineTestBot/VMs.pm +++ b/testbot/lib/WineTestBot/VMs.pm @@ -183,6 +183,13 @@ sub HasEnabledRole($) return $self->Role ne "retired" && $self->Role ne "deleted"; }
+sub HasEnabledStatus($) +{ + my ($self) = @_; + # Filter out the maintenance VMs + return $self->Status ne "maintenance"; +} + sub GetHost($) { my ($self) = @_;
Signed-off-by: Francois Gouget fgouget@codeweavers.com ---
For the next patch.
testbot/lib/ObjectModel/Collection.pm | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+)
diff --git a/testbot/lib/ObjectModel/Collection.pm b/testbot/lib/ObjectModel/Collection.pm index d4418603..c3bc438c 100644 --- a/testbot/lib/ObjectModel/Collection.pm +++ b/testbot/lib/ObjectModel/Collection.pm @@ -373,6 +373,28 @@ sub GetItems($) =pod =over 12
+=item C<GetItemsCount()> + +Returns how many Items are present in the Collection. + +=back +=cut + +sub GetItemsCount($) +{ + my ($self) = @_; + + if (! $self->{Loaded}) + { + $self->Load(); + } + + return scalar(keys %{$self->{Items}}); +} + +=pod +=over 12 + =item C<IsEmpty()>
Returns true if the Collection contains no Item.
The main index page is nice to browse the jobs but does not allow getting a synthetic picture of what the TestBot is doing at a given time. Even the VM table at the bottom falls short as it only shows the current state and does not even show what task a given VM is running.
Each row of the new Activity page shows what each VM was doing at that time, with color coding, links to the relevant task for running VMs, and a count of the runnable and queued tasks. Going down the table shows past history, allowing one to see how the TestBot got in the current state. The VMs are also grouped by host which makes it easier to verify that the scheduler is obeying its operational parameters like the limits on simultaneous running tasks, reverting VMs, etc. While the activity page shows no sensitive information for now access to it is restricted to users with a TestBot account.
Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/lib/WineTestBot/Activity.pm | 199 ++++++++++++++++++++ testbot/lib/WineTestBot/CGI/PageBase.pm | 1 + testbot/lib/WineTestBot/Config.pm | 8 +- testbot/lib/WineTestBot/ConfigLocalTemplate.pl | 4 + testbot/lib/WineTestBot/RecordGroups.pm | 12 +- testbot/lib/WineTestBot/Records.pm | 24 ++- testbot/web/Activity.pl | 249 +++++++++++++++++++++++++ testbot/web/WineTestBot.css | 39 ++++ 8 files changed, 528 insertions(+), 8 deletions(-) create mode 100644 testbot/lib/WineTestBot/Activity.pm create mode 100644 testbot/web/Activity.pl
diff --git a/testbot/lib/WineTestBot/Activity.pm b/testbot/lib/WineTestBot/Activity.pm new file mode 100644 index 00000000..3a9081a3 --- /dev/null +++ b/testbot/lib/WineTestBot/Activity.pm @@ -0,0 +1,199 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Copyright 2017 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +package WineTestBot::Activity; + +=head1 NAME + +WineTestBot::Activity - reconstruct the TestBot's activity from its history records. + +=cut + +use WineTestBot::Config; +use WineTestBot::Jobs; +use WineTestBot::RecordGroups; +use WineTestBot::Records; + +use vars qw (@ISA @EXPORT); + +require Exporter; +@ISA = qw(Exporter); +@EXPORT = qw(&GetActivity); + + +=pod +=over 12 + +=item C<GetActivity()> + +Loads the records for the specified VMs and processes them to build a structure +describing the TestBot activity. The structure is as follows: + + { <GroupNo1> => { + start => <StartTimestamp>, + end => <EndTimestamp>, + runnable => <RunnableTasksCount>, + queued => <QueuedTasksCount>, + statusvms => { + <VMName1> => { + vm => <VMObject>, + status => <VMStatus>, + value => <RecordValue>, + task => <TaskObjectIfAppopriate>, + start => <StartTimestamp>, + end => <EndTimestamp>, + rows => <NbRows>, + }, + <VMName2> => { + ... + }, + }, + }, + <GroupNo2> => { + ... + }, + ... + } + +=back +=cut + +sub GetActivity($) +{ + my ($VMs) = @_; + my ($Activity, $Counters) = ({}, {}); + + ### First load all the RecordGroups + my $RecordGroups = CreateRecordGroups(); + $Counters->{recordgroups} = $RecordGroups->GetItemsCount(); + foreach my $RecordGroup (@{$RecordGroups->GetItems()}) + { + $Activity->{$RecordGroup->Id} = { start => $RecordGroup->Timestamp }; + } + + ### And then load all the Records in one go + # Loading the whole table at once is more efficient than loading it piecemeal + # one RecordGroup at a time. + + my $Jobs = CreateJobs(); + my $Records = CreateRecords(); + $Counters->{records} = $Records->GetItemsCount(); + foreach my $Record (@{$Records->GetItems()}) + { + my $Group = $Activity->{$Record->RecordGroupId}; + if ($Record->Type eq "tasks" and $Record->Name eq "counters") + { + ($Group->{runnable}, $Group->{queued}) = split / /, $Record->Value; + } + elsif ($Record->Type eq "vmstatus") + { + # Ignore retired / deleted VMs + my ($RecordName, $RecordHost) = split / /, $Record->Name; + next if (!$VMs->ItemExists($RecordName)); + + my $StatusVMs = ( $Group->{statusvms} ||= {} ); + my $VMStatus = ( $StatusVMs->{$RecordName} ||= {} ); + + $VMStatus->{host} = $RecordHost; + $VMStatus->{vmstatus} = $VMStatus; + $VMStatus->{start} = $Group->{start}; + my ($Status, @Extra) = split / /, $Record->Value; + $VMStatus->{status} = $Status; + $VMStatus->{rows} = 1; + + if ($Status eq "running") + { + $VMStatus->{job} = $Jobs->GetItem($Extra[0]); + $VMStatus->{step} = $VMStatus->{job}->Steps->GetItem($Extra[1]) if ($VMStatus->{job}); + $VMStatus->{task} = $VMStatus->{step}->Tasks->GetItem($Extra[2]) if ($VMStatus->{step}); + } + elsif (@Extra) + { + # @Extra contains details about the current status, such as what + # type of process a dirty VM is running, or how it came to be dirty + # in the first place. + $VMStatus->{details} = join(" ", @Extra); + } + } + } + + ### Fill the holes in the table, compute end times, etc. + + my ($LastGroup, %LastStatusVMs); + foreach my $RecordGroup (sort CompareRecordGroups @{$RecordGroups->GetItems()}) + { + my $Group = $Activity->{$RecordGroup->Id}; + my $StatusVMs = $Group->{statusvms}; + next if (!$StatusVMs); + if ($LastGroup) + { + $LastGroup->{end} = $Group->{start}; + foreach my $Counter ('runnable', 'queued') + { + if (!exists $Group->{$Counter} and exists $LastGroup->{$Counter}) + { + $Group->{$Counter} = $LastGroup->{$Counter}; + } + } + } + $LastGroup = $Group; + + foreach my $VM (@{$VMs->GetItems()}) + { + my $LastVMStatus = $LastStatusVMs{$VM->Name} ? $LastStatusVMs{$VM->Name}->{$VM->Name} : undef; + + my $VMStatus = $StatusVMs->{$VM->Name}; + if ($VMStatus) + { + $LastVMStatus->{end} = $VMStatus->{start} if ($LastVMStatus); + } + elsif ($LastVMStatus) + { + $VMStatus = $StatusVMs->{$VM->Name} = $LastVMStatus; + $LastStatusVMs{$VM->Name}->{$VM->Name} = {merged => 1, vmstatus => $VMStatus}; + $VMStatus->{rows}++; + } + else + { + $VMStatus = $StatusVMs->{$VM->Name} = { + start => $Group->{start}, + status => "unknown", + rows => 1}; + $VMStatus->{vmstatus} = $VMStatus; + } + $LastStatusVMs{$VM->Name} = $StatusVMs; + } + } + $LastGroup->{end} = time() if ($LastGroup); + + foreach my $VM (@{$VMs->GetItems()}) + { + my $LastVMStatus = $LastStatusVMs{$VM->Name}->{$VM->Name}; + next if (!$LastVMStatus); + $LastVMStatus->{end} = time(); + if ($LastVMStatus->{status} eq "unknown") + { + $LastVMStatus->{status} = $VM->Status; + } + } + + return ($Activity, $Counters); +} + +1; diff --git a/testbot/lib/WineTestBot/CGI/PageBase.pm b/testbot/lib/WineTestBot/CGI/PageBase.pm index 4ae190e2..fe03a47e 100644 --- a/testbot/lib/WineTestBot/CGI/PageBase.pm +++ b/testbot/lib/WineTestBot/CGI/PageBase.pm @@ -265,6 +265,7 @@ EOF print " <li class='divider'> </li>\n"; print " <li><p><a href='", MakeSecureURL("/Submit.pl"), "'>Submit job</a></p></li>\n"; + print " <li><p><a href='/Activity.pl'>Activity</a></p></li>\n"; print " <li class='divider'> </li>\n"; print " <li><p><a href='", MakeSecureURL("/Logout.pl"), "'>Log out"; if (defined($Session)) diff --git a/testbot/lib/WineTestBot/Config.pm b/testbot/lib/WineTestBot/Config.pm index 8388a0ed..b1024b5c 100644 --- a/testbot/lib/WineTestBot/Config.pm +++ b/testbot/lib/WineTestBot/Config.pm @@ -35,8 +35,8 @@ use vars qw (@ISA @EXPORT @EXPORT_OK $UseSSL $LogDir $DataDir $BinDir $ProjectName $PatchesMailingList $LDAPServer $LDAPBindDN $LDAPSearchBase $LDAPSearchFilter $LDAPRealNameAttribute $LDAPEMailAttribute $AgentPort $Tunnel - $TunnelDefaults $JobPurgeDays $JobArchiveDays $WebHostName - $RegistrationQ $RegistrationARE); + $TunnelDefaults $PrettyHostNames $JobPurgeDays $JobArchiveDays + $WebHostName $RegistrationQ $RegistrationARE);
require Exporter; @ISA = qw(Exporter); @@ -49,8 +49,8 @@ require Exporter; $TagPrefix $ProjectName $PatchesMailingList $LDAPServer $LDAPBindDN $LDAPSearchBase $LDAPSearchFilter $LDAPRealNameAttribute $LDAPEMailAttribute $AgentPort $Tunnel - $TunnelDefaults $JobPurgeDays $JobArchiveDays $WebHostName - $RegistrationQ $RegistrationARE); + $TunnelDefaults $PrettyHostNames $JobPurgeDays $JobArchiveDays + $WebHostName $RegistrationQ $RegistrationARE); @EXPORT_OK = qw($DbDataSource $DbUsername $DbPassword);
if ($::RootDir !~ m=^/=) diff --git a/testbot/lib/WineTestBot/ConfigLocalTemplate.pl b/testbot/lib/WineTestBot/ConfigLocalTemplate.pl index 6ac6b44c..c7514179 100644 --- a/testbot/lib/WineTestBot/ConfigLocalTemplate.pl +++ b/testbot/lib/WineTestBot/ConfigLocalTemplate.pl @@ -105,5 +105,9 @@ $WineTestBot::Config::Tunnel = undef; # - local_username $WineTestBot::Config::TunnelDefaults = undef;
+# If set this remaps the hostnames returned by $VM->GetHost() into more +# user friendly hostnames. For instance: +# "gateway:port1" => "vm1" +$WineTestBot::Config::PrettyHostNames = undef;
1; diff --git a/testbot/lib/WineTestBot/RecordGroups.pm b/testbot/lib/WineTestBot/RecordGroups.pm index b33d16eb..fc1c2528 100644 --- a/testbot/lib/WineTestBot/RecordGroups.pm +++ b/testbot/lib/WineTestBot/RecordGroups.pm @@ -67,7 +67,7 @@ use vars qw (@ISA @EXPORT @PropertyDescriptors);
require Exporter; @ISA = qw(WineTestBot::WineTestBotCollection Exporter); -@EXPORT = qw(&CreateRecordGroups &SaveRecord); +@EXPORT = qw(&CreateRecordGroups &CompareRecordGroups &SaveRecord);
BEGIN @@ -93,6 +93,16 @@ sub CreateRecordGroups(;$) @PropertyDescriptors, $ScopeObject); }
+sub CompareRecordGroups($$) +{ + my ($a, $b) = @_; + + # The Id will wrap eventually so sort by Timestamp + # and only use the Id to break ties. + return $a->Timestamp <=> $b->Timestamp || + $a->Id <=> $b->Id; +} + =pod =over 12
diff --git a/testbot/lib/WineTestBot/Records.pm b/testbot/lib/WineTestBot/Records.pm index dd0fec97..ac1c0faf 100644 --- a/testbot/lib/WineTestBot/Records.pm +++ b/testbot/lib/WineTestBot/Records.pm @@ -67,7 +67,7 @@ use ObjectModel::EnumPropertyDescriptor; use ObjectModel::PropertyDescriptor; use WineTestBot::WineTestBotObjects;
-use vars qw (@ISA @EXPORT @PropertyDescriptors); +use vars qw (@ISA @EXPORT @PropertyDescriptors @FlatPropertyDescriptors);
require Exporter; @ISA = qw(WineTestBot::WineTestBotCollection Exporter); @@ -81,6 +81,10 @@ BEGIN CreateBasicPropertyDescriptor("Name", "Name", 1, 1, "A", 96), CreateBasicPropertyDescriptor("Value", "Value", !1, !1, "A", 64), ); + @FlatPropertyDescriptors = ( + CreateBasicPropertyDescriptor("RecordGroupId", "Record group id", 1, 1, "S", 6), + @PropertyDescriptors + ); }
sub CreateItem($) @@ -90,12 +94,26 @@ sub CreateItem($) return WineTestBot::Record->new($self); }
+=pod +=over 12 + +=item C<CreateRecords()> + +Creates a collection containing the records of the specified RecordGroup. In +this case the Record objects have no RecordGroupId property. + +If no RecordGroup is specified all the table records are returned and the +Record objects have a RecordGroupId property. + +=back +=cut + sub CreateRecords(;$$) { my ($ScopeObject, $RecordGroup) = @_; return WineTestBot::Records->new("Records", "Records", "Record", - @PropertyDescriptors, $ScopeObject, - $RecordGroup); + $RecordGroup ? @PropertyDescriptors : @FlatPropertyDescriptors, + $ScopeObject, $RecordGroup); }
=pod diff --git a/testbot/web/Activity.pl b/testbot/web/Activity.pl new file mode 100644 index 00000000..677272f4 --- /dev/null +++ b/testbot/web/Activity.pl @@ -0,0 +1,249 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Shows the VM activity +# +# Copyright 2017 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +package ActivityPage; + +use POSIX qw(strftime); +use URI::Escape; + +use ObjectModel::CGI::Page; +use WineTestBot::Config; +use WineTestBot::Activity; +use WineTestBot::Log; +use WineTestBot::VMs; + +@ActivityPage::ISA = qw(ObjectModel::CGI::Page); + +sub _initialize($$$) +{ + my ($self, $Request, $RequiredRole) = @_; + + $self->{start} = Time(); + $self->SUPER::_initialize($Request, $RequiredRole); +} + +sub GeneratePage($) +{ + my ($self) = @_; + + $self->{Request}->headers_out->add("Refresh", "60"); + $self->SUPER::GeneratePage(); +} + +sub _GetHtmlTime($) +{ + my ($Timestamp) = @_; + return "<noscript><div>", + strftime("%d %H:%M:%S", localtime($Timestamp)), "</div></noscript>\n" . + "<script type='text/javascript'><!--\n" . + "ShowDateTime($Timestamp);\n" . + "//--></script>"; +} + +sub _GetHtmlDuration($) +{ + my ($Secs) = @_; + + return "" if ($Secs < 2); + my $Mins = int($Secs / 60); + my $Hours = int($Mins / 60); + + my @Parts; + push @Parts, "${Hours}h" if ($Hours); + push @Parts, "${Mins}m" if ($Mins %= 60); + push @Parts, "${Secs}s" if ($Secs %= 60); + return "<span class='RecordDuration'>". join(" ", @Parts) ."</span>"; +} + +sub _CompareVMs() +{ + my ($aHost, $bHost) = ($a->GetHost(), $b->GetHost()); + if ($PrettyHostNames) + { + $aHost = $PrettyHostNames->{$aHost} || $aHost; + $bHost = $PrettyHostNames->{$bHost} || $bHost; + } + return $aHost cmp $bHost || $a->Name cmp $b->Name; +} + +sub GenerateBody($) +{ + my ($self) = @_; + + print "<h1>${ProjectName} Test Bot activity</h1>\n"; + print "<div class='Content'>\n"; + + print <<"EOF"; +<script type='text/javascript'><!--\ +function Pad2(n) +{ + return n < 10 ? '0' + n : n; +} +function ShowDateTime(Sec1970) +{ + var Dt = new Date(Sec1970 * 1000); + document.write(Pad2(Dt.getDate()) + ' ' + Pad2(Dt.getHours()) + ':' + + Pad2(Dt.getMinutes()) + ':' + Pad2(Dt.getSeconds())); +} +//--></script> +EOF + + ### Get the sorted VMs list + + my $VMs = CreateVMs(); + $VMs->FilterEnabledRole(); + my @SortedVMs = sort _CompareVMs @{$VMs->GetItems()}; + + ### Generate the table header : one column per VM + + print "<div class='CollectionBlock'><table>\n"; + print "<thead><tr><th class='Record'>Time</th>\n"; + print "<th class='Record'><a class='title' title='Runnable / queued task count before scheduling'>Tasks</a></th>\n"; + foreach my $VM (@SortedVMs) + { + my $Host = $VM->GetHost(); + if ($PrettyHostNames and defined $PrettyHostNames->{$Host}) + { + $Host = $PrettyHostNames->{$Host}; + } + $Host = " on $Host" if ($Host ne ""); + print "<th class='Record'>", $VM->Name, "$Host</th>\n"; + } + print "</tr></thead>\n"; + + ### Generate the HTML table with the newest record first + + print "<tbody>\n"; + my ($Activity, $_Counters) = GetActivity($VMs); + foreach my $GroupNo (sort { $b <=> $a } keys %$Activity) + { + my $Group = $Activity->{$GroupNo}; + next if (!$Group->{statusvms}); + + print "<tr><td>", _GetHtmlTime($Group->{start}), "</td>"; + if ($Group->{runnable} or $Group->{queued}) + { + print "<td class='Record'>", ($Group->{runnable} || 0), " / ", ($Group->{queued} || 0), "</td>"; + } + else + { + print "<td class='Record'> </td>"; + } + + foreach my $Col (0..@SortedVMs-1) + { + my $VM = $SortedVMs[$Col]; + my $VMStatus = $Group->{statusvms}->{$VM->Name}; + next if ($VMStatus->{merged}); + + # Add borders to separate VM hosts + print "<td class='Record Record-$VMStatus->{status}"; + my $Host = $VM->GetHost(); + print " Record-left" if ($Col > 0 and $SortedVMs[$Col-1]->GetHost() ne $Host); + print " Record-right" if ($Col+1 < @SortedVMs and $SortedVMs[$Col+1]->GetHost() ne $Host); + print "'"; + print " rowspan='$VMStatus->{rows}'" if ($VMStatus->{rows} > 1); + print ">"; + + my $Label; + if ($VMStatus->{task}) + { + $Label = "<span class='RecordJob'>". $VMStatus->{job}->Id .":</span>"; + if ($VMStatus->{step}->Type eq "build") + { + $Label .= " Build"; + } + elsif ($VMStatus->{step}->Type eq "reconfig") + { + $Label .= " Reconfig"; + } + elsif ($VMStatus->{step}->Type eq "suite") + { + $Label .= " WineTest"; + } + else + { + $Label .= " ". $VMStatus->{step}->FileName; + if ($VMStatus->{task}->CmdLineArg =~ /^\w+$/ and + $Label =~ s/_(?:cross)?test.exe$//) + { + $Label .= ":". $VMStatus->{task}->CmdLineArg; + } + } + $Label = "<a href='/JobDetails.pl?Key=". $VMStatus->{job}->Id ."#k". ($VMStatus->{step}->No * 100 + $VMStatus->{task}->No) ."'>$Label</a>"; + } + elsif ($VMStatus->{status} eq "dirty") + { + $Label = $VMStatus->{details} || $VMStatus->{status}; + } + else + { + $Label = $VMStatus->{status}; + } + if ($VMStatus->{host} and $VMStatus->{host} ne $VM->GetHost()) + { + my $Host = $VMStatus->{host}; + # Here we keep the original hostname if the pretty one is empty + $Host = $PrettyHostNames->{$Host} || $Host if ($PrettyHostNames); + $Label = "<span class='RecordHost'>(on $Host)</span><br>$Label"; + } + print "$Label ", _GetHtmlDuration($VMStatus->{end} - $VMStatus->{start}); + print "</td>\n"; + } + print "</tr>\n"; + } + + ### Generate the table footer + + print "</tbody></table></div>\n"; +} + +sub GenerateFooter($) +{ + my ($self) = @_; + print "<p></p><div class='CollectionBlock'><table>\n"; + print "<thead><tr><th class='Record'>Legend</th></tr></thead>\n"; + print "<tbody><tr><td class='Record'>\n"; + + print "<p>The VM typically goes through these states: <span class='Record-off'>off</span>,<br>\n"; + print "<span class='Record-reverting'>reverting</span> to the proper test configuration,<br>\n"; + print "<span class='Record-sleeping'>sleeping</span> until the server can connect to it,<br>\n"; + print "<span class='Record-running'>running</span> a task (in which case it links to it),<br>\n"; + print "<span class='Record-dirty'>dirty</span> while the server is powering off the VM after a task or while it assesses its state on startup.</p>\n"; + + print "<p>If no time is indicated then the VM remained in that state for less than 2 seconds. The tasks column indicates the number of runnable / queued tasks before that scheduling round.</p>\n"; + + print "<p>The VM could also be <span class='Record-offline'>offline</span> due to a temporary issue,<br>\n"; + print "or until the administrator can look at it for <span class='Record-maintenance'>maintenance</span>,<br>\n"; + print "or <span class='Record-retired'>retired</span> prior to a possible<br>\n"; + print "<span class='Record-deleted'>deletion</span>.</p>\n"; + + print "</td></tr></tbody>\n"; + print "</tbody></table></div>\n"; + print "<p class='GeneralFooterText'>Generated in ", Elapsed($self->{start}), " s</p>\n"; +} + +package main; + +my $Request = shift; + +my $ActivityPage = ActivityPage->new($Request, "wine-devel"); +$ActivityPage->GeneratePage(); diff --git a/testbot/web/WineTestBot.css b/testbot/web/WineTestBot.css index 5d25c42f..bb80e7f2 100644 --- a/testbot/web/WineTestBot.css +++ b/testbot/web/WineTestBot.css @@ -282,6 +282,14 @@ h2 margin-left: 4px; }
+.GeneralFooterText +{ + text-align: right; + font-size: smaller; + font-style: italic; +} + + .Screenshot { display: block; @@ -330,3 +338,34 @@ pre .testfail { color: red; } .boterror { color: #e55600; } .canceled { color: black; } + +a.title { color:white; text-decoration: none; } + +th.Record { text-align: center; } +td.Record { text-align: center; } +.RecordHost { font-size: smaller; } +.RecordJob { font-size: smaller; } +.RecordDuration { } + +.Record-start { background-color: white; } +.Record-off { color: #c0c0c0; } +.Record-unknown { background-color: #c0c0c0; } +.Record-offline { background-color: red; } + +/* Greens for preparing VMs */ +.Record-reverting { background-color: #70ff54; } +.Record-sleeping { background-color: #a3ff7a; } +.Record-idle { background-color: #ccff99; } + +/* Yellows for running VMs */ +.Record-running { background-color: #ffcc00; } +.Record-dirty { background-color: #ffe633; } + +/* Blues for disabled VMs */ +.Record-maintenance { background-color: #adccef; } +.Record-retired { background-color: #72a7e4; } +.Record-deleted { background-color: #246bbc; } + +/* Special borders */ +.Record.Record-left { border-left: thin solid #601919; } +.Record.Record-right { border-right: thin solid #601919; }