In this guide, I will how to image workstations with static IPs using a PowerShell form, System Center Configuration Manager, and SQL Server.

One of the biggest challenges I’ve struggled with while using SCCM is image deployment on computers that use a static IP. If your environment has only a few workstations that use a static IP, typing in and configuring individual network settings variables may not be a great solution, but it is certainly a manageable one. However, if your environment has well over 400 workstations that all use static IPs and can’t use DHCP at any point during or after the imaging process, setting individual variables is not very desirable or efficient.

Step 1 – First, you will need to create a database that contains the name, IP address, subnet mask, and gateway of each workstation in your environment that uses a static IP. The easiest way to do this is to create a comma-delimited CSV file in Microsoft Excel and import it into a database as a table. If you know your keyboard shortcuts and use a solid naming convention, this shouldn’t take too long.

Create Excel spreadsheet

Create Excel spreadsheet

Once you compose your spreadsheet, save it as a comma-delimited CSV file with a friendly name, such as “Workstations.csv,” and exit Excel.

Step 2 – Open the SQL Server Management Studio and connect to your server’s database engine. Create a new database or select an existing one that will hold your imported table. For this guide, I will be creating a new database called “WKS_SCS” and I will assign myself as the database owner. Click OK to create the database.

Create database

Create database

Step 3 – Now we are going to import the table with our workstation information. Right-click on the database you just created and navigate to “Tasks” > “Import Data.”

Import data

Import data

When the SQL Server Import and Export Wizard open, click Next and for the Data Source, choose “Flat File Source.” Next to file name, browse to and select the file that contains the workstation information, which you created earlier; leave all the defaults as is. To verify that the SQL read the CSV correctly, click on the Preview tab to see a preview of your table. Click Next, and for the Destination, choose “SQL Server Native Client 11.0.” Select or enter the server name and database you wish to import the information into and click next. You can either change the name of your table or leave it as is. I am going to change the name of my table to “dbo.NetworkConfig.” Click next to begin importing your table. Once the import completes, close the Import and Export Wizard.

Successful import

Successful import

When you refresh your database in Object Explorer, you should see your new table. Now we need to create an SQL login that has read privileges to that table so we can pull information down for our PowerShell form.

New database

New database

Step 4 – In Object Explorer, navigate to the Logins node and create a new login that has access to the database you created and can read the table you imported. For this guide, I created an account called “db.readonly” and have given it db_datareader and db_denydatawriter membership.

Read only account

Read only account

Once you create the login, you should see a new user under the Logins node, as well as the Users node for the database you are using to hold your table.

Read only account (cont.)

Read only account (cont.)

You may now exit SQL Server Management Studio.

Step 5 – Open Windows PowerShell ISE and copy and paste the following script (the script is longer than 10 lines):

