Tuesday, October 21, 2008

Registry and the dreaded HKCU

Good to be back in the swing of things.  Last month I spent some time at DisneyWorld and had a blast.  When I came back, another hot issue on the table for this script master to solve.

I can't tell you how many times I've need to add/delete/modify a key in the HKEY Current Users hive on remote machines.  WMI does provide a registry object that you can use and it does include an HKCU hive...  However, as some of you may know, when you access that HKCU hive, it's not the actual HKCU but rather the HKCU for the account you are using to run the script.  Not very useful.


There are several ways to get in the actual HKCU but my personal method is a three step process.  We already know that there is another hive, HKEY Users, that contains all the users on that machine. And we have no problem getting into that hive remotely.  But, that hive uses SID values to identify each unique user.  My process is to identify the current user, get the SID for that user, then drill into the HKU\SID hive. 


Step One: Get the current user of a machine.

For this I use a function (as I usually do):


Function GetCurrentUser2(sstrComputer)
    On Error Resume Next
    Dim sstrKeyPath,sstrCU,sstrCUD,sstrCurrentUser,sstrCurrentUserDomain
    sHKEY_LOCAL_MACHINE = &H80000002
    Set sobjRegistry = GetObject("winmgmts:\\" & sstrComputer & "\root\default:StdRegProv")
    sstrKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon"
    sstrCU = "DefaultUserName"
    sstrCUD = "DefaultDomainName"
    sobjRegistry.GetStringValue sHKEY_LOCAL_MACHINE, sstrKeyPath, sstrCU, sstrCurrentUser
    sobjRegistry.GetStringValue sHKEY_LOCAL_MACHINE, sstrKeyPath, sstrCUD, sstrCurrentUserDomain
    GetCurrentUser2 = sstrCurrentUserDomain & "\" & sstrCurrentUser
End Function


Simply pass the computer name and it returns the user as "domain\logon".  This works fairly well unless you have some sort of security policy that is removing that value from the remote registry. You'll notice that my function is called GetCurrentUsers2.  The original function I used looked up the explorer.exe and found the owner of that process.  That worked, but not 100%.

Now that we have the current user stored as a "domain\logon" variable, we can then query the domain to get the associated SID for that user.  Again, another function.


