With the release of the {CWoC} Site Builder version 11 [1], comes the need for a new trending report, in order to record the count of inactive computers over time. This document contains everything you need to create the report and understand what it does, and why.
Content:
Summary:
The SQL code provided here (and in a Connect download that will be kept up to date [2]) creates 3 tables:
- TREND_InactiveComputerCounts
- TREND_InactiveComputer_Current
- TREND_InactiveComputer_Previous
And a stored procedure:
The procedure can be simply called with:
exec spTrendInactiveComputers
in which case the tables will be populated if the last record was done more than 23 hours prior the query execution, or you can invoke the procedure using the force mode, that will cause data to be stored regardless of the previous execution time:
exec spTrendInactiveComputers @force = 1
This is useful if you have missed a schedule, or if you want to kick start the data gathering process.
So you can now trend inactive computers on your SMP by adding the above code in a report and scheduling it to run daily (either via a Task or Notification Policy.
Back to top
Background:
As you have probably have seen from my various "Patch trending" related posts (download, articles or blog entries) adding trending is a simple and important part of managing Patch Management Compliance for computer estate of varying size.
Quickly deploying patches to 30,000 computers world wide is not an easy task, especially when the target is to have 95% of the computers compliant at 95% of above (I call this the 95-95 rule), and when this ambitious target is not met we need to be able to explain why this is so.
So we need to know how many computers are inactive (off, or not reporting data) in over time, in order to factor those off-time in with the compliance data: given we have new updates to install every 4 to 5 weeks, having computers off during that time is going to make it harder to meet the 95-95 goal.
Back to top
Selecting metrics:
Now that we have a target, we need to make sure we select the right metric to monitor the estate, and flag any problems that could be unrelated to normal time off.
I started with 2 key metrics: count of computers inactive for more than 7 days, and count of computers inactive for more than 17 days.
I have selected 7 days because it ensure we do not capture short time-off: for example with a 5 days value you would catch when someone turns off their computer on a Friday night and returns to work on the following Thursday. And it isn't too long, so we still get to see those period of inactivity.
The upper threshold is set to 17 days, so we catch any computers that have been inactive for more than a 2 weeks holiday. This give us a bracket (7 to 17 days) to calculate the count of computers out of office for 1 to 2 weeks.
I have also added to element that I found interesting: computers added or removed from the 7 days+ pool. This will make for the most interesting part of the implementation below :D.
So here is the summary of those metrics to gather:
Metric name | Description |
---|
Managed computers | Count of managed computers in the SMP database. |
7 days | Count of computers inactive for more than 7 days |
17 days | Count of computers inactive for more than 17 days |
7 days ++ | Count of computers added to the "7 days" count. These are computers that were not inactive in the previous record (t -1) |
7 days -- | Count of computers removed from the "7 days" count. These are computers that were inactive at (t -1) and that are not currently inactive i.e. they are back to Active! |
Back to top
Implementation:
With some metrics selected making sure we get accurate data came next. So I started with the SQL query that brought in the questions about inactive computers: looking at the Evt_AeX_SWD_Execution and Evt_NS_Event_History tables I could see how many computers had reported data in the last 24 hours, or n days.
Joining in the Evt_NS_Client_Config_Request I had the same result set - so the computers requesting policies were also sending data - and the opposite pointed to computers being inactive from an agent standpoint.
Finally I checked the result against the data provided by running spGetComputersToPurge and it came clear that the data matched - so I decided to rely on this procedure content (I extracted the SQL) to list inactive computers for more than 7 and 17 days.
This is done with the following code:
set @inactive_1 = (
select count(distinct(c.Guid))
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 7
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
)
set @inactive_2 = (
select count(distinct(c.Guid))
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 17
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
)
But getting a count of inactive computers for 7 or 17 days is not sufficient to understand what is happening in the environment. If on a given day 200 computers are added to the inactive count and 195 are removed then our records will only show a delta of 5 computers. We'll have no way of calculating the churn rate - which is very important in relation to Patch Management.
Populating the "7 days ++" and "7-days --" metrics we need to compare the inactive computers dataset between the current and previous recording. As such we'll create 2 tables to store the current and previous computer guids:
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Current')
begin
CREATE TABLE [TREND_InactiveComputer_Current] (guid uniqueidentifier not null, _exec_time datetime not null)
CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Current] ON [dbo].[TREND_InactiveComputer_Current]
(
[Guid] ASC
)
end
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Previous')
begin
CREATE TABLE [TREND_InactiveComputer_Previous] (guid uniqueidentifier not null, _exec_time datetime not null)
CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Previous] ON [dbo].[TREND_InactiveComputer_Previous]
(
[Guid] ASC
)
end
We populate the 2 tables in this manner:
truncate table TREND_InactiveComputer_Previous
insert TREND_InactiveComputer_Previous (guid, _exec_time)
select * from TREND_InactiveComputer_Current
-- STAGE 2: Insert current data in the current table
truncate table TREND_InactiveComputer_Current
insert TREND_InactiveComputer_Current (guid, _exec_time)
select distinct(c.Guid) as 'Guid', getdate()
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 7
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
And then we calculate the added and removed computer counts:
declare @added as int, @removed as int
-- Added in c
set @added = (
select count(*)
from TREND_InactiveComputer_Current c
full join TREND_InactiveComputer_Previous p
on p.guid = c.guid
where p.guid is null
)
-- Removed in c
set @removed = (
select count(*)
from TREND_InactiveComputer_Current c
full join TREND_InactiveComputer_Previous p
on p.guid = c.guid
where c.guid is null
)
We also need a trending table to store the daily statistics for later use. It is defined here:
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputerCounts')
begin
create table TREND_InactiveComputerCounts (
[_exec_id] int not null,
[timestamp] datetime not null,
[Managed machines] int not null,
[Inactive computers (7 days)] int not null,
[New Inactive computers] int not null,
[New Active computers] int not null,
[Inactive computers (17 days)] int not null
)
end
And we populate in this manner:
declare @execid as int
set @execid = (select isnull(max(_exec_id), 0) from TREND_InactiveComputerCounts) + 1
insert TREND_InactiveComputerCounts (_exec_id, timestamp, [Managed machines], [inactive computers (7 days)], [New Inactive Computers], [New Active Computers], [Inactive Computers (17 days)])
values (@execid, getdate(), @managed, @inactive_1, @added, @removed, @inactive_2)
To wrap up the different task in a self contained process run the code in this manner:
if (current table is not empty) {
truncate previous table
insert current data into previous
truncate current table
insert query results into current
count added
count removed
count managed machines
count inactive 7-days
count inactive 17-days
insert data into trending table
} else {
insert query results into current
}
Back to top
And here is the full code for the procedure spTrendInactiveComputers:
create procedure spTrendInactiveComputers
@force as int = 0
as
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputerCounts')
begin
create table TREND_InactiveComputerCounts (
[_exec_id] int not null,
[timestamp] datetime not null,
[Managed machines] int not null,
[Inactive computers (7 days)] int not null,
[New Inactive computers] int not null,
[New Active computers] int not null,
[Inactive computers (17 days)] int not null
)
end
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Current')
begin
CREATE TABLE [TREND_InactiveComputer_Current] (guid uniqueidentifier not null, _exec_time datetime not null)
CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Current] ON [dbo].[TREND_InactiveComputer_Current]
(
[Guid] ASC
)
end
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Previous')
begin
CREATE TABLE [TREND_InactiveComputer_Previous] (guid uniqueidentifier not null, _exec_time datetime not null)
CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Previous] ON [dbo].[TREND_InactiveComputer_Previous]
(
[Guid] ASC
)
end
if ((select MAX(_exec_time) from TREND_InactiveComputer_Current where _exec_time > dateadd(hour, -23, getdate())) is null) or (@force = 1)
begin
-- STAGE 1: If we have current data, save it in the _previous table
if (select count (*) from TREND_InactiveComputer_Current) > 0
begin
truncate table TREND_InactiveComputer_Previous
insert TREND_InactiveComputer_Previous (guid, _exec_time)
select * from TREND_InactiveComputer_Current
-- STAGE 2: Insert current data in the current table
truncate table TREND_InactiveComputer_Current
insert TREND_InactiveComputer_Current (guid, _exec_time)
select distinct(c.Guid) as 'Guid', getdate()
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 7
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
--STAGE 3: Extract the add/drop counts and insert data in the trending table
declare @added as int, @removed as int
-- Added in c
set @added = (
select count(*)
from TREND_InactiveComputer_Current c
full join TREND_InactiveComputer_Previous p
on p.guid = c.guid
where p.guid is null
)
-- Removed in c
set @removed = (
select count(*)
from TREND_InactiveComputer_Current c
full join TREND_InactiveComputer_Previous p
on p.guid = c.guid
where c.guid is null
)
declare @managed as int, @inactive_1 as int, @inactive_2 as int
set @managed = (select count(distinct(Guid)) from RM_ResourceComputer where IsManaged = 1)
set @inactive_1 = (
select count(distinct(c.Guid))
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 7
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
)
set @inactive_2 = (
select count(distinct(c.Guid))
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 17
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
)
declare @execid as int
set @execid = (select isnull(max(_exec_id), 0) from TREND_InactiveComputerCounts) + 1
insert TREND_InactiveComputerCounts (_exec_id, timestamp, [Managed machines], [inactive computers (7 days)], [New Inactive Computers], [New Active Computers], [Inactive Computers (17 days)])
values (@execid, getdate(), @managed, @inactive_1, @added, @removed, @inactive_2)
end
else
begin
truncate table TREND_InactiveComputer_Current
insert TREND_InactiveComputer_Current (guid, _exec_time)
select distinct(c.Guid) as 'Guid', getdate()
from RM_ResourceComputer c
INNER JOIN
(
select [ResourceGuid]
from dbo.ResourceUpdateSummary
where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506'
group by [ResourceGuid]
having max([ModifiedDate]) < GETDATE() - 7
) as dt
ON c.Guid = dt.ResourceGuid
where c.IsManaged = 1
end
end
select * from TREND_InactiveComputerCounts order by _exec_id desc
Back to top
References
[1]{CWoc} Patch Trending SiteBuilder
[2]{CWoC} Patch trending stored procedures
Back to top