Friday, June 13, 2014

DSC Resources: KevMar_TcpPrinter, KevMar_WindowsUpdate

I put together a set of Desired State Configuration Resources into a module. I was looking for a good project and I did not see any other resources that managed printers or Windows Updates yet.

KevMar_TcpPrinter

This is a printer management resource that will also create printer ports as needed. Most of the settings are optional as long as you specify the name, driver, and ip address of the printer.

At the moment, the driver already needs to be installed on the target node. Driver management is one of the next things for me to look at after I do some more testing.

KevMar_WindowsUpdates

The Windows Updates module is a composite module that wraps together several registry resources. Each of those properties maps to one or more registry keys that relates to automatic updates

I have all the code up on https://github.com/kmarquette/Powershell with some example configurations.


Why can't things just work?

I love to learn new things. I tend to dive in and just run with it. I just put together a new DSC resource for managing printers. Everything felt very solid as I was testing and things were just working. Running the Get,Set,and Test TargetResource commands by hand felt solid. Then I created my first config and pushed it with a Start-DscConfiguration. I ran into one small little issue that turned into a major road block.

How hard can it be to add a printer?

Under the hood, I was just using WMI to add printers and printer ports. I could easily update and delete existing printers. I could also create new printer ports without issue. My major road block was that my code to create a new printer would only fail when ran with Start-DscConfiguration. One little feature that is kind of critical to the whole project.

Win32_Printer.Put()

I was creating a new Win32_Printer object and using the .Put() command to save it. The error message I got was very nondescript. I used a try/catch block to report back the inner message of the exception. Access Denied is all it said.

I tried creating WMI objects different ways but I was not making any progress. I validated all the properties and tested setting more/less of them at once. I took a closer look at the .Put() command and found it had a putOptions overload. I had to track down how to create the right object because it didn't accept the raw int values. I discovered that there was a .psbase.put() command that I was not using before. Trial and error.

VBScript 

I also discovered that Powershell and VBScript use different wrappers for WMI objects. For some reason VBScript uses a .put_() command (yes, that underscore is intentional) to commit changes. I was able to use an existing VBScript in the Windows folder so I didn't have to write my own. It worked well enough with manual testing, but it also failed to work correctly when ran as a DscResource. I had hope that the different wrappers would somehow make a difference. But I was mistaken.

CIM_Printer

I would have loved to have used New-Printer but I am working with Windows 7. But that gave me an idea to look at the CIM instances. Again, a different wrapper. While the CIM_Printer class is easy to pull data out with, I don't think it was ever intended to be used for adding printers. I tried to create a new object but did not have much luck. As I explored the object, all the properties look to be read only. When trying to create a new one, it complains about the key fields missing. Yet when I tried to work that out, I didn't make any progress.

rundll32.exe

I figured there had to be some way to add a printer from the command line without using WMI. I tracked down a rundll32 command that I used in the past for other things (Rundll32 printui.dll,PrintUIEntry). I had a good feeling that it would have worked. I didn't like the required parameters as much. Mostly because it wanted more details around the driver than I was using. Something I could change, but it didn't feel as clean as I wanted this part to be. But it did give me another idea.

pInvoke

I started to look at system calls and found the AddPrinter that I was looking for. I required exactly what I wanted it to require. It was almost too good to be true. I was then looking at my next challenge. How do I pInvoke from Powershell? I found a little snip of code where they used C# to do the pInvoke from Powershell. That looked like as good of an approach as any.

C# and Powershell

I had my C# code fleshed out and needed to get it running in Powershell. I want to avoid having an external assembly so I had to figure out how to do it more inline. The Add-Type command turned out to be exactly what I was looking for. After importing my C# code, I was off an running.

It just works

I was able to drop it into my DSC resource with minimal adjustment and it worked perfectly. That last piece fell into place and my DSC resource was working wonderfully. This project started out as one of the most basic resources that I could think of and it ended up taking me on journey across many different technologies before I was done.

The end result is a TCPPrinter DSC resource that I have added to my KevMar modules.

Monday, June 02, 2014

Writing DSC Resources in C#

I wanted to mention this real quick so that I could find it later. The Powershell blog just mentioned that you can write DSC resources in C#.

http://blogs.msdn.com/b/powershell/archive/2014/05/29/wish-i-can-author-dsc-resource-in-c.aspx

Sunday, June 01, 2014

Using Desired State Configuration to Set Local Passwords

DSC as a User resource that allows you to create and configure local accounts. While you can set the local account password this way, you have to store the password in plain text to do so. So if you decide to set the administrator password this way, the mof file on the machine will contain that password in plain text. This is a perfect example of just because you can do something, it does not mean that you should.

This turned out to be more complicated than I expected. I was able to find a post by Aman Dhally that dug into the details and this was the result.


$ConfigData = @{
    AllNodes = @( 
             @{ NodeName = "*"; PSDscAllowPlainTextPassword=$true }
             @{ NodeName = "localhost"; }
    );
}


Configuration LocalPasswordConfig
{
    $secpassword = ConvertTo-SecureString "Password1" -AsPlainText -Force
    $mycreds = New-Object System.Management.Automation.PSCredential("Administrator",$secpassword)

    Node $AllNodes.NodeName
    {
        User LocalAccount{
            UserName = "Administrator"
            Password = $mycreds
        }
    }
}

If you don't want to have your password in plain text in your config files, you can pass in a credential object. But the .mof file will still have the plain text password.


Configuration LocalPasswordConfig
{
    param([PsCredential]$mycreds)

    Node $AllNodes.NodeName
    {
        User LocalAccount{
            UserName = "Administrator"
            Password = $mycreds
        }
    }
}

