Scroll down to the update for the fix, continue reading below for some background.
Today I thought I would write about the process of installing .Net Framework v3.5 on Windows Server 2012 when the server does not have an internet connection. This process caused me a lot of problems and I wasted a lot of time on it today. It seems many others are experiencing the same problem.
Windows Server 2012 requires that .Net Framework is enabled via as a feature rather than using the installation file method that we are used to in previous versions of Windows. For servers with an internet connection this works fine, and can be achieved in a number of ways (eg. Add roles and features wizard, PowerShell). My problems began when I started working on a server without internet access.
The official guide from Microsoft is here, and it all sounds simple enough. Doing this with the wizard displayed a warning before I started.
Selecting the Install option (as expected) caused the installation to fail:
"Installation of one or more roles, role services, or features failed.
The source files could not be found. Try installing the roles, role services, or features again in a new Add Roles and Features Wizard session, and on the Confirmation page of the wizard, click "Specify an alternate source path" to specify a valid location of the source files that are required for the installation. The location must be accessible by the computer account of the destination server"
I followed the advice in the error, and copied the .\sources\sxs folder from the installation DVD to a local folder, then selected that folder within the "Specify an alternate source path" option of the wizard.
This did not make any difference and the installation failed with the same error.
A few google searches led me to this article, which references KB3005628 as a fix for this problem. However, this fix corrects the problem when it was caused by the installation of security update 2966827 or 2966828.
Get-Hotfix | where { ($_.HotfixID -eq "KB296682" -or $_.HotfixID -eq "KB2966828") }
This PowerShell command will reveal whether or not these updates have been installed. In my case the server was not patched and so this was not the cause of the problem.
Various further google searches revealed many other users have similar issues, and I tried several other methods of installing .Net Framework v3.5, and all of them failed. Here are a few examples:
Dism /online /enable-feature /featurename:NetFX3 /all /Source:d:\sources\sxs
Dism /Online /Enable-Feature /FeatureName:NetFx3 /Source:X:\Sources\WinSXS /all /limitaccess
Install-WindowsFeature –Name NET-Framework-Features –Source D:\Sources\sxs
I also tried to enable the policy Specify settings for optional component installation and component repair.
This is located in Computer Configuration\Administrative Templates\System
I enabled the policy and enabled "Never attempt to download payload from Windows Update". This made no difference, and the installation still fails no matter how I attempt to install it.
** Update **
01 Feb 2016
Today I finally resolved this problem, below are the steps I had to take to get it working, hopefully this method can save someone some time. Note that this fix requires a Windows Server 2012 r2 machine on which the .Net 3.5 installation has been successful
First, follow the steps in this Microsoft support page. If these steps don't help, as was the case for me, try using my method.
Step 1
On a server where the installation fails, get a list of all folders in the WinSXS folder.
I used the following PowerShell command
Get-ChildItem -Path C:\Windows\WinSxS | select name | out-file D:\WinSXSfiles.txt
Then I copied WinSXSfiles.txt to a server where a previous installation had been successful.
Step2
Copy the list of folders to a (Windows 2012 r2) server where the installation has been successful. Compare this list to the contents of the WinSXS folder (on the successful server), and get a copy of all the folders that are missing on the server where the install fails.
I used the following PowerShell script:
#Get the folder contents of local WinSXS, select only the name, pass to foreach command
Get-ChildItem C:\Windows\WinSxS | select -ExpandProperty name | foreach {
#Foreach folder name, check whether it exists in the list of folders from the problem machine
if ( !(select-string -Path D:\WinSXSfiles.txt -Pattern $_) ) {
#Write any missing folder names to the console
Write-Host $_ -ForegroundColor Yellow
#Copy missing folders to a temporary folder D:\WinSXS
Copy-Item "C:\Windows\WinSxS\$_" -Destination D:\WinSXS\ -Recurse
}
}
Step 3
Copy the missing folders to the server where the installation fails
Step 4
Use Dism, with the Source option pointing at both WinSXS folders.
Dism /Online /Enable-Feature /FeatureName:NetFx3 /Source:C:\Windows\WinSXS /Source:D:\WinSXS /all /limitaccess
In my example I use 2 WinSXS folders. I didn't want to copy nearly 700 potentially unnecessary folders into the WinSXS folder on the C: drive. To get around this I created a temporary WinSXS folder (D:\WinSXS) and gave the DISM command a second /Source folder.
I found this page useful for explaining the DISM switches.
I had nearly 700 extra folders in WinSXS on my machine where the installation was successful. Looking through these I found a large number with names that started amd64_netfx
I expect that with some trial and error I could work out the exact folders that were necessary for the installation to be successful - probably some/all of those with names starting amd64_netfx.
However, to save time, my method of creating a temporary WinSXS folder works fine.
Recently I have put all my most commonly used PowerShell scripts into a Module. By bundling them together into a module, I remove the need for individual script files to be maintained and updated on all servers in the network.
Doing this has caused a few problems of its own, so here is the process I used to get it working.
The first thing I did was copy the module to a shared location that can be accessed easily by everything on the network.
Next, anything that needs to use my module needs to be made aware of it. There are 2 steps to this process.
1. Edit the environment variable “PSModulePath” to include the path to the module
2. Import the module
Both steps can be accomplished by editing the PowerShell profile. There are several PowerShell profiles; I have chosen to use the profile for All Users, All Hosts. This ensures the module will be available no matter how PowerShell is used. This is important for me because as well as running commands locally I also call commands using external applications like Nagios.
More information on PowerShell profiles can be found here:
The All Users, All Hosts profile is located here:
C:\windows\system32\WindowsPowerShell\v1.0\profile.ps1
The profile.ps1 file will not exist by default, so it is usually necessary to create it (new text file called profile.ps1)
1. Edit the environment variable “PSModulePath” to include the path to the module This done by adding the following line to the PowerShell profile:
$env:psmodulepath = $env:psmodulepath +";\\ServerShare\Folder\modules"
2. Import the module
This done by adding the following line to the PowerShell profile:
Import-Module ModuleName
Launching PowerShell will give you an error message at this point. This is because PowerShell sees the module as coming from an external source and doesn't trust it. PowerShell uses Internet Explorer's Zone Policy to see if it trusts the file, script or module you are attempting to run.
This can be manually edited through:
Internet Options >> Security tab >> Local Intranet >> Sites
Here we can add the external source as a trusted site, \\ServerShare
Doing this will only take effect for the currently logged on user, and will still cause an error when an external program attempts to run something using PowerShell - eg. Nagios
To get around this we must add the trusted site to the local machine's trusted zone policy. This can be done by creating two new registry keys.
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains\ServerShare
Name: file
Type: DWORD
Value: 1
and
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\ServerShare
Name: file
Type: DWORD
Value: 1
I have a lot of servers that require this change, so I wrote some PowerShell that will do it for me, here is the script:
$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\ServerShare"
$registryPath1 = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains\ServerShare"
$name = "file"
$value = "1"
If (!(test-path $registryPath )) {
New-Item -Path $registryPath -Force | Out-Null
New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
}
If (!(test-path $registryPath1 )) {
New-Item -Path $registryPath1 -Force | Out-Null
New-ItemProperty -Path $registryPath1 -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
}
Using these settings let me use all the commands that are exposed by my module and I can leave the execution policy set at Remote Signed.
Below are some good articles that helped me out with this process:
Running PowerShell Scripts From An UNC Path (Share)
Signing PowerShell Scripts
Can't import a module from a UNC path
Update or Add Registry Key Value with PowerShell
UPDATE - 19-10-2015
I've been using this process quite a bit, and found that it doesn't work in all circumstances. For newer versions of Internet Explorer, there is an extra step to editing the Local Intranet zones policy:
Internet Options >> Security tab >> Local Intranet >> Sites >> Advanced
When using these later versions of IE, the registry changes don't work, and the module on a UNC path still causes an error when the profile tries to import it. I can stop the error from being displayed by changing the Execution Policy to Bypass, however, I wasn't keen on implementing that as a solution across our estate. Instead I'm now using a different method.
Copy-Item \\ServerShare\Folder\modules\ModuleName -Destination D:\scripts\modules -Recurse -Force
$env:psmodulepath = $env:psmodulepath +";D:\scripts\modules"
Import-module ModuleName
This takes a copy of the module and puts it into a local folder. The module is then imported from this local folder, which removes the need for PowerShell to treat it as a remote script.
PowerShell is an excellent tool for searching through Windows event logs. I find myself using it more and more these days as it enables me to find the information I need much quicker than using the filter feature of the Event Viewer snap in.
The cmdlet to use for searching the event logs is get-eventlog. For the full help file from PowerShell, enter the following
help Get-EventLog -Full
The get-eventlog cmdlet uses the switch -LogName. This is used to specify the event log you want to search, eg System, Application etc.
To get a list of available event logs, enter
Get-EventLog -List
or
Get-EventLog -LogName *
You can then list all events from that event log with the command Get-EventLog -LogName LogName
Eg,
Get-EventLog -LogName System
This will return everything from the event log, probably hundreds or even thousands of events, so the next job is to filter for the events you are interested in.
We can examine an individual event log to get an idea of how to filter
Get-EventLog -LogName System -Newest 1
This command will return the most recent System event log
The information displayed is a subset of the complete data available for this event log. PowerShell will automatically select the columns to display so that it fits easily on the screen. To see everything, we need to pipe the output to the format-list cmdlet.
Get-EventLog -LogName System -Newest 1 | Format-List -Property *
This command returns all the properties and values for this event log
Now you can see all the familiar properties of the event, such as EventID, EntryType, Time Written etc. I can now filter my result based on one or more of these properties.
For example,
show all events where the message contains "the service entered the stopped state"
show all events where the event id equals 41 and the date is 10 Jan 2015
show all events where the EntryType is error, the source is Asp.Net or .Net runtime and the date is between 01 Jan - 10 Jan 2015
All of this is achieved by piping the results of get-eventlog to the where-object cmdlet
Get-EventLog -LogName System | Where-Object { $_.Message -like "*the service entered the stopped state*" }
In this example you can see that I have used the Message property of the event and the -like operator to match it to the text I am looking for.
Get-EventLog -LogName System | Where-Object { $_.EventID -eq 41 -and $_.TimeWritten -like "01/14/2015*" }
In this example you can see that I have used the EventID and TimeWritten properties of the event
$_.EventID -eq 41
$_.TimeWritten -like "01/14/2015*"
The -and operator links these two properties together
Get-EventLog -LogName System -After (Get-Date -Date '1/1/2015') -Before (Get-Date -Date '10/1/2015') | Where-Object { $_.EntryType -eq "Error" -and ($_.Source -like "Asp.Net*" -or $_.Source -like ".net runtime*") }
In this example I use the date property of the get-eventlog results to filter the events before passing them to the where-object cmdlet. This is done using the -before and -after switches. After that, the where-object command is used in the same way as the previous examples.
The only difference is the two $_.Source properties are surround by brackets (). This is necessary so that the -or operator applies to only those two entries. I could add more $_.Source properties inside the brackets with additional -or operators if I wanted to increase this list beyond 2.
Understanding the PowerShell operators is key to getting the results you want, this link contains useful information about operators
http://technet.microsoft.com/en-gb/library/hh847759.aspx
Finally, you can export your results using any of PowerShell's export commands. I find export-csv works well:
Get-EventLog -LogName System | Where-Object { $_.Message -like "*the service entered the stopped state*" } | Export-Csv C:\scripts\events.csv