Function GetSIDFromUser(UserName)
'Input: UserName as domain\logon
'Output: SID
  Dim DomainName, Result, WMIUser
  If InStr(UserName, "\") > 0 Then
    DomainName = Mid(UserName, 1, InStr(UserName, "\") - 1)
    UserName = Mid(UserName, InStr(UserName, "\") + 1)
    DomainName = CreateObject("WScript.Network").UserDomain
  End If
  On Error Resume Next
  Set WMIUser = GetObject("winmgmts:{impersonationlevel=impersonate}!" _
    & "/root/cimv2:Win32_UserAccount.Domain='" & DomainName & "'" _
    & ",Name='" & UserName & "'")
  If Err = 0 Then Result = WMIUser.SID Else Result = ""
  On Error GoTo 0
  GetSIDFromUser = Result
End Function


This function will query the domain for the user and return the unique SID for the user.  Now you can put two and two together and access HKEY_Users\SID to get the HKCU hive on a machine.


I have some functions for accessing the registry remotely.  Each of them rely on these two functions for support when accessing HKCU.  I will posting up new articles with those functions soon.  In the meantime, if you have any questions on these functions or suggestions for new ones, please let me know by clicking the email icon below.


Friday, August 8, 2008

Function: getLDAPPath()

You're wondering, "Where the hell has that guy been?".  More than likely you're not actually thinking that but my blog has. So while I don't have anything super great to post, I dug through my archive of functions and found one to post. 

This neat little function will let you pass some info and return to you the full LDAP path to the object.  Some of you veteran scripters out there might ask, "Why not use the built in Wscript call?".  Well you could, but that only returns the user account.  This nifty function will let you query AD to get the LDAP path for computer objects as well.

So lets get to it.  The function has for input parameters. 

Object name = name of the object in AD.  Be sure to use the samAccount name.

Object type = type of object.  User or Computer

Server = The domain server to query

Domain = The domain to query in.  ex. na.fabrikam.com


Here's the function first for you to absorb (ahem, copy) and then we'll discuss:

Function getLDAPPath(sstrCN, sstrType, sstrServer, sstrDomain)
'Input: sstrCN = Name of the object to look For
'Input: sstrType = Type of object: "User" or "Computer"
'Input: sstrServer = DC to query On
'Input: sstrDomain = Domain to query: Example: "na.fabrikam.com"
'Output: String = LDAP path to object
    Set sCon = CreateObject("ADODB.Connection")
    Set sCom = CreateObject("ADODB.Command")
    sCon.Provider = "ADsDSOObject"
    sCon.Open "Active Directory Provider"
    Set sCom.ActiveConnection = sCon
    sstrDomain = "DC=" & Replace(sstrDomain,".",",DC=")
    sstrType = UCase(sstrType)
    Select Case sstrType
        Case "USER"
            sstrQueryField = "samAccountName"
            sstrQueryObject = "USER"
        Case "COMPUTER"
            sstrQueryField = "Name"
            sstrQueryObject = "COMPUTER"        
    End Select
    sCom.CommandText = "select adspath from 'LDAP://" & sstrServer &  _
    "/" & sstrDomain & "' WHERE " & sstrQueryField & _
    " = '" & sstrCN & "' and objectclass = '" & sstrQueryObject & "'"
    Set sRs = sCom.Execute
    Do While Not sRs.EOF
        sPath = sRs("adspath")
    getLDAPPath = sPath
    Set sCon = Nothing
    Set sCom = Nothing
End Function
Note: If you are copying and pasting, be sure to restore the proper line breaks... Also, I don't typically like using an underscore to break the lines since it's hard to read.  But it helps for copying from this blog.

There.  To break it down, the function first sets up the necessary connections. Next, it reformats the domain to an AD query friendly format.  Then we build a search command and execute.  The results come back as a recordset. We iterate through the recordset to get the "adspath" field.  Set the path to the function name and close out the connections to complete this bad boy.

It's an easy function and doesn't do a whole lot, but if you need to find the LDAP path for a machine name, then this will work for you. 

Also note that if you have multiple domains, you may need to enter in the server name as a fully qualified domain name (FQDN) if you don't have the DNS suffix for that domain setup on the computer. 

Maybe I should add domain variable to the end of the server?  Maybe.  But that could cause problems if a server handled multiple domains.  (If that's even possible, I don't know).


That's it for now.  I'll try to dig up more stuff or post new and exciting subs/functions as I get them (and not slack off again).

If you have any questions or suggestions, feel free to leave a comment and I'll be happy to reply!


-Corey Thomas

Your humble vbscripting blogger

Monday, June 16, 2008

Twitter? No, it's not porn.

You can now follow me at Twitter.com! Visit my page at http://www.twitter.com/coreytech

If I can find a good app for Twitter on my blackberry, I'll update more often. (Update: found a decent app... so follow me on Twitter!) I have a Facebook page as well. Feel free to add me there at: http://www.facebook.com/profile.php?ID=1022118181

See ya online!


Start a process remotely

I'm back with another script fresh from the oven.  This time we're going to create a function that will connect to a machine and create a process.  Before you say "why would I want to create a process, we've got too many processes around here", think of it like this:  Say you have a computer somewhere that you want to run a command like ipconfig /flushdns on.  Well, ipconfig doesn't have a way of doing it remotely.  Now you are stuck with writing out some code to do it via WMI.   While that's great, sometimes it's just easier to type in "ipconfig /flushdns" and you wish you could just tell the dang computer to run it.

Well you can and it turns out, it's pretty simple too.   First, we'll connect to the computer.  Next, we'll create a new process (in this case, ipconfig.exe).  Then we'll pass some switches to it ("/flushdns").  Lastly, we'll run the command and capture the return code to try and determine if the process started successfully.

Before you plop this code into your script, here's a tip.  The function returns a boolean value based on the return code from the OS.  I did my best job trying to interpret the codes into a True/False return code but you may need to modify it for your script.  Also, the function stores the return type (string) so you can get a nice status of the return code.  To use this, declare startExeStatus globally (in the main part of your script).  Then call the function.  It will set the variable to the status.

So here's the code:

Function startExe(sstrComputer,sstrEXE)
'Starts a process on a machine
'Input: sstrComputer = machine name (use "." for local)
'Input: sstrExe = Exe or command to execute (can pass full command line)
'Output: boolean
'Optional: Declare startExeStatus as a global variable to get status text
    startExe = False 
    Set sobjWMIService = GetObject("winmgmts:\\" & sstrComputer & "\root\cimv2:Win32_Process")
    sintReturn = sobjWMIService.Create(sstrEXE, null, null, sintProcessID)
    Select Case sintReturn
        Case 0 'Successful Completion
            startExe = True
            startExeStatus = "Successful Completion"
        Case 2 'Access Denied
            startExe = False
            startExeStatus = "Access Denied"
        Case 3 'Insufficient Privilege
            startExe = False
            startExeStatus = "Insufficient Privilege"
        Case 8 'Unknown Failure
            startExe = False
            startExeStatus = "Unknown Failure"
        Case 9 'Path not found
            startExe = False
            startExeStatus = "Path not found"
        Case 21 'Invalid Parameter
            startExe = False
            startExeStatus = "Invalid Parameter"
        Case Else 
            startExe = False
            startExeStatus = "Error code " & sintReturn & " not found"
    End Select 
End Function 


Here's the background on why I created this function.  We have a need to create a scheduled task on a lot (and I mean a LOT) of machines.  We were using schtasks.exe and for the most part it worked fine.  However, one day we found several hundreds of machines that weren't scheduling the tasks.  After some investigation, the machines were returning back as not being NT or higher even though they were on XP SP2.  The need to create the task was great so we worked around it.

I found that creating the task locally worked like a charm.  So the function was born.  It's been used for all kinds of stuff since like ipconfig /flushdns.  Use wisely.  :)

As always, these scripts are free to use and at your own caution.  So don't hold me responsible for any crashes or other bad things you do with it.


Wednesday, May 21, 2008

RemoteService Function

Slacker.  Go ahead, say it. I know your thinking it.  Work has been busy but that doesn't mean I'm slacking off on coding.  Oh no, I'm hard at work writing new functions and subs.  I finished one recently when I needed to stop and start a service on a remote machine.  And as usual, I couldn't just make a sub that only stopped and started a service.  I had to add in other abilities like Pause, Resume, and Restart.  Oh yeah, now we're talking.

But that's not all.  I started to think, what else would I want to do with a service?  How about changing it's startup type to Automatic, Manual, or Disabled?  That's something we network administrators need to do from time to time.  So I added that in too.

I wrapped all the code up in a nifty little sub (sorry, no shiny paper and bow).  To use it, simply pass the computer name, service name, and the action you want to complete and presto!  The service will be changed.

chrip chrip chrip..  What's that?  The little birdy on my shoulder said "what about errors?".  Good point.  What if we want to capture the return status of the action?  For example, what if I try to start a service that is already started?  It turns out that we can capture the return code.  The code is a user un-friendly integer so run it through a select case and set a boolean variable as true/false as well as setting some status text. So now that we have that, how do we pass two variables back?  You can't pass it back in a single function.  So here's what I did.

To get the return code and status, declare these two variables in the main part of the script: blnServiceStatus and strServiceStatus.

When the action on the service completes, the return code is filtered and these two variables are set.  All you need to do is declare them globally and now you've got the status!  One special note on the blnServiceStatus results, I tried my best to interpret the return code as a true or false as representative of the success of the action.  You may need to tweak these for your desired results.

All this talk and no action.  Let's see some code!

Sub remoteService(sstrComputer,sstrService,sstrAction)
'This accepts a computer name, service name, and action to complete on the service.
'Input: sstrComputer = Machine to perform action on.  Use "." for local
'Input: sstrService = Name of the service
'Input: sstrAction = See options below
'        "Start" = Start the service
'        "Stop" = Stop the service
'        "Restart" = Restart the service
'        "Pause" = Pause the service
'        "Resume" = Resume the service
'        "Automatic" = Set the startup type to Automatic
'        "Manual" = Set the startup type to Manual
'        "Disabled" = Set the startup type to Disabled
'To get the status codes:
'1.  Declare strServiceStatus and blnServiceStatus globally (Main part of the script)
'2.  Call this sub as usual.
'3.  blnServiceStatus will be set to True if successfull.  
'4.  strServiceStatus will be set to the status of the action
On Error Resume Next 
    Set sobjWMIService = GetObject("winmgmts:" _
        & "{impersonationLevel=impersonate}!\\" & sstrComputer & "\root\cimv2")
    Set scolServiceList = sobjWMIService.ExecQuery("Select * from Win32_Service where Name='" & sstrService &"'")
    'Set colServiceList = objWMIService.ExecQuery("Select * from Win32_Service")
    For Each sobjService in scolServiceList
        'WScript.Echo sobjService.Name
        sstrAction = UCase(sstrAction)
        Select Case sstrAction
            Case "START"
                serrReturn = sobjService.StartService()
            Case "STOP"
                serrReturn = sobjService.StopService()
            Case "RESTART"
                serrReturn = sobjService.StopService()
                serrReturn = sobjService.StartService()
            Case "PAUSE"
                serrReturn = sobjService.PauseService()
            Case "RESUME"
                serrReturn = sobjService.ResumeService()
            Case "AUTOMATIC"
                serrReturn = sobjService.ChangeStartMode("Automatic")
            Case "MANUAL"
                serrReturn = sobjService.ChangeStartMode("Manual")
            Case "DISABLED"
                serrReturn = sobjService.ChangeStartMode("Disabled")
        End Select
        'WScript.Echo serrReturn
        Select Case serrReturn
            Case 0  'Success
                blnServiceStatus = True
                strServiceStatus = "Success"
            Case 1    'Not Supported
                blnServiceStatus = False
                strServiceStatus = "Not Supported"
            Case 2    'Access Denied
                blnServiceStatus = False
                strServiceStatus = "Access Denied"
            Case 3    'Dependent Services Running
                blnServiceStatus = True
                strServiceStatus = "Dependent Services Running"
            Case 4    'Invalid Service Control
                blnServiceStatus = False
                strServiceStatus = "Invalid Service Control"
            Case 5    'Service Cannot Accept Control
                blnServiceStatus = False
                strServiceStatus = "Service Cannot Accept Control"
            Case 6    'Service Not Active
                blnServiceStatus = False
                strServiceStatus = "Service Not Active"
            Case 7    'Service Request Timeout
                blnServiceStatus = False
                strServiceStatus = "Service Request Timeout"
            Case 8    'Unknown Failure
                blnServiceStatus = False
                strServiceStatus = "Unknown Failure"
            Case 9    'Path Not Found
                blnServiceStatus = False
                strServiceStatus = "Path Not Found"
            Case 10    'Service Already Running
                blnServiceStatus = True
                strServiceStatus = "Service Already Running"
            Case 11    'Service Database Locked
                blnServiceStatus = False
                strServiceStatus = "Service Database Locked"
            Case 12    'Service Dependency Deleted
                blnServiceStatus = True
                strServiceStatus = "Service Dependency Deleted"
            Case 13    'Service Dependency Failure
                blnServiceStatus = False
                strServiceStatus = "Service Dependency Failure"
            Case 14    'Service Disabled
                blnServiceStatus = True
                strServiceStatus = "Service Disabled"
            Case 15    'Service Logon Failure
                blnServiceStatus = False
                strServiceStatus = "Service Logon Failure"
            Case 16    'Service Marked For Deletion
                blnServiceStatus = True
                strServiceStatus = "Service Marked For Deletion"
            Case 17    'Service No Thread
                blnServiceStatus = False
                strServiceStatus = "Service No Thread"
            Case 18    'Status Circular Dependency
                blnServiceStatus = False
                strServiceStatus = "Circular Dependency"
            Case 19    'Status Duplicate Name
                blnServiceStatus = False
                strServiceStatus = "Duplicate Name"
            Case 20    'Status Invalid Name
                blnServiceStatus = False
                strServiceStatus = "Invalid Name"
            Case 21    'Status Invalid Parameter
                blnServiceStatus = False
                strServiceStatus = "Invalid Paramenter"
            Case 22    'Status Invalid Service Account
                blnServiceStatus = False
                strServiceStatus = "Invalid Service Account"
            Case 23    'Status Service Exists
                blnServiceStatus = True
                strServiceStatus = "Service Exists"
            Case 24    'Service Already Paused  
                blnServiceStatus = True
                strServiceStatus = "Already Paused"
        End Select
End Sub 

And there you have it.  A nice and robust sub for handling services.  Remember, you can use this in a local script too.  Just use "." as the computer name.  :)


Happy coding!