$cred = Get-Credential
LocalPasswordConfig -mycreds $cred –ConfigurationData $ConfigData 

It may be possible to use a certificate to solve the pain text issue, but I am still trying to get my head wrapped around it. I see what looks like a good example here. See the example script at the bottom of that page.

Change Local Account Password

I wrote a CmdLet the other day to change the local account password. It was a good exercise in working with Powershell, but all the meat of the command boiled down to 2 lines of code.

$admin = [adsi]("WinNT://$ComputerName/$AccountName, user")
$admin.psbase.invoke("SetPassword", $Password)

While I was looking for that command, I remembered this other ways to do the same thing.

net user $account $password


Thursday, May 29, 2014

Change Local Account Password CmdLet

There are lots of ways to change the password on local machine accounts. I used this as a sample project as I was exploring all the features of CmdLets. At first glance, this code is overkill for the task at hand. It is a good example of how to implement -WhatIf, -Confirm, and -Force. I have another post that shows just the 2 lines of code needed to change the local account password.


#AccountManagement is used for Varifying password changes
Add-Type -AssemblyName System.DirectoryServices.AccountManagement

function Set-AccountPassword
{
<#
.Synopsis
   Sets the password for a local machine account
.DESCRIPTION
   It will set a password on a remote machine for the specified account with the specified password
.EXAMPLE
   Set-AccountPassword -ComputerName localhost -AccountName Administrator -Password BatteryStapleHorse
.EXAMPLE
    Set-AccountPassword -Password BatteryStapleHorse -SkipVerify -Force
#>
    [CmdletBinding(DefaultParameterSetName='Default', 
                  SupportsShouldProcess, 
                  PositionalBinding=$false,
                  HelpUri = 'http://www.microsoft.com/',
                  ConfirmImpact='Medium')]
    Param
    (
        # ComputerName help description
        [Parameter(Mandatory=$false, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   Position=0)]
        [string]$ComputerName = "$env:computername",

        # AccountName help description
        [Parameter(Position=1)]
        [string]
        $AccountName = 'Administrator',

        # Password help description
        [Parameter(Position=2,
                    Mandatory=$true,
                    ParameterSetName="Default")]
        [ValidateLength(15,139)]
        [String]
        $Password,

        [Parameter()]
        [switch]$SkipVerify,

        [Parameter()]
        [switch]$Force
    )


    Begin
    {
     
    }   
    Process
    {
        Write-Verbose "Testing connection to $ComputerName before we try and change the password"
        if(Test-Connection $ComputerName -Count 1){
            Write-Verbose "$ComputerName is online"

            #Example of support for -WhatIf
            #Also used with -Confirm and ConfirmImpact options
            if ($pscmdlet.ShouldProcess("$ComputerName\$AccountName", "SetPassword"))
            {
                #Example of support for -Force, this will prompt every time unless the -force param is used
                if($Force -or $pscmdlet.ShouldContinue("Change the password for this account: $ComputerName\$AccountName","Setting Password")){
                    Write-Verbose "Using ADSI for connection to WinNT://$ComputerName/$AccountName"
                    $admin = [adsi]("WinNT://$ComputerName/$AccountName, user")

                    Write-Verbose "Invoking SetPassword on $ComputerName"
                    $admin.psbase.invoke("SetPassword", $Password)
                
                    # This will verify that the password was changed to $Password
                    # Skip Verify is an optional param
                    if(!$SkipVerify){
                        Write-Verbose "Verifing that the password changed correctly"
                        
                        $obj = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('machine',"$ComputerName")
                        if($obj.ValidateCredentials("$AccountName","$Password")){
                            Write-Verbose "Verified!"
                        }else{
                            Write-Error "Failed to verify password change"
                        }
                    }else{
                        Write-Verbose "SkipVerify=True skipping verify check"
                    } #SkipVerify
                } #ShouldContinue
            } #ShouldProcess
        } #Test-Connection
    } #Process
    End
    {
    }
}

Wednesday, May 28, 2014

Simple DSC Example

One thing I am doing to get more practice with Desired State Configuration is to create configurations for existing servers. I am grabbing the easiest things to script and working out from there. Here one I quickly put together for our terminal server boxes.


$ConfigData = @{
    AllNodes = @(
        @{
            NodeName = "*"
        },
        @{
            NodeName = "ProdServer1";
            
            Database = "database";
            DatabaseServer = "DBServer"
        },
        @{
            NodeName = "ProdServer2";
            
            Database = "database"
            DatabaseServer = "DBServer"
        },
        @{
            NodeName = "QAServer1";
            
            Database = "QAdatabase"
            DatabaseServer = "DBServer"
        }
    )
}


Configuration TerminalServer
{
   node $allnodes.NodeName {
        WindowsFeature Backup {
            Name = "Windows-Server-Backup";
       }
   }

   node $allnodes.NodeName
   {
   
   

       WindowsFeature DesktopExperience {
        Name = "Desktop-Experience";
        IncludeAllSubFeature = "True";
       } 

       WindowsFeature RDS {
        Name = "RDS-RD-Server";
        IncludeAllSubFeature = "True";
       }         
   
       
       Registry ProductdDatabase{
        Key = "HKLM:\SOFTWARE\company\product";
        ValueName = 'InitialCatalog';
        ValueData = $node.Database
       }

       Registry ProductDatabaseServer{
        Key = "HKLM:\SOFTWARE\company\product";
        ValueName = 'DataSource';
        ValueData = $node.DatabaseServer
       }

   }
} 

TerminalServer -ConfigurationData $ConfigData

Start-DscConfiguration -Wait -Verbose -Path .\TerminalServer