#Generated Form Function
function GenerateForm {
#######################################################################
# System Center Configuration Manager - Workstation Unattend PS Script
# Form Generated By: SAPIEN Technologies PrimalForms (Community Edition) v1.0.9.0
# Created by: Alexander Pazik
########################################################################

#region Import the Assemblies
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
#endregion

#region Generated Form Objects
$form1 = New-Object System.Windows.Forms.Form
$button1 = New-Object System.Windows.Forms.Button
$textBox7 = New-Object System.Windows.Forms.TextBox
$label7 = New-Object System.Windows.Forms.Label
$textBox6 = New-Object System.Windows.Forms.TextBox
$label6 = New-Object System.Windows.Forms.Label
$textBox5 = New-Object System.Windows.Forms.TextBcollox
$label5 = New-Object System.Windows.Forms.Label
$textBox4 = New-Object System.Windows.Forms.TextBox
$label4 = New-Object System.Windows.Forms.Label
$textBox3 = New-Object System.Windows.Forms.TextBox
$label3 = New-Object System.Windows.Forms.Label
$textBox2 = New-Object System.Windows.Forms.TextBox
$label2 = New-Object System.Windows.Forms.Label
$textBox1 = New-Object System.Windows.Forms.TextBox
$label1 = New-Object System.Windows.Forms.Label
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
#endregion Generated Form Objects

#----------------------------------------------
#Generated Event Script Blocks
#----------------------------------------------
#Provide Custom Code for events specified in PrimalForms.
$button1_OnClick=
{
# Set Appropriate Values
# These values are generated based on the information that has been entered into the Workstation Unattend form.
$ComputerName = $textBox1.Text
$IP = $textBox2.Text
$SubnetMask = $textBox3.Text
$Gateway = $textBox4.Text
$DNS = $textBox5.Text
$Suffixes = $textBox6.Text
$WINS = $textBox7.Text 

# Configure the IP Address, Subnet Mask and Gateway
$NIC = Get-WmiObject win32_networkadapterconfiguration -filter "ipenabled ='true'";
$NIC.EnableStatic($IP, $SubnetMask);
$NIC.SetGateways($Gateway, 1);

# Configure the DNS Server, DNS Suffix Search Order and Disable Dynamic Registration
$NIC.SetDNSServerSearchOrder($DNS);
$NIC.SetDynamicDNSRegistration("FALSE");
([WMIClass]"Win32_NetworkAdapterConfiguration").SetDNSSuffixSearchOrder(("$Suffixes"));

# Configure the WINS Server
$NIC.SetWINSServer($WINS,"");

# Generate Task Sequence Variables Text File
# In a perfect world, we could simply just use the variables we defined earlier to set the Computer Name, IP, Subnet Mask, etc. 
# However, when the object Microsoft.SMS.TSEnvironment is initialized PS has a problem converting the variables we defined earlier
# into strings. To work around this, we need to generate a text file with all of our task sequence variables and then replace the 
# values of the task sequence variables unique to each computer with the values of the variables we defined earlier. After this is 
# done, PS will generate a new script and save it to the hard drive (C:).
$OUTPUT = {$tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
           $tsenv.Value("OSDComputerName") = "_Name"
           $tsenv.Value("OSDAdapterCount") = "1"
           $tsenv.Value("OSDAdapter0EnableDHCP") = "FALSE"
           $tsenv.Value("OSDAdapter0IPAddressList") = "_IP"
           $tsenv.Value("OSDAdapter0SubnetMask") = "_SubnetMask"
           $tsenv.Value("OSDAdapter0Gateways") = "_Gateway"
           $tsenv.Value("OSDAdapter0DNSDomain") = "YourDNSDomain"
           $tsenv.Value("OSDAdapter0DNSServerList") = "YourDNSServerList”
           $tsenv.Value("OSDAdapter0EnableDNSRegistration") = "FALSE"
           $tsenv.Value("OSDAdapter0TcpipNetbiosOptions") = "1"
           $tsenv.Value("OSDAdapter0EnableWINS") = "TRUE"
           $tsenv.Value("OSDAdapter0WINSServerList") = "YourWINSServerList"}

# Format the (C:) Drive
# This ensures the "Apply Custom TS Variables" step in the task sequence always points to the correct location.
Format-Volume -DriveLetter C -FileSystem NTFS

$PathTXT = "C:\Variables.txt"
$PathPS1 = "C:\Variables.ps1"

$OUTPUT | Out-File $PathTXT
(Get-Content $PathTXT | ForEach-Object { $_ -replace "_IP", "$IP" }) | Set-Content $PathTXT
(Get-Content $PathTXT | ForEach-Object { $_ -replace "_SubnetMask", "$SubnetMask" }) | Set-Content $PathTXT
(Get-Content $PathTXT | ForEach-Object { $_ -replace "_Gateway", "$Gateway" }) | Set-Content $PathTXT
(Get-Content $PathTXT | ForEach-Object { $_ -replace "_Name", "$ComputerName" }) | Set-Content $PathPS1

# Notify User Workstation Unattend Completed Successfully
# This will always appear, even if the "Variables.ps1" script isn't successfully generated. If you'd like to verify the script
# was generated, you can press F8 to call a CMD prompt and enter "dir C:\" without quotes to see the contents of drive (C:), 
# which should include the files "Variables.txt" and "Variables.ps1". These files will be erased when the workstation is formatted
# during the task sequence.
$O = new-object -comobject wscript.shell
$P = $O.popup("Done! You may now close the Workstation Unattend form.",0,"Information",1)
}

$handler_textBox1_TextChanged= 
{
# Set the ComputerName PS variable
$ComputerName = $textBox1.Text

# Connect to SQL Server
$SQLSERVER = "YourSQLServer"
$SQLDB = "YourSQLDatabase"
$SQLCONNECTION = "Server=$SQLSERVER; Database=$SQLDB; Integrated Security = False; UID=YourReadOnlyAcctUsername; PWD=YourReadOnlyAcctPassword"
$SQLQUERY = "select * FROM YourTable WHERE Name = '$ComputerName'"

$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $SQLCONNECTION
$connection.Open()
$command = $connection.CreateCommand()
$command.CommandText = $SQLQUERY

$result = $command.ExecuteReader()
$table = new-object "System.Data.DataTable"
$table.Load($result)

$table | Where-Object {$_.Name -like $ComputerName} | % {
$Name = $_.Name
$IP = $_.IP
$SubnetMask = $_.SubnetMask
$Gateway = $_.Gateway
$DNS = "YourPrimaryDNSServer"
$Suffixes = "YourDNSSuffixes"
$WINS = "YourPrinaryWINSServer"
}

# AutFill Workstation Unattend Form
$textBox2.Text = $IP
$textBox3.Text = $SubnetMask
$textBox4.Text = $Gateway
$textBox5.Text = $DNS
$textBox6.Text = $Suffixes
$textBox7.Text = $WINS

# Close SQL Server Connection
$connection.close()
}

$OnLoadForm_StateCorrection=
{#Correct the initial state of the form to prevent the .Net maximized form issue
	$form1.WindowState = $InitialFormWindowState
}

#----------------------------------------------
#region Generated Form Code
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 207
$System_Drawing_Size.Width = 509
$form1.ClientSize = $System_Drawing_Size
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon((Get-Command powershell).Path) 

$form1.Name = "form1"
$form1.Text = "Workstation Unattend"

$button1.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 320
$System_Drawing_Point.Y = 6
$button1.Location = $System_Drawing_Point
$button1.Name = "button1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 48
$System_Drawing_Size.Width = 181
$button1.Size = $System_Drawing_Size
$button1.TabIndex = 16
$button1.Text = "Apply"
$button1.UseVisualStyleBackColor = $True
$button1.add_Click($button1_OnClick)

$form1.Controls.Add($button1)

$textBox7.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 178
$textBox7.Location = $System_Drawing_Point
$textBox7.Name = "textBox7"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 179
$textBox7.Size = $System_Drawing_Size
$textBox7.TabIndex = 13

$form1.Controls.Add($textBox7)

$label7.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 3
$System_Drawing_Point.Y = 181
$label7.Location = $System_Drawing_Point
$label7.Name = "label7"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 19
$System_Drawing_Size.Width = 77
$label7.Size = $System_Drawing_Size
$label7.TabIndex = 12
$label7.Text = "WINS Server"

$form1.Controls.Add($label7)

$textBox6.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 150
$textBox6.Location = $System_Drawing_Point
$textBox6.Name = "textBox6"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 180
$textBox6.Size = $System_Drawing_Size
$textBox6.TabIndex = 11

$form1.Controls.Add($textBox6)

$label6.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 3
$System_Drawing_Point.Y = 153
$label6.Location = $System_Drawing_Point
$label6.Name = "label6"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 17
$System_Drawing_Size.Width = 78
$label6.Size = $System_Drawing_Size
$label6.TabIndex = 10
$label6.Text = "DNS Suffixes"

$form1.Controls.Add($label6)

$textBox5.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 121
$textBox5.Location = $System_Drawing_Point
$textBox5.Name = "textBox5"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 180
$textBox5.Size = $System_Drawing_Size
$textBox5.TabIndex = 9

$form1.Controls.Add($textBox5)

$label5.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 3
$System_Drawing_Point.Y = 124
$label5.Location = $System_Drawing_Point
$label5.Name = "label5"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 77
$label5.Size = $System_Drawing_Size
$label5.TabIndex = 8
$label5.Text = "DNS Server"

$form1.Controls.Add($label5)

$textBox4.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 92
$textBox4.Location = $System_Drawing_Point
$textBox4.Name = "textBox4"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 181
$textBox4.Size = $System_Drawing_Size
$textBox4.TabIndex = 7

$form1.Controls.Add($textBox4)

$label4.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 3
$System_Drawing_Point.Y = 95
$label4.Location = $System_Drawing_Point
$label4.Name = "label4"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 21
$System_Drawing_Size.Width = 73
$label4.Size = $System_Drawing_Size
$label4.TabIndex = 6
$label4.Text = "Gateway"

$form1.Controls.Add($label4)

$textBox3.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 63
$textBox3.Location = $System_Drawing_Point
$textBox3.Name = "textBox3"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 181
$textBox3.Size = $System_Drawing_Size
$textBox3.TabIndex = 5

$form1.Controls.Add($textBox3)

$label3.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 3
$System_Drawing_Point.Y = 66
$label3.Location = $System_Drawing_Point
$label3.Name = "label3"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 19
$System_Drawing_Size.Width = 75
$label3.Size = $System_Drawing_Size
$label3.TabIndex = 4
$label3.Text = "Subnet Mask"

$form1.Controls.Add($label3)

$textBox2.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 34
$textBox2.Location = $System_Drawing_Point
$textBox2.Name = "textBox2"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 181
$textBox2.Size = $System_Drawing_Size
$textBox2.TabIndex = 3

$form1.Controls.Add($textBox2)

$label2.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 5
$System_Drawing_Point.Y = 37
$label2.Location = $System_Drawing_Point
$label2.Name = "label2"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 16
$System_Drawing_Size.Width = 80
$label2.Size = $System_Drawing_Size
$label2.TabIndex = 2
$label2.Text = "IP Address"
$label2.add_Click($handler_label2_Click)

$form1.Controls.Add($label2)

$textBox1.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 109
$System_Drawing_Point.Y = 6
$textBox1.Location = $System_Drawing_Point
$textBox1.Name = "textBox1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 20
$System_Drawing_Size.Width = 181
$textBox1.Size = $System_Drawing_Size
$textBox1.TabIndex = 1
$textBox1.add_TextChanged($handler_textBox1_TextChanged)

$form1.Controls.Add($textBox1)

$label1.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 3
$System_Drawing_Point.Y = 9
$label1.Location = $System_Drawing_Point
$label1.Name = "label1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 17
$System_Drawing_Size.Width = 100
$label1.Size = $System_Drawing_Size
$label1.TabIndex = 0
$label1.Text = "Computer Name"

$form1.Controls.Add($label1)

#endregion Generated Form Code

# Save the initial state of the form
$InitialFormWindowState = $form1.WindowState
#Initialize the OnLoad event to correct the initial state of the form
$form1.add_Load($OnLoadForm_StateCorrection)
# Show the Form
$form1.ShowDialog()| Out-Null
}

