Migrating scheduled tasks from 2008 to 2012+


Today I was busy with a customer who needed to migrate some Scheduled Tasks from his Windows Server 2008 to his new Windows Server 2012 R2 machine.

Slight issue, there were 139!! Scheduled Tasks [generating reports and sending them to various sources]. There’s no way I was going to do this manually, so let’s start up my favourite tool for the job: PowerShell!

I immediately ran into the first issue: There are no PowerShell cmdlets for Windows 2008 concerning Scheduled Tasks, so you’re limited to the built in command SchTasks, however this can be a troublesome parsing etc. .

Bring on the magical solution to all of your problems: SEARCHING! I’m sure someone else has run into this problem before I did and I was proven right.

A great set of functions to obtain Scheduled Task info from Windows Server 2003/2008 machines!

Because Windows Server 2012 and up does have these cmdlets, surely I could simply import the results and I’m done right? Wrong….

Unfortunately the customer had multiple Scheduled Tasks with the same name, but with different actions linked to it, along with the fact that while some Tasks were not enabled, they did need to be copied over to make sure they could re-enable them at a later date if needed.

Time to start PowerShell Studio [or ISE]!

 The Solution

First of all I needed to make a slight change on the Get-ScheduledTasks function I had downloaded.

When running the code, I ran into an error on line 93, which didn’t recognize the [ordered] type adapter, as this is from PowerShell 3.0 and up [which doesn’t come default on a Windows 2008 server]. So a quick change is in order [line 93]:

