[PATCH 1/4] testbot: Keep a record of the status of the VMs.

Francois Gouget fgouget at codeweavers.com
Mon Dec 18 16:53:47 CST 2017


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 at 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;
+ at 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;
+ at ISA = qw(WineTestBot::WineTestBotCollection Exporter);
+ at 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;
+ at 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;
+ at ISA = qw(WineTestBot::WineTestBotCollection Exporter);
+ at 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;
 
-- 
2.15.1




More information about the wine-devel mailing list