# Call Workstation Unattend Form
GenerateForm

You will need to edit all fields that contain an entry with the suffix “Your.” You can run a search in your script to find all these entries. When you are done, save the script to a Configuration Manager-accessible network location with the name “prestart.ps1.” For this guide, I will save the script to the following location:

\\SCS-CFGMGR-MP\SWSTORE\Boot Images\WinPE_PRESTART\~WorkstationUnattend

Now we are going to create the boot image.

Step 6 – Open the System Center Configuration Manager console. Click on the Software Library tab to expand the Operating Systems node and click on the Boot Images applet. Find and open “Add Boot Image” in the Ribbon above. When the Add Boot Image wizard launches, point the wizard to the location of your boot image. When I create a new boot image, I always create a new folder, give it a logical name (i.e., WinPE 10.0 v4), and copy a vanilla boot.wim generated by the copype command. Click Next and give the boot image a name, version, and description. Click Next once more to start adding the boot image to Configuration Manager. Close the wizard once the boot image is added.

Enter boot image data

Enter boot image data

Step 7 – Right-click on the recently added boot image and select “Properties.” Click on the Drivers tab and add any necessary drivers.

Add drivers

Add drivers

Click on the Customization tab and check the box “Enable prestart command.” Copy and paste the following in the “Command line” box:

powershell.exe -noprofile -executionpolicy bypass -windowstyle hidden -file "prestart.ps1"

Check the box “Include files for the prestart command” and navigate to the folder where you saved your script. Optionally, you can choose a custom wallpaper to use with your boot image. For this guide, I am going to add a custom wallpaper. Check the box “Enable command support (testing only).”

Add customization(s)

Add customization(s)

Click on the Optional Components tab and add the following components:

Windows PowerShell (WinPE-DismCmdlets)
Windows PowerShell (WinPE-StorageWMI)
Microsoft .NET (WinPE-NetFx)
Windows PowerShell (WinPE-PowerShell)

Optional components

Optional components

Click Apply and choose Yes to update the distribution points with the boot image.

Step 8 – Navigate to the Task Sequences applet under the Operating Systems node and either create a new task sequence or select an existing one. For this guide, I am going to select an existing task sequence “(Family WKS) Windows 10 Pro x64.” Add a new “Run Command Line” task sequence step and place it before the step “Partition Disk 0 – BIOS” / “Partition Disk 0 – EUFI” but after the step “Restart in WinPE.”

Add Task Sequence step

Add Task Sequence step

Copy and paste the following in the “Command line” box:

 powershell.exe -noprofile -executionpolicy bypass -windowstyle hidden -file "C:\Variables.ps1"