1
$Props = [Ordered]@{

Should be

1
$Props = @{

Simple as that!

Now I can export this result to a .CSV file and import this on my workstation to play around with the data and get what I want.

1
Get-ScheduledTasks | Export-Csv -NoTypeInformation -Path ScheduledTasks.csv

First we will need to import the .CSV file into my workstation:

1
$tasks = Import-Csv .\ScheduledTasks.csv

Now in order to see what data I have available I’ll just check the properties of one of the tasks defined:

1
$tasks[0] | Get-Member

This calls the first entry of the $tasks variable [PowerShell starts counting from 0 up] and displays it’s Members [Properties, Methods, etc.]. It shows me that I have various properties which I need in order to create a new Scheduled Task, including the entire XML code as it was configured on the Windows 2008 Server.

Perfect!

Using the XML entry I can simply register a new Scheduled Task with minimal configuration!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$Path = 'Contoso'
$user = 'Contoso\reporting'
$pass = 'Very$ecureP@ssw0rd'

foreach ($task in $tasks)
{

	$name = $task.taskname
	[string]$xml = $task.xml

	Register-ScheduledTask -TaskName $name -User $user -Password $pass -TaskPath $path -Xml $xml

}

This will create the new Scheduled Tasks in a subfolder called Contoso and let’s the tasks be Run As the specified User. Do note that in order to use the XML property, it needs to read the XML file as a single string, not as an XML file. Otherwise you would have used:

1
[xml]$xml = $task.xml

As mentioned, currently disabled tasks need to be migrated, but also configured as disabled tasks. Because all the properties have been exported over, this isn’t a big issue.

Just add the following line within your foreach loop:

1
2
3
4
if (!($task.enabled))
{
	Disable-ScheduledTask -TaskName $name
}

TADA!

Unfortunately I had noticed that when creating the Scheduled Tasks on my test machine, that the amount of tasks created wasn’t identical to the amount of tasks I had in my $tasks variable.

The cause was that Windows Server 2012 no longer allows for duplicate Task Names, which means that you will get an error when importing that says

1
Register-ScheduledTask : Cannot create a file when that file already exists.

This meant that I needed to look into WHAT the task was doing and change the TaskName property accordingly. I had asked the customer the syntax used [custom made application] and he provided me with the following information:

If it has an Action parameter of -D, the report will be sent through email. If it has an Action parameter of -p, a PDF report will be generated instead.

Ok, great! Depending on the Action parameter, I could check whether or not the Scheduled Task was to create a Mail or PDF. My plan was to simply check the Action parameter and add “Mail - " or “PDF  -” to the Task Name

1
2
3
4
5
6
7
8
if ($task.Action -like '* -D *')
{
	$name = 'Mail - ' + $task.taskname
}
else
{
	$name = 'PDF - ' + $task.taskname
}

Unfortunately the customer had forgotten to mention that this still gave me 3 issues:

  1. There were reports which are neither Mail nor PDF’s
  2. Some Scheduled Tasks are all Mail reports with the same Task Name, but each have different recipients.
  3. Some Scheduled Tasks are Mail reports with the same Task Name, same recipient, but generates the report based on another template.

So I needed to check the Action parameter to see exactly what was in it:

1
Z:\bin\Reporting.exe EMQ -D 142 Z:\archive\reports\report1.pdf Z:\archive\rpt\79.qr2 temp@contoso.com "Report number 1"

Ok, so I can see the email address clearly and the title can be found at the end between quotes.

I know that the report title is the bit between quotes, so I can simple split the string of text at the quotes and find the first block of text after the first quote

1
$Subject = ($task.Action.split('"')[1])

If the Subject happened to be empty [or a single word, hence missing the quotes], I would keep the Subject the same as the current Task Name

1
2
3
4
if ([string]::IsNullOrEmpty($Subject))
{
	$Subject = $($task.TaskName)
}

The biggest problem here was that the report title sometimes contained invalid characters, such as >,  +, :, / or %, which would completely screw up the rest of my code when Registering the Scheduled Task.

So a little clean up was required.

I’m no star with Regular Expressions, so I used a cheat sheet 🙂 Another great site in order to test out Regular Expressions can be found here.

1
$Subject = $($Subject) -replace ">|\/|%|\+|:","-"

Now in the Action parameter, the email address was also defined, but I needed to extract this first in order to use it later on to create a new Task Name.

This produced the following code to get a Task’s email address:

1
2
$regex = '(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})'
$Address = ((Select-String -InputObject $task.Action -Pattern $regex -AllMatches).Matches).Value

AWESOME 🙂

The Code

All in all, it took a while, but I’ve learnt quite a bit again and I hope you will too. Here’s the final code used to make sure it’s as clean and correct as possible

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<#
	.NOTES
	===========================================================================
	 Created with: 	SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.95
	 Created on:   	11/11/2015 16:15
	 Created by:   	Robert Prüst
	 Organization: 	powershellpr0mpt.com
	 Filename:     	Import-ScheduledTasks.ps1
	===========================================================================
	.DESCRIPTION
		A description of the file.
#>


Import-Module ScheduledTasks
$tasks = Import-Csv .\ScheduledTasks.csv
$path = 'Contoso'
$user = 'Contoso\reporting'
$pass = 'Very$ecureP@ssw0rd'

foreach ($task in $tasks)
{
	if ($task.Action -like '* -D *')
	{
		$regex = '(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})'
		$ActionType = 'Mail'
		$Subject = ($task.Action.split('"')[1])
		if ([string]::IsNullOrEmpty($Subject))
		{
			$Subject = $($task.TaskName)
		}
		$Address = ((Select-String -InputObject $task.Action -Pattern $regex -AllMatches).Matches).Value

		# Clean up Subject to remove illegal characters like > , % and /
		$Subject = $Subject -replace ">|\/|%|\+|:","-"

	} elseif ($task.Action -like '* -p *') {
		$ActionType = 'PDF'

	} else {
		$ActionType = 'Other'
		$Address = ((Select-String -InputObject $task.Action -Pattern $regex -AllMatches).Matches).Value
	}

	if ($ActionType -eq 'Mail')
	{
		$name = 'Mail - ' + $Subject + ' - ' + $address
	} elseif ($ActionType -eq 'PDF') {
		$name = 'PDF - ' + $($task.taskname)
	}
	else
	{
		if ($Address)
		{
			$name = 'Mail - ' + $($task.taskname) + ' - ' + $address
		}
		Else
		{
			$name = $($task.taskname)
		}
	}

	[string]$xml = $task.xml
	Register-ScheduledTask -TaskName $name -User $user -Password $pass -TaskPath $path -Xml $xml

	if (!($task.enabled))
	{
		Disable-ScheduledTask -TaskName $name
	}

}

Happy Scripting! 🙂