Click Apply and close the task sequence. Right-click on the task sequence and select “Properties.” Make sure the task sequence is using the boot image you just created.

Select Task Sequence boot image

Select Task Sequence boot image

Step 9 – Now we need to create a new task sequence media. Select the task sequence you just edited and open “Create Task Sequence Media” in the Ribbon above. Specify whatever options are required in your environment and create the task sequence media. Do not add any variables or a prestart command. Close the wizard when the media is done being created. We are now ready to begin imaging workstations.

Step 10 – Boot into the task sequence using the media you created in the previous step. On the “Welcome to the Task Sequence Wizard” splash screen, click Next to bring up the Workstation Unattend form. Type the computer name into the textbox labeled “Computer Name.” When SQL runs a query off that name and finds a result, it will automatically populate the other fields in the form. Click Apply to apply these settings to the NIC and generate the “Variables.ps1” script.

Workstation unattended

Workstation unattended

Close the Workstation Unattend form. Select the task sequence you edited earlier and click Next. The wizard will resolve all dependencies and begin executing the task sequence. Shortly after the task sequence begins, you should see the following step:

Apply custom TS variables

Apply custom TS variables

To verify the task sequence variables were applied correctly, press F8 to open a Command Prompt and enter the following commands:

powershell.exe
$tsenv = New-Object –COM Microsoft.SMS.TSEnvironment
$Name = $tsenv.value("OSDComputerName")
$Name

If the variables applied correctly, PowerShell will echo the name of the computer specified in the Workstation Unattend form.

Subscribe to 4sysops newsletter!

Variables successfully applied

Variables successfully applied

When the workstation finishes imaging, it will have the correct name as well as the correct static network settings.

3 Comments
  1. MikeC 6 years ago

    Line 77: There is one of those annoying ‘curly quotes’ in your script in step 5.

    Thank you for sharing your work!

  2. MikeC 6 years ago

    Actually, there are a number of them:

    Line 57:     (“FALSE”)

    Should be:        (“FALSE”)

     

    • Thanks for the hint! I corrected the script now. I always wonder why curly quotes can’t be used in programming languages. I mean, wouldn’t it be easier to see where a quote starts and where it ends?

Leave a reply

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account